VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

VeinMind Tools 镜像&容器漏洞扫描器 – 问脉

目录导航

VeinMind Tools简介

veinmind中文名为问脉,寓意容器安全见筋脉,望闻问切治病害。 旨在成为云原生领域的一剂良方.

问脉是是由长亭科技自研,基于 veinmind-sdk (opens new window)打造的容器安全工具集,目前已支持镜像恶意文件、后门、敏感信息、弱口令等扫描功能.

功能特性

  • 支持以平行容器模式部署,开箱即用.
  • 支持与多种容器运行时无缝衔接
  • 支持多维度威胁风险检测

目前已支持的检测插件如下

插件功能
veinmind-malicious(opens new window)扫描镜像中的恶意文件
veinmind-weakpass(opens new window)扫描镜像中的弱口令
veinmind-sensitive(opens new window)扫描镜像中的敏感信息
veinmind-backdoor(opens new window)扫描镜像中的后门
veinmind-history(opens new window)扫描镜像中的异常历史命令
veinmind-asset(opens new window)扫描镜像中的资产信息
veinmind-asset(opens new window)扫描镜像中的 webshell
veinmind-escalate(opens new window)扫描镜像中的 webshell

更新日志

此次更新的 v2.0 版本,优化、增添了以下核心亮点功能

  • 🔥 veinmind-asset 升级为 veinmind-vuln,支持镜像/容器漏洞扫描
  • 🎯 新增 veinmind-escalate 逃逸检测插件,扫描容器/镜像中的逃逸风险
  • 🧶 IaC 支持扫描 kubernetes 集群
  • 🏅 插件报告输出优化:支持 cli/json/htm
  • 🍄 runner cmd 优化:协议化扫描对象
  • 💫 runner 支持插件参数自定义
  • 🥳 runner 支持扫描 tar 类型镜像
  • 📑 script.sh => makefile

🔨 工具列表

工具功能
veinmind-runner扫描工具运行宿主
veinmind-malicious扫描容器/镜像中的恶意文件
veinmind-weakpass扫描容器/镜像中的弱口令
veinmind-log4j2扫描容器/镜像中的log4j2漏洞
veinmind-sensitive扫描镜像中的敏感信息
veinmind-backdoor扫描镜像中的后门
veinmind-history扫描镜像中的异常历史命令
veinmind-vuln扫描容器/镜像中的资产信息和漏洞
veinmind-webshell扫描镜像中的 Webshell
veinmind-unsafe-mount扫描容器中的不安全挂载目录
veinmind-iac扫描IaC文件
veinmind-escalate扫描容器/镜像中的逃逸风险

PS: 目前所有工具均已支持平行容器的方式运行

下载地址

①GitHub:

②云中转网盘

yunzhongzhuan.com/#sharefile=yMnpRHte_127247
解压密码:www.ddosi.org

快速入门

基于平行容器 (推荐)

1. 确保机器上正确安装 docker

docker info

2. 安装 veinmind-runner (opens new window)镜像

docker pull veinmind/veinmind-runner:latest

3. 下载 veinmind-runner (opens new window)平行容器启动脚本

wget -q https://download.veinmind.tech/scripts/veinmind-runner-parallel-container-run.sh -O run.sh && chmod +x run.sh

4. 快速扫描本地镜像

./run.sh scan image

扫描镜像

使用问脉 runner 扫描本地镜像(容器运行时类型未指定的情况下默认会依次尝试docker,containerd)

./veinmind-runner scan image [docker/containerd]:reference

使用问脉 runner 扫描远程镜像,若远程仓库需要认证需使用 -c 参数指定 toml 格式的认证信息文件(暂不支持 dockerhub 的私有镜像扫描!!!)

./veinmind-runner scan image registry-image:reference

例如:

#扫描 docker.io 的 nginx 镜像 (所有 tag)
./veinmind-runner scan image registry-image:nginx   
#扫描 docker.io 的 bitnami/nginx 镜像 (所有 tag)
./veinmind-runner scan image registry-image:bitnami/nginx 
#扫描 registry.example.com 私有仓库下的 registry.example.com/library/ubuntu:latest 镜像
./veinmind-runner scan image -c auth.toml registry-image:registry.example.com/library/ubuntu:latest
#扫描 registry.example.com 私有仓库下的所有镜像
./veinmind-runner scan image -c auth.toml registry:registry.example.com 
#基于给定正则扫描 registry.example.com 私有仓库下的所有镜像
./veinmind-runner scan image -c auth.toml --filter "nginx$" registry:registry.example.com 

auth.toml 的格式如下, registry 代表仓库地址, username 代表用户名, password 代表密码

[[auths]]
	registry = "index.docker.io"
	username = "admin"
	password = "password"
[[auths]]
	registry = "registry.private.net"
	username = "admin"
	password = "password"

扫描容器

使用 veinmind-runner 扫描本地容器(容器运行时类型未指定的情况下默认会依次尝试docker,containerd)

./veinmind-runner scan container [dockerd:/containerd:]containerID/containerRef

容器运行时类型

  • dockerd
  • containerd

扫描IaC文件

使用 veinmind-runner 扫描本地IaC文件

./veinmind-runner scan iac host:path/to/iac-file
./veinmind-runner scan iac path/to/iac-file

使用 veinmind-runner 扫描远端 git 仓库的 IaC 文件

./veinmind-runner scan iac git:http://xxxxxx.git 
# auth
./veinmind-runner scan iac git:git@xxxxxx --sshkey=/your/ssh/key/path
./veinmind-runner scan iac git:http://{username}:[email protected]
# add proxy
./veinmind-runner scan iac git:http://xxxxxx.git --proxy=http://127.0.0.1:8080
./veinmind-runner scan iac git:http://xxxxxx.git --proxy=scoks5://127.0.0.1:8080
# disable tls
./veinmind-runner scan iac git:http://xxxxxx.git --insecure-skip=true

使用 veinmind-runner 扫描远端 kubernetes IaC 配置(需要手动指定kubeconfig file)

./veinmind-runner scan iac kubernetes:resource/name -n namespace --kubeconfig=/your/k8sConfig/path

漏洞扫描

功能特性

  • 扫描镜像/容器的OS信息
  • 扫描镜像/容器内系统安装的packages
  • 扫描镜像/容器内应用安装的libraries
  • 扫描镜像/容器是否存在已知cve (beta)
  • 支持JSON/CLI/HTML等多种报告格式输出

使用

基本命令

./veinmind-vuln scan image/container [image_name/image_id/container_name/container_id]

可选参数

参数名称含义是否必填
image/container选择扫描对象为镜像/容器
[image_name/image_id/container_name/container_id]扫描镜像/容器的名称/ID否(不填写时默认扫描全部)
-v是否显示详细信息否(默认为不展示详情)
-f [csv/json/stdout]指定输出模式: csv/json/控制台否(默认为stdout)
–type [os/python/jar/pip/npm…]指定资产类型: 系统os/python/jar/…否(默认全部)
–only-asset仅扫描资产信息(不扫描漏洞)否(默认不开启)

