2.18

View in English

2.18 依赖与供应链管理

概述与动机

打开你项目的锁文件,数一数其中的包。如果你和大多数团队一样,你自己写的代码只是薄薄一层,架在成百上千个你没有写、不完全理解、也无法轻易审计的依赖项之上。一个现代 Web 服务会引入一个框架、一个数据库驱动、一个日志库和一种序列化格式,而这些库又各自引入更多的库。结果是,你运行中的大部分软件,往往是绝大部分,来自互联网上的陌生人。这不是一种失败,而是一笔交易:正是它让一个小团队能在几周内交付曾经需要数年才能完成的东西。关键在于,要睁大眼睛去接受这笔交易。

管好这些借来的代码本身就是一门独立的工程学科,本章讲的正是这门手艺:如何选择依赖项、如何锁定版本、如何更新它们、如何复现构建,以及如何让整张依赖图在不断增长时依然清晰可读。安全威胁的那一面()攻击者蓄意污染这张图()将在第 4.2 章应用安全中得到全面讨论。这里关注的是日常工程实践:版本约束、锁文件、传递性冲突、更新节奏,以及了解你的软件里到底有什么。把这些做对了,安全就会容易得多,因为你无法保护一条你看不见的供应链。

对大型团队,尤其是企业和政府机构而言,风险会随规模上升。当五百个代码仓库各自选择自己的库时,你会得到同一个日志框架的五百个略有差异的版本、一份没人批准过的许可证,以及在严重漏洞出现时无法回答”我们是否受影响?“的窘境。企业用批准库清单和共享注册表来应对这一问题。政府则越来越多地用强制要求来应对:美国第 14028 号行政命令将软件物料清单(SBOM)和构建溯源纳入了政府采购软件的基线要求。在下一次依赖危机来临时能够保持冷静的组织,正是那些在需要之前就已经做好这些工作的组织。

关键原则

  • 你的大部分软件是别人写的代码;即便不是你写的,也要承担起这份责任。
  • 每一个依赖项既是资产,也是永久性负债;添加它们要经过深思熟虑,而不是条件反射。
  • 用锁文件锁定版本,让构建可复现、确定,而不是”当天恰好是最新版就用最新版”。
  • 以稳定的节奏、以自动化的小步增量更新,而不是在罕见的、令人胆战心惊的大跃进中更新。
  • 精确知道你的软件里有什么;你无法保护或许可你无法枚举的东西。
  • 优先选择数量更少、维护良好的依赖项,而不是数量众多但方便省事的依赖项。
  • 控制包的来源;一个未经验证的注册表就是一扇敞开的门。

建议

理解版本管理方式,并有意识地约束它

了解你所在生态系统如何表达版本号,因为你的更新行为完全建立在这之上。大多数包管理器使用某种形式的语义化版本(SemVer),版本号写作 MAJOR.MINOR.PATCH:补丁号递增只承诺修复缺陷,次版本号递增会新增向后兼容的功能,主版本号递增则意味着存在破坏性变更。你的依赖声明随后会设定一个约束,比如”兼容 4.x”或”至少 2.3.0”,这告诉解析器在挑选版本时可以走多远。

要有意识地决定这些约束是宽松还是严格。宽松的范围会自动获取修复,代价是让一个你从未审查过的次版本更新悄悄进入生产环境;严格的锁定则以人工投入为代价换取控制力。对大多数团队来说,务实的做法是在清单文件中声明相对宽松的范围,然后在锁文件中冻结实际解析出的确切版本,这样只有在你主动更新时,这个范围才会被重新评估。把 SemVer 当作维护者努力遵守的承诺,而不是他们总能兑现的保证;一次”补丁”发布仍然可能破坏你的系统,这正是你应该测试更新而非盲目信任更新的原因。

提交锁文件,并要求可复现的构建

锁文件记录了依赖图中每一个包(无论是直接依赖还是传递依赖)的确切版本和加密哈希值。把它提交到版本控制系统(第 2.6 章)中,并将其视为源代码中不可或缺的一等公民。它的作用是让你的构建成为一个函数:相同的输入,无论何时、在哪台机器上,永远产生相同的输出。没有它,两名工程师在相隔一周的时间分别运行”安装”命令,可能得到不同的代码,而在生产环境中出现的缺陷,也可能在构建它的那台笔记本电脑上根本无法复现。

