@cyclonedx/cdxgen 12.3.0 → 12.3.2

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 (121) hide show
  1. package/README.md +15 -5
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +241 -81
  4. package/bin/repl.js +138 -0
  5. package/data/rules/ai-agent-governance.yaml +249 -0
  6. package/data/rules/dependency-sources.yaml +41 -0
  7. package/data/rules/mcp-servers.yaml +304 -0
  8. package/data/rules/package-integrity.yaml +123 -0
  9. package/lib/audit/index.js +353 -29
  10. package/lib/audit/index.poku.js +247 -7
  11. package/lib/audit/reporters.js +26 -0
  12. package/lib/audit/scoring.js +262 -13
  13. package/lib/audit/scoring.poku.js +179 -0
  14. package/lib/audit/targets.js +391 -2
  15. package/lib/audit/targets.poku.js +416 -3
  16. package/lib/cli/index.js +588 -45
  17. package/lib/cli/index.poku.js +735 -1
  18. package/lib/evinser/evinser.js +8 -5
  19. package/lib/helpers/agentFormulationParser.js +318 -0
  20. package/lib/helpers/aiInventory.js +262 -0
  21. package/lib/helpers/aiInventory.poku.js +111 -0
  22. package/lib/helpers/analyzer.js +1769 -0
  23. package/lib/helpers/analyzer.poku.js +284 -3
  24. package/lib/helpers/auditCategories.js +76 -0
  25. package/lib/helpers/ciParsers/githubActions.js +140 -16
  26. package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
  27. package/lib/helpers/communityAiConfigParser.js +672 -0
  28. package/lib/helpers/communityAiConfigParser.poku.js +63 -0
  29. package/lib/helpers/depsUtils.js +108 -0
  30. package/lib/helpers/depsUtils.poku.js +72 -1
  31. package/lib/helpers/display.js +325 -3
  32. package/lib/helpers/display.poku.js +301 -0
  33. package/lib/helpers/formulationParsers.js +28 -0
  34. package/lib/helpers/formulationParsers.poku.js +504 -1
  35. package/lib/helpers/jsonLike.js +102 -0
  36. package/lib/helpers/jsonLike.poku.js +34 -0
  37. package/lib/helpers/mcp.js +248 -0
  38. package/lib/helpers/mcp.poku.js +101 -0
  39. package/lib/helpers/mcpConfigParser.js +656 -0
  40. package/lib/helpers/mcpConfigParser.poku.js +126 -0
  41. package/lib/helpers/mcpDiscovery.js +84 -0
  42. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  43. package/lib/helpers/protobom.js +3 -3
  44. package/lib/helpers/provenanceUtils.js +29 -4
  45. package/lib/helpers/provenanceUtils.poku.js +29 -3
  46. package/lib/helpers/registryProvenance.js +210 -0
  47. package/lib/helpers/registryProvenance.poku.js +144 -0
  48. package/lib/helpers/rustFormulationParser.js +330 -0
  49. package/lib/helpers/source.js +21 -2
  50. package/lib/helpers/source.poku.js +38 -0
  51. package/lib/helpers/utils.js +1331 -83
  52. package/lib/helpers/utils.poku.js +599 -188
  53. package/lib/helpers/vsixutils.js +12 -4
  54. package/lib/helpers/vsixutils.poku.js +34 -0
  55. package/lib/managers/binary.js +36 -12
  56. package/lib/managers/binary.poku.js +68 -0
  57. package/lib/managers/docker.js +59 -9
  58. package/lib/managers/docker.poku.js +61 -0
  59. package/lib/managers/piptree.js +12 -7
  60. package/lib/managers/piptree.poku.js +44 -0
  61. package/lib/stages/postgen/annotator.js +2 -1
  62. package/lib/stages/postgen/annotator.poku.js +15 -0
  63. package/lib/stages/postgen/auditBom.js +20 -6
  64. package/lib/stages/postgen/auditBom.poku.js +694 -1
  65. package/lib/stages/postgen/postgen.js +262 -11
  66. package/lib/stages/postgen/postgen.poku.js +306 -2
  67. package/lib/stages/postgen/ruleEngine.js +49 -1
  68. package/lib/stages/postgen/spdxConverter.poku.js +70 -0
  69. package/lib/stages/pregen/pregen.js +6 -4
  70. package/package.json +1 -1
  71. package/types/bin/repl.d.ts.map +1 -1
  72. package/types/lib/audit/index.d.ts.map +1 -1
  73. package/types/lib/audit/reporters.d.ts.map +1 -1
  74. package/types/lib/audit/scoring.d.ts.map +1 -1
  75. package/types/lib/audit/targets.d.ts +12 -0
  76. package/types/lib/audit/targets.d.ts.map +1 -1
  77. package/types/lib/cli/index.d.ts +2 -8
  78. package/types/lib/cli/index.d.ts.map +1 -1
  79. package/types/lib/evinser/evinser.d.ts.map +1 -1
  80. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  81. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  82. package/types/lib/helpers/aiInventory.d.ts +23 -0
  83. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  84. package/types/lib/helpers/analyzer.d.ts +10 -0
  85. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  86. package/types/lib/helpers/auditCategories.d.ts +12 -0
  87. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  88. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  89. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  90. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  91. package/types/lib/helpers/depsUtils.d.ts +8 -0
  92. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  93. package/types/lib/helpers/display.d.ts +17 -1
  94. package/types/lib/helpers/display.d.ts.map +1 -1
  95. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  96. package/types/lib/helpers/jsonLike.d.ts +4 -0
  97. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  98. package/types/lib/helpers/mcp.d.ts +29 -0
  99. package/types/lib/helpers/mcp.d.ts.map +1 -0
  100. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  101. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  102. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  103. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  104. package/types/lib/helpers/provenanceUtils.d.ts +5 -3
  105. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
  106. package/types/lib/helpers/registryProvenance.d.ts +9 -0
  107. package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
  108. package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
  109. package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
  110. package/types/lib/helpers/source.d.ts.map +1 -1
  111. package/types/lib/helpers/utils.d.ts +31 -1
  112. package/types/lib/helpers/utils.d.ts.map +1 -1
  113. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  114. package/types/lib/managers/binary.d.ts.map +1 -1
  115. package/types/lib/managers/docker.d.ts.map +1 -1
  116. package/types/lib/managers/piptree.d.ts.map +1 -1
  117. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  118. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  119. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  120. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  121. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
