@cdxgen/cdxgen 13.0.1 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -1,22 +1,43 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { chmodSync } from "node:fs";
2
+ import { chmodSync, readFileSync } from "node:fs";
3
3
  import { homedir, platform } from "node:os";
4
4
  import { basename, join, resolve } from "node:path";
5
5
 
6
6
  import { build } from "@cdxgen/cdx-purl";
7
+ import * as toml from "smol-toml";
7
8
 
8
9
  import {
9
10
  DEBUG_MODE,
10
11
  readEnvironmentVariable,
11
12
  recordDecisionActivity,
12
13
  } from "../core/activity.js";
14
+ import {
15
+ commandOutputText,
16
+ isLedgerEnabled,
17
+ LEDGER_EVENT_IMPACTS,
18
+ LEDGER_EVENT_KINDS,
19
+ recordDegradation,
20
+ recordLedgerEvent,
21
+ } from "../core/buildLedger.js";
13
22
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
14
23
  import { thoughtLog } from "../core/logger.js";
15
24
  import { isWin } from "../core/paths.js";
16
25
  import { collectJarNS } from "../inventory/deps.js";
17
- import { applyPurl } from "../inventory/purl.js";
26
+ import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
27
+ import { applyPurl, tryBuildPurl } from "../inventory/purl.js";
18
28
  import { parsePkgJson } from "./parsers-js.js";
19
29
 
30
+ /**
31
+ * True when the pre-generation stage pinned an exact JVM build tool version
32
+ * from the CLI (e.g. `-t maven3.9.9`). Project wrappers must not override
33
+ * such an explicit pin.
34
+ */
35
+ function isJvmToolPinned() {
36
+ return ["true", "1"].includes(
37
+ readEnvironmentVariable("CDXGEN_JVM_TOOL_PINNED"),
38
+ );
39
+ }
40
+
20
41
  /**
21
42
  * Method to return the gradle command to use.
22
43
  *
@@ -25,19 +46,21 @@ import { parsePkgJson } from "./parsers-js.js";
25
46
  */
