做软件供应链安全的人都有同一种感觉:系统越来越复杂,依赖越叠越高,漏洞报告像雪片一样飞过来,但真正能落地的修复少得可怜。我过去很长一段时间,就是被困在“扫描→出报告→转交研发→没人理→下个月再扫”这个循环里。后来我们重构了软件供应链安全漏洞的自动化检测与修复流程,把过去靠人肉周报驱动的模式,改成了一条从检测、评估、修复到验证自动流转的闭环链路。这篇内容就是那次重构的完整记录:我们当时怎么思考、踩过哪些坑、最后沉淀下来的方案长什么样,以及每一步为什么那么设计。
这篇文章适合正在搭建或重构供应链安全体系的团队参考,也适合刚接手漏洞治理、被一堆历史漏洞压得喘不过气的同学拿来当落地清单。
1. 从“亡羊补牢”到“关口前移”:供应链安全为什么这么难
很多团队第一次接触供应链安全,是在一次高危漏洞通报之后。安全组紧急拉网扫描,发现全公司几十个业务线都在用同一个受影响组件,然后就开始了一轮又一轮的排查、沟通、修复、反复确认。这种应急模式偶尔赢一次可以,但每次都这么来,团队迟早被拖垮。
1.1 供应链的本质:三层依赖的地毯式排查
供应链安全之所以复杂,是因为它的攻击面不是一条线,而是一整张网。以一个常见的业务应用为例:
- 直接依赖:项目直接引用的第三方库,比如一个HTTP框架、一个JSON解析库。这部分开发人员还有感知。
- 传递依赖:直接依赖所依赖的其他库。很多前端项目的锁文件里躺着几千个包,但研发真正认识的只有十来个,剩下的大多是传递依赖。
- 构建与运行链:CI系统用的构建镜像、发布包里的二进制、容器的基础镜像、甚至部署脚本里下载的辅助工具,都属于供应链的一部分。
我把这三层关系类比成一个你住的小区:直接依赖是你自己家的装修,传递依赖是楼上楼下的邻居,构建运行链则是小区的水电管网和物业系统。任何一个环节出问题,后果都会波及到你家里。传统做法往往是盯着“自己家装修”看,但真正的风险往往藏在邻居和管网里。
1.2 传统流程的四个堵点
我们当时盘点过,旧流程的瓶颈主要集中在四个地方:
- 检测频率低:人工触发扫描,最频繁也就是每周一次。漏洞公开到被利用之间的时间窗口越来越短,周级扫描经常赶不上变化。
- 误报噪声大:一份扫描报告动辄几百上千条,里面混杂着不可达的代码路径、测试依赖、开发态才存在的问题。研发看到这种报告,第一反应是关掉不看。
- 修复成本高:升级依赖不是改个版本号那么简单。小版本升级可能引入行为变化,大版本升级更可能直接破坏接口。没有一个自动化的验证机制兜底,没人敢在主干上直接升级。
- 责任边界不清:安全团队负责“发现问题”,研发团队负责“处理问题”,但问题处理的优先级、时限、验收标准往往没定清楚。最后就变成了互相拉扯。
下面这张表是我们重构前和重构后的一个对比,可以直观看出来差别在哪。
| 维度 | 旧模式 | 优化后 |
|---|---|---|
| 扫描触发 | 人工按月/按周执行 | 代码变更、依赖更新、出包发布时自动触发 |
| 漏洞筛选 | 全量报告人工review | 引擎自动过滤噪声并计算风险分 |
| 修复动作 | 研发手工升级依赖 | 自动升级小版本并生成变更单 |
| 验证环节 | 依赖是否升级成功 | 自动化测试+门禁校验双重确认 |
| 数据度量 | 漏洞总数 | 修复时长(MTTR)、阻塞率、存量趋势 |
看完这张表,你应该能感觉到,这不是简单换一个工具的事,而是整个研发与安全协作方式的调整。
2. 方案整体设计:自动化检测与修复的闭环框架
自动化检测和修复,不是买一个商业扫描器装上就完事。真正的难点在于把扫描结果和研发工作流打通,让漏洞数据能在正确的时间到达正确的人手里,并且以能执行的方式呈现出来。我们最终设计的闭环框架,概括成一句话:一次检测,全程可溯,自动兜底,人工只处理例外。
2.1 闭环流程的核心链路
整个闭环分成五个阶段。
第一阶段是检测。所有代码入库、依赖锁定文件变更、镜像构建完成,都会触发扫描。检测对象不光是仓库里的代码,还包括编译后的物料清单和容器镜像。
第二阶段是评估。扫描结果进入一个统一的评估引擎,引擎会把漏洞信息、依赖关系、资产信息合并,过滤掉不可达漏洞和已知误报,再算出每个漏洞的风险分。
第三阶段是修复。低风险漏洞进入自动修复队列,由机器人完成依赖升级、测试触发、变更提交;高风险且涉及大版本跳动的,进入人工评估队列。
第四阶段是验证。无论是机器人生成的变更,还是人工修改的版本,都要跑一遍完整的测试流水线,确认没有引入兼容性回归。
第五阶段是度量。每周自动生成漏洞趋势、修复时长、存量变化、门禁拦截情况,这些数据直接回流到下一周的排期和策略调整里。
五阶段之间不是串行关系。比如一个高危漏洞在评估阶段被认为不可达,系统会直接把它降级进观察清单,而不是阻塞主流程。这种“处理链路上分流”的设计,是之后整个流程能够高效跑起来的关键。
2.2 工具选型与取舍标准
当时我们面临一个选型问题:用商业产品还是自建轮子。我的建议是,先不要急着做技术决策,先列出需求清单,再拿需求去倒推工具。
我们的需求清单大概是这样的:
- 必须能生成标准格式的软件物料清单(SBOM),并且格式要支持主流标准,方便后续自建平台二次消费这份数据。
- 必须有命令行和API 接口。没有接口的扫描平台,无法嵌入CI流水线,无法自动触发,只能靠人打开网页去点。
- 必须区分直接依赖和传递依赖。这是优先级排序的基础信息,如果工具给不了细分数据,后面很多策略都没法做。
- 必须支持自定义漏洞抑制规则。线上跑久了必然会积累一些确认误报的情况,工具不允许配置规则,这些误报就会反复出现在每一份报告里。
- 漏洞库更新频率要够快。商业产品通常有商业化数据支撑,开源方案则需要自己关注漏洞源更新情况。
选型结论是:商业工具和开源引擎各有用武之地。我们最后采用了一主一备的搭配,商业工具负责对外发布物和核心系统的高保真扫描,开源引擎负责把扫描能力扩展到所有内部项目,同时用内部平台统一收敛两边的结果。
2.3 资产分级与策略分级
不是所有项目都值得用同一套标准。我们当时把所有纳入管理的代码仓库和制品分成四个等级:
| 资产级别 | 定义 | 修复时限 | 门禁策略 |
|---|---|---|---|
| P0 | 核心交易链路、对外提供服务、涉及资金或用户隐私 | 高危4小时,中危24小时 | 阻断发布 |
| P1 | 内部重要系统,对外无直接暴露 | 高危24小时,中危72小时 | 高危阻断,中危警告 |
| P2 | 内部工具、管理后台 | 高危5个工作日,中危纳入迭代 | 警告不阻断 |
| P3 | 实验性项目、演示环境 | 修复不设硬性时限 | 仅记录 |
很多团队在落地供应链安全时犯的最大错误,就是一视同仁地要求所有项目修复所有漏洞。这样做资源不够用,还会让研发对整套机制产生抵触。分级策略的核心是:让有限的修复资源集中在风险暴露面最大的资产上。
3. 核心环节的落地细节与实操要点
框架搭好之后,真正的硬功夫在细节里。这一章我挑几个最影响成败的环节展开讲,包括SBOM生成、漏洞过滤、优先级计算和自动修复策略。这些都是我们反复调优过的地方,也是我认为投入产出比最高的部分。
3.1 SBOM:自动化检测的第一步
SBOM听起来是一个合规概念,但从工程角度看,它其实是自动化检测的地基。没有一份完整的、随版本留存的依赖清单,漏洞扫描就只能靠临时去仓库里翻锁文件,效率和准确度都不可控。
我建议SBOM生成要在CI里完成,并且有三个要点:
- 生成时机放在依赖安装之后、构建完成之前。这样拿到的依赖清单最完整,既覆盖了直接依赖,也解析出了传递依赖。
- 每次发布都要生成对应版本的SBOM,并且归档到制品库旁边。很多团队只对主干生成SBOM,导致线上跑着的某个历史版本发生漏洞预警时,根本不知道它依赖了什么。
- SBOM要带环境标识。比如前端项目的开发依赖生产不需要,容器镜像里的构建工具产物和运行时依赖也不能混在一起。
关于SBOM格式,目前有两个主流标准:一个偏简单、以SPDX为主要代表,另一个以CycloneDX为代表、结构上更侧重依赖树和漏洞关联。我的建议是直接支持CycloneDX,它对依赖关系表达更强,后续做传递依赖分析会顺手很多。
生成做完之后,下一步是入库。把SBOM当作和源码、二进制一样的构建产物,存入内部制品元数据平台。后续任何一次漏洞通告,都可以先在平台上查一遍“哪些SBOM覆盖到的组件命中了该漏洞”,几分钟内就能圈出受影响范围。这一步如果做扎实,应急响应的效率能提升一个量级。
3.2 漏洞匹配与噪声过滤
SBOM只是把家底盘清楚了,真正头疼的是把漏洞报告里的噪声滤掉。
扫描引擎的工作原理很简单:把SBOM里的组件名和版本号,与漏洞库里的受影响版本范围做匹配,命中就算发现漏洞。但这里有一个关键问题:漏洞库的匹配是静态的,它只说明“这个版本可能受影响”,不代表“漏洞在当前代码里真的能被利用”。
所以我们的过滤策略分三层走:
第一层,过滤掉开发态依赖。前端锁文件里那几个lint工具、测试框架,如果不在生产构建产物里,就不应该进入生产漏洞列表。这一层能砍掉将近三成的条目。
第二层,过滤掉不可达代码路径。这需要结合调用链分析,判断漏洞组件是否在运行入口的可达路径上。做不到全量代码分析时,至少可以用“组件是否为直接依赖+是否被业务代码显式引用”做一个粗筛。
第三层,利用漏洞利用可能性评估数据。有几个比较成熟的外部数据源,专门评估漏洞被利用的真实活跃度。我们把这些数据拉下来,和漏洞库数据合并,一起参与风险评分。
实际跑下来,三层过滤之后的“有效漏洞”数量,通常只有原始报告的10% 到20%。这个数字很关键,因为研发团队一周能处理的漏洞条数是有限的,与其把几千条报告扔给他们,不如先帮他们筛到只剩几十条真正要紧的。
3.3 修复优先级:CVSS评分不够用
很多团队排序还靠CVSS基础分,这是一件很危险的事。CVSS描述的是漏洞本身的固有严重程度,但它不回答一个重要问题:这个漏洞在“我的环境”里,真实风险到底有多大?
我打个比方:一个8分的漏洞如果只出现在某个不联网的实验项目里,另一个5分的漏洞却暴露在核心交易链路且已经被捕获到利用尝试。后者的行动优先级,显然要高于前者,但静态CVSS会引导你先修前者。
我们最后采用的排序公式是:
风险分数 = CVSS基础分 × 资产权重 × 可达性系数 × 利用活跃度系数
其中每个系数我们都有明确的取值规则:
- 资产权重:P0资产为1.0,P1为0.7,P2为0.4,P3为0.2。
- 可达性系数:直接依赖且被业务代码引用的为1.0,直接依赖未被业务代码显式引用的为0.7,仅在传递依赖层出现的为0.5,判定不可达的为0.1。
- 利用活跃度系数:已被利用的漏洞数据源收录的为1.0,利用活跃度预测评分较高的为0.8,其余为0.5。
这样算出来之后,我们按分数从高到低排列,配合修复时限形成了一张每周的处理清单。有一次我们发现某个CVSS只有5.9分的漏洞排在了另一个CVSS为7.5分的漏洞前面,原因是它命中了P0资产、又被标记为已观察到利用尝试。团队一开始还质疑排序逻辑,后来看到数据出处也就认可了。这个例子说明,排序规则本身要能解释,否则研发不会信任它。
3.4 自动修复的执行策略与人工兜底
自动修复是整个方案里风险最高、也最可能翻车的一环。很多人把自动修复理解成机器人改版本号,那太天真了。一个依赖升级可能牵出接口变化、行为变更、传递依赖冲突,没有验证环节兜底,自动修复就是在给生产环境埋雷。
我们的自动修复策略分三条线并行:
第一条线是锁文件范围内的自动升级。具体操作是,当漏洞出现在某个直接依赖的小版本更新里,机器人会先修改锁文件,再跑单测和构建,全部通过后自动提交变更,并且把漏洞编号、版本变更说明、测试结果一并附在变更描述里。研发收到变更单后,只需要review和点击合并。
第二条线是大版本升级的人工介入。版本跨度超过一个大版本时,机器人不会自动动代码,而是会生成一份升级影响评估,包含依赖关系图、废弃接口清单、可能的兼容性问题,发给负责该模块的研发。研发可以在评估单里填上计划时间或拒绝理由,处理进度会一直跟踪到闭环。
第三条线是不可直接升级情况的处置。现实中一定会遇到“活在几百年前的老组件,上游已经停止维护,想升级却连替代品都找不到”。这时候系统给出的不是放任不管,而是自动创建一个风险接受单,必须由技术负责人、安全负责人双方签字,写上临时缓解措施和复审时间。
配合这三条修复策略,还有一个必须执行的原则:所有自动提交的变更,只能合并到特性分支,并且要过完整的测试流水线,禁止直接向主干提交。我们曾经因为没有卡这条原则,出现过一次自动升级合并后导致线上功能回退的事故。从那以后,自动修复的变更单全部强制要求在合并主干前经过人工review。
4. 与现有开发流程的集成实践
方案能不能跑起来,不取决于扫描器有多贵,而取决于研发流程愿意为它让出多少空间。我们最核心的经验是:安全流程必须变成研发流程的一部分,而不是叠加在研发流程之上的另一条线。
4.1 CI/CD门禁设计:分级阻塞
门禁的设计原则,我总结为一句话:能阻塞的不阻断,能警告的不阻塞。这里面的分寸感需要根据团队成熟度来调整。
推荐从三个模式按阶段演进:
第一模式是仅报告模式。新接入扫描能力的项目,头两周只输出检测报告,不设任何门禁。目的是校准扫描结果的准确度,把误报规则尽量沉淀下来,消除研发的恐惧感。
第二模式是紧急漏洞阻断模式。只在高危漏洞出现在P0资产、且该漏洞已被利用或存在明显利用路径时,才在合并请求和发布流水线两处设置硬性拦截。其余漏洞全部走警告。
第三模式是标准策略模式。经过约一个月的数据校准,团队对扫描结果有了信心,再把门禁策略逐步调整到我们之前定义的分级标准上。
门禁参数的配置,关键不在“等级”,而在“动作”。同样的高危漏洞,要区分“阻断合并请求”、“阻断发布”、“仅告警提醒”,分别配置。我们见过不少团队一开张就全量阻断,结果第一个月研发提交全部被卡住,安全团队一天收几十条申诉,最后不得不把门禁全部关掉。这种先猛后松的做法,对机制的可信度伤害极大。
4.2 存量漏洞的治理节奏
启动新版方案的时候,绝大多数团队仓库里都已经积累了数量可观的历史漏洞。面对存量,我们用了类似于“技术债治理”的思路。
具体做法是,把存量漏洞全部录入看板,给每个漏洞打上标签:所属系统、资产等级、发现时间、漏洞等级。每周按照资产等级和漏洞年龄加权生成一份“存量漏洞费用榜”。费用不是钱,而是一个内部度量分数,用来直观表达一件漏洞拖得越久、风险成本越高。看板上每一行都指定到一个具体的负责人,超过约定修复时限的漏洞,费用分快速上涨,并且会出现在CTO周报里。
这个机制的效果是,存量漏洞不会被遗忘,但也不会一次性压垮研发团队。我们用了大概一个季度,把初始五千多条存量漏洞压到了两千条以内,剩下的基本都是上了风险接受单或等待上游版本的长期项。
补充一个细节:存量漏洞清点必须和增量门禁并行,不能等存量清零再上增量门禁。否则一边在堵新漏洞,一边旧的还在持续发酵,安全状态永远出不来。
4.3 度量指标:让数据说真话
流程跑起来之后,怎么判断它真的好用?我们主要看四个指标:
- 平均修复时长:一条漏洞从发现到验证完成所花的时间。这个指标比“修复了多少条”更能反映流程效率。
- 门禁拦截率和误报率:每周被门禁卡住的变更数,以及review后确认其实是误报的比例。误报率高,说明过滤规则需要调优。
- 存量漏洞趋势:观察曲线整体斜率,判断治理是加速还是停滞。
- 自动修复占比:自动变更单占所有修复动作的比例。初期可能只有10%,我们认为健康状态应该逐步做到50% 以上。
月度复盘时,不看功劳簿,只看这几个数字跟上一月的变化。我们有一次发现平均修复时长从72小时升到了140小时,排查后发现是某个业务线新接入了一批存量项目,拉高了整体分母,而不是流程本身变慢。如果没有指标拆解,这种问题会被误判成流程退步,导致做出错误的调整方向。
5. 踩坑实录与避坑指南
这一章我把自己真正踩过、也帮别人填过的坑整理出来。很多问题看起来不大,但每一个都能轻易让整个自动化方案白做。
5.1 扫描阶段的坑
锁文件没有提交到代码仓库。这个问题最常见。扫描器如果没有锁文件可以解析,很多传递依赖根本扫不出来。我们早期有多条业务线因为忘记提交锁文件,线上出的SBOM缺了一大半。后来我们直接在CI里加了校验:一次扫描如果解析出来的依赖树明显少于预期,流水线直接告警。
私服依赖解析失败。内部组件库的依赖包在长时间后可能已经无法解析元数据,导致整张依赖图断掉。解决方式是在CI里做了缓存预热,定期把私服上的索引同步到扫描集群的本地缓存里,大版本升级时还要专门验证一遍。
容器镜像只扫了基础层。不少镜像扫描工具默认只审基础镜像和操作系统层,业务层通过多阶段构建塞进去的那一堆二进制,反而被漏掉。一定要确认扫描策略覆盖完整镜像的内容,不只是最下面那层。
5.2 修复阶段的坑
小版本升级引入行为变化。版本号没跨大版本,不代表行为完全不变。我们遇到过一次修一个日志库的小版本漏洞,结果升级后某个序列化字段的输出格式变了,下游解析报了错。从那以后,自动升级必须和测试流水线强绑定,并且测试用例里要覆盖核心业务链路的冒烟验证,不能只依赖单测。
传递依赖不能直接升级。有些漏洞出在传递依赖层,但是直接依赖的上游迟迟不出兼容新版本。遇到这种情况,可以尝试用依赖覆盖机制,在构建配置里显式把传递依赖替换成已知安全版本,但注意这只能作为临时兜底,必须同步给上游直接依赖开一个升级单。
自动PR合并后出了线上事故。这是我们最痛的一次教训。后来我们规定,自动PR必须加上多渠道通知,合并后立刻跑一轮冒烟验证,并且预留快速回滚入口。最好再做一个灰度期,重要资产自动PR合并后先放量观察一段时间,再做全量生效。
5.3 团队落地推广的坑
安全团队最容易犯的错是把汇报写得像定罪书。今天这个部门有十个高危漏洞,明天那个部门又超时了,这样只会换来研发的反感。真正有效的方式是把漏洞信息包装成研发团队自己关心的样式,比如按模块列出受影响的线上功能、复现条件、修复建议,而不是一条冷冰冰的“升级依赖”。
我们团队还设了一个“安全联络人”机制,每个业务线指定一两个接口人,他们负责把修复单翻译成该业务线听得懂的语言,也负责把研发的困难反馈回安全团队。这个机制落地之后,超时未修的漏洞数量下降了近一半。
最后说一下节奏。自动化流程上线后的前两周,一定会出现很多你觉得“明明没问题怎么还是没扫到”的情况。这时候不要急着加规则,先跑一段时间,把误报和不准确的扫描结果收集起来,统一调整策略。流程优化的空间,往往都藏在这些看起来琐碎的噪声里。
我个人在实际操作中的体会是,软件供应链安全漏洞的自动化检测与修复,最大的价值不是把漏洞数量清零,而是把漏洞治理从一场又一场的救火行动,变成一套有节奏、有反馈、可度量的日常工作流。如果你正在搭建这套东西,建议先从一个中等体量的项目试点,把从SBOM生成到自动修复的整条链路跑通,再谈推广到全公司。自动化流程的打磨和调优,一定要基于真实的数据和踩坑记录,靠想象设计出来的安全方案,最终一定会在现实里摔跟头。