@cdxgen/cdxgen 13.0.0 → 13.1.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 +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -8,7 +8,16 @@ import {
8
8
  isDryRun,
9
9
  isSecureMode,
10
10
  readEnvironmentVariable,
11
+ recordObservedActivity,
11
12
  } from "../../core/activity.js";
13
+ import {
14
+ commandOutputText,
15
+ isLedgerEnabled,
16
+ LEDGER_EVENT_IMPACTS,
17
+ LEDGER_EVENT_KINDS,
18
+ recordLedgerEvent,
19
+ } from "../../core/buildLedger.js";
20
+ import { deferFailOnError } from "../../core/deferredExit.js";
12
21
  import { hasAnyProjectType, isFeatureEnabled } from "../../core/env.js";
13
22
  import {
14
23
  getAllFiles,
@@ -21,6 +30,7 @@ import {
21
30
  import { isMac, isWin } from "../../core/paths.js";
22
31
  import {
23
32
  bundleInstallWithDocker,
33
+ collectJavaInfo,
24
34
  collectRubyInfo,
25
35
  getOrInstallNvmTool,
26
36
  installRubyBundler,
@@ -29,10 +39,33 @@ import {
29
39
  isNvmAvailable,
30
40
  isRbenvAvailable,
31
41
  isSdkmanAvailable,
42
+ isSdkmanToolAvailable,
32
43
  performBundleInstall,
33
44
  runSwiftCommand,
34
45
  SDKMAN_JAVA_TOOL_ALIASES,
35
46
  } from "../../inventory/envcontext.js";
47
+ import {
48
+ compareSdkmanVersions,
49
+ DEFAULT_JAVA_MAJOR,
50
+ detectProjectJavaMajor,
51
+ determineRequiredJavaVersion,
52
+ extractJavaMajor,
53
+ minimumGradleVersionForJava,
54
+ parseJvmToolProjectType,
55
+ parseSdkmanrc,
56
+ readGradleWrapperVersion,
57
+ readMavenWrapperVersion,
58
+ readSbtBuildPropertiesVersion,
59
+ readToolVersionEnvHint,
60
+ readToolVersionOverride,
61
+ resolvePartialVersion,
62
+ } from "../../inventory/jvmToolEnv.js";
63
+ import {
64
+ classifyProbeResult,
65
+ describeSpawnRestriction,
66
+ parseGemfileLockBundlerVersion,
67
+ readDeclaredToolRequirements,
68
+ } from "../../inventory/toolRequirements.js";
36
69
 
37
70
  /**
38
71
  * Method to prepare the build environment for BOM generation purposes.
@@ -41,18 +74,29 @@ import {
41
74
  * @param {Object} options CLI options
42
75
  */
43
76
  export function prepareEnv(filePath, options) {
44
- if (!options.projectType || isSecureMode || isDryRun) {
77
+ if (!options.projectType || isSecureMode) {
78
+ return;
79
+ }
80
+ if (filePath) {
81
+ filePath = resolve(filePath);
82
+ }
83
+ // The JVM preparation provisions the JDK ahead of the build tools, so an
84
+ // alias it already installed must not be installed a second time here.
85
+ // It runs under dry-run too, where it reports the toolchain it would set up
86
+ // instead of setting it up.
87
+ const jdkAliasHandled = prepareJvmBuildEnv(filePath, options);
88
+ recordDeclaredToolRequirements(filePath);
89
+ if (isDryRun) {
45
90
  return;
46
91
  }
47
92
  for (const pt of options.projectType) {
48
93
  if (SDKMAN_JAVA_TOOL_ALIASES[pt]) {
49
- prepareSdkmanBuild(pt);
94
+ if (pt !== jdkAliasHandled) {
95
+ prepareSdkmanBuild(pt);
96
+ }
50
97
  break;
51
98
  }
52
99
  }
53
- if (filePath) {
54
- filePath = resolve(filePath);
55
- }
56
100
  // Check the pre-requisites for various types
57
101
  preparePythonEnv(filePath, options);
58
102
  prepareNodeEnv(filePath, options);
@@ -60,6 +104,72 @@ export function prepareEnv(filePath, options) {
60
104
  prepareRubyEnv(filePath, options);
61
105
  }
62
106
 
107
+ /**
108
+ * Record the tool requirements the project declares through its well-known
109
+ * tool pin files (`.tool-versions`, `.nvmrc`, `package.json`, `go.mod`, …),
110
+ * one `tool.expected` event per requirement. Recording is the only purpose of
111
+ * the file walk, so it is skipped entirely when the ledger is disabled.
112
+ *
113
+ * @param {String} filePath Project path
114
+ */
115
+ function recordDeclaredToolRequirements(filePath) {
116
+ if (!isLedgerEnabled() || !filePath) {
117
+ return;
118
+ }
119
+ for (const requirement of readDeclaredToolRequirements(filePath)) {
120
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
121
+ ecosystem: requirement.ecosystem,
122
+ tool: requirement.tool,
123
+ wanted: requirement.wanted,
124
+ source: requirement.source,
125
+ path: requirement.path,
126
+ });
127
+ }
128
+ recordDeclaredJvmToolRequirements(filePath);
129
+ }
130
+
131
+ /**
132
+ * Record the JVM tool versions a repository pins through its build tool
133
+ * wrappers and `.sdkmanrc`.
134
+ *
135
+ * These readers live in `jvmToolEnv.js`, which imports the shared comparator
136
+ * from `toolRequirements.js`; reading them here rather than inside
137
+ * `readDeclaredToolRequirements` keeps that dependency one-directional.
138
+ *
139
+ * @param {String} filePath Project path
140
+ * @returns {void}
141
+ */
142
+ function recordDeclaredJvmToolRequirements(filePath) {
143
+ const declarations = [
144
+ ["gradle", readGradleWrapperVersion(filePath), "gradle-wrapper.properties"],
145
+ ["maven", readMavenWrapperVersion(filePath), "maven-wrapper.properties"],
146
+ [
147
+ "sbt",
148
+ readSbtBuildPropertiesVersion(filePath),
149
+ "project/build.properties",
150
+ ],
151
+ ];
152
+ const sdkmanPins = parseSdkmanrc(filePath) || {};
153
+ for (const [tool, wanted] of Object.entries(sdkmanPins)) {
154
+ declarations.push([tool, wanted, ".sdkmanrc"]);
155
+ }
156
+ for (const [tool, declared, source] of declarations) {
157
+ // The wrapper readers return `{ version, distributionUrl }` while the sbt
158
+ // and `.sdkmanrc` readers return the version directly.
159
+ const wanted = typeof declared === "string" ? declared : declared?.version;
160
+ if (!wanted) {
161
+ continue;
162
+ }
163
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
164
+ ecosystem: "java",
165
+ tool,
166
+ wanted: `${wanted}`,
167
+ source,
168
+ path: join(filePath, source),
169
+ });
170
+ }
171
+ }
172
+
63
173
  /**
64
174
  * Method to prepare sdkman build environment for BOM generation purposes.
65
175
  *
@@ -76,6 +186,912 @@ export function prepareSdkmanBuild(projectType) {
76
186
  return installSdkmanTool(toolType, projectType);
77
187
  }
78
188
 
189
+ /**
190
+ * Binary names used to probe and select each JVM build tool, keyed by sdkman
191
+ * candidate name.
192
+ *
193
+ * @type {Object<string, Object<string, string>>}
194
+ */
195
+ const JVM_TOOL_BINARIES = {
196
+ maven: { env: "MVN_CMD", command: "mvn", windowsCommand: "mvn.cmd" },
197
+ gradle: {
198
+ env: "GRADLE_CMD",
199
+ command: "gradle",
200
+ windowsCommand: "gradle.bat",
201
+ },
202
+ sbt: { env: "SBT_CMD", command: "sbt", windowsCommand: "sbt.bat" },
203
+ // No env entry: scala runs are driven through SCALA_HOME set by the installer.
204
+ scala: { command: "scala", windowsCommand: "scala.bat" },
205
+ };
206
+
207
+ /**
208
+ * Version prefix used to pick a default version when a tool is required but
209
+ * nothing pins it. Maven is restricted to the 3.9 line because it keeps JDK
210
+ * 8-25 compatibility; other tools take the newest stable release.
211
+ *
212
+ * @type {Object<string, string>}
213
+ */
214
+ const JVM_TOOL_DEFAULT_PREFIXES = {
215
+ maven: "3.9",
216
+ };
217
+
218
+ /** sdkman candidates cdxgen can provision for JVM builds. */
219
+ const JVM_BUILD_TOOLS = ["maven", "gradle", "sbt", "scala"];
220
+
221
+ /**
222
+ * True while `CDXGEN_JVM_TOOL_PINNED` holds a value cdxgen set itself, which
223
+ * makes it safe to clear on the next preparation run. Long-lived processes
224
+ * such as the server prepare the environment once per request.
225
+ */
226
+ let cdxgenOwnsPinnedToolEnv = false;
227
+
228
+ /**
229
+ * Method to detect and provision JVM build tools (maven, gradle, sbt, scala)
230
+ * plus a compatible JDK before BOM generation.
231
+ *
232
+ * Two styles are supported:
233
+ * - Explicit: a versioned project type such as `-t maven3.9.9` installs that
234
+ * exact sdkman version and pins it for the generation step.
235
+ * - Automatic: with the `jvm-tool-setup` feature flag, tool versions are
236
+ * derived from repo markers (`.sdkmanrc`, build tool wrappers,
237
+ * `project/build.properties`) and missing tools are provisioned. Projects
238
+ * pinning a tool through a wrapper are left alone because the wrapper
239
+ * provisions itself; only JDK compatibility is checked.
240
+ *
241
+ * @param {String} filePath Project path
242
+ * @param {Object} options CLI options
243
+ *
244
+ * @returns {String|undefined} The `javaNN` project type whose JDK this call
245
+ * already provisioned, so the caller can skip installing it again.
246
+ */
247
+ export function prepareJvmBuildEnv(filePath, options) {
248
+ // Each invocation re-derives the pin, so a signal left behind by an earlier
249
+ // run must not leak into this one. A value the user set stays untouched.
250
+ if (cdxgenOwnsPinnedToolEnv) {
251
+ delete process.env.CDXGEN_JVM_TOOL_PINNED;
252
+ cdxgenOwnsPinnedToolEnv = false;
253
+ }
254
+ const explicitPins = [];
255
+ for (const pt of options.projectType || []) {
256
+ const parsed = parseJvmToolProjectType(pt);
257
+ if (!parsed) {
258
+ continue;
259
+ }
260
+ if (!parsed.valid) {
261
+ console.log(
262
+ `Invalid version '${parsed.version}' in the project type '${pt}'. Pass an exact sdkman version identifier such as -t maven3.9.9, -t gradle8.14.3, -t sbt1.10.11, or -t scala3.6.4. Run 'sdk list maven' style commands to see the available identifiers.`,
263
+ );
264
+ continue;
265
+ }
266
+ explicitPins.push({
267
+ tool: parsed.tool,
268
+ version: parsed.version,
269
+ source: "cli",
270
+ });
271
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
272
+ ecosystem: "java",
273
+ tool: parsed.tool,
274
+ wanted: parsed.version,
275
+ source: "cli",
276
+ });
277
+ }
278
+ const autoMode = isFeatureEnabled(options, "jvm-tool-setup");
279
+ if (!explicitPins.length && !autoMode) {
280
+ return;
281
+ }
282
+ if (isWin) {
283
+ console.log(
284
+ "Automatic JVM build tool provisioning requires sdkman, which is unavailable on Windows. Use the project's mvnw/gradlew wrappers or the cdxgen container image, which bundles maven, gradle, sbt, and scala.",
285
+ );
286
+ return;
287
+ }
288
+ const detected = autoMode
289
+ ? detectJvmToolsFromRepo(filePath, explicitPins)
290
+ : { installPins: [], wrapperVersions: [], javaVersionHint: undefined };
291
+ const installPins = [...explicitPins, ...detected.installPins];
292
+ if (!installPins.length && !detected.wrapperVersions.length) {
293
+ return;
294
+ }
295
+ if (isDryRun) {
296
+ // Versions are left unresolved: resolving them means asking sdkman over
297
+ // the network, which dry-run does not do.
298
+ recordJvmBuildEnvIntent(options, installPins, detected);
299
+ return;
300
+ }
301
+ // Resolve partial versions (e.g. maven3.9 -> 3.9.16) before touching the JDK
302
+ // so the compatibility matrix sees the real versions.
303
+ const resolvedPins = [];
304
+ for (const pin of installPins) {
305
+ const resolved = resolveJvmToolPinVersion(pin);
306
+ if (resolved) {
307
+ resolvedPins.push(resolved);
308
+ }
309
+ }
310
+ // Install the JDK first: build tools and the generation step both need it.
311
+ const knownToolVersions = [...resolvedPins, ...detected.wrapperVersions];
312
+ const javaMajor = ensureCompatibleJdk(
313
+ filePath,
314
+ options,
315
+ knownToolVersions,
316
+ detected.javaVersionHint,
317
+ );
318
+ warnGradleJavaIncompatibility(knownToolVersions, javaMajor);
319
+ let pinnedToolSelected = false;
320
+ for (const pin of resolvedPins) {
321
+ const installed = provisionJvmTool(pin, options);
322
+ if (installed) {
323
+ setJvmToolCommandEnv(pin);
324
+ if (pin.source === "cli") {
325
+ pinnedToolSelected = true;
326
+ }
327
+ }
328
+ }
329
+ // Signal the maven/gradle resolvers that an explicit CLI pin must beat the
330
+ // project wrappers.
331
+ if (pinnedToolSelected) {
332
+ process.env.CDXGEN_JVM_TOOL_PINNED = "true";
333
+ cdxgenOwnsPinnedToolEnv = true;
334
+ }
335
+ return (options.projectType || []).find((pt) =>
336
+ Boolean(SDKMAN_JAVA_TOOL_ALIASES[pt]),
337
+ );
338
+ }
339
+
340
+ /**
341
+ * Human-readable explanation of where a tool version came from, used in the
342
+ * dry-run report so the reader can trace every decision back to its input.
343
+ *
344
+ * @type {Object<string, string>}
345
+ */
346
+ const JVM_PIN_SOURCE_REASONS = {
347
+ cli: "pinned by the versioned project type on the command line",
348
+ "env-override": "pinned by the tool's *_TOOL environment variable",
349
+ sdkmanrc: "pinned by the project's .sdkmanrc",
350
+ "env-hint": "taken from the tool's *_VERSION environment variable",
351
+ default:
352
+ "the default version, because the project uses the tool without a wrapper and no usable command was found",
353
+ wrapper: "pinned by the project's build tool wrapper",
354
+ "build-properties": "declared in the project's project/build.properties",
355
+ };
356
+
357
+ /**
358
+ * Report the JVM toolchain that would be provisioned, without provisioning it.
359
+ *
360
+ * Dry-run exists so that the reader can see every side effect cdxgen would
361
+ * cause before allowing it to run for real, and provisioning a toolchain is
362
+ * the largest side effect this stage has: it downloads and installs software
363
+ * and then rewrites the command environment used for the rest of the run.
364
+ * Skipping the stage silently would hide exactly what dry-run is for, so each
365
+ * install, each JDK decision, and each environment variable that would change
366
+ * is recorded as a blocked activity instead.
367
+ *
368
+ * @param {Object} options CLI options
369
+ * @param {Array} installPins Tools that would be installed
370
+ * @param {Object} detected Repo detection result
371
+ */
372
+ export function recordJvmBuildEnvIntent(options, installPins, detected) {
373
+ const knownToolVersions = [...installPins, ...detected.wrapperVersions];
374
+ for (const pin of installPins) {
375
+ const version = pin.version || JVM_TOOL_DEFAULT_PREFIXES[pin.tool] || "";
376
+ const versionLabel = pin.version
377
+ ? pin.version
378
+ : `${version ? `${version}.x` : "latest"} (resolved from sdkman at run time)`;
379
+ recordObservedActivity("provision", `sdkman:${pin.tool}@${versionLabel}`, {
380
+ metadata: {
381
+ packageType: pin.tool,
382
+ toolSource: pin.source,
383
+ },
384
+ reason: `Would install ${pin.tool} ${versionLabel} with sdkman: ${JVM_PIN_SOURCE_REASONS[pin.source] || pin.source}.`,
385
+ status: "blocked",
386
+ traceDetail: `${pin.tool}:${pin.source}`,
387
+ });
388
+ const commandEnv = JVM_TOOL_BINARIES[pin.tool]?.env;
389
+ if (commandEnv) {
390
+ recordObservedActivity("env", `process.env:${commandEnv}`, {
391
+ metadata: { packageType: pin.tool },
392
+ reason: `Would point ${commandEnv} at the provisioned ${pin.tool}.`,
393
+ status: "blocked",
394
+ });
395
+ }
396
+ }
397
+ for (const pin of detected.wrapperVersions) {
398
+ recordObservedActivity("discover", `${pin.tool}@${pin.version}`, {
399
+ metadata: {
400
+ packageType: pin.tool,
401
+ toolSource: pin.source,
402
+ },
403
+ reason: `Detected ${pin.tool} ${pin.version} ${JVM_PIN_SOURCE_REASONS[pin.source] || pin.source}. Nothing would be installed because the project provisions it itself.`,
404
+ traceDetail: `${pin.tool}:${pin.source}`,
405
+ });
406
+ }
407
+ const targetMajor = determineTargetJavaMajor(
408
+ knownToolVersions,
409
+ detected.javaVersionHint,
410
+ );
411
+ const explicitJavaType = (options.projectType || []).find((pt) =>
412
+ Boolean(SDKMAN_JAVA_TOOL_ALIASES[pt]),
413
+ );
414
+ if (explicitJavaType) {
415
+ // An explicit javaNN type is always installed, so it is reported as one.
416
+ recordObservedActivity(
417
+ "provision",
418
+ `sdkman:java@${SDKMAN_JAVA_TOOL_ALIASES[explicitJavaType]}`,
419
+ {
420
+ metadata: { packageType: "java" },
421
+ reason: `Would install ${SDKMAN_JAVA_TOOL_ALIASES[explicitJavaType]} with sdkman: requested by the ${explicitJavaType} project type.`,
422
+ status: "blocked",
423
+ traceDetail: explicitJavaType,
424
+ },
425
+ );
426
+ } else {
427
+ // Otherwise the JDK is installed only when the active one is too old, and
428
+ // the active version cannot be read without running java. Report the
429
+ // requirement and the fallback rather than implying a certain install.
430
+ const javaAlias = pickJavaAliasForMajor(targetMajor);
431
+ recordObservedActivity("decision", `java>=${targetMajor}`, {
432
+ metadata: { packageType: "java" },
433
+ reason: javaAlias
434
+ ? `The detected build tools need Java ${targetMajor} or higher. The active JDK would be checked first, and ${SDKMAN_JAVA_TOOL_ALIASES[javaAlias]} installed with sdkman only if it is older.`
435
+ : `The detected build tools need Java ${targetMajor} or higher, which cdxgen cannot provision automatically.`,
436
+ status: "blocked",
437
+ traceDetail: `java:${targetMajor}`,
438
+ });
439
+ }
440
+ if (installPins.some((pin) => pin.source === "cli")) {
441
+ recordObservedActivity("env", "process.env:CDXGEN_JVM_TOOL_PINNED", {
442
+ reason:
443
+ "Would be set so the Maven and Gradle resolvers prefer the pinned command over the project's wrapper scripts.",
444
+ status: "blocked",
445
+ });
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Highest JDK major the detected toolchain needs, falling back to the default
451
+ * when nothing constrains it.
452
+ *
453
+ * @param {Array} knownToolVersions Tool versions that will run
454
+ * @param {Number} javaVersionHint JDK major requested by the repo
455
+ *
456
+ * @returns {Number} JDK major to target.
457
+ */
458
+ function determineTargetJavaMajor(knownToolVersions, javaVersionHint) {
459
+ const neededJava = determineRequiredJavaVersion(knownToolVersions);
460
+ return Math.max(javaVersionHint || 0, neededJava || 0) || DEFAULT_JAVA_MAJOR;
461
+ }
462
+
463
+ /**
464
+ * Detect the JVM build tools a project needs from its own version pins, in
465
+ * precedence order: MAVEN_TOOL style overrides, `.sdkmanrc`, container/CI
466
+ * version hints, then build tool wrappers and repo markers. Wrapper-pinned
467
+ * tools are returned as known versions without an install because the
468
+ * wrapper downloads its own distribution.
469
+ *
470
+ * @param {String} filePath Project path
471
+ * @param {Array} explicitPins Pins collected from versioned project types
472
+ *
473
+ * @returns {Object} `{ installPins, wrapperVersions, javaVersionHint }`
474
+ */
475
+ export function detectJvmToolsFromRepo(filePath, explicitPins) {
476
+ const installPins = [];
477
+ const wrapperVersions = [];
478
+ const explicitTools = new Set((explicitPins || []).map((p) => p.tool));
479
+ const sdkmanrc = filePath ? parseSdkmanrc(filePath) : undefined;
480
+ const gradleWrapper = filePath
481
+ ? readGradleWrapperVersion(filePath)
482
+ : undefined;
483
+ const mavenWrapper = filePath ? readMavenWrapperVersion(filePath) : undefined;
484
+ const sbtBuildVersion = filePath
485
+ ? readSbtBuildPropertiesVersion(filePath)
486
+ : undefined;
487
+ for (const tool of JVM_BUILD_TOOLS) {
488
+ if (explicitTools.has(tool)) {
489
+ continue;
490
+ }
491
+ const override = readToolVersionOverride(tool);
492
+ if (override) {
493
+ if (DEBUG_MODE) {
494
+ console.log(
495
+ `Using ${tool} ${override} from the ${tool.toUpperCase()}_TOOL environment variable.`,
496
+ );
497
+ }
498
+ installPins.push({ tool, version: override, source: "env-override" });
499
+ continue;
500
+ }
501
+ if (sdkmanrc?.[tool]) {
502
+ console.log(`Using ${tool} ${sdkmanrc[tool]} from the .sdkmanrc file.`);
503
+ installPins.push({ tool, version: sdkmanrc[tool], source: "sdkmanrc" });
504
+ continue;
505
+ }
506
+ const envHint = readToolVersionEnvHint(tool);
507
+ if (envHint) {
508
+ if (DEBUG_MODE) {
509
+ console.log(
510
+ `Using ${tool} ${envHint} from the ${tool.toUpperCase()}_VERSION environment variable.`,
511
+ );
512
+ }
513
+ installPins.push({ tool, version: envHint, source: "env-hint" });
514
+ continue;
515
+ }
516
+ // Wrapper-first: a project-pinned tool is never installed by cdxgen.
517
+ if (tool === "gradle" && gradleWrapper) {
518
+ if (DEBUG_MODE) {
519
+ console.log(
520
+ `Gradle ${gradleWrapper.version} is pinned by the project wrapper. The wrapper will provision it.`,
521
+ );
522
+ }
523
+ wrapperVersions.push({
524
+ tool,
525
+ version: gradleWrapper.version,
526
+ source: "wrapper",
527
+ });
528
+ continue;
529
+ }
530
+ if (tool === "maven" && mavenWrapper) {
531
+ if (DEBUG_MODE) {
532
+ console.log(
533
+ `Maven ${mavenWrapper.version} is pinned by the project wrapper. The wrapper will provision it.`,
534
+ );
535
+ }
536
+ wrapperVersions.push({
537
+ tool,
538
+ version: mavenWrapper.version,
539
+ source: "wrapper",
540
+ });
541
+ continue;
542
+ }
543
+ if (tool === "sbt") {
544
+ if (!filePath) {
545
+ continue;
546
+ }
547
+ if (sbtBuildVersion) {
548
+ if (DEBUG_MODE) {
549
+ console.log(
550
+ `sbt ${sbtBuildVersion} is declared in project/build.properties. The sbt launcher will provision it.`,
551
+ );
552
+ }
553
+ wrapperVersions.push({
554
+ tool,
555
+ version: sbtBuildVersion,
556
+ source: "build-properties",
557
+ });
558
+ }
559
+ if (
560
+ (sbtBuildVersion || safeExistsSync(join(filePath, "build.sbt"))) &&
561
+ !isJvmToolUsable(tool)
562
+ ) {
563
+ installPins.push({ tool, version: undefined, source: "default" });
564
+ }
565
+ continue;
566
+ }
567
+ if (tool === "scala") {
568
+ // scala has no reliable repo marker; it is only provisioned when
569
+ // explicitly pinned via .sdkmanrc, env hints, or overrides.
570
+ continue;
571
+ }
572
+ // maven and gradle fall back to a default install when the repo uses the
573
+ // tool without a wrapper and no usable command exists.
574
+ const toolMarkers =
575
+ tool === "maven"
576
+ ? ["pom.xml"]
577
+ : [
578
+ "build.gradle",
579
+ "build.gradle.kts",
580
+ "settings.gradle",
581
+ "settings.gradle.kts",
582
+ ];
583
+ const markerFound = filePath
584
+ ? toolMarkers.some((marker) => safeExistsSync(join(filePath, marker)))
585
+ : false;
586
+ if (markerFound && !isJvmToolUsable(tool)) {
587
+ installPins.push({ tool, version: undefined, source: "default" });
588
+ }
589
+ }
590
+ const javaVersionHint =
591
+ (sdkmanrc?.java ? extractJavaMajor(sdkmanrc.java) : undefined) ??
592
+ (filePath ? detectProjectJavaMajor(filePath) : undefined);
593
+ recordDetectedJvmPins(installPins, wrapperVersions);
594
+ return { installPins, wrapperVersions, javaVersionHint };
595
+ }
596
+
597
+ /**
598
+ * Record one `tool.expected` event per JVM pin detected from the repo, with
599
+ * the pin's declared version and the source that declared it.
600
+ *
601
+ * @param {Array} installPins Pins that cdxgen would install
602
+ * @param {Array} wrapperVersions Versions the project provisions itself
603
+ */
604
+ function recordDetectedJvmPins(installPins, wrapperVersions) {
605
+ if (!isLedgerEnabled()) {
606
+ return;
607
+ }
608
+ for (const pin of installPins) {
609
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
610
+ ecosystem: "java",
611
+ tool: pin.tool,
612
+ wanted: pin.version,
613
+ source: pin.source === "env-override" ? "override" : pin.source,
614
+ });
615
+ }
616
+ for (const pin of wrapperVersions) {
617
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
618
+ ecosystem: "java",
619
+ tool: pin.tool,
620
+ wanted: pin.version,
621
+ source: pin.source,
622
+ });
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Resolve a tool pin to an exact, locally known version. Exact versions that
628
+ * are already installed pass through untouched; otherwise the available
629
+ * sdkman versions are consulted to resolve partial prefixes such as `3.9`
630
+ * and to pick defaults.
631
+ *
632
+ * @param {Object} pin `{ tool, version?, source }`
633
+ *
634
+ * @returns {Object|undefined} Pin with an exact version, or undefined when no
635
+ * version could be determined.
636
+ */
637
+ export function resolveJvmToolPinVersion(pin) {
638
+ if (pin.version && isSdkmanToolAvailable(pin.tool, pin.version)) {
639
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
640
+ ecosystem: "java",
641
+ tool: pin.tool,
642
+ wanted: pin.version,
643
+ found: pin.version,
644
+ source: "sdkman",
645
+ detail: `${pin.tool} ${pin.version} is already installed.`,
646
+ });
647
+ return pin;
648
+ }
649
+ const versionList = fetchSdkmanVersionList(pin.tool);
650
+ if (versionList === undefined) {
651
+ // sdk list failed (offline or sdkman missing). Exact versions are still
652
+ // worth attempting directly; defaults are not.
653
+ if (pin.version) {
654
+ return pin;
655
+ }
656
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
657
+ ecosystem: "java",
658
+ tool: pin.tool,
659
+ source: "sdkman",
660
+ detail: `No installable ${pin.tool} version could be determined: 'sdk list ${pin.tool}' did not return any versions.`,
661
+ });
662
+ console.log(
663
+ `Could not determine an installable version for ${pin.tool}: 'sdk list ${pin.tool}' did not return any versions.`,
664
+ );
665
+ return undefined;
666
+ }
667
+ const prefix = pin.version || JVM_TOOL_DEFAULT_PREFIXES[pin.tool] || "";
668
+ const resolved = resolvePartialVersion(versionList, prefix);
669
+ if (!resolved) {
670
+ if (pin.version) {
671
+ return pin;
672
+ }
673
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
674
+ ecosystem: "java",
675
+ tool: pin.tool,
676
+ wanted: prefix,
677
+ source: "sdkman",
678
+ detail: `No stable ${pin.tool} version matched the default prefix '${prefix}'.`,
679
+ });
680
+ console.log(
681
+ `No stable ${pin.tool} version matched the default prefix '${prefix}'. Pass an exact version identifier via -t ${pin.tool}<version>.`,
682
+ );
683
+ return undefined;
684
+ }
685
+ if (resolved !== pin.version) {
686
+ console.log(
687
+ `Resolved ${pin.tool} version '${pin.version || "default"}' to '${resolved}' from the available sdkman versions.`,
688
+ );
689
+ }
690
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
691
+ ecosystem: "java",
692
+ tool: pin.tool,
693
+ wanted: pin.version,
694
+ found: resolved,
695
+ source: "sdkman",
696
+ });
697
+ return { ...pin, version: resolved };
698
+ }
699
+
700
+ /**
701
+ * Fetch the versions available for a candidate via `sdk list`.
702
+ *
703
+ * @param {String} tool sdkman candidate name
704
+ *
705
+ * @returns {String|undefined} Command output, or undefined on failure.
706
+ */
707
+ export function fetchSdkmanVersionList(tool) {
708
+ if (!isSdkmanAvailable()) {
709
+ return undefined;
710
+ }
711
+ const result = safeSpawnSync(
712
+ readEnvironmentVariable("SHELL") || "bash",
713
+ ["-i", "-c", `sdk list ${tool}`],
714
+ {
715
+ encoding: "utf-8",
716
+ timeout: TIMEOUT_MS,
717
+ },
718
+ );
719
+ if (result.error || result.status !== 0) {
720
+ return undefined;
721
+ }
722
+ return result.stdout ? result.stdout.toString() : "";
723
+ }
724
+
725
+ /**
726
+ * Check whether a JVM build tool already has a usable command, honouring the
727
+ * existing `MVN_CMD`/`GRADLE_CMD`/`SBT_CMD` overrides.
728
+ *
729
+ * The probe deliberately runs outside the scanned project. Build tool
730
+ * launchers read JVM arguments from the working directory before doing
731
+ * anything else - Maven from `.mvn/jvm.config` and sbt from `.jvmopts` or
732
+ * `.sbtopts` - so a `--version` call inside a project would let that project
733
+ * inject options such as `-javaagent` into the probe. Only the presence of a
734
+ * working command is being established here, which the project cannot affect.
735
+ *
736
+ * @param {String} tool Tool name
737
+ *
738
+ * @returns {Boolean} True when the tool responds to `--version`.
739
+ */
740
+ export function isJvmToolUsable(tool) {
741
+ const toolBinary = JVM_TOOL_BINARIES[tool];
742
+ if (!toolBinary) {
743
+ return false;
744
+ }
745
+ // Honour explicit command overrides such as MVN_CMD/GRADLE_CMD/SBT_CMD.
746
+ const binaryName = toolBinary[isWin ? "windowsCommand" : "command"];
747
+ const overrideCommand = toolBinary.env
748
+ ? readEnvironmentVariable(toolBinary.env)
749
+ : undefined;
750
+ const probeCommand = overrideCommand || binaryName;
751
+ if (isDryRun) {
752
+ // Executing the tool is blocked here, and a blocked probe would report
753
+ // every tool as missing. Looking the binary up on PATH answers the same
754
+ // question without running anything.
755
+ const located = overrideCommand
756
+ ? safeExistsSync(overrideCommand) && overrideCommand
757
+ : locateToolBinaryOnPath(binaryName);
758
+ recordObservedActivity("probe", probeCommand, {
759
+ metadata: { packageType: tool, probeType: "build-tool-availability" },
760
+ reason: located
761
+ ? `Found ${tool} at ${located} on PATH without executing it.`
762
+ : `No ${tool} command found on PATH. It was not executed.`,
763
+ status: "blocked",
764
+ traceDetail: tool,
765
+ });
766
+ if (located) {
767
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
768
+ ecosystem: "java",
769
+ tool,
770
+ source: "PATH",
771
+ path: located,
772
+ });
773
+ } else {
774
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
775
+ ecosystem: "java",
776
+ tool,
777
+ source: "PATH",
778
+ detail: `No ${tool} command was found on PATH.`,
779
+ });
780
+ }
781
+ return Boolean(located);
782
+ }
783
+ const result = safeSpawnSync(probeCommand, ["--version"], {
784
+ cdxgenActivity: {
785
+ kind: "probe",
786
+ metadata: { tool },
787
+ probeType: "build-tool-availability",
788
+ },
789
+ cwd: getTmpDir(),
790
+ encoding: "utf-8",
791
+ shell: isWin,
792
+ timeout: TIMEOUT_MS,
793
+ });
794
+ const usable = !result.error && result.status === 0;
795
+ if (usable) {
796
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
797
+ ecosystem: "java",
798
+ tool,
799
+ source: "PATH",
800
+ });
801
+ } else if (
802
+ describeSpawnRestriction(probeCommand) ||
803
+ classifyProbeResult(result) === "denied"
804
+ ) {
805
+ // A probe that could not run says nothing about the tool's presence.
806
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
807
+ ecosystem: "java",
808
+ tool,
809
+ source: "PATH",
810
+ detail: `The ${tool} availability probe could not run, so the tool's presence is unknown.`,
811
+ });
812
+ } else {
813
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
814
+ ecosystem: "java",
815
+ tool,
816
+ source: "PATH",
817
+ detail: `No working ${tool} command answered the availability probe.`,
818
+ });
819
+ }
820
+ return usable;
821
+ }
822
+
823
+ /**
824
+ * Find an executable on PATH without running it.
825
+ *
826
+ * @param {String} binaryName Binary to look for
827
+ *
828
+ * @returns {String|undefined} Full path to the binary, when present.
829
+ */
830
+ function locateToolBinaryOnPath(binaryName) {
831
+ for (const pathEntry of (readEnvironmentVariable("PATH") || "").split(
832
+ delimiter,
833
+ )) {
834
+ if (!pathEntry) {
835
+ continue;
836
+ }
837
+ const candidate = join(pathEntry, binaryName);
838
+ if (safeExistsSync(candidate)) {
839
+ return candidate;
840
+ }
841
+ }
842
+ return undefined;
843
+ }
844
+
845
+ /**
846
+ * Ensure a JDK compatible with the pinned tools is active. Installs a JDK
847
+ * only when the current java is missing or older than required; an explicit
848
+ * `javaNN` project type is honoured as-is with a warning when it is too old.
849
+ *
850
+ * @param {String} filePath Project path
851
+ * @param {Object} options CLI options
852
+ * @param {Array} knownToolVersions Tool versions that will run
853
+ * @param {Number} javaVersionHint JDK major requested by the repo
854
+ *
855
+ * @returns {Number|undefined} Effective JDK major after the check.
856
+ */
857
+ export function ensureCompatibleJdk(
858
+ filePath,
859
+ options,
860
+ knownToolVersions,
861
+ javaVersionHint,
862
+ ) {
863
+ const neededJava = determineRequiredJavaVersion(knownToolVersions);
864
+ const explicitJavaType = (options.projectType || []).find((pt) =>
865
+ Boolean(SDKMAN_JAVA_TOOL_ALIASES[pt]),
866
+ );
867
+ if (explicitJavaType) {
868
+ const aliasMajor = Number.parseInt(
869
+ explicitJavaType.replace("java", ""),
870
+ 10,
871
+ );
872
+ if (neededJava && aliasMajor < neededJava) {
873
+ console.log(
874
+ `WARNING: the pinned JVM build tools require Java ${neededJava} or higher, but Java ${aliasMajor} was requested.`,
875
+ );
876
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
877
+ ecosystem: "java",
878
+ tool: "java",
879
+ wanted: `${neededJava}`,
880
+ found: `${aliasMajor}`,
881
+ source: "cli",
882
+ detail: `The pinned JVM build tools require Java ${neededJava} or higher, but Java ${aliasMajor} was requested.`,
883
+ });
884
+ }
885
+ // The javaNN alias install is idempotent; running it here keeps the JDK
886
+ // ahead of the build tool installs.
887
+ prepareSdkmanBuild(explicitJavaType);
888
+ return Math.max(aliasMajor || 0, neededJava || 0);
889
+ }
890
+ const currentJava = collectJavaInfo(filePath || process.cwd());
891
+ const currentMajor = currentJava?.version
892
+ ? extractJavaMajor(currentJava.version)
893
+ : undefined;
894
+ const targetMajor = determineTargetJavaMajor(
895
+ knownToolVersions,
896
+ javaVersionHint,
897
+ );
898
+ if (currentMajor !== undefined && currentMajor >= targetMajor) {
899
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
900
+ ecosystem: "java",
901
+ tool: "java",
902
+ wanted: `${targetMajor}`,
903
+ found: `${currentMajor}`,
904
+ source: "env",
905
+ detail: `The active Java ${currentMajor} satisfies the Java ${targetMajor}+ requirement of the pinned build tools.`,
906
+ });
907
+ return currentMajor;
908
+ }
909
+ const javaAlias = pickJavaAliasForMajor(targetMajor);
910
+ if (!javaAlias) {
911
+ console.log(
912
+ `WARNING: Java ${targetMajor} or higher is required to run the pinned JVM build tools, and it could not be provisioned automatically. Install a suitable JDK or pass a version type such as -t java${targetMajor}.`,
913
+ );
914
+ recordJvmRequirementShortfall(targetMajor, currentMajor);
915
+ return currentMajor;
916
+ }
917
+ console.log(
918
+ `The pinned JVM build tools need Java ${targetMajor} or higher.${currentMajor ? ` The current Java ${currentMajor} is older.` : ""} Installing ${SDKMAN_JAVA_TOOL_ALIASES[javaAlias]} with sdkman.`,
919
+ );
920
+ recordJvmRequirementShortfall(targetMajor, currentMajor);
921
+ prepareSdkmanBuild(javaAlias);
922
+ return targetMajor;
923
+ }
924
+
925
+ /**
926
+ * Record the JDK shortfall the pinned build tools run into: a mismatch when a
927
+ * live JDK was found and is too old, a missing tool when no JDK answered the
928
+ * probe at all.
929
+ *
930
+ * @param {Number} targetMajor Required JDK major
931
+ * @param {Number} currentMajor Active JDK major, when one was found
932
+ */
933
+ function recordJvmRequirementShortfall(targetMajor, currentMajor) {
934
+ if (currentMajor === undefined) {
935
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
936
+ ecosystem: "java",
937
+ tool: "java",
938
+ wanted: `${targetMajor}`,
939
+ source: "env",
940
+ detail: `No active JDK answered the version probe, and the pinned build tools need Java ${targetMajor} or higher.`,
941
+ });
942
+ return;
943
+ }
944
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
945
+ ecosystem: "java",
946
+ tool: "java",
947
+ wanted: `${targetMajor}`,
948
+ found: `${currentMajor}`,
949
+ source: "env",
950
+ detail: `The pinned build tools need Java ${targetMajor} or higher; the active Java ${currentMajor} is older.`,
951
+ });
952
+ }
953
+
954
+ /**
955
+ * Pick the closest `javaNN` alias covering a required JDK major.
956
+ *
957
+ * @param {Number} major Required JDK major
958
+ *
959
+ * @returns {String|undefined} Alias such as `java17`.
960
+ */
961
+ export function pickJavaAliasForMajor(major) {
962
+ if (SDKMAN_JAVA_TOOL_ALIASES[`java${major}`]) {
963
+ return `java${major}`;
964
+ }
965
+ const knownMajors = Object.keys(SDKMAN_JAVA_TOOL_ALIASES)
966
+ .map((pt) => Number.parseInt(pt.replace("java", ""), 10))
967
+ .filter((m) => !Number.isNaN(m) && m >= major)
968
+ .sort((a, b) => a - b);
969
+ return knownMajors.length ? `java${knownMajors[0]}` : undefined;
970
+ }
971
+
972
+ /**
973
+ * Warn when the Gradle version in play cannot run on the effective JDK.
974
+ *
975
+ * @param {Array} knownToolVersions Tool versions that will run
976
+ * @param {Number} javaMajor Effective JDK major
977
+ */
978
+ export function warnGradleJavaIncompatibility(knownToolVersions, javaMajor) {
979
+ const gradleVersion = (knownToolVersions || []).find(
980
+ (pin) => pin.tool === "gradle",
981
+ )?.version;
982
+ if (!gradleVersion || !javaMajor) {
983
+ return;
984
+ }
985
+ const minimumGradle = minimumGradleVersionForJava(javaMajor);
986
+ if (
987
+ minimumGradle &&
988
+ compareSdkmanVersions(gradleVersion, minimumGradle) < 0
989
+ ) {
990
+ console.log(
991
+ `WARNING: Gradle ${gradleVersion} cannot run on Java ${javaMajor}; Gradle ${minimumGradle} or higher is required. Pass a compatible JDK type such as -t java17, or update the project's Gradle wrapper.`,
992
+ );
993
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
994
+ ecosystem: "java",
995
+ tool: "gradle",
996
+ wanted: minimumGradle,
997
+ found: gradleVersion,
998
+ source: "env",
999
+ detail: `Gradle ${gradleVersion} cannot run on Java ${javaMajor}; Gradle ${minimumGradle} or higher is required.`,
1000
+ });
1001
+ }
1002
+ }
1003
+
1004
+ /**
1005
+ * Install a pinned tool unless it is already available, applying the failure
1006
+ * policy used by the ruby preparation: continue with whatever exists unless
1007
+ * both `deep` and `failOnError` are set.
1008
+ *
1009
+ * @param {Object} pin `{ tool, version, source }`
1010
+ * @param {Object} options CLI options
1011
+ *
1012
+ * @returns {Boolean} True when the exact version is active.
1013
+ */
1014
+ export function provisionJvmTool(pin, options) {
1015
+ if (!isSdkmanAvailable()) {
1016
+ if (isJvmToolUsable(pin.tool)) {
1017
+ console.log(
1018
+ `sdkman is unavailable, but a working ${pin.tool} command exists on PATH. Continuing with it instead of ${pin.version || "the pinned version"}.`,
1019
+ );
1020
+ return false;
1021
+ }
1022
+ console.log(
1023
+ "Install sdkman by following the instructions at https://sdkman.io/install",
1024
+ );
1025
+ if (options.deep) {
1026
+ deferFailOnError(options, {
1027
+ ecosystem: "java",
1028
+ tool: pin.tool,
1029
+ detail:
1030
+ "sdkman is not installed, so the declared JVM tool pin could not be provisioned",
1031
+ });
1032
+ }
1033
+ return false;
1034
+ }
1035
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1036
+ ecosystem: "java",
1037
+ tool: pin.tool,
1038
+ command: `sdk install ${pin.tool} ${pin.version}`,
1039
+ detail: `Installing ${pin.tool} ${pin.version} with sdkman.`,
1040
+ });
1041
+ if (!installSdkmanTool(pin.tool, pin.version)) {
1042
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1043
+ ecosystem: "java",
1044
+ tool: pin.tool,
1045
+ command: `sdk install ${pin.tool} ${pin.version}`,
1046
+ detail: `The sdkman install of ${pin.tool} ${pin.version} did not succeed.`,
1047
+ });
1048
+ if (options.deep) {
1049
+ deferFailOnError(options, {
1050
+ ecosystem: "java",
1051
+ tool: pin.tool,
1052
+ detail: `the sdkman install of ${pin.tool} ${pin.version} did not succeed`,
1053
+ });
1054
+ }
1055
+ return false;
1056
+ }
1057
+ return true;
1058
+ }
1059
+
1060
+ /**
1061
+ * Point the tool command environment variables at the freshly provisioned
1062
+ * binary so the existing resolvers pick it up deterministically.
1063
+ *
1064
+ * @param {Object} pin `{ tool, version }`
1065
+ */
1066
+ export function setJvmToolCommandEnv(pin) {
1067
+ const toolBinary = JVM_TOOL_BINARIES[pin.tool];
1068
+ if (!toolBinary?.env) {
1069
+ // scala is driven through SCALA_HOME, which the installer already set.
1070
+ return;
1071
+ }
1072
+ const toolHome = readEnvironmentVariable(`${pin.tool.toUpperCase()}_HOME`);
1073
+ if (!toolHome) {
1074
+ return;
1075
+ }
1076
+ const toolBin = join(
1077
+ toolHome,
1078
+ "bin",
1079
+ toolBinary[isWin ? "windowsCommand" : "command"],
1080
+ );
1081
+ if (safeExistsSync(toolBin)) {
1082
+ process.env[toolBinary.env] = toolBin;
1083
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
1084
+ ecosystem: "java",
1085
+ tool: pin.tool,
1086
+ wanted: pin.version,
1087
+ found: pin.version,
1088
+ source: "sdkman",
1089
+ path: toolBin,
1090
+ detail: `The ${pin.tool} command environment now points at the provisioned ${pin.version}.`,
1091
+ });
1092
+ }
1093
+ }
1094
+
79
1095
  /**
80
1096
  * Method to check and prepare the environment for python
81
1097
  *
@@ -84,6 +1100,18 @@ export function prepareSdkmanBuild(projectType) {
84
1100
  */