以真正的可复现构建为目标,让某个给定的提交始终产出行为完全一致的构建产物。在持续集成(第 8.1 章)中,严格从锁文件安装依赖,并在锁文件与清单文件不一致时让构建失败,而不是悄悄地重新解析出新版本。锁文件中的哈希值发挥着双重作用:它们既锁定了行为,也能检测篡改,因为一个内容已不再与其记录哈希值相符的包将无法安装。可复现性是本章其余一切内容赖以立足的基础。

有意识地管理传递依赖和菱形冲突

你的直接依赖只是你亲自命名的那些。在它们之下,是一张规模大得多的传递依赖图()也就是你的依赖所依赖的包()而你的大部分风险和大部分意外,都潜藏在那里。一个经典的失败场景是菱形依赖:库 A 需要某个共享工具库的版本 1,库 B 需要版本 2,此时解析器必须调和一个不可能满足的请求。一些生态系统允许多个版本共存,用磁盘和内存空间换取安宁;另一些则强制只保留单一版本,把冲突的调解工作留给你。

让这些冲突显现出来,而不是任其潜伏滋长。使用你的工具打印完整的依赖树,并解释为什么某个包会出现在其中、是谁把它引入的。当冲突出现时,要有意识地去解决它:升级落后的一方、锁定一个覆盖版本,或者放弃一个你无法满足其要求的依赖。持续关注这张图随时间的增长,因为传递依赖中失控的蔓延,是一种缓慢累积的技术债务,最终会表现为一次无解的升级,或者一个你不重写代码就无法修补的漏洞。

以稳定的节奏、借助自动化拉取请求进行更新

最危险的更新策略,恰恰是大多数团队在不知不觉中滑入的那一种:从不更新,直到某个严重漏洞逼迫你不得不在紧急压力下一次性更新所有东西。到那时,你已经落后了好几年,更新日志堆积如山,升级也从一件日常小事变成了一个耗时数周的大项目。解决办法就是节奏。采用一个自动化的依赖更新工具(Dependabot 和 Renovate 是常见的例子),当某个依赖有新版本时,它会自动开一个拉取请求,并附上更新日志和你的测试结果。

然后调整这个流程,让它成为助力而不是负担。如果每天早上都涌来一堆各自独立的拉取请求,人们就会渐渐学会无视它们,这比完全没有自动化还要糟糕。把低风险的更新(比如补丁发布)批量处理,在测试通过时让它们自动合并,把人工关注留给主版本升级和任何涉及敏感库的改动。设定一个团队能够长期维持的节奏,也许是每周评审一次,这样更新就始终是一笔稳定的小额税款,而不是一张罕见但痛苦的大账单。这正是强有力的测试策略(第 2.4 章)发挥价值的地方,因为自动化更新只有在你的测试能够捕获它们所破坏的东西时,才是安全的。

精简你的依赖足迹,采纳前先评估

你添加的每一个依赖都是一份长期承诺:承诺承受它的缺陷、它的漏洞、它的许可证、它维护者能否持续投入,以及它自身不断增长的子依赖图。最容易管理的依赖,就是你根本没有添加的那个。在伸手拿一个包之前,先问一问,自己写几十行代码是否就够了,尤其是对那些微不足道的功能而言。包生态系统的历史上,不乏这样的警示故事:一个极小、却被广泛依赖的包被移除或遭到劫持,结果让半个互联网瘫痪。

当你确实要采纳一个依赖时,要像对待一段长期关系那样去评估这个候选者。检查其维护健康状况:近期的提交、响应及时的维护者、真实的发布历史,以及不止一个人掌握着发布权限。检查许可证,确认它在你的批准清单上(第 10.3 章)。检查它的安全记录、体积大小,以及它自身的传递依赖足迹,因为一个小功能不值得因此拖入上百个包。把这些标准写下来,让”我们该不该加这个?“成为整个团队一致遵循的清单,而不是凭一时心情决定。

生成 SBOM 并记录构建溯源

