@cyclonedx/cdxgen 12.3.3 → 12.4.1

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.
Files changed (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -1,7 +1,11 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
1
4
  import { PackageURL } from "packageurl-js";
2
5
 
3
6
  import { hasTrustedPublishingProperties } from "../helpers/provenanceUtils.js";
4
7
  import {
8
+ dirNameStr,
5
9
  getCratesMetadata,
6
10
  getNpmMetadata,
7
11
  getPyMetadata,
@@ -9,13 +13,126 @@ import {
9
13
 
10
14
  const SUPPORTED_PURL_TYPES = new Set(["cargo", "npm", "pypi"]);
11
15
  const NON_REQUIRED_SCOPES = new Set(["excluded", "optional"]);
16
+ const BUILTIN_PREDICTIVE_AUDIT_ALLOWLIST = Object.freeze(
17
+ loadAllowlistPrefixes(
18
+ join(dirNameStr, "data", "predictive-audit-allowlist.json"),
19
+ "built-in predictive audit allowlist",
20
+ ),
21
+ );
22
+
23
+ function normalizeAllowlistPrefix(prefix) {
24
+ if (typeof prefix !== "string") {
25
+ return undefined;
26
+ }
27
+ const normalizedPrefix = prefix.trim().toLowerCase();
28
+ return normalizedPrefix.startsWith("pkg:") ? normalizedPrefix : undefined;
29
+ }
30
+
31
+ function normalizeAllowlistPrefixes(prefixes) {
32
+ return [
33
+ ...new Set((prefixes || []).map(normalizeAllowlistPrefix).filter(Boolean)),
34
+ ];
35
+ }
36
+
37
+ function parseAllowlistPrefixes(rawContent, sourceLabel) {
38
+ const trimmedContent = rawContent?.trim();
39
+ if (!trimmedContent) {
40
+ return [];
41
+ }
42
+ if (trimmedContent.startsWith("[") || trimmedContent.startsWith("{")) {
43
+ const parsedValue = JSON.parse(trimmedContent);
44
+ if (Array.isArray(parsedValue)) {
45
+ return normalizeAllowlistPrefixes(parsedValue);
46
+ }
47
+ if (Array.isArray(parsedValue?.prefixes)) {
48
+ return normalizeAllowlistPrefixes(parsedValue.prefixes);
49
+ }
50
+ throw new Error(
51
+ `${sourceLabel} must be a JSON array or an object with a 'prefixes' array.`,
52
+ );
53
+ }
54
+ return normalizeAllowlistPrefixes(
55
+ trimmedContent
56
+ .split(/\r?\n/u)
57
+ .map((line) => line.replace(/\s+#.*$/u, "").trim())
58
+ .filter(Boolean),
59
+ );
60
+ }
61
+
62
+ function loadAllowlistPrefixes(filePath, sourceLabel) {
63
+ let rawContent;
64
+ try {
65
+ rawContent = readFileSync(filePath, "utf8");
66
+ } catch (error) {
67
+ const errorCodeSuffix =
68
+ typeof error?.code === "string" ? ` (${error.code})` : "";
69
+ throw new Error(
70
+ `Failed to load ${sourceLabel} from ${filePath}${errorCodeSuffix}.`,
71
+ {
72
+ cause: error,
73
+ },
74
+ );
75
+ }
76
+ try {
77
+ return parseAllowlistPrefixes(rawContent, sourceLabel);
78
+ } catch (error) {
79
+ throw new Error(
80
+ `Invalid ${sourceLabel} in ${filePath}. Use a JSON array, a JSON object with a 'prefixes' array, or newline-delimited purl prefixes.`,
81
+ {
82
+ cause: error,
83
+ },
84
+ );
85
+ }
86
+ }
87
+
88
+ function getAllowlistPrefixes(options) {
89
+ const customPrefixes = options?.allowlistFile
90
+ ? loadAllowlistPrefixes(options.allowlistFile, "predictive audit allowlist")
91
+ : normalizeAllowlistPrefixes(options?.allowlistPrefixes);
92
+ return normalizeAllowlistPrefixes([
93
+ ...BUILTIN_PREDICTIVE_AUDIT_ALLOWLIST,
94
+ ...customPrefixes,
95
+ ]);
96
+ }
97
+
98
+ /**
99
+ * Find the first allowlisted purl prefix that matches a component purl using
100
+ * a real purl boundary.
101
+ *
102
+ * This avoids over-filtering when one package name is only a lexical prefix of
103
+ * another package name (for example `pkg:npm/npm` vs `pkg:npm/npm-run-all`).
104
+ *
105
+ * @param {string | undefined} componentPurl Candidate component purl
106
+ * @param {string[] | undefined} allowlistPrefixes Normalized allowlist prefixes
107
+ * @returns {string | undefined} matched allowlist prefix, if any
108
+ */
109
+ function findMatchingAllowlistPrefix(componentPurl, allowlistPrefixes) {
110
+ const normalizedPurl = componentPurl?.toLowerCase();
111
+ if (!normalizedPurl || !allowlistPrefixes?.length) {
112
+ return undefined;
113
+ }
114
+ return allowlistPrefixes.find((prefix) => {
115
+ if (!normalizedPurl.startsWith(prefix)) {
116
+ return false;
117
+ }
118
+ const boundaryCharacter = normalizedPurl[prefix.length];
119
+ return (
120
+ // Purl boundaries after a matched prefix can be the version separator,
121
+ // namespace/package separator, qualifier separator, or subpath separator.
122
+ boundaryCharacter === undefined ||
123
+ ["/", "@", "?", "#"].includes(boundaryCharacter)
124
+ );
125
+ });
126
+ }
12
127
 
13
128
  /**
14
129
  * Normalize predictive audit target selection options.
15
130
  *
16
131
  * @param {number | object | undefined} options selector options or legacy maxTargets value
17
132
  * @returns {{
133
+ * allowlistPrefixes: string[],
18
134
  * maxTargets: number | undefined,
135
+ * prioritizeDirectRuntime: boolean,
19
136
  * scope: string | undefined,
20
137
  * trusted: "exclude" | "include" | "only",
21
138
  * }} normalized options
@@ -23,6 +140,7 @@ const NON_REQUIRED_SCOPES = new Set(["excluded", "optional"]);
23
140
  function normalizeTargetSelectionOptions(options) {
24
141
  if (typeof options === "number") {
25
142
  return {
143
+ allowlistPrefixes: BUILTIN_PREDICTIVE_AUDIT_ALLOWLIST,
26
144
  maxTargets: options,
27
145
  prioritizeDirectRuntime: true,
28
146
  scope: undefined,
@@ -30,6 +148,7 @@ function normalizeTargetSelectionOptions(options) {
30
148
  };
31
149
  }
32
150
  return {
151
+ allowlistPrefixes: getAllowlistPrefixes(options),
33
152
  maxTargets: options?.maxTargets,
34
153
  prioritizeDirectRuntime: options?.prioritizeDirectRuntime ?? true,
35
154
  scope: options?.scope === "required" ? "required" : undefined,
@@ -452,6 +571,22 @@ export function extractPurlTargetsFromBom(bomJson, sourceName, options) {
452
571
  });
453
572
  continue;
454
573
  }
574
+ const matchedAllowlistPrefix = findMatchingAllowlistPrefix(
575
+ componentPurl,
576
+ selectorOptions.allowlistPrefixes,
577
+ );
578
+ if (matchedAllowlistPrefix) {
579
+ skipped.push({
580
+ reason: "allowlisted-purl-prefix",
581
+ matchedPrefix: matchedAllowlistPrefix,
582
+ source: sourceName,
583
+ purl: componentPurl,
584
+ bomRef: component?.["bom-ref"],
585
+ name: component?.name,
586
+ type: purlObj.type,
587
+ });
588
+ continue;
589
+ }
455
590
  targets.push({
456
591
  bomRef: component?.["bom-ref"],
457
592
  buildOnlyWorkspace:
@@ -511,6 +646,7 @@ export function extractPurlTargetsFromBom(bomJson, sourceName, options) {
511
646
  */
512
647
  export function collectAuditTargets(inputBoms, options) {
513
648
  const selectorOptions = normalizeTargetSelectionOptions(options);
649
+ const allowlistedTargetPurls = new Set();
514
650
  const skipped = [];
515
651
  const targetMap = new Map();
516
652
  for (const inputBom of inputBoms) {
@@ -519,7 +655,15 @@ export function collectAuditTargets(inputBoms, options) {
519
655
  inputBom.source,
520
656
  selectorOptions,
521
657
  );
522
- skipped.push(...extracted.skipped);
658
+ for (const skippedEntry of extracted.skipped) {
659
+ skipped.push(skippedEntry);
660
+ if (
661
+ skippedEntry?.reason === "allowlisted-purl-prefix" &&
662
+ skippedEntry?.purl
663
+ ) {
664
+ allowlistedTargetPurls.add(skippedEntry.purl);
665
+ }
666
+ }
523
667
  for (const target of extracted.targets) {
524
668
  const existing = targetMap.get(target.purl);
525
669
  if (existing) {
@@ -630,6 +774,7 @@ export function collectAuditTargets(inputBoms, options) {
630
774
  skipped,
631
775
  stats: {
632
776
  availableTargets,
777
+ allowlistedTargetsExcluded: allowlistedTargetPurls.size,
633
778
  directRuntimeTargets: directRuntimeTargets.length,
634
779
  buildOnlyWorkspaceTargets: buildOnlyWorkspaceTargets.length,
635
780
  cargoRuntimeFacingTargets: cargoRuntimeFacingTargets.length,
@@ -1,3 +1,7 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
1
5
  import esmock from "esmock";
2
6
  import { assert, describe, it } from "poku";
3
7
 
@@ -17,6 +21,42 @@ function makeBom(components, extra = {}) {
17
21
  };
18
22
  }
19
23
 
24
+ function makeAllowlistInputBom(source) {
25
+ return {
26
+ bomJson: makeBom([
27
+ {
28
+ "bom-ref": "pkg:npm/%40acme/core@1.0.0",
29
+ name: "core",
30
+ purl: "pkg:npm/%40acme/core@1.0.0",
31
+ },
32
+ {
33
+ "bom-ref": "pkg:pypi/internal-tool@1.0.0",
34
+ name: "internal-tool",
35
+ purl: "pkg:pypi/internal-tool@1.0.0",
36
+ },
37
+ {
38
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
39
+ name: "left-pad",
40
+ purl: "pkg:npm/left-pad@1.3.0",
41
+ },
42
+ ]),
43
+ source,
44
+ };
45
+ }
46
+
47
+ function withTemporaryAllowlistFile(fileName, content, callback) {
48
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdx-audit-allowlist-"));
49
+ const allowlistFile = path.join(tmpDir, fileName);
50
+ writeFileSync(allowlistFile, content);
51
+ try {
52
+ // Let test failures propagate after cleanup so the original assertion error
53
+ // is preserved for the runner.
54
+ callback(allowlistFile);
55
+ } finally {
56
+ rmSync(tmpDir, { force: true, recursive: true });
57
+ }
58
+ }
59
+
20
60
  describe("normalizePackageName()", () => {
21
61
  it("normalizes Python-style package separators", () => {
22
62
  assert.strictEqual(
@@ -312,6 +352,152 @@ describe("collectAuditTargets()", () => {
312
352
  assert.strictEqual(collected.stats.nonRequiredTargets, 0);
313
353
  });
314
354
 
355
+ it("skips built-in well-known npm allowlist prefixes by default", () => {
356
+ const inputBoms = [
357
+ {
358
+ bomJson: makeBom([
359
+ {
360
+ "bom-ref": "pkg:npm/%40babel/parser@7.29.3",
361
+ name: "parser",
362
+ purl: "pkg:npm/%40babel/parser@7.29.3",
363
+ },
364
+ {
365
+ "bom-ref": "pkg:npm/npm@10.9.0",
366
+ name: "npm",
367
+ purl: "pkg:npm/npm@10.9.0",
368
+ },
369
+ {
370
+ "bom-ref": "pkg:npm/%40types/node@24.0.0",
371
+ name: "node",
372
+ purl: "pkg:npm/%40types/node@24.0.0",
373
+ },
374
+ {
375
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
376
+ name: "left-pad",
377
+ purl: "pkg:npm/left-pad@1.3.0",
378
+ },
379
+ ]),
380
+ source: "allowlist-default.json",
381
+ },
382
+ ];
383
+
384
+ const collected = collectAuditTargets(inputBoms, { trusted: "include" });
385
+
386
+ assert.deepStrictEqual(
387
+ collected.targets.map((target) => target.purl),
388
+ ["pkg:npm/left-pad@1.3.0"],
389
+ );
390
+ assert.strictEqual(collected.stats.allowlistedTargetsExcluded, 3);
391
+ assert.strictEqual(
392
+ collected.skipped.filter(
393
+ (entry) => entry.reason === "allowlisted-purl-prefix",
394
+ ).length,
395
+ 3,
396
+ );
397
+ });
398
+
399
+ it("supports additive custom predictive audit allowlists", () => {
400
+ withTemporaryAllowlistFile(
401
+ "custom-allowlist.json",
402
+ `${JSON.stringify(["pkg:npm/%40acme", "pkg:pypi/internal-tool"])}\n`,
403
+ (allowlistFile) => {
404
+ const inputBoms = [makeAllowlistInputBom("allowlist-custom.json")];
405
+
406
+ const collected = collectAuditTargets(inputBoms, {
407
+ allowlistFile,
408
+ trusted: "include",
409
+ });
410
+
411
+ assert.deepStrictEqual(
412
+ collected.targets.map((target) => target.purl),
413
+ ["pkg:npm/left-pad@1.3.0"],
414
+ );
415
+ assert.strictEqual(collected.stats.allowlistedTargetsExcluded, 2);
416
+ },
417
+ );
418
+ });
419
+
420
+ it("supports newline-delimited custom allowlists with comments", () => {
421
+ withTemporaryAllowlistFile(
422
+ "custom-allowlist.txt",
423
+ [
424
+ "pkg:npm/%40acme # internal namespace",
425
+ "",
426
+ "pkg:pypi/internal-tool",
427
+ ].join("\n"),
428
+ (allowlistFile) => {
429
+ const inputBoms = [makeAllowlistInputBom("allowlist-custom-text.json")];
430
+
431
+ const collected = collectAuditTargets(inputBoms, {
432
+ allowlistFile,
433
+ trusted: "include",
434
+ });
435
+
436
+ assert.deepStrictEqual(
437
+ collected.targets.map((target) => target.purl),
438
+ ["pkg:npm/left-pad@1.3.0"],
439
+ );
440
+ assert.strictEqual(collected.stats.allowlistedTargetsExcluded, 2);
441
+ },
442
+ );
443
+ });
444
+
445
+ it("supports custom allowlists provided as a prefixes object", () => {
446
+ withTemporaryAllowlistFile(
447
+ "custom-allowlist.json",
448
+ `${JSON.stringify({ prefixes: ["pkg:npm/%40acme", "pkg:pypi/internal-tool"] })}\n`,
449
+ (allowlistFile) => {
450
+ const inputBoms = [
451
+ makeAllowlistInputBom("allowlist-custom-prefixes.json"),
452
+ ];
453
+
454
+ const collected = collectAuditTargets(inputBoms, {
455
+ allowlistFile,
456
+ trusted: "include",
457
+ });
458
+
459
+ assert.deepStrictEqual(
460
+ collected.targets.map((target) => target.purl),
461
+ ["pkg:npm/left-pad@1.3.0"],
462
+ );
463
+ assert.strictEqual(collected.stats.allowlistedTargetsExcluded, 2);
464
+ },
465
+ );
466
+ });
467
+
468
+ it("requires a purl boundary after an allowlisted prefix", () => {
469
+ const inputBoms = [
470
+ {
471
+ bomJson: makeBom([
472
+ {
473
+ "bom-ref": "pkg:npm/npm@10.9.0",
474
+ name: "npm",
475
+ purl: "pkg:npm/npm@10.9.0",
476
+ },
477
+ {
478
+ "bom-ref": "pkg:npm/npm-run-all@4.1.5",
479
+ name: "npm-run-all",
480
+ purl: "pkg:npm/npm-run-all@4.1.5",
481
+ },
482
+ {
483
+ "bom-ref": "pkg:npm/npm-check-updates@17.1.0",
484
+ name: "npm-check-updates",
485
+ purl: "pkg:npm/npm-check-updates@17.1.0",
486
+ },
487
+ ]),
488
+ source: "allowlist-boundary.json",
489
+ },
490
+ ];
491
+
492
+ const collected = collectAuditTargets(inputBoms, { trusted: "include" });
493
+
494
+ assert.deepStrictEqual(
495
+ collected.targets.map((target) => target.purl),
496
+ ["pkg:npm/npm-check-updates@17.1.0", "pkg:npm/npm-run-all@4.1.5"],
497
+ );
498
+ assert.strictEqual(collected.stats.allowlistedTargetsExcluded, 1);
499
+ });
500
+
315
501
  it("prioritizes required targets before optional ones when maxTargets is set", () => {
316
502
  const inputBoms = [
317
503
  {