@cdxgen/cdxgen 13.4.0 → 13.6.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -165,7 +165,18 @@ function hasProperty(obj, propName, expectedValue) {
165
165
  if (expectedValue === undefined) {
166
166
  return value !== null;
167
167
  }
168
- return value === String(expectedValue);
168
+ // A repeatable property (cdx:crypto:sourceType on a certificate found by
169
+ // several producers) matches when any of its values does.
170
+ return extractPropertyValues(obj, propName).includes(String(expectedValue));
171
+ }
172
+
173
+ function extractPropertyValues(obj, propName) {
174
+ if (!Array.isArray(obj?.properties)) {
175
+ return [];
176
+ }
177
+ return obj.properties
178
+ .filter((p) => p?.name === propName && p.value !== undefined)
179
+ .map((p) => String(p.value));
169
180
  }
170
181
 
171
182
  /**
@@ -315,6 +326,9 @@ function registerCdxHelpers(expression, helpers = {}) {
315
326
  expression.registerFunction("prop", (obj, propName) =>
316
327
  extractProperty(obj, propName),
317
328
  );
329
+ expression.registerFunction("propValues", (obj, propName) =>
330
+ extractPropertyValues(obj, propName),
331
+ );
318
332
  expression.registerFunction("nullSafeProp", (obj, propName) => {
319
333
  const value = extractProperty(obj, propName);
320
334
  return value === null ? "" : value;
@@ -385,6 +399,13 @@ function registerCdxHelpers(expression, helpers = {}) {
385
399
  expression.registerFunction("safeStr", (val) => {
386
400
  return val === null || val === undefined ? "" : String(val).trim();
387
401
  });
402
+ // End of validity in epoch milliseconds. A date-only value (2026-01-31)
403
+ // is valid through the end of that day in UTC; a full RFC 3339 timestamp
404
+ // is taken as is; anything else, or an impossible date, gives no value so
405
+ // the rule skips the component.
406
+ expression.registerFunction("certEndMillis", (val) =>
407
+ parseCertificateEndMillis(val),
408
+ );
388
409
  expression.registerFunction("parseSizeBytes", (val) => {
389
410
  if (val === null || val === undefined || val === "") {
390
411
  return null;
@@ -519,6 +540,42 @@ function registerCdxHelpers(expression, helpers = {}) {
519
540
  * @param {string} rulesDir - Path to directory containing .yaml rule files
520
541
  * @returns {Promise<Array>} Array of parsed rule objects
521
542
  */