除非你已经知道自己软件里有什么,否则你无法迅速回答”我们是否受这个漏洞影响?“。SBOM 就是答案:以 SPDX 或 CycloneDX 等标准格式,对一次构建中的每个组件(包括版本和许可证)建立机器可读的清单。把它作为构建流水线的一部分自动生成,与构建产物一起存放,并且只要这个产物还在任何地方运行,就一直保留它。当下一个成为头条新闻的漏洞出现时,一次针对你的 SBOM 的查询,就能把原本一周的疯狂 grep 变成一份五分钟的报告。

更进一步,记录溯源信息:一份经过签名、防篡改的记录,说明某个构建产物是如何被构建出来的()源自哪个源代码提交,由哪条流水线构建。开源软件社区已就此凝聚共识,形成了 SLSA 框架(Supply-chain Levels for Software Artifacts,软件产物供应链等级),作为一个分级模型,从”我们能描述我们的构建过程”逐步提升到”我们能证明这一点,而且这份证明即便在构建系统被攻陷的情况下依然可信”。认证机制让消费者能够验证某个产物确实来自你的流水线。对政府相关工作而言,这一点越来越不是可选项;溯源与 SBOM 已被纳入采购强制要求,因此及早建立这项能力,能让你保有投标资格。

用注册表、镜像和依赖内置来控制你的来源

你的包来自哪里,和你选择哪些包同样重要。如果每次构建都直接从公共互联网拉取,你就继承了它的宕机、被撤回的版本,以及它的攻击者。搭建一个内部包注册表,或一个代理公共生态系统的缓存镜像,这样构建就能既快速又可重复,并且不受上游消失的影响。这个注册表也自然而然地成为执行策略的场所:拦截已知的问题版本、让新发布版本经历一段短暂的隔离观察期,以及拒绝未通过你许可证或安全关卡的包。

要谨慎配置这个注册表,以避免两个特定的陷阱。依赖混淆(dependency confusion)发生在这样的情况下:构建工具在同名的私有内部包和公共包之间做选择时,取到了攻击者的公共包;防御方法是将内部包名限定作用域,并明确将内部包锁定到内部源。抢注相似名称攻击(typosquatting)发生在一个恶意包使用与热门包只差一个键位的名字,等着有人手滑输错;一个带有允许清单的、经过策展的注册表能把它挡在门外。对于一小部分关键或更新缓慢的依赖,可以考虑依赖内置(vendoring),即把实际的依赖源代码检入你自己的仓库,让你的构建完全不依赖任何外部资源。这是用更新的便利性换取完全的控制权,有时候这恰恰是正确的选择。

权衡:优点与缺点

方法优点缺点
宽松的版本范围自动获取修复;人工投入低未经审查的代码会进入生产环境;没有锁文件时不确定
严格锁定加锁文件可复现、可审计的构建需要主动投入更新工作;可能延迟获得修复
激进的更新节奏步子小而安全;始终接近最新版本持续的变动;需要评审者持续投入精力
罕见的、批量的大型升级日常打断更少令人胆战心惊、风险高、被迫进行时代价高昂
大量方便的依赖构建功能速度快攻击面大;维护负担重
最小依赖足迹加依赖内置控制力强、暴露面小、无上游风险你自己拥有的代码更多;更新需要自己承担
直接使用公共注册表零配置成本面临宕机、撤回版本、混淆攻击和抢注攻击的风险
内部注册表和镜像速度、策略执行、隔离保护需要基础设施来运行和维护

核心张力在于速度与控制之间。上面的每一个选择,其实都是同一个旋钮从不同角度观察的结果:你打算主动治理多少借来的代码,又愿意让多少借来的代码凭信任自由流入?如果过分偏向控制,你会被淹没在人工评审中,安全修复落后,拖慢了本该由依赖项加速的团队。如果过分偏向速度,你会有一天醒来,发现自己面对着一张无法审计、无法升级的依赖图,以及一起你无法向法律顾问解释清楚的许可证违规事件。真正的解决方案是一种姿态,而不是一个固定点:锁定并复现一切、以小步持续更新、精简你所采纳的东西,并在一个你掌控的关卡上执行策略。这样的组合能同时买到速度和安全,而这正是在大规模场景下值得做的交易。

