不只查 CVE:用 cargo-vet 把 Rust 依赖升级变成可审查的供应链策略
Rust 项目升级依赖时,团队常见的判断只有两种:cargo update 能否解析,以及漏洞扫描有没有命中已公开的 CVE。它们都很重要,但都没有回答另一个工程问题:当前锁定的第三方 crate,是否按团队认可的标准接受过审计?
Mozilla 的开源工具 cargo-vet 试图把这个问题落到仓库里的配置和 CI 上。它是 Cargo 的 cargo vet 子命令,目标是帮助项目确认第三方 Rust 依赖已经由受信任实体审计。当前 crates.io 发布版为 0.10.2,采用 Apache-2.0 或 MIT 双许可证。它不替代漏洞扫描,也不宣称证明依赖绝对安全;它管理的是“审过什么、按什么标准审、哪些暂时例外、谁的审计可以引用”这些可复核的决策。
漏洞库命中与审计覆盖,不是同一条控制线
漏洞扫描依赖公开披露的信息:若某个版本尚未有已知公告,扫描结果可以是干净的。依赖审计则要求团队面对代码和风险边界本身。两者的输入、失败模式和处置方式不同,因而适合并行放进供应链流程:前者发现已知问题,后者要求依赖变更有可追溯的审查路径。
cargo-vet 默认提供 safe-to-run 与 safe-to-deploy 两个审计标准。官方文档用前者表达较低的运行风险,用后者覆盖需要处理不可信输入的生产暴露场景。不要把这两个名称理解为安全保证书:它们是团队在审计记录中使用的准则标签。项目还可以定义自己的准则,例如对密码学实现要求 crypto-reviewed,再只把该准则施加给相关 crate。
[criteria.crypto-reviewed] description = ''' The cryptographic code in this crate has been reviewed for correctness by a member of a designated set of cryptography experts within the project. '''
真正值得保留在 Git 里的不是“某人说已经看过”,而是该准则的含义、适用范围和随依赖升级发生的 diff。这样 reviewer 在审 PR 时能同时看到 Cargo.lock 的变化与供应链策略的变化。
先初始化,再把例外项当成待偿还的审计债务
在已有 Rust 项目的根目录安装并初始化:
cargo install cargo-vet --version 0.10.2 cargo vet init cargo vet --locked
cargo vet 不带子命令时会运行 check。init 的设计并不是假装历史依赖已经全部审完:它会为当前需要的包建立让检查先通过的例外与初始设置,使团队能在不冻结开发的情况下逐步接管审计工作。生成的记录通常位于仓库的 supply-chain/ 目录,应像源代码一样提交、评审和回滚。
接下来不要急着运行“自动接受全部”的操作。先用下面的命令查看低成本、优先级较高的审查候选:
cargo vet suggest cargo vet inspect serde 1.0.0 cargo vet diff serde 1.0.0 1.0.1
其中版本号只是命令形态示例,必须替换为项目锁定的真实 crate 和版本。suggest 会把现有例外临时移开,用来展示审查积压;因此更适合在正常 check 已通过之后使用。inspect 获取指定包的源码,diff 则协助查看目标版本相对上次审查版本的变化。审查完成后,使用 certify 记录结果;它也支持对两个版本之间的增量审计,而不是每次升级都重新审完整 crate。
策略的重点是分级,而不是把所有依赖一刀切
大型依赖图里,要求每个间接依赖立即达到同一强度,通常会把工具变成阻塞器。更可行的做法是把风险判断显式化。例如,官方配置语法允许给根 crate 设置较强的 safe-to-deploy,并对非生产暴露的特定依赖设定较弱的 safe-to-run:
[policy.mycrate]
criteria = ["safe-to-deploy"]
dependency-criteria = { non-exposed-crate = ["safe-to-run"] }
这不是降低标准的借口。相反,它让例外从聊天记录进入版本控制:为什么是这个依赖、为什么只需较弱准则、是否仍然成立,都可以在后续 PR 中复核。对处理密钥、网络输入、构建脚本或过程宏的依赖,团队可以采用更高准则或更短的复审周期;对仅在开发工具链中使用的 crate,则应明确其不进入生产运行面的证据。
另一个边界是“信任”。cargo-vet 可以导入其他审计集合,也支持对 crate 与发布者建立信任关系。信任不是跳过安全,而是承认团队会复用外部审查,同时把信任源和范围写入配置。导入前应检查来源的审计准则是否与本项目一致、维护者身份和仓库迁移情况,以及信任范围是否被意外放大。不能解释的广泛信任,比一个可见的临时例外更难治理。
CI 只负责阻断,审计判断留在 PR 中
一个稳妥的流程是:开发者在本地升级依赖后运行 cargo vet;若失败,用 suggest、inspect 与 diff 生成待审工作;审查者确认新增 audit、policy 或有期限的 exemption;最后让 CI 只验证仓库已提交的结果。
官方 CI 文档的关键执行命令是:
- name: Invoke cargo-vet run: cargo vet --locked
--locked 的意义是避免 CI 在校验阶段悄悄改写锁文件或拉取新的导入审计数据。工具版本也应在工作流中固定,而不是每次 cargo install 最新版。下面是有意精简的 GitHub Actions 骨架;生产项目可再加缓存以缩短安装时间:
name: dependency-vet
on: [push, pull_request]
jobs:
vet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- run: rustup update stable && rustup default stable
- run: cargo install --version 0.10.2 cargo-vet
- run: cargo vet --locked
这里不能把 CI 绿灯等同于“所有依赖没有风险”。它只证明:在当前锁文件、当前 supply-chain/ 记录和当前规则下,依赖图满足了团队写下的准则。若 CI 失败,也不应直接执行批量生成例外的命令来消除告警;先判断是确实需要审计、策略应细分,还是依赖本身不该被引入。
审计记录要能解释“为什么通过”
把 cargo vet 接入 CI 后,最常见的坏结果不是检查失败,而是检查通过却没有人说得清依据。审计记录应当能回答四个问题:这个 crate 的哪个版本被覆盖;覆盖的是完整版本还是两个版本之间的增量;满足的是哪一档 criteria;记录来自本团队还是导入的外部集合。遇到事故、维护者交接或依赖重新评估时,这四项信息比一次性的扫描报告更容易追溯。
例如,升级范围很小的 crate,审查者不必重新阅读旧版本全部代码,而可先以 cargo vet diff 确认增量边界,再决定是否以增量审计记录该升级。反过来,若升级引入 build script、网络客户端、unsafe 边界或新的特性组合,即使 diff 很短,也不应机械沿用较弱标准。工具帮助把证据放在同一处;风险判断仍应由了解项目攻击面的工程师完成。
对于暂时无法完成的审计,例外项必须被当作有成本的待办,而不是永久白名单。建议在 PR 描述或记录备注中写清触发原因、责任人和下一次复核条件,例如“仅用于开发期”、“等待上游修复”或“在下次大版本升级前重新审计”。定期运行 cargo vet suggest 的意义也在这里:它能让团队重新看到被例外掩盖的审计积压,而非让例外在依赖图中无限累积。
与现有供应链检查怎样组合
成熟流程通常至少有三层。第一层是依赖解析与锁定:提交 Cargo.lock,确保构建输入可重复;第二层是漏洞和许可证检查,针对已知披露与合规限制;第三层才是 cargo-vet 的审计覆盖与策略证据。它们的输出不能互相替代:没有 CVE 不等于代码符合审计准则,存在审计记录也不等于新增漏洞公告可以忽略。
还应避免把 cargo-vet 的例外机制当作绕过 CI 的快捷方式。对于业务必须继续推进的依赖,临时例外可以降低迁移阻力,但应缩小到明确的 crate、版本和准则范围,并在 review 中保留理由。若某个间接依赖频繁触发例外,优先检查能否升级直接依赖、关闭不需要的 feature,或替换维护更透明的组件。供应链治理的目标不是让每一次检查都变绿,而是让“为什么绿”经得起下一次变更和下一位维护者的检查。
哪些团队适合先试
cargo-vet 最适合已经有 Cargo.lock、依赖升级 PR 和基本 CI 的 Rust 团队,尤其是网络服务、客户端、开发者工具或需要长期维护的内部库。可以从一个仓库开始:初始化、提交 supply-chain/、把 cargo vet --locked 设为必经检查,再每周处理少量 suggest 给出的审查积压。
它不适合作为替代 SCA、许可证合规、签名验证或人工安全评估的万能工具。它的价值在于把“这次升级为什么可以接受”从口头判断变成可读、可 diff、可在 CI 重放的供应链证据。依赖越多、升级越频繁,这层证据越值得提前建立。