示例

  1. 扫描本地全部镜像
./veinmind-vuln scan image
  1. 扫描本地全部容器
./veinmind-vuln scan container
  1. 输出详细结果(包含漏洞详情)
./veinmind-vuln scan image [imagename/imageid] -v
  1. 输出指定类型的详细结果
./veinmind-vuln scan image [imagename/imageid] -v --type [os/python/jar/pip/npm.......]
  1. 指定输出格式
./veinmind-vuln scan image [imagename/imageid] -f [csv/json]

目前支持的输出格式:

  • html
  • json
  • cli(默认)
  1. 输出详细信息
./veinmind-vulnm scan image [imagename/imageid] -v
  1. 仅扫描资产信息
./veinmind-vuln scan image [imagename/imageid] --only-asset

恶意文件

功能特性

  • 快速扫描镜像中的恶意文件 (目前支持ClamAV以及VirusTotal)
  • 支持 docker/containerd 容器运行时
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64
  • linux/arm

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

确保机器上安装了docker以及docker-compose,并启动ClamAV

chmod +x veinmind-malicious && ./veinmind-malicious extract && cd scripts && docker-compose pull && docker-compose up -d

如果您使用的是VirusTotal,则需要在环境变量或scripts/.env文件中声明VT_API_KEY

export VT_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

安装方式二

基于平行容器的模式,获取 veinmind-malicious 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' -v `pwd`:/tool/data veinmind/veinmind-malicious scan

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh scan

使用

  1. 指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)
./veinmind-malicious scan [imagename/imageid]
  1. 扫描所有本地镜像
./veinmind-malicious scan
  1. 指定输出报告格式 (目前支持html/csv/json)
./veinmind-malicious scan -f [html/csv/json]

目前支持的输出格式:

  • html
  • json
  • cli(默认)
  1. 指定输出报告名称
./veinmind-malicious scan -n [reportname]
  1. 指定输出路径
./veinmind-malicious scan -o [outputpath]
  1. 指定容器运行时类型
./veinmind-malicious scan --containerd

容器运行时类型

  • dockerd
  • containerd
  1. 输出详细信息
./veinmind-malicious scan [imagename/imageid] -v

演示

1.扫描指定镜像名称 xmrig/xmrig

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

2.扫描指定镜像ID 

sha256:ba6acccedd2923aee4c2acc6a23780b14ed4b8a5fa4e14e252a23b846df9b6c1
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

3.指定输出格式以及输出名称

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

报告

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

敏感信息

功能特性

  • 快速扫描镜像中的敏感信息
  • 支持敏感信息扫描规则自定义
  • 支持containerd/dockerd镜像文件系统弱口令扫描
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64
  • linux/arm

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

然后安装veinmind-sensitive所需要的python依赖

pip install -r requirements.txt

安装方式二

基于平行容器的模式,获取 veinmind-sensitive 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind/veinmind-sensitive-go

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh

使用

1.指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)

./veinmind-sensitive scan [imagename/imageid]

2.扫描所有本地镜像

./veinmind-sensitive scan

3.指定镜像类型

./veinmind-sensitive scan --containerd

镜像类型

  • dockerd
  • containerd

4.指定输出类型

./veinmind-sensitive -f html scan

目前支持的输出格式:

  • html
  • json
  • cli(默认)

5.输出详细信息

./veinmind-sensitive scan [imagename/imageid] -v

规则字段说明

  • id: 规则标识符
  • description: 规则描述
  • match: 内容匹配规则,默认为正则
  • filepath: 路径匹配规则,默认为正则
  • env: 环境变量匹配规则,默认为正则且忽略大小写

演示

1.扫描指定镜像名称 sensitive

2.扫描所有镜像

弱口令

功能特性

  • 快速扫描镜像中的弱口令
  • 支持弱口令宏定义
  • 支持并发扫描弱口令
  • 支持自定义用户名以及字典
  • 支持containerd/dockerd容器运行时
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64
  • linux/arm

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

安装方式二

基于平行容器的模式,获取 veinmind-weakpass 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind/veinmind-weakpass scan

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh scan

使用

1.指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)

./veinmind-weakpass scan [imagename/imageid]

2.扫描所有本地镜像

./veinmind-weakpass scan

3.指定容器运行时类型

./veinmind-weakpass scan --containerd

容器运行时类型

  • dockerd
  • containerd

4.指定扫描用户名类型

./veinmind-weakpass scan -u username

5.指定自定义扫描字典

./veinmind-weakpass scan -d ./pass.dict

6.指定自定义扫描的服务

./veinmind-weakpass scan -a ssh,mysql,redis
  • 目前已经支持的服务
serverNameversion
sshall
mysql8.X
redisall
tomcatall

7.解压默认字典到本地磁盘

./veinmind-weakpass extract

8.指定输出类型

./veinmind-weakpass scan -f html

目前支持的输出格式:

  • html
  • json
  • cli(默认)

9.输出详细信息

./veinmind-weakpass scan -v

演示

1.扫描指定镜像名称 test 的所有服务

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

2.扫描指定镜像名称 test 的 ssh 服务

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

3.扫描所有镜像的 ssh 服务

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

异常历史命令

功能特性

  • 快速扫描镜像中的异常历史命令
  • 支持自定义历史命令检测规则
  • 支持containerd/dockerd两种容器运行时
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64
  • linux/arm

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

然后安装veinmind-history所需要的python依赖

pip install -r requirements.txt

安装方式二

基于平行容器的模式,获取 veinmind-history 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind/veinmind-history

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh

使用

1.指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)

python scan.py scan-images [imagename/imageid]

2.扫描所有本地镜像

python scan.py scan-images

3.指定容器运行时类型

python scan.py scan-images --containerd

镜像类型

  • dockerd
  • containerd

4.指定输出类型

python scan.py -f [outputtype] scan-images

目前支持的输出格式:

  • html
  • json
  • cli(默认)

5.输出详细信息

python scan.py -v scan-images

规则字段说明

演示

1.扫描指定镜像名称 history

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

2.扫描所有镜像

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

后门

功能特性

  • 快速扫描镜像中的后门
插件功能
crontab扫描定时任务中是否包含后门
bashrc扫描 bash 启动脚本是否包含后门
sshd扫描 sshd 软链接后门
service扫描恶意的系统服务
tcpwrapper扫描 tcpwrapper 后门
  • 支持以插件模式编写后门检测脚本
  • 支持containerd/dockerd镜像后门扫描
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64
  • linux/arm

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

然后安装veinmind-backdoor所需要的python依赖

pip install -r requirements.txt

