From 4bb205fe0f9c7671e82ea4b6c9e2e33bb2ad4834 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 08:42:08 -0400 Subject: [PATCH] fix(16): CR-01 enforce audit-waiver expiry; IN-01 realpath isMain Add isWaived() predicate: a waiver with a past 'expires' date is treated as absent so the High/Critical advisory re-blocks. Applied in both selectBlocking and partitionAdvisories. Add expired-waiver unit tests. isMain now compares fully-resolved real paths (mirrors index.ts). --- scripts/__tests__/check-audit.test.mjs | 40 ++++++++++++++++++++- scripts/check-audit.mjs | 49 ++++++++++++++++++++------ 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/scripts/__tests__/check-audit.test.mjs b/scripts/__tests__/check-audit.test.mjs index da058f2..d6aaa3d 100644 --- a/scripts/__tests__/check-audit.test.mjs +++ b/scripts/__tests__/check-audit.test.mjs @@ -10,7 +10,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -import { selectBlocking, partitionAdvisories } from '../check-audit.mjs'; +import { selectBlocking, partitionAdvisories, isWaived } from '../check-audit.mjs'; // Fixture: a High advisory not in the allowlist const highUnwaived = { @@ -59,6 +59,23 @@ const allowlist = { const emptyAllowlist = {}; +// Fixture: allowlist whose esbuild waiver has already expired (CR-01). +const expiredAllowlist = { + 'GHSA-gv7w-rqvm-qjhr': { + reason: 'esbuild dev transitive — not in production runtime', + reviewer: 'luc', + expires: '2000-01-01', + }, +}; + +// Fixture: allowlist with no expiry field (waives indefinitely). +const noExpiryAllowlist = { + 'GHSA-gv7w-rqvm-qjhr': { + reason: 'esbuild dev transitive — not in production runtime', + reviewer: 'luc', + }, +}; + test('unwaived High advisory is blocking', () => { const blocking = selectBlocking(highUnwaived, emptyAllowlist); assert.equal(blocking.length, 1); @@ -86,3 +103,24 @@ test('partitionAdvisories splits blocking and advisory correctly', () => { assert.equal(blocking.length, 1); assert.equal(advisory.length, 2); }); + +test('expired waiver is treated as absent — High advisory re-blocks (selectBlocking)', () => { + const blocking = selectBlocking(highWaived, expiredAllowlist); + assert.equal(blocking.length, 1); + assert.equal(blocking[0].github_advisory_id, 'GHSA-gv7w-rqvm-qjhr'); +}); + +test('expired waiver is treated as absent — High advisory re-blocks (partitionAdvisories)', () => { + const { blocking, advisory } = partitionAdvisories(highWaived, expiredAllowlist); + assert.equal(blocking.length, 1); + assert.equal(advisory.length, 0); + assert.equal(blocking[0].github_advisory_id, 'GHSA-gv7w-rqvm-qjhr'); +}); + +test('isWaived: future expiry waives, past expiry does not, missing entry does not', () => { + const adv = { severity: 'high', github_advisory_id: 'GHSA-gv7w-rqvm-qjhr' }; + assert.equal(isWaived(adv, allowlist), true); // future expiry (2026-09-01) + assert.equal(isWaived(adv, expiredAllowlist), false); // past expiry + assert.equal(isWaived(adv, noExpiryAllowlist), true); // no expiry → indefinite waive + assert.equal(isWaived(adv, emptyAllowlist), false); // not listed +}); diff --git a/scripts/check-audit.mjs b/scripts/check-audit.mjs index feca3ba..5b9719d 100644 --- a/scripts/check-audit.mjs +++ b/scripts/check-audit.mjs @@ -15,34 +15,53 @@ */ import { execSync } from 'node:child_process'; -import { readFileSync } from 'node:fs'; +import { readFileSync, realpathSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve, dirname } from 'node:path'; const BLOCKING_SEVERITIES = new Set(['high', 'critical']); +/** + * Decides whether an advisory is currently waived by the allowlist. + * + * A waiver entry suppresses the advisory ONLY while it is in force: an entry + * with no `expires` field, or with an `expires` date strictly in the future, + * waives the advisory. An entry whose `expires` date is in the past (≤ now) is + * treated as absent — the advisory re-blocks. This makes the time-boxed waiver + * actually time-boxed (CR-01). + * + * @param {{severity: string, github_advisory_id: string}} adv + * @param {Record} allowlist + * @returns {boolean} + */ +export function isWaived(adv, allowlist) { + const w = allowlist[adv.github_advisory_id]; + if (!w) return false; + // No expiry or future expiry → waived; past (or equal) expiry → NOT waived (re-blocks). + if (w.expires && Date.parse(w.expires) <= Date.now()) return false; + return true; +} + /** * 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. + * are High or Critical AND whose github_advisory_id is NOT currently waived. * * @param {Record} advisories - * @param {Record} allowlist + * @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], + (adv) => BLOCKING_SEVERITIES.has(adv.severity) && !isWaived(adv, allowlist), ); } /** * Partitions all advisories into blocking (unwaived High/Critical) and - * advisory-only (moderate/low, or waived High/Critical). + * advisory-only (moderate/low, or currently-waived High/Critical). * * @param {Record} advisories - * @param {Record} allowlist + * @param {Record} allowlist * @returns {{ blocking: Array, advisory: Array }} */ export function partitionAdvisories(advisories, allowlist) { @@ -50,7 +69,7 @@ export function partitionAdvisories(advisories, allowlist) { const advisory = []; for (const adv of Object.values(advisories)) { - if (BLOCKING_SEVERITIES.has(adv.severity) && !allowlist[adv.github_advisory_id]) { + if (BLOCKING_SEVERITIES.has(adv.severity) && !isWaived(adv, allowlist)) { blocking.push(adv); } else { advisory.push(adv); @@ -61,8 +80,18 @@ export function partitionAdvisories(advisories, allowlist) { } // Main body — only runs when invoked directly (not when imported as a module). +// IN-01: compare fully-resolved real paths (mirrors index.ts isMainModule) so a +// symlinked or non-canonical entrypoint does not silently skip the gate. const __filename = fileURLToPath(import.meta.url); -const isMain = process.argv[1] === __filename; +function isMainModule() { + if (!process.argv[1]) return false; + try { + return __filename === realpathSync(process.argv[1]); + } catch { + return false; + } +} +const isMain = isMainModule(); if (isMain) { const __dirname = dirname(__filename);