543
+ const FULL_TIMESTAMP_RE =
544
+ /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?(Z|[+-]([01]\d|2[0-3]):[0-5]\d)$/;
545
+ const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
546
+
547
+ /**
548
+ * Parse a certificate notValidAfter value.
549
+ *
550
+ * @param {*} value timestamp or date-only string
551
+ * @returns {number|undefined} epoch milliseconds, or undefined when the value
552
+ * does not parse
553
+ */
554
+ export function parseCertificateEndMillis(value) {
555
+ if (typeof value !== "string") {
556
+ return undefined;
557
+ }
558
+ const text = value.trim();
559
+ const dateOnly = DATE_ONLY_RE.exec(text);
560
+ if (dateOnly) {
561
+ const [year, month, day] = dateOnly.slice(1).map(Number);
562
+ const start = new Date(Date.UTC(year, month - 1, day));
563
+ if (
564
+ start.getUTCFullYear() !== year ||
565
+ start.getUTCMonth() !== month - 1 ||
566
+ start.getUTCDate() !== day
567
+ ) {
568
+ return undefined;
569
+ }
570
+ return start.getTime() + 24 * 60 * 60 * 1000 - 1;
571
+ }
572
+ if (!FULL_TIMESTAMP_RE.test(text)) {
573
+ return undefined;
574
+ }
575
+ const millis = Date.parse(text);
576
+ return Number.isNaN(millis) ? undefined : millis;
577
+ }
578
+
522
579
  export async function loadRules(rulesDir) {
523
580
  const rules = [];
524
581
  if (!safeExistsSync(rulesDir)) {
@@ -615,16 +672,21 @@ async function interpolateTemplate(template, context, options = {}) {
615
672
  return template;
616
673
  }
617
674
  const templateRegex = /\{\{\s*([^}]+)\s*}}/g;
618
- let result = template;
619
- const matches = [...template.matchAll(templateRegex)];
620
- for (const match of matches) {
675
+ // The template is rebuilt from its own slices in one pass, so a value taken
676
+ // from the BOM is never scanned for {{ }} again and never read as a
677
+ // String.replace pattern such as `$&`.
678
+ let result = "";
679
+ let cursor = 0;
680
+ for (const match of template.matchAll(templateRegex)) {
621
681
  const [fullMatch, expr] = match;
682
+ let replacement = fullMatch;
622
683
  try {
623
684
  const expression = jsonata(expr.trim());
624
685
  registerCdxHelpers(expression, options);
625
686
  const value = await safeEvaluate(expression, context);
626
- const replacement = value !== undefined ? String(value) : fullMatch;
627
- result = result.replace(fullMatch, replacement);
687
+ if (value !== undefined) {
688
+ replacement = String(value);
689
+ }
628
690
  } catch (err) {
629
691
  if (DEBUG_MODE) {
630
692
  console.warn(
@@ -633,7 +695,10 @@ async function interpolateTemplate(template, context, options = {}) {
633
695
  );
634
696
  }
635
697
  }
698
+ result += template.slice(cursor, match.index) + replacement;
699
+ cursor = match.index + fullMatch.length;
636
700
  }
701
+ result += template.slice(cursor);
637
702
  return result;
638
703
  }
639
704
 
@@ -604,9 +604,39 @@ function downgradeSubjectForSpecVersion(subject, specVersion, parentKey) {
604
604
  certificateProperties.certificateExtension =
605
605
  certificateProperties.certificateFileExtension;
606
606
  }
607
+ // `relatedCryptographicAssets` and `certificateExtensions` are 1.7-only;
608
+ // the deprecated `subjectPublicKeyRef`/`signatureAlgorithmRef` (valid in
609
+ // 1.6, see data/bom-1.6.schema.json) carry the same links, and the
610
+ // extension facts survive as `cdx:crypto:` properties.
611
+ const relatedAssets = certificateProperties.relatedCryptographicAssets;
612
+ if (Array.isArray(relatedAssets)) {
613
+ for (const asset of relatedAssets) {
614
+ if (
615
+ asset?.type === "publicKey" &&
616
+ !certificateProperties.subjectPublicKeyRef
617
+ ) {
618
+ certificateProperties.subjectPublicKeyRef = asset.ref;
619
+ } else if (
620
+ asset?.type === "algorithm" &&
621
+ !certificateProperties.signatureAlgorithmRef
622
+ ) {
623
+ certificateProperties.signatureAlgorithmRef = asset.ref;
624
+ }
625
+ }
626
+ delete certificateProperties.relatedCryptographicAssets;
627
+ }
628
+ delete certificateProperties.certificateExtensions;
607
629
  delete certificateProperties.serialNumber;
608
630
  delete certificateProperties.certificateFileExtension;
609
631
  delete certificateProperties.fingerprint;
632
+ } else if (
633
+ specVersion < 1.7 &&
634
+ subject.cryptoProperties?.assetType === "related-crypto-material" &&
635
+ subject.cryptoProperties.relatedCryptoMaterialProperties
636
+ ) {
637
+ // The public-key `fingerprint` object is 1.7-only; the SPKI digest stays
638
+ // available as the `cdx:crypto:spkiSha256` property.
639
+ delete subject.cryptoProperties.relatedCryptoMaterialProperties.fingerprint;
610
640
  }
611
641
  if (specVersion < 1.7 && subject.cryptoProperties?.algorithmProperties) {
612
642
  // `algorithmFamily` and `ellipticCurve` are CycloneDX 1.7 additions to
@@ -110,6 +110,7 @@ const JVM_PROJECT_TYPES = new Set([
110
110
  "java24",
111
111
  "java25",
112
112
  "java26",
113
+ "java27",
113
114
  "jvm",
114
115
  "kt",
115
116
  "kotlin",
@@ -28,6 +28,15 @@ import {
28
28
  TIMEOUT_MS,
29
29
  } from "../../core/fs.js";
30
30
  import { isMac, isWin } from "../../core/paths.js";
31
+ import { isProjectToolingAllowed } from "../../core/projectTooling.js";
32
+ import { recordToolVersion } from "../../core/toolRegistry.js";
33
+ import { collectProjectRubyDeclarations } from "../../ecosystems/rubyutils.js";
34
+ import {
35
+ dosaiDotnetRequirement,
36
+ dosaiDotnetRequirementAdvice,
37
+ dosaiDotnetRequirementDetail,
38
+ markDosaiLaunchFailureReported,
39
+ } from "../../inventory/dosai.js";
31
40
  import {
32
41
  bundleInstallWithDocker,
33
42
  collectJavaInfo,
@@ -38,6 +47,7 @@ import {
38
47
  installSdkmanTool,
39
48
  isNvmAvailable,
40
49
  isRbenvAvailable,
50
+ isRubyVersionActive,
41
51
  isSdkmanAvailable,
42
52
  isSdkmanToolAvailable,
43
53
  performBundleInstall,
@@ -58,8 +68,10 @@ import {
58
68
  readSbtBuildPropertiesVersion,
59
69
  readToolVersionEnvHint,
60
70
  readToolVersionOverride,
71
+ resolveGradleVersionForJava,
61
72
  resolvePartialVersion,
62
73
  } from "../../inventory/jvmToolEnv.js";
74
+ import { resolvePluginBinary } from "../../inventory/plugins.js";
63
75
  import {
64
76
  classifyProbeResult,
65
77
  describeSpawnRestriction,
@@ -74,12 +86,20 @@ import {
74
86
  * @param {Object} options CLI options
75
87
  */
76
88
  export function prepareEnv(filePath, options) {
77
- if (!options.projectType || isSecureMode) {
89
+ if (isSecureMode) {
78
90
  return;
79
91
  }
80
92
  if (filePath) {
81
93
  filePath = resolve(filePath);
82
94
  }
95
+ // Runs before the project type check: a .NET tree is also found when cdxgen
96
+ // detects the project types itself.
97
+ if (!isDryRun) {
98
+ prepareDotnetEnv(filePath, options);
99
+ }
100
+ if (!options.projectType) {
101
+ return;
102
+ }
83
103
  // The JVM preparation provisions the JDK ahead of the build tools, so an
84
104
  // alias it already installed must not be installed a second time here.
85
105
  // It runs under dry-run too, where it reports the toolchain it would set up
@@ -104,6 +124,91 @@ export function prepareEnv(filePath, options) {
104
124
  prepareRubyEnv(filePath, options);
105
125
  }
106
126
 
127
+ /**
128
+ * Check that dosai, which deep runs use for .NET evidence, can start. The
129
+ * dosai bundled with cdxgen-plugins-bin is framework-dependent, so it needs a
130
+ * .NET runtime at least as new as the one it targets (.NET 11 from dosai
131
+ * 5.0.0). Running it lets the .NET host apply its own version rules. Skipped
132
+ * in the container images, which ship a dosai that works with their SDK, and
133
+ * when DOSAI_CMD names another build, such as the self-contained -full one.
134
+ *
135
+ * @param {String} filePath Project path
136
+ * @param {Object} options CLI options
137
+ */
138
+ export function prepareDotnetEnv(filePath, options) {
139
+ if (
140
+ !options.deep ||
141
+ !filePath ||
142
+ readEnvironmentVariable("CDXGEN_IN_CONTAINER") === "true" ||
143
+ readEnvironmentVariable("DOSAI_CMD") ||
144
+ !isDotnetInScope(filePath, options)
145
+ ) {
146
+ return;
147
+ }
148
+ const dosaiBin = resolvePluginBinary("dosai");
149
+ if (!dosaiBin) {
150
+ // A missing plugin is reported where dosai runs
151
+ return;
152
+ }
153
+ const result = safeSpawnSync(dosaiBin, ["--version"], {
154
+ encoding: "utf-8",
155
+ timeout: TIMEOUT_MS,
156
+ });
157
+ if (result?.status === 0 && !result.error) {
158
+ return;
159
+ }
160
+ const output = commandOutputText(result) || "";
161
+ const requirement = dosaiDotnetRequirement(output);
162
+ const detail = requirement
163
+ ? dosaiDotnetRequirementDetail(requirement)
164
+ : "dosai could not start";
165
+ console.log(
166
+ `WARNING: ${detail}, so this deep run has no dosai evidence for .NET (call graph, reachability, crypto). ${dosaiDotnetRequirementAdvice(requirement)}`,
167
+ );
168
+ if (requirement) {
169
+ markDosaiLaunchFailureReported(dosaiBin);
170
+ }
171
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
172
+ ecosystem: "csharp",
173
+ tool: "dosai",
174
+ detail: `${detail}; .NET evidence from dosai is unavailable.`,
175
+ impact: LEDGER_EVENT_IMPACTS.COMPONENTS,
176
+ });
177
+ deferFailOnError(options, {
178
+ ecosystem: "csharp",
179
+ tool: "dosai",
180
+ detail,
181
+ exitCode: typeof result?.status === "number" ? result.status : undefined,
182
+ command: `${dosaiBin} --version`,
183
+ outputExcerpt: output.trim() || undefined,
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Whether the run covers .NET: a .NET project type, or, when cdxgen detects
189
+ * the types itself, a .NET project or solution file in the tree.
190
+ *
191
+ * @param {String} filePath Project path
192
+ * @param {Object} options CLI options
193
+ *
194
+ * @returns {Boolean}
195
+ */
196
+ function isDotnetInScope(filePath, options) {
197
+ if (options.projectType?.length) {
198
+ return hasAnyProjectType(["csharp"], options, false);
199
+ }
200
+ if (!hasAnyProjectType(["csharp"], options, true)) {
201
+ return false;
202
+ }
203
+ return (
204
+ getAllFiles(
205
+ filePath,
206
+ `${options.multiProject ? "**/" : ""}*.{csproj,vbproj,fsproj,sln,slnx}`,
207
+ options,
208
+ ).length > 0
209
+ );
210
+ }
211
+
107
212
  /**
108
213
  * Record the tool requirements the project declares through its well-known
109
214
  * tool pin files (`.tool-versions`, `.nvmrc`, `package.json`, `go.mod`, …),
@@ -215,6 +320,27 @@ const JVM_TOOL_DEFAULT_PREFIXES = {
215
320
  maven: "3.9",
216
321
  };
217
322
 
323
+ /**
324
+ * JDK major a versioned java project type names, such as 27 for `java27`.
325
+ *
326
+ * @param {string} projectType Project type
327
+ * @returns {number|undefined} The major, or undefined for any other type
328
+ */
329
+ export function javaMajorFromProjectType(projectType) {
330
+ if (typeof projectType !== "string" || !projectType.startsWith("java")) {
331
+ return undefined;
332
+ }
333
+ const major = Number.parseInt(projectType.slice(4), 10);
334
+ return Number.isInteger(major) && `java${major}` === projectType
335
+ ? major
336
+ : undefined;
337
+ }
338
+
339
+ /** Command each provisioned sdkman candidate is invoked as. */
340
+ const JVM_TOOL_COMMANDS = {
341
+ maven: "mvn",
342
+ };
343
+
218
344
  /** sdkman candidates cdxgen can provision for JVM builds. */
219
345
  const JVM_BUILD_TOOLS = ["maven", "gradle", "sbt", "scala"];
220
346
 
@@ -299,10 +425,26 @@ export function prepareJvmBuildEnv(filePath, options) {
299
425
  return;
300
426
  }
301
427
  // Resolve partial versions (e.g. maven3.9 -> 3.9.16) before touching the JDK
302
- // so the compatibility matrix sees the real versions.
428
+ // so the compatibility matrix sees the real versions. The Gradle default
429
+ // install is constrained by the JDK the project declared or asked for, so a
430
+ // Java 27 project never gets a Gradle that cannot run on it.
431
+ // A versioned java type counts even without an sdkman alias: java27 has
432
+ // none until the broker serves a Java 27 build, but the project still asked
433
+ // for that JDK.
434
+ const explicitJavaMajor = Math.max(
435
+ 0,
436
+ ...(options.projectType || []).map((pt) =>
437
+ SDKMAN_JAVA_TOOL_ALIASES[pt]
438
+ ? extractJavaMajor(SDKMAN_JAVA_TOOL_ALIASES[pt]) || 0
439
+ : javaMajorFromProjectType(pt) || 0,
440
+ ),
441
+ );
442
+ const gradleJavaMajor =
443
+ Math.max(detected.javaVersionHint || 0, explicitJavaMajor || 0) ||
444
+ undefined;
303
445
  const resolvedPins = [];
304
446
  for (const pin of installPins) {
305
- const resolved = resolveJvmToolPinVersion(pin);
447
+ const resolved = resolveJvmToolPinVersion(pin, gradleJavaMajor);
306
448
  if (resolved) {
307
449
  resolvedPins.push(resolved);
308
450
  }
@@ -321,6 +463,11 @@ export function prepareJvmBuildEnv(filePath, options) {
321
463
  const installed = provisionJvmTool(pin, options);
322
464
  if (installed) {
323
465
  setJvmToolCommandEnv(pin);
466
+ if (pin.version) {
467
+ // The resolved version is already known, so metadata.tools can carry
468
+ // it without spawning another probe, once the tool runs (issue #598).
469
+ recordToolVersion(JVM_TOOL_COMMANDS[pin.tool] || pin.tool, pin.version);
470
+ }
324
471
  if (pin.source === "cli") {
325
472
  pinnedToolSelected = true;
326
473
  }
@@ -630,11 +777,14 @@ function recordDetectedJvmPins(installPins, wrapperVersions) {
630
777
  * and to pick defaults.
631
778
  *
632
779
  * @param {Object} pin `{ tool, version?, source }`
780
+ * @param {Number|undefined} javaMajor JDK major the project declared or
781
+ * configured, when known. Constrains the default Gradle install so a
782
+ * project on a new JDK never resolves a Gradle that cannot run on it.
633
783
  *
634
784
  * @returns {Object|undefined} Pin with an exact version, or undefined when no
635
785
  * version could be determined.
636
786
  */
637
- export function resolveJvmToolPinVersion(pin) {
787
+ export function resolveJvmToolPinVersion(pin, javaMajor) {
638
788
  if (pin.version && isSdkmanToolAvailable(pin.tool, pin.version)) {
639
789
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
640
790
  ecosystem: "java",
@@ -665,20 +815,30 @@ export function resolveJvmToolPinVersion(pin) {
665
815
  return undefined;
666
816
  }
667
817
  const prefix = pin.version || JVM_TOOL_DEFAULT_PREFIXES[pin.tool] || "";
668
- const resolved = resolvePartialVersion(versionList, prefix);
818
+ const resolved =
819
+ pin.tool === "gradle" && !pin.version
820
+ ? resolveGradleVersionForJava(versionList, javaMajor)
821
+ : resolvePartialVersion(versionList, prefix);
669
822
  if (!resolved) {
670
823
  if (pin.version) {
671
824
  return pin;
672
825
  }
826
+ const gradleMinimum =
827
+ pin.tool === "gradle"
828
+ ? minimumGradleVersionForJava(javaMajor)
829
+ : undefined;
830
+ const missingDetail = gradleMinimum
831
+ ? `No stable gradle version at or above ${gradleMinimum}, the first that runs on Java ${javaMajor}, is listed by sdkman.`
832
+ : `No stable ${pin.tool} version matched the default prefix '${prefix}'.`;
673
833
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
674
834
  ecosystem: "java",
675
835
  tool: pin.tool,
676
- wanted: prefix,
836
+ wanted: gradleMinimum ? `>=${gradleMinimum}` : prefix,
677
837
  source: "sdkman",
678
- detail: `No stable ${pin.tool} version matched the default prefix '${prefix}'.`,
838
+ detail: missingDetail,
679
839
  });
680
840
  console.log(
681
- `No stable ${pin.tool} version matched the default prefix '${prefix}'. Pass an exact version identifier via -t ${pin.tool}<version>.`,
841
+ `${missingDetail} Pass an exact version identifier via -t ${pin.tool}<version>.`,
682
842
  );
683
843
  return undefined;
684
844
  }
@@ -1205,7 +1365,7 @@ export function prepareNodeEnv(filePath, options) {
1205
1365
  }
1206
1366
  }
1207
1367
  if (nvmNodePath) {
1208
- doNpmInstall(filePath, nvmNodePath);
1368
+ doNpmInstall(filePath, nvmNodePath, options);
1209
1369
  npmInstallAttempted = true;
1210
1370
  } else {
1211
1371
  console.log(
@@ -1228,7 +1388,7 @@ export function prepareNodeEnv(filePath, options) {
1228
1388
  );
1229
1389
  continue;
1230
1390
  }
1231
- doNpmInstall(filePath, nvmNodePath);
1391
+ doNpmInstall(filePath, nvmNodePath, options);
1232
1392
  npmInstallAttempted = true;
1233
1393
  }
1234
1394
  }
@@ -1302,12 +1462,18 @@ export function tryLoadNvmAndInstallTool(nodeVersion) {
1302
1462
  *
1303
1463
  * @param {String} filePath Path
1304
1464
  * @param {String} nvmNodePath Path to node version in nvm
1465
+ * @param {Object} [options] CLI options
1305
1466
  */
1306
- export function doNpmInstall(filePath, nvmNodePath) {
1467
+ export function doNpmInstall(filePath, nvmNodePath, options = {}) {
1307
1468
  // we do not install if INSTALL_ARGS set false
1308
1469
  if (["0", "false"].includes(readEnvironmentVariable("NODE_INSTALL_ARGS"))) {
1309
1470
  return;
1310
1471
  }
1472
+ // npm install runs the project's own lifecycle scripts, such as preinstall,
1473
+ // even with --package-lock-only.
1474
+ if (options.installDeps === false) {
1475
+ return;
1476
+ }
1311
1477
  const newPath = `${nvmNodePath}${delimiter}${readEnvironmentVariable("PATH")}`;
1312
1478
  let installArgs =
1313
1479
  readEnvironmentVariable("NPM_INSTALL_ARGS") || "--package-lock-only";
@@ -1386,7 +1552,12 @@ export function prepareSwiftEnv(filePath, options) {
1386
1552
  "INFO: Swift for Linux has known issues on non x64 machines. Run the cdxgen container image with --platform=linux/amd64 for best experience.",
1387
1553
  );
1388
1554
  }
1389
- if (options.deep || options?.lifecycle?.includes("post-build")) {
1555
+ // `swift package resolve` runs the project's Package.swift, so it waits for
1556
+ // the same permission as the scan's other project tooling.
1557
+ if (
1558
+ (options.deep || options?.lifecycle?.includes("post-build")) &&
1559
+ isProjectToolingAllowed(options)
1560
+ ) {
1390
1561
  const swiftFiles = getAllFiles(
1391
1562
  filePath,
1392
1563
  `${options.multiProject ? "**/" : ""}Package*.swift`,
@@ -1470,6 +1641,17 @@ export function prepareSwiftEnv(filePath, options) {
1470
1641
  * @param {String} filePath Path
1471
1642
  * @param {Object} options CLI Options
1472
1643
  */
1644
+ // The order pregen reads the Ruby to install in.
1645
+ const PREGEN_RUBY_SOURCE_ORDER = [
1646
+ ".ruby-version",
1647
+ ".tool-versions",
1648
+ "mise.toml",
1649
+ ".mise.toml",
1650
+ ".config/mise.toml",
1651
+ "Gemfile.lock",
1652
+ "Gemfile",
1653
+ ];
1654
+
1473
1655
  export function prepareRubyEnv(filePath, options) {
1474
1656
  // Skip preparation early
1475
1657
  if (
@@ -1515,53 +1697,39 @@ export function prepareRubyEnv(filePath, options) {
1515
1697
  readEnvironmentVariable("GEM_HOME") ||
1516
1698
  safeMkdtempSync(join(getTmpDir(), "cdxgen-gem-home-"));
1517
1699
  process.env.CDXGEN_GEM_HOME = cdxgenGemHome;
1518
- // Is there a .ruby-version file in the project?
1519
- if (safeExistsSync(join(filePath, ".ruby-version"))) {
1520
- rubyVersionNeeded = readFileSync(join(filePath, ".ruby-version"), {
1521
- encoding: "utf-8",
1522
- })
1523
- .trim()
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
- }
1534
- } else if (safeExistsSync(join(filePath, "Gemfile.lock"))) {
1535
- // Is there a lock file that can be used to identify the needed Ruby version?
1700
+ // The Ruby to install is the one developers select: .ruby-version, then the
1701
+ // asdf and mise pins, then the Ruby the lockfile was resolved with, then the
1702
+ // Gemfile directive. Only an exact MRI version can be installed with rbenv.
1703
+ const declarations = collectProjectRubyDeclarations(filePath);
1704
+ for (const declaration of declarations) {
1705
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
1706
+ ecosystem: "ruby",
1707
+ tool: declaration.engine === "ruby" ? "ruby" : declaration.engine,
1708
+ wanted: declaration.version || declaration.requirement,
1709
+ source: declaration.source,
1710
+ path: declaration.path,
1711
+ });
1712
+ }
1713
+ const installable = PREGEN_RUBY_SOURCE_ORDER.map((source) =>
1714
+ declarations.find(
1715
+ (declaration) => declaration.source === source && declaration.version,
1716
+ ),
1717
+ ).find(Boolean);
1718
+ if (installable && installable.engine !== "ruby") {
1719
+ console.log(
1720
+ `This project selects ${installable.engine} ${installable.version} (${installable.source}). cdxgen installs only MRI Ruby with rbenv; build the project with ${installable.engine} first and then invoke cdxgen with the arguments "--lifecycle pre-build".`,
1721
+ );
1722
+ } else if (installable?.prerelease) {
1723
+ console.log(
1724
+ `This project was locked with a pre-release of Ruby ${installable.version}, whose tag the lockfile does not record. Install it before invoking cdxgen.`,
1725
+ );
1726
+ } else if (installable) {
1727
+ rubyVersionNeeded = installable.version;
1728
+ }
1729
+ if (safeExistsSync(join(filePath, "Gemfile.lock"))) {
1536
1730
  const gemlockData = readFileSync(join(filePath, "Gemfile.lock"), {
1537
1731
  encoding: "utf-8",
1538
1732
  });
1539
- let rubyVersionMarker = false;
1540
- for (let l of gemlockData.split("\n")) {
1541
- l = l.replaceAll("\r", "").trim();
1542
- if (l.includes("RUBY VERSION")) {
1543
- rubyVersionMarker = true;
1544
- }
1545
- if (rubyVersionMarker && l.includes("ruby ")) {
1546
- const possibleVersion = l
1547
- .split("ruby ")
1548
- .pop()
1549
- .split("p")[0]
1550
- .split("d")[0];
1551
- if (/^\d+/.test(possibleVersion)) {
1552
- rubyVersionNeeded = possibleVersion;
1553
- break;
1554
- }
1555
- }
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
1733
  const bundlerVersion = parseGemfileLockBundlerVersion(gemlockData);
1566
1734
  if (bundlerVersion) {
1567
1735
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
@@ -1571,7 +1739,8 @@ export function prepareRubyEnv(filePath, options) {
1571
1739
  source: "Gemfile.lock",
1572
1740
  });
1573
1741
  }
1574
- } else {
1742
+ }
1743
+ if (!installable) {
1575
1744
  // Is the user invoking with a ruby with custom version type. Eg: -t ruby2.5.4
1576
1745
  let projectTypes = options.projectType;
1577
1746
  if (
@@ -1604,13 +1773,13 @@ export function prepareRubyEnv(filePath, options) {
1604
1773
  const existingRuby = collectRubyInfo(filePath);
1605
1774
  if (
1606
1775
  rubyVersionNeeded &&
1607
- existingRuby?.version?.startsWith(`ruby ${rubyVersionNeeded} `)
1776
+ isRubyVersionActive(existingRuby, rubyVersionNeeded)
1608
1777
  ) {
1609
1778
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
1610
1779
  ecosystem: "ruby",
1611
1780
  tool: "ruby",
1612
1781
  wanted: rubyVersionNeeded,
1613
- found: existingRuby.version,
1782
+ found: existingRuby.description || existingRuby.version,
1614
1783
  source: "env",
1615
1784
  detail: `The active interpreter is the required Ruby ${rubyVersionNeeded}.`,
1616
1785
  });
@@ -1642,15 +1811,15 @@ export function prepareRubyEnv(filePath, options) {
1642
1811
  });
1643
1812
  } else if (
1644
1813
  rubyVersionNeeded &&
1645
- !existingRuby.version.startsWith(`ruby ${rubyVersionNeeded} `)
1814
+ !isRubyVersionActive(existingRuby, rubyVersionNeeded)
1646
1815
  ) {
1647
1816
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
1648
1817
  ecosystem: "ruby",
1649
1818
  tool: "ruby",
1650
1819
  wanted: rubyVersionNeeded,
1651
- found: existingRuby.version,
1820
+ found: existingRuby.description || existingRuby.version,
1652
1821
  source: "env",
1653
- detail: `Ruby ${rubyVersionNeeded} is required and the active interpreter reports ${existingRuby.version}.`,
1822
+ detail: `Ruby ${rubyVersionNeeded} is required and the active interpreter reports ${existingRuby.description || existingRuby.version}.`,
1654
1823
  });
1655
1824
  }
1656
1825
  if (rubyVersionNeeded && !rbenvPresent) {