与团队讨论的问题

  1. 你们真正的更新节奏是什么样的?一次被迫的紧急升级,会花掉几小时还是几周? 大多数团队直到一个严重漏洞逼出这个问题之前,都无法诚实地回答这个问题。本章把稳定的、自动化的小步更新视为安全的路径,而罕见的大爆炸式升级则是危险的路径,因为你让它敞开的这个缺口,正是你日后必须在压力下狂奔跨越的那个缺口。带上证据来讨论:你有多少依赖项已经落后一个以上的主版本?上一次重大升级实际花了多长时间?讨论你们是否能采用自动化更新工具,如何批量处理低风险变更以免大家对通知视而不见,以及要具备什么样的测试才能安全地实现自动合并。这个答案应该改变你们分配工程时间的方式,把一场罕见的危机变成一项常规的每周税负。如果诚实的答案是”要几周”,那就是一个现在就该点名的风险,而不是等事故发生时才发现的风险。

  2. 如果现在有一个严重漏洞在某个常用库中被公开,你们能多快列出所有受影响的构建产物? 这正是 SBOM 存在的意义所在,而你们回答这个问题的速度,直接衡量着你们供应链的成熟度。没有清单,你们就只能靠翻查代码仓库和访谈各个团队,而这可能要耗费你们负担不起的好几天时间。带上具体的信号来讨论:你们是否为每次构建生成 SBOM?它存放在哪里?你们今天真的能跨所有 SBOM 进行查询吗?讨论你们是否不仅知道直接依赖,还了解传递依赖图,因为那个存在漏洞的包,通常是你们从未主动命名过的那一个。这个答案决定了你们下一次事故是一次查询,还是一场消防演习,而这项能力值得在你们真正需要之前就建立起来。政府之所以现在强制要求这一点,正是出于这个原因。

  3. 你们如何决定一个新依赖是否值得采纳,所有人使用的标准是否一致? 本章认为,每一个依赖项既是一种便利,也是一份永久性负债,而最容易管理的那一个,就是你从未添加过的那个。然而在大多数团队里,这个决策过程是隐形的:一名工程师需要某个功能,找到一个包,午饭前它就已经进了锁文件,没有人评审过它的维护状况、许可证、安全历史或体积足迹。从你们自己的依赖图里带上一些例子()那些没人记得当初为什么采纳、现在也无法为其辩护的包。讨论一份书面的评估清单,以及一份批准库清单(第 10.3 章),究竟是会有帮助,还是只会增加摩擦,以及那条界限应该划在哪里()哪些是你应该自己写的琐碎小工具,哪些是值得依赖的真正基础设施。这个答案,会一点一点地塑造你团队长期承担的重量。

  4. 你们真的在治理传递依赖,还是只治理那些你们亲自命名的依赖? 你们的大部分风险都潜藏在低一层,也就是你的依赖所引入的那些包里,而当两个库对同一个共享工具库要求不兼容的版本时产生的菱形冲突,可能会在最糟糕的时刻卡住一次升级。这在大规模场景下尤其重要,因为一个无法修补的传递依赖包,就可能让一项安全修复在数百个代码仓库中被冻结,而这里存在真实的取舍:把整张图揭示出来并锁定它,需要持续投入精力,而忽视它则是用这份精力换取一种缓慢累积的债务,最终表现为一次无解的升级。带上证据来讨论:你们的工具能否打印出完整的依赖树,并解释任何一个包为什么存在、是谁引入的?你们最常用的那些库,今天到底并存着多少个不同版本?对企业和政府而言,还要补充讨论你们的清单和策略是否真的覆盖到了传递组件,因为如果一半的依赖图对你们来说都是不可见的,那么”必须知道软件里有什么”这项强制要求就毫无意义。这个答案会告诉你们,下一次被迫升级,是一次常规合并,还是一场多团队的大挖掘。

  5. 你们的包实际上来自哪里?是什么阻止了攻击者把一个恶意包混进去? 每一次直接从公共互联网拉取的构建,都继承了它的宕机、被撤回的版本,以及两种特定的攻击:依赖混淆()构建工具取到了一个冒充你私有包的公共包;以及抢注相似名称攻击()一个恶意包的名字与某个热门包只差一个键位。这对大型团队尤为重要,因为一次被污染的拉取,可能在任何人察觉之前就已经蔓延到你们整个技术资产中,而这里的取舍是真实存在的:内部注册表或缓存镜像能给你一个策略关卡和对上游的隔离保护,但那是需要有人运行并持续维护的基础设施。带上具体的信号来讨论:内部包名是否被限定了作用域,并明确锁定到内部源?是否存在一份允许清单?任何新发布的版本,在被使用之前是否会经历一段短暂的隔离观察期?对政府和受监管的采购方,把这个问题与采购方越来越要求的批准软件清单和不直连互联网的姿态联系起来,并诚实地评估你们目前的配置今天能否通过这道门槛。

  6. 你们真的能复现并证明你们的构建产物是如何被构建出来的吗? 一份提交了加密哈希值的锁文件,应该能让你的构建成为一个函数()相同的输入,无论今年还是明年、在哪台机器上,都产出相同的输出()而溯源信息应该能让任何人验证某个产物确实来自你所声称的流水线和源代码提交。这很重要,因为一次无法复现的构建,会把一个生产缺陷变成一个无解之谜,也让你无法证明篡改没有发生过,而这里存在的取舍是投入与保证之间的权衡:严格的锁文件安装、经签名的认证,以及与 SLSA 对齐的溯源,都需要投入设置成本和纪律,而”直接安装最新版”这种做法则省去了这些。带上证据来讨论:当锁文件与清单文件不一致时,持续集成是否会失败?你们是否为每次构建生成并存储 SBOM 和经签名的溯源记录?是否有人真正验证过其中的任何一份?对企业、尤其是政府相关工作而言,溯源与 SBOM 越来越多地被纳入采购强制要求,所以这里的诚实回答,决定了你们能否继续保有投标资格,还是会被拒之门外。