安装方式二

基于平行容器的模式,获取 veinmind-backdoor 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind/veinmind-backdoor

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh

使用

1.指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)

python scan.py scan-images [imagename/imageid]

2.扫描所有本地镜像

python scan.py scan-images

3.指定容器运行时类型

python scan.py scan-images --containerd

容器运行时类型

  • dockerd
  • containerd

4.指定输出类型

python scan.py -f [formattype] scan-images

目前支持的输出格式:

  • html
  • json
  • cli(默认)

5.输出详细信息

python scan.py -v scan-images

6.指定输出路径

python scan.py --format json --output /tmp scan-images

演示

1.扫描指定镜像名称 service

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

2.扫描所有镜像

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

资产识别

功能特性

  • 扫描镜像的OS信息
  • 扫描镜像内系统安装的packages
  • 扫描镜像内应用安装的libraries
  • 支持JSON/CLI/HTML等多种报告格式输出

使用

1.指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)

./veinmind-asset scan [imagename/imageid]
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

2.扫描本地全部镜像

./veinmind-asset scan

3.输出详细结果

./veinmind-asset scan -v
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

4.输出指定类型的详细结果

./veinmind-asset scan -v --type [os/python/jar/pip/npm.......]
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

5.指定输出格式

./veinmind-asset scan -f html
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

目前支持的输出格式:

  • html
  • json
  • cli(默认)

容器逃逸

功能特性

  • 快速扫描容器中的逃逸风险
  • 支持扫描镜像
  • 支持 docker/containerd 容器运行时
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64
  • linux/arm

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

确保机器上安装了docker以及docker-compose,并启动ClamAV

chmod +x veinmind-escalate && ./veinmind-escalte extract && cd scripts && docker-compose pull && docker-compose up -d

安装方式二

基于平行容器的模式,获取 veinmind-escalate 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' -v `pwd`:/tool/data veinmind/veinmind-escalate scan

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh scan

使用

1.指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)

./veinmind-escalate scan image [imageID/imageName]

2.扫描所有本地镜像

./veinmind-escalate scan image

3.指定容器名称或容器ID并扫描

./veinmind-escalate scan container [containerID/containerName]

4.扫描所有本地容器

./veinmind-escalate scan container

5.指定输出格式

./veinmind-escalate scan image/conatainer reference -f html

目前支持的输出格式:

  • html
  • json
  • cli(默认)

6.显示详细信息

./veinmind-escalate scan image/conatainer reference -v

IaC文件扫描

功能特性

  • 支持 dockerfile/kubernetes IaC类型文件
  • 支持指定目录自动递归扫描
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

安装方式二

基于平行容器的模式,获取 veinmind-iac 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind-iac scan-iac

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh scan-iac

使用参数

  1. 指定扫描IaC文件
./veinmind-iac scan-iac IACFILE
  1. 指定扫描目录下可能存在的IaC文件类型
./veinmind-iac scan-iac PATH
  1. 也可也混合使用
./veinmind-iac scan-iac IACFILE PATH
  1. 指定扫描特定的IaC文件类型
./veinmind-iac scan-iac --iac-type dockerfile IACFILE/PATH
  1. 指定输出格式
./veinmind-iac scan-iac -f json IACFILE/PATH

目前支持的输出格式:

  • html
  • json
  • cli(默认)

6.显示详细信息

./veinmind-iac scan-iac IACFILE/PATH -v

挂载扫描

功能特性

  • 快速扫描容器中的 unsafe-mount
  • 支持containerd/dockerd容器运行时
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

安装方式二

基于平行容器的模式,获取 veinmind-unsafe-mount 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind/veinmind-unsafe-mount scan-container

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh scan

使用参数

  1. 扫描指定容器
./veinmind-unsafe-mount scan-container [containerID/containerName]
  1. 扫描所有本地容器
./veinmind-unsafe-mount scan-container
  1. 指定输出格式
./veinmind-unsafe-mount scan-container [containerID/containerName] -f html

目前支持的输出格式:

  • html
  • json
  • cli(默认)
  1. 显示详细信息
./veinmind-unsafe-mount scan-container [containerID/containerName]  -v

webshell扫描

功能特性

  • 快速扫描镜像中的 Webshell
  • 支持containerd/dockerd容器运行时
  • 支持JSON/CLI/HTML等多种报告格式输出

兼容性

  • linux/amd64
  • linux/386
  • linux/arm64

开始之前

安装方式一

请先安装libveinmind,安装方法可以参考官方文档(opens new window)

安装方式二

基于平行容器的模式,获取 veinmind-webshell 的镜像并启动

docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind/veinmind-webshell scan --token [关山token]

或者使用项目提供的脚本启动

chmod +x parallel-container-run.sh && ./parallel-container-run.sh scan --token [关山token]

使用

  1. 登录百川平台 (opens new window),激活关山 Webshell 检测产品
  2. 点击左下角组织配置创建 API Token (基础版每日限制检测 100 次, 高级版可联系问脉小助手/百川平台获取)
  3. 执行 veinmind-webshell 时填入创建的 token
./veinmind-webshell scan --token [关山token]
  1. 指定输出格式
./veinmind-webshell scan --token [关山token] -f html

目前支持的输出格式:

  • html
  • json
  • cli(默认)
  1. 显示详细信息
./veinmind-webshell scan --token [关山token] -v

log4j2扫描

功能特性

  • 支持 fat jar、jar 中包含 jar 等情况的检测
  • 支持JSON/CLI/HTML等多种报告格式输出

使用

1.指定镜像名称或镜像ID并扫描 (需要本地存在对应的镜像)

./veinmind-log4j2 scan-image [imagename/imageid]

2.扫描本地全部镜像

./veinmind-log4j2 scan-image 

3.扫描本地容器

./veinmind-log4j2 scan-container [containerId]

Jenkins


Veinmind Jenkins

veinmind-tools 集成 Jenkins

欢迎使用 Veinmind Jenkins 安全插件,使用该插件,您可以快速集成 veinmind-runner 扫描能力到您的CI中。

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

快速开始

Veinmind Jenkins 支持如下两种方式使用

✨ 方式1: 使用 Veinmind Scanner 插件 (推荐)

  • 支持自动扫描模式,无需修改Jenkinsfile文件或BuildStep,自动识别docker build的动作,触发扫描任务。
  • 支持手动模式,可以手动增加Build Step/Pipeline Step来手动触发扫描。
  • 简便安装,一次安装,永久使用。
  • 使用简单,无需记住复杂的参数,鼠标配置即可。
  • 支持阻断功能。
  • 提供数据统计和详情页面。
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

适配性

Veinmind Scanner 需要如下的条件。

  1. Jenkins 支持版本 > v2.332.4
  2. 对于自动扫描功能,目前支持的自动扫描的方法:
