@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.
- package/README.md +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- 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 +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- 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/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- 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/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -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/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- 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 +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- 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 +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- 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/depsUtils.d.ts.map +1 -1
- 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/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.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/table.d.ts +6 -0
- package/types/lib/helpers/table.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 +30 -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 -35
- 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
package/lib/helpers/display.js
CHANGED
|
@@ -2,8 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
hasComponentRegistryProvenance,
|
|
7
|
+
REGISTRY_PROVENANCE_ICON,
|
|
8
|
+
} from "./provenanceUtils.js";
|
|
9
|
+
import { createStream, table } from "./table.js";
|
|
7
10
|
import { isSecureMode, safeExistsSync, toCamel } from "./utils.js";
|
|
8
11
|
|
|
9
12
|
// https://github.com/yangshun/tree-node-cli/blob/master/src/index.js
|
|
@@ -16,12 +19,48 @@ const SYMBOLS_ANSI = {
|
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
const MAX_TREE_DEPTH = 6;
|
|
22
|
+
const CYCLE_NODE_ICON = "↺";
|
|
23
|
+
const REPEATED_NODE_ICON = "⤴";
|
|
19
24
|
const highlightStr = (s, highlight) => {
|
|
20
25
|
if (highlight && s?.includes(highlight)) {
|
|
21
26
|
s = s.replaceAll(highlight, `\x1b[1;33m${highlight}\x1b[0m`);
|
|
22
27
|
}
|
|
23
28
|
return s;
|
|
24
29
|
};
|
|
30
|
+
|
|
31
|
+
const formatComponentName = (component, highlight) => {
|
|
32
|
+
const displayName = highlightStr(component?.name || "", highlight);
|
|
33
|
+
if (hasComponentRegistryProvenance(component)) {
|
|
34
|
+
return `${REGISTRY_PROVENANCE_ICON} ${displayName}`;
|
|
35
|
+
}
|
|
36
|
+
return displayName;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const printProvenanceLegend = () => {
|
|
40
|
+
console.log(
|
|
41
|
+
`Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds legend lines for dependency tree marker icons.
|
|
47
|
+
*
|
|
48
|
+
* @param {string[]} treeGraphics Dependency tree lines
|
|
49
|
+
* @returns {string[]} Legend lines to print after the tree output
|
|
50
|
+
*/
|
|
51
|
+
export const buildDependencyTreeLegendLines = (treeGraphics) => {
|
|
52
|
+
const legendLines = [];
|
|
53
|
+
if (treeGraphics.some((line) => line.includes(`${REPEATED_NODE_ICON} `))) {
|
|
54
|
+
legendLines.push(`${REPEATED_NODE_ICON} = already shown`);
|
|
55
|
+
}
|
|
56
|
+
if (treeGraphics.some((line) => line.includes(`${CYCLE_NODE_ICON} `))) {
|
|
57
|
+
legendLines.push(`${CYCLE_NODE_ICON} = cycle`);
|
|
58
|
+
}
|
|
59
|
+
if (!legendLines.length) {
|
|
60
|
+
return legendLines;
|
|
61
|
+
}
|
|
62
|
+
return [`Legend: ${legendLines.join("; ")}`];
|
|
63
|
+
};
|
|
25
64
|
/**
|
|
26
65
|
* Prints the BOM components as a streaming table to the console.
|
|
27
66
|
* Delegates to {@link printOSTable} automatically when the BOM metadata indicates
|
|
@@ -30,12 +69,14 @@ const highlightStr = (s, highlight) => {
|
|
|
30
69
|
* @param {Object} bomJson CycloneDX BOM JSON object
|
|
31
70
|
* @param {string[]} [filterTypes] Optional list of component types to include; all types shown when omitted
|
|
32
71
|
* @param {string} [highlight] Optional string to highlight in the output
|
|
72
|
+
* @param {string} [summaryText] Optional summary message to print after the table
|
|
33
73
|
* @returns {void}
|
|
34
74
|
*/
|
|
35
75
|
export function printTable(
|
|
36
76
|
bomJson,
|
|
37
77
|
filterTypes = undefined,
|
|
38
78
|
highlight = undefined,
|
|
79
|
+
summaryText = undefined,
|
|
39
80
|
) {
|
|
40
81
|
if (!bomJson?.components) {
|
|
41
82
|
return;
|
|
@@ -60,6 +101,7 @@ export function printTable(
|
|
|
60
101
|
],
|
|
61
102
|
};
|
|
62
103
|
const stream = createStream(config);
|
|
104
|
+
let displayedProvenanceCount = 0;
|
|
63
105
|
stream.write([
|
|
64
106
|
filterTypes?.includes("cryptographic-asset")
|
|
65
107
|
? "Asset Type / Group"
|
|
@@ -82,17 +124,23 @@ export function printTable(
|
|
|
82
124
|
(comp.tags || []).join(", "),
|
|
83
125
|
]);
|
|
84
126
|
} else {
|
|
127
|
+
if (hasComponentRegistryProvenance(comp)) {
|
|
128
|
+
displayedProvenanceCount += 1;
|
|
129
|
+
}
|
|
85
130
|
stream.write([
|
|
86
131
|
highlightStr(comp.group || "", highlight),
|
|
87
|
-
|
|
132
|
+
formatComponentName(comp, highlight),
|
|
88
133
|
`\x1b[1;35m${comp.version || ""}\x1b[0m`,
|
|
89
134
|
comp.scope || "",
|
|
90
135
|
(comp.tags || []).join(", "),
|
|
91
136
|
]);
|
|
92
137
|
}
|
|
93
138
|
}
|
|
139
|
+
stream.end();
|
|
94
140
|
console.log();
|
|
95
|
-
if (
|
|
141
|
+
if (summaryText) {
|
|
142
|
+
console.log(summaryText);
|
|
143
|
+
} else if (!filterTypes) {
|
|
96
144
|
console.log(
|
|
97
145
|
"BOM includes",
|
|
98
146
|
bomJson?.components?.length || 0,
|
|
@@ -103,6 +151,12 @@ export function printTable(
|
|
|
103
151
|
} else {
|
|
104
152
|
console.log(`Components filtered based on type: ${filterTypes.join(", ")}`);
|
|
105
153
|
}
|
|
154
|
+
if (displayedProvenanceCount > 0) {
|
|
155
|
+
printProvenanceLegend();
|
|
156
|
+
console.log(
|
|
157
|
+
`${REGISTRY_PROVENANCE_ICON} ${displayedProvenanceCount} component(s) include registry provenance or trusted publishing metadata.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
106
160
|
}
|
|
107
161
|
const formatProps = (props) => {
|
|
108
162
|
const retList = [];
|
|
@@ -135,6 +189,7 @@ export function printOSTable(bomJson) {
|
|
|
135
189
|
(comp.tags || []).join(", "),
|
|
136
190
|
]);
|
|
137
191
|
}
|
|
192
|
+
stream.end();
|
|
138
193
|
console.log();
|
|
139
194
|
}
|
|
140
195
|
/**
|
|
@@ -253,6 +308,7 @@ export function printOccurrences(bomJson) {
|
|
|
253
308
|
stream.write(row);
|
|
254
309
|
}
|
|
255
310
|
}
|
|
311
|
+
stream.end();
|
|
256
312
|
console.log();
|
|
257
313
|
}
|
|
258
314
|
|
|
@@ -324,19 +380,8 @@ export function printDependencyTree(
|
|
|
324
380
|
if (!dependencies.length) {
|
|
325
381
|
return;
|
|
326
382
|
}
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
for (const d of dependencies) {
|
|
330
|
-
if (d[mode]?.length) {
|
|
331
|
-
depMap[d.ref] = d[mode].sort();
|
|
332
|
-
} else {
|
|
333
|
-
if (mode === "provides") {
|
|
334
|
-
shownList.push(d.ref);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const treeGraphics = [];
|
|
339
|
-
recursePrint(depMap, dependencies, 0, shownList, treeGraphics);
|
|
383
|
+
const treeGraphics = buildDependencyTreeLines(dependencies, mode);
|
|
384
|
+
const legendLines = buildDependencyTreeLegendLines(treeGraphics);
|
|
340
385
|
// table library is too slow for display large lists.
|
|
341
386
|
// Fixes #491
|
|
342
387
|
if (treeGraphics.length && treeGraphics.length < 100) {
|
|
@@ -357,64 +402,203 @@ export function printDependencyTree(
|
|
|
357
402
|
} else {
|
|
358
403
|
console.log(highlightStr(treeGraphics.slice(0, 500).join("\n"), highlight));
|
|
359
404
|
}
|
|
405
|
+
if (legendLines.length) {
|
|
406
|
+
console.log(legendLines.join("\n"));
|
|
407
|
+
}
|
|
360
408
|
}
|
|
361
409
|
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
410
|
+
const dependencyTreePrefix = (ancestorContinuations, isLast) => {
|
|
411
|
+
let prefix = "";
|
|
412
|
+
for (const hasNextSibling of ancestorContinuations) {
|
|
413
|
+
prefix = `${prefix}${hasNextSibling ? "│ " : " "}`;
|
|
365
414
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
415
|
+
return `${prefix}${isLast ? SYMBOLS_ANSI.LAST_BRANCH : SYMBOLS_ANSI.BRANCH}`;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const dependencyTreeRefKey = (ref) => ref.toLowerCase();
|
|
419
|
+
|
|
420
|
+
const compareDependencyTreeNodes = (a, b) => {
|
|
421
|
+
if (a.order !== b.order) {
|
|
422
|
+
return a.order - b.order;
|
|
373
423
|
}
|
|
374
|
-
return
|
|
424
|
+
return a.ref.localeCompare(b.ref);
|
|
375
425
|
};
|
|
376
426
|
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
427
|
+
const createDependencyTreeGraph = (dependencies, mode) => {
|
|
428
|
+
const nodes = new Map();
|
|
429
|
+
let nextOrder = 0;
|
|
430
|
+
|
|
431
|
+
const ensureNode = (ref) => {
|
|
432
|
+
if (!ref) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
const refKey = dependencyTreeRefKey(ref);
|
|
436
|
+
if (!nodes.has(refKey)) {
|
|
437
|
+
nodes.set(refKey, {
|
|
438
|
+
childKeys: new Set(),
|
|
439
|
+
children: [],
|
|
440
|
+
order: nextOrder,
|
|
441
|
+
parents: new Set(),
|
|
442
|
+
ref,
|
|
443
|
+
});
|
|
444
|
+
nextOrder += 1;
|
|
445
|
+
}
|
|
446
|
+
return nodes.get(refKey);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
for (const dependency of dependencies) {
|
|
450
|
+
const rawChildren = Array.isArray(dependency?.[mode])
|
|
451
|
+
? dependency[mode].filter(Boolean)
|
|
452
|
+
: [];
|
|
453
|
+
const childRefs = Array.from(new Set(rawChildren)).sort((a, b) =>
|
|
454
|
+
a.localeCompare(b),
|
|
455
|
+
);
|
|
456
|
+
let parentNode;
|
|
457
|
+
if (mode !== "provides" || childRefs.length) {
|
|
458
|
+
parentNode = ensureNode(dependency.ref);
|
|
459
|
+
}
|
|
460
|
+
if (!childRefs.length) {
|
|
461
|
+
continue;
|
|
385
462
|
}
|
|
463
|
+
parentNode = parentNode || ensureNode(dependency.ref);
|
|
464
|
+
for (const childRef of childRefs) {
|
|
465
|
+
const childNode = ensureNode(childRef);
|
|
466
|
+
if (!parentNode || !childNode) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
parentNode.childKeys.add(dependencyTreeRefKey(childRef));
|
|
470
|
+
childNode.parents.add(dependencyTreeRefKey(parentNode.ref));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const node of nodes.values()) {
|
|
475
|
+
node.children = Array.from(node.childKeys).sort((a, b) =>
|
|
476
|
+
compareDependencyTreeNodes(nodes.get(a), nodes.get(b)),
|
|
477
|
+
);
|
|
386
478
|
}
|
|
387
|
-
|
|
479
|
+
|
|
480
|
+
return nodes;
|
|
388
481
|
};
|
|
389
482
|
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
483
|
+
const renderDependencyTreeNode = (
|
|
484
|
+
nodes,
|
|
485
|
+
nodeKey,
|
|
486
|
+
depth,
|
|
487
|
+
ancestorContinuations,
|
|
488
|
+
isLast,
|
|
489
|
+
renderedNodes,
|
|
490
|
+
treeGraphics,
|
|
491
|
+
visitingNodes = new Set(),
|
|
492
|
+
) => {
|
|
493
|
+
const node = nodes.get(nodeKey);
|
|
494
|
+
if (!node || renderedNodes.has(nodeKey)) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const prefix =
|
|
498
|
+
depth === 0
|
|
499
|
+
? SYMBOLS_ANSI.EMPTY
|
|
500
|
+
: dependencyTreePrefix(ancestorContinuations, isLast);
|
|
501
|
+
treeGraphics.push(`${prefix}${node.ref}`);
|
|
502
|
+
renderedNodes.add(nodeKey);
|
|
503
|
+
if (depth >= MAX_TREE_DEPTH) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const nextVisitingNodes = new Set(visitingNodes);
|
|
507
|
+
nextVisitingNodes.add(nodeKey);
|
|
508
|
+
const nextAncestorContinuations =
|
|
509
|
+
depth === 0 ? ancestorContinuations : [...ancestorContinuations, !isLast];
|
|
510
|
+
const childEntries = [];
|
|
511
|
+
for (const childKey of node.children) {
|
|
512
|
+
if (nextVisitingNodes.has(childKey)) {
|
|
513
|
+
childEntries.push({ childKey, isCycle: true });
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (renderedNodes.has(childKey)) {
|
|
517
|
+
childEntries.push({ childKey, isRepeated: true });
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
childEntries.push({ childKey, isCycle: false });
|
|
521
|
+
}
|
|
522
|
+
for (let i = 0; i < childEntries.length; i++) {
|
|
523
|
+
const childEntry = childEntries[i];
|
|
524
|
+
const childNode = nodes.get(childEntry.childKey);
|
|
525
|
+
const childIsLast = i === childEntries.length - 1;
|
|
526
|
+
if (!childNode) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (childEntry.isCycle) {
|
|
401
530
|
treeGraphics.push(
|
|
402
|
-
`${
|
|
531
|
+
`${dependencyTreePrefix(nextAncestorContinuations, childIsLast)}${CYCLE_NODE_ICON} ${childNode.ref}`,
|
|
403
532
|
);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
shownList,
|
|
412
|
-
treeGraphics,
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (childEntry.isRepeated) {
|
|
536
|
+
treeGraphics.push(
|
|
537
|
+
`${dependencyTreePrefix(nextAncestorContinuations, childIsLast)}${REPEATED_NODE_ICON} ${childNode.ref}`,
|
|
538
|
+
);
|
|
539
|
+
continue;
|
|
416
540
|
}
|
|
541
|
+
renderDependencyTreeNode(
|
|
542
|
+
nodes,
|
|
543
|
+
childEntry.childKey,
|
|
544
|
+
depth + 1,
|
|
545
|
+
nextAncestorContinuations,
|
|
546
|
+
childIsLast,
|
|
547
|
+
renderedNodes,
|
|
548
|
+
treeGraphics,
|
|
549
|
+
nextVisitingNodes,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Builds printable dependency tree lines from a BOM dependency graph.
|
|
556
|
+
* Produces a spanning forest so shared children are rendered once, while
|
|
557
|
+
* disconnected or cyclic subgraphs are still emitted as dangling trees.
|
|
558
|
+
*
|
|
559
|
+
* @param {Object[]} dependencies CycloneDX dependency objects
|
|
560
|
+
* @param {string} [mode="dependsOn"] Dependency relation to traverse
|
|
561
|
+
* @returns {string[]} Dependency tree lines ready for console rendering
|
|
562
|
+
*/
|
|
563
|
+
export const buildDependencyTreeLines = (dependencies, mode = "dependsOn") => {
|
|
564
|
+
const nodes = createDependencyTreeGraph(dependencies, mode);
|
|
565
|
+
if (!nodes.size) {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
const nodeEntries = Array.from(nodes.entries()).sort(([, a], [, b]) =>
|
|
569
|
+
compareDependencyTreeNodes(a, b),
|
|
570
|
+
);
|
|
571
|
+
const rootKeys = nodeEntries
|
|
572
|
+
.filter(([, node]) => !node.parents.size)
|
|
573
|
+
.map(([nodeKey]) => nodeKey);
|
|
574
|
+
const renderedNodes = new Set();
|
|
575
|
+
const treeGraphics = [];
|
|
576
|
+
for (let i = 0; i < rootKeys.length; i++) {
|
|
577
|
+
renderDependencyTreeNode(
|
|
578
|
+
nodes,
|
|
579
|
+
rootKeys[i],
|
|
580
|
+
0,
|
|
581
|
+
[],
|
|
582
|
+
i === rootKeys.length - 1,
|
|
583
|
+
renderedNodes,
|
|
584
|
+
treeGraphics,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const danglingNodeKeys = nodeEntries
|
|
588
|
+
.map(([nodeKey]) => nodeKey)
|
|
589
|
+
.filter((nodeKey) => !renderedNodes.has(nodeKey));
|
|
590
|
+
for (let i = 0; i < danglingNodeKeys.length; i++) {
|
|
591
|
+
renderDependencyTreeNode(
|
|
592
|
+
nodes,
|
|
593
|
+
danglingNodeKeys[i],
|
|
594
|
+
0,
|
|
595
|
+
[],
|
|
596
|
+
i === danglingNodeKeys.length - 1,
|
|
597
|
+
renderedNodes,
|
|
598
|
+
treeGraphics,
|
|
599
|
+
);
|
|
417
600
|
}
|
|
601
|
+
return treeGraphics;
|
|
418
602
|
};
|
|
419
603
|
|
|
420
604
|
/**
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import esmock from "esmock";
|
|
4
|
+
import { assert, it } from "poku";
|
|
5
|
+
import sinon from "sinon";
|
|
4
6
|
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildDependencyTreeLegendLines,
|
|
9
|
+
buildDependencyTreeLines,
|
|
10
|
+
printDependencyTree,
|
|
11
|
+
} from "./display.js";
|
|
12
|
+
import { REGISTRY_PROVENANCE_ICON } from "./provenanceUtils.js";
|
|
6
13
|
|
|
7
14
|
it("print tree test", () => {
|
|
8
15
|
const bomJson = JSON.parse(
|
|
@@ -10,3 +17,156 @@ it("print tree test", () => {
|
|
|
10
17
|
);
|
|
11
18
|
printDependencyTree(bomJson);
|
|
12
19
|
});
|
|
20
|
+
|
|
21
|
+
it("prints a provenance icon for registry-backed components", async () => {
|
|
22
|
+
const rows = [];
|
|
23
|
+
const consoleLogStub = sinon.stub(console, "log");
|
|
24
|
+
try {
|
|
25
|
+
const { printTable } = await esmock("./display.js", {
|
|
26
|
+
"./table.js": {
|
|
27
|
+
createStream: () => ({
|
|
28
|
+
end() {
|
|
29
|
+
// intentional no-op for stream stub
|
|
30
|
+
},
|
|
31
|
+
write(row) {
|
|
32
|
+
rows.push(row);
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
table: sinon.stub().returns(""),
|
|
36
|
+
},
|
|
37
|
+
"./utils.js": {
|
|
38
|
+
isSecureMode: false,
|
|
39
|
+
safeExistsSync: sinon.stub(),
|
|
40
|
+
toCamel: sinon.stub(),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
printTable(
|
|
45
|
+
{
|
|
46
|
+
components: [
|
|
47
|
+
{
|
|
48
|
+
group: "",
|
|
49
|
+
name: "left-pad",
|
|
50
|
+
properties: [
|
|
51
|
+
{
|
|
52
|
+
name: "cdx:npm:provenanceUrl",
|
|
53
|
+
value:
|
|
54
|
+
"https://registry.npmjs.org/-/npm/v1/attestations/left-pad",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
type: "library",
|
|
58
|
+
version: "1.3.0",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
group: "",
|
|
62
|
+
name: "lodash",
|
|
63
|
+
properties: [],
|
|
64
|
+
type: "library",
|
|
65
|
+
version: "4.17.21",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
dependencies: [],
|
|
69
|
+
},
|
|
70
|
+
undefined,
|
|
71
|
+
undefined,
|
|
72
|
+
"Found 1 trusted component.",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
assert.strictEqual(rows[1][1], `${REGISTRY_PROVENANCE_ICON} left-pad`);
|
|
76
|
+
assert.strictEqual(rows[2][1], "lodash");
|
|
77
|
+
sinon.assert.calledWithExactly(
|
|
78
|
+
consoleLogStub,
|
|
79
|
+
"Found 1 trusted component.",
|
|
80
|
+
);
|
|
81
|
+
sinon.assert.calledWithExactly(
|
|
82
|
+
consoleLogStub,
|
|
83
|
+
`Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
|
|
84
|
+
);
|
|
85
|
+
sinon.assert.calledWithExactly(
|
|
86
|
+
consoleLogStub,
|
|
87
|
+
`${REGISTRY_PROVENANCE_ICON} 1 component(s) include registry provenance or trusted publishing metadata.`,
|
|
88
|
+
);
|
|
89
|
+
} finally {
|
|
90
|
+
consoleLogStub.restore();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("renders shared dependencies once while including dangling trees", () => {
|
|
95
|
+
const treeLines = buildDependencyTreeLines([
|
|
96
|
+
{
|
|
97
|
+
ref: "pkg:root/a@1.0.0",
|
|
98
|
+
dependsOn: ["pkg:shared/c@1.0.0"],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
ref: "pkg:root/b@1.0.0",
|
|
102
|
+
dependsOn: ["pkg:shared/c@1.0.0"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
ref: "pkg:shared/c@1.0.0",
|
|
106
|
+
dependsOn: ["pkg:leaf/d@1.0.0"],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
ref: "pkg:cycle/e@1.0.0",
|
|
110
|
+
dependsOn: ["pkg:cycle/f@1.0.0"],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
ref: "pkg:cycle/f@1.0.0",
|
|
114
|
+
dependsOn: ["pkg:cycle/e@1.0.0"],
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
assert.deepStrictEqual(treeLines, [
|
|
119
|
+
"pkg:root/a@1.0.0",
|
|
120
|
+
"└── pkg:shared/c@1.0.0",
|
|
121
|
+
" └── pkg:leaf/d@1.0.0",
|
|
122
|
+
"pkg:root/b@1.0.0",
|
|
123
|
+
"└── ⤴ pkg:shared/c@1.0.0",
|
|
124
|
+
"pkg:cycle/e@1.0.0",
|
|
125
|
+
"└── pkg:cycle/f@1.0.0",
|
|
126
|
+
" └── ↺ pkg:cycle/e@1.0.0",
|
|
127
|
+
]);
|
|
128
|
+
assert.deepStrictEqual(buildDependencyTreeLegendLines(treeLines), [
|
|
129
|
+
"Legend: ⤴ = already shown; ↺ = cycle",
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("omits empty providers while marking shared provides with an icon", () => {
|
|
134
|
+
const treeLines = buildDependencyTreeLines(
|
|
135
|
+
[
|
|
136
|
+
{
|
|
137
|
+
ref: "pkg:npm/app@1.0.0",
|
|
138
|
+
provides: ["crypto/aes", "crypto/sha256"],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
ref: "pkg:npm/helper@1.0.0",
|
|
142
|
+
provides: ["crypto/sha256"],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
ref: "pkg:npm/unused@1.0.0",
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
"provides",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
assert.deepStrictEqual(treeLines, [
|
|
152
|
+
"pkg:npm/app@1.0.0",
|
|
153
|
+
"├── crypto/aes",
|
|
154
|
+
"└── crypto/sha256",
|
|
155
|
+
"pkg:npm/helper@1.0.0",
|
|
156
|
+
"└── ⤴ crypto/sha256",
|
|
157
|
+
]);
|
|
158
|
+
assert.deepStrictEqual(buildDependencyTreeLegendLines(treeLines), [
|
|
159
|
+
"Legend: ⤴ = already shown",
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("returns no legend lines when the dependency tree has no markers", () => {
|
|
164
|
+
assert.deepStrictEqual(
|
|
165
|
+
buildDependencyTreeLegendLines([
|
|
166
|
+
"pkg:root/a@1.0.0",
|
|
167
|
+
"└── pkg:shared/c@1.0.0",
|
|
168
|
+
" └── pkg:leaf/d@1.0.0",
|
|
169
|
+
]),
|
|
170
|
+
[],
|
|
171
|
+
);
|
|
172
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_EXPORT_FORMATS = new Set(["cyclonedx", "spdx"]);
|
|
4
|
+
const EXPORT_FORMAT_ALIASES = {
|
|
5
|
+
cdx: "cyclonedx",
|
|
6
|
+
cyclonedx: "cyclonedx",
|
|
7
|
+
spdx: "spdx",
|
|
8
|
+
"spdx-json": "spdx",
|
|
9
|
+
spdx3: "spdx",
|
|
10
|
+
"spdx3-json": "spdx",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize the requested export formats.
|
|
15
|
+
*
|
|
16
|
+
* @param {string|string[]|undefined|null} format Raw format value
|
|
17
|
+
* @returns {string[]} Normalized export formats
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeOutputFormats(format) {
|
|
20
|
+
if (format === undefined || format === null || format === "") {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const values = Array.isArray(format) ? format : [format];
|
|
24
|
+
const normalized = new Set();
|
|
25
|
+
for (const value of values) {
|
|
26
|
+
if (!value) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
for (const token of `${value}`.split(",")) {
|
|
30
|
+
const normalizedToken = EXPORT_FORMAT_ALIASES[token.trim().toLowerCase()];
|
|
31
|
+
if (normalizedToken && SUPPORTED_EXPORT_FORMATS.has(normalizedToken)) {
|
|
32
|
+
normalized.add(normalizedToken);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return Array.from(normalized);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Derive the SPDX output path from a base output path.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} outputPath Output path
|
|
43
|
+
* @returns {string} SPDX output path
|
|
44
|
+
*/
|
|
45
|
+
export function deriveSpdxOutputPath(outputPath) {
|
|
46
|
+
if (!outputPath) {
|
|
47
|
+
return "bom.spdx.json";
|
|
48
|
+
}
|
|
49
|
+
if (outputPath.endsWith(".spdx.json")) {
|
|
50
|
+
return outputPath;
|
|
51
|
+
}
|
|
52
|
+
if (outputPath.endsWith(".cdx.json")) {
|
|
53
|
+
return outputPath.replace(/\.cdx\.json$/u, ".spdx.json");
|
|
54
|
+
}
|
|
55
|
+
if (outputPath.endsWith(".json")) {
|
|
56
|
+
return outputPath.replace(/\.json$/u, ".spdx.json");
|
|
57
|
+
}
|
|
58
|
+
return `${outputPath}.spdx.json`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive the CycloneDX output path from a base output path.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} outputPath Output path
|
|
65
|
+
* @returns {string} CycloneDX output path
|
|
66
|
+
*/
|
|
67
|
+
export function deriveCycloneDxOutputPath(outputPath) {
|
|
68
|
+
if (!outputPath) {
|
|
69
|
+
return "bom.json";
|
|
70
|
+
}
|
|
71
|
+
if (outputPath.endsWith(".spdx.json")) {
|
|
72
|
+
return outputPath.replace(/\.spdx\.json$/u, ".cdx.json");
|
|
73
|
+
}
|
|
74
|
+
return outputPath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Determine the final output plan for the requested export formats.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} options CLI options
|
|
81
|
+
* @returns {{ formats: Set<string>, outputs: Record<string, string>, explicitFormat: boolean }} Output plan
|
|
82
|
+
*/
|
|
83
|
+
export function createOutputPlan(options) {
|
|
84
|
+
const explicitFormat =
|
|
85
|
+
options?.format !== undefined &&
|
|
86
|
+
options?.format !== null &&
|
|
87
|
+
options?.format !== "";
|
|
88
|
+
const requestedFormats = normalizeOutputFormats(options?.format);
|
|
89
|
+
const outputPath = options?.output || "bom.json";
|
|
90
|
+
const formats = new Set(
|
|
91
|
+
requestedFormats.length
|
|
92
|
+
? requestedFormats
|
|
93
|
+
: [outputPath.endsWith(".spdx.json") ? "spdx" : "cyclonedx"],
|
|
94
|
+
);
|
|
95
|
+
const outputs = {};
|
|
96
|
+
if (formats.has("cyclonedx")) {
|
|
97
|
+
outputs.cyclonedx =
|
|
98
|
+
outputPath.endsWith(".spdx.json") && formats.size > 1
|
|
99
|
+
? deriveCycloneDxOutputPath(outputPath)
|
|
100
|
+
: outputPath;
|
|
101
|
+
}
|
|
102
|
+
if (formats.has("spdx")) {
|
|
103
|
+
if (!formats.has("cyclonedx") || outputPath.endsWith(".spdx.json")) {
|
|
104
|
+
outputs.spdx =
|
|
105
|
+
outputPath === "bom.json"
|
|
106
|
+
? deriveSpdxOutputPath(outputPath)
|
|
107
|
+
: outputPath;
|
|
108
|
+
} else {
|
|
109
|
+
outputs.spdx = deriveSpdxOutputPath(outputPath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { formats, outputs, explicitFormat };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Return the output directory for a planned export path.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} outputPath Output path
|
|
119
|
+
* @returns {string} Output directory
|
|
120
|
+
*/
|
|
121
|
+
export function getOutputDirectory(outputPath) {
|
|
122
|
+
return path.dirname(outputPath);
|
|
123
|
+
}
|