@cyclonedx/cdxgen 12.1.4 → 12.2.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 (184) hide show
  1. package/README.md +47 -39
  2. package/bin/cdxgen.js +181 -90
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +3 -3
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +484 -440
  14. package/lib/evinser/db.js +137 -0
  15. package/lib/{helpers → evinser}/db.poku.js +2 -6
  16. package/lib/evinser/evinser.js +5 -18
  17. package/lib/evinser/swiftsem.js +1 -1
  18. package/lib/helpers/bomSigner.js +312 -0
  19. package/lib/helpers/bomSigner.poku.js +156 -0
  20. package/lib/helpers/caxa.js +1 -1
  21. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  22. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  23. package/lib/helpers/ciParsers/circleCi.js +286 -0
  24. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  25. package/lib/helpers/ciParsers/common.js +24 -0
  26. package/lib/helpers/ciParsers/githubActions.js +636 -0
  27. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  28. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  29. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  30. package/lib/helpers/ciParsers/jenkins.js +181 -0
  31. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  32. package/lib/helpers/depsUtils.js +203 -0
  33. package/lib/helpers/depsUtils.poku.js +150 -0
  34. package/lib/helpers/display.js +429 -14
  35. package/lib/helpers/envcontext.js +23 -8
  36. package/lib/helpers/formulationParsers.js +351 -0
  37. package/lib/helpers/logger.js +14 -0
  38. package/lib/helpers/protobom.js +9 -9
  39. package/lib/helpers/pythonutils.js +305 -0
  40. package/lib/helpers/pythonutils.poku.js +469 -0
  41. package/lib/helpers/utils.js +970 -528
  42. package/lib/helpers/utils.poku.js +139 -256
  43. package/lib/helpers/versutils.js +202 -0
  44. package/lib/helpers/versutils.poku.js +315 -0
  45. package/lib/helpers/vsixutils.js +1061 -0
  46. package/lib/helpers/vsixutils.poku.js +2247 -0
  47. package/lib/managers/binary.js +19 -19
  48. package/lib/managers/docker.js +108 -1
  49. package/lib/managers/oci.js +10 -0
  50. package/lib/managers/piptree.js +4 -10
  51. package/lib/parsers/npmrc.js +92 -0
  52. package/lib/parsers/npmrc.poku.js +528 -0
  53. package/lib/server/openapi.yaml +1 -10
  54. package/lib/server/server.js +58 -16
  55. package/lib/server/server.poku.js +123 -144
  56. package/lib/stages/postgen/annotator.js +1 -1
  57. package/lib/stages/postgen/auditBom.js +197 -0
  58. package/lib/stages/postgen/auditBom.poku.js +378 -0
  59. package/lib/stages/postgen/postgen.js +54 -1
  60. package/lib/stages/postgen/postgen.poku.js +90 -1
  61. package/lib/stages/postgen/ruleEngine.js +369 -0
  62. package/lib/stages/pregen/envAudit.js +299 -0
  63. package/lib/stages/pregen/envAudit.poku.js +572 -0
  64. package/lib/stages/pregen/pregen.js +12 -8
  65. package/lib/third-party/arborist/lib/deepest-nesting-target.js +1 -1
  66. package/lib/third-party/arborist/lib/node.js +3 -3
  67. package/lib/third-party/arborist/lib/shrinkwrap.js +1 -1
  68. package/lib/third-party/arborist/lib/tree-check.js +1 -1
  69. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  70. package/lib/validator/complianceEngine.js +241 -0
  71. package/lib/validator/complianceEngine.poku.js +168 -0
  72. package/lib/validator/complianceRules.js +1610 -0
  73. package/lib/validator/complianceRules.poku.js +328 -0
  74. package/lib/validator/index.js +222 -0
  75. package/lib/validator/index.poku.js +144 -0
  76. package/lib/validator/reporters/annotations.js +121 -0
  77. package/lib/validator/reporters/console.js +149 -0
  78. package/lib/validator/reporters/index.js +41 -0
  79. package/lib/validator/reporters/json.js +37 -0
  80. package/lib/validator/reporters/sarif.js +184 -0
  81. package/lib/validator/reporters.poku.js +150 -0
  82. package/package.json +8 -8
  83. package/types/bin/sign.d.ts +3 -0
  84. package/types/bin/sign.d.ts.map +1 -0
  85. package/types/bin/validate.d.ts +3 -0
  86. package/types/bin/validate.d.ts.map +1 -0
  87. package/types/helpers/utils.d.ts +0 -1
  88. package/types/lib/cli/index.d.ts +49 -52
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/evinser/db.d.ts +34 -0
  91. package/types/lib/evinser/db.d.ts.map +1 -0
  92. package/types/lib/evinser/evinser.d.ts +63 -16
  93. package/types/lib/evinser/evinser.d.ts.map +1 -1
  94. package/types/lib/helpers/bomSigner.d.ts +27 -0
  95. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  96. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  98. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  100. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  102. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  104. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  106. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  108. package/types/lib/helpers/depsUtils.d.ts +21 -0
  109. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  110. package/types/lib/helpers/display.d.ts +111 -11
  111. package/types/lib/helpers/display.d.ts.map +1 -1
  112. package/types/lib/helpers/envcontext.d.ts +19 -7
  113. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  114. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  115. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  116. package/types/lib/helpers/logger.d.ts +15 -1
  117. package/types/lib/helpers/logger.d.ts.map +1 -1
  118. package/types/lib/helpers/protobom.d.ts +2 -2
  119. package/types/lib/helpers/pythonutils.d.ts +18 -0
  120. package/types/lib/helpers/pythonutils.d.ts.map +1 -0
  121. package/types/lib/helpers/utils.d.ts +532 -128
  122. package/types/lib/helpers/utils.d.ts.map +1 -1
  123. package/types/lib/helpers/versutils.d.ts +8 -0
  124. package/types/lib/helpers/versutils.d.ts.map +1 -0
  125. package/types/lib/helpers/vsixutils.d.ts +130 -0
  126. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  127. package/types/lib/managers/docker.d.ts +12 -31
  128. package/types/lib/managers/docker.d.ts.map +1 -1
  129. package/types/lib/managers/oci.d.ts +11 -1
  130. package/types/lib/managers/oci.d.ts.map +1 -1
  131. package/types/lib/managers/piptree.d.ts.map +1 -1
  132. package/types/lib/parsers/npmrc.d.ts +26 -0
  133. package/types/lib/parsers/npmrc.d.ts.map +1 -0
  134. package/types/lib/server/server.d.ts +21 -2
  135. package/types/lib/server/server.d.ts.map +1 -1
  136. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  137. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  138. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  139. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  140. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  141. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  142. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  143. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  144. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  145. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  146. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  147. package/types/lib/validator/complianceEngine.d.ts +66 -0
  148. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  149. package/types/lib/validator/complianceRules.d.ts +70 -0
  150. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  151. package/types/lib/validator/index.d.ts +70 -0
  152. package/types/lib/validator/index.d.ts.map +1 -0
  153. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  154. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  155. package/types/lib/validator/reporters/console.d.ts +30 -0
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  157. package/types/lib/validator/reporters/index.d.ts +21 -0
  158. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  159. package/types/lib/validator/reporters/json.d.ts +11 -0
  160. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  161. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  162. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  163. package/lib/helpers/db.js +0 -162
  164. package/types/helpers/db.d.ts +0 -35
  165. package/types/helpers/db.d.ts.map +0 -1
  166. package/types/lib/helpers/db.d.ts +0 -35
  167. package/types/lib/helpers/db.d.ts.map +0 -1
  168. package/types/lib/helpers/validator.d.ts.map +0 -1
  169. package/types/managers/binary.d.ts +0 -37
  170. package/types/managers/binary.d.ts.map +0 -1
  171. package/types/managers/docker.d.ts +0 -56
  172. package/types/managers/docker.d.ts.map +0 -1
  173. package/types/managers/oci.d.ts +0 -2
  174. package/types/managers/oci.d.ts.map +0 -1
  175. package/types/managers/piptree.d.ts +0 -2
  176. package/types/managers/piptree.d.ts.map +0 -1
  177. package/types/server/server.d.ts +0 -34
  178. package/types/server/server.d.ts.map +0 -1
  179. package/types/stages/postgen/annotator.d.ts +0 -27
  180. package/types/stages/postgen/annotator.d.ts.map +0 -1
  181. package/types/stages/postgen/postgen.d.ts +0 -51
  182. package/types/stages/postgen/postgen.d.ts.map +0 -1
  183. package/types/stages/pregen/pregen.d.ts +0 -59
  184. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -1,8 +1,11 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