插件名称Job类型是否支持Auto
Linux ShellFreeStyle
Docker plugin(opens new window)FreeStyle
Docker build step(opens new window)FreeStyle
Pipeline Shell(sh)Pipeline
Docker Pipeline(opens new window)Pipeline

使用步骤

  1. 将插件安装至 Jenkins 并重新启动。
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

2.在 Manage Jenkins -> Configure System 勾选自动扫描选项, 配置好全局变量并保存。

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

3.立即构建一个存在镜像build的Job。

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

4.等待扫描结束,即可在侧边栏查看到扫描结果。

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

进阶使用

  1. 使用 veinmindScanner 来在 Jenkinsfile 内手动插入扫描步骤。
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉
veinmindScanner block: true, image: 'YOOUR_IMAGE_REF', scanConfig: [$class: 'RunnerConfig', agentVersion: 'latest', scanArgs: '-v /var/run/docker.sock:/var/run/docker.sock', workSpace: '']

如果不熟悉Pipeline语法,可以在 pipeline-syntax 流水线语法处,选择veinmindScanner,根据配置项生成对应的Pipeline语句。

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉
VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

参数设置

配置参数分为全局配置和job配置。

自动扫描默认使用的是全局配置,因此如果您想要使用自动扫描功能,请先配置Global Config。

对于全局配置和job配置都存在的参数,优先使用job配置的参数。

参数名称参数作用默认值
是否开启自动(全局)自动扫描开关false
日志等级(全局)在控制台输出的日志级别info
超时时间(全局)扫描异常的超时时间600
是否阻断当发现风险时是否阻断流程false
Agent使用扫描的探针veinmind-runner
版本号使用探针的版本号,需要能够访问github,若无法访问则默认使用latest版本latest
高级配置扫描参数的高级配置,默认无需修改-v /var/run/docker.sock:/var/run/docker.sock
工作区配置如果您的Jenkins是使用容器部署的,并且配置了jenkins工作目录的挂载 (如: -v /home/jenkins_home:/var/jenkins_home) 请填写您挂在的实际路径。(在上述例子中,此处应该填写/home/jenkins_home)“”

✈️ 方式2: 使用 Veinmind Jenkins Lib (不推荐)

适配性

注意:所有的使用方式都是默认Jenkins安装了以下插件

  • Docker plugin
  • Pipeline: Groovy Libraries

使用步骤

1. 通过Pipeline Libraries引入配置

在 Manage Jenkins » Configure System » Global Pipeline Libraries 添加

ttps://github.com/chaitin/veinmind-jenkins

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

随后即可在Jenkinsfile内使用:

// import library
@Library('veinmind-runner') _

pipeline {
  agent any

  stages {
    stage('build') {
        steps {
            script {
                sh 'docker build -t YOUR_IMAGE:YOUR_TAG  .'
            }
        }
    }

    // add scan
    stage('scan') {
        steps {
            script {
                // easy mod
                veinmindRunner.scan("YOUR_IMAGE:YOUR_TAG")
                
                // set exit-code
                veinmindRunner.scan("YOUR_IMAGE:YOUR_TAG", 1)
                
                // set output
                veinmindRunner.scan("YOUR_IMAGE:YOUR_TAG", outPut="report.json", exitCode=0)
                
                // set all config params
                veinmindRunner.scan("YOUR_IMAGE:YOUR_TAG", "scan-host", "report.json", 0)
            }
        }
    }
  }
}

参数设置

参数名称参数作用默认值
imageRef镜像 Reference
scanAction扫描功能类型scan-host
outPut报告输出名称report.json
exitCode当发现安全问题时的程序退出码, 非零时阻断Pipeline0

Github Action

Veinmind Action

基于 veinmind-tools 实现的 Github Action

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

快速开始

扫描在 Action 过程中构建的镜像

on: [push]
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build image from your dockerfile
        run: docker build -t my-app:${{ github.sha }} .
      - name: Run veinmind-tools to scan image
        uses: chaitin/veinmind-action@main
        with:
          scan-action: scan-host
          image-ref: 'my-app:${{ github.sha }}'

🏳️‍🌈 参数设置

参数名称参数作用默认值
scan-action扫描功能类型scan-host
image-ref镜像 Reference
exit-code当发现安全问题时的程序退出码0
output报告输出名称report.json

Gitlab


Veinmind GitLab CI

veinmind-tools 集成进 GitLab-CI

VeinMind Tools 镜像&容器漏洞扫描器 - 问脉

快速开始

扫描在 Job 过程中构建的镜像 1. 通过远程配置引入

stages:
  - build

# import scan runner
include:
  - remote: https://download.veinmind.tech/scripts/veinmind-runner-gitlab-ci.yml
    
YOUR_JOB:
  stage: build
  image: YOUR_BUILD_IMAGE:latest
  # add default config
  extends: .scan-config
  # add your config
  variables:
    IMAGE_REF: YOUR_APP:APP_TAG
  script:
    - docker build -t YOUR_APP:APP_TAG .
    # add scan script
    - !reference [.scan-script, script]

2. 通过构建 veinmind/veinmind-gitlab-CI 仓库方式引入

此方式需要首先clone该仓库到您的的gitlab内。

stages:
  - build

# import scan runner
include:
  - project: veinmind-gitlab-CI # absolute path
    file: runner.yml


YOUR_JOB:
  stage: build
  image: YOUR_BUILD_IMAGE:latest
  # add default config
  extends: .scan-config
  # add your config
  variables:
    IMAGE_REF: YOUR_APP:APP_TAG
  script:
    - docker build -t YOUR_APP:APP_TAG .
    # add scan script
    - !reference [.scan-script, script]

3. 直接修改.gitlab-ci.yml文件

stages:
  - build

YOUR_JOB:
  stage: build
  image: YOUR_BUILD_IMAGE:latest
  # add your config
  variables:
    SCAN_ACTION: scan-host
    IMAGE_REF: YOUR_APP:APP_TAG
    OUT_PUT: report.json
    EXIT_CODE: 0
  script:
    - docker build -t YOUR_APP:APP_TAG .
    # add scan script
    - docker run --rm --mount 'type=bind,source=/,target=/host,readonly' -v /var/run/docker.sock:/var/run/docker.sock -v `pwd`:/tool/resource veinmind/veinmind-runner $SCAN_ACTION $IMAGE_REF -o $OUT_PUT -e $EXIT_CODE

🏳️‍🌈 参数设置

参数名称参数作用默认值
SCAN_ACTION扫描功能类型scan-host
IMAGE_REF镜像 Reference
EXIT_CODE当发现安全问题时的程序退出码, 非零时阻断Pipeline0
OUT_PUT报告输出名称report.json

平行容器

