Clair源码分析

本文主要描述Clair架构、编译、部署、源码分析等内容。

Clair架构


Clair主要包括以下模块:

  • 获取器(Fetcher)- 从公共源收集漏洞数据

  • 检测器(Detector)- 指出容器镜像中包含的Feature

  • 容器格式器(Image Format)- Clair已知的容器镜像格式,包括Docker,ACI

  • 通知钩子(Notification Hook)- 当新的漏洞被发现时或者已经存在的漏洞发生改变时通知用户/机器

  • 数据库(Databases)- 存储容器中各个层以及漏洞

  • Worker - 每个Post Layer都会启动一个worker进行Layer Detect

Clair源码编译和使用

  • 启动一个pgsql容器作为Clair的Backend DB

    docker run -p 5432:5432 -e POSTGRES_PASSWORD=passw0rd postgres:latest

  • 从源码编译clair

    go get github.com/coreos/clair

    go install github.com/coreos/clair/cmd/clair

  • 配置Clair的Backend DB (vim /etc/clair/config.yaml)

  • 启动clair

    clair -config config.yaml

  • 安装并启动本地镜像分析工具: analyze-local-images

    go get -u github.com/coreos/clair/contrib/analyze-local-images

  • 执行镜像扫描

    analyze-local-images -endpoint “http://10.199.244.27:6060” -my-address “10.199.244.27” vipdocker-f9nub.vclound.com/centos:6.6

-endpoint配置clair部署的主机IP

docker-compose部署Clair

通过docker-compose部署clair的yaml文件内容如下:

version: '2'
services:postgresql:image: /libary/postgres:0.1restart: alwaysports:- 5432:5432
    volumes:- /docker/postgresql/data:/var/lib/postgresql/data
  clair:image: libary/clair:0.2depends_on:- postgresql
    ports:- 6060:6060
      - 6061:6061
    environment:- POSTGRESQL_HOST=postgresql

Clair源码分析

Clair内部各个模块之间的关系如下:

以Rest API请求为入口,相关模块的流程大致如下:

下面将具体进行入口和Post Layer接口的源码进行分析。

main方法

/cmd/clair/main.go

func main() {...// 加载配置文件config, err := config.Load(*flagConfigPath)...// Enable CPU Profiling if specifiedif *flagCPUProfilePath != "" {defer stopCPUProfiling(startCPUProfiling(*flagCPUProfilePath))}// 启动clairclair.Boot(config)
}

/clair.go

func Boot(config *config.Config) {...// 连接后端DB,默认配置为pgsqldb, err := database.Open(config.Database)if err != nil {log.Fatal(err)}defer db.Close()// 启动Notifier服务,clair实现了webhook notifierst.Begin()go notifier.Run(config.Notifier, db, st)// 启动clair的Rest API 服务st.Begin()go api.Run(config.API, &context.RouteContext{db, config.API}, st)// 启动clair的健康检查端口st.Begin()go api.RunHealth(config.API, &context.RouteContext{db, config.API}, st)// 启动定期的Updater服务,clair实现了fetcher updaterst.Begin()go updater.Run(config.Updater, db, st)...
}

从上面的Boot方法可见,clair在启动时启动了其主体服务有:

  • 连接配置的Backend DB
  • 启动Notifier服务(配置webhook endpoints)
  • 启动Rest API服务监听API请求
  • 启动健康检查端口监听,方便用户进行clair进程的监控
  • 启动Fetcher,定期从公共配置源(Debian, Ubuntu, Redhat)中获取Features并更新到DB。

细心的你,可能发现,怎么没有启动Worker ?Worker其实只是Post Layer API的后端封装处理封装而已。下面就以Post Layer API请求为例,走读一下代码。

Post Layer API Workflow

从上面的main方法分析可知,Boot方法会调用api.Run启动服务:

/api/api.go


func Run(config *config.APIConfig, ctx *context.RouteContext, st *utils.Stopper) {...srv := &graceful.Server{Timeout:          0,    // Already handled by our TimeOut middlewareNoSignalHandling: true, // We want to use our own StopperServer: &http.Server{Addr:      ":" + strconv.Itoa(config.Port),TLSConfig: tlsConfig,Handler:   http.TimeoutHandler(newAPIHandler(ctx), config.Timeout, timeoutResponse),},}listenAndServeWithStopper(srv, st, config.CertFile, config.KeyFile)...
}

api.Run中调用api.newAPIHandler生成了一个API Handler来处理所有的API请求。
/api/router.go

func newAPIHandler(ctx *context.RouteContext) http.Handler {router := make(router)router["/v1"] = v1.NewRouter(ctx)return router
}

所有的Router对应Handler配置在:
/api/v1/router.go

// NewRouter creates an HTTP router for version 1 of the Clair API.
func NewRouter(ctx *context.RouteContext) *httprouter.Router {router := httprouter.New()// Layersrouter.POST("/layers", context.HTTPHandler(postLayer, ctx))router.GET("/layers/:layerName", context.HTTPHandler(getLayer, ctx))router.DELETE("/layers/:layerName", context.HTTPHandler(deleteLayer, ctx))// Namespacesrouter.GET("/namespaces", context.HTTPHandler(getNamespaces, ctx))// Vulnerabilitiesrouter.GET("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(getVulnerabilities, ctx))router.POST("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(postVulnerability, ctx))router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(getVulnerability, ctx))router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(putVulnerability, ctx))router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(deleteVulnerability, ctx))// Fixesrouter.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes", context.HTTPHandler(getFixes, ctx))router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(putFix, ctx))router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(deleteFix, ctx))// Notificationsrouter.GET("/notifications/:notificationName", context.HTTPHandler(getNotification, ctx))router.DELETE("/notifications/:notificationName", context.HTTPHandler(deleteNotification, ctx))// Metricsrouter.GET("/metrics", context.HTTPHandler(getMetrics, ctx))return router
}

可见,Post Layer API的Handler为:
/api/v1/routes.go


func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) {...err = worker.Process(ctx.Store, request.Layer.Format, request.Layer.Name, request.Layer.ParentName, request.Layer.Path, request.Layer.Headers)...
}