package/README.md CHANGED
@@ -69,7 +69,17 @@ Most SBOM tools are like simple barcode scanners. For easy applications, they ca
69
69
  - _Precision:_ Try using multiple techniques to improve precision, even if it takes extra time.
70
70
  - _Personas:_ Cater to the needs of a range of personas such as security researchers, compliance auditors, developers, and SOC.
71
71
  - _Machine Learning:_ Optimize the generated data for Machine Learning (ML) purposes by considering the various model properties.
72
- - _Safety:_ Execute external build tools and handle untrusted inputs defensively, with hardened defaults and a [secure mode](docs/PERMISSIONS.md) for sensitive environments.
72
+ - _Safety:_ Execute external build tools and handle untrusted inputs defensively, with hardened defaults, a [secure mode](docs/PERMISSIONS.md) for sensitive environments, and a read-only `--dry-run` mode for review-first workflows.
73
+
74
+ ### Review-first dry runs
75
+
76
+ When you want to inspect what cdxgen would do before allowing side effects, use `--dry-run`.
77
+
78
+ ```shell
79
+ cdxgen --dry-run -p -t js .
80
+ ```
81
+
82
+ Dry-run mode keeps cdxgen read-only: it reads local files, blocks writes/exec/temp creation/cloning/submission, and prints an activity summary table for both beginners and power users.
73
83
 
74
84
  ## Documentation
75
85
 
@@ -170,7 +180,7 @@ Common asset names:
170
180
  #### Linux
171
181
 
172
182
  ```bash
173
- VERSION="v12.3.0"
183
+ VERSION="v12.3.1"
174
184
  ASSET="cdx-audit-linux-amd64"
175
185
  BASE_URL="https://github.com/cdxgen/cdxgen/releases/download/${VERSION}"
176
186
 
@@ -184,7 +194,7 @@ chmod +x "${ASSET}"
184
194
  #### macOS
185
195
 
186
196
  ```bash
187
- VERSION="v12.3.0"
197
+ VERSION="v12.3.1"
188
198
  ASSET="cdx-audit-darwin-arm64"
189
199
  BASE_URL="https://github.com/cdxgen/cdxgen/releases/download/${VERSION}"
190
200
 
@@ -198,7 +208,7 @@ chmod +x "${ASSET}"
198
208
  #### Windows (PowerShell)
199
209
 
