From eeba3b0134fce20abd11a7b7e65734d67687b7bf Mon Sep 17 00:00:00 2001 From: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:36:49 -0400 Subject: [PATCH 1/2] fix: honour every package-scoped npm audit exception for one GHSA Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- ...ed-520-npm-audit-gate-scoped-exceptions.md | 2 ++ scripts/npm-audit-gate.test.ts | 36 +++++++++++++++++++ scripts/npm-audit-gate.ts | 20 +++++++---- 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fixed-520-npm-audit-gate-scoped-exceptions.md diff --git a/changelog.d/fixed-520-npm-audit-gate-scoped-exceptions.md b/changelog.d/fixed-520-npm-audit-gate-scoped-exceptions.md new file mode 100644 index 00000000..4f5e44fd --- /dev/null +++ b/changelog.d/fixed-520-npm-audit-gate-scoped-exceptions.md @@ -0,0 +1,2 @@ +- Honour every package-scoped npm audit exception when one GHSA is listed for + several packages instead of keeping only the last entry. diff --git a/scripts/npm-audit-gate.test.ts b/scripts/npm-audit-gate.test.ts index 0cf6272b..eaf25939 100644 --- a/scripts/npm-audit-gate.test.ts +++ b/scripts/npm-audit-gate.test.ts @@ -195,6 +195,42 @@ describe("evaluateAudit", () => { expect(r.unused).toHaveLength(1); }); + it("honours one scoped exception per package for the same GHSA", () => { + const report = { + vulnerabilities: { + "pkg-a": { via: [{ name: "pkg-a", url: BRACES_URL, severity: "high" }] }, + "pkg-b": { via: [{ name: "pkg-b", url: BRACES_URL, severity: "high" }] }, + }, + }; + const r = evaluateAudit( + report, + [exception({ package: "pkg-a" }), exception({ package: "pkg-b" })], + "high", + NOW + ); + expect(r.excepted.map(a => a.package)).toEqual(["pkg-a", "pkg-b"]); + expect(r.blocking).toEqual([]); + expect(r.unused).toEqual([]); + }); + + it("lets a scoped exception and an unscoped one for the same GHSA coexist", () => { + const report = { + vulnerabilities: { + "pkg-a": { via: [{ name: "pkg-a", url: BRACES_URL, severity: "high" }] }, + "pkg-b": { via: [{ name: "pkg-b", url: BRACES_URL, severity: "high" }] }, + }, + }; + const r = evaluateAudit( + report, + [exception({ package: "pkg-a" }), exception({ package: undefined })], + "high", + NOW + ); + expect(r.excepted.map(a => a.package)).toEqual(["pkg-a", "pkg-b"]); + expect(r.blocking).toEqual([]); + expect(r.unused).toEqual([]); + }); + it.each(["moderate", "high"])( "keeps the second package blocking with a %s first package and scoped exception", severity => { diff --git a/scripts/npm-audit-gate.ts b/scripts/npm-audit-gate.ts index 3caf98f1..93e5740a 100644 --- a/scripts/npm-audit-gate.ts +++ b/scripts/npm-audit-gate.ts @@ -123,6 +123,10 @@ export interface GateResult { invalid: string[]; } +function exceptionKey(id: string, pkg?: string): string { + return `${id.toUpperCase()}:${pkg ?? "*"}`; +} + export function evaluateAudit( report: AuditReport, exceptions: AuditException[], @@ -151,7 +155,7 @@ export function evaluateAudit( expired.push(ex); continue; } - active.set(ex.id.toUpperCase(), ex); + active.set(exceptionKey(ex.id, ex.package), ex); } const blocking: Advisory[] = []; @@ -163,17 +167,19 @@ export function evaluateAudit( belowLevel.push(adv); continue; } - const ex = active.get(adv.id); - if (ex && (!ex.package || ex.package === adv.package)) { - used.add(adv.id); + const key = active.has(exceptionKey(adv.id, adv.package)) + ? exceptionKey(adv.id, adv.package) + : exceptionKey(adv.id); + if (active.has(key)) { + used.add(key); excepted.push(adv); } else { blocking.push(adv); } } - const unused = [...active.values()].filter( - ex => !used.has(ex.id.toUpperCase()) - ); + const unused = [...active.entries()] + .filter(([key]) => !used.has(key)) + .map(([, ex]) => ex); return { blocking, excepted, belowLevel, expired, unused, invalid }; } From d16cc665577628189b5b0e721251591dfc1edfb4 Mon Sep 17 00:00:00 2001 From: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:04:13 -0400 Subject: [PATCH 2/2] style: format npm-audit-gate test with prettier Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- scripts/npm-audit-gate.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/npm-audit-gate.test.ts b/scripts/npm-audit-gate.test.ts index eaf25939..6871937e 100644 --- a/scripts/npm-audit-gate.test.ts +++ b/scripts/npm-audit-gate.test.ts @@ -198,8 +198,12 @@ describe("evaluateAudit", () => { it("honours one scoped exception per package for the same GHSA", () => { const report = { vulnerabilities: { - "pkg-a": { via: [{ name: "pkg-a", url: BRACES_URL, severity: "high" }] }, - "pkg-b": { via: [{ name: "pkg-b", url: BRACES_URL, severity: "high" }] }, + "pkg-a": { + via: [{ name: "pkg-a", url: BRACES_URL, severity: "high" }], + }, + "pkg-b": { + via: [{ name: "pkg-b", url: BRACES_URL, severity: "high" }], + }, }, }; const r = evaluateAudit( @@ -216,8 +220,12 @@ describe("evaluateAudit", () => { it("lets a scoped exception and an unscoped one for the same GHSA coexist", () => { const report = { vulnerabilities: { - "pkg-a": { via: [{ name: "pkg-a", url: BRACES_URL, severity: "high" }] }, - "pkg-b": { via: [{ name: "pkg-b", url: BRACES_URL, severity: "high" }] }, + "pkg-a": { + via: [{ name: "pkg-a", url: BRACES_URL, severity: "high" }], + }, + "pkg-b": { + via: [{ name: "pkg-b", url: BRACES_URL, severity: "high" }], + }, }, }; const r = evaluateAudit(