软件供应链安全这件事真正让人难受的地方不在于概念多而在于出问题的时候你根本不知道问题从哪儿来。前两年我参与过一次内部排查某个线上服务被扫出一个高危组件业务代码里翻了个底朝天也没找到最后追到基础镜像的某个构建工具链依赖上——那个包从来没被业务代码调用过却在镜像里躺了半年。这类事情碰到两三次之后你自然会开始认真对待 SBOM、SCA、SLSA 这几个词也会开始琢磨怎么把它们真正塞进 CI/CD 流水线而不是停在 PPT 上。这篇就按我自己的落地顺序讲先把三个概念各自管什么、不管什么说清楚再讲流水线里该在哪些卡点做哪些事最后把踩过的坑和参数选择一并摊开适合正在做供应链安全治理的研发、安全和平台工程同学对照参考。1. 三个名词先掰开各自管什么、不管什么很多人一上来就把 SBOM、SCA、SLSA 混成一件事觉得装个扫描器就算做完了供应链安全。实际这三者解决的是完全不同层面的问题混着做最后一定是三件事都没做扎实。1.1 SBOM给软件开一张能对上号的“配料表”SBOM 是 Software Bill of Materials 的缩写直译就是软件物料清单。我习惯用食品配料表来类比一包饼干背面写着用了小麦粉、植物油、食用盐SBOM 写的就是你这个软件用了哪些组件、什么版本、从哪来、什么许可证、谁依赖谁。它本身不判断好坏只负责记录事实。这里有个关键选择要先定下来SBOM 是源码级生成还是制品级生成。源码级是扫描依赖清单文件比如package-lock.json、go.mod、pom.xml、requirements.txt速度快、能精确到声明关系但有个盲区——构建过程本身会引入东西比如基础镜像的操作系统包、编译器运行时、打包插件带进来的传递依赖这些在源码清单里根本看不到。制品级是直接扫最终产物容器镜像就扫镜像层二进制就扫二进制覆盖全但慢而且有些静态链接的语言拿不到准确的版本号。我的建议是两个都做用途分开源码级放在提交和合并阶段给开发快速反馈制品级放在发布阶段作为归档和审计依据。归档的那份必须是制品级因为它和你实际部署的东西是同一个哈希能对得上。格式上主流就两个SPDX 和 CycloneDX。SPDX 偏合规审计字段全、规范严CycloneDX 偏安全和工具链集成体积小、生态支持广。选哪个不重要重要的是组件必须带 PURL。PURL 是组件的统一标识形如pkg:maven/org.apache.xxx/yyy1.2.3有了它不同工具之间的 SBOM 才能互通SCA 扫描器才能准确匹配漏洞库。我见过不少团队生成的 SBOM 只有组件名和版本号没有 PURL 也没有 CPE拿去喂扫描器匹配率直接掉一半。还有一点容易忽略SBOM 要记录依赖关系不只是组件列表。扁平列表在排查“这个高危包是谁引进来的”时几乎没用你必须能顺着dependsOn往上追到直接依赖。这个字段在 CycloneDX 里是dependencies在 SPDX 里是relationships生成时务必打开。1.2 SCA拿着配料表去对照“问题清单”SCA 是 Software Composition Analysis软件成分分析。它干的事说白了就是拿 SBOM 里的组件去比对漏洞库看有没有已知问题。工作原理分三步解析组件标识PURL 或 CPE、到漏洞库里查、按版本区间判断是否受影响。这里必须先泼一盆冷水SCA 的准确性高度依赖组件标识的准确性。同样是log4j-core版本号写成2.14.1能匹配上写成2.14或者干脆丢了版本号匹配就会失败或者误判。所以我在上一节反复强调 PURL就是因为这个环节一旦标识错了后面全是无用功。SCA 的误报主要来自三个地方。第一是版本区间判断过宽很多漏洞库对某些包只写了“小于某版本受影响”你用的是打了补丁的重新打包版本版本号没变照样报。第二是不可达代码组件在依赖树里但你的代码从来没调用到那条路径风险实际上很低可扫描器不知道。第三是构建工具依赖测试框架、代码生成器、打包插件带进来的包被算进了运行时依赖这类误报在 Java 项目里特别多。漏报同样存在。私有组件、内部二方库不会出现在公共漏洞库里漏洞库同步有延迟新披露的漏洞可能几天后才进库传递依赖如果解析不完整深层的包根本不在扫描范围内。所以我一直认为 SCA 的产出不应该只有“通过/不通过”二元结果而是要给每个发现标注可利用性和可达性让人来判断优先级。CVSS 分数只能作为初筛真正决定先修哪个的是这个包在不在运行时路径上、有没有公开的利用代码、暴露面有多大。1.3 SLSA证明这锅菜是同一批人按同一流程做的SLSA 读作“salsa”是 Supply-chain Levels for Software Artifacts 的缩写。它和前两个不是一类东西——SBOM 和 SCA 关注“用了什么、有没有问题”SLSA 关注的是“这个产物是不是按预期流程、在可信环境里构建出来的”。为什么需要这个因为供应链攻击有两个入口。一个是源码恶意提交混进主干另一个是构建环节构建机被入侵、构建脚本被篡改、依赖在构建时被替换。前一个问题靠代码评审和分支保护解决后一个问题就得靠 SLSA 这套机制。它的核心产物叫来源证明provenance是一份结构化声明记录这次构建用了哪个仓库的哪个 commit、在哪个构建环境里跑的、构建参数是什么、输入材料的哈希是多少。这份证明会被签名签名之后就无法伪造或篡改。SLSA 规范目前把构建轨道分到 L3。L1 要求构建过程自动化并且产出 provenanceL2 要求使用托管构建服务provenance 经过签名L3 要求构建环境加固provenance 由构建平台生成、使用者无法伪造。早期版本还有 L4强调双人评审和可复现构建后来在规范演进中被合并调整了你如果看到旧资料写“四级”那是历史版本的说法不用纠结。落地 SLSA 最容易走进的误区是把它当成一个可以“买”的东西。它不是产品是规范需要你用具体工具去实现provenance 用 in-toto 的 attestation 格式承载签名可以用密钥对也可以用基于身份令牌的短期证书方案验证端用策略引擎做准入判断。这套链路缺任何一环等级都上不去。1.4 一张表看清三者的边界与依赖关系维度SBOMSCASLSA解决的问题软件里有什么这些东西有没有已知漏洞产物是不是可信构建出来的输入源码依赖清单或制品SBOM 或依赖树构建过程的元数据与材料哈希输出组件清单与依赖关系漏洞列表与优先级签名后的来源证明消费方安全团队、合规审计、下游客户研发团队、安全运营部署准入、下游集成方是否阻断流水线一般不阻断按策略阻断部署阶段阻断成熟度高格式标准化高但误报治理是长期活中工具链仍在演进三者是递进关系SBOM 是数据基础SCA 消费 SBOM 做判断SLSA 保证 SBOM 和产物本身没被掉包。缺了 SBOMSCA 就是无源之水缺了 SLSA你手里的 SBOM 可能描述的根本不是部署上去的那个东西。2. 落地前的设计先定风险模型再定工具链我见过太多团队一上来就比价采购工具结果买回来发现和自己的流水线根本接不上或者扫出来的东西没人处理。工具是最后一步前面得先把风险模型和卡点位置想清楚。2.1 从“你最怕什么”倒推要做的三件事供应链风险粗分四类对应不同的技术手段。第一类是引入已知漏洞组件这是最高频的对应 SCA。第二类是许可证冲突尤其是对外交付的产品里混进了传染性许可证的库对应 SBOM 的许可证字段分析。第三类是依赖投毒和仿冒包攻击者发布一个名字和知名库差一个字母的包或者劫持维护者账号发新版本这类靠 SBOM 加包源校验、依赖白名单来防。第四类是构建产物被篡改从源码到制品的链路上任何一个环节被动手脚对应 SLSA 加签名验证。你要做的是给这四类排个序。如果是面向企业客户交付的产品许可证和 SBOM 交付往往是硬性合同要求优先级最高。如果是互联网服务漏洞响应速度是第一位的SCA 要先跑起来。如果你们的制品会被下游大量集成那签名和 provenance 就是刚需。排序不同落地顺序完全不同别照搬别人的方案。2.2 把三个动作嵌进流水线的哪几个卡点流水线上有四个天然卡点我按投入产出比从高到低排提交与合并阶段做增量检查。只扫这次改动新增或升级的依赖判断是否命中高危、是否在禁用清单里。这个阶段必须快超过一分钟开发就有意见所以不做全量、不做镜像扫描。构建完成阶段做全量 SBOM 生成和 SCA 扫描。这时候依赖树是完整的扫描结果最准耗时可以放宽到几分钟但要有缓存机制漏洞库不要每次重新下载。发布阶段做签名和 provenance 生成。产物一旦推送制品库就按摘要签名同时把 SBOM 作为 attestation 一起附上。部署阶段做准入验证。检查镜像有没有签名、provenance 里的构建来源是不是白名单内的流水线、源码 commit 是不是来自受保护分支。有个细节必须提醒部署验证要按镜像摘要而不是标签。标签是可变的同一个latest今天指向这个镜像明天指向那个按标签验证等于没验证。2.3 工具选型的几个硬指标选型别只看功能列表我一般盯这几条指标为什么重要判断方法格式支持决定 SBOM 能否跨工具流转是否同时支持 SPDX 与 CycloneDX 导入导出离线漏洞库内网环境能否用是否支持本地库同步与增量更新误报抑制决定治理成本是否支持按包、按路径、按规则抑制并设到期时间CI 集成方式决定接入成本是否有独立命令行工具而非只能走网页平台扫描性能决定能否进主干流水线千级组件项目的全量扫描是否在两分钟内证明格式决定 SLSA 能否落地是否产出标准 in-toto attestation方案上大致三类商业 SCA 平台功能全、误报治理成熟但按项目或按扫描次数计费规模化之后成本不低开源工具组合灵活、可离线但需要自己搭漏洞库同步和结果聚合代码平台内置的扫描最省事但格式和策略定制能力通常偏弱。我的经验是核心流水线用开源命令行工具保证可控性治理层用平台做聚合和工单流转两头各取所长。2.4 分三阶段推进别一口气上全套第一阶段只做可见性。生成 SBOM、跑 SCA结果进报表不阻断任何流水线。这个阶段的目标是摸清家底一共有多少制品、平均每个制品多少组件、高危存量多少、哪些团队问题最集中。跑两周你就有数据支撑后面的决策了。第二阶段开始阻断但只阻断高危且确认可利用的。策略要留白名单通道白名单必须带责任人和到期时间到期自动失效。这个阶段最容易被研发抵触所以阻断信息必须写清楚“哪个包、什么漏洞、升级到哪个版本能解决”而不是甩一个 CVE 编号。第三阶段上签名和准入。先把签名做起来验证端以审计模式运行观察两周误拦率再切成强制。这一步的技术难点不在签名本身在于密钥或身份的管理以及制品库是否支持存储 attestation。3. 实操拆解从生成 SBOM 到部署前验签下面这套流程是我目前项目里在跑的简化版工具用的是常见的开源实现你可以按自己的技术栈替换思路是通用的。3.1 第一步确定 SBOM 的格式、粒度和生成时机我选 CycloneDX 的 JSON 格式原因是体积比 XML 小、工具支持广、attestation 集成方便。源码级和制品级都生成但归档只认制品级。源码级生成在构建前的准备阶段跑syft dir:. -o cyclonedx-jsonsbom-source.cdx.json --exclude **/test/**制品级生成在镜像构建推送之后跑syft registry:registry.internal/app/servicesha256:abc123... \ -o cyclonedx-jsonsbom-image.cdx.json \ -o spdx-jsonsbom-image.spdx.json这里注意两点。一是镜像要用摘要而不是标签否则你扫的和你发布的可能不是同一个东西。二是同时输出两种格式CycloneDX 给内部工具消费SPDX 留给有合规要求的交付场景成本几乎为零。SBOM 生成后要做一次轻量校验至少检查三件事组件数量是否在合理区间突然从八百掉到两百大概率是解析失败、PURL 覆盖率是否超过九成、依赖关系字段是否非空。我写过一个十行的脚本做这个检查拦下过好几次因为构建脚本改动导致 SBOM 变成空壳的情况。归档策略上SBOM 和制品放同一个存储位置保留期与制品一致。建议额外做一件事把 SBOM 的哈希写进制品库的元数据这样后续能验证 SBOM 本身没被篡改。3.2 第二步SCA 扫描的阈值设计与阻断策略扫描直接消费上一步的 SBOM不要重新解析依赖这样能保证扫描的对象和归档的对象是同一份数据。grype sbom:sbom-image.cdx.json \ --fail-on high \ --only-fixed \ -o json sca-result.json--only-fixed这个参数很关键它只报告有修复版本的漏洞。没有修复版本的漏洞报了也没用研发改不动只会制造噪音。这类漏洞应该走另一条流程记录、评估缓解措施、定期复核而不是卡在流水线上。阻断阈值我按环境分主干分支合并要求无高危发布流水线要求无严重定时全量扫描允许存在中危但要有工单跟踪。这个分级是有意为之合并阶段卡太死会导致研发绕过流程发布阶段卡太松又失去意义。白名单配置要结构化不能只写 CVE 编号ignore: - vulnerability: CVE-2024-XXXXX package: pkg:maven/com.example/legacy-util2.3.1 reason: 该组件仅用于离线数据迁移工具不在服务运行时路径 owner: platform-team expires: 2026-06-30到期时间这一条必须有。我见过太多白名单变成永久豁免三年后翻出来发现当初写理由的人已经离职了。关于扫描耗时给个实测参考一个约一千二百个组件的 Java 服务本地漏洞库已缓存的情况下grype 全量匹配大约五十到九十秒如果每次都重新下载漏洞库会多出两到三分钟。所以流水线里一定要把漏洞库目录做成缓存每日定时同步一次即可。另外建议加一层增量逻辑如果本次提交没有改动依赖清单文件跳过全量扫描只做制品级快速校验。3.3 第三步生成来源证明并签名这一步是整个链路里技术门槛最高的。先明确要证明什么构建用的源码仓库地址、commit 摘要、构建流水线标识、构建参数、输入材料的哈希。这些信息大部分能从流水线环境变量里拿到需要自己拼装成 in-toto 的 attestation 格式。签名方式有两种选择。基于密钥的方式自己生成密钥对私钥放密钥管理服务流水线运行时拉取好处是不依赖外部身份系统内网可用坏处是密钥轮换和权限管理要自己做。基于身份令牌的方式流水线用自身的身份令牌换取短期证书签名记录进透明日志好处是没有长期密钥、可审计性强坏处是对流水线的身份配置有要求内网环境可能走不通。我目前的项目用密钥方式因为构建全在内网。签名命令大致是这样cosign attest --predicate provenance.json \ --type slsaprovenance \ --key kms://internal/signing-key \ registry.internal/app/servicesha256:abc123... cosign sign --key kms://internal/signing-key \ registry.internal/app/servicesha256:abc123...两个命令的区别attest把 provenance 作为 attestation 附在镜像上sign是给镜像本身签名。两个都要做缺一个验证端就没法建立完整信任链。这里有个实操细节值得单独说provenance 里的材料哈希必须包含 SBOM。也就是说你签的不只是镜像还有“这个镜像对应这份 SBOM”这个事实。这样下游拿到镜像、SBOM、provenance 三样东西可以互相校验任何一样被换掉都会验证失败。3.4 第四步部署准入时验证签名与证明验证逻辑放在集群的准入控制层镜像要进集群先过一道检查。检查项至少三条签名有效且来自受信任的密钥、provenance 里的构建流水线在白名单内、provenance 里的源码仓库和分支符合预期。策略用声明式的方式写伪代码大致是这个意思拒绝 如果 镜像未签名 拒绝 如果 签名密钥不在 trusted_keys 列表 拒绝 如果 provenance.builder.id 不在 allowed_builders 拒绝 如果 provenance.source.repository 不在 allowed_repos 拒绝 如果 provenance.source.commit 对应的分支不是 main 或 release/*上线节奏上先跑审计模式两周把所有本该拒绝的请求记录下来但不实际拦截观察误拦率。误拦通常来自三个原因历史镜像没签名、临时调试镜像走了非标准流水线、多集群共用策略但某些集群的构建源不同。这些问题在审计期解决完再切强制模式。3.5 一条完整流水线的骨架示例把上面几步串起来流水线结构大致如下stages: - prepare - build - analyze - publish - verify prepare: script: - syft dir:. -o cyclonedx-jsonsbom-source.cdx.json - ./check-sbom.sh sbom-source.cdx.json - grype sbom:sbom-source.cdx.json --fail-on critical --only-fixed build: script: - build-image --tag $IMAGE_TAG - push-image $IMAGE_TAG - export IMAGE_DIGEST$(get-digest $IMAGE_TAG) analyze: script: - syft registry:$IMAGE_REF$IMAGE_DIGEST -o cyclonedx-jsonsbom-image.cdx.json - grype sbom:sbom-image.cdx.json --fail-on high --only-fixed -o json sca.json - ./sbom-policy-check.sh sbom-image.cdx.json publish: script: - ./gen-provenance.sh provenance.json - cosign attest --predicate provenance.json --type slsaprovenance --key $KEY $IMAGE_REF$IMAGE_DIGEST - cosign sign --key $KEY $IMAGE_REF$IMAGE_DIGEST - archive-sbom sbom-image.cdx.json $IMAGE_DIGEST verify: script: - cosign verify --key $PUB $IMAGE_REF$IMAGE_DIGEST - cosign verify-attestation --type slsaprovenance --key $PUB $IMAGE_REF$IMAGE_DIGEST - ./check-provenance-policy.shanalyze和publish之间不要有手动审批环节否则整条链路的自动化就断了provenance 的可信度也会打折扣。要控制就在prepare阶段控制也就是代码合并的时候控制住。4. 踩坑记录这些问题我在实际项目里都遇到过4.1 SBOM 相关的三个高频坑第一个坑是组件数量爆炸。一个中等规模的 Java 服务SBOM 里出现三四千个组件是正常的因为 Maven 的传递依赖非常深。更麻烦的是同一个库的多个版本共存比如三个模块分别引了不同版本的日志库SBOM 里就会出现三条记录。这时候做漏洞匹配会重复报告治理时要先做去重和版本收敛把“同一 PURL 不同版本”的条目合并展示否则研发看到一屏几十条相同漏洞直接就不想看了。第二个坑是版本号丢失。Go 的静态编译二进制、C 语言静态链接的库、某些经过裁剪的运行时扫出来的组件只有名字没有版本号。这种情况 SCA 基本无解只能靠构建时的依赖清单做补充。我的处理办法是在构建阶段同时保留依赖解析的原始输出制品级 SBOM 拿不到版本的组件用它来补。第三个坑是许可证识别错误。双许可证的库经常只识别出其中一个文件头缺失的库直接标成未知。对外交付场景这个错误代价很高所以许可证字段我建议人工复核 Top 50 的高频依赖剩下的按“未知即需审查”的原则处理不要把未知当作安全。4.2 SCA 误报与漏报的治理手法误报治理的核心思路是按包治理而不是按漏洞治理。同一个包上的十个漏洞往往是因为这个包整体版本太旧升级一次全解决。所以工单应该开给“升级某某包到某版本”而不是开给十条 CVE。我们做过统计按包聚合之后工单数量能降六成以上研发的接受度完全不一样。预发布版本误判是另一个高频问题。某些漏洞库的版本区间写得不严谨1.2.0-beta会被判定在受影响范围内实际这个版本根本没包含问题代码。处理方式是给预发布版本单独设一条规则先降级为低优先级人工确认后再决定。漏报方面私有组件的漏洞只能靠自己积累。我们的做法是把内部二方库也纳入 SBOM并且维护一个内部漏洞登记表发现问题的组件版本手工登记进去扫描时合并两套数据源。这件事前期投入不小但内部库复用率高收益是长期的。4.3 SLSA 等级提升过程中的现实阻力最大的阻力是构建时长。L3 要求构建环境隔离意味着每次构建都是干净环境依赖缓存全部失效一个原本八分钟的构建可能变成二十分钟。解决办法是把依赖缓存做成独立于构建环境的制品构建开始前拉取构建结束后校验哈希这样既满足隔离要求又不至于每次都从零下载。第二个阻力是多流水线统一 provenance 格式。一个团队可能同时有容器构建、二进制构建、前端静态资源构建每种构建的元数据结构都不一样。我的建议是先定一个内部统一的 provenance 模板只保留验证端真正会用的字段其余字段留空等验证策略稳定了再逐步补全。想一步到位把所有字段填满是典型的过度设计。第三个阻力是制品库不支持 attestation 存储。签名和证明要跟着制品走老一代的制品库可能只支持存储镜像没有附件的概念。这种情况有两个绕法一是把 attestation 作为独立的制品类型存进同一个仓库用摘要关联二是在部署验证时从透明日志或独立的证明服务里查。前者改动小我更推荐。还有一个软性阻力团队会觉得“我们又不是攻击目标”。这话在平时说没问题但供应链攻击的特点就是批量化和自动化攻击者扫的是整个生态里的薄弱环节不针对具体某一家。真出事的时候你能不能在两小时内说清楚“哪些产物用了受影响版本、分别部署在哪儿”取决于平时有没有把 SBOM 和 provenance 建起来。4.4 问题速查表现象可能原因处理方式SBOM 组件数骤降依赖解析失败或扫描路径错误检查构建日志校验 PURL 覆盖率同一组件重复报告多版本共存未去重按 PURL 聚合推动版本收敛高危漏洞修不动无修复版本转人工评估记录缓解措施并设复核期签名验证失败按标签而非摘要验证统一改用镜像摘要provenance 校验不过构建流水线标识变更更新验证策略白名单审计期观察扫描耗时过长漏洞库未缓存每日定时同步流水线内复用缓存目录准入误拦历史镜像存量镜像未签名设例外清单并制定回补计划5. 长期运营让这套东西不变成摆设5.1 漏洞响应的 SLA 与责任划分工具跑起来只是开始真正决定效果的是响应机制。我按严重程度定四级 SLA这个分级考虑了两个维度漏洞本身的严重程度以及是否已有公开的利用方式。等级判定标准修复时限处置方式紧急严重级别且有公开利用七十二小时内立即修复或下线相关功能高严重级别或高危且有利用十五天排入当前迭代中高危无利用或中危有利用三十天排入下个迭代低其余九十天随版本升级处理责任划分上组件升级由业务团队负责因为只有他们知道升级会不会破坏功能扫描策略和白名单审批由安全团队负责流水线集成和工具维护由平台团队负责。这三方职责不清是治理失败最常见的原因工具报警了没人认领两个月后还是那批漏洞。5.2 值得盯的几个度量指标不要用“扫描次数”“发现漏洞总数”这种虚荣指标它们只会鼓励刷量。我盯的是这几个SBOM 覆盖率即有 SBOM 归档的制品占全部制品的比例目标接近百分之百。高危组件存量按周统计趋势看的是收敛速度而不是绝对值。平均修复时长从漏洞被发现到修复上线的中位数这个指标最能反映流程是否通畅。准入拦截率部署阶段被拦下的请求占比太低说明策略形同虚设太高说明流程有问题需要调整。白名单存量与到期率过期未处理的白名单数量是最能暴露治理松懈的信号。5.3 几条个人经验先说一个反直觉的不要追求把所有漏洞清零。依赖树的深度决定了这件事不可能做到强行清零的结果一定是大范围白名单豁免反而失去治理意义。合理的目标是把高危存量控制在一个稳定低位并且保证新引入的高危能被及时拦住。第二个经验是把 SBOM 当作对外交付物来做。很多客户现在会主动要 SBOM如果你平时就在生成、格式规范、字段完整交付时直接导出就行如果是临时抱佛脚光是把几千个组件的许可证理清楚就要两周。这件事提前做的边际成本极低。第三个经验是签名密钥的管理比签名本身重要。我见过密钥直接写在流水线环境变量里、所有项目共用一个密钥的情况这种签名提供的信任度接近于零。密钥至少要按环境隔离私钥放密钥管理服务访问权限最小化轮换周期定下来并真的执行。最后一个提醒这套体系的价值在出事那一刻才体现。平时它只是流水线里多出来的几十秒和一屏报表看起来是负担但当你需要在半小时内回答“这个漏洞影响我们哪些服务”的时候有没有 SBOM 和 provenance差别就是半小时和三天。我自己的做法是每个季度做一次桌面推演随便挑一个刚披露的漏洞走一遍从查询影响到确认修复的完整流程把卡住的地方记下来改进。演练比任何规范文档都管用。