200
210
  ```powershell
201
- $Version = "v12.3.0"
211
+ $Version = "v12.3.1"
202
212
  $Asset = "cdx-audit-windows-amd64.exe"
203
213
  $BaseUrl = "https://github.com/cdxgen/cdxgen/releases/download/$Version"
204
214
 
@@ -223,7 +233,7 @@ steps:
223
233
  env:
224
234
  GH_TOKEN: ${{ github.token }}
225
235
  run: |
226
- gh release download v12.3.0 \
236
+ gh release download v12.3.1 \
227
237
  --repo cdxgen/cdxgen \
228
238
  --pattern 'cdx-audit-linux-amd64' \
229
239
  --pattern 'cdx-audit-linux-amd64.sha256'
package/bin/audit.js CHANGED
@@ -92,6 +92,12 @@ const args = yargs(hideBin(process.argv))
92
92
  "Restrict predictive audit target selection to packages marked with trusted publishing metadata.",
93
93
  type: "boolean",
94
94
  })
95
+ .option("prioritize-direct-runtime", {
96
+ default: true,
97
+ description:
98
+ "Prioritize direct runtime dependencies ahead of optional, development-only, or platform-specific transitive packages during target selection.",
99
+ type: "boolean",
100
+ })
95
101
  .check((argv) => {
96
102
  if (!argv.bom && !argv.bomDir) {
97
103
  throw new Error("Specify --bom or --bom-dir.");
@@ -163,6 +169,7 @@ function writeOrPrint(output, outputPath) {
163
169
  maxTargets: args.maxTargets,
164
170
  minSeverity: args.minSeverity,
165
171
  onProgress: progressTracker.onProgress,
172
+ prioritizeDirectRuntime: args.prioritizeDirectRuntime,
166
173
  report: args.report,
167
174
  reportsDir: args.reportsDir,
168
175
  scope: args.scope === "required" ? "required" : undefined,
package/bin/cdxgen.js CHANGED
@@ -22,8 +22,10 @@ import { createBom, submitBom } from "../lib/cli/index.js";
22
22
  import { signBom, verifyBom } from "../lib/helpers/bomSigner.js";
23
23
  import {
24
24
  displaySelfThreatModel,
25
+ printActivitySummary,
25
26
  printCallStack,
26
27
  printDependencyTree,
28
+ printEnvironmentAuditFindings,
27
29
  printFormulation,
28
30
  printOccurrences,
29
31
  printReachables,
@@ -58,13 +60,19 @@ import {
58
60
  getTmpDir,
59
61
  isBun,
60
62
  isDeno,
63
+ isDryRun,
61
64
  isMac,
62
65
  isNode,
63
66
  isSecureMode,
64
67
  isWin,
68
+ recordActivity,
65
69
  remoteHostsAccessed,
66
70
  retrieveCdxgenVersion,
67
71
  safeExistsSync,
72
+ safeMkdirSync,
73
+ safeWriteSync,
74
+ setActivityContext,
75
+ setDryRunMode,
68
76
  toCamel,
69
77
  } from "../lib/helpers/utils.js";
70
78
  import { postProcess } from "../lib/stages/postgen/postgen.js";
@@ -230,6 +238,18 @@ const args = _yargs
230
238
  default: isSecureMode,
231
239
  description: "Fail if any dependency extractor fails.",
232
240
  })
241
+ .option("dry-run", {
242
+ type: "boolean",
243
+ default: isDryRun,
244
+ description:
245
+ "Read-only mode. cdxgen only performs file reads and reports blocked writes, command execution, temp creation, network access, and submissions.",
246
+ })
247
+ .option("activity-report", {
248
+ choices: ["json", "jsonl"],
249
+ description: "Render the activity report as JSON or JSON Lines.",
250
+ hidden: true,
251
+ type: "string",
252
+ })
233
253
  .option("no-babel", {
234
254
  type: "boolean",
235
255
  description:
@@ -442,9 +462,8 @@ const args = _yargs
442
462
  })
443
463
  .option("tlp-classification", {
444
464
  description:
445
- 'Traffic Light Protocol (TLP) is a classification system for identifying the potential risk associated with artefact, including whether it is subject to certain types of legal, financial, or technical threats. Refer to [https://www.first.org/tlp/](https://www.first.org/tlp/) for further information.\nThe default classification is "CLEAR"',
465
+ "Traffic Light Protocol (TLP) is a classification system for identifying the potential risk associated with an artefact, including whether it is subject to certain types of legal, financial, or technical threats. Refer to [https://www.first.org/tlp/](https://www.first.org/tlp/) for further information.",
446
466
  choices: ["CLEAR", "GREEN", "AMBER", "AMBER_AND_STRICT", "RED"],
447
- default: "CLEAR",
448
467
  hidden: true,
449
468
  })
450
469
  .option("env-audit", {
@@ -658,6 +677,13 @@ const options = Object.assign({}, args, {
658
677
  exclude: args.exclude || args.excludeRegex,
659
678
  include: args.include || args.includeRegex,
660
679
  });
680
+ setDryRunMode(options.dryRun);
681
+ setActivityContext({
682
+ projectType: Array.isArray(options.projectType)
683
+ ? options.projectType.join(",")
684
+ : options.projectType,
685
+ sourcePath: filePath,
686
+ });
661
687
  const outputPlan = createOutputPlan(options);
662
688
  for (const outputFile of Object.values(outputPlan.outputs)) {
663
689
  const outputDirectory = getOutputDirectory(outputFile);
@@ -666,7 +692,7 @@ for (const outputFile of Object.values(outputPlan.outputs)) {
666
692
  outputDirectory !== process.cwd() &&
667
693
  !safeExistsSync(outputDirectory)
668
694
  ) {
669
- fs.mkdirSync(outputDirectory, { recursive: true });
695
+ safeMkdirSync(outputDirectory, { recursive: true });
670
696
  }
671
697
  }
672
698
  // Filter duplicate types. Eg: -t gradle -t gradle
@@ -709,6 +735,12 @@ if (process.argv[1].includes("cdxgen-secure")) {
709
735
  options.installDeps = false;
710
736
  process.env.CDXGEN_SECURE_MODE = true;
711
737
  }
738
+ if (isDryRun) {
739
+ thoughtLog(
740
+ "Ok, the user wants cdxgen to run in dry-run mode. I must avoid writes, child processes, temp directories, network submissions, and cloning.",
741
+ );
742
+ options.installDeps = false;
743
+ }
712
744
  if (options.standard) {
713
745
  options.specVersion = 1.7;
714
746
  }
@@ -905,9 +937,15 @@ const checkPermissions = (filePath, options) => {
905
937
  console.log(
906
938
  "\x1b[1;35mSecure mode requires permission-related arguments. These can be passed as CLI arguments directly to the node runtime or set the NODE_OPTIONS environment variable as shown below.\x1b[0m",
907
939
  );
908
- const childProcessArgs =
909
- options?.lifecycle !== "pre-build" ? " --allow-child-process" : "";
910
- const nodeOptionsVal = `--permission --allow-fs-read="${getTmpDir()}/*" --allow-fs-write="${getTmpDir()}/*" --allow-fs-read="${fullFilePath}/*" --allow-fs-write="${options.output}"${childProcessArgs}`;
940
+ const childProcessArgs = isDryRun
941
+ ? ""
942
+ : options?.lifecycle !== "pre-build"
943
+ ? " --allow-child-process"
944
+ : "";
945
+ const fsWriteArgs = isDryRun
946
+ ? ""
947
+ : ` --allow-fs-write="${getTmpDir()}/*" --allow-fs-write="${options.output}"`;
948
+ const nodeOptionsVal = `--permission --allow-fs-read="${getTmpDir()}/*" --allow-fs-read="${fullFilePath}/*"${fsWriteArgs}${childProcessArgs}`;
911
949
  console.log(
912
950
  `${isWin ? "$env:" : "export "}NODE_OPTIONS='${nodeOptionsVal}'`,
913
951
  );
@@ -966,12 +1004,12 @@ const checkPermissions = (filePath, options) => {
966
1004
  );
967
1005
  return false;
968
1006
  }
969
- if (!process.permission.has("fs.write", options.output)) {
1007
+ if (!isDryRun && !process.permission.has("fs.write", options.output)) {
970
1008
  console.log(
971
1009
  `\x1b[1;35mSECURE MODE: FileSystemWrite permission is required to create the output BOM file. Please invoke cdxgen with the argument --allow-fs-write="${options.output}"\x1b[0m`,
972
1010
  );
973
1011
  }
974
- if (options.evidence) {
1012
+ if (!isDryRun && options.evidence) {
975
1013
  const slicesFilesKeys = [
976
1014
  "deps-slices-file",
977
1015
  "usages-slices-file",
@@ -995,7 +1033,7 @@ const checkPermissions = (filePath, options) => {
995
1033
  }
996
1034
  }
997
1035
  }
998
- if (!process.permission.has("fs.write", getTmpDir())) {
1036
+ if (!isDryRun && !process.permission.has("fs.write", getTmpDir())) {
999
1037
  console.log(
1000
1038
  `FileSystemWrite permission may be required for the TEMP directory. Please invoke cdxgen with the argument --allow-fs-write="${join(getTmpDir(), "*")}" in case of any crashes.`,
1001
1039
  );
@@ -1005,12 +1043,16 @@ const checkPermissions = (filePath, options) => {
1005
1043
  );
1006
1044
  }
1007
1045
  }
1008
- if (!process.permission.has("child") && !isSecureMode) {
1046
+ if (!isDryRun && !process.permission.has("child") && !isSecureMode) {
1009
1047
  console.log(
1010
1048
  "ChildProcess permission is missing. This is required to spawn commands for some languages. Please invoke cdxgen with the argument --allow-child-process in case of issues.",
1011
1049
  );
1012
1050
  }
1013
- if (process.permission.has("child") && options?.lifecycle === "pre-build") {
1051
+ if (
1052
+ !isDryRun &&
1053
+ process.permission.has("child") &&
1054
+ options?.lifecycle === "pre-build"
1055
+ ) {
1014
1056
  console.log(
1015
1057
  "SECURE MODE: ChildProcess permission is not required for pre-build SBOM generation. Please invoke cdxgen without the argument --allow-child-process.",
1016
1058
  );
@@ -1034,7 +1076,7 @@ const stringifyJson = (jsonPayload, jsonPretty) =>
1034
1076
 
1035
1077
  const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1036
1078
  const jsonPayload = stringifyJson(bomJson, options.jsonPretty);
1037
- fs.writeFileSync(jsonFile, jsonPayload);
1079
+ safeWriteSync(jsonFile, jsonPayload);
1038
1080
  if (jsonFile.endsWith("bom.json")) {
1039
1081
  thoughtLog(
1040
1082
  `Let's save the file to "${jsonFile}". Should I suggest the '.cdx.json' file extension for better semantics?`,
@@ -1045,6 +1087,15 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1045
1087
  if (!jsonPayload || !needsBomSigning(options)) {
1046
1088
  return jsonPayload;
1047
1089
  }
1090
+ if (isDryRun) {
1091
+ recordActivity({
1092
+ kind: "sign",
1093
+ reason: "Dry run mode skips BOM signing and key generation.",
1094
+ status: "blocked",
1095
+ target: jsonFile,
1096
+ });
1097
+ return jsonPayload;
1098
+ }
1048
1099
  let alg = process.env.SBOM_SIGN_ALGORITHM || "RS512";
1049
1100
  if (alg.includes("none")) {
1050
1101
  alg = "RS512";
@@ -1068,9 +1119,9 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1068
1119
  format: "pem",
1069
1120
  },
1070
1121
  });