平行容器是指将云原生安全(如容器安全、镜像安全等)应用在其目标云原生平台进行容器化部署的方案。将安全应用进行容器化部署,可以充分利用云原生平台提供的集群化部署、容器编排、高可用、自动恢复等特性,提升安全产品的鲁棒性,降低用户的使用难度和运维成本。

问脉 SDK 在设计时充分考虑到了平行容器的场景,并且提供了能覆盖绝大多数使用场景的基础镜像。在绝大多数情况下,问脉 SDK 及其基础镜像对宿主机和平行容器的运行环境差异进行了统一化处理,因此问脉 SDK 的用户编写应用时无需感知当前是否处于平行容器内。

与之相对,大部分云原生平台运行容器时都需要对容器的挂载卷、命名空间和 Capabilities 等参数进行详细配置,并通过 docker 命令行、docker-compose.yml 文件和 Kubernetes Pod 的 yaml 文件等告知云原生平台如何创建和配置容器。而基于问脉 SDK 的基础镜像创建平行容器时,若未对平行容器的运行环境进行正确配置,则会导致平行容器内的问脉 SDK 无法正常工作,从而影响上层应用的运行。

以 python 为例,基于问脉 SDK 的基础镜像创建平行容器并使用问脉 SDK 的示例如下:

$ docker run --rm -it --mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave' veinmind/python3:1.0.2-stretch python3
Python 3.5.10 (default, Sep 10 2020, 18:47:38) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from veinmind import *
>>> d = docker.Docker()
>>> d.find_image_ids('python3')
['sha256:ff2047e9fe4b823a15d98d1d0622a235caad42dc0384518e351b9ba920d3fd39', 'sha256:2048b775ce1f9b53d91d637bcb0bfe34d8e463cb07de9bc113c4604035e3a2fd']

倘若把其中的 --mount 参数去掉,则在平行容器内运行的问脉 SDK 将因为配置错误而无法工作(输出有删减):

$ docker run --rm -it veinmind/python3:1.0.2-stretch python3
Python 3.5.10 (default, Sep 10 2020, 18:47:38) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from veinmind import *
>>> d = docker.Docker()
Traceback (most recent call last):
...
  File "/usr/local/lib/python3.5/site-packages/veinmind/binding.py", line 223, in _handle_syscall_error
    raise OSError(errvalue, os.strerror(errvalue))
FileNotFoundError: [Errno 2] No such file or directory: '/host/var/lib/docker'
...

诚然要提供一个“一把梭”命令/配置来最简化平行容器的创建是很容易的,只需要尽可能消除容器内外的区别并给予足够高的访问权限即可。譬如在创建 docker 容器时通过 --privileged 给予全部 Capabilities,将平行容器的除了 mount 以外的命名空间全部指定为 host,再加上本文描述的和问脉 SDK 运行环境相关的一些配置,即可简单地创建这样的容器。然而这样的容器被创建出来,将在用户的宿主机上展开一个巨大的暴露面,入侵者只需要成功利用 Linux 内核、云原生平台或容器内资产中的漏洞或配置错误,即可以获得当前宿主机上的 root 权限并“入驻”当前机器,这显然是我们安全从业人员需要尽力避免的。

因此比起直接给一个“一把梭”指令创建平行容器,我们更希望使用问脉 SDK 的应用在部署应用给终端用户时,其平行容器的创建配置是经过审慎编写的,在满足其实际运行需求的同时,遵循最小权限原则。本文将对现阶段问脉 SDK 和基础镜像中与平行容器有关的配置、含义及其安全影响进行尽可能详细的阐述,以供应用的编写者在充分理解其含义和后果的前提下作出准确的判断。

容器配置

宿主机根文件系统

--mount 'type=bind,source=/,target=/host,readonly,bind-propagation=rslave'

我们应该非常熟悉通过 Dockerfile 等构建方式构建镜像,然后通过构建好的镜像创建容器的使用方式:Dockerfile 构建时会添加应用运行所需的依赖和配置等文件,它们逐步组成了支持应用运行的根文件系统。根文件系统包含在镜像中进行持久化和分发,并且在容器运行时加载以运行容器。而由于应用运行所需的依赖与配置均包含在了镜像中,因此目标宿主机只需要安装了相应的容器运行时 即可运行应用,无需再在宿主机上安装对应的依赖或进行配置。

在基于问脉 SDK 构建安全应用的场景,应用的创建者可以选用合适的基础镜像并安装其应用所需的依赖,以为其应用提供运行环境,这与常规的实践是一致的。而问脉 SDK 工作时,需要访问宿主机上相应容器运行时和云原生平台的本地文件,读取相关数据和配置,以构建容器运行时、镜像、容器等 API 对象供 SDK 用户访问和操作。因此在平行容器内,除了访问容器根文件系统外,还需要能访问宿主机根文件系统。访问容器根文件系统是容器技术常规实践的要求,而访问宿主机根文件系统是问脉 SDK 正常运行的需求。

在 Linux 下,根文件系统是以 / 为根节点组织的单一树状结构,因此容器的根文件系统和宿主机的根文件系统不可能同时存在于 / 下。但是可以利用 Linux 的挂载点机制,创建 /host 目录作为挂载点并将宿主机的文件系统挂载于其下,即可实现同时访问宿主机和容器的根文件系统。

以 Docker 为例,依据其用户文档 (opens new window)所述,在创建容器时指定 --mount 参数创建 bind 挂载点,将宿主机上的 / 路径挂载到容器内的 /host,即通过 --mount 'type=bind,source=/,target=/host' 可完成宿主机文件系统的到容器内的映射。

宿主机的根文件系统上本来就存在许多挂载点,若只挂载 / 处的挂载点所包含的文件系统,可能会导致需要访问的关键文件或目录未被映射到容器内。因此挂载时需要指定 bind-propagation 选项为 rslave,使 / 目录树下的其他挂载点也被递归地映射到容器内。

而 readonly 选项则限制了应用通过 /host 对宿主根文件系统的修改操作。

容器镜像

容器名称基础镜像描述
veinmind/base:*-stretch(opens new window)buildpack-deps:stretch-scm(opens new window)安装了问脉 SDK 的基础镜像
veinmind/python3:*-stretch(opens new window)python:3.5-stretch(opens new window)安装了问脉 SDK 和 Python 3.5 基础镜像
veinmind/go1.16:*-stretch(opens new window)golang:1.16-stretch(opens new window)安装了问脉 SDK 和 Go 1.16 基础镜像

插件系统

为什么我们需要一套插件系统

当我们开发了一个容器安全工具,我们自然而然地会想在各种场景下被复用该工具。譬如,当我们写好了一个镜像后门扫描工具,会想在以下场景使用它:

  1. 本地扫描:当我们直接执行这个工具时,它能够对本地镜像进行扫描,并且输出镜像中的后门的检测结果;
  2. 操作授权:我们可以编写一个 Docker 授权插件 (opens new window),在拉取镜像和启动容器时,自动执行该工具进行镜像后门检测,并阻断风险操作;
  3. 远程扫描:我们希望使用这个工具对远程仓库中的镜像进行扫描,发现存在后门的镜像,以为后续的修复、溯源和封禁操作提供支持。