2
3
  import process from "node:process";
3
4
 
4
5
  import { createStream, table } from "table";
5
6
 
7
+ import { isSecureMode, safeExistsSync, toCamel } from "./utils.js";
8
+
6
9
  // https://github.com/yangshun/tree-node-cli/blob/master/src/index.js
7
10
  const SYMBOLS_ANSI = {
8
11
  BRANCH: "├── ",
@@ -19,12 +22,22 @@ const highlightStr = (s, highlight) => {
19
22
  }
20
23
  return s;
21
24
  };
25
+ /**
26
+ * Prints the BOM components as a streaming table to the console.
27
+ * Delegates to {@link printOSTable} automatically when the BOM metadata indicates
28
+ * an operating-system or platform component type.
29
+ *
30
+ * @param {Object} bomJson CycloneDX BOM JSON object
31
+ * @param {string[]} [filterTypes] Optional list of component types to include; all types shown when omitted
32
+ * @param {string} [highlight] Optional string to highlight in the output
33
+ * @returns {void}
34
+ */
22
35
  export function printTable(
23
36
  bomJson,
24
37
  filterTypes = undefined,
25
38
  highlight = undefined,
26
39
  ) {
27
- if (!bomJson || !bomJson.components) {
40
+ if (!bomJson?.components) {
28
41
  return;
29
42
  }
30
43
  if (
@@ -98,6 +111,12 @@ const formatProps = (props) => {
98
111
  }
99
112
  return retList.join("\n");
100
113
  };
114
+ /**
115
+ * Prints OS package components from the BOM as a formatted streaming table.
116
+ *
117
+ * @param {Object} bomJson CycloneDX BOM JSON object
118
+ * @returns {void}
119
+ */
101
120
  export function printOSTable(bomJson) {
102
121
  const config = {
103
122
  columnDefault: {
@@ -118,9 +137,16 @@ export function printOSTable(bomJson) {
118
137
  }
119
138
  console.log();
120
139
  }
140
+ /**
141
+ * Prints the services listed in the BOM as a formatted table.
142
+ * Includes endpoint URLs, authentication flag, and cross-trust-boundary flag.
143
+ *
144
+ * @param {Object} bomJson CycloneDX BOM JSON object
145
+ * @returns {void}
146
+ */
121
147
  export function printServices(bomJson) {
122
148
  const data = [["Name", "Endpoints", "Authenticated", "X Trust Boundary"]];
123
- if (!bomJson || !bomJson.services) {
149
+ if (!bomJson?.services) {
124
150
  return;
125
151
  }
126
152
  for (const aservice of bomJson.services) {
@@ -142,9 +168,15 @@ export function printServices(bomJson) {
142
168
  }
143
169
  }
144
170
 
171
+ /**
172
+ * Prints the formulation components from the BOM as a formatted table.
173
+ *
174
+ * @param {Object} bomJson CycloneDX BOM JSON object
175
+ * @returns {void}
176
+ */
145
177
  export function printFormulation(bomJson) {
146
- const data = [["Tyoe", "Name", "Version"]];
147
- if (!bomJson || !bomJson.formulation) {
178
+ const data = [["Type", "Name", "Version"]];
179
+ if (!bomJson?.formulation) {
148
180
  return;
149
181
  }
150
182
  for (const aform of bomJson.formulation) {
@@ -178,8 +210,15 @@ const locationComparator = (a, b) => {
178
210
  return a.localeCompare(b);
179
211
  };
180
212
 
213
+ /**
214
+ * Prints component evidence occurrences (file locations) as a streaming table.
215
+ * Only components that have `evidence.occurrences` are included.
216
+ *
217
+ * @param {Object} bomJson CycloneDX BOM JSON object
218
+ * @returns {void}
219
+ */
181
220
  export function printOccurrences(bomJson) {
182
- if (!bomJson || !bomJson.components) {
221
+ if (!bomJson?.components) {
183
222
  return;
184
223
  }
185
224
  const data = ["Group", "Name", "Version", "Occurrences"];
@@ -217,17 +256,20 @@ export function printOccurrences(bomJson) {
217
256
  console.log();
218
257
  }
219
258
 
259
+ /**
260
+ * Prints the call stack evidence for each component in the BOM as a formatted table.
261
+ * Only components that have `evidence.callstack.frames` are included.
262
+ *
263
+ * @param {Object} bomJson CycloneDX BOM JSON object
264
+ * @returns {void}
265
+ */
220
266
  export function printCallStack(bomJson) {
221
267
  const data = [["Group", "Name", "Version", "Call Stack"]];
222
- if (!bomJson || !bomJson.components) {
268
+ if (!bomJson?.components) {
223
269
  return;
224
270
  }
225
271
  for (const comp of bomJson.components) {
226
- if (
227
- !comp.evidence ||
228
- !comp.evidence.callstack ||
229
- !comp.evidence.callstack.frames
230
- ) {
272
+ if (!comp.evidence?.callstack?.frames) {
231
273
  continue;
232
274
  }
233
275
  const frames = Array.from(
@@ -264,6 +306,15 @@ export function printCallStack(bomJson) {
264
306
  console.log(table(data, config));
265
307
  }
266
308
  }
309
+ /**
310
+ * Prints the dependency tree from the BOM as an ASCII tree diagram.
311
+ * Uses the `table` library for small trees and plain console output for larger ones.
312
+ *
313
+ * @param {Object} bomJson CycloneDX BOM JSON object containing a `dependencies` array
314
+ * @param {string} [mode="dependsOn"] Dependency relation to traverse (`"dependsOn"` or `"provides"`)
315
+ * @param {string} [highlight] Optional string to highlight in the tree output
316
+ * @returns {void}
317
+ */
267
318
  export function printDependencyTree(
268
319
  bomJson,
269
320
  mode = "dependsOn",
@@ -366,9 +417,16 @@ const recursePrint = (depMap, subtree, level, shownList, treeGraphics) => {
366
417
  }
367
418
  };
368
419
 
420
+ /**
421
+ * Prints a table of reachable components derived from a reachability slices file.
422
+ * Aggregates per-purl reachable-flow counts and sorts them descending.
423
+ *
424
+ * @param {Object} sliceArtefacts Slice artefact paths, must include `reachablesSlicesFile`
425
+ * @returns {void}
426
+ */
369
427
  export function printReachables(sliceArtefacts) {
370
428
  const reachablesSlicesFile = sliceArtefacts.reachablesSlicesFile;
371
- if (!existsSync(reachablesSlicesFile)) {
429
+ if (!safeExistsSync(reachablesSlicesFile)) {
372
430
  return;
373
431
  }
374
432
  const purlCounts = {};
@@ -402,6 +460,12 @@ export function printReachables(sliceArtefacts) {
402
460
  }
403
461
  }
404
462
 
463
+ /**
464
+ * Prints a formatted table of CycloneDX vulnerability objects.
465
+ *
466
+ * @param {Object[]} vulnerabilities Array of CycloneDX vulnerability objects
467
+ * @returns {void}
468
+ */
405
469
  export function printVulnerabilities(vulnerabilities) {
406
470
  if (!vulnerabilities) {
407
471
  return;
@@ -430,6 +494,14 @@ export function printVulnerabilities(vulnerabilities) {
430
494
  console.log(`${vulnerabilities.length} vulnerabilities found.`);
431
495
  }
432
496
 
497
+ /**
498
+ * Prints an OWASP donation banner when running in a CI environment.
499
+ * The banner is suppressed when `options.noBanner` is set or the repository
500
+ * belongs to the cdxgen project itself.
501
+ *
502
+ * @param {Object} options CLI options
503
+ * @returns {void}
504
+ */
433
505
  export function printSponsorBanner(options) {
434
506
  if (
435
507
  process?.env?.CI &&
@@ -443,7 +515,7 @@ export function printSponsorBanner(options) {
443
515
  },
444
516
  };
445
517
  let message =
446
- "OWASP foundation relies on donations to fund our projects.\nDonation link: https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX";
518
+ "OWASP foundation relies on donations to fund our projects.\nDonation link: https://owasp.org/donate/?reponame=www-project-cdxgen&title=OWASP+cdxgen";
447
519
  if (options.serverUrl && options.apiKey) {
448
520
  message = `${message}\nDependency Track: https://owasp.org/donate/?reponame=www-project-dependency-track&title=OWASP+Dependency-Track`;
449
521
  }
@@ -452,6 +524,13 @@ export function printSponsorBanner(options) {
452
524
  }
453
525
  }
454
526
 
527
+ /**
528
+ * Prints a BOM summary table including generator tool names, component package types,
529
+ * and component namespaces extracted from BOM metadata properties.
530
+ *
531
+ * @param {Object} bomJson CycloneDX BOM JSON object
532
+ * @returns {void}
533
+ */
455
534
  export function printSummary(bomJson) {
456
535
  const config = {
457
536
  header: {
@@ -501,3 +580,339 @@ export function printSummary(bomJson) {
501
580
  const data = [[message]];
502
581
  console.log(table(data, config));
503
582
  }
583
+
584
+ /**
585
+ * @typedef {{type: string, variable: string, severity: string, message: string, mitigation: string}} EnvAuditFinding
586
+ */
587
+
588
+ /**
589
+ * Runs the pre-generation environment audit and renders the results as formatted
590
+ * tables to the console. Called when the --env-audit CLI flag is set.
591
+ *
592
+ * @param {string} filePath Project path being scanned
593
+ * @param {Object} config Loaded .cdxgenrc / config-file values
594
+ * @param {Object} options Effective CLI options
595
+ * @param {EnvAuditFinding[]} envAuditFindings Audit findings to display
596
+ */
597
+ export function displaySelfThreatModel(
598
+ filePath,
599
+ config,
600
+ options,
601
+ envAuditFindings,
602
+ ) {
603
+ const TLP = options.tlpClassification || "CLEAR";
604
+ const risks = [];
605
+ let riskScore = 0;
606
+
607
+ const addRisk = (level, reason, category = "configuration") => {
608
+ const scores = { low: 1, medium: 3, high: 5, critical: 8 };
609
+ riskScore = Math.min(10, riskScore + scores[level]);
610
+ risks.push({ level, reason, category });
611
+ };
612
+
613
+ // Config file risks
614
+ if (Object.keys(config).length > 0) {
615
+ addRisk(
616
+ "medium",
617
+ "A .cdxgenrc config file was loaded from the working directory. It may override security-relevant settings without being visible on the command line.",
618
+ "configuration",
619
+ );
620
+ const sensitive = ["server-url", "api-key", "include-formulation"];
621
+ for (const key of sensitive) {
622
+ if (config[key] || config[toCamel(key)]) {
623
+ addRisk(
624
+ key === "api-key" ? "high" : "medium",
625
+ `Config file sets '${key}', which affects SBOM content or remote submission behavior.`,
626
+ "configuration",
627
+ );
628
+ }
629
+ }
630
+ }
631
+
632
+ // Remote submission risks
633
+ if (options.serverUrl) {
634
+ const isHttps = options.serverUrl.startsWith("https://");
635
+ addRisk(
636
+ isHttps ? "medium" : "critical",
637
+ `SBOM will be submitted to ${options.serverUrl}${!isHttps ? " over plain HTTP — contents may be intercepted or tampered in transit." : "."}`,
638
+ "network",
639
+ );
640
+ if (options.skipDtTlsCheck) {
641
+ addRisk(
642
+ "high",
643
+ "TLS certificate validation is disabled for Dependency-Track uploads. SBOM contents may be intercepted or tampered in transit.",
644
+ "network",
645
+ );
646
+ }
647
+ }
648
+
649
+ // Data exposure risks
650
+ if (options.includeFormulation) {
651
+ addRisk(
652
+ "medium",
653
+ "Formulation mode is active. The SBOM will include build metadata such as git history, committer identities, and CI environment variables.",
654
+ "data-exposure",
655
+ );
656
+ }
657
+ if (options.evidence || options.deep) {
658
+ addRisk(
659
+ "medium",
660
+ "Evidence / deep mode will invoke build tools and parse source files to collect call graph and reachability evidence. Malicious build scripts may execute.",
661
+ "data-exposure",
662
+ );
663
+ }
664
+ if (options.installDeps) {
665
+ addRisk(
666
+ "high",
667
+ "Dependency auto-install is enabled. Lifecycle hooks (install scripts) from third-party packages will execute in the current environment.",
668
+ "data-exposure",
669
+ );
670
+ }
671
+
672
+ // Output path outside the project directory
673
+ if (options.output) {
674
+ const resolvedOutput = path.resolve(options.output);
675
+ const resolvedProject = path.resolve(filePath);
676
+ if (
677
+ !resolvedOutput.startsWith(resolvedProject + path.sep) &&
678
+ resolvedOutput !== resolvedProject
679
+ ) {
680
+ addRisk(
681
+ "medium",
682
+ `Output path '${options.output}' resolves to '${resolvedOutput}', which is outside the project directory '${resolvedProject}'. Ensure this is intentional.`,
683
+ "configuration",
684
+ );
685
+ }
686
+ }
687
+
688
+ // Environment variable risks (config-layer only; env-audit covers the rest)
689
+ if (process.env.CDXGEN_SERVER_URL) {
690
+ addRisk(
691
+ "low",
692
+ "CDXGEN_SERVER_URL is set in the environment and will override any --server-url value.",
693
+ "environment",
694
+ );
695
+ }
696
+
697
+ // Integrate environment audit findings
698
+ if (envAuditFindings?.length) {
699
+ for (const f of envAuditFindings) {
700
+ const categoryMap = {
701
+ "code-execution": "runtime",
702
+ "debug-exposure": "runtime",
703
+ "environment-variable": "environment",
704
+ "network-interception": "network",
705
+ "credential-exposure": "environment",
706
+ "permission-misuse": "runtime",
707
+ privilege: "runtime",
708
+ };
709
+ addRisk(
710
+ f.severity,
711
+ `${f.variable}: ${f.message}`,
712
+ categoryMap[f.type] || "configuration",
713
+ );
714
+ }
715
+ }
716
+
717
+ const nodeOptions = process.env.NODE_OPTIONS || "";
718
+ const riskLevel =
719
+ riskScore >= 8
720
+ ? "CRITICAL"
721
+ : riskScore >= 5
722
+ ? "HIGH"
723
+ : riskScore >= 3
724
+ ? "MEDIUM"
725
+ : "LOW";
726
+
727
+ const riskColor = {
728
+ CRITICAL: "\x1b[1;31m",
729
+ HIGH: "\x1b[1;33m",
730
+ MEDIUM: "\x1b[1;36m",
731
+ LOW: "\x1b[1;32m",
732
+ };
733
+ const reset = "\x1b[0m";
734
+ const tlpGuidance = {
735
+ CLEAR: "May be shared publicly. No restrictions.",
736
+ GREEN: "Limited to community/peers. Not for public posting.",
737
+ AMBER:
738
+ "Limited to organisation and trusted partners. Handle-in-confidence.",
739
+ AMBER_AND_STRICT: "Organisation only. No external sharing.",
740
+ RED: "Named recipients only. Do not forward or store beyond session.",
741
+ };
742
+ const headerData = [
743
+ ["TLP Classification", `${TLP} — ${tlpGuidance[TLP]}`],
744
+ ["Risk Score", `${riskScore}/10`],
745
+ ["Risk Level", `${riskColor[riskLevel]}${riskLevel}${reset}`],
746
+ ];
747
+ const headerConfig = {
748
+ header: {
749
+ alignment: "center",
750
+ content:
751
+ "SBOM Generation Environment Assessment\nPre-generation security audit by cdxgen",
752
+ },
753
+ columns: [{ width: 30, alignment: "right" }, { width: 70 }],
754
+ columnDefault: { wrapWord: true },
755
+ };
756
+
757
+ console.log(table(headerData, headerConfig));
758
+ if (risks.length > 0) {
759
+ const findingsData = [["#", "Severity", "Category", "Finding"]];
760
+ risks.forEach(({ level, reason, category }, i) => {
761
+ const severityColor =
762
+ level === "critical"
763
+ ? "\x1b[1;31m"
764
+ : level === "high"
765
+ ? "\x1b[1;33m"
766
+ : level === "medium"
767
+ ? "\x1b[1;36m"
768
+ : "\x1b[1;32m";
769
+ findingsData.push([
770
+ `${i + 1}`,
771
+ `${severityColor}${level.toUpperCase()}${reset}`,
772
+ category,
773
+ reason,
774
+ ]);
775
+ });
776
+ const findingsConfig = {
777
+ header: {
778
+ alignment: "center",
779
+ content: `Findings (${risks.length})`,
780
+ },
781
+ columns: [
782
+ { width: 5, alignment: "right" },
783
+ { width: 12 },
784
+ { width: 17 },
785
+ { width: 66 },
786
+ ],
787
+ columnDefault: { wrapWord: true },
788
+ };
789
+ console.log(table(findingsData, findingsConfig));
790
+ } else {
791
+ const noFindingsData = [
792
+ [
793
+ `${riskColor[riskLevel]}✅ No risks detected in the current configuration.${reset}`,
794
+ ],
795
+ ];
796
+ const noFindingsConfig = {
797
+ header: { alignment: "center", content: "📋 Findings" },
798
+ columns: [{ width: 100, alignment: "center" }],
799
+ };
800
+ console.log(table(noFindingsData, noFindingsConfig));
801
+ }
802
+
803
+ const configData = [
804
+ ["Setting", "Value"],
805
+ ["Project", options.projectName || filePath],
806
+ ["Type(s)", options.projectType?.join(", ") || "auto-detect"],
807
+ ["Profile", options.profile || "generic"],
808
+ ["Path", filePath],
809
+ ["Output", options.output || "(stdout)"],
810
+ ["Recursive", options.recursive ? "yes" : "no"],
811
+ ["Remote Submission", options.serverUrl || "none"],
812
+ ["Formulation", options.includeFormulation ? "yes" : "no"],
813
+ ["Evidence / Deep Mode", options.evidence || options.deep ? "yes" : "no"],
814
+ ["Auto-install Dependencies", options.installDeps ? "yes" : "no"],
815
+ ["NODE_OPTIONS", nodeOptions || "(not set)"],
816
+ ];
817
+ const effConfigTableConfig = {
818
+ header: { alignment: "center", content: "Effective Configuration" },
819
+ columns: [{ width: 28 }, { width: 72 }],
820
+ columnDefault: { wrapWord: true },
821
+ };
822
+ console.log(table(configData, effConfigTableConfig));
823
+
824
+ const recommendations = [];
825
+ if (["AMBER", "AMBER_AND_STRICT", "RED"].includes(TLP)) {
826
+ recommendations.push([
827
+ "High",
828
+ "Omit --include-formulation to avoid embedding committer identities and CI secrets in the SBOM.",
829
+ ]);
830
+ if (TLP === "RED") {
831
+ recommendations.push([
832
+ "Critical",
833
+ "Run cdxgen inside an isolated container or VM with no access to production credentials.",
834
+ ]);
835
+ recommendations.push([
836
+ "Critical",
837
+ "Do not set --server-url; review and handle the output SBOM manually before sharing.",
838
+ ]);
839
+ }
840
+ }
841
+ if (riskScore >= 5) {
842
+ recommendations.push([
843
+ "High",
844
+ "Address the findings above before scanning untrusted repositories.",
845
+ ]);
846
+ recommendations.push([
847
+ "Medium",
848
+ "Pass --no-install-deps to prevent package manager hooks from executing.",
849
+ ]);
850
+ }
851
+ if (envAuditFindings.some((f) => f.type === "code-execution")) {
852
+ recommendations.push([
853
+ "High",
854
+ "Remove code-execution flags (--require, --eval, --loader, --import) from NODE_OPTIONS and JAVA_TOOL_OPTIONS.",
855
+ ]);
856
+ }
857
+ if (envAuditFindings.some((f) => f.variable === "NODE_PATH")) {
858
+ recommendations.push([
859
+ "High",
860
+ "Unset NODE_PATH to prevent module-resolution poisoning by malicious packages.",
861
+ ]);
862
+ }
863
+ if (envAuditFindings.some((f) => f.type === "privilege")) {
864
+ recommendations.push([
865
+ "High",
866
+ "Do not run cdxgen as root. Create a dedicated low-privilege user or use a rootless container.",
867
+ ]);
868
+ }
869
+ if (/--permission\b/i.test(nodeOptions)) {
870
+ recommendations.push([
871
+ "Medium",
872
+ "Audit every --allow-* scope; use absolute paths rather than wildcards to minimise the permission surface.",
873
+ ]);
874
+ }
875
+ recommendations.push([
876
+ "Info",
877
+ "Minimal safe invocation: cdxgen --no-install-deps --output ./sbom.cdx.json <path>",
878
+ ]);
879
+ const recommendationsData = [["Priority", "Action"]];
880
+ recommendations.forEach(([priority, action]) => {
881
+ const priorityColor =
882
+ priority === "Critical"
883
+ ? "\x1b[1;31m"
884
+ : priority === "High"
885
+ ? "\x1b[1;33m"
886
+ : priority === "Medium"
887
+ ? "\x1b[1;36m"
888
+ : "\x1b[1;32m";
889
+ recommendationsData.push([`${priorityColor}${priority}${reset}`, action]);
890
+ });
891
+ const recommendationsConfig = {
892
+ header: {
893
+ alignment: "center",
894
+ content: `Recommendations for TLP:${TLP}`,
895
+ },
896
+ columns: [{ width: 12 }, { width: 88 }],
897
+ columnDefault: { wrapWord: true },
898
+ };
899
+
900
+ console.log(table(recommendationsData, recommendationsConfig));
901
+ // Only abort in secure mode when at least one finding is high or critical severity.
902
+ // Accumulated low/medium findings may push riskScore above 5 but should not abort.
903
+ if (
904
+ isSecureMode &&
905
+ envAuditFindings?.some((f) => ["high", "critical"].includes(f.severity))
906
+ ) {
907
+ const abortData = [
908
+ [
909
+ `${riskColor[riskLevel]}🚫 SECURE MODE: High-risk configuration detected. Aborting SBOM generation.${reset}`,
910
+ ],
911
+ ];
912
+ const abortConfig = {
913
+ columns: [{ width: 100, alignment: "center" }],
914
+ };
915
+ console.log(table(abortData, abortConfig));
916
+ process.exit(1);
917
+ }
918
+ }
@@ -30,13 +30,14 @@ export const GIT_COMMAND = process.env.GIT_CMD || "git";
30
30
  // sdkman tool aliases
31
31
  export const SDKMAN_JAVA_TOOL_ALIASES = {
32
32
  java8: process.env.JAVA8_TOOL || "8.0.452-amzn", // Temurin no longer offers java8 :(
33
- java11: process.env.JAVA11_TOOL || "11.0.29-tem",
34
- java17: process.env.JAVA17_TOOL || "17.0.17-tem",
35
- java21: process.env.JAVA21_TOOL || "21.0.9-tem",
33
+ java11: process.env.JAVA11_TOOL || "11.0.30-tem",
34
+ java17: process.env.JAVA17_TOOL || "17.0.18-tem",
35
+ java21: process.env.JAVA21_TOOL || "21.0.10-tem",
36
36
  java22: process.env.JAVA22_TOOL || "22.0.2-tem",
37
37
  java23: process.env.JAVA23_TOOL || "23.0.2-tem",
38
38
  java24: process.env.JAVA24_TOOL || "24.0.2-tem",
39
- java25: process.env.JAVA25_TOOL || "25.0.1-tem",
39
+ java25: process.env.JAVA25_TOOL || "25.0.2-tem",
40
+ java26: process.env.JAVA26_TOOL || "26-tem",
40
41
  };
41
42
 
42
43
  /**
@@ -206,7 +207,7 @@ export function collectPythonInfo(dir) {
206
207
  ]);
207
208
  const moduleDesc =
208
209
  getCommandOutput(getPythonCommand(), dir, [
209
- "-S",
210
+ "-I",
210
211
  "-m",
211
212
  "pip",
212
213
  "--version",
@@ -257,7 +258,7 @@ export function collectGccInfo(dir) {
257
258
  type: "platform",
258
259
  name: "gcc",
259
260
  version: versionDesc.split("\n")[0],
260
- description: moduleDesc.replaceAll("\n", "\\n"),
261
+ description: (moduleDesc || "").replaceAll("\n", "\\n"),
261
262
  };
262
263
  }
263
264
  return undefined;
@@ -277,7 +278,7 @@ export function collectRustInfo(dir) {
277
278
  type: "platform",
278
279
  name: "rustc",
279
280
  version: versionDesc.trim(),
280
- description: moduleDesc.trim(),
281
+ description: (moduleDesc || "").trim(),
281
282
  };
282
283
  }
283
284
  return undefined;
@@ -405,6 +406,12 @@ const getCommandOutput = (cmd, dir, args) => {
405
406
  }
406
407
  if (DEBUG_MODE) {
407
408
  if (dir) {
409
+ if (safeExistsSync(join(dir, commandToUse))) {
410
+ console.warn(
411
+ `SECURE MODE: Found ${commandToUse} inside ${dir}. This command will not be executed.`,
412
+ );
413
+ return undefined;
414
+ }
408
415
  console.log(`Executing ${commandToUse} in ${dir}`);
409
416
  } else {
410
417
  console.log(`Executing ${commandToUse}`);
@@ -417,7 +424,7 @@ const getCommandOutput = (cmd, dir, args) => {
417
424
  });
418
425
  const stdout = result.stdout ? result.stdout.toString() : "";
419
426
  const stderr = result.stderr ? result.stderr.toString() : "";
420
- return `${stdout} + "\n" + ${stderr}`;
427
+ return `${stdout}\n${stderr}`.trim() || undefined;
421
428
  };
422
429
 
423
430
  /**
@@ -690,6 +697,14 @@ export function isRbenvAvailable() {
690
697
  }
691
698
  }
692
699
 
700
+ /**
701
+ * Returns the rbenv binary directory for the given Ruby version.
702
+ * Respects the `RBENV_ROOT` environment variable when set; otherwise falls back
703
+ * to `~/.rbenv/versions/<rubyVersion>/bin`.
704
+ *
705
+ * @param {string} rubyVersion Ruby version string (e.g. `"3.2.2"`)
706
+ * @returns {string} Absolute path to the rbenv bin directory for that version
707
+ */
693
708
  export function rubyVersionDir(rubyVersion) {
694
709
  return process.env.RBENV_ROOT
695
710
  ? join(process.env.RBENV_ROOT, "versions", rubyVersion, "bin")