@cyclonedx/cdxgen 12.2.0 → 12.3.0

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 (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,260 @@
1
+ import { PackageURL } from "packageurl-js";
2
+
3
+ import { hasTrustedPublishingProperties } from "../helpers/provenanceUtils.js";
4
+
5
+ const SUPPORTED_PURL_TYPES = new Set(["npm", "pypi"]);
6
+ const NON_REQUIRED_SCOPES = new Set(["excluded", "optional"]);
7
+
8
+ /**
9
+ * Normalize predictive audit target selection options.
10
+ *
11
+ * @param {number | object | undefined} options selector options or legacy maxTargets value
12
+ * @returns {{
13
+ * maxTargets: number | undefined,
14
+ * scope: string | undefined,
15
+ * trusted: "exclude" | "include" | "only",
16
+ * }} normalized options
17
+ */
18
+ function normalizeTargetSelectionOptions(options) {
19
+ if (typeof options === "number") {
20
+ return {
21
+ maxTargets: options,
22
+ scope: undefined,
23
+ trusted: "exclude",
24
+ };
25
+ }
26
+ return {
27
+ maxTargets: options?.maxTargets,
28
+ scope: options?.scope === "required" ? "required" : undefined,
29
+ trusted:
30
+ options?.trusted === "only"
31
+ ? "only"
32
+ : options?.trusted === "include"
33
+ ? "include"
34
+ : "exclude",
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Determine whether a CycloneDX component scope should be treated as required.
40
+ *
41
+ * Missing scope is treated as required to match the main BOM filtering flow.
42
+ *
43
+ * @param {string | undefined} scope component scope
44
+ * @returns {boolean} true when the component is required for predictive audit selection
45
+ */
46
+ export function isRequiredComponentScope(scope) {
47
+ if (!scope || typeof scope !== "string") {
48
+ return true;
49
+ }
50
+ return !NON_REQUIRED_SCOPES.has(scope.toLowerCase());
51
+ }
52
+
53
+ function normalizeComponentScope(scope) {
54
+ if (!scope || typeof scope !== "string") {
55
+ return undefined;
56
+ }
57
+ return scope.toLowerCase();
58
+ }
59
+
60
+ function mergeTargetScope(existingTarget, nextTarget) {
61
+ const mergedRequired = Boolean(
62
+ existingTarget.required || nextTarget.required,
63
+ );
64
+ const existingScope = normalizeComponentScope(existingTarget.scope);
65
+ const nextScope = normalizeComponentScope(nextTarget.scope);
66
+ if (mergedRequired) {
67
+ return existingScope === "required" || nextScope === "required"
68
+ ? "required"
69
+ : existingScope || nextScope;
70
+ }
71
+ return existingScope === "optional" || nextScope === "optional"
72
+ ? "optional"
73
+ : existingScope || nextScope;
74
+ }
75
+
76
+ /**
77
+ * Normalize package names for safe matching and grouping.
78
+ *
79
+ * @param {string | undefined} packageName package name
80
+ * @returns {string} normalized package name
81
+ */
82
+ export function normalizePackageName(packageName) {
83
+ if (!packageName || typeof packageName !== "string") {
84
+ return "";
85
+ }
86
+ return packageName.toLowerCase().replace(/[-_.]+/g, "-");
87
+ }
88
+
89
+ /**
90
+ * Extract npm and PyPI package-url targets from a CycloneDX BOM.
91
+ *
92
+ * @param {object} bomJson CycloneDX BOM
93
+ * @param {string} sourceName source BOM path or label
94
+ * @param {number | object | undefined} [options] selector options
95
+ * @returns {{ targets: object[], skipped: object[] }} extracted targets and skipped components
96
+ */
97
+ export function extractPurlTargetsFromBom(bomJson, sourceName, options) {
98
+ const selectorOptions = normalizeTargetSelectionOptions(options);
99
+ const targets = [];
100
+ const skipped = [];
101
+ const components = Array.isArray(bomJson?.components)
102
+ ? bomJson.components
103
+ : [];
104
+ for (const component of components) {
105
+ const componentScope = normalizeComponentScope(component?.scope);
106
+ if (
107
+ selectorOptions.scope === "required" &&
108
+ !isRequiredComponentScope(componentScope)
109
+ ) {
110
+ continue;
111
+ }
112
+ const componentPurl = component?.purl;
113
+ if (!componentPurl) {
114
+ continue;
115
+ }
116
+ let purlObj;
117
+ try {
118
+ purlObj = PackageURL.fromString(componentPurl);
119
+ } catch {
120
+ skipped.push({
121
+ reason: "invalid-purl",
122
+ source: sourceName,
123
+ purl: componentPurl,
124
+ bomRef: component?.["bom-ref"],
125
+ name: component?.name,
126
+ });
127
+ continue;
128
+ }
129
+ if (!SUPPORTED_PURL_TYPES.has(purlObj.type)) {
130
+ skipped.push({
131
+ reason: "unsupported-ecosystem",
132
+ source: sourceName,
133
+ purl: componentPurl,
134
+ bomRef: component?.["bom-ref"],
135
+ name: component?.name,
136
+ type: purlObj.type,
137
+ });
138
+ continue;
139
+ }
140
+ targets.push({
141
+ bomRef: component?.["bom-ref"],
142
+ name: purlObj.name,
143
+ namespace: purlObj.namespace,
144
+ purl: componentPurl,
145
+ properties: Array.isArray(component?.properties)
146
+ ? component.properties.map((property) => ({ ...property }))
147
+ : [],
148
+ qualifiers: purlObj.qualifiers,
149
+ required: isRequiredComponentScope(componentScope),
150
+ scope: componentScope,
151
+ source: sourceName,
152
+ trustedPublishing: hasTrustedPublishingProperties(component?.properties),
153
+ type: purlObj.type,
154
+ version: purlObj.version,
155
+ });
156
+ }
157
+ return { skipped, targets };
158
+ }
159
+
160
+ /**
161
+ * Merge targets across many BOMs by purl.
162
+ *
163
+ * @param {{ source: string, bomJson: object }[]} inputBoms input BOMs
164
+ * @param {number | object | undefined} [options] selector options or a legacy maxTargets value
165
+ * @returns {{
166
+ * skipped: object[],
167
+ * stats: {
168
+ * availableTargets: number,
169
+ * nonRequiredTargets: number,
170
+ * requiredTargets: number,
171
+ * trustedTargets: number,
172
+ * trustedTargetsExcluded: number,
173
+ * truncatedTargets: number,
174
+ * },
175
+ * targets: object[],
176
+ * }} merged targets and skipped components
177
+ */
178
+ export function collectAuditTargets(inputBoms, options) {
179
+ const selectorOptions = normalizeTargetSelectionOptions(options);
180
+ const skipped = [];
181
+ const targetMap = new Map();
182
+ for (const inputBom of inputBoms) {
183
+ const extracted = extractPurlTargetsFromBom(
184
+ inputBom.bomJson,
185
+ inputBom.source,
186
+ selectorOptions,
187
+ );
188
+ skipped.push(...extracted.skipped);
189
+ for (const target of extracted.targets) {
190
+ const existing = targetMap.get(target.purl);
191
+ if (existing) {
192
+ existing.required = Boolean(existing.required || target.required);
193
+ existing.scope = mergeTargetScope(existing, target);
194
+ existing.trustedPublishing = Boolean(
195
+ existing.trustedPublishing || target.trustedPublishing,
196
+ );
197
+ existing.sources.add(target.source);
198
+ if (target.bomRef) {
199
+ existing.bomRefs.add(target.bomRef);
200
+ }
201
+ for (const property of target.properties || []) {
202
+ const alreadyPresent = existing.properties.some(
203
+ (existingProperty) =>
204
+ existingProperty.name === property.name &&
205
+ existingProperty.value === property.value,
206
+ );
207
+ if (!alreadyPresent) {
208
+ existing.properties.push(property);
209
+ }
210
+ }
211
+ continue;
212
+ }
213
+ targetMap.set(target.purl, {
214
+ ...target,
215
+ bomRefs: new Set(target.bomRef ? [target.bomRef] : []),
216
+ sources: new Set([target.source]),
217
+ });
218
+ }
219
+ }
220
+ let targets = [...targetMap.values()].map((target) => ({
221
+ ...target,
222
+ bomRefs: [...target.bomRefs].sort(),
223
+ normalizedName: normalizePackageName(target.name),
224
+ sources: [...target.sources].sort(),
225
+ }));
226
+ targets.sort((left, right) => left.purl.localeCompare(right.purl));
227
+ const trustedTargets = targets.filter((target) => target.trustedPublishing);
228
+ if (selectorOptions.trusted === "only") {
229
+ targets = trustedTargets;
230
+ } else if (selectorOptions.trusted === "exclude") {
231
+ targets = targets.filter((target) => !target.trustedPublishing);
232
+ }
233
+ const requiredTargets = targets.filter((target) => target.required);
234
+ const nonRequiredTargets = targets.filter((target) => !target.required);
235
+ const availableTargets = targets.length;
236
+ if (
237
+ typeof selectorOptions.maxTargets === "number" &&
238
+ selectorOptions.maxTargets > 0
239
+ ) {
240
+ targets = [...requiredTargets, ...nonRequiredTargets].slice(
241
+ 0,
242
+ selectorOptions.maxTargets,
243
+ );
244
+ }
245
+ return {
246
+ skipped,
247
+ stats: {
248
+ availableTargets,
249
+ nonRequiredTargets: nonRequiredTargets.length,
250
+ requiredTargets: requiredTargets.length,
251
+ trustedTargets: trustedTargets.length,
252
+ trustedTargetsExcluded:
253
+ selectorOptions.trusted === "exclude" ? trustedTargets.length : 0,
254
+ truncatedTargets: Math.max(0, availableTargets - targets.length),
255
+ },
256
+ targets,
257
+ };
258
+ }
259
+
260
+ export { SUPPORTED_PURL_TYPES };
@@ -0,0 +1,331 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ collectAuditTargets,
5
+ extractPurlTargetsFromBom,
6
+ isRequiredComponentScope,
7
+ normalizePackageName,
8
+ } from "./targets.js";
9
+
10
+ function makeBom(components) {
11
+ return {
12
+ bomFormat: "CycloneDX",
13
+ components,
14
+ specVersion: "1.6",
15
+ };
16
+ }
17
+
18
+ describe("normalizePackageName()", () => {
19
+ it("normalizes Python-style package separators", () => {
20
+ assert.strictEqual(
21
+ normalizePackageName("My_Package.Name"),
22
+ "my-package-name",
23
+ );
24
+ });
25
+ });
26
+
27
+ describe("extractPurlTargetsFromBom()", () => {
28
+ it("extracts only npm and pypi purls", () => {
29
+ const bom = makeBom([
30
+ {
31
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
32
+ name: "left-pad",
33
+ properties: [
34
+ { name: "cdx:npm:trustedPublishing", value: "true" },
35
+ { name: "cdx:npm:provenanceKeyId", value: "sigstore-key" },
36
+ ],
37
+ purl: "pkg:npm/left-pad@1.3.0",
38
+ },
39
+ {
40
+ "bom-ref": "pkg:pypi/requests@2.32.3",
41
+ name: "requests",
42
+ purl: "pkg:pypi/requests@2.32.3",
43
+ },
44
+ {
45
+ "bom-ref": "pkg:gem/rails@8.0.0",
46
+ name: "rails",
47
+ purl: "pkg:gem/rails@8.0.0",
48
+ },
49
+ ]);
50
+
51
+ const extracted = extractPurlTargetsFromBom(bom, "bom.json");
52
+
53
+ assert.strictEqual(extracted.targets.length, 2);
54
+ assert.strictEqual(extracted.skipped.length, 1);
55
+ assert.strictEqual(extracted.targets[0].type, "npm");
56
+ assert.strictEqual(
57
+ extracted.targets[0].properties[0].name,
58
+ "cdx:npm:trustedPublishing",
59
+ );
60
+ assert.strictEqual(
61
+ extracted.targets[0].properties[1].name,
62
+ "cdx:npm:provenanceKeyId",
63
+ );
64
+ assert.strictEqual(extracted.targets[1].type, "pypi");
65
+ assert.strictEqual(extracted.skipped[0].reason, "unsupported-ecosystem");
66
+ });
67
+
68
+ it("records invalid purls as skipped entries", () => {
69
+ const bom = makeBom([
70
+ {
71
+ "bom-ref": "bad-ref",
72
+ name: "broken",
73
+ purl: "not-a-purl",
74
+ },
75
+ ]);
76
+
77
+ const extracted = extractPurlTargetsFromBom(bom, "broken.json");
78
+
79
+ assert.strictEqual(extracted.targets.length, 0);
80
+ assert.strictEqual(extracted.skipped.length, 1);
81
+ assert.strictEqual(extracted.skipped[0].reason, "invalid-purl");
82
+ });
83
+ });
84
+
85
+ describe("isRequiredComponentScope()", () => {
86
+ it("treats missing scope as required and excludes optional/excluded scopes", () => {
87
+ assert.strictEqual(isRequiredComponentScope(undefined), true);
88
+ assert.strictEqual(isRequiredComponentScope("required"), true);
89
+ assert.strictEqual(isRequiredComponentScope("optional"), false);
90
+ assert.strictEqual(isRequiredComponentScope("excluded"), false);
91
+ });
92
+ });
93
+
94
+ describe("collectAuditTargets()", () => {
95
+ it("deduplicates targets across multiple BOMs while preserving sources", () => {
96
+ const inputBoms = [
97
+ {
98
+ bomJson: makeBom([
99
+ {
100
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
101
+ name: "left-pad",
102
+ properties: [{ name: "cdx:npm:trustedPublishing", value: "true" }],
103
+ purl: "pkg:npm/left-pad@1.3.0",
104
+ },
105
+ ]),
106
+ source: "one.json",
107
+ },
108
+ {
109
+ bomJson: makeBom([
110
+ {
111
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
112
+ name: "left-pad",
113
+ properties: [{ name: "cdx:npm:publisher", value: "octo" }],
114
+ purl: "pkg:npm/left-pad@1.3.0",
115
+ },
116
+ {
117
+ "bom-ref": "pkg:pypi/requests@2.32.3",
118
+ name: "requests",
119
+ purl: "pkg:pypi/requests@2.32.3",
120
+ },
121
+ ]),
122
+ source: "two.json",
123
+ },
124
+ ];
125
+
126
+ const collected = collectAuditTargets(inputBoms, { trusted: "include" });
127
+
128
+ assert.strictEqual(collected.targets.length, 2);
129
+ const npmTarget = collected.targets.find((target) => target.type === "npm");
130
+ assert.deepStrictEqual(npmTarget.sources, ["one.json", "two.json"]);
131
+ assert.strictEqual(npmTarget.bomRefs.length, 1);
132
+ assert.strictEqual(npmTarget.properties.length, 2);
133
+ });
134
+
135
+ it("respects maxTargets when supplied", () => {
136
+ const inputBoms = [
137
+ {
138
+ bomJson: makeBom([
139
+ {
140
+ "bom-ref": "pkg:npm/a@1.0.0",
141
+ name: "a",
142
+ purl: "pkg:npm/a@1.0.0",
143
+ },
144
+ {
145
+ "bom-ref": "pkg:npm/b@1.0.0",
146
+ name: "b",
147
+ purl: "pkg:npm/b@1.0.0",
148
+ },
149
+ ]),
150
+ source: "limit.json",
151
+ },
152
+ ];
153
+
154
+ const collected = collectAuditTargets(inputBoms, 1);
155
+
156
+ assert.strictEqual(collected.targets.length, 1);
157
+ });
158
+
159
+ it("filters predictive audit targets to required scope when requested", () => {
160
+ const inputBoms = [
161
+ {
162
+ bomJson: makeBom([
163
+ {
164
+ "bom-ref": "pkg:npm/core@1.0.0",
165
+ name: "core",
166
+ purl: "pkg:npm/core@1.0.0",
167
+ scope: "required",
168
+ },
169
+ {
170
+ "bom-ref": "pkg:npm/transitive@1.0.0",
171
+ name: "transitive",
172
+ purl: "pkg:npm/transitive@1.0.0",
173
+ },
174
+ {
175
+ "bom-ref": "pkg:npm/optional-addon@1.0.0",
176
+ name: "optional-addon",
177
+ purl: "pkg:npm/optional-addon@1.0.0",
178
+ scope: "optional",
179
+ },
180
+ {
181
+ "bom-ref": "pkg:pypi/unused@1.0.0",
182
+ name: "unused",
183
+ purl: "pkg:pypi/unused@1.0.0",
184
+ scope: "excluded",
185
+ },
186
+ ]),
187
+ source: "required.json",
188
+ },
189
+ ];
190
+
191
+ const collected = collectAuditTargets(inputBoms, { scope: "required" });
192
+
193
+ assert.deepStrictEqual(
194
+ collected.targets.map((target) => target.purl),
195
+ ["pkg:npm/core@1.0.0", "pkg:npm/transitive@1.0.0"],
196
+ );
197
+ assert.strictEqual(collected.stats.requiredTargets, 2);
198
+ assert.strictEqual(collected.stats.nonRequiredTargets, 0);
199
+ });
200
+
201
+ it("prioritizes required targets before optional ones when maxTargets is set", () => {
202
+ const inputBoms = [
203
+ {
204
+ bomJson: makeBom([
205
+ {
206
+ "bom-ref": "pkg:npm/a-optional@1.0.0",
207
+ name: "a-optional",
208
+ purl: "pkg:npm/a-optional@1.0.0",
209
+ scope: "optional",
210
+ },
211
+ {
212
+ "bom-ref": "pkg:npm/z-required@1.0.0",
213
+ name: "z-required",
214
+ purl: "pkg:npm/z-required@1.0.0",
215
+ scope: "required",
216
+ },
217
+ ]),
218
+ source: "priority.json",
219
+ },
220
+ ];
221
+
222
+ const collected = collectAuditTargets(inputBoms, { maxTargets: 1 });
223
+
224
+ assert.strictEqual(collected.targets.length, 1);
225
+ assert.strictEqual(collected.targets[0].purl, "pkg:npm/z-required@1.0.0");
226
+ assert.strictEqual(collected.stats.truncatedTargets, 1);
227
+ });
228
+
229
+ it("excludes trusted-publishing-backed targets by default", () => {
230
+ const inputBoms = [
231
+ {
232
+ bomJson: makeBom([
233
+ {
234
+ "bom-ref": "pkg:npm/trusted@1.0.0",
235
+ name: "trusted",
236
+ properties: [
237
+ {
238
+ name: "cdx:npm:trustedPublishing",
239
+ value: "true",
240
+ },
241
+ ],
242
+ purl: "pkg:npm/trusted@1.0.0",
243
+ scope: "required",
244
+ },
245
+ {
246
+ "bom-ref": "pkg:npm/plain@1.0.0",
247
+ name: "plain",
248
+ purl: "pkg:npm/plain@1.0.0",
249
+ scope: "required",
250
+ },
251
+ ]),
252
+ source: "trusted.json",
253
+ },
254
+ ];
255
+
256
+ const collected = collectAuditTargets(inputBoms);
257
+
258
+ assert.deepStrictEqual(
259
+ collected.targets.map((target) => target.purl),
260
+ ["pkg:npm/plain@1.0.0"],
261
+ );
262
+ assert.strictEqual(collected.stats.trustedTargets, 1);
263
+ assert.strictEqual(collected.stats.trustedTargetsExcluded, 1);
264
+ });
265
+
266
+ it("includes trusted-publishing-backed targets when explicitly requested", () => {
267
+ const inputBoms = [
268
+ {
269
+ bomJson: makeBom([
270
+ {
271
+ "bom-ref": "pkg:npm/trusted@1.0.0",
272
+ name: "trusted",
273
+ properties: [
274
+ {
275
+ name: "cdx:npm:trustedPublishing",
276
+ value: "true",
277
+ },
278
+ ],
279
+ purl: "pkg:npm/trusted@1.0.0",
280
+ },
281
+ {
282
+ "bom-ref": "pkg:pypi/plain@1.0.0",
283
+ name: "plain",
284
+ purl: "pkg:pypi/plain@1.0.0",
285
+ },
286
+ ]),
287
+ source: "include-trusted.json",
288
+ },
289
+ ];
290
+
291
+ const collected = collectAuditTargets(inputBoms, { trusted: "include" });
292
+
293
+ assert.strictEqual(collected.targets.length, 2);
294
+ assert.strictEqual(collected.stats.trustedTargetsExcluded, 0);
295
+ });
296
+
297
+ it("can restrict predictive audit targets to only trusted-publishing-backed packages", () => {
298
+ const inputBoms = [
299
+ {
300
+ bomJson: makeBom([
301
+ {
302
+ "bom-ref": "pkg:npm/trusted@1.0.0",
303
+ name: "trusted",
304
+ properties: [
305
+ {
306
+ name: "cdx:npm:trustedPublishing",
307
+ value: "true",
308
+ },
309
+ ],
310
+ purl: "pkg:npm/trusted@1.0.0",
311
+ },
312
+ {
313
+ "bom-ref": "pkg:npm/plain@1.0.0",
314
+ name: "plain",
315
+ purl: "pkg:npm/plain@1.0.0",
316
+ },
317
+ ]),
318
+ source: "only-trusted.json",
319
+ },
320
+ ];
321
+
322
+ const collected = collectAuditTargets(inputBoms, { trusted: "only" });
323
+
324
+ assert.deepStrictEqual(
325
+ collected.targets.map((target) => target.purl),
326
+ ["pkg:npm/trusted@1.0.0"],
327
+ );
328
+ assert.strictEqual(collected.stats.availableTargets, 1);
329
+ assert.strictEqual(collected.stats.trustedTargets, 1);
330
+ });
331
+ });