aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
216 lines (190 loc) • 8.1 kB
JavaScript
/**
* audit-signatures.mjs — A12 / #1288
*
* Runs `npm audit signatures` and fails when any non-waived package has
* a missing or invalid signature. Catches upstream-package tampering and
* the unsigned-package supply-chain surface in one gate.
*
* Waivers live in ci/npm-audit-signatures-waivers.yaml. Each waiver has
* a hard `expires` date — once expired, the waiver no longer applies
* and the package must be re-evaluated. This prevents permanent waivers
* from silently accumulating into a regression-by-default state.
*
* Two failure modes:
* - "invalid" : the package has a signature, but it does not verify.
* Hard stop. NEVER waiveable. This is the tampering
* signal we MUST surface.
* - "missing" : the package has no signature at all. Waiveable when
* the package predates npm's signature infrastructure
* AND the waiver has a non-expired entry in the
* waiver file.
*
* Exit codes:
* 0 → all packages either verified or have non-expired waivers
* 1 → policy violation (unsigned non-waived, invalid, expired waiver, ...)
* 2 → fatal error (waiver file unparseable, npm audit failed, ...)
*
* See:
* - ci/npm-audit-signatures-waivers.yaml
* - .aiwg/architecture/adr-publish-time-evidence.md
*/
import { readFileSync, existsSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import yaml from 'js-yaml';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..', '..');
// ---------------------------------------------------------------------------
// CLI flag parsing
// ---------------------------------------------------------------------------
const { values: args } = parseArgs({
options: {
waivers: { type: 'string', default: 'ci/npm-audit-signatures-waivers.yaml' },
quiet: { type: 'boolean', default: false },
help: { type: 'boolean', default: false },
},
});
if (args.help) {
console.log(`Usage: node tools/lint/audit-signatures.mjs [options]
Options:
--waivers <path> Path to waiver YAML (default: ci/npm-audit-signatures-waivers.yaml)
--quiet Suppress success output (failures still print)
--help Show this help
Exits 0 when all non-waived packages have verified signatures.
Exits 1 on policy violation (unsigned non-waived, invalid sig, expired waiver).
Exits 2 on fatal error (waiver file unparseable, npm audit failed, ...).
`);
process.exit(0);
}
// ---------------------------------------------------------------------------
// Load waivers
// ---------------------------------------------------------------------------
const waiversPath = resolve(REPO_ROOT, args.waivers);
if (!existsSync(waiversPath)) {
console.error(`✗ Waiver file not found: ${waiversPath}`);
process.exit(2);
}
let waivers;
try {
const parsed = yaml.load(readFileSync(waiversPath, 'utf8'));
waivers = Array.isArray(parsed?.waivers) ? parsed.waivers : [];
} catch (err) {
console.error(`✗ Could not parse waiver YAML: ${err.message}`);
process.exit(2);
}
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
const activeWaivers = new Map(); // key: "name@version" → waiver
const expiredWaivers = [];
for (const w of waivers) {
if (!w?.name || !w?.version || !w?.expires) {
console.error(`✗ Waiver entry missing required field (name/version/expires): ${JSON.stringify(w)}`);
process.exit(2);
}
const key = `${w.name}@${w.version}`;
if (w.expires < today) {
expiredWaivers.push({ ...w, key });
continue;
}
activeWaivers.set(key, w);
}
// ---------------------------------------------------------------------------
// Run `npm audit signatures --json`
// ---------------------------------------------------------------------------
if (!args.quiet) {
console.log(`Running \`npm audit signatures\` (waivers: ${activeWaivers.size} active, ${expiredWaivers.length} expired)…`);
}
const result = spawnSync('npm', ['audit', 'signatures', '--json'], {
cwd: REPO_ROOT,
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024,
});
// `npm audit signatures` returns non-zero on signature failures. We need
// to parse the JSON either way to distinguish failure modes.
let auditJson;
try {
auditJson = JSON.parse(result.stdout);
} catch (err) {
// If stdout isn't JSON, treat it as fatal — we can't make a policy
// decision without parseable output.
console.error(`✗ Could not parse \`npm audit signatures --json\` output: ${err.message}`);
if (result.stderr) console.error(result.stderr);
if (result.stdout) console.error(result.stdout.slice(0, 2000));
process.exit(2);
}
// `npm audit signatures --json` shape (npm 9+):
// { invalid: [...], missing: [...], ... }
// Each invalid/missing entry has { name, version, ... }.
const invalid = Array.isArray(auditJson.invalid) ? auditJson.invalid : [];
const missing = Array.isArray(auditJson.missing) ? auditJson.missing : [];
// ---------------------------------------------------------------------------
// Apply waiver policy
// ---------------------------------------------------------------------------
const violations = [];
// INVALID signatures are never waiveable. This is the tampering signal.
for (const pkg of invalid) {
violations.push({
kind: 'invalid',
name: pkg.name,
version: pkg.version,
message: `Signature INVALID (signature exists but does not verify) — tampering signal, not waiveable`,
});
}
// MISSING signatures are waiveable if an active (non-expired) waiver covers them.
for (const pkg of missing) {
const key = `${pkg.name}@${pkg.version}`;
if (activeWaivers.has(key)) {
if (!args.quiet) {
console.log(` · waived: ${key} (expires ${activeWaivers.get(key).expires})`);
}
continue;
}
violations.push({
kind: 'missing',
name: pkg.name,
version: pkg.version,
message: `No registry signature, no active waiver`,
});
}
// Expired waivers are themselves violations — they force re-evaluation.
for (const w of expiredWaivers) {
violations.push({
kind: 'expired-waiver',
name: w.name,
version: w.version,
message: `Waiver expired ${w.expires} (today ${today}); re-evaluate per the procedure in ci/npm-audit-signatures-waivers.yaml`,
});
}
// ---------------------------------------------------------------------------
// Report
// ---------------------------------------------------------------------------
if (violations.length === 0) {
if (!args.quiet) {
const verifiedTotal =
(Array.isArray(auditJson.verified) ? auditJson.verified.length : 0) ||
(typeof auditJson.auditCount === 'number' ? auditJson.auditCount : 0);
if (verifiedTotal > 0) {
console.log(`✓ All ${verifiedTotal} packages have verified registry signatures (waivers: ${activeWaivers.size})`);
} else {
console.log(`✓ npm audit signatures passed (waivers: ${activeWaivers.size})`);
}
}
process.exit(0);
}
console.error(`✗ npm audit signatures policy violations (${violations.length}):`);
for (const v of violations) {
console.error(` [${v.kind}] ${v.name}@${v.version} — ${v.message}`);
}
console.error('');
console.error('Remediation:');
console.error(' - "invalid": HARD STOP. A signed package failed verification.');
console.error(' Treat as a supply-chain incident. Do NOT publish.');
console.error(' - "missing": Either (a) bump the dep to a version with a signed');
console.error(' release if one exists, or (b) add a time-bounded');
console.error(' waiver to ci/npm-audit-signatures-waivers.yaml.');
console.error(' - "expired-waiver": re-evaluate per the procedure documented at');
console.error(' the top of ci/npm-audit-signatures-waivers.yaml.');
console.error(' See .aiwg/architecture/adr-publish-time-evidence.md for context.');
process.exit(1);