文档章节

Clair介绍和源码分析

WaltonWang
 WaltonWang
发布于 2017/01/19 12:41
字数 1669
阅读 696
收藏 2

更多关于kubernetes的深入文章,请看我csdn或者oschina的博客主页。

本文主要描述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.1
    restart: always
    ports:
      - 5432:5432
    volumes:
      - /docker/postgresql/data:/var/lib/postgresql/data
  clair:
    image: libary/clair:0.2
    depends_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 specified
	if *flagCPUProfilePath != "" {
		defer stopCPUProfiling(startCPUProfiling(*flagCPUProfilePath))
	}
	// 启动clair
	clair.Boot(config)
}

/clair.go

func Boot(config *config.Config) {
	...
	// 连接后端DB,默认配置为pgsql
	db, err := database.Open(config.Database)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 启动Notifier服务,clair实现了webhook notifier
	st.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 updater
	st.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 middleware
		NoSignalHandling: true, // We want to use our own Stopper
		Server: &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()

	// Layers
	router.POST("/layers", context.HTTPHandler(postLayer, ctx))
	router.GET("/layers/:layerName", context.HTTPHandler(getLayer, ctx))
	router.DELETE("/layers/:layerName", context.HTTPHandler(deleteLayer, ctx))

	// Namespaces
	router.GET("/namespaces", context.HTTPHandler(getNamespaces, ctx))

	// Vulnerabilities
	router.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))

	// Fixes
	router.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))

	// Notifications
	router.GET("/notifications/:notificationName", context.HTTPHandler(getNotification, ctx))
	router.DELETE("/notifications/:notificationName", context.HTTPHandler(deleteNotification, ctx))

	// Metrics
	router.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. Current
      engine 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 Data
	data, 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交给读者自行分析。

更多关于kubernetes的深入文章,请看我csdn或者oschina的博客主页。

© 著作权归作者所有

WaltonWang
粉丝 226
博文 106
码字总数 226882
作品 0
深圳
程序员
私信 提问
Clair: 开源容器漏洞分析工具

Clair: 开源容器漏洞分析工具 Clair是一个用于Docker容器安全监控的工具,它是一个API驱动的分析引擎,能针对Docker image的文件系统逐层地对已知的安全漏洞进行审查。 Docker 安全产品Quay的...

yeit
2015/11/22
9
1
Harbor镜像仓库漏洞扫描功能

镜像漏洞扫描功能简介 开源企业级镜像仓库 Harbor v1.2 新增了镜像漏洞扫描的功能,可以帮助用户发现容器镜像中的安全漏洞,及时采取防范措施。 容器镜像本质上是一系列静态文件的集合,也是...

党志强
2018/07/04
0
0
Harbor容器镜像安全漏洞扫描详述和视频

题图摄于黄花水长城 阅读导航 一、Harbor v1.2 镜像仓库发布镜像扫描功能 二、镜像扫描功能原理 三、镜像扫描演示视频 四、Harbor征文活动送T-Shirt等纪念品,含平板电脑、Kindle等大奖 五、...

q48s71bczbeylou9t0n
2017/10/08
0
0
容器漏洞分析服务--Clair

Clair 是一个容器漏洞分析服务。它提供一个能威胁容器漏洞的列表,并且在有新的容器漏洞发布出来后会发送通知给用户。

孔小菜
2015/11/16
952
0
Redis 专栏(使用介绍、源码分析、常见问题...)

来源http://blog.csdn.net/yangbodong22011/article/details/78529448 https://github.com/hurley25 https://github.com/hurley25/ANet ANet 基于Redis网络模型的简易网络库,网络模块代码取......

libaineu2004
2017/12/16
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Linux 运行shell文件,出现 $'\r': command not found

运行编写的shell脚本时,出现了 $'\\r': command not found 这样的错误提示。 报错的原因是我们在windows系统操作时,编辑器里的换行符是\r\n ,而Linux上为\n,两个系统之间有差异导致的。 ...

芥末无敌
今天
7
0
Java数据结构(上)

枚举(Enumeration) 位集合(BitSet) 向量(Vector) 栈(Stack) 1.Enumeration(枚举) boolean hasMoreElements( ):测试是否有更多的元素 Object nextElement( ):如果此枚举对象至少还...

Firefly-
昨天
11
0
vue 跨层组件通讯 provide inject

https://cn.vuejs.org/v2/api/#provide-inject 类型: provide:Object | () => Object inject:Array<string> | { [key: string]: string | Symbol | Object } 详细: provide 和 inject 主......

阿豪boy
昨天
7
0
黑马程序员面试宝典(Java)Beta6.0免费下载

场景 JavaSE基础 面向对象特征以及理解 访问权限修饰符区别 理解clone对象 JavaSE语法 java有没有goto语句 &和&&的区别 如何跳出当前的多重嵌套循环? 是否可以继承String? 重载与重写的区别...

badaoliumang
昨天
9
0
监控linux系统状态

查看系统负载: w/uptime 最后面三个数字表示1分钟,5分钟,15分钟平均有多少个进程占用CPU 占用CPU的进程可以是Running,也可以是Waiting 某一时刻1颗CPU只能有一个进程在使用其资源 #查看c...

asnfuy
昨天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部