@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
package/bin/cdxgen.js CHANGED
@@ -4,7 +4,14 @@ import crypto from "node:crypto";
4
4
  import fs from "node:fs";
5
5
  import http from "node:http";
6
6
  import https from "node:https";
7
- import { basename, dirname, join, resolve } from "node:path";
7
+ import {
8
+ basename,
9
+ dirname,
10
+ isAbsolute,
11
+ join,
12
+ relative,
13
+ resolve,
14
+ } from "node:path";
8
15
  import process from "node:process";
9
16
 
10
17
  import { parse as _load } from "yaml";
@@ -25,7 +32,26 @@ import {
25
32
  printSummary,
26
33
  printTable,
27
34
  } from "../lib/helpers/display.js";
35
+ import {
36
+ createOutputPlan,
37
+ getOutputDirectory,
38
+ } from "../lib/helpers/exportUtils.js";
28
39
  import { TRACE_MODE, thoughtEnd, thoughtLog } from "../lib/helpers/logger.js";
40
+ import {
41
+ cleanupSourceDir,
42
+ findGitRefForPurlVersion,
43
+ gitClone,
44
+ isAllowedPath,
45
+ isAllowedWinPath,
46
+ maybePurlSource,
47
+ maybeRemotePath,
48
+ PURL_REGISTRY_LOOKUP_WARNING,
49
+ resolveGitUrlFromPurl,
50
+ resolvePurlSourceDirectory,
51
+ sanitizeRemoteUrlForLogs,
52
+ validateAndRejectGitSource,
53
+ validatePurlSource,
54
+ } from "../lib/helpers/source.js";
29
55
  import {
30
56
  commandsExecuted,
31
57
  DEBUG_MODE,
@@ -42,9 +68,10 @@ import {
42
68
  toCamel,
43
69
  } from "../lib/helpers/utils.js";
44
70
  import { postProcess } from "../lib/stages/postgen/postgen.js";
71
+ import { convertCycloneDxToSpdx } from "../lib/stages/postgen/spdxConverter.js";
45
72
  import { auditEnvironment } from "../lib/stages/pregen/envAudit.js";
46
73
  import { prepareEnv } from "../lib/stages/pregen/pregen.js";
47
- import { validateBom } from "../lib/validator/bomValidator.js";
74
+ import { validateBom, validateSpdx } from "../lib/validator/bomValidator.js";
48
75
 
49
76
  // Support for config files
50
77
  const configPaths = [
@@ -134,6 +161,10 @@ const args = _yargs
134
161
  description:
135
162
  "Perform deep searches for components. Useful while scanning C/C++ apps, live OS and oci images.",
136
163
  })
164
+ .option("git-branch", {
165
+ description: "Git branch to clone when the source is a git URL or purl",
166
+ type: "string",
167
+ })
137
168
  .option("server-url", {
138
169
  description: "Dependency track url. Eg: https://deptrack.cyclonedx.io",
139
170
  type: "string",
@@ -307,6 +338,25 @@ const args = _yargs
307
338
  hidden: true,
308
339
  choices: ["pre-build", "build", "post-build"],
309
340
  })
341
+ .option("include-release-notes", {
342
+ type: "boolean",
343
+ default: false,
344
+ hidden: true,
345
+ description:
346
+ "Attach CycloneDX releaseNotes to the cdxgen tool component in metadata.",
347
+ })
348
+ .option("release-notes-current-tag", {
349
+ type: "string",
350
+ hidden: true,
351
+ description:
352
+ "Current git tag used to build CycloneDX releaseNotes for cdxgen metadata.",
353
+ })
354
+ .option("release-notes-previous-tag", {
355
+ type: "string",
356
+ hidden: true,
357
+ description:
358
+ "Previous git tag used to build CycloneDX releaseNotes for cdxgen metadata.",
359
+ })
310
360
  .option("include-regex", {
311
361
  description:
312
362
  "glob pattern to include. This overrides the default pattern used during auto-detection.",
@@ -322,6 +372,10 @@ const args = _yargs
322
372
  default: false,
323
373
  description: "Serialize and export BOM as protobuf binary.",
324
374
  })
375
+ .option("format", {
376
+ description:
377
+ "Export format(s). Supports cyclonedx, spdx, repeated --format flags, or a comma-separated list such as cyclonedx,spdx.",
378
+ })
325
379
  .option("proto-bin-file", {
326
380
  description: "Path for the serialized protobuf binary.",
327
381
  default: "bom.cdx",
@@ -433,12 +487,41 @@ const args = _yargs
433
487
  default: "high",
434
488
  hidden: true,
435
489
  })
