janpfajfr/vouch
GitHub: janpfajfr/vouch
vouch 是一个 Node.js 依赖决策记录账本工具,通过在 CI 中强制记录每个依赖的添加原因与来源,让依赖变更在 PR 中可见、可审查且永久可审计。
Stars: 7 | Forks: 0
# vouch

一个专为 Node.js 项目和编程智能体设计的**依赖决策记录账本**。
**它是什么。** `vouch` 不负责判断一个依赖是否安全。它确保进入你仓库的每一个依赖都
被**记录、解释,并能在 pull request 中审查** —— 作为你 `package.json` 的记忆与良知。该记录保存在一个已提交的
账本中,以 `name@version` 为键,在 diff 中可见,并可永远审计。
**为什么使用它**
- **每个依赖都是被记录的决策** —— 谁添加了它以及为什么,保存在已提交的账本中,
并显示在 PR diff 中,而不是没人看的 lockfile 变更里。
- **CI 拦截静默添加** —— 原始的 `npm install `(由人类*或*智能体执行)再也无法
在不被记录的情况下进入 `main` 分支。
- **记录随着时间的推移保持真实** —— 在人类重新决策之前,会针对你已经记录的内容
提示**已知 CVE** 或**版本漂移**。
- **零依赖,Node 18+** —— 一款自身没有任何依赖的依赖安全工具,
并且附带 `AGENTS.md`,让编程智能体遵守相同的规则。
**它不是什么**
- **不是扫描器。** 深度的单包分析(如拼写抢注、行为分析)是 `npq`
和 Socket 的工作。`vouch` 负责的是**来源溯源与强制执行**,并在事后标记漂移。
- **不是你包管理器安装门槛的替代品。** 它*补充*了 pnpm 的
`minimumReleaseAge`、Yarn 的 age gate 以及 npm 的 release-age 控制。
- **不是审批机构。** `vouch` *记录*决策;而 *PR 审查*负责批准它。
(以上每一点都在下文有详细展开 —— 参见[它不是什么](#what-it-is-not)和
[vouch 负责记录;PR 审查负责批准](#vouch-records-the-pr-review-approves)。)

## 目录
- [快速开始](#quick-start)
- [核心理念](#the-idea)
- [你将看到什么](#what-youll-see)
- [工作原理](#how-it-works)
- [vouch 负责记录;PR 审查负责批准](#vouch-records-the-pr-review-approves)
- [当 `check` 因 CVE 拦截时](#when-check-blocks-on-a-cve)
- [命令](#commands)
- [在 monorepo 中使用 vouch](#using-vouch-in-a-monorepo)
- [配置](#configuration)
- [写给编程智能体](#for-coding-agents)
- [它不是什么](#what-it-is-not)
- [零依赖](#zero-dependencies)
## 快速开始
**环境要求:** Node.js 18+。无其他依赖。
**安装** —— 使用 `npx` 按需运行,或者全局安装 CLI:
```
npx @vouchjs/vouch --help # no install
npm install -g @vouchjs/vouch # or install the `vouch` command
```
**初始化配置(可选,推荐)** —— 一条命令,绝不覆盖:
```
vouch init # writes vouch.config.{mjs,js} with all defaults shown
```
这会为你提供一个**带有类型的配置**(Playwright 风格):每个选项及其默认值都清晰可见,
随时可以编辑。删除你满意的键,vouch 将沿用未来的默认值;
修改你不在意的键。`init` 还会生成一个包含编程智能体规则的 `AGENTS.md`。
为了在生成的配置上获得完整的编辑器**自动补全 + 类型错误提示**,还需要将 vouch 作为
开发依赖安装,以便你的项目能够访问其类型:
```
npm install -D @vouchjs/vouch
```
`vouch init` 会检测 vouch 是否在你的 `node_modules` 中,并写入相应的
变体 —— 安装时使用 `import { defineConfig } from "@vouchjs/vouch"`,未安装时则使用带有 JSDoc 类型的普通导出。
两种变体都**能在运行时加载**;仅编辑器体验有所不同。
**添加依赖** —— 而不是运行 `npm install` / `pnpm add`,请运行:
```
vouch some-package # reviews, installs, and records the decision
vouch some-package -D # devDependency
```
**在 CI 中设置门槛** —— 添加一个步骤,只要存在未记录的依赖就使构建失败。将此内容放入
`.github/workflows/vouch.yml`(也可在 [`examples/github-actions-check.yml`](examples/github-actions-check.yml) 中找到):
```
name: vouch
on: [pull_request]
jobs:
vouch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npx @vouchjs/vouch check
```
就这样。原始的 `npm install `(由人类*或*智能体执行)再也无法
在不被记录的情况下进入 `main` 分支。全新的克隆则不受影响 —— `npm install` / `npm ci` 会像
往常一样恢复 lockfile,并且 `check` 会通过,因为已提交的账本已经涵盖了每个依赖。
## 核心理念
1. **每个依赖都是一项决策。** 添加依赖应当被记录下来 —— 包含是谁以及为什么 ——
而不是悄悄塞进没人看的 lockfile diff 中。
2. **记录保存在你的仓库中。** 决策会进入一个已提交的账本
(`.security/dependency-approvals.json`),以 `name@version` 为键,并包裹在
`{ "version": 2, "entries": { … } }` 信封结构中,在 PR diff 中可见且可永久审计。
来自 0.1.x(以 name 为键)的账本会在首次读取时自动迁移;更新后的文件会显示在
下一次变更的 diff 中。
3. **记录随着时间的推移保持真实。** 当你记录的依赖随后出现已知的
安全通告,或者其版本偏离了审查时的版本,CI 会将其暴露出来,直到由人类
重新决策。
## 你将看到什么
未使用 `vouch` 添加的依赖会导致 CI 失败:
```
✦ vouch Dependency review failed
- axios: missing ledger entry
Next — record the unrecorded dependency:
vouch axios
```
使用 `vouch` 添加依赖会记录该决策(并在你说明原因前拦截有风险的包):
```
✦ vouch Dependency needs review
esbuild@0.28.0
- install-time script detected: postinstall
Next:
vouch esbuild --force-with-reason ""
```
一旦记录,账本条目就会随着 diff 一起呈现,并且 CI 会变绿:
```
✦ vouch Dependency review passed
All dependencies are recorded.
```
## 工作原理
**当你添加一个包时** (`vouch `):
1. 审查它:**版本存在时长**、**安装时脚本**(默认拦截)以及
**来源溯源** —— 即该版本发布时是否带有
[npm 来源溯源证明](https://docs.npmjs.com/generating-provenance-statements),
以及它是从哪个仓库/工作流构建的。这些都会记录在账本条目中;可通过
`requireProvenance` 开启门槛。
2. 如果该版本**当时已存在已知的 CVE**,会**立刻**警告你 —— 这样 `check` 绝不会是
第一个通风报信的。可配置:`cveAtInstall: "warn"`(默认),`"block"`(拒绝安装严重程度
在 `cveAtInstallMinSeverity` 及以上的通告,默认为 `"high"`),或 `"off"`。
3. 安装它并在账本中记录该决策(版本、风险、谁添加了它以及为什么)。
**在 CI 中** (`vouch check`) —— 每个依赖有三种状态:*已记录* (ok)、*未记录*
(拦截) 或 *需要审查* (拦截)。发生失败的情况如下:
- `package.json` 中的直接依赖**没有账本条目** —— 未通过 `vouch` 添加
(涵盖 `dependencies`、`devDependencies` 和 `optionalDependencies`;当启用
`checkPeerDependencies` 时,也包括 `peerDependencies`);
- 某个依赖**出现了人类尚未确认的 CVE**;
- **高风险**条目**没有记录 `reason`** 供审查者判断;
- **版本漂移** —— `check` 从 `node_modules` 解析每个依赖的**已安装版本**,
并断言该精确的 `name@version` 已经过审查。请在完成安装后运行 `check`。
如果已安装版本偏离了记录的内容,请运行
`vouch @` 重新记录它们;
- **版本锁定** —— 可选的 `requirePinned` (`"warn"`,默认 `"off"`) 会标记使用
版本范围而非精确版本的依赖,并建议锁定到记录的版本。
## vouch 负责记录;PR 审查负责批准
这一区别是该工具的核心:
- **vouch 负责记录决策。** 账本条目 —— 谁添加了它(`addedBy`,来自 `git config`),
为什么(`reason`),在什么版本和风险下 —— 只是*归因*。它是自我声明的,
其本身并不构成授权。
- **PR/MR 审查才是授权。** 人类批准 pull request(并且在 diff 中能看到
账本条目)才是真正的批准行为。vouch 让决策变得有意识且可审查;
它并不试图验证或取代该审查。
你始终可以使用 `--force-with-reason` 强行通过某项操作。但你永远无法
*隐蔽地*做到这一点 —— 原因和你的身份会落入已提交的账本中,呈现在
审查者面前。请参阅 [`THREAT_MODEL.md`](THREAT_MODEL.md) 了解 vouch 能够
防御什么以及不能防御什么。
## 当 `check` 因 CVE 拦截时
拦截并不是破坏 —— 而是一次暂停:*你记录的依赖携带了人类尚未确认的 CVE ——
要么是你记录时它就已经存在(尚未确认),要么是后来才出现的(`check` 将其标记为 NEW)。*
以下是三个诚恳的选项,按推荐程度排序:
1. **修复它** —— 运行 `vouch @` 记录已修复的版本。
2. **移除或替换它** —— 丢弃该依赖,或者换成一个更轻量的依赖。
3. **知情后接受它** —— 一旦你判定风险可接受(仅用于开发、不可达的
代码路径、暂时无法修复),请使用 `vouch acknowledge --reason ""`。
`acknowledge` 会针对记录的版本重新查询安全通告,并记录已确认的集合、
确认者(来自 `git config`)、原因以及时间 —— 这些在 PR diff 中可见。它拒绝在
离线状态下写入(我们绝不记录无法验证的确认),并且它只
在 **已确认** 的 CVE 上进行拦截:离线或响应迟缓的 endpoint 会*开启*失败状态
(只发出警告,绝不让构建失败),并且只有发生漂移的特定依赖会被拦截 ——
绝不会波及你的整个项目。
## 命令
| 命令 | 功能描述 |
|---|---|
| `vouch [-D]` | 审查、安装并记录一个依赖(`-D` 用于 devDependencies)。 |
| `vouch --force-with-reason ""` | 强行解除拦截,并在账本中记录原因。 |
| `vouch check` | CI 门槛:遇到未记录的依赖、未解释的高风险、CVE 漂移或版本漂移时失败。 |
| `vouch adopt` | 为整个仓库建立基准:记录所有 workspace 中已安装但未记录的依赖(按 `name@version` 去重)。绝不执行安装。具备幂等性。 |
| `vouch acknowledge --reason ""` | 知情并接受依赖当前的安全通告(CVE 漂移)。 |
| `vouch init` | 初始化 `vouch.config.{mjs,js}`(显示所有默认值 + 检测到的 `packageManager`),并生成 `AGENTS.md`。拒绝覆盖已有文件。 |
| `vouch --help` · `vouch --version` | 帮助(包含商标)和版本号。 |
环境变量:`VOUCH_ADVISORY_URL` 可覆盖 npm 安全通告 endpoint(适用于企业镜像/代理)。
## 在 monorepo 中使用 vouch
vouch 能够感知 **pnpm** (`pnpm-workspace.yaml` `packages:`) 和 **npm/yarn** (`package.json` `workspaces`) 的 workspace。它会发现每一个 workspace 包,获取它们声明的依赖的并集,并针对位于 `/.security/dependency-approvals.json` 的**单一根账本**进行操作。
- **建立仓库基准:** 在仓库根目录运行 `vouch adopt` —— 它会记录所有 workspace 中已安装但未记录的依赖,按 `name@version` 去重(使用同一个包不同版本的两个 workspace 会获得两个条目)。
- **在 CI 中强制执行:** 安装后运行 `vouch check`。它会解析每个 workspace 中每个依赖的**已安装**版本(从 `node_modules` 解析,对于没有本地 `node_modules` 的 workspace,则回退到 `pnpm-lock.yaml`),如果该确切的 `name@version` 从未被审查过,就会失败。请在完整安装(`pnpm install --frozen-lockfile`)之后运行它。
- **添加依赖:** 在需要该依赖的 workspace 内运行 `vouch `。管理器会将其安装到该 workspace 中;vouch 会按照该 workspace 安装的版本将其记录到**根**账本中。
内部依赖(`workspace:`、`link:`、`file:`、`catalog:`)会被跳过 —— 它们是仓库内部的边缘连接,而不是外部包。除了以 `name@version` 作为键之外,单一包仓库不受影响。违规情况会按 workspace 分组,每组内的修复列表数量有上限,并附带指向 `vouch adopt` 的提示。
## 配置
首选形式是带类型的配置 —— `vouch.config.{ts,mjs,js,cjs}` —— 导出一个
`defineConfig()` 调用。每个键都是可选的;默认值会顺延。使用
`vouch init` 生成一个:
```
// vouch.config.mjs (or .js if your project has "type": "module" — vouch init picks correctly)
import { defineConfig } from "@vouchjs/vouch";
export default defineConfig({
packageManager: "auto", // "auto" | "pnpm" | "npm" | "yarn"
allowScopedPackages: [],
// Install-time gate
minimumVersionAgeHours: 24,
warnVersionAgeHours: 168,
blockInstallScripts: true,
requireCooldownConfigured: false,
// CI gate — `vouch check`
requirePinned: "off", // "warn" | "off"
checkPeerDependencies: false, // also gate peerDependencies (prod/dev/optional always gated)
// CVE handling at add time
cveAtInstall: "warn", // "warn" | "block" | "off"
cveAtInstallMinSeverity: "high", // "low" | "moderate" | "high" | "critical"
// Provenance at add time — always recorded; this only controls the gate
requireProvenance: "off", // "warn" | "block" | "off"
});
```
运行时验证依然会触发(拼错的枚举值会明确报错,而不是静默地
降低门槛)。
### 编辑器类型 —— 作为开发依赖安装的步骤
为了让 `import { defineConfig } from "@vouchjs/vouch"` 能够正确解析并点亮配置的编辑器自动补全,
vouch 需要位于你项目的 `node_modules` 中:
```
npm install -D @vouchjs/vouch
```
`vouch init` 会自动检测到这一点,并写入相应的变体:
| 状态 | 生成的配置 |
|---|---|
| `@vouchjs/vouch` 在你的 `node_modules` 中 | `import { defineConfig } from "@vouchjs/vouch"; export default defineConfig({ ... })` —— 通过内置的 `.d.ts` 提供完整的编辑器类型支持 |
| 本地未安装 `@vouchjs/vouch` | `/** @type {import("@vouchjs/vouch").Config} */ export default { ... }` —— **无运行时导入**,在任何地方都能加载;当你执行 `npm install -D @vouchjs/vouch` 后,类型提示就会立刻生效 |
两种变体都能在运行时加载;只是编辑器体验有所不同。
### 文件格式
`vouch.config.ts` 适用于 Node 23+(或 22.6+ 搭配 `--experimental-strip-types`)。对于较老的
Node 版本,请编写 `.js`/`.mjs`/`.cjs` —— vouch 会通过动态 `import()` 加载其中任何一种。
### 包管理器检测
### 历史遗留:`.safe-dep.json`
## 写给编程智能体
`AGENTS.md` 会告知智能体使用 `vouch` 而不是原始安装,在添加依赖之前解释*为什么*需要它,并且 —— 关键的是 —— **不要**代替人类静默绕过门槛。
`vouch init` 会生成此文件(创建新文件,或在已有文件中追加一个围栏代码块区域),
从而让这些规则随着仓库一起传播。随着智能体添加更多依赖,账本将成为
人类以异步且可追责的方式审查这些决策的地方。
## 它不是什么
它不是扫描器。深度的单包分析(如拼写抢注、行为分析)是 `npq`
和 Socket 等工具的工作。我们不会为了发现 CVE 而*扫描* —— 我们记录的是你所记录内容的安全通告姿态,并在事后标记**漂移**。`vouch` 负责**来源溯源与强制执行**。
来源溯源的记录方式也是一样的:vouch 记录来自发布时间的、经 registry 验证的证明声明
(哪个仓库和工作流构建了该版本);它**不会**重新验证
sigstore 签名 —— `npm audit signatures` 才是用于加密重新验证的工具。
它也不是你包管理器原生防御的替代品。现代版本自带了
安装时的门槛 —— pnpm 的 `minimumReleaseAge`(在 pnpm 11 中默认开启)、Yarn 的
`npmMinimalAgeGate`、npm 的 release-age 控制。`vouch` **补充**了它们:那些门槛拦截的是*安装什么*;
而 `vouch` 记录的是*谁决定了安装,以及为什么*,这正是 PR 能够看到的内容 —— 这也是它们都没做到的一件事。
在没有这些默认设置的包管理器版本(例如 pnpm 9)中,`vouch` 的安装时
审查将成为你原本所缺失的门槛。
## 零依赖
`"dependencies": {}`。基于 Node 18+ 的内建模块构建。一款自身没有任何
依赖的依赖安全工具。
标签:MITM代理, 文档结构分析, 暗色界面, 自动化攻击