janpfajfr/vouch

GitHub: janpfajfr/vouch

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

Stars: 7 | Forks: 0

# vouch ![banner](https://static.pigsec.cn/wp-content/uploads/repos/cas/33/33a1af147a16512c09329fbe4f9c45716ba801cd952e16e3b656b0185871d656.png) 一个专为 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)。) ![vouch demo](https://static.pigsec.cn/wp-content/uploads/repos/cas/a4/a450b472aed22937b4711b1d89cf26fa60f48064026fa7777f6e894d2b2d4db5.gif) ## 目录 - [快速开始](#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代理, 文档结构分析, 暗色界面, 自动化攻击