85
1101
  export function preparePythonEnv(_filePath, options) {
86
1102
  if (hasAnyProjectType(["python"], options, false)) {
1103
+ if (
1104
+ isLedgerEnabled() &&
1105
+ !readEnvironmentVariable("VIRTUAL_ENV") &&
1106
+ !readEnvironmentVariable("CONDA_PREFIX")
1107
+ ) {
1108
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
1109
+ ecosystem: "python",
1110
+ detail:
1111
+ "No active virtualenv or conda environment was detected; dependency resolution will use the system interpreter.",
1112
+ impact: LEDGER_EVENT_IMPACTS.COMPONENTS,
1113
+ });
1114
+ }
87
1115
  if (
88
1116
  DEBUG_MODE &&
89
1117
  readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true" &&
@@ -136,6 +1164,7 @@ export function preparePythonEnv(_filePath, options) {
136
1164
  */
137
1165
  export function prepareNodeEnv(filePath, options) {
138
1166
  // check tool for windows
1167
+ let npmInstallAttempted = false;
139
1168
  for (const pt of options.projectType) {
140
1169
  const nodeVersion = pt.replace(/\D/g, "");
141
1170
  if (
@@ -177,6 +1206,7 @@ export function prepareNodeEnv(filePath, options) {
177
1206
  }
178
1207
  if (nvmNodePath) {
179
1208
  doNpmInstall(filePath, nvmNodePath);
1209
+ npmInstallAttempted = true;
180
1210
  } else {
181
1211
  console.log(
182
1212
  `"node version ${nodeVersion} was not found. Please install it with 'nvm install ${nodeVersion}"`,
@@ -199,8 +1229,39 @@ export function prepareNodeEnv(filePath, options) {
199
1229
  continue;
200
1230
  }
201
1231
  doNpmInstall(filePath, nvmNodePath);
1232
+ npmInstallAttempted = true;
202
1233
  }
203
1234
  }
1235
+ recordNodeModulesEvidence(filePath, npmInstallAttempted);
1236
+ }
1237
+
1238
+ /**
1239
+ * Record when the scanned project has no node_modules tree and no npm install
1240
+ * was attempted during preparation, since installed-package evidence will be
1241
+ * unavailable to the generation step.
1242
+ *
1243
+ * @param {String} filePath Project path
1244
+ * @param {Boolean} npmInstallAttempted Whether an npm install ran
1245
+ */
1246
+ function recordNodeModulesEvidence(filePath, npmInstallAttempted) {
1247
+ if (!isLedgerEnabled() || !filePath || npmInstallAttempted) {
1248
+ return;
1249
+ }
1250
+ // Node preparation runs for every project type, so a missing node_modules
1251
+ // only says something about a project that declares npm dependencies at all.
1252
+ if (!safeExistsSync(join(filePath, "package.json"))) {
1253
+ return;
1254
+ }
1255
+ if (safeExistsSync(join(filePath, "node_modules"))) {
1256
+ return;
1257
+ }
1258
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
1259
+ ecosystem: "npm",
1260
+ remediationId: "js.no-node-modules",
1261
+ detail:
1262
+ "node_modules is absent and no npm install was attempted, so installed-package evidence will be unavailable.",
1263
+ impact: LEDGER_EVENT_IMPACTS.TRANSITIVE_DEPS,
1264
+ });
204
1265
  }
205
1266
 
206
1267
  /**
@@ -254,6 +1315,13 @@ export function doNpmInstall(filePath, nvmNodePath) {
254
1315
  if (isSecureMode) {
255
1316
  installArgs = `${installArgs} --ignore-scripts --no-audit`;
256
1317
  }
1318
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1319
+ ecosystem: "npm",
1320
+ tool: "npm",
1321
+ command: `npm ${installCommand} ${installArgs}`,
1322
+ path: filePath,
1323
+ detail: "Installing the project dependencies with npm.",
1324
+ });
257
1325
  const resultNpmInstall = safeSpawnSync(
258
1326
  readEnvironmentVariable("SHELL") || "bash",
259
1327
  // The nvm node directory is already prepended to PATH below
@@ -271,6 +1339,18 @@ export function doNpmInstall(filePath, nvmNodePath) {
271
1339
 
272
1340
  if (resultNpmInstall.status !== 0 || resultNpmInstall.error) {
273
1341
  // There was some problem with NpmInstall
1342
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1343
+ ecosystem: "npm",
1344
+ tool: "npm",
1345
+ command: `npm ${installCommand} ${installArgs}`,
1346
+ path: filePath,
1347
+ exitCode:
1348
+ typeof resultNpmInstall.status === "number"
1349
+ ? resultNpmInstall.status
1350
+ : undefined,
1351
+ detail: "The npm install did not succeed.",
1352
+ outputExcerpt: commandOutputText(resultNpmInstall),
1353
+ });
274
1354
  if (DEBUG_MODE) {
275
1355
  if (resultNpmInstall.stdout) {
276
1356
  console.log(resultNpmInstall.stdout);
@@ -356,6 +1436,23 @@ export function prepareSwiftEnv(filePath, options) {
356
1436
  console.log(
357
1437
  "The Swift package command did not yield the expected result. Build this project manually before invoking cdxgen.",
358
1438
  );
1439
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1440
+ ecosystem: "swift",
1441
+ tool: "swift",
1442
+ command: "swift package -v resolve",
1443
+ path: basePath,
1444
+ remediationId: "swift.package-command",
1445
+ detail:
1446
+ "The swift package resolve command did not produce a usable result.",
1447
+ });
1448
+ recordLedgerEvent(LEDGER_EVENT_KINDS.FALLBACK_ENGAGED, {
1449
+ ecosystem: "swift",
1450
+ tool: "swift",
1451
+ remediationId: "swift.package-command",
1452
+ impact: LEDGER_EVENT_IMPACTS.TRANSITIVE_DEPS,
1453
+ detail:
1454
+ "Swift dependencies will be read from the manifest alone because package resolution did not yield a Package.resolved file.",
1455
+ });
359
1456
  }
360
1457
  if (!safeExistsSync(resolvedFile)) {
361
1458
  console.log(
@@ -425,6 +1522,15 @@ export function prepareRubyEnv(filePath, options) {
425
1522
  })
426
1523
  .trim()
427
1524
  .replace("ruby-", "");
1525
+ if (rubyVersionNeeded) {
1526
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
1527
+ ecosystem: "ruby",
1528
+ tool: "ruby",
1529
+ wanted: rubyVersionNeeded,
1530
+ source: ".ruby-version",
1531
+ path: join(filePath, ".ruby-version"),
1532
+ });
1533
+ }
428
1534
  } else if (safeExistsSync(join(filePath, "Gemfile.lock"))) {
429
1535
  // Is there a lock file that can be used to identify the needed Ruby version?
430
1536
  const gemlockData = readFileSync(join(filePath, "Gemfile.lock"), {
@@ -448,6 +1554,23 @@ export function prepareRubyEnv(filePath, options) {
448
1554
  }
449
1555
  }
450
1556
  }
1557
+ if (rubyVersionNeeded) {
1558
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
1559
+ ecosystem: "ruby",
1560
+ tool: "ruby",
1561
+ wanted: rubyVersionNeeded,
1562
+ source: "Gemfile.lock",
1563
+ });
1564
+ }
1565
+ const bundlerVersion = parseGemfileLockBundlerVersion(gemlockData);
1566
+ if (bundlerVersion) {
1567
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
1568
+ ecosystem: "ruby",
1569
+ tool: "bundler",
1570
+ wanted: bundlerVersion,
1571
+ source: "Gemfile.lock",
1572
+ });
1573
+ }
451
1574
  } else {
452
1575
  // Is the user invoking with a ruby with custom version type. Eg: -t ruby2.5.4
453
1576
  let projectTypes = options.projectType;
@@ -468,6 +1591,14 @@ export function prepareRubyEnv(filePath, options) {
468
1591
  break;
469
1592
  }
470
1593
  }
1594
+ if (rubyVersionNeeded) {
1595
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
1596
+ ecosystem: "ruby",
1597
+ tool: "ruby",
1598
+ wanted: rubyVersionNeeded,
1599
+ source: "project-type",
1600
+ });
1601
+ }
471
1602
  }
472
1603
  // Do we already have this version
473
1604
  const existingRuby = collectRubyInfo(filePath);
@@ -475,6 +1606,14 @@ export function prepareRubyEnv(filePath, options) {
475
1606
  rubyVersionNeeded &&
476
1607
  existingRuby?.version?.startsWith(`ruby ${rubyVersionNeeded} `)
477
1608
  ) {
1609
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
1610
+ ecosystem: "ruby",
1611
+ tool: "ruby",
1612
+ wanted: rubyVersionNeeded,
1613
+ found: existingRuby.version,
1614
+ source: "env",
1615
+ detail: `The active interpreter is the required Ruby ${rubyVersionNeeded}.`,
1616
+ });
478
1617
  if (DEBUG_MODE) {
479
1618
  console.log(`Required Ruby version ${rubyVersionNeeded} is present.`);
480
1619
  }
@@ -493,6 +1632,26 @@ export function prepareRubyEnv(filePath, options) {
493
1632
  return;
494
1633
  }
495
1634
  }
1635
+ } else if (rubyVersionNeeded && !existingRuby?.version) {
1636
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
1637
+ ecosystem: "ruby",
1638
+ tool: "ruby",
1639
+ wanted: rubyVersionNeeded,
1640
+ source: "env",
1641
+ detail: `No Ruby interpreter answered the version probe, and Ruby ${rubyVersionNeeded} is required.`,
1642
+ });
1643
+ } else if (
1644
+ rubyVersionNeeded &&
1645
+ !existingRuby.version.startsWith(`ruby ${rubyVersionNeeded} `)
1646
+ ) {
1647
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
1648
+ ecosystem: "ruby",
1649
+ tool: "ruby",
1650
+ wanted: rubyVersionNeeded,
1651
+ found: existingRuby.version,
1652
+ source: "env",
1653
+ detail: `Ruby ${rubyVersionNeeded} is required and the active interpreter reports ${existingRuby.version}.`,
1654
+ });
496
1655
  }
497
1656
  if (rubyVersionNeeded && !rbenvPresent) {
498
1657
  console.log(
@@ -618,7 +1777,13 @@ export function prepareRubyEnv(filePath, options) {
618
1777
  }
619
1778
  } else {
620
1779
  console.log(`Ruby install has failed for version ${rubyVersionNeeded}.`);
621
- options.deep && options.failOnError && process.exit(1);
1780
+ if (options.deep) {
1781
+ deferFailOnError(options, {
1782
+ ecosystem: "ruby",
1783
+ tool: "ruby",
1784
+ detail: `the ruby ${rubyVersionNeeded} install failed`,
1785
+ });
1786
+ }
622
1787
  }
623
1788
  } else {
624
1789
  // Just attempt bundle install