Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/fixed-520-npm-audit-gate-scoped-exceptions.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 44 additions & 0 deletions scripts/npm-audit-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,50 @@ 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 => {
Expand Down
20 changes: 13 additions & 7 deletions scripts/npm-audit-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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 };
}
Expand Down
Loading