janpfajfr/vouch

GitHub: janpfajfr/vouch

Stars: 6 | Forks: 0

# vouch ![banner](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/8f39a9d8e1122133.png) A **dependency-decision ledger** for Node.js projects and coding agents. **What it is.** `vouch` doesn't decide whether a dependency is safe. It makes sure every dependency that enters your repo was **recorded, explained, and reviewable in the pull request** — a memory and conscience for your `package.json`. The record lives in a committed ledger, keyed by `name@version`, visible in the diff and auditable forever. **Why use it** - **Every dependency is a recorded decision** — who added it and why, in a committed ledger that shows up in the PR diff instead of a lockfile change nobody reads. - **CI blocks the silent add** — a raw `npm install ` (by a human *or* an agent) can no longer reach `main` unrecorded. - **The record stays honest over time** — surfaces a **known CVE** or **version drift** on something you already recorded, until a human re-decides. - **Zero dependencies, Node 18+** — a dependency-security tool with none of its own, and it ships an `AGENTS.md` so coding agents play by the same rules. **What it is *not*** - **Not a scanner.** Deep per-package analysis (typosquatting, behavioral) is the job of `npq` and Socket. `vouch` owns **provenance and enforcement**, and flags drift after the fact. - **Not a replacement for your package manager's install gates.** It *complements* pnpm's `minimumReleaseAge`, Yarn's age gate, and npm's release-age controls. - **Not an approval authority.** `vouch` *records* the decision; the *PR review* approves it. (Each of these is expanded below — see [What it is *not*](#what-it-is-not) and [vouch records; the PR review approves](#vouch-records-the-pr-review-approves).) ![vouch demo](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/ed738e14bf122141.gif) ## Contents - [Quick start](#quick-start) - [The idea](#the-idea) - [What you'll see](#what-youll-see) - [How it works](#how-it-works) - [vouch records; the PR review approves](#vouch-records-the-pr-review-approves) - [When `check` blocks on a CVE](#when-check-blocks-on-a-cve) - [Commands](#commands) - [Using vouch in a monorepo](#using-vouch-in-a-monorepo) - [Configuration](#configuration) - [For coding agents](#for-coding-agents) - [What it is *not*](#what-it-is-not) - [Zero dependencies](#zero-dependencies) ## Quick start **Requirements:** Node.js 18+. No other dependencies. **Install** — run on demand with `npx`, or install the CLI globally: npx @vouchjs/vouch --help # no install npm install -g @vouchjs/vouch # or install the `vouch` command **Bootstrap the config (optional, recommended)** — one command, never overwrites: vouch init # writes vouch.config.{mjs,js} with all defaults shown That gives you a **typed config** (Playwright-style): every option visible with its default, ready to be edited. Delete the keys you're happy with and vouch will pick up future defaults; change the ones you're not. `init` also seeds an `AGENTS.md` with the rules for coding agents. For full editor **autocomplete + type errors** on the generated config, also install vouch as a dev dependency so its types are reachable from your project: npm install -D @vouchjs/vouch `vouch init` detects whether vouch is in your `node_modules` and writes the appropriate variant — `import { defineConfig } from "@vouchjs/vouch"` when installed, a JSDoc-typed plain export when not. Either variant **loads at runtime**; only the editor experience differs. **Add a dependency** — instead of `npm install` / `pnpm add`, run: vouch some-package # reviews, installs, and records the decision vouch some-package -D # devDependency **Gate it in CI** — add one step that fails the build on any unrecorded dependency. Drop this in `.github/workflows/vouch.yml` (also in [`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 That's it. A raw `npm install ` (by a human *or* an agent) can no longer reach `main` unrecorded. A fresh clone is unaffected — `npm install` / `npm ci` restores the lockfile as usual, and `check` passes because the committed ledger already covers every dependency. ## The idea 1. **Every dependency is a decision.** Adding one should be recorded — with who and why — not slipped into a lockfile diff nobody reads. 2. **The record lives in your repo.** Decisions go into a committed ledger (`.security/dependency-approvals.json`), keyed by `name@version` and wrapped in a `{ "version": 2, "entries": { … } }` envelope, visible in the PR diff and auditable forever. Ledgers from 0.1.x (name-keyed) auto-migrate on first read; the updated file appears in the next mutation's diff. 3. **The record stays honest over time.** When a dependency you recorded later gains a known advisory, or its version drifts from what was reviewed, CI surfaces it until a human re-decides. ## What you'll see A dependency added without `vouch` fails CI: ✦ vouch Dependency review failed - axios: missing ledger entry Next — record the unrecorded dependency: vouch axios Adding one with `vouch` records the decision (and blocks risky packages until you say why): ✦ vouch Dependency needs review esbuild@0.28.0 - install-time script detected: postinstall Next: vouch esbuild --force-with-reason "" Once recorded, the ledger entry travels with the diff and CI is green: ✦ vouch Dependency review passed All dependencies are recorded. ## How it works **When you add a package** (`vouch `): 1. Reviews it: **version age** and **install-time scripts** (blocked by default). 2. Warns you **right then** if the version already has a **known CVE** — so `check` is never the first messenger. Configurable: `cveAtInstall: "warn"` (default), `"block"` (refuse to install advisories at or above `cveAtInstallMinSeverity`, default `"high"`), or `"off"`. 3. Installs it and records the decision (version, risk, who added it and why) in the ledger. **In CI** (`vouch check`) — three states per dependency: *recorded* (ok), *unrecorded* (blocked), or *needs review* (blocked). It fails when: - a direct dependency in `package.json` has **no ledger entry** — added without `vouch` (covers `dependencies`, `devDependencies`, and `optionalDependencies`; `peerDependencies` too when `checkPeerDependencies` is enabled); - a dependency **gained a CVE** that no human has acknowledged; - a **high-risk** entry has **no `reason`** recorded for the reviewer to judge; - **version drift** — `check` resolves each dependency's **installed version** from `node_modules` and asserts that exact `name@version` was reviewed. Run `check` after a complete install. If installed versions have drifted from what was recorded, run `vouch @` to re-record them; - **pinning** — an opt-in `requirePinned` (`"warn"`, default `"off"`) flags deps that use a range instead of an exact version, suggesting the recorded version to pin to. ## vouch records; the PR review approves This distinction is the heart of the tool: - **vouch records a decision.** The ledger entry — who added it (`addedBy`, from `git config`), why (`reason`), at what version and risk — is *attribution*. It's self-asserted, not by itself an authorization. - **The PR/MR review is the authorization.** A human approving the pull request, with the ledger entry visible in the diff, is the act that approves. vouch makes the decision conscious and reviewable; it doesn't try to verify or replace that review. You can always force a thing through with `--force-with-reason`. You can never do it *invisibly* — the reason and your identity land in the committed ledger, in front of the reviewer. See [`THREAT_MODEL.md`](THREAT_MODEL.md) for what vouch does and does not defend. ## When `check` blocks on a CVE A block isn't damage — it's a pause: *a dependency you recorded carries a CVE no human has signed off on — either one present when you recorded it (not yet acknowledged), or one that appeared since (`check` flags it as NEW).* Three honest options, in order of preference: 1. **Fix it** — `vouch @` to record a fixed release. 2. **Remove or replace it** — drop the dependency, or swap in a lighter one. 3. **Accept it knowingly** — once you've judged the risk acceptable (dev-only, unreachable code path, no fix yet), `vouch acknowledge --reason ""`. `acknowledge` re-queries advisories for the recorded version and records the acknowledged set, who acknowledged it (from `git config`), why, and when — visible in the PR diff. It refuses to write while offline (we never record an acknowledgement we couldn't verify), and it blocks only on a CVE it **confirmed**: offline or a stalled endpoint fails *open* (a warning, never a failed build), and only the specific dependency that drifted — never your whole project. ## Commands | Command | What it does | |---|---| | `vouch [-D]` | Review, install, and record a dependency (`-D` for devDependencies). | | `vouch --force-with-reason ""` | Override a block, recording the reason in the ledger. | | `vouch check` | CI gate: fail on unrecorded deps, unexplained high-risk, CVE drift, or version drift. | | `vouch adopt` | Baseline the whole repo: records every installed, unrecorded dependency across all workspaces (deduped by `name@version`). Never installs. Idempotent. | | `vouch acknowledge --reason ""` | Knowingly accept a dependency's current advisories (CVE drift). | | `vouch init` | Bootstrap `vouch.config.{mjs,js}` with all defaults shown + detected `packageManager`, and seed `AGENTS.md`. Refuses to overwrite. | | `vouch --help` · `vouch --version` | Help (with the wordmark) and version. | Environment: `VOUCH_ADVISORY_URL` overrides the npm advisory endpoint (for enterprise mirrors/proxies). ## Using vouch in a monorepo vouch is workspace-aware for **pnpm** (`pnpm-workspace.yaml` `packages:`) and **npm/yarn** (`package.json` `workspaces`). It discovers every workspace package, takes the union of their declared dependencies, and operates against **one root ledger** at `/.security/dependency-approvals.json`. - **Baseline the repo:** run `vouch adopt` at the repo root — it records every installed, unrecorded dependency across all workspaces, deduped by `name@version` (two workspaces on different versions of the same package get two entries). - **Enforce in CI:** run `vouch check` after installing. It resolves each dependency's **installed** version (from `node_modules`, falling back to `pnpm-lock.yaml` for workspaces with no local `node_modules`) per workspace and fails if that exact `name@version` was never reviewed. Run it after a complete install (`pnpm install --frozen-lockfile`). - **Add a dependency:** run `vouch ` inside the workspace that needs it. The package manager installs it into that workspace; vouch records it to the **root** ledger at the version installed for that workspace. Internal dependencies (`workspace:`, `link:`, `file:`, `catalog:`) are skipped — they are intra-repo edges, not external packages. Single-package repos are unaffected beyond the `name@version` keying. Violations are grouped by workspace, and the fix-it list is capped per group with a pointer to `vouch adopt`. ## Configuration The preferred form is a typed config — `vouch.config.{ts,mjs,js,cjs}` — exporting a `defineConfig()` call. Every key is optional; the defaults flow through. Generate one with `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" }); Runtime validation still fires (a typo'd enum value fails loudly instead of silently downgrading a gate). ### Editor types — the install-as-devDep step To make `import { defineConfig } from "@vouchjs/vouch"` resolve and light up editor autocomplete on the config, vouch needs to be in your project's `node_modules`: npm install -D @vouchjs/vouch `vouch init` detects this automatically and writes the appropriate variant: | State | Generated config | |---|---| | `@vouchjs/vouch` in your `node_modules` | `import { defineConfig } from "@vouchjs/vouch"; export default defineConfig({ ... })` — full editor types via the bundled `.d.ts` | | `@vouchjs/vouch` not installed locally | `/** @type {import("@vouchjs/vouch").Config} */ export default { ... }` — **no runtime import**, loads anywhere; types light up the moment you `npm install -D @vouchjs/vouch` | Both variants load at runtime; only the editor experience differs. ### File format `vouch.config.ts` works on Node 23+ (or 22.6+ with `--experimental-strip-types`). For older Node, write `.js`/`.mjs`/`.cjs` — vouch loads any of them via dynamic `import()`. ### Package manager detection ### Legacy: `.safe-dep.json` ## For coding agents `AGENTS.md` tells agents to use `vouch` instead of raw installs, to explain *why* a dependency is needed before adding it, and — crucially — **not** to silence the gate on a human's behalf. `vouch init` seeds this file (creating it, or appending a fenced section to an existing one), so the rules travel with the repo. As agents add more dependencies, the ledger becomes the place a human reviews those decisions, asynchronously and accountably. ## What it is *not* Not a scanner. Deep per-package analysis (typosquatting, behavioral) is the job of tools like `npq` and Socket. We don't *scan* for CVEs to discover them — we record the advisory posture of what you recorded and flag **drift** after the fact. `vouch` owns **provenance and enforcement**. Not a replacement for your package manager's native defenses, either. Modern versions ship install-time gates — pnpm's `minimumReleaseAge` (default on in pnpm 11), Yarn's `npmMinimalAgeGate`, npm's release-age controls. `vouch` **complements** them: those gate *what* installs; `vouch` records *who decided, and why*, where the PR can see it — the one thing none of them do. On package-manager versions without those defaults (e.g. pnpm 9), `vouch`'s install-time review is the gate you'd otherwise lack. ## Zero dependencies `"dependencies": {}`. Built on Node 18+ built-ins. A dependency-security tool with no dependencies of its own.
标签:自动化攻击