除此以外,我们还能想到很多其他的使用场景,譬如集成到 CI/CD 流程中进行扫描等。但我们不妨先以上述范围作为典型展开讨论。

一般情况下,为了方便开发和调试,我们编写的安全工具都会先支持本地扫描的功能。事实上我们大多数时候都“超量完成”了这个目标:我们会花很多时间在装饰这个工具的输出功能上,可能这个工具一半的代码都花在了命令行配色、输出格式化和各式的报告生成等输出功能的优化上。

现在让我们开始思考如何支持操作授权。显然可以在镜像后门扫描工具的基础上添加一个子命令,并把它封装为一个 Docker 授权插件,可是这要处理很多繁复细节。因此我们会希望存在一个开源的 Docker 授权插件项目,它以子进程的方式调用我们的工具,并且依据工具的检测结果决定是否要阻断用户的操作。如果这样的 Docker 授权插件存在(或者若不存在我们可以自己做一个),那必然会减少很多的我们工作量。在我们开始寻找或编写这样的 Docker 授权插件之前,不妨先思考一个问题:它应该如何获取我们的检测结果呢?

先考虑不对工具本身进行任何修改的方案。能不能直接读取工具的输出和报告,利用正则表达式等匹配出检测结果呢?除却使用正则表达式进行模式匹配本身可能会产生的错误匹配,可以相信的一点是,如果放任不同的安全工具作者对输出结果进行自由发挥,一千个工具可能就有一千种输出模式。因此要想基于正则匹配生成一个通用方案,只能让工具作者提供匹配其工具输出的正则表达式。而相信大部分人都会觉得这个方案处处散发着坏味道,且宁可对工具进行一些修改也不愿意接受这个方案。

如果允许对工具本身进行一些修改,又会有哪些可行的方案呢?既然我们依靠了子进程的方式执行工具,一个最简单的做法是判断命令的退出状态码。譬如状态码为 0 时认为当前检查没有发现威胁,否则认为发现了威胁并进行阻断。这确实是一个可行的而且容易接受的方案,但仍然存在一些问题。

首先状态码常用于表达程序是否成功执行并退出,那么当一个工具执行失败并返回非零状态码 时,是否蕴含了应该阻断用户操作的语义呢?有的用户会认为所有安全工具都应该正常执行完毕且报告无威胁后才应该放行用户操作,这样才能确保不会放过任何一个安全问题;有的用户则会认为仍在试用阶段的安全工具,若不能正常工作则应该忽略其结果,只需要确保处于生产阶段的安全工具正常工作即可。因此一刀切地处理非零状态码,并不一定能满足所有用户的使用需求。

其次在发生阻断时,很多时候都希望把完整的阻断理由(譬如在哪个路径下发现了哪种后门,哪个软件资产有哪个重大漏洞)呈现给用户,但是子进程的状态码是不包含这些信息的。而如果工具没有其他渠道报告具体的检测结果的话,只能去尝试解析工具的输出或报告,并重蹈我们先前认为充满坏味道的正则匹配老路。

因此若想以一种合理且可维护的方法提供检测结果给前文所述的 Docker 授权插件,工具应该输出一个计算机可读、格式预定义的检测结果,以供 Docker 授权插件进行解析和处理。

让我们再考虑如何支持远程扫描。我们会希望编写的工具能提供一个子命令,接收远程仓库 URL 及其认证信息,而该子命令只需要把相应的镜像 tar 下载下来,执行原来的扫描代码即可。

在只有单个安全工具需要执行时,这个思路很自然,但是很多时候我们会不止关心一种威胁,因此会同时执行多个检测工具。在这种情况下,若各个工具之间都各自独立下载远程镜像,除了会因为重复下载镜像 tar 文件(注意到镜像中一个 Layer 的大小经常在几百 MB 量级),占用大量网络带宽并且扫描效率低下外,部分公有仓库如 Docker Hub 还有下载频率限制 (opens new window),对于 Docker Hub 的非付费用户而言,其能扫描的镜像个数随着其使用的工具个数呈反比例关系迅速下降,用户体验极差 。

一个简单可行的解决方法是,执行每一个工具之前,先把待扫描镜像下载到本地(不管是直接下载 tar 还是使用已经存在的容器运行的 Pull 指令),然后执行各工具进行扫描,待扫描完成后再移除镜像释放资源。值得注意的是,这样就把针对远程仓库的扫描转化为本地扫描了,而进行扫描时只需要为各工具指定下载好的镜像,而无需让工具感知当前是否在扫描远程仓库。这也就意味着工具即使只支持本地扫描,也可以在远程扫描的场景下复用。同时为了方便使用,我们往往会编写一个远程扫描入口程序,接收扫描指令,并依此完成镜像下载、工具调用和镜像卸载的工作。

除了操作授权和远程扫描外,我们还能针对很多其他的使用场景展开实现细节上的讨论,并且总能发现在新的场景下,工具总需要作出或多或少的调整才能完美满足需求。事实上,使用场景的种类之多和各场景之间的差异之大,已然让在工具或者 SDK 中通过堆砌代码来应对成为不可能完成的任务。因此,我们会通过抽象和适配等手段,想方设法将新的场景处理为已知的场景(如远程扫描中将扫描远程仓库处理为扫描本地仓库),这样才能达成在不同的使用场景下复用已有工具。

至此,一个插件系统的想法呼之欲出:如前文所述的 Docker 授权插件、远程扫描的入口程序,我们将这样的程序称为宿主程序(Host Program),它们负责处理各使用场景下的具体细节,将其转化为容器、镜像等具体实体的扫描问题;与之相对的,如前文所述的镜像后门扫描工具、镜像漏洞扫描工具,我们将这样的程序称为插件(Plugin),它们则能对容器、镜像等具体实体进行扫描,并发现其中存在的安全问题。

针对某一具体的使用场景,其相关细节的处理是相对固定的,往往有一个与之相对应的宿主程序;而对于某一具体的容器、镜像等实体,往往需要检查多个方面的安全问题,每个方面的安全问题往往有与之对应的插件可以进行检查。因此宿主程序和插件属于一对多的关系,在实际使用中我们往往先配置好了具体的宿主程序,然后依据我们所关心的安全问题插拔安全插件。

