← Back to Blog

依赖不是装上就完事:把第三方风险管住的三层做法

92 阅读 点赞

很多项目不是被自己的代码拖垮的,而是被别人的代码拖垮的。一次 npm install 拉进上千个包,构建日志里躺着一堆叫不出名字的依赖,某天其中一个包发了个小版本,你的线上就白了。依赖治理这件事,大多数团队直到出事那天才开始做,而它真正需要的不是”删包”,是一套能长期运转的机制。

一个齿的缺口,足以让整组齿轮停摆——依赖治理要防的就是那一刻
一个齿的缺口,足以让整组齿轮停摆——依赖治理要防的就是那一刻

下面这三层,是我在几个项目里反复验证过的路径:先从可见性开始,再谈约束,最后落到流程上。

第一层:先看见——你根本不知道自己在依赖什么

做依赖治理的第一个障碍,不是技术,是认知。绝大多数开发者对项目的依赖认知停留在直接写在 package.json / pom.xml 里的那二十来行,而真实情况是:那二十个直接依赖,可能带进来八百个间接依赖。而真正引入漏洞、真正拖慢构建、真正在某天改变行为的,往往是那些你从没听说过的传递依赖。

所以第一步永远是把依赖树摊开来看。常用的命令无非是 npm ls --all、mvn dependency:tree、pipdeptree、cargo tree,它们能把嵌套关系完整打印出来。第一次看到完整依赖树的人通常会有两种反应:一是惊讶于自己”请了这么多人回家”,二是发现同一个包的不同版本被重复引入了好几遍——比如 lodash 同时存在 4.17.15 和 4.17.21 两份,React 有 17 和 18 混着用。这种重复不只是体积问题,它意味着某些代码运行在你以为已经修好的旧版本上。

光看结构还不够,还要把依赖的属性标注出来。至少要问清楚四件事:这个包多久没更新了?它的维护者是组织还是个人?它的许可证是什么?它有没有已知的 CVE?现在这些都是免费的:npm audit、osv-scanner、GitHub 的 Dependabot、Snyk 都能自动给出报告。把这些信息汇总成一张清单,定期刷一遍,你就从”完全不知道”变成了”大致知道”,这已经是质量上的一大步。

顺便说一句,看见之后最常见的发现是:有一批依赖其实根本没被用到。它们或是早期试验留下的残骸,或是重构后忘记清理的配角。这一类的处理成本最低、收益却最直接——删掉它们,构建快一点,攻击面小一点,什么都不用付出。

第二层:设边界——让依赖的变更不能随便进来

看得见之后,第二层的目标是把”被动接受”变成”主动选择”。核心动作只有一个:锁定版本,并让升级成为一个有意识的动作,而不是偶然事件。

先说锁定。用浮动版本号(^1.2.3、~1.2.3 甚至 *)意味着你默认同意”以后发布的任何兼容版本都可以直接进我的生产环境”,而”兼容”是别人的判断,不是你的验证。这不是说不能用浮动版本,而是必须在应用层用 lockfile 把实际安装的版本钉死——package-lock.json、yarn.lock、poetry.lock、go.sum 都是干这个的。而 lockfile 唯一正确的用法是:提交进版本库,CI 里用 npm ci 这类”严格按 lock 安装”的命令,而不是 npm install。很多”我本地好好的,线上就炸了”的事故,根源就是 CI 没有遵守 lock。

再说升级。既然不能永远不升(安全补丁和生态演进的债会越滚越大),那就把它变成节奏化的事情。一个可操作的做法是分层策略:补丁版本(patch)可以自动合并、每周批量过;次版本(minor)走常规评审,双周或每月一次;主版本(major)单独开分支,配套测试和回滚方案。同时给关键依赖设一个”被依赖度”的标签——如果一个包被项目里三十个模块直接引用,那它就是硬骨头,升级它必须比其他包谨慎十倍。反过来,那些只有一处引用的工具库,想换就换。

还有一个容易被忽视的边界问题:不要让同一个能力有两套依赖。典型症状是 HTTP 客户端用了三个(axios、request、fetch 封装各一套),日期库用了两个(moment 和 dayjs 并存)。每多一套,就多一份漏洞面、多一份升级负担、多一份新人理解成本。收敛到一套,是性价比极高的治理动作。

三层递进:看见、设边界、进流程
三层递进:看见、设边界、进流程

第三层:进流程——把治理变成不依赖意志力的机制

前两层靠一次专项行动就能完成大半,但真正难的是让它不退化。经验是:所有”靠人记得去检查”的治理,三个月内必然形同虚设;只有进了流程、有了自动化红线的治理,才能长期活着。

第一件必须自动化的事是把扫描接进 CI。在流水线上跑一次依赖漏洞扫描,遇到高危(critical / high)直接让构建失败。注意分级:不要把所有告警都设成阻塞,否则一周之内团队就会集体绕过它。做法是”高危阻塞、中危告警、低危记录”,并且给每个高危项一个明确的处理时限。红线的价值在于它不可被忽略,而不在于它多严格。

第二件事是把依赖变更单独成 PR。一个 commit 里混着业务改动和十个依赖升级,评审者根本没法判断风险。让依赖升级走独立的、标题清晰的 PR,附上升级日志和变更说明,评审成本立刻下降,回滚也干净——出事时只需要 revert 那一个 PR。

第三件事是给关键路径上的依赖写契约测试。你没法控制别人怎么改代码,但你可以把”我依赖它的哪些行为”写下来,变成测试用例。它一旦变了,你的测试先红,而不是用户先投诉。这比任何文档都可靠,因为它会在 CI 里替你持续执行。

最后一点,也是最少被提到的一点:定期清理,而不是只在引入时把关。依赖治理不是一次性的入场审查,而是持续的新陈代谢。可以约定每个季度做一次”依赖盘点”——删掉不再使用的、收敛重复的、升级欠账最多的那几个、复核许可证是否仍然合适。给这件事排进迭代计划,而不是等它出问题。

深夜排查依赖问题的桌面,是很多团队治理的真正起点
深夜排查依赖问题的桌面,是很多团队治理的真正起点

结语

依赖治理真正难的地方,从来不是删包或者升级,而是让一群人在很长的时间里持续在意一件不紧急的事。所以别指望靠一次彻底的重构解决问题,那只会带来一次漂亮的报告,和半年后的原地复位。

更现实的路径是三层递进:先看见自己依赖了什么,把清单和维护状态摊在阳光下;再设边界,用 lockfile 和分层升级策略把不确定性挡在门外;最后进流程,让自动化红线、独立 PR 和契约测试替人扛住记忆负担。第一层一天就能做完,第二层需要一两周,第三层则要靠团队共识慢慢沉淀。

你的项目现在能一眼说清楚有哪些关键依赖、它们上次更新是什么时候、有没有已知漏洞吗?如果这三个问题答不上来,那就从今天的 npm ls --all 开始吧。

💬 发表评论