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).
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { test } from 'node:test';
|
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
|
// Fixture: a High advisory not in the allowlist
|
||||||
const highUnwaived = {
|
const highUnwaived = {
|
||||||
@@ -59,6 +59,23 @@ const allowlist = {
|
|||||||
|
|
||||||
const emptyAllowlist = {};
|
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', () => {
|
test('unwaived High advisory is blocking', () => {
|
||||||
const blocking = selectBlocking(highUnwaived, emptyAllowlist);
|
const blocking = selectBlocking(highUnwaived, emptyAllowlist);
|
||||||
assert.equal(blocking.length, 1);
|
assert.equal(blocking.length, 1);
|
||||||
@@ -86,3 +103,24 @@ test('partitionAdvisories splits blocking and advisory correctly', () => {
|
|||||||
assert.equal(blocking.length, 1);
|
assert.equal(blocking.length, 1);
|
||||||
assert.equal(advisory.length, 2);
|
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
|
||||||
|
});
|
||||||
|
|||||||
+39
-10
@@ -15,34 +15,53 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync, realpathSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { resolve, dirname } from 'node:path';
|
import { resolve, dirname } from 'node:path';
|
||||||
|
|
||||||
const BLOCKING_SEVERITIES = new Set(['high', 'critical']);
|
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<string, {reason: string, reviewer: string, expires?: string}>} 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
|
* 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<string, {severity: string, github_advisory_id: string, module_name: string, title: string}>} advisories
|
* @param {Record<string, {severity: string, github_advisory_id: string, module_name: string, title: string}>} advisories
|
||||||
* @param {Record<string, {reason: string, reviewer: string, expires: string}>} allowlist
|
* @param {Record<string, {reason: string, reviewer: string, expires?: string}>} allowlist
|
||||||
* @returns {Array<{severity: string, github_advisory_id: string, module_name: string, title: string}>}
|
* @returns {Array<{severity: string, github_advisory_id: string, module_name: string, title: string}>}
|
||||||
*/
|
*/
|
||||||
export function selectBlocking(advisories, allowlist) {
|
export function selectBlocking(advisories, allowlist) {
|
||||||
return Object.values(advisories).filter(
|
return Object.values(advisories).filter(
|
||||||
(adv) =>
|
(adv) => BLOCKING_SEVERITIES.has(adv.severity) && !isWaived(adv, allowlist),
|
||||||
BLOCKING_SEVERITIES.has(adv.severity) &&
|
|
||||||
!allowlist[adv.github_advisory_id],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Partitions all advisories into blocking (unwaived High/Critical) and
|
* 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<string, {severity: string, github_advisory_id: string, module_name: string, title: string}>} advisories
|
* @param {Record<string, {severity: string, github_advisory_id: string, module_name: string, title: string}>} advisories
|
||||||
* @param {Record<string, {reason: string, reviewer: string, expires: string}>} allowlist
|
* @param {Record<string, {reason: string, reviewer: string, expires?: string}>} allowlist
|
||||||
* @returns {{ blocking: Array, advisory: Array }}
|
* @returns {{ blocking: Array, advisory: Array }}
|
||||||
*/
|
*/
|
||||||
export function partitionAdvisories(advisories, allowlist) {
|
export function partitionAdvisories(advisories, allowlist) {
|
||||||
@@ -50,7 +69,7 @@ export function partitionAdvisories(advisories, allowlist) {
|
|||||||
const advisory = [];
|
const advisory = [];
|
||||||
|
|
||||||
for (const adv of Object.values(advisories)) {
|
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);
|
blocking.push(adv);
|
||||||
} else {
|
} else {
|
||||||
advisory.push(adv);
|
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).
|
// 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 __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) {
|
if (isMain) {
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|||||||
Reference in New Issue
Block a user