@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
@@ -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 { createStream, table } from "table";
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
- highlightStr(comp.name, highlight),
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 (!filterTypes) {
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 depMap = {};
328
- const shownList = [];
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 levelPrefix = (level, isLast) => {
363
- if (level === 0) {
364
- return SYMBOLS_ANSI.EMPTY;
410
+ const dependencyTreePrefix = (ancestorContinuations, isLast) => {
411
+ let prefix = "";
412
+ for (const hasNextSibling of ancestorContinuations) {
413
+ prefix = `${prefix}${hasNextSibling ? "│ " : " "}`;
365
414
  }
366
- let prefix = `${isLast ? SYMBOLS_ANSI.LAST_BRANCH : SYMBOLS_ANSI.BRANCH}`;
367
- for (let i = 0; i < level - 1; i++) {
368
- prefix = `${
369
- isLast
370
- ? SYMBOLS_ANSI.LAST_BRANCH.replace(" ", "─")
371
- : SYMBOLS_ANSI.VERTICAL
372
- }${isLast ? "" : SYMBOLS_ANSI.INDENT}${prefix}`;
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 prefix;
424
+ return a.ref.localeCompare(b.ref);
375
425
  };
376
426
 
377
- const isReallyRoot = (depMap, refStr) => {
378
- for (const k of Object.keys(depMap)) {
379
- const dependsOn = depMap[k] || [];
380
- if (
381
- dependsOn.includes(refStr) ||
382
- dependsOn.includes(refStr.toLowerCase())
383
- ) {
384
- return false;
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
- return true;
479
+
480
+ return nodes;
388
481
  };
389
482
 
390
- const recursePrint = (depMap, subtree, level, shownList, treeGraphics) => {
391
- const listToUse = Array.isArray(subtree) ? subtree : [subtree];
392
- for (let i = 0; i < listToUse.length; i++) {
393
- const l = listToUse[i];
394
- const refStr = l.ref || l;
395
- if (
396
- (level === 0 &&
397
- isReallyRoot(depMap, refStr) &&
398
- !shownList.includes(refStr.toLowerCase())) ||
399
- level > 0
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
- `${levelPrefix(level, i === listToUse.length - 1)}${refStr}`,
531
+ `${dependencyTreePrefix(nextAncestorContinuations, childIsLast)}${CYCLE_NODE_ICON} ${childNode.ref}`,
403
532
  );
404
- shownList.push(refStr.toLowerCase());
405
- if (l && depMap[refStr]) {
406
- if (level < MAX_TREE_DEPTH) {
407
- recursePrint(
408
- depMap,
409
- depMap[refStr],
410
- level + 1,
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 { it } from "poku";
3
+ import esmock from "esmock";
4
+ import { assert, it } from "poku";
5
+ import sinon from "sinon";
4
6
 
5
- import { printDependencyTree } from "./display.js";
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
+ }