#!/usr/bin/env node /** * Audit gate: fail the build on any advisory that is not a tracked exception. * * `npm audit --audit-level=` is all-or-nothing, and this tree cannot pass a * flat gate: the Gatsby 5 build toolchain carries ~100 advisories at * low/moderate/high with no non-breaking fix, because gatsby is a runtime * dependency of the build. Blocking on any of those stops publishing * permanently; ignoring the tree lets a genuinely new advisory through. So the * known set is committed to audit-baseline.json with an owner and a reason, * and this script fails only when something NEW appears at or above the * threshold. * * Usage: node scripts/audit-gate.mjs [--write-baseline] */ import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const BASELINE = join(HERE, 'audit-baseline.json'); const BLOCKING = new Set(['critical', 'high']); /** npm audit exits non-zero when it finds anything, so ignore the status. */ function auditReport() { let out; try { out = execFileSync('npm', ['audit', '--json', '--omit=optional'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 }); } catch (err) { out = err.stdout; if (!out) throw err; } return JSON.parse(out); } /** Flatten the report to one key per package/severity/advisory triple. */ function currentFindings(report) { const findings = new Map(); for (const [pkg, v] of Object.entries(report.vulnerabilities ?? {})) { if (!BLOCKING.has(v.severity)) continue; const advisories = (v.via ?? []).filter((x) => typeof x === 'object'); if (advisories.length === 0) { // via is a chain of parent package names - attribute to the package. findings.set(`${pkg}|${v.severity}|transitive`, { pkg, severity: v.severity, advisory: 'transitive' }); continue; } for (const a of advisories) { const id = (a.url ?? '').split('/').pop() || a.title || 'unknown'; findings.set(`${pkg}|${v.severity}|${id}`, { pkg, severity: v.severity, advisory: id, title: a.title, range: a.range }); } } return findings; } const report = auditReport(); const current = currentFindings(report); if (process.argv.includes('--write-baseline')) { const accepted = [...current.values()].sort((a, b) => `${a.pkg}${a.advisory}`.localeCompare(`${b.pkg}${b.advisory}`)); writeFileSync( BASELINE, JSON.stringify( { $comment: 'Advisories at critical/high accepted for this tree. scripts/audit-gate.mjs fails the build on anything not listed here, so removing an entry re-arms the gate for that advisory. Regenerate with: node scripts/audit-gate.mjs --write-baseline', owner: 'Site maintainers (allan@4gl.io)', reason: 'Gatsby 5 build toolchain: no non-breaking fix available upstream, and these are build-time dependencies of a statically generated site. Re-check on each Gatsby major bump.', accepted }, null, 2 ) + '\n' ); console.log(`wrote ${accepted.length} accepted advisories to ${BASELINE}`); process.exit(0); } const baseline = JSON.parse(readFileSync(BASELINE, 'utf8')); const accepted = new Set((baseline.accepted ?? []).map((a) => `${a.pkg}|${a.severity}|${a.advisory}`)); const fresh = [...current.entries()].filter(([key]) => !accepted.has(key)); const resolved = [...accepted].filter((key) => !current.has(key)); for (const [, f] of fresh) { console.error(`NEW ${f.severity.toUpperCase()}: ${f.pkg} - ${f.title ?? f.advisory} (${f.advisory})`); } if (resolved.length > 0) { // Not a failure: a bumped or removed dependency closes advisories, and the // baseline should be trimmed in the same change that closes them. console.log(`${resolved.length} baseline entries no longer present - consider re-running with --write-baseline:`); for (const key of resolved) console.log(` stale: ${key}`); } if (fresh.length > 0) { console.error(`\n${fresh.length} untracked advisory/ies at critical or high.`); console.error('Fix it, or add it to scripts/audit-baseline.json with a reason and an owner.'); process.exit(1); } console.log(`audit gate: no new critical/high advisories (${current.size} tracked in baseline)`);