行业视角

初创企业。 团队规模很小,也没有平台小组,因此应依靠默认设置和自动化,而不是流程。从第一天起就提交锁文件,打开一个能批量处理补丁发布、并在测试通过(绿灯)时自动合并的自动化更新工具,并保持一条轻量级的添加规则:优先选择朴实无华、维护良好的库,对体积很小的库要三思而后行。你们暂时不会去搭建内部注册表,这没关系,但仅凭已提交的哈希值,你们就已经获得了保护:一个被污染的版本根本无法安装。

小型企业。 你们没有专职的依赖管理专家,预算也很紧张,所以应该购买这份纪律,而不是自己搭建它。依靠你们的托管平台和代码平台已经提供的更新自动化,偏好一小组成熟的库以保持升级成本低廉,并在流水线中使用一个免费的 SBOM 生成工具,这样你们无需专门配置人手,也能回答”我们是否受影响?“。把稀缺的注意力花在许可证检查上,以及不去采纳那些你自己十几行代码就能写出来的琐碎小包上。

企业。 问题在于要在众多团队之间保持一致:一个镜像公共生态系统、并在一个关卡上执行许可证、来源和版本策略的共享内部注册表,再加上一组经过策展的黄金标准库作为默认选择,以及针对其他任何情况的、有文档记录的例外流程。为每次构建生成 SBOM 并存入中央存储库,这样一次查询就能回答你们在整个技术资产中的暴露面,通过自动化拉取请求推动协调一致的升级,并把依赖健康状况当作一个被度量、被治理的投资组合来对待,而不是每个仓库各自为政的偶然结果。

政府机构。 采购规则和公共问责塑造了一切。要求供应商在每次发布时交付机器可读的 SBOM 以及与 SLSA 对齐的构建溯源,内部只从一份由不直连公共互联网的镜像提供的批准软件清单中安装软件,并优先选择维护稳定、许可清晰的依赖项,因为一个系统可能要运行十五年,而且必须在这整段时间内都能被打补丁。有计划地规划生命周期终止的迁移,而不是把它当成紧急事件来处理,并保留能让审计人员将任何已交付的产物追溯到其源头的记录。

示例

初创企业。 一家六人规模的初创公司,基于一个框架、一个支付库,以及大约九百个他们从未检查过的传递依赖包,构建并交付了一款 Web 应用。他们负担不起一个平台团队,因此依靠自动化:从第一天起就提交锁文件、一个能批量处理补丁发布并在测试通过时合并它们的自动化更新工具,以及每月留出一小时来评审堆积起来的主版本升级。他们添加依赖的规则基本上可以浓缩为一句话:“优先选择朴实无华、维护良好的库,对体积很小的库要三思而后行。“当一个热门包遭到攻陷时,正是他们已提交的锁文件哈希值,让那个被污染的版本根本无法安装,他们只是从新闻里读到了这起事件,而没有亲身经历它。

