mandrel-platform 1.13.0 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,6 +49,7 @@ import {
49
49
  detectPackageManager,
50
50
  ghsaIdFromUrl,
51
51
  recognizeReport,
52
+ normalizeAdvisoryId,
52
53
  } from "./audit-check.mjs";
53
54
 
54
55
  const TODAY = "2026-07-02";
@@ -909,10 +910,18 @@ test("detectPackageManager: two lockfiles are a loud error, never a guess", () =
909
910
  // GHSA id extraction — npm exposes the id ONLY inside the advisory url
910
911
  // ---------------------------------------------------------------------------
911
912
 
912
- test("ghsaIdFromUrl: reads the id from a GitHub advisory url", () => {
913
+ test("ghsaIdFromUrl: reads the id from a GitHub advisory url, canonically", () => {
914
+ // Canonical = uppercase `GHSA-` prefix, lowercase body: what GitHub renders
915
+ // and what an operator pastes into the allowlist. This used to upper-case
916
+ // the whole id while the pnpm path kept its `ghsa_id` verbatim, so the two
917
+ // managers disagreed about what an id even looks like.
913
918
  assert.equal(
914
919
  ghsaIdFromUrl("https://github.com/advisories/GHSA-aaaa-bbbb-cccc"),
915
- "GHSA-AAAA-BBBB-CCCC",
920
+ "GHSA-aaaa-bbbb-cccc",
921
+ );
922
+ assert.equal(
923
+ ghsaIdFromUrl("https://github.com/advisories/GHSA-AAAA-BbBb-CCCC"),
924
+ "GHSA-aaaa-bbbb-cccc",
916
925
  );
917
926
  });
918
927
 
@@ -940,39 +949,86 @@ test("ghsaIdFromUrl: rejects non-urls, empty values and non-GHSA segments", () =
940
949
  // so no live npm sample carrying an advisory can be captured from it.
941
950
  // ---------------------------------------------------------------------------
942
951
 
943
- const NPM_HIGH_URL = "https://github.com/advisories/GHSA-dddd-eeee-ffff";
952
+ // The committed sample of a REAL `npm audit --json` v2 report. Every other
953
+ // fixture in this suite is built inline, and this one deliberately is not:
954
+ // the npm schema is npm's, not ours, and an inline literal drifts toward
955
+ // whatever the test needed rather than what the tool emits. Committing the
956
+ // shape means a future reader can diff it against a live `npm audit --json`.
957
+ //
958
+ // It carries no `via[].cve` key, because npm's bundled advisory calculator
959
+ // never emits one. The normalizer still READS `cve` — another producer may
960
+ // write it — but nothing in this suite asserts a field npm does not emit,
961
+ // which is how a test can otherwise certify a code path against a schema that
962
+ // does not exist.
963
+ const NPM_FIXTURE_URL = new URL("./fixtures/npm-audit-v2.json", import.meta.url);
944
964
 
945
- /** An npm v2 report with one high advisory reached through `pkg`. */
946
- function npmReportWithHigh({ severity = "high", cve = [] } = {}) {
947
- return {
948
- auditReportVersion: 2,
949
- vulnerabilities: {
950
- pkg: {
951
- name: "pkg",
952
- severity,
953
- via: [
954
- {
955
- source: 123456,
956
- name: "pkg",
957
- title: "Prototype pollution in pkg",
958
- url: NPM_HIGH_URL,
959
- severity,
960
- cve,
961
- },
962
- ],
963
- },
964
- },
965
- metadata: { vulnerabilities: { high: 1, total: 1 } },
966
- };
965
+ /** The canonical id the fixture's single advisory carries. */
966
+ const NPM_FIXTURE_ID = "GHSA-fixt-ure0-0001";
967
+
968
+ /**
969
+ * A fresh parse of the committed fixture, optionally re-severitied so the
970
+ * below-gate cases exercise the same real shape.
971
+ *
972
+ * @param {{ severity?: string }} [opts]
973
+ */
974
+ function npmFixtureReport({ severity } = {}) {
975
+ const report = JSON.parse(readFileSync(NPM_FIXTURE_URL, "utf8"));
976
+ if (severity !== undefined) {
977
+ for (const entry of Object.values(report.vulnerabilities)) {
978
+ entry.severity = severity;
979
+ for (const via of entry.via) {
980
+ if (typeof via === "object") {
981
+ via.severity = severity;
982
+ }
983
+ }
984
+ }
985
+ }
986
+ return report;
967
987
  }
968
988
 
989
+ test("#488 AC-5: the committed fixture is the real npm audit v2 shape", () => {
990
+ const report = npmFixtureReport();
991
+ assert.equal(report.auditReportVersion, 2);
992
+
993
+ const via = report.vulnerabilities["fixture-lib"].via[0];
994
+ for (const key of [
995
+ "source",
996
+ "name",
997
+ "dependency",
998
+ "title",
999
+ "url",
1000
+ "severity",
1001
+ "cwe",
1002
+ "cvss",
1003
+ "range",
1004
+ ]) {
1005
+ assert.ok(key in via, `via[] advisory must carry \`${key}\``);
1006
+ }
1007
+ assert.equal(
1008
+ "cve" in via,
1009
+ false,
1010
+ "npm's advisory calculator emits no `cve` on a via[] advisory",
1011
+ );
1012
+ });
1013
+
1014
+ test("#488 AC-5: recognizeReport normalizes the committed npm fixture", () => {
1015
+ const { schema, advisories } = recognizeReport(npmFixtureReport());
1016
+ assert.equal(schema, "npm");
1017
+ // The fixture's second package reaches the same advisory through a STRING
1018
+ // edge, so this also pins the transitive chain to one finding.
1019
+ assert.equal(advisories.length, 1);
1020
+ assert.deepEqual(advisories[0].ids, [NPM_FIXTURE_ID]);
1021
+ assert.equal(advisories[0].severity, "high");
1022
+ assert.equal(advisories[0].title, "Prototype pollution in fixture-lib");
1023
+ });
1024
+
969
1025
  test("recognizeReport: identifies each schema and normalizes its advisories", () => {
970
1026
  assert.equal(recognizeReport(reportWith({ 1: HIGH_GHSA })).schema, "legacy");
971
1027
 
972
- const npm = recognizeReport(npmReportWithHigh());
1028
+ const npm = recognizeReport(npmFixtureReport());
973
1029
  assert.equal(npm.schema, "npm");
974
1030
  assert.equal(npm.advisories.length, 1);
975
- assert.deepEqual(npm.advisories[0].ids, ["GHSA-DDDD-EEEE-FFFF"]);
1031
+ assert.deepEqual(npm.advisories[0].ids, [NPM_FIXTURE_ID]);
976
1032
  assert.equal(npm.advisories[0].severity, "high");
977
1033
  });
978
1034
 
@@ -990,55 +1046,429 @@ test("recognizeReport: an empty npm report is RECOGNIZED, not unreadable", () =>
990
1046
  });
991
1047
 
992
1048
  test("evaluateReport: an unsuppressed high in the NPM shape blocks", () => {
993
- const result = evaluateReport(npmReportWithHigh(), 1, new Set());
1049
+ const result = evaluateReport(npmFixtureReport(), 1, new Set());
994
1050
  assert.equal(result.exitCode, 1);
995
1051
  assert.equal(result.reason, "unsuppressed");
996
1052
  assert.equal(result.schema, "npm");
997
1053
  assert.equal(result.blocking.length, 1);
998
- assert.equal(result.blocking[0].id, "GHSA-DDDD-EEEE-FFFF");
1054
+ assert.equal(result.blocking[0].id, NPM_FIXTURE_ID);
999
1055
  });
1000
1056
 
1001
1057
  test("evaluateReport: an allowlisted GHSA id suppresses in the NPM shape", () => {
1002
1058
  // The id exists only inside `via[].url` here, so this is the assertion that
1003
1059
  // the allowlist still means something once the schema changes.
1004
1060
  const result = evaluateReport(
1005
- npmReportWithHigh(),
1061
+ npmFixtureReport(),
1006
1062
  1,
1007
- new Set(["GHSA-DDDD-EEEE-FFFF"]),
1063
+ new Set([NPM_FIXTURE_ID]),
1008
1064
  );
1009
1065
  assert.equal(result.exitCode, 0);
1010
1066
  assert.equal(result.reason, "clean");
1011
1067
  });
1012
1068
 
1013
- test("evaluateReport: a CVE id suppresses in the NPM shape too", () => {
1014
- const report = npmReportWithHigh({ cve: ["CVE-2026-9999"] });
1015
- assert.equal(
1016
- evaluateReport(report, 1, new Set(["CVE-2026-9999"])).exitCode,
1017
- 0,
1018
- );
1019
- assert.equal(evaluateReport(report, 1, new Set()).exitCode, 1);
1020
- });
1021
-
1022
1069
  test("evaluateReport: below-gate npm severities never block", () => {
1023
1070
  for (const severity of ["moderate", "low", "info"]) {
1024
- const result = evaluateReport(npmReportWithHigh({ severity }), 0, new Set());
1071
+ const result = evaluateReport(npmFixtureReport({ severity }), 0, new Set());
1025
1072
  assert.equal(result.exitCode, 0, severity);
1026
1073
  assert.equal(result.reason, "clean", severity);
1027
1074
  }
1028
1075
  });
1029
1076
 
1030
- test("recognizeReport: a transitive npm chain counts its advisory once", () => {
1031
- // `via` carries STRING edges for transitive chains alongside advisory
1032
- // objects. Counting an edge as an advisory would inflate the finding set;
1033
- // counting the same advisory once per reaching package would duplicate it.
1077
+ // ---------------------------------------------------------------------------
1078
+ // Story #488 — id normalization
1079
+ //
1080
+ // The allowlist is this gate's ONLY sanctioned suppression mechanism, and it
1081
+ // was inert for the id form GitHub renders and every operator pastes: the npm
1082
+ // path upper-cased the id it parsed out of `via[].url`, the pnpm path kept
1083
+ // `ghsa_id` verbatim, and `partitionAllowlist` matched with an exact
1084
+ // `Set.has`. A canonical `GHSA-7w5x-hrqm-74c2` therefore suppressed under pnpm
1085
+ // and silently did nothing under npm — the worst shape of failure available,
1086
+ // because the entry LOOKS applied.
1087
+ // ---------------------------------------------------------------------------
1088
+
1089
+ test("normalizeAdvisoryId: GHSA ids fold to the canonical rendering", () => {
1090
+ for (const spelling of [
1091
+ "GHSA-7w5x-hrqm-74c2",
1092
+ "ghsa-7w5x-hrqm-74c2",
1093
+ "GHSA-7W5X-HRQM-74C2",
1094
+ "GHSA-7W5x-HrQm-74c2",
1095
+ " GHSA-7w5x-hrqm-74c2 ",
1096
+ ]) {
1097
+ assert.equal(
1098
+ normalizeAdvisoryId(spelling),
1099
+ "GHSA-7w5x-hrqm-74c2",
1100
+ spelling,
1101
+ );
1102
+ }
1103
+ });
1104
+
1105
+ test("normalizeAdvisoryId: a CVE id folds to its own canonical upper case", () => {
1106
+ assert.equal(normalizeAdvisoryId("cve-2026-0994"), "CVE-2026-0994");
1107
+ assert.equal(normalizeAdvisoryId("CVE-2026-0994"), "CVE-2026-0994");
1108
+ });
1109
+
1110
+ test("normalizeAdvisoryId: nothing to normalize yields the empty string", () => {
1111
+ for (const value of ["", " ", null, undefined, 42, {}, []]) {
1112
+ assert.equal(normalizeAdvisoryId(value), "", JSON.stringify(value));
1113
+ }
1114
+ });
1115
+
1116
+ test("partitionAllowlist: a mixed-case allowlist id lands canonical", () => {
1117
+ const { suppressed } = partitionAllowlist(
1118
+ [{ id: "GHSA-7W5X-HRQM-74C2", reason: "accepted", expires: FUTURE }],
1119
+ TODAY,
1120
+ );
1121
+ assert.ok(suppressed.has("GHSA-7w5x-hrqm-74c2"));
1122
+ });
1123
+
1124
+ // ---------------------------------------------------------------------------
1125
+ // Story #488 — the CLI, driven through its injectable subprocess seam
1126
+ //
1127
+ // `runCli(argv, { spawnImpl })` is the seam (`.agents/rules/test-seams.md`):
1128
+ // production keeps the real `spawnSync`, and these cases substitute a stub so
1129
+ // the whole CLI path — allowlist, manager detection, audit invocation, report
1130
+ // evaluation, exit code — is executable without spawning a package manager or
1131
+ // reaching a registry. Before it existed, every one of those paths was
1132
+ // reachable only by running a real audit, which is why the ones below shipped
1133
+ // broken.
1134
+ // ---------------------------------------------------------------------------
1135
+
1136
+ /** A recording `spawnSync` stub returning a canned result. */
1137
+ function stubSpawn(result, calls = []) {
1138
+ return (bin, args, options) => {
1139
+ calls.push({ bin, args, options });
1140
+ return { status: 0, stdout: "", stderr: "", ...result };
1141
+ };
1142
+ }
1143
+
1144
+ /**
1145
+ * Materialize a throwaway project — package.json, one lockfile, an optional
1146
+ * allowlist — and run `fn` against its absolute argv.
1147
+ *
1148
+ * @param {{ lockfile: string; allowlist?: unknown[] }} spec
1149
+ * @param {(ctx: { dir: string; argv: string[] }) => void} fn
1150
+ */
1151
+ function withProject({ lockfile, allowlist }, fn) {
1152
+ const dir = mkdtempSync(join(tmpdir(), "audit-check-cli-"));
1153
+ try {
1154
+ const packageJsonPath = join(dir, "package.json");
1155
+ writeFileSync(
1156
+ packageJsonPath,
1157
+ JSON.stringify({ name: "fixture", version: "1.0.0" }),
1158
+ );
1159
+ writeFileSync(
1160
+ join(dir, lockfile),
1161
+ lockfile.endsWith(".json") ? "{}\n" : "lockfileVersion: '9.0'\n",
1162
+ );
1163
+
1164
+ const allowlistPath = join(dir, "audit-allowlist.json");
1165
+ if (allowlist !== undefined) {
1166
+ writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2));
1167
+ }
1168
+
1169
+ fn({
1170
+ dir,
1171
+ argv: ["--package-json", packageJsonPath, "--allowlist", allowlistPath],
1172
+ });
1173
+ } finally {
1174
+ rmSync(dir, { recursive: true, force: true });
1175
+ }
1176
+ }
1177
+
1178
+ /** Run `fn` with console output collected rather than printed. */
1179
+ function captureConsole(fn) {
1180
+ /** @type {string[]} */
1181
+ const lines = [];
1182
+ const originalLog = console.log;
1183
+ const originalError = console.error;
1184
+ console.log = (...args) => lines.push(args.map(String).join(" "));
1185
+ console.error = (...args) => lines.push(args.map(String).join(" "));
1186
+ try {
1187
+ return { value: fn(), output: lines.join("\n") };
1188
+ } finally {
1189
+ console.log = originalLog;
1190
+ console.error = originalError;
1191
+ }
1192
+ }
1193
+
1194
+ test("#488 AC-1: a canonical lowercase allowlist id suppresses an npm advisory written in any case", () => {
1195
+ // The npm report exposes its id ONLY inside `via[].url`, and GitHub's own
1196
+ // links are mixed-case-tolerant. The allowlist entry is written exactly as
1197
+ // the runbook shows it.
1198
+ const report = npmFixtureReport();
1199
+ report.vulnerabilities["fixture-lib"].via[0].url =
1200
+ "https://github.com/advisories/GHSA-7W5X-HrQm-74C2";
1201
+ const stdout = JSON.stringify(report);
1202
+
1203
+ withProject(
1204
+ {
1205
+ lockfile: "package-lock.json",
1206
+ allowlist: [
1207
+ {
1208
+ id: "GHSA-7w5x-hrqm-74c2",
1209
+ reason: "no fix available upstream yet",
1210
+ expires: FUTURE,
1211
+ },
1212
+ ],
1213
+ },
1214
+ ({ argv }) => {
1215
+ const { value } = captureConsole(() =>
1216
+ runCli(argv, { spawnImpl: stubSpawn({ status: 1, stdout }) }),
1217
+ );
1218
+ assert.equal(value, 0, "the canonical id must suppress under npm");
1219
+ },
1220
+ );
1221
+
1222
+ // Control: the same advisory with nothing allowlisted still blocks, so the
1223
+ // exit 0 above is suppression rather than a report that was never read.
1224
+ withProject({ lockfile: "package-lock.json" }, ({ argv }) => {
1225
+ const { value } = captureConsole(() =>
1226
+ runCli(argv, { spawnImpl: stubSpawn({ status: 1, stdout }) }),
1227
+ );
1228
+ assert.equal(value, 1);
1229
+ });
1230
+ });
1231
+
1232
+ test("#488 AC-1: the same canonical id suppresses a pnpm advisory written in any case", () => {
1233
+ const stdout = JSON.stringify(
1234
+ reportWith({
1235
+ 1: {
1236
+ ghsa_id: "GHSA-7W5x-HrQm-74C2",
1237
+ severity: "high",
1238
+ title: "High severity in a transitive dep",
1239
+ url: "https://github.com/advisories/GHSA-7w5x-hrqm-74c2",
1240
+ },
1241
+ }),
1242
+ );
1243
+
1244
+ withProject(
1245
+ {
1246
+ lockfile: "pnpm-lock.yaml",
1247
+ allowlist: [
1248
+ {
1249
+ id: "GHSA-7w5x-hrqm-74c2",
1250
+ reason: "no fix available upstream yet",
1251
+ expires: FUTURE,
1252
+ },
1253
+ ],
1254
+ },
1255
+ ({ argv }) => {
1256
+ const { value } = captureConsole(() =>
1257
+ runCli(argv, { spawnImpl: stubSpawn({ status: 1, stdout }) }),
1258
+ );
1259
+ assert.equal(value, 0, "the canonical id must suppress under pnpm");
1260
+ },
1261
+ );
1262
+
1263
+ withProject({ lockfile: "pnpm-lock.yaml" }, ({ argv }) => {
1264
+ const { value } = captureConsole(() =>
1265
+ runCli(argv, { spawnImpl: stubSpawn({ status: 1, stdout }) }),
1266
+ );
1267
+ assert.equal(value, 1);
1268
+ });
1269
+ });
1270
+
1271
+ test("#488 AC-2: the audit runs in the directory whose lockfile was detected, bounded", () => {
1272
+ // `detectPackageManager` reads `dirname(--package-json)`; the audit used to
1273
+ // run in `process.cwd()`. When those differ the gate reported on a graph it
1274
+ // never audited — an unfalsifiable claim, which is the one thing this gate
1275
+ // must not produce.
1276
+ const calls = [];
1277
+ const stdout = JSON.stringify({
1278
+ auditReportVersion: 2,
1279
+ vulnerabilities: {},
1280
+ metadata: {},
1281
+ });
1282
+
1283
+ withProject({ lockfile: "package-lock.json" }, ({ dir, argv }) => {
1284
+ const { value } = captureConsole(() =>
1285
+ runCli(argv, { spawnImpl: stubSpawn({ status: 0, stdout }, calls) }),
1286
+ );
1287
+ assert.equal(value, 0);
1288
+ assert.equal(calls.length, 1);
1289
+
1290
+ const [call] = calls;
1291
+ assert.equal(call.bin, "npm");
1292
+ assert.deepEqual(call.args, ["audit", "--omit=dev", "--json"]);
1293
+ assert.equal(call.options.cwd, dir);
1294
+ assert.ok(
1295
+ typeof call.options.timeout === "number" && call.options.timeout > 0,
1296
+ "the audit must be time-bounded",
1297
+ );
1298
+ assert.ok(
1299
+ call.options.maxBuffer >= 64 * 1024 * 1024,
1300
+ `maxBuffer must be at least 64 MiB, got ${call.options.maxBuffer}`,
1301
+ );
1302
+ // No shell string, ever: the argv form is what keeps this call off an
1303
+ // injection surface, and what kept stderr out of `/dev/null`.
1304
+ assert.equal(call.options.shell, undefined);
1305
+ });
1306
+ });
1307
+
1308
+ test("#488 AC-2: the pnpm branch spawns pnpm's own argv in the detected directory", () => {
1309
+ const calls = [];
1310
+ const stdout = JSON.stringify({ advisories: {}, metadata: {} });
1311
+
1312
+ withProject({ lockfile: "pnpm-lock.yaml" }, ({ dir, argv }) => {
1313
+ const { value } = captureConsole(() =>
1314
+ runCli(argv, { spawnImpl: stubSpawn({ status: 0, stdout }, calls) }),
1315
+ );
1316
+ assert.equal(value, 0);
1317
+ assert.equal(calls[0].bin, "pnpm");
1318
+ assert.deepEqual(calls[0].args, ["audit", "--prod", "--json"]);
1319
+ assert.equal(calls[0].options.cwd, dir);
1320
+ });
1321
+ });
1322
+
1323
+ test("#488 AC-3: an audit that says nothing is never clean, and its stderr is shown", () => {
1324
+ // Exit 0 with unreadable stdout is how EVERY failure to run at all presents:
1325
+ // a missing binary, a killed child, an output ceiling hit mid-write. The old
1326
+ // contract printed "No vulnerabilities found" and exited 0 for all of them.
1327
+ const stderrText = "npm error code ENETUNREACH: registry unreachable";
1328
+
1329
+ for (const [label, stdout] of [
1330
+ ["empty stdout", ""],
1331
+ ["whitespace only", " \n"],
1332
+ ["a human-readable notice", "up to date, audited 214 packages\n"],
1333
+ ]) {
1334
+ withProject({ lockfile: "package-lock.json" }, ({ argv }) => {
1335
+ const { value, output } = captureConsole(() =>
1336
+ runCli(argv, {
1337
+ spawnImpl: stubSpawn({ status: 0, stdout, stderr: stderrText }),
1338
+ }),
1339
+ );
1340
+ assert.equal(value, 1, label);
1341
+ assert.match(output, /Failing closed/, label);
1342
+ assert.ok(output.includes(stderrText), `${label} must show the stderr`);
1343
+ assert.ok(
1344
+ !output.includes("No vulnerabilities found"),
1345
+ `${label} must not report clean`,
1346
+ );
1347
+ });
1348
+ }
1349
+ });
1350
+
1351
+ test("#488 AC-3: a spawn that never ran at all fails closed with its reason", () => {
1352
+ // `spawnSync` reports a missing binary or a fired timeout through `error`,
1353
+ // with no exit status at all.
1354
+ withProject({ lockfile: "package-lock.json" }, ({ argv }) => {
1355
+ const { value, output } = captureConsole(() =>
1356
+ runCli(argv, {
1357
+ spawnImpl: () => ({
1358
+ status: null,
1359
+ stdout: "",
1360
+ stderr: "",
1361
+ error: new Error("spawnSync npm ENOENT"),
1362
+ }),
1363
+ }),
1364
+ );
1365
+ assert.equal(value, 1);
1366
+ assert.match(output, /ENOENT/);
1367
+ });
1368
+ });
1369
+
1370
+ test("#488 AC-3: a parsed report matching no known schema still fails closed", () => {
1371
+ withProject({ lockfile: "package-lock.json" }, ({ argv }) => {
1372
+ const { value, output } = captureConsole(() =>
1373
+ runCli(argv, {
1374
+ spawnImpl: stubSpawn({
1375
+ status: 0,
1376
+ stdout: JSON.stringify({ auditReportVersion: 3, findings: [] }),
1377
+ stderr: "npm warn audit schema changed",
1378
+ }),
1379
+ }),
1380
+ );
1381
+ assert.equal(value, 1);
1382
+ assert.match(output, /matching no known audit schema/);
1383
+ assert.match(output, /npm warn audit schema changed/);
1384
+ });
1385
+ });
1386
+
1387
+ test("#488 AC-4: a blocking advisory with no parseable id is reported, never dropped", () => {
1388
+ // The npm identity fallback was `ghsaId ?? source ?? url`, and `source` is
1389
+ // coerced to `""` when absent — so `"" ?? url` is `""`, the url arm was dead
1390
+ // code, and the empty key hit a `continue` that DISCARDED the advisory. A
1391
+ // Critical silently vanished for lacking a name.
1034
1392
  const report = {
1035
1393
  auditReportVersion: 2,
1036
1394
  vulnerabilities: {
1037
- pkg: npmReportWithHigh().vulnerabilities.pkg,
1038
- dependent: { name: "dependent", severity: "high", via: ["pkg"] },
1039
- other: npmReportWithHigh().vulnerabilities.pkg,
1395
+ nameless: {
1396
+ name: "nameless",
1397
+ severity: "critical",
1398
+ via: [{ name: "nameless", title: "Critical with no id", severity: "critical" }],
1399
+ },
1040
1400
  },
1041
1401
  metadata: {},
1042
1402
  };
1043
- assert.equal(recognizeReport(report).advisories.length, 1);
1403
+
1404
+ const result = evaluateReport(report, 0, new Set());
1405
+ assert.equal(result.exitCode, 1);
1406
+ assert.equal(result.reason, "unsuppressed");
1407
+ assert.equal(result.blocking.length, 1);
1408
+ assert.equal(result.blocking[0].id, "(unknown)");
1409
+ assert.equal(result.blocking[0].title, "Critical with no id");
1410
+ });
1411
+
1412
+ test("#488 AC-4: two id-less advisories are two findings, not one collapsed key", () => {
1413
+ const report = {
1414
+ auditReportVersion: 2,
1415
+ vulnerabilities: {
1416
+ first: {
1417
+ name: "first",
1418
+ severity: "high",
1419
+ via: [{ name: "first", title: "First nameless high", severity: "high" }],
1420
+ },
1421
+ second: {
1422
+ name: "second",
1423
+ severity: "critical",
1424
+ via: [{ name: "second", title: "Second nameless critical", severity: "critical" }],
1425
+ },
1426
+ },
1427
+ metadata: {},
1428
+ };
1429
+
1430
+ assert.equal(evaluateReport(report, 0, new Set()).blocking.length, 2);
1431
+ });
1432
+
1433
+ test("#488 AC-4: an id-less advisory blocks through the CLI and prints (unknown)", () => {
1434
+ const stdout = JSON.stringify({
1435
+ auditReportVersion: 2,
1436
+ vulnerabilities: {
1437
+ nameless: {
1438
+ name: "nameless",
1439
+ severity: "critical",
1440
+ via: [{ name: "nameless", title: "Critical with no id", severity: "critical" }],
1441
+ },
1442
+ },
1443
+ metadata: {},
1444
+ });
1445
+
1446
+ withProject({ lockfile: "package-lock.json" }, ({ argv }) => {
1447
+ const { value, output } = captureConsole(() =>
1448
+ runCli(argv, { spawnImpl: stubSpawn({ status: 1, stdout }) }),
1449
+ );
1450
+ assert.equal(value, 1);
1451
+ assert.match(output, /\(unknown\)/);
1452
+ });
1453
+ });
1454
+
1455
+ test("#488 AC-4: an id-less advisory in the legacy schema blocks too", () => {
1456
+ const report = reportWith({
1457
+ 1: { severity: "critical", title: "Legacy critical with no id" },
1458
+ });
1459
+ const result = evaluateReport(report, 0, new Set());
1460
+ assert.equal(result.blocking.length, 1);
1461
+ assert.equal(result.blocking[0].id, "(unknown)");
1462
+ });
1463
+
1464
+ test("#488 AC-5: the committed fixture blocks through the CLI, naming its canonical id", () => {
1465
+ const stdout = JSON.stringify(npmFixtureReport());
1466
+
1467
+ withProject({ lockfile: "package-lock.json" }, ({ argv }) => {
1468
+ const { value, output } = captureConsole(() =>
1469
+ runCli(argv, { spawnImpl: stubSpawn({ status: 1, stdout }) }),
1470
+ );
1471
+ assert.equal(value, 1);
1472
+ assert.ok(output.includes(NPM_FIXTURE_ID), output);
1473
+ });
1044
1474
  });