而宿主进程和插件之间并非只有简单的而单向的父子进程的调用关系。在前文讨论如何支持操作授权时,宿主进程就需要收集插件输出的检测结果并处理,生成操作授权响应。在本文未讨论到的场景中,也存在其他采集检测结果并进行定制化处理的需求,如生成检测报告文件,生成 Syslog 并转发到 SIEM 等。同理,针对插件的日志输出,会希望支持进行采集和过滤等操作,以支持不同场景下的日志持久化、工具调试和界面展示等需求。为了处理这些插件产生的、需要可定制化处理的数据,会需要打通从插件到宿主进程的通路,并通过宿主进程向插件提供服务(Service)的方式,在插件中通过调用服务产生数据,然后在宿主进程接收这些数据并进行具体处理 。

在问脉 SDK 中,我们除了针对容器安全相关实体设计了相应的 API 外,还设计并实现了一套插件系统。基于插件系统编写的安全工具只需一次编写,并在本地扫描的场景中验证,便可集成到不同使用场景下的宿主程序中得到复用;同样地,而对于新的使用场景,只需基于插件系统编写新的宿主程序,便可复用现有的已经编写好的插件。

从零开始的插件系统

你好,插件

我们使用 Python 语言来编写我们的第一个插件,在此前请先安装问脉 SDK 软件包,并通过 sudo pip3 install veinmind 安装问脉 SDK 的 Python Binding。

创建文件 hello-plugin,向文件内写入以下内容,并通过 chmod a+x ./hello-plugin 赋予该文件可执行权限:

#!/usr/bin/env python3
from veinmind import *
from os.path import join
from stat import *

command.set_manifest(name="hello-plugin", version="1.0.0")

@command.image_command()
def scan(image):
    """Find executables inside images"""
    reporefs = image.reporefs()
    name = reporefs[0] if len(reporefs) > 0 else image.id()
    log.info('image %s scan start', name)
    for root, _, filenames in image.walk('/'):
        for filename in filenames:
            filepath = join(root, filename)
            mode = image.lstat(filepath).st_mode
            if S_ISREG(mode) and (S_IMODE(mode) & 0o111) != 0:
                log.info('image %s has executable: %s', name, filepath)
    log.info('image %s scan done', name)

if __name__ == '__main__':
    command.main()

这可能看上去比一般的 Hello World 程序要复杂一些,不过我相信,一个包含容器操作的样例比起简单地打印一串 Hello World 文本更适合作为一个容器安全 SDK 的 Hello World。

上述样例除了一些结果输出的美化处理外,最核心的地方是一个对镜像内文件进行处理的循环。事实上,这个循环只不过是下述代码的“容器化版本”:

#!/usr/bin/env python3
import os
from os.path import join
from stat import *

for root, _, filenames in os.walk('/'):
    for filename in filenames:
        filepath = join(root, filename)
        mode = os.lstat(filepath).st_mode
        if S_ISREG(mode) and (S_IMODE(mode) & 0o111) != 0:
            print('found executable %s' % (filepath))

如果读者比较熟悉 Python 的话,不难发现这段代码的功能是从宿主机根目录开始遍历,并打印其中的可执行文件。而 hello-plugin 中的代码不过是把 os 替换为了当前正在处理的镜像 image,因此不难猜出这段代码的功能是从镜像 image 的根目录开始遍历,并打印其中的可执行文件。

通过执行 sudo ./hello-plugin scan 指令即可印证我们的猜测:执行该指令时,hello-plugin 对本机上所有镜像进行了扫描,并打印了每个镜像中的可执行文件。我们也可以在其后添加一个或多个镜像名称或镜像 ID 等参数,如 sudo ./hello-plugin scan nginx 1a2d3c,来限定所需扫描的镜像范围。

我们详细考察一下 hello-plugin 的代码细节,注意到通过 from veinmind import * 我们导入了问脉 SDK 所提供的 command 和 log 模块,而 command 模块提供了 command.image_command 和 command.main 等函数,为我们处理了 hello-plugin 指令执行的诸多细节,简化了我们的开发。

不过相信看到这里,有很多读者会有疑问:迄今为止所展示的样例中,hello-plugin 行为怎么看都像是一个可以独立执行的工具,难以看出为何会被称为插件,以及其与本文所谓插件系统之间的联系。

事实上,在插件系统中,插件和宿主程序是成对的概念,脱离宿主程序是无法清晰地解释插件的概念的。为了简单地解释这个问题,我们不妨马上进入第一个宿主程序的编写。

你好,宿主程序

我们不妨针对前文所述的远程扫描的场景编写一个简单的宿主程序。依据前文的思路,这样的宿主程序将接收一个远程仓库列表作为参数,调用宿主机上的容器运行时下载相应的远程仓库,并调用插件对仓库进行扫描。

我们选择 Containerd 作为容器运行时,利用它提供的命名空间功能,创建命名空间并在其中进行操作,以避免我们影响宿主机上其他容器的运行。一般情况下,Docker 都会以 Containerd 作为所依赖的下层容器运行时,这就意味着在安装了 Docker 的机器上我们不必额外安装 Containerd。

我们使用 Go 语言来编写宿主程序,以便使用 Containerd 提供的 API。创建文件 hello-host.go,并写入以下内容:

package main

import (
	"context"
	"os"
	"path"

	"github.com/containerd/containerd"
	"github.com/distribution/distribution/reference"
	"github.com/spf13/cobra"

	"github.com/chaitin/libveinmind/go/cmd"
	veinmindContainerd "github.com/chaitin/libveinmind/go/containerd"
	"github.com/chaitin/libveinmind/go/plugin"
	"github.com/chaitin/libveinmind/go/plugin/log"
	"github.com/chaitin/libveinmind/go/plugin/service"
)

func scanImages(ctx context.Context, reporefs []string) error {
	client, err := containerd.New(
		"/run/containerd/containerd.sock",
		containerd.WithDefaultNamespace("hello-host"))
	if err != nil {
		return err
	}
	defer client.Close()
	var imageIDs []string
	for _, reporef := range reporefs {
		if named, err := reference.ParseDockerRef(reporef); err == nil {
			reporef = named.String()
		}
		log.Infof("pulling image %q", reporef)
		image, err := client.Pull(ctx, reporef, containerd.WithPullUnpack)
		if err != nil {
			log.Errorf("cannot pull image %q: %v", reporef, err)
			continue
		}
		log.Infof("pulled image %q", reporef)
		imageID := "hello-host/" + string(image.Target().Digest)
		imageIDs = append(imageIDs, imageID)
	}

	plugins, err := plugin.DiscoverPlugins(ctx, ".")
	if err != nil {
		return err
	}
	c, err := veinmindContainerd.New()
	if err != nil {
		return err
	}
	defer c.Close()
	return cmd.ScanImageIDs(ctx, plugins, c, imageIDs,
		plugin.WithExecInterceptor(func(
			ctx context.Context, plug *plugin.Plugin, c *plugin.Command,
			next func(context.Context, ...plugin.ExecOption) error,
		) error {
			reg := service.NewRegistry()
			reg.AddServices(log.WithFields(log.Fields{
				"plugin":  plug.Name,
				"command": path.Join(c.Path...),
			}))
			return next(ctx, reg.Bind())
		}),
	)
}

