4. SBOM 生成策略:构建时、运行时与 CI/CD 集成

SBOM 怎么生成?什么时候生成?这是很多团队刚开始搞软件供应链安全时最常问我的问题。说实话,没有银弹。不同的场景、不同的安全诉求,决定了你该用哪种策略。我这些年踩过的坑告诉我:选对生成时机,比选对工具更重要。

今天咱们就掰开揉碎,聊聊三种主流策略:构建时生成、运行时生成,以及把它们塞进 CI/CD 流水线 的实战方法。

SBOM 生成策略全景图 构建时生成 依赖解析 + 元数据捕获 运行时生成 动态分析 + 实际加载清单 CI/CD 集成 流水线自动化 + 门禁 关键能力 • 包管理器锁文件解析 • 构建工具插件集成 • 版本哈希精确匹配 关键能力 • 容器镜像层扫描 • 动态链接库枚举 • 条件依赖发现 关键能力 • 构建阶段自动触发 • 门禁策略绑定 • 制品仓库归档 💡 最佳实践:构建时生成作为基线,运行时生成作为补充,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 生成不是一次性工作。你的依赖在变,应用在变,攻击面也在变。把生成策略自动化、常态化,才是软件供应链安全的正道。