3. 固件依赖清单与SBOM
固件依赖治理的第一步不是扫描漏洞,而是先回答一个更基础的问题:这个固件里到底有什么。SBOM(Software Bill of Materials,软件物料清单)就是对这个问题的结构化回答。它把固件镜像中可识别的第三方组件、版本、来源、依赖关系以及必要的哈希标识,组织成机器可读的清单,使后续的漏洞匹配、许可证审查和供应链追溯有据可依。
SBOM 标准:SPDX 与 CycloneDX 的取舍
当前固件场景中最常用的两个标准是 SPDX 和 CycloneDX。SPDX 由 Linux 基金会维护,字段体系完整,强调许可证信息和文件级溯源,适合需要严格合规审计的场景;CycloneDX 由 OWASP 社区推动,结构更轻量,原生支持组件依赖图、漏洞引用和服务清单,在 CI/CD 流水线中集成更顺手。两者都支持 JSON 输出,也都能表达组件名称、版本、供应方、唯一标识(如 PURL)和哈希。
选择时不必追求“唯一正确”。如果团队已有许可证合规流程,SPDX 的许可证表达式和文件归属更自然;如果重点是漏洞响应和依赖关系追踪,CycloneDX 的 dependencies 与 vulnerabilities 字段更直接。实际项目中也可以同时输出两种格式,用 SPDX 做归档,用 CycloneDX 做流水线内消费。
生成方法:从源码、构建到镜像的三层采集
固件 SBOM 的生成不能只依赖单一手段。可靠的做法是分层采集,再合并去重:
- 源码层:解析包管理器清单,如
package-lock.json、go.mod、Cargo.lock、requirements.txt,得到直接依赖与传递依赖的精确版本。 - 构建层:在编译过程中记录链接进来的静态库、头文件来源和编译选项,尤其对 C/C++ 固件,很多组件不会出现在包管理器里。
- 镜像层:对最终固件镜像做文件系统扫描,识别二进制中的版本字符串、许可证文件和已知组件指纹,补足源码层遗漏的预编译库。
三层结果合并时,以 PURL 或“组件名+版本+哈希”作为主键去重。若同一组件在源码层和镜像层版本不一致,应保留两条记录并标记冲突,因为这种不一致本身就是重要的风险信号。
固件场景适配:与通用 SBOM 的差异
固件 SBOM 和普通应用 SBOM 有四个明显差异。第一,固件常包含大量静态链接的 C 库,组件边界不清晰,需要借助符号表和字符串扫描辅助识别。第二,固件往往没有运行时包管理器,无法在设备上动态查询依赖,SBOM 必须在构建阶段固化。第三,固件镜像通常经过裁剪、压缩和签名,扫描前需要先解包、解压并校验签名,否则会漏掉被压缩的组件。第四,固件生命周期长,同一型号可能多年不更新,SBOM 需要与设备型号、硬件版本、固件版本三者绑定,而不是只绑定一次构建。
工具链与最小可行实践
工具链不必一次铺满。一个可落地的最小组合是:源码层用 syft 或语言原生工具生成 CycloneDX;构建层用编译数据库 compile_commands.json 提取链接库;镜像层用 syft dir: 扫描解包后的根文件系统;最后用 cyclonedx-cli merge 合并并输出统一 SBOM。若需要 SPDX,可用 syft -o spdx-json 直接转换。
下面给出一个最小可行流水线示例,假设固件已解包到 ./rootfs,源码在 ./src:
# 1. 源码层生成 CycloneDX
syft ./src -o cyclonedx-json=src.cdx.json
# 2. 镜像层扫描解包后的根文件系统
syft dir:./rootfs -o cyclonedx-json=rootfs.cdx.json
# 3. 合并两份清单
cyclonedx-cli merge --input-files src.cdx.json rootfs.cdx.json \
--output-file firmware.cdx.json
# 4. 校验并输出组件数量
cyclonedx-cli validate --input-file firmware.cdx.json
jq '.components | length' firmware.cdx.json
执行后应重点观察三个结果:组件总数是否与预期量级相符;是否存在同一组件多版本并存;是否有组件缺少版本或 PURL。若组件总数明显偏低,通常说明镜像未完全解包或扫描路径遗漏;若多版本并存,需要回到构建层确认链接顺序和静态库来源。
SBOM 生成后,最小可行的验收标准是:组件总数稳定可复现、关键组件版本可追溯、冲突项有明确标记、SBOM 与固件版本一一对应。做到这四点,后续的漏洞匹配和依赖治理才有可靠输入。若一开始就追求字段完备而忽略可复现性,SBOM 很快会变成一份无人维护的静态文档,失去治理价值。