企业。 一家拥有四百个代码仓库的银行,运行着一个镜像公共生态系统、并在该关卡上执行策略的内部包注册表。一组经过策展的黄金标准库()一个被批准的日志框架、一个 HTTP 客户端、一个 JSON 解析器()是默认选择,其他任何选择都需要有文档记录的例外申请。一种内源(inner-source)模式让任何团队都能为这些共享库做贡献,同时由一个小型平台小组负责它们的健康状况。协调一致的升级能通过自动化拉取请求,在几天之内将一项安全补丁推送到全部四百个代码仓库中,而每次构建都会生成一份 SBOM 存入中央存储库。当一个严重漏洞被公开时,他们只需运行一次查询,就能在新闻周期结束之前了解自己的暴露面。

政府机构。 一家联邦机构在采购软件时,遵循可追溯到第 14028 号行政命令的溯源与 SBOM 要求。供应商必须在每次发布时交付机器可读的 SBOM,并展示与 SLSA 框架对齐的构建溯源,这样该机构就能验证每一个产物确实来自其所声称的源头。在内部,开发者只能从一份由不直连公共互联网的内部镜像提供的批准软件清单中安装软件。长期可支持性驱动着这些选择:他们优先选择维护稳定、许可清晰的依赖项,因为一个系统可能要运行十五年,而且必须在这整段时间内都能被打补丁。当某个组件到达生命周期终点时,一次有计划的迁移会将其替换掉,而不是靠一次紧急事件来解决。

业务论证:动机、投资回报率与总拥有成本

依赖纪律带来的回报,大多体现在那些从未发生过的灾难上。一份已提交的锁文件加上可复现的构建,采纳成本几乎为零,却能消除整整一类”在我电脑上能跑”的缺陷,以及无法复现的生产环境缺陷()每一个这样的缺陷都可能耗费资深工程师数天的时间。一个自动化的更新节奏,能把那种偶尔出现、拖慢路线图、耗尽团队精力的多周紧急升级,转化为一种由小规模合并变更构成的、稳定而低调的持续节奏。在一个由众多代码仓库组成的组合中,这种从”罕见而庞大”到”频繁而微小”的转变,是工程组织所能采用的杠杆效应最高的流程变革之一。

总拥有成本这个论证,讲的是你在数年间背负的东西,而不是你这个冲刺周期花掉了多少。疏于管理的依赖项会悄悄累积成本:那些已经过时、不重写代码就无法升级的版本;那些造成了没人事先估算过的法律风险的许可证;以及一张纠缠不清的依赖图,只要一个必需的补丁就会触发一连串破坏性变更。不这样做的代价会在最糟糕的时刻一次性到来()在一次安全事件、一次审计,或者一次被迫的迁移过程中,多年被推迟的维护工作所欠下的账单,会连本带利一起送到你面前。要向管理层证明这一点,就用他们的语言来表述:可复现的构建能降低事故成本,SBOM 能把漏洞响应时间从数天缩短到数分钟,而批准库加溯源信息,能让你们继续有资格参与受监管的合同和政府合同的竞标,否则你们本会被排除在外。

反模式与陷阱

  • 没有锁文件,或者锁文件没有被提交: 每次都重新解析依赖,因此没有人能可靠地复现当初交付的是什么、又是什么出了问题。
  • 在生产环境中放任”latest”浮动: 注册表在那一分钟恰好提供的版本,就成了你的发布内容,未经审查,也无法追溯。
  • 从不更新,直到被逼无奈: 多年的漂移最终坍缩成一次在漏洞压力下进行的、令人胆战心惊的高风险紧急升级。
  • 更新机器人疲劳: 未经批量处理的拉取请求洪流,会训练团队对所有请求都视而不见,包括那些紧急的。
  • 依赖蔓延: 为了琐碎的功能而条件反射式地添加包,最终养成一张无法维护的依赖图和一个庞大的攻击面。
  • 没有清单: 没有 SBOM,回答”我们是否受影响?“就意味着要在代码仓库中进行数天的人工考古。
  • 盲目信任公共注册表: 直接拉取会让你暴露在宕机、被撤回的版本、依赖混淆和抢注攻击面前。
  • 忽视传递依赖: 只治理那些你亲自命名的依赖,而你的大部分风险却藏在低一层。
  • 未经审查的许可证: 引入了许可证与你的交付方式相冲突的代码,却直到一次审计或收购时才被发现。