1071
- fs.writeFileSync(publicKeyFile, publicKey);
1072
- fs.writeFileSync(privateKeyFile, privateKey);
1073
- fs.writeFileSync(
1122
+ safeWriteSync(publicKeyFile, publicKey);
1123
+ safeWriteSync(privateKeyFile, privateKey);
1124
+ safeWriteSync(
1074
1125
  privateKeyB64File,
1075
1126
  Buffer.from(privateKey, "utf8").toString("base64"),
1076
1127
  );
@@ -1122,8 +1173,13 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1122
1173
  signAnnotations: true,
1123
1174
  };
1124
1175
  thoughtLog(`Signing the BOM file "${jsonFile}".`);
1176
+ recordActivity({
1177
+ kind: "sign",
1178
+ status: "completed",
1179
+ target: jsonFile,
1180
+ });
1125
1181
  const signedBom = signBom(bomJsonUnsignedObj, signOptions);
1126
- fs.writeFileSync(
1182
+ safeWriteSync(
1127
1183
  jsonFile,
1128
1184
  JSON.stringify(signedBom, null, options.jsonPretty ? 2 : null),
1129
1185
  );
@@ -1159,9 +1215,7 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1159
1215
  // Our quest to audit and check the SBOM generation environment to prevent our users from getting exploited
1160
1216
  // during SBOM generation.
1161
1217
  if (envAuditFindings?.length) {
1162
- for (const f of envAuditFindings) {
1163
- console.log(`SECURE MODE: ${f.variable}: ${f.message}`);
1164
- }
1218
+ printEnvironmentAuditFindings(envAuditFindings);
1165
1219
  // Only abort in secure mode for high or critical findings; low/medium are informational.
1166
1220
  if (
1167
1221
  isSecureMode &&
@@ -1177,6 +1231,18 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1177
1231
  }
1178
1232
  let sourcePath = filePath;
1179
1233
  let purlResolution;
1234
+ if (isDryRun && maybePurlSource(sourcePath)) {
1235
+ recordActivity({
1236
+ kind: "clone",
1237
+ reason:
1238
+ "Dry run mode blocks package-url source resolution and repository cloning.",
1239
+ status: "blocked",
1240
+ target: sourcePath,
1241
+ });
1242
+ console.warn("Dry run mode skips purl source resolution.");
1243
+ printActivitySummary(options.activityReport);
1244
+ return;
1245
+ }
1180
1246
  if (maybePurlSource(sourcePath)) {
1181
1247
  const purlValidationError = validatePurlSource(sourcePath);
1182
1248
  if (purlValidationError) {
@@ -1237,6 +1303,17 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1237
1303
  let cleanup = false;
1238
1304
  let gitRef = options.gitBranch;
1239
1305
  if (maybeRemotePath(sourcePath)) {
1306
+ if (isDryRun) {
1307
+ recordActivity({
1308
+ kind: "clone",
1309
+ reason: "Dry run mode blocks cloning git URL sources.",
1310
+ status: "blocked",
1311
+ target: sourcePath,
1312
+ });
1313
+ console.warn("Dry run mode skips remote git source cloning.");
1314
+ printActivitySummary(options.activityReport);
1315
+ return;
1316
+ }
1240
1317
  if (!gitRef && purlResolution?.version) {
1241
1318
  gitRef = findGitRefForPurlVersion(sourcePath, purlResolution);
1242
1319
  if (!gitRef) {
@@ -1267,6 +1344,7 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1267
1344
  }
1268
1345
  cleanup = true;
1269
1346
  }
1347
+ setActivityContext({ sourcePath: srcDir });
1270
1348
  prepareEnv(srcDir, options);
1271
1349
  thoughtLog("Getting ready to generate the BOM ⚡️.");
1272
1350
  const originalFetchPackageMetadata = process.env.CDXGEN_FETCH_PKG_METADATA;
@@ -1290,6 +1368,12 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1290
1368
  }
1291
1369
  // Add extra metadata and annotations with post processing
1292
1370
  bomNSData = postProcess(bomNSData, options, srcDir);
1371
+ setActivityContext({
1372
+ projectType: Array.isArray(options.projectType)
1373
+ ? options.projectType.join(",")
1374
+ : options.projectType,
1375
+ sourcePath: srcDir,
1376
+ });
1293
1377
  if (options.bomAudit && bomNSData?.bomJson) {
1294
1378
  const { finalizeAuditReport, runAuditFromBoms } = await import(
1295
1379
  "../lib/audit/index.js"
@@ -1440,53 +1524,73 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1440
1524
  `cdxgen-${Date.now()}-${basename(filePath)}.cdx.json`,
1441
1525
  );
1442
1526
  }
1443
- fs.writeFileSync(
1444
- internalCycloneDxInputPath,
1445
- stringifyJson(bomNSData.bomJson, options.jsonPretty),
1446
- );
1527
+ if (isDryRun) {
1528
+ recordActivity({
1529
+ kind: "write",
1530
+ reason:
1531
+ "Dry run mode skips evidence input materialization because it writes a temporary BOM file.",
1532
+ status: "blocked",
1533
+ target: internalCycloneDxInputPath,
1534
+ });
1535
+ } else {
1536
+ safeWriteSync(
1537
+ internalCycloneDxInputPath,
1538
+ stringifyJson(bomNSData.bomJson, options.jsonPretty),
1539
+ );
1540
+ }
1447
1541
  }
1448
1542
  // Evidence generation
1449
1543
  if (options.evidence || options.includeCrypto) {
1450
- // Set the evinse output file to be the same as output file
1451
- if (!options.evinseOutput) {
1452
- options.evinseOutput = options.output;
1453
- }
1454
- const evinserModule = await import("../lib/evinser/evinser.js");
1455
- options.projectType = options.projectType || ["java"];
1456
- const evinseOptions = {
1457
- _: args._,
1458
- input: internalCycloneDxInputPath || options.output,
1459
- output: options.evinseOutput,
1460
- language: options.projectType,
1461
- skipMavenCollector: false,
1462
- force: false,
1463
- withReachables: options.deep,
1464
- usagesSlicesFile: options.usagesSlicesFile,
1465
- dataFlowSlicesFile: options.dataFlowSlicesFile,
1466
- reachablesSlicesFile: options.reachablesSlicesFile,
1467
- semanticsSlicesFile: options.semanticsSlicesFile,
1468
- openapiSpecFile: options.openapiSpecFile,
1469
- includeCrypto: options.includeCrypto,
1470
- specVersion: options.specVersion,
1471
- profile: options.profile,
1472
- jsonPretty: options.jsonPretty,
1473
- };
1474
- const dbObjMap = await evinserModule.prepareDB(evinseOptions);
1475
- if (dbObjMap) {
1476
- const sliceArtefacts = await evinserModule.analyzeProject(
1477
- dbObjMap,
1478
- evinseOptions,
1479
- );
1480
- const evinseJson = evinserModule.createEvinseFile(
1481
- sliceArtefacts,
1482
- evinseOptions,
1483
- );
1484
- bomNSData.bomJson = evinseJson;
1485
- if (options.print && evinseJson) {
1486
- printOccurrences(evinseJson);
1487
- printCallStack(evinseJson);
1488
- printReachables(sliceArtefacts);
1489
- printServices(evinseJson);
1544
+ if (isDryRun) {
1545
+ recordActivity({
1546
+ kind: "write",
1547
+ reason:
1548
+ "Dry run mode skips evidence and crypto enrichment because those flows require temp files and additional processing.",
1549
+ status: "blocked",
1550
+ target: options.evinseOutput || options.output || "evinse",
1551
+ });
1552
+ } else {
1553
+ // Set the evinse output file to be the same as output file
1554
+ if (!options.evinseOutput) {
1555
+ options.evinseOutput = options.output;
1556
+ }
1557
+ const evinserModule = await import("../lib/evinser/evinser.js");
1558
+ options.projectType = options.projectType || ["java"];
1559
+ const evinseOptions = {
1560
+ _: args._,
1561
+ input: internalCycloneDxInputPath || options.output,
1562
+ output: options.evinseOutput,
1563
+ language: options.projectType,
1564
+ skipMavenCollector: false,
1565
+ force: false,
1566
+ withReachables: options.deep,
1567
+ usagesSlicesFile: options.usagesSlicesFile,
1568
+ dataFlowSlicesFile: options.dataFlowSlicesFile,
1569
+ reachablesSlicesFile: options.reachablesSlicesFile,
1570
+ semanticsSlicesFile: options.semanticsSlicesFile,
1571
+ openapiSpecFile: options.openapiSpecFile,
1572
+ includeCrypto: options.includeCrypto,
1573
+ specVersion: options.specVersion,
1574
+ profile: options.profile,
1575
+ jsonPretty: options.jsonPretty,
1576
+ };
1577
+ const dbObjMap = await evinserModule.prepareDB(evinseOptions);
1578
+ if (dbObjMap) {
1579
+ const sliceArtefacts = await evinserModule.analyzeProject(
1580
+ dbObjMap,
1581
+ evinseOptions,
1582
+ );
1583
+ const evinseJson = evinserModule.createEvinseFile(
1584
+ sliceArtefacts,
1585
+ evinseOptions,
1586
+ );
1587
+ bomNSData.bomJson = evinseJson;
1588
+ if (options.print && evinseJson) {
1589
+ printOccurrences(evinseJson);
1590
+ printCallStack(evinseJson);
1591
+ printReachables(sliceArtefacts);
1592
+ printServices(evinseJson);
1593
+ }
1490
1594
  }
1491
1595
  }
1492
1596
  }
@@ -1498,8 +1602,9 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1498
1602
  cleanupSourceDir(srcDir);
1499
1603
  }
1500
1604
  process.exit(1);
1605
+ } else {
1606
+ thoughtLog("✅ BOM file looks valid.");
1501
1607
  }
1502
- thoughtLog("✅ BOM file looks valid.");
1503
1608
  }
1504
1609
  if (
1505
1610
  outputPlan.formats.has("spdx") &&
@@ -1509,16 +1614,31 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1509
1614
  thoughtLog(
1510
1615
  "Preparing the SPDX 3.0.1 export from the validated CycloneDX BOM.",
1511
1616
  );
1512
- bomNSData.spdxJson = convertCycloneDxToSpdx(bomNSData.bomJson, options);
1513
- if (options.validate && !validateSpdx(bomNSData.spdxJson)) {
1514
- process.exit(1);
1617
+ if (isDryRun) {
1618
+ recordActivity({
1619
+ kind: "convert",
1620
+ reason:
1621
+ "Dry run mode skips SPDX conversion because the export path is read-only.",
1622
+ status: "blocked",
1623
+ target: "spdx",
1624
+ });
1625
+ } else {
1626
+ bomNSData.spdxJson = convertCycloneDxToSpdx(bomNSData.bomJson, options);
1627
+ recordActivity({
1628
+ kind: "convert",
1629
+ status: "completed",
1630
+ target: "spdx",
1631
+ });
1632
+ if (options.validate && !validateSpdx(bomNSData.spdxJson)) {
1633
+ process.exit(1);
1634
+ }
1515
1635
  }
1516
1636
  }
1517
1637
  if (
1518
1638
  options.output &&
1519
1639
  (typeof options.output === "string" || options.output instanceof String)
1520
1640
  ) {
1521
- if (outputPlan.outputs.cyclonedx && bomNSData.bomJson) {
1641
+ if (!isDryRun && outputPlan.outputs.cyclonedx && bomNSData.bomJson) {
1522
1642
  writeCycloneDxOutput(
1523
1643
  outputPlan.outputs.cyclonedx,
1524
1644
  bomNSData.bomJson,
@@ -1526,15 +1646,29 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1526
1646
  );
1527
1647
  if (bomNSData.nsMapping && Object.keys(bomNSData.nsMapping).length) {
1528
1648
  const nsFile = `${outputPlan.outputs.cyclonedx}.map`;
1529
- fs.writeFileSync(nsFile, JSON.stringify(bomNSData.nsMapping));
1649
+ safeWriteSync(nsFile, JSON.stringify(bomNSData.nsMapping));
1530
1650
  }
1651
+ } else if (isDryRun && outputPlan.outputs.cyclonedx) {
1652
+ recordActivity({
1653
+ kind: "write",
1654
+ reason: "Dry run mode skips CycloneDX file output.",
1655
+ status: "blocked",
1656
+ target: outputPlan.outputs.cyclonedx,
1657
+ });
1531
1658
  }
1532
- if (outputPlan.outputs.spdx && bomNSData.spdxJson) {
1533
- fs.writeFileSync(
1659
+ if (!isDryRun && outputPlan.outputs.spdx && bomNSData.spdxJson) {
1660
+ safeWriteSync(
1534
1661
  outputPlan.outputs.spdx,
1535
1662
  stringifyJson(bomNSData.spdxJson, options.jsonPretty),
1536
1663
  );
1537
1664
  thoughtLog(`Let's save the SPDX file to "${outputPlan.outputs.spdx}".`);
1665
+ } else if (isDryRun && outputPlan.outputs.spdx) {
1666
+ recordActivity({
1667
+ kind: "write",
1668
+ reason: "Dry run mode skips SPDX file output.",
1669
+ status: "blocked",
1670
+ target: outputPlan.outputs.spdx,
1671
+ });
1538
1672
  }
1539
1673
  } else if (!options.print) {
1540
1674
  if (outputPlan.formats.has("spdx") && bomNSData?.spdxJson) {
@@ -1550,21 +1684,44 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1550
1684
  // Automatically submit the bom data
1551
1685
  // biome-ignore lint/suspicious/noDoubleEquals: yargs passes true for empty values
1552
1686
  if (options.serverUrl && options.serverUrl != true && options.apiKey) {
1553
- try {
1554
- await submitBom(options, bomNSData.bomJson);
1555
- } catch (err) {
1556
- console.log(err);
1557
- if (cleanup) {
1558
- cleanupSourceDir(srcDir);
1687
+ if (isDryRun) {
1688
+ recordActivity({
1689
+ kind: "submit",
1690
+ reason: "Dry run mode skips remote BOM submission.",
1691
+ status: "blocked",
1692
+ target: options.serverUrl,
1693
+ });
1694
+ } else {
1695
+ try {
1696
+ recordActivity({
1697
+ kind: "submit",
1698
+ status: "completed",
1699
+ target: options.serverUrl,
1700
+ });
1701
+ await submitBom(options, bomNSData.bomJson);
1702
+ } catch (err) {
1703
+ console.log(err);
1704
+ if (cleanup) {
1705
+ cleanupSourceDir(srcDir);
1706
+ }
1707
+ process.exit(1);
1559
1708
  }
1560
- process.exit(1);
1561
1709
  }
1562
1710
  }
1563
1711
  // Protobuf serialization
1564
1712
  if (options.exportProto) {
1565
- const protobomModule = await import("../lib/helpers/protobom.js");
1566
- protobomModule.writeBinary(bomNSData.bomJson, options.protoBinFile);
1567
- thoughtLog("BOM file is also available in .proto format!");
1713
+ if (isDryRun) {
1714
+ recordActivity({
1715
+ kind: "write",
1716
+ reason: "Dry run mode skips protobuf export.",
1717
+ status: "blocked",
1718
+ target: options.protoBinFile,
1719
+ });
1720
+ } else {
1721
+ const protobomModule = await import("../lib/helpers/protobom.js");
1722
+ protobomModule.writeBinary(bomNSData.bomJson, options.protoBinFile);
1723
+ thoughtLog("BOM file is also available in .proto format!");
1724
+ }
1568
1725
  }
1569
1726
  if (options.print && bomNSData.bomJson?.components) {
1570
1727
  printSummary(bomNSData.bomJson);
@@ -1579,6 +1736,9 @@ const writeCycloneDxOutput = (jsonFile, bomJson, options) => {
1579
1736
  printDependencyTree(bomNSData.bomJson, "provides");
1580
1737
  }
1581
1738
  }
1739
+ if (isDryRun || DEBUG_MODE) {
1740
+ printActivitySummary(options.activityReport);
1741
+ }
1582
1742
  if (
1583
1743
  (DEBUG_MODE || TRACE_MODE) &&
1584
1744
  (!process.env?.CDXGEN_ALLOWED_HOSTS ||