招标文件的交付要求里新增了一条:提供软件物料清单(SBOM)。多数团队的第一反应是找一个开源生成器,在流水线里跑一遍,导出一份 JSON,随交付包一起发出去。清单交上去了,验收条款也划掉了。真正的考验往往出现在几周甚至几个月之后:客户发来一封邮件,提到某个刚公布的 CVE,只问一句——我们这个交付版本受影响吗?这时候能给出答案的,不是当初那份 JSON 文件本身。
讨论 SBOM 时最容易发生的混淆,是把三件相互独立的事当成一件事:清单、漏洞可利用性断言、完整性证明。它们由不同的规范描述,产生的时机不同,更新的频率也不同,混在一起谈会让团队在准备材料时分不清优先级。
SBOM 说明"你交付了什么组件"。它是一份结构化的成分表:组件名、精确版本、来源、许可证、依赖关系。它描述事实,不做判断。一份 SBOM 里即使列出了某个带有公开 CVE 的库,清单本身也不会告诉你这个 CVE 在你的构建里是否真的能被触发。
VEX(Vulnerability Exploitability eXchange)说明"已公布的 CVE 里哪些在你这份构建里真的可利用"。漏洞情报库判断的是"某组件的某个版本区间存在某缺陷",但缺陷存在不等于在你的产品里可利用——调用路径可能没走到那段代码,配置项可能默认关闭,发行版可能已经打过补丁。VEX 就是把这种判断写成机器可读的断言,常见状态包括不受影响、已修复、已知受影响、正在调查。
签名说明"这份清单没被改过"。它证明 SBOM 与某个构建产物出自同一方、内容未被替换。公开资料显示,欧盟《网络韧性法案》(Cyber Resilience Act,Regulation (EU) 2024/2847)并未强制要求 SBOM 签名,但签名能提供防篡改证据,也会在采购与审计环节增加说服力。常见做法包括 Sigstore/cosign 的无密钥签名与 PGP/GPG 签名。
把三者的分工摆清楚之后,招标文件里那句话的隐含含义就明确了:客户要的其实不是一份文件,而是三样东西的组合——一份覆盖完整的清单、一套在新漏洞出现时能快速给出可利用性判断的机制、以及能证明清单来源可信的凭据。缺少任何一样,"上星期那个 CVE 影响我们吗"这个问题都无法在当天被回答。
格式支持上也存在差异:CycloneDX 原生支持 VEX,可以直接在同一份文档里承载断言;SPDX 2.x 通常需要额外搭配 OpenVEX 或 CSAF 文档来表达这部分信息,SPDX 3.0 引入了按 profile 划分的结构,其中包含安全相关的 profile。这个差异会直接影响你后续查询的复杂度。
《网络韧性法案》附件一第二部分要求制造商编制并维护机器可读的软件物料清单,公开资料口径为至少覆盖产品的顶层依赖。这是法规给出的下限,不是能力建设的终点。
原因很直接:漏洞很少只发生在顶层依赖上。一个典型的 Java 或 Node.js 应用,直接声明的依赖可能只有几十个,加上传递依赖后是几百到上千个组件。真正被攻击者利用的组件,往往藏在第三层、第四层。如果清单只记录了顶层依赖,那么当某个 CVE 落在一个由顶层依赖间接引入的库上时,清单里根本没有这一行,比对也就无从谈起,最终只能回到"翻源码、问开发、临时装个工具扫一遍"的救火模式。
所以颗粒度的选择本质上是在选择"愿意承担哪种答不上来"。只列顶层依赖,等于承诺只能回答顶层组件的问题;列全传递依赖,才具备在漏洞通报到来时做定位的前提。行业常规做法与工具默认行为一致——syft(Anchore)与 cdxgen(OWASP)默认生成的就是包含传递依赖的完整清单,trivy(Aqua)同样可以输出两种格式下完整的依赖树。
还有一个经常被漏掉的层次:构建与运行环境自身的组件。容器基础镜像里的系统包、CI 构建机上的编译器与工具链、静态链接进去的第三方代码,这些都会随交付物一起到达客户现场。如果清单只统计应用层的语言依赖,那么操作系统层出现的问题同样不在可查询范围内。判断标准很简单:只要它会被打包进最终交付物,或者会参与生成最终交付物,就应该考虑纳入。
需要提醒的是,深度不是越深越好,而是要与"你打算回答什么问题"匹配。一个内部工具类组件,被静态链接但从未被调用,是否要单独成行、是否要在依赖关系图里标注可达性,需要在清单设计阶段就定好口径,否则清单会变成一张没有重点的长表。
把上面这些颗粒度上的取舍落到字段层面,可以得到一张对照表。它同时是内部规范的核心部分:每一列都可以直接变成流水线里的检查项和评审时的核对项。
| 清单要素 | 从哪里来 | 缺失会怎样 | 能回答什么问题 | 日常维护动作 |
|---|---|---|---|---|
| 组件名与精确版本 | 构建产物扫描结果或锁文件解析结果,取实际解析出的版本字符串 | 版本写成范围时只能给出"可能受影响"的模糊结论,答复无法闭环 | 这个交付版本里到底装的是哪一个具体版本 | 每次构建重新生成,版本字段不允许手工编辑 |
| purl 标识 | 生成器按包管理器类型自动推导,或在生成后补全 | 无法与漏洞数据的组件标识做自动匹配,比对退回人工核对 | 新公布的 CVE 命中的是不是我们用的这个包 | 统一包类型与命名空间口径,跨生态的同名组件做归并校验 |
| 供应商 / 来源方 | 包元数据的维护者字段,或从制品来源记录回填 | 无法区分同名不同源的组件,出问题时不知道该向哪一方确认 | 这个组件由谁发布、后续修复情报该跟谁走 | 私有源与镜像源的来源标注随清单一并留存 |
| 许可证标识 | 包元数据中的许可声明,配合许可证扫描补全标准标识 | 无法回答"这个组件能不能用于当前交付形态"的合规询问 | 交付物整体的许可义务边界在哪里 | 依赖升级后重新核对,许可证变更纳入变更评审 |
| 依赖关系图 | 依赖解析结果中的父子关系与可达路径 | 只知道命中了组件,排不出优先级,也定位不到引入位置 | 受影响的组件是从哪条依赖链进来的、调用路径是否可达 | 每次构建重建,依赖结构变化时同步复核可达性标注 |
| VEX 断言 | 安全团队调查后出具,或由持续比对平台生成并经人工确认 | 每次通报都从零调查,历史结论无法复用,工作量随版本数增长 | 已公布的 CVE 里哪些在这份构建里真的可利用、当前状态如何 | 漏洞确认后回写到对应版本记录,状态变化时更新并留痕 |
这张表里的六项不是并列的加分项,而是一条链:前五项决定能不能"找得到",最后一项决定能不能"答得准"。任何一项缺失,链条上就出现一个断点,而断点的代价会在漏洞通报到来的那一天集中体现。
两种主流格式的出身决定了它们的侧重。CycloneDX 由 OWASP 维护,从安全用例出发设计:1.6 版于 2024 年被 Ecma International 采纳为 ECMA-424;1.7 版于 2025 年 10 月发布,扩展到密码资产清单(CBOM)、知识产权可见性与数据来源。序列化支持 JSON、XML、Protocol Buffers。它原生支持 VEX 与依赖关系图,组件标识以 purl(package URL)为主。
SPDX 由 Linux 基金会维护,从许可证合规出发:SPDX 2.2.1 是 ISO/IEC 5962:2021;SPDX 3.0 于 2024 年发布,按 profile 划分,涵盖安全、许可、AI、数据集、构建等不同领域。许可证表达能力是它的传统强项,也是很多法务与合规团队的习惯语言。
法规层面,两种格式都没有被指定为唯一选择。《网络韧性法案》未指定必须使用的格式;美国 EO 14028 及 CISA 相关指南对两种格式均接受,NTIA 提出了 SBOM 的最小元素。真正给出具体版本门槛的是行业标准:德国联邦信息安全局(BSI)技术指南 TR-03183-2(公开资料显示 2.1.0 版为 2025 年 8 月)接受 CycloneDX 1.6 及以上或 SPDX 3.0.1 及以上,序列化为 JSON 或 XML。注意这里的版本要求是硬性的——生成的是 CycloneDX 1.4 或 SPDX 2.3,即使格式名字对得上,也不满足该指南的门槛。
关于互转,需要提前建立一个预期:格式互转会在边缘字段上丢信息。SPDX 的关系类型更丰富,转换到 CycloneDX 时这些关系语义难以完整保留;反过来,CycloneDX 的 services、compositions 与原生 VEX 内容在 SPDX 2.x 中没有对应位置,转过去就等于丢弃。公开建议是尽量直接生成目标格式,把转换当成一次性的单向动作,而不是日常流程里的常规步骤。
所以格式选择的真实判断依据不是"哪个更好",而是"你的下游要什么":需要提交给特定监管或采购方的,先确认对方指南里写的格式与最低版本;内部要做持续漏洞比对与可视化的,Dependency-Track 是较常部署的开源 SBOM 分析平台,它以 CycloneDX 为主,这种情况下直接生成 CycloneDX 能省掉一整层转换;法务与许可合规是主要驱动力的,SPDX 的表达能力更贴合。如果下游要求不明确,让生成器同时输出两种格式(syft 与 trivy 都支持)成本不高,但要明确其中一种是主格式、另一种只作提交用途,避免两份清单内容不一致。
一份清单能不能被查询,很大程度取决于它是在哪一步生成的。常见的错误是从源码仓库的依赖声明文件(package.json、pom.xml、requirements.txt)直接生成。问题在于这些文件记录的是"声明",不是"实际"——依赖解析的结果可能因锁文件状态、私有源版本、构建时参数而与声明不同。更稳妥的做法是在依赖解析完成、构建产物生成之后,针对构建产物本身生成:容器镜像就扫描镜像,二进制包就扫描包内容。这样清单描述的是真正会被交付出去的东西。
绑定关系必须在生成那一刻就写进去。每份清单都要能明确对应到唯一交付物:产品名、版本号、构建号、源码提交号、制品摘要值。推荐的命名规范里同时包含这些字段,并且在清单文档的元数据区块里记录制品哈希。缺少这层绑定,几个月后面对"你们 3.2.1 版本受影响吗"的询问,就无法确认手里的清单到底对应哪一次构建。
版本化则是第二个要求:每次构建生成一份新清单,清单与制品同生命周期、同样不可变,存放在同一处。不要把清单当成一份可以被覆盖更新的活文档,也不要把"最新版 SBOM"作为唯一留存——历史版本恰恰是回答客户历史版本问题时唯一可用的依据。
存储位置需要考虑三件事:留存周期要与产品支持周期对齐,因为支持期内的每个已交付版本都可能需要被查询;要有备份,清单丢了等于该版本的应答能力归零;要有访问控制,一份完整的组件清单等于把你产品内部用了什么、什么版本摊开给拿到它的人看,它本身属于需要管理的敏感信息。
自建生成器与制品归档通常要落在持续可用的基础设施上,而不是某台个人机器或临时目录。一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;自建生成器与制品归档通常要单独预留存储与备份位置。这类归档数据的增长是长期且单向的,规划时按"每个版本一份、只增不改"来估算容量,比按当前项目数量估算更贴近实际。
最后要指定责任人。清单生成、存储、查询、更新、对外答复,这五件事要落到明确的角色上。招标文件里那条要求真正的含义是:客户预期你在未来几年内都能就这个交付物应答,而不只是在签约时递上一份文件。
生成只是起点,比对才是把清单变成能力的那一步。一条完整的查询路径是这样的:
第一步,把 CVE 映射到受影响的组件标识与版本区间。公开漏洞数据给出的是"某组件的哪些版本受影响",通常以 purl 或 CPE 表达,版本以区间描述。
第二步,拿这个标识去清单里做匹配。这一步做的是标识与版本的比对,输入是清单里的 purl 与精确版本字符串,输出是命中或未命中。这里有个关键前提:版本必须是精确字符串而不是范围。如果清单里记录的是 "^1.2.0" 这样的声明范围,就无法判断实际构建落在 1.2.3 还是 1.4.0,比对结果只能停留在"可能受影响"。
第三步,看依赖关系图。命中的组件是直接依赖还是传递依赖、位于哪条调用链上、对应的代码路径是否可达。这一步决定的是优先级:一个位于传递依赖深处、调用路径不可达的组件,与一个直接暴露在入口处理逻辑里的组件,处理方式完全不同。
第四步,查 VEX 断言。看是否已有针对该 CVE 与当前构建的结论。如果有,直接采用;如果没有,进入调查流程并补充新断言。
第五步,落到修复决策并留痕。决策要记录:哪个构建、哪个组件、结论是什么、依据是什么、谁确认。这份记录连同 VEX 断言一起,才是真正发给客户的答复材料,原始 JSON 只是它的输入。
为什么手工对表格撑不住?三个原因。其一是数量级:几百个组件乘以每周新增的 CVE,人工比对不可能持续。其二是版本判断的复杂性:发行版维护方常常在不改变上游版本号的情况下反向移植安全补丁,仅凭版本字符串会误判为仍受影响;反过来,某些组件的版本号与实际修复状态不一致的情况也普遍存在。其三是变更频率:每发布一个新版本就产生一份新清单,人工维护的历史对应关系很快就会失真。Dependency-Track 这类平台的价值正在于把第二步到第四步做成可重复执行的流程,并保留每次结论的历史。
还需要划清一条边界:SBOM 本身不做漏洞发现。清单是输入,漏洞数据来源是另一套东西。反过来,单纯依赖镜像扫描而忽视清单,也会失去"这份答复是基于哪次构建"的追溯基础。两者是互补关系,不是替代关系。
清单过期通常不是因为有人删了它,而是因为产品变了而清单没跟着变。常见的触发条件有四类:
版本发布。每次构建生成新清单,这是最不容易漏的一类,因为它可以绑进流水线。
依赖变更。锁文件发生 diff、依赖升级、私有源里某个包被重新发布,都意味着组件集合变了。很多团队只在发版时生成,导致开发分支上的安全整改在清单里长期看不到。
漏洞确认。当某个 CVE 被确认影响某个已交付版本时,除了修复之外,还要补写 VEX 断言并回写到该版本对应的清单记录里,否则下一次面对同一个问题还要重新调查一遍。
周期性复核。即使产品没变,漏洞数据也在变。把已交付版本的清单定期与最新漏洞数据重新比对,是发现"旧版本里 newly disclosed 的问题"的唯一方式。这一步不需要重新生成清单,只需要用新数据重跑比对。
有几类失效特别隐蔽。基础镜像被重建:应用代码一行没改,但重新拉取的基础镜像里系统包版本变了,组件集合随之改变,而版本号没动,清单与制品已经不匹配。没有锁文件:依赖按范围解析,构建时间不同得到的组件不同,清单只在生成那一刻有效。静态链接与 vendored 代码:源码被复制进仓库后不再被依赖扫描覆盖,清单里看不到它,但它在二进制里。下游重新打包:交付物到达客户后被二次封装,此时应答范围需要事先界定清楚——你负责的是你交付的那一层。
要区分两个动作:重新生成是制品级的,每次构建做一次;重新比对是数据级的,可以在不重新生成的前提下反复执行。把这两件事混为一谈,要么会导致清单被反复覆盖、历史丢失,要么会导致漏洞数据更新了却没人重跑。
把顶层依赖当成全部依赖。前面说过,法规给的是下限。按下限交付,得到的也只能回答下限范围内的问题。验收能过,但被追问时答不上来。
把一次生成当成长期资产。在项目收尾阶段补一份清单,是很多团队的真实做法。它的用途仅限于签约那一刻。真正需要的是把生成嵌进流程,让它随每次构建自动产生。
把 SBOM 当成漏洞扫描器。清单里没有任何漏洞判断能力,它只是"成分 + 版本"。漏洞结论来自外部数据与可利用性分析。反过来说,只有扫描器而没有清单,也难以回答"某个历史交付版本当时装了什么"。
忽视构建环境自身的组件。基础镜像、构建工具、CI 运行器上的依赖,都可能进入最终产物或影响产物。清单覆盖范围的口径要覆盖到这一层,否则会出现"清单里没有,但二进制里有"的盲区。
还有一个次生问题:把格式转换当作兼容性方案长期使用。转换带来的字段丢失会累积,最终两份清单内容不一致,却没人能说清哪份是准的。
在把工具装进流水线之前,建议把下面这些问题先回答一遍,它们决定了后续能不能真的应答:
下游要什么?目标格式、最低版本、序列化方式、是否需要签名,先向客户或适用指南确认,再反推格式选择。
从哪里生成?优先针对构建产物而不是源码声明文件;确认生成器覆盖的语言与包管理器类型,syft、trivy、cdxgen 各有覆盖范围,npm 自 v10.2 起内置 npm sbom 命令(v10.2 于 2023-10 发布),可作为补充手段而非唯一来源,因为它的覆盖范围通常只限该语言生态自身的依赖树。
覆盖多深?明确顶层依赖之外是否包含完整传递依赖、是否包含操作系统层包、是否包含静态链接与 vendored 代码,并把口径写进内部规范。
字段齐不齐?对照最小元素的公开口径逐项检查:组件名、组件版本(精确版本字符串而非范围)、供应商或来源方、许可证标识、purl 标识、已知漏洞或 VEX 断言。缺任何一项,比对链条上就有一个断点。
和哪个制品绑定?命名规范、元数据里的制品摘要、存放位置,三者要一起定。
怎么版本化?每次构建一份、不可变、与制品同生命周期、历史版本全部保留。
谁来查询?比对工具与漏洞数据来源、执行频率、负责人;已交付版本是否纳入周期性重跑范围。
VEX 谁维护?断言由谁出具、依据什么、多久复核一次、如何回写。
对外答复是什么形态?不是原始 JSON,而是"受影响/不受影响 + 依据 + 处理措施 + 确认人"的结论记录。
清单与实际交付物脱节。从源码声明生成、或用旧构建产物生成,清单看起来完整,但描述的东西没被交付出去。这种情况下任何比对结论都不可信。
只有清单没有查询机制。清单存进了归档目录,但没有工具、没有责任人、没有比对流程。漏洞通报到来时仍然靠人工翻文件,清单退化成一堆存档。
只有清单没有 VEX。每次通报都要从零调查,历史结论无法复用,工作量随交付版本数量线性增长。
版本字段是范围而非精确值。比对只能给出"可能受影响",答复无法闭环。
历史版本清单被覆盖或未留存。客户问的是半年前交付的版本,而那份清单已经不在了。
依赖关系图缺失。能知道命中了哪个组件,却无法判断它处于哪条路径、是否可达,优先级排不出来,修复决策只能靠拍脑袋。
这几种情形有一个共同点:文件都在,能力没有。判断标准只有一条——当一条新的 CVE 通报进来,你能否在当天说清楚它影响了哪些已交付版本、每个版本里是否真的可利用、什么时候修、谁来确认。能,清单就是能力;不能,它就只是一张会过期的快照。
扫描报告是某个时间点对某个目标跑一遍的结果,回答"扫到了什么问题";SBOM 是交付物的成分记录,回答"这个东西由什么组成"。扫描报告依赖目标处于可运行状态,且结果随扫描器版本与规则库变化;SBOM 与具体构建绑定,不随扫描器变化。二者互补:扫描发现当前问题,清单提供历史追溯与精确定位的基础。客户要清单,通常是因为他们需要自己持续比对,而不是想要你某次扫描的输出。
按下游要求定。提交给有明确格式门槛的采购方或监管方时,先查对方指南写的最低版本——例如 BSI TR-03183-2 接受 CycloneDX 1.6 及以上或 SPDX 3.0.1 及以上,序列化为 JSON 或 XML。内部做持续比对且使用 Dependency-Track 的,以 CycloneDX 为主更省事。法务与许可合规驱动的,SPDX 的表达更贴合。两个都给是可以的,但必须指定一个主格式作为查询与维护依据,另一个仅用于对外提交,避免两份内容漂移。
体积通常不是问题,几百到上千个组件的 JSON 文件量级仍在可管理范围,压缩后更小。真正需要管理的是可读性——人不会直接读清单,人读的是比对结果。所以正确的做法不是精简清单,而是把"人要看的东西"与"机器要比对的东西"分开:清单保持完整,呈现层按风险排序。反过来,为了"不太臃肿"砍掉传递依赖,等于主动放弃对绝大多数漏洞位置的定位能力。
规则是:每次构建生成一份新清单,而不是按固定周期刷新一份。重新生成与重新比对是两件事——前者跟随制品,后者跟随漏洞数据。即使产品长期不发版,也应定期用最新漏洞数据对已交付版本的清单重跑比对;这段时间内清单本身不需要重新生成,除非制品发生了变化。
分两条线走。当前线上版本优先:如果还能拿到当时的构建产物(镜像或安装包),直接对产物生成,得到的清单能真实反映现状;如果产物已不可得,只能从锁文件与构建记录重建,并明确标注这是重建结果而非原始记录。历史版本则按保留下来的制品逐个补,补不了的就如实标注缺失范围。同时把生成器接进流水线,保证新版本从某一天起自动带清单,不再产生新的历史欠账。
按交付物给,并且要说清楚边界。客户需要的是"你交付的这个软件由什么组成",不是整个运行环境所有机器的清单。正确做法是针对交付物生成一份清单,在文档元数据里写明产品名、版本、构建号与制品摘要;如果交付形态是容器镜像,基础镜像里的系统包属于交付物的一部分,应当包含。同时建议明确下游二次封装后的责任边界,避免后续把不属于你的组件也纳入应答范围。
公开资料显示,《网络韧性法案》并未强制要求 SBOM 签名,所以从条文角度通常不是硬性门槛。但签名解决的是另一个问题:证明清单确实来自你、内容未被替换。在采购与审计环节,签名过的清单更容易被接受,也能避免"清单与制品是否同源"的争议。常见做法包括 Sigstore/cosign 的无密钥签名与 PGP/GPG 签名,前者不需要管理长期私钥,运维负担相对低一些。
把上述流程跑起来,需要的资源大致分三块:构建环节生成清单带来的额外计算与存储开销、清单与制品的长期归档、以及持续比对所需的分析平台。前两项随发版频率线性增长,第三项可以按部署方式二选一——开源平台自建,或使用托管服务。自建路线上,服务器、存储与带宽的选型要看留存周期与并发构建量,一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。真正的成本大头往往不在硬件,而在"谁负责答复"这件事上的人力投入,这一点在规划阶段最容易被低估。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品