流程交给了worker.Process进行处理:
/worker/worker.go

func Process(datastore database.Datastore, imageFormat, name, parentName, path string, headers map[string]string) error {...// Check to see if the layer is already in the database.layer, err := datastore.FindLayer(name, false, false)if err != nil && err != cerrors.ErrNotFound {return err}if err == cerrors.ErrNotFound {// New layer case.layer = database.Layer{Name: name, EngineVersion: Version}// Retrieve the parent if it has one.// We need to get it with its Features in order to diff them.if parentName != "" {parent, err := datastore.FindLayer(parentName, true, false)if err != nil && err != cerrors.ErrNotFound {return err}if err == cerrors.ErrNotFound {log.Warningf("layer %s: the parent layer (%s) is unknown. it must be processed first", name,parentName)return ErrParentUnknown}layer.Parent = &parent}} else {// The layer is already in the database, check if we need to update it.if layer.EngineVersion >= Version {log.Debugf(`layer %s: layer content has already been processed in the past with engine %d.Current engine is %d. skipping analysis`, name, layer.EngineVersion, Version)return nil}log.Debugf(`layer %s: layer content has been analyzed in the past with engine %d. Currentengine is %d. analyzing again`, name, layer.EngineVersion, Version)}// Analyze the content.layer.Namespace, layer.Features, err = detectContent(imageFormat, name, path, headers, layer.Parent)if err != nil {return err}return datastore.InsertLayer(layer)
}// detectContent downloads a layer's archive and extracts its Namespace and Features.
func detectContent(imageFormat, name, path string, headers map[string]string, parent *database.Layer) (namespace *database.Namespace, featureVersions []database.FeatureVersion, err error) {// Detect Datadata, err := detectors.DetectData(imageFormat, path, headers, append(detectors.GetRequiredFilesFeatures(), detectors.GetRequiredFilesNamespace()...), maxFileSize)...// Detect namespace.namespace = detectNamespace(name, data, parent)// Detect features.featureVersions, err = detectFeatureVersions(name, data, namespace, parent)...
}

POST Layer API首先去DB中查询该layer的记录,如果存在并且该layer的Engine Version比DB中记录的大于等于3(目前最大的worker version),则表明已经detect过这个layer,则结束返回。否则就调用detector对data进行map数据封装,然后再根据这个map数据对features, namesapces分别进行扫描检测。

其中,对data的检测是关键的部分:
/worker/detectors/data.go