490
+ .option("bom-audit-scope", {
491
+ description:
492
+ "Predictive audit target scope. Use 'required' to scan only dependencies with scope=required (missing scope is treated as required).",
493
+ type: "string",
494
+ choices: ["all", "required"],
495
+ default: "all",
496
+ hidden: true,
497
+ })
498
+ .option("bom-audit-max-targets", {
499
+ description:
500
+ "Optional upper bound for predictive audit targets. By default cdxgen scans required dependencies first and expands to at least 50 targets.",
501
+ type: "number",
502
+ hidden: true,
503
+ })
504
+ .option("bom-audit-include-trusted", {
505
+ description:
506
+ "Include packages already marked with trusted publishing metadata in predictive BOM audit target selection.",
507
+ type: "boolean",
508
+ default: false,
509
+ hidden: true,
510
+ })
511
+ .option("bom-audit-only-trusted", {
512
+ description:
513
+ "Restrict predictive BOM audit target selection to packages marked with trusted publishing metadata.",
514
+ type: "boolean",
515
+ default: false,
516
+ hidden: true,
517
+ })
436
518
  .completion("completion", "Generate bash/zsh completion")
437
519
  .array("type")
438
520
  .array("excludeType")
439
521
  .array("filter")
440
522
  .array("only")
441
523
  .array("author")
524
+ .array("format")
442
525
  .array("standard")
443
526
  .array("feature-flags")
444
527
  .array("technique")
@@ -486,6 +569,12 @@ if (args.help) {
486
569
  _yargs.showHelp();
487
570
  process.exit(0);
488
571
  }
572
+ if (args.bomAuditIncludeTrusted && args.bomAuditOnlyTrusted) {
573
+ console.error(
574
+ "Use either --bom-audit-include-trusted or --bom-audit-only-trusted, not both.",
575
+ );
576
+ process.exit(1);
577
+ }
489
578
 
490
579
  // Native Enterprise Network Configuration (Node.js v22.21+, Bun, Deno)
491
580
  // https://nodejs.org/en/learn/http/enterprise-network-configuration
@@ -518,6 +607,8 @@ if (!process.env.NODE_USE_SYSTEM_CA) {
518
607
  }
519
608
 
520
609
  const filePath = args._[0] || process.cwd();