var rootCmd = &cobra.Command{
	Use:   "hello-host",
	Short: "Our first host program to scan images from repositories",
}

func init() {
	rootCmd.AddCommand(&cobra.Command{
		Use:   "scan",
		Short: "scan images from repositories",
		Args:  cobra.MinimumNArgs(1),
		RunE: func(c *cobra.Command, args []string) error {
			return scanImages(c.Context(), args)
		},
	})
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}

执行指令 go mod init hello-host && go build -o hello-host ./hello-host.go 进行编译 ,得到我们的第一个宿主程序 hello-host

将 hello-host 和 hello-plugin 置于同一目录,然后执行 sudo ./hello-host scan 加镜像名,譬如 sudo ./hello-host scan nginx redis golang:1.16,即可调用 hello-plugin 插件对远程仓库进行扫描。

直接调用 Containerd 的 API 清空命名空间的代码比较复杂,因此为了我们理解和说明的方便,没有在宿主程序中包含扫描完毕后清空命名空间的代码。读者可以直接执行以下代码来清空 hello-host 创建的命名空间:

sudo ctr -n hello-host images ls -q | xargs sudo ctr -n hello-host images rm
sudo ctr -n hello-host content ls -q | xargs sudo ctr -n hello-host content rm
sudo ctr namespace rm hello-host

尽管 hello-host 的代码比起 hello-plugin 的代码来说长多了,但是我们可以很容易发现其核心逻辑位于 scanImages 函数中。scanImages 函数包含两部分,前一部分依据参数列表中指定的镜像名称,使用 Containerd 的 API 拉取镜像,并记录镜像 ID 到列表 imageIDs 中;后一部分则发现当前工作目录中存在的插件,并调用问脉 SDK 的 ScanImageIDs 函数驱动这些插件对 imageIDs 中指定的镜像进行扫描。让我们把注意力集中到与插件相关的后一部分。

首先要解决的一个问题是,尽管我们发现了 hello-plugin 插件并成功执行了其中的 scan 函数,但是我们尚不清楚什么样的可执行文件中,什么样的函数会被成功识别并执行。譬如将 hello-host 与 hello-plugin 置于当前工作目录下时,hello-host 中的 scanImages 函数就没有被执行。而如果尝试将 hello-plugin 中的 scan 函数重命名为 scan_images 函数,再以同样的指令执行 hello-host,重命名后的 scan_images 函数依然会被执行。这说明我们执行插件中的函数并不是依靠简单的名字匹配,而是有更高阶的规则去发现待执行的候选函数。

直接执行 hello-plugin,我们可以发现其下有两个子命令:info 和 scan(重命名为 scan_images 函数后为 scan-images 子命令,以下略)。其中 info 并没有被我们手动定义过,而执行 info 子命令可以看到以下输出:

$ sudo ./hello-plugin info
{"manifestVersion": 1, "name": "hello-plugin", "version": "1.0.0", "author": "", "description": "", "tags": [], "commands": [{"type": "image", "data": {}, "path": ["scan"]}]}

在这里我们可以明显地看到先前通过 @command.image_command 对函数进行标注的成果:插件通过问脉 SDK 对函数进行标注,除了简化代码以外,更重要的是在代码层面确定了每个函数的功能语义。如在 hello-plugin 中定义的 scan 函数被 @command.image_command 标注确定为是针对镜像的扫描函数,那么在 hello-host 执行 cmd.ScanImageIDs 函数扫描指定镜像时理应调用该函数,这也是我们最终观察到的运行结果。

宿主程序 hello-host 与插件 hello-plugin 分别使用 Go 和 Python 语言编写,具有不同的语言运行时。而利用子命令的方式可以将语言的差异封装起来,对外仅在进程级别暴露相互调用的接口,对内通过问脉 SDK 的代码约束了各子命令的参数传递与解析方式,最终不同语言编写的宿主程序和插件得以协同运行。

至此,宿主程序发现并调用插件中的函数的方式就一目了然了:

  1. 插件编写不同类型的扫描函数并对它们进行分别标注,而 info 子命令利用标注信息生成元数据;
  2. 宿主程序对每一个放置在插件目录中的可执行文件,调用 info 子命令获取插件的元数据并解析,生成插件列表;
  3. 宿主程序识别并生成相应的扫描对象,并依据插件元数据调用插件的对应函数,实现可扩展的扫描。

然后另一个问题是,在宿主程序 hello-host 调用 cmd.ScanImageIDs 时,指定了一个显眼到不可忽略的 plugin.WithExecInterceptor 参数,其作用是什么,是否每次调用都需要?

在问脉 SDK 中,plugin.WithExecInterceptor 参数允许我们通过类似职责链的方式为每次插件的执行添加一些额外行为,如初始化某一资源,配置运行环境等,其风格和原理类似于 gRPC 的 WithUnaryInterceptor (opens new window)函数。

在 hello-host 的例子中,我们在 plugin.WithExecInterceptor 使用 service.NewRegistry 初始化了一个 Registry,并且往其中添加了一个通过 log.WithFields 初始化的服务。而在运行时我们也发现,hello-plugin 通过 log.info 记录的日志被转发到了 hello-host 中,并且在往每条日志添加 log.Fields 中的指定的字段后,作为 hello-host 本身的日志打印出来。

这便是问脉 SDK 插件系统的的另一重要机制:服务。在问脉 SDK 中,宿主程序只需要将能访问的服务注册到 Registry 中进行索引,即可向插件提供服务,问脉 SDK 为宿主程序和插件处理了与服务相关的诸多细节,如进程间通信、路由、服务调度等;同时,作为常用服务之一,问脉 SDK 也提供了日志服务,以便宿主接收各插件等运行日志,并且在宿主程序中进行过滤、归档和轮转等处理,它也为其他类型服务的编写提供了参考。

Q&A

docker 版本过低不支持以 --mount 运行平行容器

使用 --mount 的原因在于假设根目录下的容器相关目录进行了其他形式的挂载,则普通的 -v 无法获取到相应的磁盘目录。如果没有这种情况,可以使用 -v /:/host 运行平行容器,如

docker run -v /:/host -v /var/run/docker.sock:/var/run/docker.sock veinmind/veinmind-runner

-g 无法正确筛选插件

-g 实际上是用 glob 模式去筛选插件,因此针对部分 python 编写的插件,如 veinmind-sensitive,需要使用 **/veinmind-sensitive/* 去筛选

编写插件

可以通过 example 快速创建一个 veinmind-tools 插件, 具体查看 veinmind-example

☁️ 云原生设施兼容性

项目地址:

GitHub:
https://github.com/chaitin/veinmind-tools

文档地址

https://veinmind.chaitin.com/docs

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注