成熟度模型

  • 第 1 级,初始阶段: 依赖项被随意添加,没有任何评估。没有已提交的锁文件,构建不可复现,更新只在被迫的紧急情况下发生,也没有人能列举出软件里到底有什么。
  • 第 2 级,发展阶段: 一些团队提交锁文件,获得了大体可复现的构建,也有一点自动化在开启更新拉取请求,但各代码仓库之间的实践并不一致。对许可证和传递依赖风险的意识还停留在非正式层面,没有共享的策略、清单,也没有对包来源的控制。
  • 第 3 级,标准化阶段: 实践已经形成文档,并在整个组织范围内得到执行。一个自动化更新工具以稳定的节奏运行,并进行合理的批量处理;构建严格从锁文件安装,并在锁文件与清单文件不一致时失败;每次构建都会生成 SBOM;一个内部注册表执行着来源和许可证策略;新的依赖项会依据一份每个团队都遵循的书面清单接受评估。
  • 第 4 级,管理阶段: 依赖资产被度量、并依据基准进行数据化控制。你会跟踪版本滞后度(有多少依赖项落后一个以上的主版本)、跨所有产物修复严重漏洞的平均耗时、自动化更新的合并率、已交付构建中 SBOM 的覆盖率,以及未解决的菱形冲突和策略例外的数量。这些指标为发布设置门槛,也决定了你们把精力投向何处,因此升级和补救工作是依据证据来管理的,而不是取决于谁的嗓门最大。
  • 第 5 级,编排阶段: 依赖管理在整个组织中被持续改进和整合。构建溯源和认证被记录并验证,SBOM 可在整个投资组合范围内被查询,实现即时的漏洞响应,协调一致的升级能自动推送到众多代码仓库,黄金标准库经过策展并以内源方式维护,整套体系随着生态系统、威胁形势和采购强制要求的变化而不断适应。

讨论想法

  1. 对你的团队来说,自己写一个小工具和为它引入一个依赖之间,正确的分界线在哪里?
  2. 你的版本约束应该有多宽松或多严格?对应用程序和对已发布的库,这个答案是否应该不同?
  3. 低风险的补丁更新是否应该在测试通过时自动合并?你的测试套件需要具备什么条件才能让这种做法安全?
  4. 对你组织的规模和风险状况而言,一个内部注册表或镜像是否值得付出运维成本?
  5. 你会如何决定优先把哪些依赖内置以获得最大控制力,又把哪些留在公共注册表上?
  6. 从这个季度开始,要为你交付的每一个产物生成并真正使用 SBOM,需要做些什么?

关键要点

  • 你的大部分软件都是借来的代码;管好它是一项核心的工程学科,而不是事后补救的附加项。
  • 提交锁文件,并要求可复现、确定性的构建,让相同的输入始终产出相同的输出。
  • 以自动化的小步持续更新,而不是罕见的、被迫进行的、令人胆战心惊的大跃进。
  • 依据一份书面标准审慎地添加依赖;最容易管理的那一个,就是你从未采纳过的那一个。
  • 生成 SBOM 并记录溯源信息,这样你就始终知道自己的软件里有什么、来自何处。
  • 用内部注册表控制你的来源,以防御混淆攻击、抢注攻击和上游故障。

参考文献与延伸阅读

  • U.S. Executive Order 14028, Improving the Nation’s Cybersecurity (2021)
  • National Institute of Standards and Technology (NIST), Secure Software Development Framework (SP 800-218)
  • SLSA (Supply-chain Levels for Software Artifacts) framework specification, Open Source Security Foundation
  • OWASP CycloneDX specification and the SPDX specification, for SBOM formats
  • Tom Preston-Werner, Semantic Versioning Specification (SemVer)
  • The Reproducible Builds project documentation
  • Nicole Forsgren, Jez Humble, Gene Kim, Accelerate: The Science of Lean Software and DevOps