610
+ const sourceInputIsRemoteOrPurl =
611
+ maybeRemotePath(filePath) || maybePurlSource(filePath);
521
612
  if (!args.projectName) {
522
613
  if (filePath !== ".") {
523
614
  args.projectName = basename(filePath);
@@ -527,9 +618,8 @@ if (!args.projectName) {
527
618
  }
528
619
  thoughtLog(`Let's try to generate a CycloneDX BOM for the path '${filePath}'`);
529
620
  if (
530
- filePath.includes(" ") ||
531
- filePath.includes("\r") ||
532
- filePath.includes("\n")
621
+ !sourceInputIsRemoteOrPurl &&
622
+ (filePath.includes(" ") || filePath.includes("\r") || filePath.includes("\n"))
533
623
  ) {
534
624
  console.log(
535
625
  `'${filePath}' contains spaces. This could lead to bugs when invoking external build tools.`,
@@ -545,6 +635,10 @@ if (process.argv[1].includes("obom") && !args.type) {
545
635
  "Ok, the user wants to generate an Operations Bill-of-Materials (OBOM).",
546
636
  );
547
637
  }
638
+ if (process.argv[1].includes("spdxgen") && !args.format) {
639
+ args.format = "spdx";
640
+ thoughtLog("Ok, defaulting the export format to SPDX.");
641
+ }
548
642
 
549
643
  /**
550
644
  * Command line options
@@ -557,19 +651,23 @@ const options = Object.assign({}, args, {
557
651
  deep: args.deep || args.evidence,
558
652
  output:
559
653
  isSecureMode && args.output === "bom.json"
560
- ? resolve(join(filePath, args.output))
654
+ ? sourceInputIsRemoteOrPurl
655
+ ? resolve(args.output)
656
+ : resolve(join(filePath, args.output))
561
657
  : args.output,
562
658
  exclude: args.exclude || args.excludeRegex,
563
659
  include: args.include || args.includeRegex,
564
660
  });
565
- // Should we create the output directory?
566
- const outputDirectory = dirname(options.output);
567
- if (
568
- outputDirectory &&
569
- outputDirectory !== process.cwd() &&
570
- !safeExistsSync(outputDirectory)
571
- ) {
572
- fs.mkdirSync(outputDirectory, { recursive: true });
661
+ const outputPlan = createOutputPlan(options);
662
+ for (const outputFile of Object.values(outputPlan.outputs)) {
663
+ const outputDirectory = getOutputDirectory(outputFile);
664
+ if (
665
+ outputDirectory &&
666
+ outputDirectory !== process.cwd() &&
667
+ !safeExistsSync(outputDirectory)
668
+ ) {
669
+ fs.mkdirSync(outputDirectory, { recursive: true });
670
+ }
573
671
  }
574
672
  // Filter duplicate types. Eg: -t gradle -t gradle
575
673
  if (options.projectType && Array.isArray(options.projectType)) {
@@ -929,6 +1027,129 @@ const needsBomSigning = ({ generateKeyAndSign }) =>
929
1027
  safeExistsSync(process.env.SBOM_SIGN_PRIVATE_KEY)) ||
930
1028
  process.env.SBOM_SIGN_PRIVATE_KEY_BASE64));
931
1029
 
1030
+ const stringifyJson = (jsonPayload, jsonPretty) =>
1031
+ typeof jsonPayload === "string" || jsonPayload instanceof String
1032
+ ? jsonPayload
1033
+ : JSON.stringify(jsonPayload, null, jsonPretty ? 2 : null);
1034
+
1035
+ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1036
+ const jsonPayload = stringifyJson(bomJson, options.jsonPretty);
1037
+ fs.writeFileSync(jsonFile, jsonPayload);
1038
+ if (jsonFile.endsWith("bom.json")) {
1039
+ thoughtLog(
1040
+ `Let's save the file to "${jsonFile}". Should I suggest the '.cdx.json' file extension for better semantics?`,
1041
+ );
1042
+ } else {
1043
+ thoughtLog(`Let's save the file to "${jsonFile}".`);
1044
+ }
1045
+ if (!jsonPayload || !needsBomSigning(options)) {
1046
+ return jsonPayload;
1047
+ }
1048
+ let alg = process.env.SBOM_SIGN_ALGORITHM || "RS512";
1049
+ if (alg.includes("none")) {
1050
+ alg = "RS512";
1051
+ }
1052
+ let privateKeyToUse;
1053
+ let jwkPublicKey;
1054
+ let publicKeyFile;
1055
+ if (options.generateKeyAndSign) {
1056
+ const jdirName = dirname(jsonFile);
1057
+ publicKeyFile = join(jdirName, "public.key");
1058
+ const privateKeyFile = join(jdirName, "private.key");
1059
+ const privateKeyB64File = join(jdirName, "private.key.base64");
1060
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
1061
+ modulusLength: 4096,
1062
+ publicKeyEncoding: {
1063
+ type: "spki",
1064
+ format: "pem",
1065
+ },
1066
+ privateKeyEncoding: {
1067
+ type: "pkcs8",
1068
+ format: "pem",
1069
+ },
1070
+ });
1071
+ fs.writeFileSync(publicKeyFile, publicKey);
1072
+ fs.writeFileSync(privateKeyFile, privateKey);
1073
+ fs.writeFileSync(
1074
+ privateKeyB64File,
1075
+ Buffer.from(privateKey, "utf8").toString("base64"),
1076
+ );
1077
+ console.log(
1078
+ "Created public/private key pairs for testing purposes",
1079
+ publicKeyFile,
1080
+ privateKeyFile,
1081
+ privateKeyB64File,
1082
+ );
1083
+ privateKeyToUse = privateKey;
1084
+ jwkPublicKey = crypto.createPublicKey(publicKey).export({ format: "jwk" });
1085
+ } else {
1086
+ if (process.env?.SBOM_SIGN_PRIVATE_KEY) {
1087
+ privateKeyToUse = fs.readFileSync(
1088
+ process.env.SBOM_SIGN_PRIVATE_KEY,
1089
+ "utf8",
1090
+ );
1091
+ } else if (process.env?.SBOM_SIGN_PRIVATE_KEY_BASE64) {
1092
+ privateKeyToUse = Buffer.from(
1093
+ process.env.SBOM_SIGN_PRIVATE_KEY_BASE64,
1094
+ "base64",
1095
+ ).toString("utf8");
1096
+ }
1097
+ if (
1098
+ process.env.SBOM_SIGN_PUBLIC_KEY &&
1099
+ safeExistsSync(process.env.SBOM_SIGN_PUBLIC_KEY)
1100
+ ) {
1101
+ jwkPublicKey = crypto
1102
+ .createPublicKey(
1103
+ fs.readFileSync(process.env.SBOM_SIGN_PUBLIC_KEY, "utf8"),
1104
+ )
1105
+ .export({ format: "jwk" });
1106
+ } else if (process.env?.SBOM_SIGN_PUBLIC_KEY_BASE64) {
1107
+ jwkPublicKey = Buffer.from(
1108
+ process.env.SBOM_SIGN_PUBLIC_KEY_BASE64,
1109
+ "base64",
1110
+ ).toString("utf8");
1111
+ }
1112
+ }
1113
+ try {
1114
+ const bomJsonUnsignedObj = JSON.parse(jsonPayload);
1115
+ const signOptions = {
1116
+ privateKey: privateKeyToUse,
1117
+ algorithm: alg,
1118
+ publicKeyJwk: jwkPublicKey,
1119
+ mode: process.env.SBOM_SIGN_MODE || "replace",
1120
+ signComponents: true,
1121
+ signServices: true,
1122
+ signAnnotations: true,
1123
+ };
1124
+ thoughtLog(`Signing the BOM file "${jsonFile}".`);
1125
+ const signedBom = signBom(bomJsonUnsignedObj, signOptions);
1126
+ fs.writeFileSync(
1127
+ jsonFile,
1128
+ JSON.stringify(signedBom, null, options.jsonPretty ? 2 : null),
1129
+ );
1130
+ if (publicKeyFile) {
1131
+ const publicKeyStr = fs.readFileSync(publicKeyFile, "utf8");
1132
+ const signatureVerification = verifyBom(signedBom, publicKeyStr);
1133
+ if (signatureVerification) {
1134
+ console.log(
1135
+ "SBOM signature is verifiable natively with the public key and the algorithm",
1136
+ publicKeyFile,
1137
+ alg,
1138
+ );
1139
+ } else {
1140
+ console.log("SBOM signature verification was unsuccessful");
1141
+ console.log("Check if the public key was exported in PEM format");
1142
+ }
1143
+ }
1144
+ } catch (ex) {
1145
+ console.log("SBOM signing was unsuccessful:", ex.message);
1146
+ console.log(
1147
+ "Check if the private key was exported in PEM format and the algorithm is JSF-compliant.",
1148
+ );
1149
+ }
1150
+ return jsonPayload;
1151
+ };
1152
+
932
1153
  /**
933
1154
  * Method to start the bom creation process
934
1155
  */
@@ -954,24 +1175,130 @@ const needsBomSigning = ({ generateKeyAndSign }) =>
954
1175
  const serverModule = await import("../lib/server/server.js");
955
1176
  return serverModule.start(options);
956
1177
  }
957
- // Check if cdxgen has the required permissions
958
- if (!checkPermissions(filePath, options)) {
1178
+ let sourcePath = filePath;
1179
+ let purlResolution;
1180
+ if (maybePurlSource(sourcePath)) {
1181
+ const purlValidationError = validatePurlSource(sourcePath);
1182
+ if (purlValidationError) {
1183
+ console.error(purlValidationError.error, purlValidationError.details);
1184
+ process.exit(1);
1185
+ }
1186
+ purlResolution = await resolveGitUrlFromPurl(sourcePath);
1187
+ if (!purlResolution?.repoUrl) {
1188
+ console.error(
1189
+ "Unable to resolve the provided package URL to a repository URL.",
1190
+ );
1191
+ process.exit(1);
1192
+ }
1193
+ console.warn(
1194
+ `${PURL_REGISTRY_LOOKUP_WARNING} Registry: ${purlResolution.registry}, purl type: ${purlResolution.type}, resolved URL: ${sanitizeRemoteUrlForLogs(purlResolution.repoUrl)}`,
1195
+ );
1196
+ sourcePath = purlResolution.repoUrl;
1197
+ }
1198
+ if (
1199
+ maybeRemotePath(sourcePath) &&
1200
+ isSecureMode &&
1201
+ !process.env.CDXGEN_GIT_ALLOWED_HOSTS &&
1202
+ !process.env.CDXGEN_SERVER_ALLOWED_HOSTS
1203
+ ) {
1204
+ console.error(
1205
+ "SECURE MODE: Configure CDXGEN_GIT_ALLOWED_HOSTS (or CDXGEN_SERVER_ALLOWED_HOSTS) before using git URL or purl sources.",
1206
+ );
1207
+ process.exit(1);
1208
+ }
1209
+ if (!maybeRemotePath(sourcePath) && !isAllowedPath(resolve(sourcePath))) {
1210
+ console.error(
1211
+ "Path is not allowed as per CDXGEN_ALLOWED_PATHS/CDXGEN_SERVER_ALLOWED_PATHS.",
1212
+ );
1213
+ process.exit(1);
1214
+ }
1215
+ if (!maybeRemotePath(sourcePath) && !isAllowedWinPath(resolve(sourcePath))) {
1216
+ console.error("Path is not allowed on this platform.");
1217
+ process.exit(1);
1218
+ }
1219
+ if (maybeRemotePath(sourcePath)) {
1220
+ const validationError = validateAndRejectGitSource(sourcePath);
1221
+ if (validationError) {
1222
+ console.error(validationError.error, validationError.details);
1223
+ process.exit(1);
1224
+ }
1225
+ }
1226
+ const checkPath = maybeRemotePath(sourcePath) ? getTmpDir() : sourcePath;
1227
+ if (maybeRemotePath(sourcePath)) {
1228
+ options.releaseNotesGitUrl = sourcePath;
1229
+ }
1230
+ if (!checkPermissions(checkPath, options)) {
959
1231
  if (isSecureMode) {
960
1232
  process.exit(1);
961
1233
  }
962
1234
  return;
963
1235
  }
964
- prepareEnv(filePath, options);
1236
+ let srcDir = sourcePath;
1237
+ let cleanup = false;
1238
+ let gitRef = options.gitBranch;
1239
+ if (maybeRemotePath(sourcePath)) {
1240
+ if (!gitRef && purlResolution?.version) {
1241
+ gitRef = findGitRefForPurlVersion(sourcePath, purlResolution);
1242
+ if (!gitRef) {
1243
+ console.warn(
1244
+ `Unable to find a matching git tag for version '${purlResolution.version}'. Falling back to repository default branch.`,
1245
+ );
1246
+ }
1247
+ }
1248
+ srcDir = gitClone(sourcePath, gitRef);
1249
+ if (purlResolution?.type === "npm") {
1250
+ const cloneRootDir = srcDir;
1251
+ const purlSourceDir = resolvePurlSourceDirectory(srcDir, purlResolution);
1252
+ if (purlSourceDir) {
1253
+ if (purlSourceDir !== cloneRootDir) {
1254
+ const relativeDir = relative(cloneRootDir, purlSourceDir);
1255
+ if (relativeDir.startsWith("..") || isAbsolute(relativeDir)) {
1256
+ console.warn(
1257
+ `Ignoring detected npm package directory outside clone root: ${purlSourceDir}`,
1258
+ );
1259
+ } else {
1260
+ console.warn(
1261
+ `Using npm package directory '${purlSourceDir}' for purl '${purlResolution.namespace ? `${purlResolution.namespace}/` : ""}${purlResolution.name}'.`,
1262
+ );
1263
+ srcDir = purlSourceDir;
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ cleanup = true;
1269
+ }
1270
+ prepareEnv(srcDir, options);
965
1271
  thoughtLog("Getting ready to generate the BOM ⚡️.");
966
- let bomNSData = (await createBom(filePath, options)) || {};
1272
+ const originalFetchPackageMetadata = process.env.CDXGEN_FETCH_PKG_METADATA;
1273
+ if (options.bomAudit) {
1274
+ process.env.CDXGEN_FETCH_PKG_METADATA = "true";
1275
+ }
1276
+ let bomNSData;
1277
+ try {
1278
+ bomNSData = (await createBom(srcDir, options)) || {};
1279
+ } finally {
1280
+ if (originalFetchPackageMetadata === undefined) {
1281
+ delete process.env.CDXGEN_FETCH_PKG_METADATA;
1282
+ } else {
1283
+ process.env.CDXGEN_FETCH_PKG_METADATA = originalFetchPackageMetadata;
1284
+ }
1285
+ }
967
1286
  if (bomNSData?.bomJson) {
968
1287
  thoughtLog(
969
1288
  "Tweaking the generated BOM data with useful annotations and properties.",
970
1289
  );
971
1290
  }
972
1291
  // Add extra metadata and annotations with post processing
973
- bomNSData = postProcess(bomNSData, options, filePath);
1292
+ bomNSData = postProcess(bomNSData, options, srcDir);
974
1293
  if (options.bomAudit && bomNSData?.bomJson) {
1294
+ const { finalizeAuditReport, runAuditFromBoms } = await import(
1295
+ "../lib/audit/index.js"
1296
+ );
1297
+ const { createProgressTracker } = await import("../lib/audit/progress.js");
1298
+ const { collectAuditTargets } = await import("../lib/audit/targets.js");
1299
+ const { formatPredictiveAnnotations, renderConsoleReport } = await import(
1300
+ "../lib/audit/reporters.js"
1301
+ );
975
1302
  const {
976
1303
  auditBom,
977
1304
  formatAnnotations,
@@ -999,160 +1326,125 @@ const needsBomSigning = ({ generateKeyAndSign }) =>
999
1326
  console.error(
1000
1327
  "Review findings above or adjust --bom-audit-fail-severity to proceed.",
1001
1328
  );
1329
+ if (cleanup) {
1330
+ cleanupSourceDir(srcDir);
1331
+ }
1002
1332
  process.exit(1);
1003
1333
  }
1004
- }
1005
- if (
1006
- options.output &&
1007
- (typeof options.output === "string" || options.output instanceof String)
1008
- ) {
1009
- const jsonFile = options.output;
1010
- // Create bom json file
1011
- if (bomNSData.bomJson) {
1012
- let jsonPayload;
1013
- if (
1014
- typeof bomNSData.bomJson === "string" ||
1015
- bomNSData.bomJson instanceof String
1016
- ) {
1017
- fs.writeFileSync(jsonFile, bomNSData.bomJson);
1018
- jsonPayload = bomNSData.bomJson;
1019
- } else {
1020
- jsonPayload = JSON.stringify(
1021
- bomNSData.bomJson,
1022
- null,
1023
- options.jsonPretty ? 2 : null,
1024
- );
1025
- fs.writeFileSync(jsonFile, jsonPayload);
1026
- if (jsonFile.endsWith("bom.json")) {
1027
- thoughtLog(
1028
- `Let's save the file to "${jsonFile}". Should I suggest the '.cdx.json' file extension for better semantics?`,
1029
- );
1030
- } else {
1031
- thoughtLog(`Let's save the file to "${jsonFile}".`);
1032
- }
1033
- }
1034
- if (jsonPayload && needsBomSigning(options)) {
1035
- let alg = process.env.SBOM_SIGN_ALGORITHM || "RS512";
1036
- if (alg.includes("none")) {
1037
- alg = "RS512";
1038
- }
1039
- let privateKeyToUse;
1040
- let jwkPublicKey;
1041
- let publicKeyFile;
1042
- if (options.generateKeyAndSign) {
1043
- const jdirName = dirname(jsonFile);
1044
- publicKeyFile = join(jdirName, "public.key");
1045
- const privateKeyFile = join(jdirName, "private.key");
1046
- const privateKeyB64File = join(jdirName, "private.key.base64");
1047
- const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
1048
- modulusLength: 4096,
1049
- publicKeyEncoding: {
1050
- type: "spki",
1051
- format: "pem",
1052
- },
1053
- privateKeyEncoding: {
1054
- type: "pkcs8",
1055
- format: "pem",
1056
- },
1057
- });
1058
- fs.writeFileSync(publicKeyFile, publicKey);
1059
- fs.writeFileSync(privateKeyFile, privateKey);
1060
- fs.writeFileSync(
1061
- privateKeyB64File,
1062
- Buffer.from(privateKey, "utf8").toString("base64"),
1063
- );
1064
- console.log(
1065
- "Created public/private key pairs for testing purposes",
1066
- publicKeyFile,
1067
- privateKeyFile,
1068
- privateKeyB64File,
1069
- );
1070
- privateKeyToUse = privateKey;
1071
- jwkPublicKey = crypto
1072
- .createPublicKey(publicKey)
1073
- .export({ format: "jwk" });
1074
- } else {
1075
- if (process.env?.SBOM_SIGN_PRIVATE_KEY) {
1076
- privateKeyToUse = fs.readFileSync(
1077
- process.env.SBOM_SIGN_PRIVATE_KEY,
1078
- "utf8",
1079
- );
1080
- } else if (process.env?.SBOM_SIGN_PRIVATE_KEY_BASE64) {
1081
- privateKeyToUse = Buffer.from(
1082
- process.env.SBOM_SIGN_PRIVATE_KEY_BASE64,
1083
- "base64",
1084
- ).toString("utf8");
1085
- }
1086
- if (
1087
- process.env.SBOM_SIGN_PUBLIC_KEY &&
1088
- safeExistsSync(process.env.SBOM_SIGN_PUBLIC_KEY)
1089
- ) {
1090
- jwkPublicKey = crypto
1091
- .createPublicKey(
1092
- fs.readFileSync(process.env.SBOM_SIGN_PUBLIC_KEY, "utf8"),
1093
- )
1094
- .export({ format: "jwk" });
1095
- } else if (process.env?.SBOM_SIGN_PUBLIC_KEY_BASE64) {
1096
- jwkPublicKey = Buffer.from(
1097
- process.env.SBOM_SIGN_PUBLIC_KEY_BASE64,
1098
- "base64",
1099
- ).toString("utf8");
1100
- }
1101
- }
1102
- try {
1103
- const bomJsonUnsignedObj = JSON.parse(jsonPayload);
1104
- const signOptions = {
1105
- privateKey: privateKeyToUse,
1106
- algorithm: alg,
1107
- publicKeyJwk: jwkPublicKey,
1108
- mode: process.env.SBOM_SIGN_MODE || "replace",
1109
- signComponents: true,
1110
- signServices: true,
1111
- signAnnotations: true,
1112
- };
1113
- thoughtLog(`Signing the BOM file "${jsonFile}".`);
1114
- const signedBom = signBom(bomJsonUnsignedObj, signOptions);
1115
- fs.writeFileSync(
1116
- jsonFile,
1117
- JSON.stringify(signedBom, null, options.jsonPretty ? 2 : null),
1118
- );
1119
- if (publicKeyFile) {
1120
- const publicKeyStr = fs.readFileSync(publicKeyFile, "utf8");
1121
- const signatureVerification = verifyBom(signedBom, publicKeyStr);
1122
- if (signatureVerification) {
1123
- console.log(
1124
- "SBOM signature is verifiable natively with the public key and the algorithm",
1125
- publicKeyFile,
1126
- alg,
1127
- );
1128
- } else {
1129
- console.log("SBOM signature verification was unsuccessful");
1130
- console.log("Check if the public key was exported in PEM format");
1131
- }
1132
- }
1133
- } catch (ex) {
1134
- console.log("SBOM signing was unsuccessful:", ex.message);
1135
- console.log(
1136
- "Check if the private key was exported in PEM format and the algorithm is JSF-compliant.",
1137
- );
1138
- }
1139
- }
1334
+
1335
+ thoughtLog("Let's run predictive dependency audit...");
1336
+ const progressTracker = createProgressTracker();
1337
+ const predictiveAuditScope =
1338
+ options.bomAuditScope === "required" ? "required" : undefined;
1339
+ const predictiveAuditTrusted = options.bomAuditOnlyTrusted
1340
+ ? "only"
1341
+ : options.bomAuditIncludeTrusted
1342
+ ? "include"
1343
+ : undefined;
1344
+ const requiredAuditTargetCount = collectAuditTargets(
1345
+ [
1346
+ {
1347
+ bomJson: bomNSData.bomJson,
1348
+ source: filePath,
1349
+ },
1350
+ ],
1351
+ {
1352
+ scope: "required",
1353
+ trusted: predictiveAuditTrusted,
1354
+ },
1355
+ ).targets.length;
1356
+ const predictiveAuditMaxTargets =
1357
+ typeof options.bomAuditMaxTargets === "number" &&
1358
+ options.bomAuditMaxTargets > 0
1359
+ ? options.bomAuditMaxTargets
1360
+ : predictiveAuditScope === "required"
1361
+ ? undefined
1362
+ : Math.max(50, requiredAuditTargetCount);
1363
+ let predictiveReport;
1364
+ try {
1365
+ predictiveReport = await runAuditFromBoms(
1366
+ [
1367
+ {
1368
+ bomJson: bomNSData.bomJson,
1369
+ source: filePath,
1370
+ },
1371
+ ],
1372
+ {
1373
+ categories: options.bomAuditCategories
1374
+ ? options.bomAuditCategories
1375
+ .split(",")
1376
+ .map((category) => category.trim())
1377
+ .filter(Boolean)
1378
+ : undefined,
1379
+ failSeverity: options.bomAuditFailSeverity,
1380
+ maxTargets: predictiveAuditMaxTargets,
1381
+ minSeverity: options.bomAuditMinSeverity,
1382
+ onProgress: progressTracker.onProgress,
1383
+ scope: predictiveAuditScope,
1384
+ trusted: predictiveAuditTrusted,
1385
+ trustedSelectionHelp:
1386
+ "Use --bom-audit-include-trusted to include them or --bom-audit-only-trusted to audit just those packages.",
1387
+ },
1388
+ );
1389
+ } finally {
1390
+ progressTracker.stop();
1140
1391
  }
1141
- // bom ns mapping
1142
- if (bomNSData.nsMapping && Object.keys(bomNSData.nsMapping).length) {
1143
- const nsFile = `${jsonFile}.map`;
1144
- fs.writeFileSync(nsFile, JSON.stringify(bomNSData.nsMapping));
1392
+ if (predictiveReport.summary.totalTargets > 0) {
1393
+ process.stderr.write(
1394
+ renderConsoleReport(predictiveReport, {
1395
+ minSeverity: options.bomAuditMinSeverity,
1396
+ }),
1397
+ );
1398
+ } else if (DEBUG_MODE) {
1399
+ console.log("Predictive BOM audit: No supported npm/PyPI targets found");
1145
1400
  }
1146
- } else if (!options.print) {
1147
- if (bomNSData.bomJson) {
1148
- console.log(
1149
- JSON.stringify(bomNSData.bomJson, null, options.jsonPretty ? 2 : null),
1401
+ const predictiveAnnotations = formatPredictiveAnnotations(
1402
+ predictiveReport,
1403
+ bomNSData.bomJson,
1404
+ {
1405
+ minSeverity: options.bomAuditMinSeverity,
1406
+ },
1407
+ );
1408
+ if (predictiveAnnotations.length && options.specVersion >= 1.4) {
1409
+ bomNSData.bomJson.annotations = [
1410
+ ...(bomNSData.bomJson.annotations || []),
1411
+ ...predictiveAnnotations,
1412
+ ];
1413
+ thoughtLog(
1414
+ `Embedded ${predictiveAnnotations.length} predictive audit annotations`,
1150
1415
  );
1151
- } else {
1152
- console.log("Unable to produce BOM for", filePath);
1153
- console.log("Try running the command with -t <type> or -r argument");
1416
+ }
1417
+ const predictiveResult = finalizeAuditReport(predictiveReport, {
1418
+ failSeverity: options.bomAuditFailSeverity,
1419
+ minSeverity: options.bomAuditMinSeverity,
1420
+ report: "console",
1421
+ });
1422
+ if (isSecureMode && predictiveResult.exitCode === 3) {
1423
+ console.error(
1424
+ "\nSecure mode: Predictive audit findings exceeded the configured threshold.",
1425
+ );
1426
+ console.error(
1427
+ "Review findings above or adjust --bom-audit-fail-severity to proceed.",
1428
+ );
1429
+ if (cleanup) {
1430
+ cleanupSourceDir(srcDir);
1431
+ }
1432
+ process.exit(1);
1154
1433
  }
1155
1434
  }
1435
+ let internalCycloneDxInputPath = outputPlan.outputs.cyclonedx;
1436
+ if ((options.evidence || options.includeCrypto) && bomNSData?.bomJson) {
1437
+ if (!internalCycloneDxInputPath) {
1438
+ internalCycloneDxInputPath = join(
1439
+ getTmpDir(),
1440
+ `cdxgen-${Date.now()}-${basename(filePath)}.cdx.json`,
1441
+ );
1442
+ }
1443
+ fs.writeFileSync(
1444
+ internalCycloneDxInputPath,
1445
+ stringifyJson(bomNSData.bomJson, options.jsonPretty),
1446
+ );
1447
+ }
1156
1448
  // Evidence generation
1157
1449
  if (options.evidence || options.includeCrypto) {
1158
1450
  // Set the evinse output file to be the same as output file
@@ -1163,7 +1455,7 @@ const needsBomSigning = ({ generateKeyAndSign }) =>
1163
1455
  options.projectType = options.projectType || ["java"];
1164
1456
  const evinseOptions = {
1165
1457
  _: args._,
1166
- input: options.output,
1458
+ input: internalCycloneDxInputPath || options.output,
1167
1459
  output: options.evinseOutput,
1168
1460
  language: options.projectType,
1169
1461
  skipMavenCollector: false,
@@ -1202,10 +1494,58 @@ const needsBomSigning = ({ generateKeyAndSign }) =>
1202
1494
  if (options.validate && bomNSData?.bomJson) {
1203
1495
  thoughtLog("Wait, let's check the generated BOM file for any issues.");
1204
1496
  if (!validateBom(bomNSData.bomJson)) {
1497
+ if (cleanup) {
1498
+ cleanupSourceDir(srcDir);
1499
+ }
1205
1500
  process.exit(1);
1206
1501
  }
1207
1502
  thoughtLog("✅ BOM file looks valid.");
1208
1503
  }
1504
+ if (
1505
+ outputPlan.formats.has("spdx") &&
1506
+ bomNSData?.bomJson &&
1507
+ bomNSData?.bomJson?.bomFormat === "CycloneDX"
1508
+ ) {
1509
+ thoughtLog(
1510
+ "Preparing the SPDX 3.0.1 export from the validated CycloneDX BOM.",
1511
+ );
1512
+ bomNSData.spdxJson = convertCycloneDxToSpdx(bomNSData.bomJson, options);
1513
+ if (options.validate && !validateSpdx(bomNSData.spdxJson)) {
1514
+ process.exit(1);
1515
+ }
1516
+ }
1517
+ if (
1518
+ options.output &&
1519
+ (typeof options.output === "string" || options.output instanceof String)
1520
+ ) {
1521
+ if (outputPlan.outputs.cyclonedx && bomNSData.bomJson) {
1522
+ writeCycloneDxOutput(
1523
+ outputPlan.outputs.cyclonedx,
1524
+ bomNSData.bomJson,
1525
+ options,
1526
+ );
1527
+ if (bomNSData.nsMapping && Object.keys(bomNSData.nsMapping).length) {
1528
+ const nsFile = `${outputPlan.outputs.cyclonedx}.map`;
1529
+ fs.writeFileSync(nsFile, JSON.stringify(bomNSData.nsMapping));
1530
+ }
1531
+ }
1532
+ if (outputPlan.outputs.spdx && bomNSData.spdxJson) {
1533
+ fs.writeFileSync(
1534
+ outputPlan.outputs.spdx,
1535
+ stringifyJson(bomNSData.spdxJson, options.jsonPretty),
1536
+ );
1537
+ thoughtLog(`Let's save the SPDX file to "${outputPlan.outputs.spdx}".`);
1538
+ }
1539
+ } else if (!options.print) {
1540
+ if (outputPlan.formats.has("spdx") && bomNSData?.spdxJson) {
1541
+ console.log(stringifyJson(bomNSData.spdxJson, options.jsonPretty));
1542
+ } else if (bomNSData.bomJson) {
1543
+ console.log(stringifyJson(bomNSData.bomJson, options.jsonPretty));
1544
+ } else {
1545
+ console.log("Unable to produce BOM for", filePath);
1546
+ console.log("Try running the command with -t <type> or -r argument");
1547
+ }
1548
+ }
1209
1549
  thoughtEnd();
1210
1550
  // Automatically submit the bom data
1211
1551
  // biome-ignore lint/suspicious/noDoubleEquals: yargs passes true for empty values
@@ -1214,6 +1554,9 @@ const needsBomSigning = ({ generateKeyAndSign }) =>
1214
1554
  await submitBom(options, bomNSData.bomJson);
1215
1555
  } catch (err) {
1216
1556
  console.log(err);
1557
+ if (cleanup) {
1558
+ cleanupSourceDir(srcDir);
1559
+ }
1217
1560
  process.exit(1);
1218
1561
  }
1219
1562
  }
@@ -1256,4 +1599,7 @@ const needsBomSigning = ({ generateKeyAndSign }) =>
1256
1599
  console.log(allowListSuggestion);
1257
1600
  }
1258
1601
  }
1602
+ if (cleanup) {
1603
+ cleanupSourceDir(srcDir);
1604
+ }
1259
1605
  })();