@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.
Files changed (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  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 +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  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/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,186 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+
4
+ import { getGtfoBinsMetadata } from "./gtfobins.js";
5
+ import { dirNameStr, safeExistsSync } from "./utils.js";
6
+
7
+ const CONTAINER_RISK_INDEX_FILE = join(
8
+ dirNameStr,
9
+ "data",
10
+ "container-knowledge-index.json",
11
+ );
12
+ const DEFAULT_CONTAINER_RISK_INDEX = { entries: {}, sources: {} };
13
+ const CONTAINER_RISK_INDEX = loadContainerRiskIndex();
14
+
15
+ function loadContainerRiskIndex() {
16
+ if (!safeExistsSync(CONTAINER_RISK_INDEX_FILE)) {
17
+ return DEFAULT_CONTAINER_RISK_INDEX;
18
+ }
19
+ try {
20
+ return JSON.parse(readFileSync(CONTAINER_RISK_INDEX_FILE, "utf8"));
21
+ } catch {
22
+ return DEFAULT_CONTAINER_RISK_INDEX;
23
+ }
24
+ }
25
+
26
+ function normalizeCandidate(candidate) {
27
+ if (!candidate || typeof candidate !== "string") {
28
+ return undefined;
29
+ }
30
+ const trimmed = basename(candidate.trim()).toLowerCase();
31
+ if (!trimmed) {
32
+ return undefined;
33
+ }
34
+ return trimmed;
35
+ }
36
+
37
+ function uniqueSortedStrings(values) {
38
+ return Array.from(
39
+ new Set(
40
+ values.filter(
41
+ (value) => typeof value === "string" && value.trim().length,
42
+ ),
43
+ ),
44
+ ).sort();
45
+ }
46
+
47
+ function resolveContainerEntry(name, linkedName, gtfoMetadata) {
48
+ const directCandidate = normalizeCandidate(name);
49
+ if (directCandidate && CONTAINER_RISK_INDEX.entries?.[directCandidate]) {
50
+ return {
51
+ canonicalName: directCandidate,
52
+ entry: CONTAINER_RISK_INDEX.entries[directCandidate],
53
+ matchSource: "basename",
54
+ };
55
+ }
56
+ const linkedCandidate = normalizeCandidate(linkedName);
57
+ if (linkedCandidate && CONTAINER_RISK_INDEX.entries?.[linkedCandidate]) {
58
+ return {
59
+ canonicalName: linkedCandidate,
60
+ entry: CONTAINER_RISK_INDEX.entries[linkedCandidate],
61
+ matchSource: "symlink",
62
+ };
63
+ }
64
+ const gtfoCandidate = normalizeCandidate(gtfoMetadata?.canonicalName);
65
+ if (gtfoCandidate && CONTAINER_RISK_INDEX.entries?.[gtfoCandidate]) {
66
+ return {
67
+ canonicalName: gtfoCandidate,
68
+ entry: CONTAINER_RISK_INDEX.entries[gtfoCandidate],
69
+ matchSource: "gtfobins",
70
+ };
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ function resolveKnowledgeSourceRefs(sourceKeys) {
76
+ const refs = [];
77
+ for (const sourceKey of sourceKeys || []) {
78
+ const ref = CONTAINER_RISK_INDEX.sources?.[sourceKey];
79
+ if (ref) {
80
+ refs.push(ref);
81
+ }
82
+ }
83
+ return uniqueSortedStrings(refs);
84
+ }
85
+
86
+ export function getContainerRiskMetadata(name, linkedName) {
87
+ const gtfoMetadata = getGtfoBinsMetadata(name, linkedName);
88
+ const resolvedEntry = resolveContainerEntry(name, linkedName, gtfoMetadata);
89
+ if (!resolvedEntry) {
90
+ return undefined;
91
+ }
92
+ const attackTactics = uniqueSortedStrings(
93
+ resolvedEntry.entry.attackTactics || [],
94
+ );
95
+ const attackTechniques = uniqueSortedStrings([
96
+ ...(resolvedEntry.entry.attackTechniques || []),
97
+ ...(gtfoMetadata?.mitreTechniques || []),
98
+ ]);
99
+ const knowledgeSources = uniqueSortedStrings(
100
+ resolvedEntry.entry.sourceKeys || [],
101
+ );
102
+ const knowledgeSourceRefs = resolveKnowledgeSourceRefs(knowledgeSources);
103
+ const offenseTools = uniqueSortedStrings(
104
+ resolvedEntry.entry.offenseTools || [],
105
+ );
106
+ const riskTags = uniqueSortedStrings([
107
+ ...(resolvedEntry.entry.riskTags || []),
108
+ ...(gtfoMetadata?.riskTags || []),
109
+ ]);
110
+ const seccompBlockedSyscalls = uniqueSortedStrings(
111
+ resolvedEntry.entry.seccompBlockedSyscalls || [],
112
+ );
113
+ return {
114
+ attackTactics,
115
+ attackTechniques,
116
+ canonicalName: resolvedEntry.canonicalName,
117
+ knowledgeSourceRefs,
118
+ knowledgeSources,
119
+ matchSource: resolvedEntry.matchSource,
120
+ offenseTools,
121
+ riskTags,
122
+ seccompBlockedSyscalls,
123
+ seccompProfile: resolvedEntry.entry.seccompProfile || "",
124
+ };
125
+ }
126
+
127
+ export function createContainerRiskProperties(name, linkedName) {
128
+ const metadata = getContainerRiskMetadata(name, linkedName);
129
+ if (!metadata) {
130
+ return [];
131
+ }
132
+ const properties = [
133
+ { name: "cdx:container:matched", value: "true" },
134
+ { name: "cdx:container:name", value: metadata.canonicalName },
135
+ { name: "cdx:container:matchSource", value: metadata.matchSource },
136
+ ];
137
+ if (metadata.attackTactics.length) {
138
+ properties.push({
139
+ name: "cdx:container:attackTactics",
140
+ value: metadata.attackTactics.join(","),
141
+ });
142
+ }
143
+ if (metadata.attackTechniques.length) {
144
+ properties.push({
145
+ name: "cdx:container:attackTechniques",
146
+ value: metadata.attackTechniques.join(","),
147
+ });
148
+ }
149
+ if (metadata.knowledgeSources.length) {
150
+ properties.push({
151
+ name: "cdx:container:knowledgeSources",
152
+ value: metadata.knowledgeSources.join(","),
153
+ });
154
+ }
155
+ if (metadata.knowledgeSourceRefs.length) {
156
+ properties.push({
157
+ name: "cdx:container:knowledgeSourceRefs",
158
+ value: metadata.knowledgeSourceRefs.join(","),
159
+ });
160
+ }
161
+ if (metadata.offenseTools.length) {
162
+ properties.push({
163
+ name: "cdx:container:offenseTools",
164
+ value: metadata.offenseTools.join(","),
165
+ });
166
+ }
167
+ if (metadata.riskTags.length) {
168
+ properties.push({
169
+ name: "cdx:container:riskTags",
170
+ value: metadata.riskTags.join(","),
171
+ });
172
+ }
173
+ if (metadata.seccompBlockedSyscalls.length) {
174
+ properties.push({
175
+ name: "cdx:container:seccompBlockedSyscalls",
176
+ value: metadata.seccompBlockedSyscalls.join(","),
177
+ });
178
+ }
179
+ if (metadata.seccompProfile) {
180
+ properties.push({
181
+ name: "cdx:container:seccompProfile",
182
+ value: metadata.seccompProfile,
183
+ });
184
+ }
185
+ return properties;
186
+ }
@@ -0,0 +1,52 @@
1
+ import { strict as assert } from "node:assert";
2
+
3
+ import { describe, it } from "poku";
4
+
5
+ import {
6
+ createContainerRiskProperties,
7
+ getContainerRiskMetadata,
8
+ } from "./containerRisk.js";
9
+
10
+ describe("container risk helpers", () => {
11
+ it("returns offensive toolkit metadata for direct container tool matches", () => {
12
+ const metadata = getContainerRiskMetadata("cdk");
13
+ assert.ok(metadata);
14
+ assert.strictEqual(metadata.canonicalName, "cdk");
15
+ assert.ok(metadata.offenseTools.includes("cdk"));
16
+ assert.ok(metadata.riskTags.includes("offensive-toolkit"));
17
+ assert.ok(metadata.attackTechniques.includes("T1611"));
18
+ });
19
+
20
+ it("maps kubernetes control-plane helpers to ATT&CK and offensive playbooks", () => {
21
+ const metadata = getContainerRiskMetadata("kubectl");
22
+ assert.ok(metadata);
23
+ assert.ok(metadata.offenseTools.includes("peirates"));
24
+ assert.ok(metadata.offenseTools.includes("cdk"));
25
+ assert.ok(metadata.attackTechniques.includes("T1613"));
26
+ assert.ok(metadata.riskTags.includes("k8s-cluster-pivot"));
27
+ });
28
+
29
+ it("tracks seccomp-sensitive namespace escape helpers", () => {
30
+ const metadata = getContainerRiskMetadata("nsenter");
31
+ assert.ok(metadata);
32
+ assert.strictEqual(metadata.seccompProfile, "docker-default");
33
+ assert.ok(metadata.seccompBlockedSyscalls.includes("setns"));
34
+ assert.ok(metadata.seccompBlockedSyscalls.includes("unshare"));
35
+ });
36
+
37
+ it("emits stable CycloneDX properties for enriched container binaries", () => {
38
+ const properties = createContainerRiskProperties("docker");
39
+ const propertyMap = Object.fromEntries(
40
+ properties.map((property) => [property.name, property.value]),
41
+ );
42
+ assert.strictEqual(propertyMap["cdx:container:matched"], "true");
43
+ assert.strictEqual(propertyMap["cdx:container:name"], "docker");
44
+ assert.ok(propertyMap["cdx:container:attackTechniques"].includes("T1611"));
45
+ assert.ok(propertyMap["cdx:container:offenseTools"].includes("deepce"));
46
+ assert.ok(
47
+ propertyMap["cdx:container:knowledgeSources"].includes(
48
+ "attack-containers",
49
+ ),
50
+ );
51
+ });
52
+ });
@@ -2,6 +2,10 @@ import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
 
5
+ import {
6
+ hasComponentRegistryProvenance,
7
+ REGISTRY_PROVENANCE_ICON,
8
+ } from "./provenanceUtils.js";
5
9
  import { createStream, table } from "./table.js";
6
10
  import { isSecureMode, safeExistsSync, toCamel } from "./utils.js";
7
11
 
@@ -15,12 +19,48 @@ const SYMBOLS_ANSI = {
15
19
  };
16
20
 
17
21
  const MAX_TREE_DEPTH = 6;
22
+ const CYCLE_NODE_ICON = "↺";
23
+ const REPEATED_NODE_ICON = "⤴";
18
24
  const highlightStr = (s, highlight) => {
19
25
  if (highlight && s?.includes(highlight)) {
20
26
  s = s.replaceAll(highlight, `\x1b[1;33m${highlight}\x1b[0m`);
21
27
  }
22
28
  return s;
23
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
+ };
24
64
  /**
25
65
  * Prints the BOM components as a streaming table to the console.
26
66
  * Delegates to {@link printOSTable} automatically when the BOM metadata indicates
@@ -29,12 +69,14 @@ const highlightStr = (s, highlight) => {
29
69
  * @param {Object} bomJson CycloneDX BOM JSON object
30
70
  * @param {string[]} [filterTypes] Optional list of component types to include; all types shown when omitted
31
71
  * @param {string} [highlight] Optional string to highlight in the output
72
+ * @param {string} [summaryText] Optional summary message to print after the table
32
73
  * @returns {void}
33
74
  */
34
75
  export function printTable(
35
76
  bomJson,
36
77
  filterTypes = undefined,
37
78
  highlight = undefined,
79
+ summaryText = undefined,
38
80
  ) {
39
81
  if (!bomJson?.components) {
40
82
  return;
@@ -59,6 +101,7 @@ export function printTable(
59
101
  ],
60
102
  };
61
103
  const stream = createStream(config);
104
+ let displayedProvenanceCount = 0;
62
105
  stream.write([
63
106
  filterTypes?.includes("cryptographic-asset")
64
107
  ? "Asset Type / Group"
@@ -81,9 +124,12 @@ export function printTable(
81
124
  (comp.tags || []).join(", "),
82
125
  ]);
83
126
  } else {
127
+ if (hasComponentRegistryProvenance(comp)) {
128
+ displayedProvenanceCount += 1;
129
+ }
84
130
  stream.write([
85
131
  highlightStr(comp.group || "", highlight),
86
- highlightStr(comp.name, highlight),
132
+ formatComponentName(comp, highlight),
87
133
  `\x1b[1;35m${comp.version || ""}\x1b[0m`,
88
134
  comp.scope || "",
89
135
  (comp.tags || []).join(", "),
@@ -92,7 +138,9 @@ export function printTable(
92
138
  }
93
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 = [];
@@ -326,19 +380,8 @@ export function printDependencyTree(
326
380
  if (!dependencies.length) {
327
381
  return;
328
382
  }
329
- const depMap = {};
330
- const shownList = [];
331
- for (const d of dependencies) {
332
- if (d[mode]?.length) {
333
- depMap[d.ref] = d[mode].sort();
334
- } else {
335
- if (mode === "provides") {
336
- shownList.push(d.ref);
337
- }
338
- }
339
- }
340
- const treeGraphics = [];
341
- recursePrint(depMap, dependencies, 0, shownList, treeGraphics);
383
+ const treeGraphics = buildDependencyTreeLines(dependencies, mode);
384
+ const legendLines = buildDependencyTreeLegendLines(treeGraphics);
342
385
  // table library is too slow for display large lists.
343
386
  // Fixes #491
344
387
  if (treeGraphics.length && treeGraphics.length < 100) {
@@ -359,64 +402,203 @@ export function printDependencyTree(
359
402
  } else {
360
403
  console.log(highlightStr(treeGraphics.slice(0, 500).join("\n"), highlight));
361
404
  }
405
+ if (legendLines.length) {
406
+ console.log(legendLines.join("\n"));
407
+ }
362
408
  }
363
409
 
364
- const levelPrefix = (level, isLast) => {
365
- if (level === 0) {
366
- return SYMBOLS_ANSI.EMPTY;
410
+ const dependencyTreePrefix = (ancestorContinuations, isLast) => {
411
+ let prefix = "";
412
+ for (const hasNextSibling of ancestorContinuations) {
413
+ prefix = `${prefix}${hasNextSibling ? "│ " : " "}`;
367
414
  }
368
- let prefix = `${isLast ? SYMBOLS_ANSI.LAST_BRANCH : SYMBOLS_ANSI.BRANCH}`;
369
- for (let i = 0; i < level - 1; i++) {
370
- prefix = `${
371
- isLast
372
- ? SYMBOLS_ANSI.LAST_BRANCH.replace(" ", "─")
373
- : SYMBOLS_ANSI.VERTICAL
374
- }${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;
375
423
  }
376
- return prefix;
424
+ return a.ref.localeCompare(b.ref);
377
425
  };
378
426
 
379
- const isReallyRoot = (depMap, refStr) => {
380
- for (const k of Object.keys(depMap)) {
381
- const dependsOn = depMap[k] || [];
382
- if (
383
- dependsOn.includes(refStr) ||
384
- dependsOn.includes(refStr.toLowerCase())
385
- ) {
386
- 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;
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));
387
471
  }
388
472
  }
389
- return true;
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
+ );
478
+ }
479
+
480
+ return nodes;
390
481
  };
391
482
 
392
- const recursePrint = (depMap, subtree, level, shownList, treeGraphics) => {
393
- const listToUse = Array.isArray(subtree) ? subtree : [subtree];
394
- for (let i = 0; i < listToUse.length; i++) {
395
- const l = listToUse[i];
396
- const refStr = l.ref || l;
397
- if (
398
- (level === 0 &&
399
- isReallyRoot(depMap, refStr) &&
400
- !shownList.includes(refStr.toLowerCase())) ||
401
- level > 0
402
- ) {
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) {
403
530
  treeGraphics.push(
404
- `${levelPrefix(level, i === listToUse.length - 1)}${refStr}`,
531
+ `${dependencyTreePrefix(nextAncestorContinuations, childIsLast)}${CYCLE_NODE_ICON} ${childNode.ref}`,
405
532
  );
406
- shownList.push(refStr.toLowerCase());
407
- if (l && depMap[refStr]) {
408
- if (level < MAX_TREE_DEPTH) {
409
- recursePrint(
410
- depMap,
411
- depMap[refStr],
412
- level + 1,
413
- shownList,
414
- treeGraphics,
415
- );
416
- }
417
- }
533
+ continue;
534
+ }
535
+ if (childEntry.isRepeated) {
536
+ treeGraphics.push(
537
+ `${dependencyTreePrefix(nextAncestorContinuations, childIsLast)}${REPEATED_NODE_ICON} ${childNode.ref}`,
538
+ );
539
+ continue;
418
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
+ );
419
600
  }
601
+ return treeGraphics;
420
602
  };
421
603
 
422
604
  /**