4. SBOM 生成策略:构建时、运行时与 CI/CD 集成
SBOM 怎么生成?什么时候生成?这是很多团队刚开始搞软件供应链安全时最常问我的问题。说实话,没有银弹。不同的场景、不同的安全诉求,决定了你该用哪种策略。我这些年踩过的坑告诉我:选对生成时机,比选对工具更重要。
今天咱们就掰开揉碎,聊聊三种主流策略:构建时生成、运行时生成,以及把它们塞进 CI/CD 流水线 的实战方法。
4.1 构建时生成策略
构建时生成,说白了就是在你编译、打包、构建镜像的那一刻,把依赖清单给抓出来。这是我最推荐的方式,也是目前业界的主流做法。
为什么?因为构建时信息最全、最准。你的包管理器锁文件(比如 package-lock.json、go.sum、pom.xml)都在,依赖树是完整的,版本是确定的。这时候生成的 SBOM,可信度最高。
核心思路:在构建工具链中嵌入 SBOM 生成插件,把依赖元数据直接输出为 SPDX 或 CycloneDX 格式。
我给大家看个实际例子。用 syft 这个工具,在构建时生成 SBOM:
# 构建时直接生成 SBOM
syft packages dir:./my-app -o spdx-json > sbom.spdx.json
# 或者配合 Docker 构建
docker build -t my-app:latest .
syft my-app:latest -o cyclonedx-json > sbom.cdx.json
我个人习惯把 SBOM 生成命令直接写在 Dockerfile 里,做成多阶段构建的一部分。这样生成的 SBOM 和镜像是一一对应的,不会出现「这个 SBOM 对应哪个版本」的混乱。
小技巧:构建时生成的 SBOM 一定要包含 supplier(供应商)和 downloadLocation(下载地址)字段。这两个字段在漏洞溯源时特别有用。
4.2 运行时生成策略
运行时生成,嗯,这个策略有点意思。它不是在构建时抓依赖,而是在应用跑起来之后,去分析实际加载了哪些组件。
你可能会问:构建时不是已经抓过了吗?为什么还要运行时再搞一次?
原因很简单——构建时抓到的,不一定全被加载了。有些条件依赖、动态加载的库、或者通过反射调用的组件,构建时根本看不到。我遇到过好几次,构建时 SBOM 里没有的组件,运行时却出现了,结果一查是某个插件在启动时动态下载的。
运行时生成的常用手段:
- 容器镜像扫描:对运行中的容器做文件系统扫描,枚举所有二进制和库文件
- 进程内存分析:通过
/proc文件系统或 eBPF 技术,捕获实际加载的共享库 - 语言运行时钩子:比如 Java 的
-javaagent参数,拦截 ClassLoader 加载行为
# 运行时扫描容器
docker run -d --name my-app my-app:latest
syft docker:my-app -o spdx-json > runtime-sbom.spdx.json
# 或者用 trivy 扫描运行中的容器
trivy image --format cyclonedx my-app:latest > runtime-sbom.cdx.json
注意:运行时生成的 SBOM 有一个天然缺陷——它只能看到「当前时刻」加载的组件。如果应用有热加载、动态插件机制,你可能需要多次扫描才能覆盖全。
4.3 CI/CD 流水线集成策略
好了,前面两种策略各有优劣。但真正让 SBOM 发挥价值的,是把它们集成到 CI/CD 流水线里,实现自动化。
我见过太多团队,手动生成 SBOM,然后扔到某个共享文件夹里吃灰。这不行。SBOM 必须和你的构建、部署流程绑定,才能形成闭环。
下面是我总结的一套 CI/CD 集成方案:
| 流水线阶段 | SBOM 操作 | 推荐工具 |
|---|---|---|
| 代码提交 | 触发依赖扫描,生成增量 SBOM | syft, cyclonedx-gradle-plugin |
| 构建阶段 | 生成完整构建时 SBOM,附加到制品 | syft, trivy, docker sbom |
| 测试阶段 | SBOM 与漏洞库比对,执行门禁策略 | grype, trivy, dependency-check |
| 部署阶段 | 运行时 SBOM 生成,归档到 SBOM 仓库 | syft, trivy, in-toto |
来看一个 GitHub Actions 的实际配置片段:
name: SBOM Generation Pipeline
on:
push:
branches: [main]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate Build-time SBOM
uses: anchore/sbom-action@v0
with:
path: ./
format: spdx-json
- name: Scan for Vulnerabilities
uses: anchore/scan-action@v3
with:
sbom: sbom.spdx.json
fail-build: true
- name: Upload SBOM as Artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
这段流水线做了三件事:生成 SBOM、扫描漏洞、归档制品。其中 fail-build: true 就是门禁——如果发现高危漏洞,构建直接失败。
关键原则:SBOM 必须和制品「同生共死」。构建时生成,测试时验证,部署时归档。不要等到出事了才想起来找 SBOM。
4.4 三种策略的对比与选择
说了这么多,到底该选哪种?我给大家一个决策矩阵:
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 新项目,从零开始 | 构建时 + CI/CD 集成 | 成本最低,效果最好,一次配置长期受益 |
| 已有遗留系统 | 运行时生成 | 不需要改构建流程,直接扫描运行环境 |
| 合规审计要求高 | 构建时 + 运行时双保险 | 两份 SBOM 交叉验证,覆盖更全面 |
| 容器化部署 | 构建时(镜像层)+ 运行时 | 镜像层 SBOM 用于发布,运行时用于监控 |
我曾经在一个金融客户那里,他们要求每个上线版本都必须有 SBOM。我们一开始只做了构建时生成,结果有一次线上出了个漏洞,查 SBOM 发现那个组件根本没在构建依赖里——是运行时通过环境变量动态加载的。从那以后,我们改成了「构建时 + 运行时」双轨制,再也没出过类似问题。
我的建议:如果团队资源有限,优先做构建时生成 + CI/CD 集成。这是投入产出比最高的方案。等流程跑顺了,再考虑补充运行时扫描。
最后说一句:SBOM 生成不是一次性工作。你的依赖在变,应用在变,攻击面也在变。把生成策略自动化、常态化,才是软件供应链安全的正道。