26
47
  export function getGradleCommand(srcPath, rootPath) {
27
48
  let gradleCmd = "gradle";
49
+ const pinnedToolSelected = isJvmToolPinned();
28
50
 
29
51
  let findGradleFile = "gradlew";
30
52
  if (platform() === "win32") {
31
53
  findGradleFile = "gradlew.bat";
32
54
  }
33
55
 
34
- if (safeExistsSync(join(srcPath, findGradleFile))) {
56
+ if (!pinnedToolSelected && safeExistsSync(join(srcPath, findGradleFile))) {
35
57
  // Use local gradle wrapper if available
36
58
  // Enable execute permission
37
59
  try {
38
60
  chmodSync(join(srcPath, findGradleFile), 0o775);
39
61
  } catch (_e) {
40
- // continue regardless of error
62
+ // The wrapper stays un-chmod'ed; the spawn below still reports any
63
+ // permission failure as a normal command failure.
41
64
  }
42
65
  gradleCmd = resolve(join(srcPath, findGradleFile));
43
66
  recordDecisionActivity(gradleCmd, {
@@ -48,12 +71,17 @@ export function getGradleCommand(srcPath, rootPath) {
48
71
  },
49
72
  reason: `Selected project-local Gradle wrapper ${gradleCmd}.`,
50
73
  });
51
- } else if (rootPath && safeExistsSync(join(rootPath, findGradleFile))) {
74
+ } else if (
75
+ !pinnedToolSelected &&
76
+ rootPath &&
77
+ safeExistsSync(join(rootPath, findGradleFile))
78
+ ) {
52
79
  // Check if the root directory has a wrapper script
53
80
  try {
54
81
  chmodSync(join(rootPath, findGradleFile), 0o775);
55
82
  } catch (_e) {
56
- // continue regardless of error
83
+ // The wrapper stays un-chmod'ed; the spawn below still reports any
84
+ // permission failure as a normal command failure.
57
85
  }
58
86
  gradleCmd = resolve(join(rootPath, findGradleFile));
59
87
  recordDecisionActivity(gradleCmd, {
@@ -69,10 +97,12 @@ export function getGradleCommand(srcPath, rootPath) {
69
97
  recordDecisionActivity(gradleCmd, {
70
98
  metadata: {
71
99
  decisionType: "path-resolution",
72
- selectedSource: "GRADLE_CMD",
100
+ selectedSource: pinnedToolSelected ? "pinned-cli" : "GRADLE_CMD",
73
101
  tool: "gradle",
74
102
  },
75
- reason: `Selected Gradle command from GRADLE_CMD (${gradleCmd}).`,
103
+ reason: pinnedToolSelected
104
+ ? `Selected Gradle command ${gradleCmd} pinned via the CLI project type.`
105
+ : `Selected Gradle command from GRADLE_CMD (${gradleCmd}).`,
76
106
  });
77
107
  } else if (readEnvironmentVariable("GRADLE_HOME")) {
78
108
  gradleCmd = join(readEnvironmentVariable("GRADLE_HOME"), "bin", "gradle");
@@ -93,6 +123,16 @@ export function getGradleCommand(srcPath, rootPath) {
93
123
  },
94
124
  reason: "Falling back to Gradle from PATH.",
95
125
  });
126
+ // A Gradle from PATH is a sound outcome whenever its version satisfies
127
+ // the wrapper pin, so the fallback is recorded as costing nothing.
128
+ recordLedgerEvent(LEDGER_EVENT_KINDS.FALLBACK_ENGAGED, {
129
+ ecosystem: "java",
130
+ tool: "gradle",
131
+ source: "PATH",
132
+ impact: LEDGER_EVENT_IMPACTS.NONE,
133
+ detail:
134
+ "No wrapper, GRADLE_CMD, or GRADLE_HOME was found; Gradle will run from PATH.",
135
+ });
96
136
  }
97
137
  return gradleCmd;
98
138
  }
@@ -368,6 +408,9 @@ export function executeParallelGradleProperties(
368
408
  shell: isWin,
369
409
  });
370
410
  if (result.status !== 0 || result.error) {
411
+ recordGradleInvocationFailure(result, {
412
+ command: `${basename(gradleCmd)} ${gradleArg.join(" ")}`,
413
+ });
371
414
  if (readEnvironmentVariable("CDXGEN_IN_CONTAINER") === "true") {
372
415
  thoughtLog(
373
416
  "Gradle build has failed. Perhaps the user is using the wrong container image?",
@@ -420,6 +463,279 @@ export function executeParallelGradleProperties(
420
463
  return "";
421
464
  }
422
465
 
466
+ /** First class-file major a JDK maps from: class file 52 is Java 8. */
467
+ const CLASS_FILE_MAJOR_OFFSET = 44;
468
+
469
+ /**
470
+ * Causes already reported this run, so a properties invocation and a
471
+ * dependencies invocation failing for the same reason record the diagnosis
472
+ * once.
473
+ *
474
+ * @type {Set<string>}
475
+ */
476
+ const reportedGradleFailureCauses = new Set();
477
+
478
+ /** Whether an invocation failure was recorded this run. */
479
+ let gradleInvocationFailed = false;
480
+
481
+ /**
482
+ * Forget the causes diagnosed so far, so a process that generates more than
483
+ * one BOM diagnoses each run on its own.
484
+ *
485
+ * @returns {void}
486
+ */
487
+ export function resetGradleFailureCauses() {
488
+ reportedGradleFailureCauses.clear();
489
+ gradleInvocationFailed = false;
490
+ }
491
+
492
+ /**
493
+ * Whether a gradle invocation has already failed outright this run, so a
494
+ * caller can skip reporting a consequence of that failure as its own
495
+ * degradation.
496
+ *
497
+ * @returns {boolean} True once an invocation failure has been recorded.
498
+ */
499
+ export function hasGradleInvocationFailure() {
500
+ return gradleInvocationFailed;
501
+ }
502
+
503
+ /**
504
+ * Extract the Java major from an `Unsupported class file major version N`
505
+ * message: the class file the running JVM refused, which names the JDK that
506
+ * is too new for this Gradle.
507
+ *
508
+ * @param {string} text Combined gradle output.
509
+ * @returns {number|undefined} The refused Java major, when named.
510
+ */
511
+ function refusedJavaMajorFromMessage(text) {
512
+ const marker = "Unsupported class file major version ";
513
+ const start = text.indexOf(marker);
514
+ if (start === -1) {
515
+ return undefined;
516
+ }
517
+ const digits = text
518
+ .slice(start + marker.length)
519
+ .split(/\s|\.|,|\r|\n/)[0]
520
+ .trim();
521
+ const major = Number.parseInt(digits, 10);
522
+ return Number.isNaN(major) ? undefined : major - CLASS_FILE_MAJOR_OFFSET;
523
+ }
524
+
525
+ /**
526
+ * Extract the Java pair the modern launcher refusal names: the JVM major
527
+ * Gradle demands and, when the message says so, the one the build is
528
+ * configured to use. Anything the message does not clearly name stays
529
+ * undefined — the demanded major is substituted into an install command, so
530
+ * it must be a bare Java major and nothing else.
531
+ *
532
+ * @param {string} text Combined gradle output.
533
+ * @returns {{wanted: number, found: number|undefined}|undefined} The required and running Java majors, when the refusal names them.
534
+ */
535
+ function javaPairFromLauncherDemand(text) {
536
+ const demandMarker = "Gradle requires JVM ";
537
+ const demandStart = text.indexOf(demandMarker);
538
+ if (demandStart === -1) {
539
+ return undefined;
540
+ }
541
+ const wantedDigits = text
542
+ .slice(demandStart + demandMarker.length)
543
+ .split(/\s|\.|,|\r|\n/)[0]
544
+ .trim();
545
+ const wanted = Number.parseInt(wantedDigits, 10);
546
+ if (Number.isNaN(wanted)) {
547
+ return undefined;
548
+ }
549
+ const foundMarker = "configured to use JVM ";
550
+ const foundStart = text.indexOf(foundMarker);
551
+ let found;
552
+ if (foundStart !== -1) {
553
+ const foundDigits = text
554
+ .slice(foundStart + foundMarker.length)
555
+ .split(/\s|\.|,|\r|\n/)[0]
556
+ .trim();
557
+ const parsed = Number.parseInt(foundDigits, 10);
558
+ if (!Number.isNaN(parsed)) {
559
+ found = parsed;
560
+ }
561
+ }
562
+ return { wanted, found };
563
+ }
564
+
565
+ /**
566
+ * Extract the Java pair an `UnsupportedClassVersionError` names: the class
567
+ * file version that was compiled for a newer JDK and the version the running
568
+ * JDK recognises, so the event can say "needs Java A, found Java B".
569
+ *
570
+ * @param {string} text Combined gradle output.
571
+ * @returns {{wanted: number, found: number}|undefined} The required and running Java majors.
572
+ */
573
+ function javaPairFromClassVersionError(text) {
574
+ if (!text.includes("UnsupportedClassVersionError")) {
575
+ return undefined;
576
+ }
577
+ const compiledMarker = "class file version ";
578
+ const compiledStart = text.indexOf(compiledMarker);
579
+ const limitMarker = "recognizes class file versions up to ";
580
+ const limitStart = text.indexOf(limitMarker);
581
+ if (compiledStart === -1 || limitStart === -1) {
582
+ return { wanted: Number.NaN, found: Number.NaN };
583
+ }
584
+ const wanted = Number.parseInt(
585
+ text.slice(compiledStart + compiledMarker.length).trim(),
586
+ 10,
587
+ );
588
+ const found = Number.parseInt(
589
+ text.slice(limitStart + limitMarker.length).trim(),
590
+ 10,
591
+ );
592
+ return {
593
+ wanted: wanted - CLASS_FILE_MAJOR_OFFSET,
594
+ found: found - CLASS_FILE_MAJOR_OFFSET,
595
+ };
596
+ }
597
+
598
+ /**
599
+ * Record why a gradle invocation failed. Every failure also records the
600
+ * `jvm.gradle.invocation-failed` degradation so the reflection can rank the
601
+ * repair; version-incompatibility causes additionally record a
602
+ * `tool.mismatch` event naming the versions, which is the fact the report
603
+ * cannot derive from an empty BOM alone. Diagnosis events are recorded once
604
+ * per cause per run; the degradation dedupes in the reflection by id.
605
+ *
606
+ * @param {Object} result The spawnSync result of the failed invocation.
607
+ * @param {Object} context Call-site facts.
608
+ * @param {string} [context.command] The redacted command that was attempted.
609
+ * @param {string} [context.gradleVersion] The gradle version in play, when the caller knows it.
610
+ * @returns {void}
611
+ */
612
+ export function recordGradleInvocationFailure(result, context = {}) {
613
+ if (!isLedgerEnabled()) {
614
+ return;
615
+ }
616
+ if (!result || (result.status === 0 && !result.error)) {
617
+ return;
618
+ }
619
+ const output = `${result.stderr || ""}\n${result.stdout || ""}`;
620
+ const exitCode =
621
+ typeof result.status === "number" ? result.status : undefined;
622
+ let causeDetail = "the gradle invocation failed";
623
+
624
+ // The JDK is too new for this Gradle: the build script or the launcher
625
+ // refuses its class files. Gradle's own compatibility table names the
626
+ // minimum release that runs on the refused Java major.
627
+ const refusedJavaMajor = refusedJavaMajorFromMessage(output);
628
+ // Gradle's own modern refusal, which names the JVM it demands before any
629
+ // class file is loaded.
630
+ const launcherDemand =
631
+ refusedJavaMajor === undefined
632
+ ? javaPairFromLauncherDemand(output)
633
+ : undefined;
634
+ if (refusedJavaMajor !== undefined) {
635
+ const minimumGradle = minimumGradleVersionForJava(refusedJavaMajor);
636
+ const causeKey = `java-too-new:${refusedJavaMajor}`;
637
+ if (!reportedGradleFailureCauses.has(causeKey)) {
638
+ reportedGradleFailureCauses.add(causeKey);
639
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
640
+ ecosystem: "java",
641
+ tool: "gradle",
642
+ wanted: minimumGradle ? `${minimumGradle}` : undefined,
643
+ found: context.gradleVersion,
644
+ source: "invocation",
645
+ detail: minimumGradle
646
+ ? `Gradle cannot run on Java ${refusedJavaMajor} and refused its class files; Gradle ${minimumGradle} or higher supports it.`
647
+ : `Gradle cannot run on Java ${refusedJavaMajor} and refused its class files.`,
648
+ });
649
+ }
650
+ causeDetail = `gradle cannot run on Java ${refusedJavaMajor}`;
651
+ } else if (launcherDemand) {
652
+ // The demand names the JDK Gradle needs; the JVM that answered it is
653
+ // the one the build was configured to use.
654
+ const causeKey = `jvm-demanded:${launcherDemand.wanted}:${launcherDemand.found}`;
655
+ if (!reportedGradleFailureCauses.has(causeKey)) {
656
+ reportedGradleFailureCauses.add(causeKey);
657
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
658
+ ecosystem: "java",
659
+ tool: "java",
660
+ wanted: `${launcherDemand.wanted}`,
661
+ found:
662
+ launcherDemand.found === undefined
663
+ ? undefined
664
+ : `${launcherDemand.found}`,
665
+ source: "invocation",
666
+ detail:
667
+ launcherDemand.found === undefined
668
+ ? `Gradle requires Java ${launcherDemand.wanted} or later to run.`
669
+ : `Gradle requires Java ${launcherDemand.wanted} or later to run; the active JVM is Java ${launcherDemand.found}.`,
670
+ });
671
+ }
672
+ causeDetail = "the gradle launcher requires a newer JDK";
673
+ } else {
674
+ // The opposite skew: Gradle (or its wrapper) is compiled for a newer JDK
675
+ // than the running one, so the JVM refuses to load it.
676
+ const javaPair = javaPairFromClassVersionError(output);
677
+ if (javaPair) {
678
+ const causeKey = `jvm-too-old:${javaPair.wanted}:${javaPair.found}`;
679
+ if (!reportedGradleFailureCauses.has(causeKey)) {
680
+ reportedGradleFailureCauses.add(causeKey);
681
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
682
+ ecosystem: "java",
683
+ tool: "java",
684
+ wanted: Number.isNaN(javaPair.wanted)
685
+ ? undefined
686
+ : `${javaPair.wanted}`,
687
+ found: Number.isNaN(javaPair.found) ? undefined : `${javaPair.found}`,
688
+ source: "invocation",
689
+ detail: `The gradle launcher requires Java ${Number.isNaN(javaPair.wanted) ? "a newer release" : javaPair.wanted}; the active JDK is older and refused its class files.`,
690
+ });
691
+ }
692
+ causeDetail = "the gradle launcher requires a newer JDK";
693
+ } else if (output.includes("JAVA_HOME is set to an invalid directory")) {
694
+ const causeKey = "invalid-java-home";
695
+ if (!reportedGradleFailureCauses.has(causeKey)) {
696
+ reportedGradleFailureCauses.add(causeKey);
697
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
698
+ ecosystem: "java",
699
+ tool: "java",
700
+ source: "JAVA_HOME",
701
+ detail:
702
+ "JAVA_HOME names a directory that does not hold a JDK, so the gradle launcher refused to start.",
703
+ });
704
+ }
705
+ causeDetail = "JAVA_HOME does not point at a JDK";
706
+ } else if (
707
+ output.includes("Could not start Gradle Daemon") ||
708
+ output.includes("Daemon could not be started") ||
709
+ output.includes("could not install Gradle distribution")
710
+ ) {
711
+ const causeKey = "daemon-start";
712
+ if (!reportedGradleFailureCauses.has(causeKey)) {
713
+ reportedGradleFailureCauses.add(causeKey);
714
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
715
+ ecosystem: "java",
716
+ tool: "gradle",
717
+ impact: LEDGER_EVENT_IMPACTS.VERSIONS,
718
+ detail:
719
+ "The gradle daemon did not start, so no dependency resolution ran.",
720
+ });
721
+ }
722
+ causeDetail = "the gradle daemon did not start";
723
+ }
724
+ }
725
+
726
+ gradleInvocationFailed = true;
727
+ recordDegradation("jvm.gradle.invocation-failed", {
728
+ ecosystem: "java",
729
+ tool: "gradle",
730
+ impact: LEDGER_EVENT_IMPACTS.TRANSITIVE_DEPS,
731
+ command: context.command,
732
+ exitCode,
733
+ detail: causeDetail,
734
+ causeDetail,
735
+ outputExcerpt: commandOutputText(result),
736
+ });
737
+ }
738
+
423
739
  /**
424
740
  * Method to resolve dependencies from a gradle output
425
741
  *
@@ -583,6 +899,8 @@ export async function parseGradleDep(
583
899
  });
584
900
  bomRef = decodeURIComponent(purl);
585
901
  } catch {
902
+ // A purl that cannot be built keeps a name-only bom-ref so the
903
+ // component still lands in the graph.
586
904
  purl = undefined;
587
905
  bomRef = name.replace(/^:/, "");
588
906
  }
@@ -724,6 +1042,7 @@ export async function buildObjectForGradleModule(name, metadata) {
724
1042
  });
725
1043
  applyPurl(component, purl);
726
1044
  } catch {
1045
+ // The module component keeps its name identity without a purl.
727
1046
  applyPurl(component, null);
728
1047
  }
729
1048
  }
@@ -896,7 +1215,8 @@ export function getMillCommand(srcPath) {
896
1215
  try {
897
1216
  chmodSync(join(srcPath, millCmd), 0o775);
898
1217
  } catch (_e) {
899
- // continue regardless of error
1218
+ // The mill wrapper stays un-chmod'ed; the spawn below still reports any
1219
+ // permission failure as a normal command failure.
900
1220
  }
901
1221
  millCmd = resolve(join(srcPath, millCmd));
902
1222
  }
@@ -914,6 +1234,7 @@ export function getMavenCommand(srcPath, rootPath) {
914
1234
  // Check if the wrapper script is both available and functional
915
1235
  let isWrapperReady = false;
916
1236
  let isWrapperFound = false;
1237
+ const pinnedToolSelected = isJvmToolPinned();
917
1238
  let findMavenFile = "mvnw";
918
1239
  let mavenWrapperCmd = null;
919
1240
  if (platform() === "win32") {
@@ -926,13 +1247,14 @@ export function getMavenCommand(srcPath, rootPath) {
926
1247
  }
927
1248
  }
928
1249
 
929
- if (safeExistsSync(join(srcPath, findMavenFile))) {
1250
+ if (!pinnedToolSelected && safeExistsSync(join(srcPath, findMavenFile))) {
930
1251
  // Use local maven wrapper if available
931
1252
  // Enable execute permission
932
1253
  try {
933
1254
  chmodSync(join(srcPath, findMavenFile), 0o775);
934
1255
  } catch (_e) {
935
- // continue regardless of error
1256
+ // The wrapper stays un-chmod'ed; the spawn below still reports any
1257
+ // permission failure as a normal command failure.
936
1258
  }
937
1259
  mavenWrapperCmd = resolve(join(srcPath, findMavenFile));
938
1260
  isWrapperFound = true;
@@ -944,12 +1266,17 @@ export function getMavenCommand(srcPath, rootPath) {
944
1266
  },
945
1267
  reason: `Found Maven wrapper candidate ${mavenWrapperCmd}.`,
946
1268
  });
947
- } else if (rootPath && safeExistsSync(join(rootPath, findMavenFile))) {
1269
+ } else if (
1270
+ !pinnedToolSelected &&
1271
+ rootPath &&
1272
+ safeExistsSync(join(rootPath, findMavenFile))
1273
+ ) {
948
1274
  // Check if the root directory has a wrapper script
949
1275
  try {
950
1276
  chmodSync(join(rootPath, findMavenFile), 0o775);
951
1277
  } catch (_e) {
952
- // continue regardless of error
1278
+ // The wrapper stays un-chmod'ed; the spawn below still reports any
1279
+ // permission failure as a normal command failure.
953
1280
  }
954
1281
  mavenWrapperCmd = resolve(join(rootPath, findMavenFile));
955
1282
  isWrapperFound = true;
@@ -1016,12 +1343,16 @@ export function getMavenCommand(srcPath, rootPath) {
1016
1343
  recordDecisionActivity(mavenCmd, {
1017
1344
  metadata: {
1018
1345
  decisionType: "path-resolution",
1019
- selectedSource: readEnvironmentVariable("MVN_CMD")
1020
- ? "MVN_CMD"
1021
- : "MAVEN_CMD",
1346
+ selectedSource: pinnedToolSelected
1347
+ ? "pinned-cli"
1348
+ : readEnvironmentVariable("MVN_CMD")
1349
+ ? "MVN_CMD"
1350
+ : "MAVEN_CMD",
1022
1351
  tool: "maven",
1023
1352
  },
1024
- reason: `Selected Maven command from environment (${mavenCmd}).`,
1353
+ reason: pinnedToolSelected
1354
+ ? `Selected Maven command ${mavenCmd} pinned via the CLI project type.`
1355
+ : `Selected Maven command from environment (${mavenCmd}).`,
1025
1356
  });
1026
1357
  } else if (readEnvironmentVariable("MAVEN_HOME")) {
1027
1358
  mavenCmd = join(readEnvironmentVariable("MAVEN_HOME"), "bin", "mvn");
@@ -1042,7 +1373,157 @@ export function getMavenCommand(srcPath, rootPath) {
1042
1373
  },
1043
1374
  reason: "Falling back to Maven from PATH.",
1044
1375
  });
1376
+ // A Maven from PATH is a sound outcome whenever its version satisfies
1377
+ // the wrapper pin, so the fallback is recorded as costing nothing.
1378
+ recordLedgerEvent(LEDGER_EVENT_KINDS.FALLBACK_ENGAGED, {
1379
+ ecosystem: "java",
1380
+ tool: "maven",
1381
+ source: "PATH",
1382
+ impact: LEDGER_EVENT_IMPACTS.NONE,
1383
+ detail:
1384
+ "No wrapper, MVN_CMD/MAVEN_CMD, or MAVEN_HOME was found; Maven will run from PATH.",
1385
+ });
1045
1386
  }
1046
1387
  }
1047
1388
  return mavenCmd;
1048
1389
  }
1390
+
1391
+ /**
1392
+ * Parse a Gradle version catalog (`gradle/libs.versions.toml`).
1393
+ *
1394
+ * A catalog declares coordinates the build scripts then reference by alias.
1395
+ * It is a declaration, not a resolution: entries may go unused, and their
1396
+ * versions may be references into the `[versions]` table. Because of that,
1397
+ * catalog components are emitted only as a fallback when Gradle itself
1398
+ * produced no dependency information, and every component carries a
1399
+ * `cdx:gradle:catalog=true` property so consumers can tell declared catalog
1400
+ * entries from a resolved graph.
1401
+ *
1402
+ * Only libraries whose version resolves to a literal (inline or via
1403
+ * `version.ref`) are emitted; rich or unresolved versions would produce purls
1404
+ * that identify no artifact. Plugins are skipped because they are build-time
1405
+ * only.
1406
+ *
1407
+ * @param {string} catalogFile Path to `libs.versions.toml`
1408
+ * @returns {Object[]} Package records for resolvable library entries
1409
+ */
1410
+ export function parseGradleVersionCatalog(catalogFile) {
1411
+ let catalog;
1412
+ try {
1413
+ catalog = toml.parse(readFileSync(catalogFile, "utf-8"));
1414
+ } catch (error) {
1415
+ console.warn(`Failed to parse ${catalogFile}: ${error.message}`);
1416
+ return [];
1417
+ }
1418
+ const versions =
1419
+ catalog?.versions && typeof catalog.versions === "object"
1420
+ ? catalog.versions
1421
+ : {};
1422
+ const libraries =
1423
+ catalog?.libraries && typeof catalog.libraries === "object"
1424
+ ? catalog.libraries
1425
+ : {};
1426
+ const pkgList = [];
1427
+ for (const entry of Object.values(libraries)) {
1428
+ const coordinate = catalogCoordinate(entry);
1429
+ if (!coordinate) {
1430
+ continue;
1431
+ }
1432
+ const version = resolveCatalogVersion(entry, versions);
1433
+ if (!version) {
1434
+ continue;
1435
+ }
1436
+ const { group, name } = coordinate;
1437
+ // A catalog is authored input, but a malformed coordinate must cost the
1438
+ // one entry, not the whole scan.
1439
+ const purl = tryBuildPurl({
1440
+ type: "maven",
1441
+ namespace: group,
1442
+ name,
1443
+ version,
1444
+ });
1445
+ if (!purl) {
1446
+ continue;
1447
+ }
1448
+ pkgList.push({
1449
+ group,
1450
+ name,
1451
+ version,
1452
+ type: "library",
1453
+ scope: "required",
1454
+ purl,
1455
+ "bom-ref": decodeURIComponent(purl),
1456
+ properties: [
1457
+ { name: "internal:SrcFile", value: catalogFile },
1458
+ { name: "cdx:gradle:catalog", value: "true" },
1459
+ ],
1460
+ evidence: {
1461
+ identity: {
1462
+ field: "purl",
1463
+ confidence: 0.5,
1464
+ methods: [
1465
+ {
1466
+ technique: "source-code-analysis",
1467
+ confidence: 0.5,
1468
+ value: `Filename ${catalogFile}`,
1469
+ },
1470
+ ],
1471
+ },
1472
+ },
1473
+ });
1474
+ }
1475
+ return pkgList;
1476
+ }
1477
+
1478
+ /**
1479
+ * Read the Maven coordinate of one catalog entry.
1480
+ *
1481
+ * A catalog writes the coordinate either as `module = "group:name"` or as a
1482
+ * separate `group` and `name` pair; both forms are equivalent and both appear
1483
+ * in real catalogs.
1484
+ *
1485
+ * @param {object} entry Catalog library entry
1486
+ * @returns {{group: string, name: string}|undefined} Coordinate, or undefined
1487
+ */
1488
+ function catalogCoordinate(entry) {
1489
+ const module = typeof entry?.module === "string" ? entry.module : undefined;
1490
+ if (module?.includes(":")) {
1491
+ const separator = module.indexOf(":");
1492
+ return {
1493
+ group: module.slice(0, separator),
1494
+ name: module.slice(separator + 1),
1495
+ };
1496
+ }
1497
+ if (typeof entry?.group === "string" && typeof entry?.name === "string") {
1498
+ return { group: entry.group, name: entry.name };
1499
+ }
1500
+ return undefined;
1501
+ }
1502
+
1503
+ /**
1504
+ * Resolve the version of one catalog entry.
1505
+ *
1506
+ * An entry may pin `version = "1.2.3"` directly or reference the versions
1507
+ * table with `version.ref = "alias"`. Version references that are missing, or
1508
+ * that carry Gradle rich-version syntax (ranges, `!!`, separators), resolve
1509
+ * to nothing and the entry is skipped.
1510
+ *
1511
+ * @param {object} entry Catalog library entry
1512
+ * @param {object} versions Parsed `[versions]` table
1513
+ * @returns {string|undefined} Concrete version, or undefined
1514
+ */
1515
+ function resolveCatalogVersion(entry, versions) {
1516
+ const raw =
1517
+ typeof entry?.version === "string"
1518
+ ? entry.version
1519
+ : typeof entry?.version?.ref === "string"
1520
+ ? versions[entry.version.ref]
1521
+ : undefined;
1522
+ if (typeof raw !== "string" || !raw) {
1523
+ return undefined;
1524
+ }
1525
+ if (/[[\](),!]|->|\.\./u.test(raw)) {
1526
+ return undefined;
1527
+ }
1528
+ return raw;
1529
+ }