func DetectData(format, path string, headers map[string]string, toExtract []string, maxFileSize int64) (data map[string][]byte, err error) {...if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {// Create a new HTTP request object.request, err := http.NewRequest("GET", path, nil)...layerReader = r.Body} else {layerReader, err = os.Open(path)...}defer layerReader.Close()for _, detector := range dataDetectors {if detector.Supported(path, format) {data, err = detector.Detect(layerReader, toExtract, maxFileSize)...}}...
}

detector检测data时,如果layer path是https/http开头的,则会调用GET请求将blog下载下来;否则就认为在本地,直接打开path定义的文件读取blob内容。
之后,根据该image的格式,调用对应的detector.Detect接口实现,完成对应的工作。目前支持aci和docker两种image格式。

/worker/detectors/data/docker/docker.go

func (detector *DockerDataDetector) Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (map[string][]byte, error) {return utils.SelectivelyExtractArchive(layerReader, "", toExtract, maxFileSize)
}

/worker/detectors/data/aci/aci.go

func (detector *ACIDataDetector) Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (map[string][]byte, error) {
    return utils.SelectivelyExtractArchive(layerReader, "rootfs/", toExtract, maxFileSize)
}

无论是docker还是aci格式的image,最终都是交给utils.SelectiveExtractArchive进行处理:

/utils/tar.go

// SelectivelyExtractArchive extracts the specified files and folders
// from targz data read from the given reader and store them in a map indexed by file paths
func SelectivelyExtractArchive(r io.Reader, prefix string, toExtract []string, maxFileSize int64) (map[string][]byte, error) {
    data := make(map[string][]byte)
    ...
    return data, nil
}

worker.detectNamespace和worker.detectFeatureVersions交给读者自行分析。

Clair介绍和源码分析相关推荐

  1. docker container DNS配置介绍和源码分析

    2019独角兽企业重金招聘Python工程师标准>>> 本文主要介绍了docker容器的DNS配置及其注意点,重点对docker 1.10发布的embedded DNS server ...

  2. SLIC超像素分割的算法介绍和源码分析

    前述 最近在看显著性检测,发现很多算法的基础是超像素分割,而正在看的Saliency Optimization from Robust Background Detection算法的预处理是SLIC算 ...

  3. Google Mock(Gmock)简单使用和源码分析——源码分析

    源码分析 通过<Google Mock(Gmock)简单使用和源码分析--简单使用>中的例子,我们发现被mock的相关方法在mock类中已经被重新实现了,否则它们也不会按照我们的期待的行为 ...

  4. android lottie字体json,从json文件到炫酷动画-Lottie实现思路和源码分析

    从json文件到炫酷动画-Lottie实现思路和源码分析,Lottie是最近Airbnb开源的动画项目,支持Android.iOS.ReactNaitve三个平台,本文分析主要Lottie把json文 ...

  5. Nacos高级特性Raft算法以及原理和源码分析

    Nacos高级特性Raft算法以及原理和源码分析 对比springcloud-config配置中心 springcloud-config工作原理 Nacos的工作原理图 springcloud-con ...

  6. 购物车及商品php代码_php实现商城购物车的思路和源码分析

    本文介绍一个php实现的购物车代码,功能实现完整,具有一定的参考价值 这里我们为你提供个简单的php购物车代码,从增加购物产品与发生购买了,在商城开发中,这个功能是少不了的 具体分析如下: 对购物车里 ...

  7. java.lang.ThreadLocal实现原理和源码分析

    java.lang.ThreadLocal实现原理和源码分析 1.ThreadLocal的原理:为每一个线程维护变量的副本.某个线程修改的只是自己的副本. 2.ThreadLocal是如何做到把变量变 ...

  8. 【原创】【专栏】《Linux设备驱动程序》--- LDD3源码目录结构和源码分析经典链接

    http://blog.csdn.net/geng823/article/details/37567557 [原创][专栏]<Linux设备驱动程序>--- LDD3源码目录结构和源码分析 ...

  9. java校验框架源码解析_Spring Boot原理剖析和源码分析

    Spring Boot原理剖析和源码分析 依赖管理 问题一:为什么导入dependency时不需要指定版本? spring-boot-starter-parent依赖 org.springframew ...

最新文章

  1. the application has stopped unexpected。please try again
  2. 运动检测(前景检测)之(二)混合高斯模型GMM
  3. 在听伏尔加纤夫曲 超酷
  4. java代码开发规范
  5. android c 对象为空,ndk-jni中C/C++接口函数修改函数参数jobject对象成员值(数组)的有关问题...
  6. mysql行级锁作用_Mysql事务及行级锁的理解
  7. 喜茶部分产品降价,企查查显示其最新估值达600亿元
  8. pandas基本操作函数
  9. android热敏打印机图片乱码,小票打印机常见故障及解决方法,小票打印机打印乱码怎么办...
  10. hcfax2e伺服驱动器说明书_ABB用户手册MicroFlexe150伺服驱动器.pdf
  11. bs 网站获取电子秤重量方案
  12. 软件的高可用性、可扩展性和高性能
  13. 【CTF】明御攻防实验室misc ningen
  14. 笔记本linux系统重装步骤(Centos7.0)
  15. 赵小楼《天道》《遥远的救世主》深度解析(6)为什么肖亚文说丁元英是魔是鬼都可以,就是不是人?
  16. Android 如何更换系统字体
  17. 设计模式(一):简介
  18. Mac系统之----教你怎么显示隐藏文件,或者关闭显示隐藏文件
  19. 最大熵阈值python_李航统计学习方法(六)----逻辑斯谛回归与最大熵模型
  20. 模电学习2. NPN型三极管电流电压计算实战

热门文章

  1. OK JUMPSTART三期项目En-Tan-Mo抢购结束,火爆程度超出预期
  2. 利用SNMP获取H3C交换机端口信息
  3. 有关于Mem函数的实用总结
  4. win10系统打印机服务器在哪个文件夹,win10的打印机驱动在哪个文件夹
  5. 小小项目2:多项式四目计算器
  6. LABVIEW通过串口VISA通讯松下FP系列PLC.读取写入寄存器,辅助继电器,实际项目应用
  7. mysql 5.7.23用户权限_MySQL权限的详细解答
  8. easywechat php例子,thinkphp6使用easywechat笔记!
  9. HATEOAS 约束
  10. QC缺陷管理操作-细说(转)