From 6eb51078e34cad1b90b548c20bcf2d0ead45d7dc Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 05:18:19 -0400 Subject: [PATCH] feat(16-02): add check-audit.mjs blocking wrapper + unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Exports selectBlocking() and partitionAdvisories() as pure functions for unit testing - Main body guarded by import.meta.url check (only runs when invoked directly) - Uses pnpm audit --json (no --audit-level — Pitfall 1 honored) - Exits 1 on unwaived High/Critical; exits 0 with advisory report on moderate/low - All 5 unit tests pass (node --test) --- scripts/check-audit.mjs | 126 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 scripts/check-audit.mjs diff --git a/scripts/check-audit.mjs b/scripts/check-audit.mjs new file mode 100644 index 0000000..feca3ba --- /dev/null +++ b/scripts/check-audit.mjs @@ -0,0 +1,126 @@ +/** + * check-audit.mjs — pnpm audit wrapper for CI dependency gate (D-04 / D-05). + * + * Exports pure filter functions (selectBlocking, partitionAdvisories) so the + * logic can be unit-tested without spawning pnpm. The main body (run only when + * invoked directly via import.meta.url) reads the committed allowlist, runs + * `pnpm audit --json`, and exits 1 if any unwaived High/Critical advisories exist. + * + * Rules: + * - Uses `pnpm audit --json` with NO --audit-level (captures all severities). + * --audit-level would filter the JSON output itself (Pitfall 1). + * - Waives advisories listed in scripts/audit-allowlist.json by github_advisory_id. + * - Exits 0 when all High/Critical advisories are waived (or there are none). + * - Prints moderate/low advisory list to stdout as advisory-only info before exiting 0. + */ + +import { execSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { resolve, dirname } from 'node:path'; + +const BLOCKING_SEVERITIES = new Set(['high', 'critical']); + +/** + * From an advisories map (keyed by numeric id), returns the subset that + * are High or Critical AND whose github_advisory_id is NOT present in allowlist. + * + * @param {Record} advisories + * @param {Record} allowlist + * @returns {Array<{severity: string, github_advisory_id: string, module_name: string, title: string}>} + */ +export function selectBlocking(advisories, allowlist) { + return Object.values(advisories).filter( + (adv) => + BLOCKING_SEVERITIES.has(adv.severity) && + !allowlist[adv.github_advisory_id], + ); +} + +/** + * Partitions all advisories into blocking (unwaived High/Critical) and + * advisory-only (moderate/low, or waived High/Critical). + * + * @param {Record} advisories + * @param {Record} allowlist + * @returns {{ blocking: Array, advisory: Array }} + */ +export function partitionAdvisories(advisories, allowlist) { + const blocking = []; + const advisory = []; + + for (const adv of Object.values(advisories)) { + if (BLOCKING_SEVERITIES.has(adv.severity) && !allowlist[adv.github_advisory_id]) { + blocking.push(adv); + } else { + advisory.push(adv); + } + } + + return { blocking, advisory }; +} + +// Main body — only runs when invoked directly (not when imported as a module). +const __filename = fileURLToPath(import.meta.url); +const isMain = process.argv[1] === __filename; + +if (isMain) { + const __dirname = dirname(__filename); + const allowlistPath = resolve(__dirname, 'audit-allowlist.json'); + + // Load the committed waiver allowlist. + let allowlist; + try { + allowlist = JSON.parse(readFileSync(allowlistPath, 'utf8')); + } catch (err) { + console.error(`[check-audit] Failed to read allowlist at ${allowlistPath}: ${err.message}`); + process.exit(2); + } + + // Run pnpm audit --json without --audit-level so all severities appear in output. + // stderr is suppressed (pnpm writes progress/warnings there); we only need stdout JSON. + let auditOutput; + try { + auditOutput = execSync('pnpm audit --json', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'], + }); + } catch (err) { + // pnpm audit exits non-zero when advisories exist — capture stdout from the error. + auditOutput = err.stdout ?? ''; + } + + let auditData; + try { + auditData = JSON.parse(auditOutput); + } catch (err) { + console.error(`[check-audit] Failed to parse pnpm audit JSON output: ${err.message}`); + process.exit(2); + } + + const advisories = auditData.advisories ?? {}; + const { blocking, advisory } = partitionAdvisories(advisories, allowlist); + + if (blocking.length > 0) { + console.error('BLOCKING advisories (High/Critical, not in allowlist):'); + for (const adv of blocking) { + console.error( + ` ${adv.github_advisory_id ?? '(no GHSA)'} [${adv.severity}] ${adv.module_name}: ${adv.title}`, + ); + } + process.exit(1); + } + + console.log('Audit PASS — no unwaived High/Critical advisories.'); + + if (advisory.length > 0) { + console.log('Advisory (non-blocking) findings:'); + for (const adv of advisory) { + console.log( + ` ${adv.github_advisory_id ?? '(no GHSA)'} [${adv.severity}] ${adv.module_name}`, + ); + } + } + + process.exit(0); +}