@cyclonedx/cdxgen 12.2.1 → 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.
- package/README.md +239 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +513 -167
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +154 -11
- package/lib/cli/index.poku.js +251 -0
- package/lib/helpers/analyzer.js +446 -2
- package/lib/helpers/analyzer.poku.js +72 -1
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/display.js +241 -59
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +700 -128
- package/lib/helpers/utils.poku.js +877 -80
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +17 -0
- package/lib/server/server.js +225 -336
- package/lib/server/server.poku.js +16 -10
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +19 -3
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +11 -0
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -7
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +29 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -36
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- 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
|
+
});
|