jwang996/runtime-vulnerability-scan

GitHub: jwang996/runtime-vulnerability-scan

轻量级自托管的 Docker 主机运行时漏洞扫描器,通过 cron 定时调用 Trivy 扫描所有运行容器镜像并输出结构化 JSON 日志。

Stars: 1 | Forks: 0

# runtime-vul-detect [![检查](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/9c1660feb2160749.svg)](https://github.com/jwang996/runtime-vulnerability-scan/actions/workflows/checks.yaml) [![验证](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/bbb17bf363160755.svg)](https://github.com/jwang996/runtime-vulnerability-scan/actions/workflows/verify.yaml) [![发布](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/8b90d4c12c160800.svg)](https://github.com/jwang996/runtime-vulnerability-scan/actions/workflows/release.yaml) ![发布](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/jwang996/8a8e52e678565c0bbdf1446054f39033/raw/release.json) ![测试覆盖率](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/jwang996/8a8e52e678565c0bbdf1446054f39033/raw/coverage.json) ![测试覆盖率](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/jwang996/8a8e52e678565c0bbdf1446054f39033/raw/coverage-go.json) ![Trivy 高危](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/jwang996/8a8e52e678565c0bbdf1446054f39033/raw/trivy-high.json) ![Trivy 严重](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/jwang996/8a8e52e678565c0bbdf1446054f39033/raw/trivy-critical.json) ![许可证](https://img.shields.io/badge/License-Apache%202.0-blue.svg) ![Go](https://img.shields.io/badge/Go-1.25-00ADD8?logo=go&logoColor=white) ![Python](https://img.shields.io/badge/Python-3.13-3776AB?logo=python&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-distroless%20%7C%20alpine-2496ED?logo=docker&logoColor=white) 一个轻量级、自托管的 Docker 主机漏洞扫描器。它会发现主机上所有容器中正在使用的每个 image,通过 cron 定时任务使用 [Trivy](https://github.com/aquasecurity/trivy) 对它们进行扫描,并输出结构化的 JSON 日志 —— 随时准备被 [Grafana Alloy](https://grafana.com/docs/alloy/latest/)、Promtail 或任何输入到 Loki 或类似后端的日志收集器收集。 ## 什么是漏洞? **软件漏洞** 是程序中的一个弱点或缺陷,可被利用以破坏系统安全。业界通过一套众所周知的标准来跟踪它们: | 术语 | 含义 | |---|---| | **CVE** | Common Vulnerabilities and Exposures(通用漏洞披露)—— 分配给每个已知缺陷的唯一标识符(例如 `CVE-2021-44228`)。由 [MITRE](https://www.cve.org/) 编目。 | | **NVD** | National Vulnerability Database(国家漏洞数据库)—— NIST 提供的丰富 CVE 数据源,包含 CVSS 分数、受影响的 package 范围和补丁可用性。 | | **CVSS** | Common Vulnerability Scoring System(通用漏洞评分系统)—— 反映可利用性和影响的 0–10 分数。分数 ≥ 9.0 为**严重**,7.0–8.9 为**高危**。 | | **SCA** | Software Composition Analysis(软件成分分析)—— 将软件构件(container image、lock file)与已知的 CVE 数据库进行比对扫描,以发现存在漏洞的依赖项。 | Trivy 对每个 container image 内部的 OS package 层和语言生态系统 package(pip、npm、gem 等)执行 SCA,将安装的版本与 NVD 和供应商公告进行匹配。 ## 为什么选择 runtime-vul-detect? Container image 会随着时间的推移积累 CVE。在构建时干净的 base image,当其某个 package 被发布了新的 CVE 后,几天后可能就会变得脆弱。大多数团队只在 CI 阶段进行扫描 —— 当漏洞被披露时,image 已经在生产环境中运行,且没有任何程序会重新检查它。 ### 市场格局 | 工具 | 自托管 | 独立 Docker | 定时任务 | 结构化日志 | 备注 | |---|---|---|---|---|---| | **runtime-vul-detect** | 是 | 是 | 是 | JSON | 本项目 | | [Trivy CLI](https://github.com/aquasecurity/trivy) | 是 | 是 | 否 | JSON | 优秀的扫描器;无 scheduler,无去重,无日志传送 | | [Grype](https://github.com/anchore/grype) | 是 | 是 | 否 | JSON | 强大的替代扫描器(Go binary);与 Trivy CLI 存在相同缺口 | | [Trivy Operator](https://github.com/aquasecurity/trivy-operator) | 是 | **否 — 仅限 k8s** | 是 | CRD 事件 | 基于 Kubernetes CRD;需要 cluster | | [Kubescape](https://kubescape.io/) | 是 | **否 — 仅限 k8s** | 是 | – | 底层使用 Grype;与 cluster 耦合 | | [Docker Scout](https://docs.docker.com/scout/) | 否 (SaaS) | 是 | 否 | – | 仅限云端;免费层有限 | | [Snyk Container](https://snyk.io/product/container-vulnerability-management/) | 否 (SaaS) | 是 | 否 | – | 需将数据发回 Snyk 云端进行分析 | | [Clair](https://github.com/quay/clair) | 是 | 是 | 否 | API | 优先考虑 Registry 的微服务;需要 Postgres;无 CLI | | [Aqua Security](https://www.aquasec.com/) | 是 (付费) | 是 | 是 | – | 基于 Trivy 构建的商业平台;重量级 | | [Falco](https://falco.org/) | 是 | 是 | – | JSON | runtime 行为检测,而非 CVE 扫描 | **市场缺口**:对于独立的 Docker 主机(没有 Kubernetes),市面上没有一款轻量级的自托管工具能够开箱即用地结合定时任务、image 去重和结构化日志输出。runtime-vul-detect 填补了这一空白。未来的 Kubernetes 支持已列入路线图,届时它将与 Trivy Operator 并驾齐驱,但运营模式更简单(无需 CRD)。 ## 架构 ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ Docker host │ │ │ │ ┌──────────────────────┐ tcp:2375 ┌──────────────────────────────┐ │ │ │ runtime-vul-detect │◄────────────│ docker-socket-proxy │ │ │ │ │ GET-only │ wollomatic/socket-proxy:1.12.1 │ │ │ │ APScheduler │ allowlist │ │ │ │ │ ├── scan (cron) │ │ UID 65534 (nobody:nobody) │ │ │ │ └── db-update │ │ read-only rootfs │ │ │ │ │ │ │ ALL capabilities dropped │ │ │ │ ▼ │ └──────────────┬───────────────┘ │ │ │ Trivy binary │ │ │ │ │ │ │ /var/run/docker.sock (ro) │ │ │ ▼ │ │ │ │ JSON log lines ────┼──► stdout │ │ └──────────────────────┘ │ │ │ ▼ │ │ Grafana Alloy / Promtail │ │ │ │ │ ▼ │ │ Loki / SIEM │ └──────────────────────────────────────────────────────────────────────────┘ ``` ### Docker socket 代理 扫描器永远不会直接挂载 `/var/run/docker.sock`。相反,一个 `wollomatic/socket-proxy` sidecar 持有真正的 socket,并在仅限内部的 Docker bridge 网络(`socket-proxy`,`internal: true`)上暴露一个 TCP endpoint。 **为什么这很重要**:Docker socket 实际上等同于主机上的 root 权限 —— 任何能够对其进行写入操作的进程都可以启动特权 container、挂载主机文件系统或直接逃逸到主机上。如果扫描器被攻破(例如通过恶意的 Trivy DB 或依赖项中的 CVE),它将拥有完整的 daemon 访问权限。代理则彻底消除了这一风险途径。 **代理强制执行的限制:** | 控制项 | 详情 | |---|---| | 协议 | 仅限 TCP;不对扫描器暴露 Unix socket | | 方法 | 仅限 `GET` —— 没有 `POST`、`DELETE`、`PUT` | | Endpoint | `containers/json`、`images/sha256:.../json`、`version`、`/_ping` —— 其他所有请求均返回 403 | | 网络 | `internal: true` bridge —— 代理 container 没有互联网路由 | | 权限 | UID 65534 (nobody),丢弃所有 Linux capabilities,只读 rootfs | 即使扫描器 container 被完全攻破,攻击者也只能枚举 container 名称和 image SHA —— 他们无法在主机上启动、停止、执行或删除任何内容。 ## 资源使用情况 | 资源 | 空闲时 | 扫描期间 | 备注 | |---|---|---|---| | **内存 (RSS)** | ~60–80 MB | ~300–500 MB 峰值 | Python 解释器和库在空闲时;Trivy 每次调用加载其 DB(~250 MB) | | **CPU** | ~0% | 单核 100%(每个 image 5–30 秒) | Trivy 是 Go binary —— 扫描对 CPU 消耗很高效;否则 Python orchestrator 处于空闲状态 | | **磁盘** | ~250 MB | – | Python 3.13-alpine + Trivy binary + Cython `.so` 文件 | | **磁盘 (缓存卷)** | ~300–500 MB | – | Trivy 漏洞 DB + layer cache;随着 DB 更新缓慢增长 | | **网络** | 0 | DB 更新时 ~50–100 MB | 从 GitHub 增量下载 DB;image 扫描仅使用本地 Docker socket | | **Docker socket** | 未挂载 | 未挂载 | 通过 socket 代理 sidecar 访问;扫描器从不直接触碰 socket | | **Socket 代理** | ~2–4 MB | ~2–4 MB | `wollomatic/socket-proxy:1.12.1`;distroless,UID 65534,丢弃所有 caps;64 MB 内存限制 | **Cython 的影响**:将 `.py` 编译为 `.so` 稍微减少了 container 启动时间(导入时无需进行字节码编译),但并不能实质性地减少稳态内存,因为 Python 解释器及其 C 扩展基础设施依然存在。其主要好处是保护了源代码。 **Trivy 子进程开销**:每次扫描都会生成一个 `trivy image` 子进程。Trivy 在每次调用时都会加载其漏洞 DB(从磁盘读取约 100 MB)。对于运行许多不同 image 的主机,持久的 Trivy server 模式(`trivy server`)可以分摊此成本 —— 已计划作为未来的优化项。 ## 快速开始 ``` git clone cd runtime-vul-detect chmod +x *.sh && ./up.sh python # or: ./up.sh go ``` 所有的编排脚本都位于**仓库根目录**(这样 `python/` 和 `go/` 文件夹就只保留纯粹的代码和 Docker 资产)。`./up.sh ` 会构建 image, 启动 container,等待 Docker healthcheck 通过,然后输出日志。 使用 `./down.sh python`(或 `go`)停止它 —— 添加 `--purge` 可以删除缓存 卷。`up.sh` 和 `down.sh` 作用于单个变体;`build.sh`、 `scan_vul.sh`、`test_up.sh` 和 `verify_metrics.sh` 也支持 `all`。 Trivy 版本固定在 `/docker-compose.yml` 中的 `build.args.TRIVY_VERSION` 下。在此处更新它以进行升级。运行 `./build.sh python` (或 `go` / `all`)重新构建并立即扫描生成的 image 以查找 漏洞。 ## 实现方式:Python(主要)与 Go 该项目为同一个扫描器提供了**两种可互换的实现**。它们具有相同的行为、配置(`python/config/config.yaml` + `python/config/ignore.yaml`)和 JSON 日志 schema —— 选择适合您运营限制的即可。 | | **Python**(主要 / 参考) | **Go**(替代) | |---|---|---| | 源码 | [`python/`](python/) — `src/`、`tests/` | [`go/`](go/) — `cmd/`、`internal/` | | 基础/运行时 | `python:3.13-alpine`,Cython 编译的 `.so` | `gcr.io/distroless/static-debian12:nonroot`,单一静态 binary | | Scheduler | APScheduler | `robfig/cron/v3` (`SkipIfStillRunning`) | | Docker 访问 | `docker` SDK | 通过 Unix socket 使用 Go 标准库 `net/http`(无 SDK,无 `containerd`/`otel` 依赖) | | 日志 | `python-json-logger` | `log/slog`(标准库) | | 发布的 tag | `ghcr.io/jwang996/runtime-vul-scanner:1.0.0-python-alpine` | `ghcr.io/jwang996/runtime-vul-scanner:1.0.0-go-distroless` | | Image 大小 | ~250 MB | ~30–50 MB | | 空闲内存 | ~60–80 MB | ~8–15 MB | **Python 仍然是主要实现** —— 它是行为和文档的参考标准。Go 版本是一个直接可用的替代方案,适用于需要最小化 distroless image、更小攻击面(无 shell、无 package 管理器、无 Docker SDK 依赖树)以及更低的内存/启动成本的环境。 使用根目录下的脚本驱动任一实现 —— 变体文件夹仅包含 源码 + Docker 资产: ``` chmod +x *.sh ./up.sh python # build, start, wait for healthy, tail logs (one variant) ./build.sh python # rebuild + Trivy-scan the produced image (or: go / all) ./test_up.sh all # containerised unit tests + coverage gate (both variants) ./verify_metrics.sh all # bring up with all /metrics protections and assert them ./verify_webhook.sh all # bring up against the vulnerable target and assert webhook alerts ./down.sh python # stop the variant (add --purge to drop volumes) ``` `up.sh` 和 `down.sh` 作用于**单个**变体(`python` 或 `go`) —— 由于两者 共享 container 名称并绑定9090 端口,因此不需要也不支持同时 运行两者。`build.sh`、`scan_vul.sh`、`test_up.sh` 和 `verify_metrics.sh` 支持 `python | go | all`。 两种实现都在 CI 中独立进行构建、扫描(`Image Scan` workflow)、单元测试和发布(`CI` workflow)。Go 版本中的测试文件是同名的 `_test.go` 文件(Go 惯例),并且**绝不**会被编译到 runtime binary 中。 ## 配置 所有的扫描器行为都由两个 YAML 文件控制,这些文件以只读方式挂载到 container 内部的 `/config/` 目录中。这两个文件开箱即用,提供了合理的默认值。 **热重载:** 扫描器每 15 秒轮询一次配置和忽略文件,并应用更改,**无需重启 container**。编辑 `scan_cron`、`db_update_cron`、`severity`、`ignore_files`、`db_update` 或 `timezone` 会就地重新调度正在运行的任务(会输出一行 `Config reloaded` 日志)。无效的新配置(无法解析的 YAML 或错误的 cron/timezone)会被记录为错误,并继续使用先前的配置运行。`scan_on_start`、`metrics_*` 设置、`webhook_*` 设置和 `events_*` 设置仅在启动时应用;更改这些设置需要重启。 ### `python/config/config.yaml` — 扫描器设置 ``` # 用于 cron schedule 解释的 IANA timezone # 完整列表:https://en.wikipedia.org/wiki/List_of_tz_database_time_zones timezone: "Europe/Berlin" # 扫描所有 image 的频率(标准 5 字段 cron,使用上述 timezone) scan_cron: "*/5 * * * *" # 刷新 Trivy vulnerability database 的频率 db_update_cron: "0 0 * * *" # 包含在结果中的严重级别 — 低于此阈值的条目将被静默丢弃 # 选项:CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN severity: "CRITICAL,HIGH" # 自动保持 Trivy CVE database 为最新 db_update: true # 在 container 启动时立即运行完整扫描(在第一次 cron 触发之前) scan_on_start: true # Trivy cache directory — 必须与 docker-compose.yml 中的 volume mount 匹配 cache_dir: "/tmp/trivy-cache" # 启动时加载的 ignore 文件 — 多个文件将被合并;重复项将作为 error 记录 ignore_files: - "/config/ignore.yaml" # Prometheus /metrics endpoint(严重级别计数作为 gauges + 扫描元数据) # 在启动时应用;更改这些需要重启 container metrics_enabled: true metrics_port: 9090 ``` | 键 | 默认值 | 描述 | |---|---|---| | `timezone` | `Europe/Berlin` | 用于解释两个 cron 表达式的 IANA 时区字符串 | | `scan_cron` | `0 * * * *` | 扫描所有 image 的频率(默认为每小时) | | `db_update_cron` | `0 0 * * *` | 拉取最新 CVE 数据库的频率(默认为每天) | | `severity` | `CRITICAL,HIGH` | 包含哪些严重级别;较低级别在记录日志前会被过滤掉 | | `db_update` | `true` | 切换自动数据库更新;在气隙隔离环境下禁用 | | `scan_on_start` | `true` | 在启动时立即扫描,而不是等待第一次 cron 触发 | | `cache_dir` | `/tmp/trivy-cache` | Trivy 的本地数据库和 layer cache | | `ignore_files` | `[/config/ignore.yaml]` | 忽略配置路径列表;所有路径在启动时合并 | | `metrics_enabled` | `true` | 提供 Prometheus `/metrics` endpoint;设置为 `false` 以禁用 HTTP server | | `metrics_port` | `9090` | container 内部 `/metrics` endpoint 监听的端口 | | `metrics_basic_auth_user` | `""` | 设置后(连同密码一起)在 `/metrics` 上要求 HTTP Basic auth | | `metrics_basic_auth_password` | `""` | Basic auth 的密码 | | `metrics_tls_cert_file` | `""` | PEM 证书路径;与密钥一起使用时,通过 HTTPS 提供 `/metrics` 服务 | | `metrics_tls_key_file` | `""` | TLS 证书的 PEM 私钥路径 | | `metrics_tls_client_ca_file` | `""` | 要求并验证客户端证书的 CA bundle(双向 TLS) | | `webhook_enabled` | `false` | 针对新出现发现的 webhook 警报的选填主开关;`false` = 完全没有 HTTP(纯监控) | | `webhook_url` | `""` | 接收 POST 请求的 HTTP(S) endpoint | | `webhook_timeout_seconds` | `10` | 以整数秒为单位的单次请求 HTTP 超时时间 | | `webhook_auth_header` | `""` | 附加到每个 POST 请求的可选 HTTP 头**名称**(例如 `Authorization`) | | `webhook_auth_token` | `""` | 可选的密钥值;仅当**两者**都非空时作为 `: ` 发送(绝不记录日志) | | `events_enabled` | `false` | 通过 Docker `/events` 流进行事件驱动扫描的选填主开关;`false` = 完全没有 `/events` 连接(纯粹的 cron 行为) | | `events_debounce_seconds` | `5` | 在每次针对不同 ref 触发一次定向扫描之前,合并事件 image ref 的空闲时间窗口 | ### `python/config/ignore.yaml` — 抑制已知的非问题 使用此文件将您环境中不可利用的 CVE 加入白名单,或者完全跳过对特定 image 的扫描。 ``` # 完全跳过这些 image — 支持 fnmatch 通配符 images: # - "busybox:*" # skip all busybox tags # - "internal-tool:dev" # skip a specific dev image # 抑制特定的 CVE — 使用 Trivy 的原生 .trivyignore.yaml 格式 # 参见:https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/ vulnerabilities: # - id: CVE-2021-44228 # statement: "log4j not reachable — no Java runtime in this image" # expired_at: 2027-01-01 # optional: unquoted YAML date; re-surfaces after this date # # - id: CVE-2023-1234 # paths: # - usr/local/lib/python3.13/site-packages/requests # statement: "requests not exposed to untrusted input" ``` 您可以通过在 `config.yaml` 中的 `ignore_files` 下列出多个忽略文件来提供它们。如果同一个 CVE ID 或 image 模式出现在多个文件中,启动时会输出一条 `ERROR` 日志 —— 这使得跨文件冲突能被立即发现,而不是以未定义的顺序默默地被覆盖。 ## 日志输出 每次扫描结果都会作为单行 JSON 输出到 stdout: ``` { "timestamp": "2026-05-28T12:00:00Z", "level": "info", "logger": "runtime-vul-detect", "message": "scan_result", "event": "scan_result", "image": "nginx:1.21", "image_id": "sha256:abc123...", "containers": ["a1b2c3d4e5f6", "b2c3d4e5f6a1"], "total_vulnerabilities": 3, "severity_counts": { "CRITICAL": 1, "HIGH": 2, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0 }, "vulnerabilities": [ { "id": "CVE-2021-1234", "package": "openssl", "version": "1.1.1k-r0", "fixed_version": "1.1.1l-r0", "severity": "CRITICAL", "title": "Memory corruption in openssl", "target": "alpine:3.15" } ] } ``` `containers` 列出了共享该 image 的所有 container 短 ID,因此您可以将发现追踪到具体运行的工作负载上。 ### 使用 Grafana Alloy 进行收集 ``` // Discover the scanner container and forward its stdout to Loki loki.source.docker "scanner" { host = "unix:///var/run/docker.sock" targets = discovery.docker.containers.targets forward_to = [loki.process.parse_json.receiver] labels = { job = "vulnerability-scanner" } } // Parse the JSON and promote scan fields as structured metadata loki.process "parse_json" { stage.json { expressions = { event = "event", image = "image", total_vulns = "total_vulnerabilities", critical = "severity_counts.CRITICAL", } } stage.labels { values = { image = "", event = "" } } forward_to = [loki.write.default.receiver] } ``` 因为每一行都是有效的 JSON,Loki 的 `json` 解析器可以将 `severity_counts`、`image` 和 `total_vulnerabilities` 提取为结构化字段或[指标值](https://grafana.com/docs/loki/latest/query/metric_queries/),而无需使用正则表达式。 ## Prometheus 指标 除了 JSON 日志之外,扫描器还在 `GET /metrics` 提供一个 Prometheus 展示 endpoint(默认启用;参见 `metrics_enabled` / `metrics_port`)。它 仅在主机 loopback 上发布 —— 在 `docker-compose.yml` 中为 `127.0.0.1:9090` —— 因此 不会暴露在公共接口上。Python 和 Go 实现均 输出字节完全相同的输出: ``` # TYPE runtime_vul_severity_count gauge runtime_vul_severity_count{image="nginx:1.21",severity="CRITICAL"} 1 runtime_vul_severity_count{image="nginx:1.21",severity="HIGH"} 2 runtime_vul_severity_count{image="nginx:1.21",severity="MEDIUM"} 0 runtime_vul_severity_count{image="nginx:1.21",severity="LOW"} 0 runtime_vul_severity_count{image="nginx:1.21",severity="UNKNOWN"} 0 # TYPE runtime_vul_last_scan_timestamp_seconds gauge runtime_vul_last_scan_timestamp_seconds 1749081600 # TYPE runtime_vul_images_scanned gauge runtime_vul_images_scanned 3 # TYPE runtime_vul_scan_errors_total counter runtime_vul_scan_errors_total 0 ``` | 指标 | 类型 | 标签 | 含义 | |---|---|---|---| | `runtime_vul_severity_count` | gauge | `image`、`severity` | 最近一次扫描中每个 image 和严重级别发现的漏洞。完整的每个 image 的集合会在每个周期被原子性地替换,因此不再运行的 image 会消失 —— 没有过时的数据系列。 | | `runtime_vul_last_scan_timestamp_seconds` | gauge | – | 上次完成扫描周期的 Unix 时间戳。使用 `time() - runtime_vul_last_scan_timestamp_seconds` 对陈旧度进行告警。 | | `runtime_vul_images_scanned` | gauge | – | 上个周期扫描的 image 数量。 | | `runtime_vul_scan_errors_total` | counter | – | 自启动以来每个 image 扫描错误的单调计数。 | 该展示是基于标准库 HTTP server 手动渲染的 —— 任何一个 image 都没有引入客户端 库,从而使 distroless Go binary 和 经过强化的 Alpine Python image 保持无依赖状态。 ### 保护 endpoint 保护是**分层且可选的** —— 组合适合您抓取路径的层级。 所有选项均在启动时应用(更改它们需要重启); 配置错误(例如不可读的证书)会被记录下来,扫描器会继续保持 endpoint 禁用状态运行, 而不是崩溃。 | 层级 | 配置 | 效果 | |---|---|---| | **Basic auth** | `metrics_basic_auth_user` + `metrics_basic_auth_password` | 没有有效的 HTTP Basic 凭据,`/metrics` 返回 `401`(以恒定时间进行比较)。 | | **TLS** | `metrics_tls_cert_file` + `metrics_tls_key_file` | 提供 HTTPS 服务。当抓取器设置 `insecure_skip_verify` / `tls_config.insecure_skip_verify: true`(“tls verify=false”)时,自签名证书即可工作;CA 签名的证书允许抓取器完全验证该证书。 | | **双向 TLS** | 添加 `metrics_tls_client_ca_file` | 要求并验证由给定 CA 签名的客户端证书(`RequireAndVerifyClientCert`) —— 最强的一级。 | ``` # 示例:TLS + Basic auth(certs 以只读方式挂载在 /config 下) metrics_basic_auth_user: "prometheus" metrics_basic_auth_password: "change-me" metrics_tls_cert_file: "/config/tls/metrics-cert.pem" metrics_tls_key_file: "/config/tls/metrics-key.pem" # metrics_tls_client_ca_file: "/config/tls/client-ca.pem" # 为 mutual TLS 添加 ``` 一个匹配的 Prometheus 抓取配置: ``` scrape_configs: - job_name: runtime-vul-detect scheme: https basic_auth: { username: prometheus, password: change-me } tls_config: { insecure_skip_verify: true } # or `ca_file:` for full validation static_configs: [ { targets: ["127.0.0.1:9090"] } ] ``` `./verify_metrics.sh [python|go|all]`(仓库根目录)是一个集成检查, 它会启用所有三个层级启动一个变体 —— 生成一个一次性的 PKI —— 并断言 endpoint 强制执行 Basic auth、TLS 和双向 TLS。所有生成的 材料都位于 `/config/tls/` 下(被 gitignore 忽略)并在退出时删除。 ## 技术评估 ### Distroless 基础 image Google 的 `gcr.io/distroless/python3-debian12` 对于此项目而言,**不能作为 `python:3.13-alpine` 的直接替代品**。主要障碍: | 问题 | 详情 | |---|---| | Python 版本 | Distroless 固定在 Python 3.11(Debian 12 package)。不存在官方的 3.12/3.13 变体([issue #1703](https://github.com/GoogleContainerTools/distroless/issues/1703))。 | | ctypes 缺失 | 基础的 `python3-debian12` image 不包含 ctypes,而 Cython 编译的 `.so` 文件可能依赖于此。`python3-plus` 变体添加了它,但也添加了 shell。 | | musl vs glibc | 我们目前使用的 Alpine 基础使用 musl libc。针对 musl 编译的 Cython `.so` 文件无法在 glibc(Debian)环境中运行 —— 需要全面重新构建。 | **结论**:Distroless Python 将需要将整个构建链从 Alpine/musl 切换到 Debian/glibc,固定到 Python 3.11,并验证 Cython 的兼容性。如今,通过 Cython 剥离 `.py` 源码并利用 Alpine 极简的攻击面,已经部分实现了安全收益(无 shell、无 package 管理器)。等 distroless 官方支持 Python 3.13 时再作考虑。 ### Go 迁移 用 Go 重写 orchestrator **具有长远的重大意义,但在今天并非迫在眉睫**。 **Go 的具体好处:** | 维度 | Python(当前) | Go(未来) | |---|---|---| | Container image | ~250 MB | ~30–50 MB(单一静态 binary) | | 空闲时内存 | ~60–80 MB | ~8–15 MB | | 启动时间 | ~500 ms | ~20 ms | | 交叉编译 | 需要单独的环境 | `GOOS=linux GOARCH=arm64 go build` | | 运行时依赖 | 需要 Python 解释器 | 无(静态链接) | **Trivy Go 库**:Trivy 暴露了内部 package(`pkg/commands/artifact`、`pkg/fanal/artifact/image`),可以直接调用而无需子进程,从而消除了每次扫描的进程 fork 和 DB 重新加载的开销。但是,Trivy 的内部 API 并未被声明为稳定的 —— 升级需要修改代码。一种更安全的方法是使用 Trivy 的 `--server` 模式(gRPC),它提供了一个稳定的接口,同时将 Trivy 保留为一个独立的进程。 **实际工作量**:一个通过 shell 调用 Trivy binary 的最小化 Go 重写(等同于今天 Python 的行为)需要 1–2 周。将 Trivy 作为 Go 库嵌入,并进行适当的 DB 缓存和 gRPC server 集成需要 4–8 周。子进程的方法风险较低,并能跟上 Trivy CLI 契约的步伐。 **建议**:在添加 Kubernetes 支持时进行迁移 —— Go 的 `client-go` 和 Kubernetes informer 模式在适应 cluster 级别的工作负载监控方面,明显比 Python 的 `kubernetes` 客户端更具优势。 ## 路线图 | 功能 | 状态 | |---|---| | 带有 JSON 日志输出的核心 cron 扫描 | 已完成 | | `docker ps -a` image 发现(运行中 + 已停止) | 已完成 | | 具有启动重复检测的多文件忽略 | 已完成 | | 配置驱动的时区 (IANA) | 已完成 | | Cython 编译的生产级 image(runtime 无 `.py` 源码) | 已完成 | | 带有注入 logger 的单元测试套件(无需 patching) | 已完成 | | Go 实现 —— distroless image,标准库 Docker 客户端(无 SDK) | 已完成 | | Docker socket 代理 —— 仅限 GET 白名单,内部 bridge 网络,无直接 socket 挂载 | 已完成 | | Prometheus `/metrics` endpoint —— 作为 gauge 的严重级别计数 | 已完成 | | 针对新 CRITICAL 发现的 Webhook / 警报 | 已完成(可选) | | 私有 Registry 支持(`TRIVY_USERNAME` / `TRIVY_PASSWORD`) | 已计划 | | 文件更改时热重载配置,无需重启 container | 已完成 | | image 拉取时的事件驱动扫描(订阅 Dockerevents` API) | 已完成(可选) | | `scan_result` 日志行中的 Docker Compose 项目/服务标签 | 已计划 | | container 重启间的扫描状态持久化(消除重启时的 webhook 误报) | 已计划 | | 多架构 image 构建(`linux/amd64` + `linux/arm64`) | 已计划 | | Kubernetes 支持(监控 Pod,通过 k8s API 扫描 image) | 已计划 | | 使用 Trivy server 模式的 Go 重写(降低每次扫描的开销) | 评估中 | ## 针对新发现的 Webhook 警报 默认情况下,扫描器是**仅检测模式** —— 每个周期都会输出完整的发现集合 作为 JSON,对结果采取的行动(寻呼、开 ticket、Slack)被委派给 下游流水线(Alloy → Loki 告警规则,或 SIEM)。在此之上,一个 **可选的 webhook** 会对*新出现的* CRITICAL/HIGH 发现触发原生警报: 扫描器会在内存中保留上一周期为每个 image 看到的 CVE ID 集合, 将当前发现与其进行 diff,并仅 POST 自上次扫描以来的**新** CVE —— 因此,对于长时间运行的 image,新披露的严重漏洞会触发 通知,而无需先连接日志后端。 Webhook **默认禁用**(`webhook_enabled: false`)→ 行为与 今天的完全相同,零 HTTP。启用后,结果将与*现有的* `severity` 过滤器进行 diff (没有单独的严重级别键);**第一次**看到 image 时, 它的所有发现都被视为新的(首次扫描时全部告警)。交付是 尽力而为的,且绝不中断扫描循环:配置的 `webhook_timeout_seconds` 适用于每个请求,任何错误(超时、拒绝连接、非 2xx) 都会作为结构化的 `webhook_error` 事件记录下来,周期 继续进行。HTTP 仅使用标准库(无第三方依赖)。 `webhook_auth_token` 绝不会被记录。`webhook_*` 设置在启动时应用; 更改它们需要重启 container。 在 `config.yaml` 中设置 `webhook_enabled: true` 和 `webhook_url`,可选择加上 `webhook_auth_header` + `webhook_auth_token`(例如 `Authorization` / `Bearer …`)。 每个周期**每个受影响的 image** 会发送一个 POST,仅当该 image 有新 发现时才发送。 ``` POST Content-Type: application/json : # only when both are configured ``` ``` { "timestamp": "2026-06-06T12:00:00Z", "event": "new_vulnerabilities", "image": "python:3.8-slim", "image_id": "sha256:abc123...", "containers": ["a1b2c3d4e5f6"], "new_vulnerabilities": [ { "id": "CVE-2021-1234", "package": "openssl", "version": "1.1.1k-r0", "fixed_version": "1.1.1l-r0", "severity": "CRITICAL", "title": "Memory corruption in openssl", "target": "python:3.8-slim (debian 11.7)" } ], "new_severity_counts": { "CRITICAL": 1, "HIGH": 0 } } ``` 每个漏洞对象的形状与 `scan_result` 日志行中的 `vulnerabilities[]` 条目完全相同。 `new_severity_counts` 仅计算**新** 漏洞,并且始终包含 `CRITICAL` 和 `HIGH` 键(一个存储桶可以是 `0`)。Python 和 Go 变体产生逐字节完全相同的配置键和 POST 正文。 `./verify_webhook.sh [python|go|all]`(仓库根目录)是一个集成检查,它会针对故意留有漏洞的测试目标启用 webhook 并启动一个 变体 —— 启动一个一次性的接收器 —— 并断言包含 至少一个 CRITICAL/HIGH 级别的 `new_vulnerabilities` POST 被送达,然后使用 `webhook_enabled: false` 重新运行并 断言没有 POST 到达。所有生成的材料都位于 `/config/webhook/` 下(被 gitignore 忽略)并在退出时删除。 ## 事件驱动扫描 cron 计划回答了“我*已经*在运行的东西最近有新的漏洞吗?” —— 但是新拉取的 image 或在两次触发之间启动的 container 需要等待下一个 周期才会被扫描。**事件驱动扫描**关闭了那个窗口:一个 **可选的** listener 订阅 Docker `/events` 流并对两种 事件类型做出反应 —— 一个 **image 拉取**(`type=image, action=pull`)和一个 **container 启动**(`type=container, action=start`)。对于 container 启动,image ref 直接 取自事件 payload(`Actor.Attributes.image`);无需进行额外的 Docker API 调用。 每个符合条件的事件都会触发**仅针对该 image 的定向扫描** —— 而不是对每个 container 进行全面重扫。定向扫描会重用常规的按 image 处理路径, 因此它会输出相同的 `scan_result` 日志行,为 webhook 的新发现 diff 提供数据,并 更新该 image 的按 image 划分的严重性指标。它有意**不**触碰 周期级别的记录:health 标记、`runtime_vul_images_scanned` 和 `runtime_vul_last_scan_timestamp_seconds` 属于 cron 周期。事件驱动 扫描因此**作为 cron 计划的补充 —— 它并不能替代它**。 突发请求会被**去抖动(debounced)**:在事件持续到达时观察到的 image ref 会被 收集到一个集合中,只有当流空闲 达到 `events_debounce_seconds`(默认为 5)时,每个**不同的** ref 才会触发一次扫描。一个启动了来自三个 image 的十个 container 的 `docker compose up` 操作 将导致三次定向扫描,而不是 十次。适用与 cron 路径相同的排除规则 —— 遵循 `ignore.yaml` 的 image 模式 并且扫描器绝不会扫描自己的 image。Trivy 执行在 内部是串行化的,因此事件扫描和并发的 cron 周期绝不会 同时针对共享缓存运行 Trivy。如果 `/events` 流断开 (daemon 重启、代理故障),listener 会记录结构化的 `event_stream_error` 并在较小的退避后重新连接。 该 listener **默认禁用**(`events_enabled: false`)→ 完全没有 打开 `/events` 连接,与今天纯粹的 cron 行为完全一致。在 `config.yaml` 中启用它: ``` events_enabled: true events_debounce_seconds: 5 # idle window before coalesced refs are scanned ``` `events_*` 设置在启动时应用;更改它们需要重启 container。由于扫描器仅通过仅支持 GET 的 socket 代理访问 Docker,因此代理的 `-allowGET` 白名单也必须允许事件 endpoint —— 每个变体附带的 `docker-compose.yml` 都包含 `events` 条目(包括 Python SDK 使用的基于 API 版本的 `/v1.NN/events` 形式和 Go 客户端使用的裸 `/events` 路径);如果没有它,流将被阻止,listener 将记录重连错误。 ## 参考 - [Trivy](https://github.com/aquasecurity/trivy) — 为每次扫描提供动力的漏洞扫描器 - [Trivy 忽略文件 schema](https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/) — 完整的 `.trivyignore.yaml` 参考 - [Grype](https://github.com/anchore/grype) — 替代的基于 Go 的扫描器 (Anchore) - [NVD — 国家漏洞数据库](https://nvd.nist.gov/) — 带有 CVSS 分数的权威 CVE 源 - [CVE 计划](https://www.cve.org/) — CVE 编号机构和完整的 CVE 列表 - [CVSS v3.1 规范](https://www.first.org/cvss/v3.1/specification-document) — 严重性分数的计算方式 - [IANA 时区数据库](https://www.iana.org/time-zones) — `timezone` 配置键的有效时区字符串 - [Grafana Alloy](https://grafana.com/docs/alloy/latest/) — 日志收集和流水线 - [APScheduler](https://apscheduler.readthedocs.io/) — 内部使用的 cron scheduler - [distroless Python](https://github.com/GoogleContainerTools/distroless) — 最小化的 container 基础 image(未来考虑)
标签:Docker, EVTX分析, GET参数, OpenTelemetry, PB级数据处理, 安全运维, 安全防御评估, 定时任务, 日志审计, 日志收集, 请求拦截, 逆向工具