@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
@@ -16,6 +16,7 @@ import { build, Purl } from "@cdxgen/cdx-purl";
16
16
  // utils.js does not import rubyutils.js, so importing from it here is safe and
17
17
  // does not introduce a cyclic dependency.
18
18
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
19
+ import { readRegularTextFile } from "../core/boundedFile.js";
19
20
  import { shouldFetchLicense } from "../core/env.js";
20
21
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
21
22
  import { thoughtLog } from "../core/logger.js";
@@ -29,6 +30,12 @@ import {
29
30
  recordPolicyDegradationFromError,
30
31
  } from "../inventory/fetchBatch.js";
31
32
  import { genericPurl } from "../inventory/purl.js";
33
+ import {
34
+ createRubyRuntimeComponent,
35
+ defaultGemInstall,
36
+ parseRubyVersion,
37
+ } from "../inventory/rubyBuildInfo.js";
38
+ import { isValidIdnEmail } from "../parsers/email.js";
32
39
 
33
40
  // FIXME. This has to get removed, once we improve the module detection one-liner.
34
41
  // If you're a Rubyist, please help us improve this code.
@@ -1562,6 +1569,40 @@ function listGemBindirFiles(gemspecFile, bindir = "bin") {
1562
1569
  }
1563
1570
  }
1564
1571
 
1572
+ /**
1573
+ * Whether gemspec text is really a Marshal dump. RubyGems' spec cache stores
1574
+ * Marshal-dumped specifications in files named `<gem>-<version>.gemspec`; they
1575
+ * start with the Marshal 4.8 header bytes 0x04 0x08 and hold no Ruby source to
1576
+ * parse, so reading one would leave only the file name to guess from.
1577
+ *
1578
+ * @param {string} gemspecData file contents
1579
+ * @returns {boolean} true for a Marshal dump
1580
+ */
1581
+ function isMarshalData(gemspecData) {
1582
+ return (
1583
+ typeof gemspecData === "string" &&
1584
+ gemspecData.charCodeAt(0) === 4 &&
1585
+ gemspecData.charCodeAt(1) === 8
1586
+ );
1587
+ }
1588
+
1589
+ /**
1590
+ * Whether a gemspec is a default gem, one that ships with Ruby: RubyGems
1591
+ * keeps them under `specifications/default/`.
1592
+ *
1593
+ * @param {string} gemspecFile path of the gemspec, either separator
1594
+ * @returns {boolean}
1595
+ */
1596
+ export function isDefaultGemspecPath(gemspecFile) {
1597
+ if (typeof gemspecFile !== "string") {
1598
+ return false;
1599
+ }
1600
+ return (
1601
+ defaultGemInstall(gemspecFile) !== undefined ||
1602
+ gemspecFile.replaceAll("\\", "/").includes("/specifications/default/")
1603
+ );
1604
+ }
1605
+
1565
1606
  /**
1566
1607
  * Method to parse Gemspec file contents.
1567
1608
  *
@@ -1593,7 +1634,7 @@ export async function parseGemspecData(gemspecData, gemspecFile, options = {}) {
1593
1634
  if (gemspecFile) {
1594
1635
  pkg.name = basename(gemspecFile).replace(".gemspec", "");
1595
1636
  }
1596
- if (!gemspecData) {
1637
+ if (!gemspecData || isMarshalData(gemspecData)) {
1597
1638
  return pkgList;
1598
1639
  }
1599
1640
  let versionHackMatch = false;
@@ -1948,6 +1989,12 @@ export async function parseGemspecData(gemspecData, gemspecFile, options = {}) {
1948
1989
  }
1949
1990
  if (gemspecFile) {
1950
1991
  pkg.properties.push({ name: "internal:SrcFile", value: gemspecFile });
1992
+ // RubyGems keeps the gems that ship with Ruby itself, such as resolv and
1993
+ // net-http, under specifications/default. They are updated with Ruby, so
1994
+ // their advisories are often fixed by a Ruby release.
1995
+ if (isDefaultGemspecPath(gemspecFile)) {
1996
+ pkg.properties.push({ name: "cdx:gem:defaultGem", value: "true" });
1997
+ }
1951
1998
  // Did we find the version number from the directory name? Let's reduce the confidence and set the correct technique
1952
1999
  pkg.evidence = {
1953
2000
  identity: {
@@ -1967,9 +2014,11 @@ export async function parseGemspecData(gemspecData, gemspecFile, options = {}) {
1967
2014
  // `spec.email` holds the contact addresses of the gem, not of an author.
1968
2015
  // An address can only be attributed when there is one per author, in the
1969
2016
  // same order, or when the gem has a single author.
1970
- const emails = (lists.email || []).filter(
1971
- (email) => email.includes("@") && !email.includes(" "),
1972
- );
2017
+ // Only a valid `idn-email` may reach `organizationalContact.email`; the
2018
+ // schema asserts the format, so anything else fails BOM validation.
2019
+ const emails = (lists.email || [])
2020
+ .map((email) => `${email}`.trim())
2021
+ .filter(isValidIdnEmail);
1973
2022
  pkg.authors = lists.authors.map((name, i) => {
1974
2023
  let email;
1975
2024
  if (emails.length === lists.authors.length) {
@@ -2858,30 +2907,434 @@ export function rubyRequirementToVers(requirements) {
2858
2907
  return `vers:gem/${constraints.map((c) => c.text).join("|")}`;
2859
2908
  }
2860
2909
 
2910
+ // Ruby engines a version manager or Bundler can name. MRI is `ruby`.
2911
+ const RUBY_ENGINES = new Set(["ruby", "jruby", "truffleruby"]);
2912
+ const MAX_PIN_FILE_BYTES = 64 * 1024;
2913
+ const MAX_GEMFILE_BYTES = 1024 * 1024;
2914
+ const MAX_LOCKFILE_BYTES = 16 * 1024 * 1024;
2915
+
2916
+ // An exact version, or a partial one such as `3.3` that pins a series and is
2917
+ // read as the requirement `~> 3.3.0`.
2918
+ function rubyPin(engine, text, source) {
2919
+ const value = `${text || ""}`.trim();
2920
+ if (!value || value.length > 64) {
2921
+ return undefined;
2922
+ }
2923
+ if (engine === "ruby") {
2924
+ const version = parseRubyVersion(value);
2925
+ if (version) {
2926
+ return { engine, version, source };
2927
+ }
2928
+ if (/^\d+\.\d+$/.test(value)) {
2929
+ return { engine, requirement: `~> ${value}.0`, source };
2930
+ }
2931
+ if (/^\d+$/.test(value)) {
2932
+ return { engine, requirement: `~> ${value}.0`, source };
2933
+ }
2934
+ return undefined;
2935
+ }
2936
+ return /^\d+(\.\d+){1,3}$/.test(value)
2937
+ ? { engine, version: value, source }
2938
+ : undefined;
2939
+ }
2940
+
2941
+ /**
2942
+ * Read the Ruby a `.ruby-version` file selects, as rbenv, chruby, rvm, asdf
2943
+ * and mise read it: `3.3.6`, `ruby-3.3.6`, `3.3`, `jruby-9.4.8.0`,
2944
+ * `truffleruby-24.1.0`, `truffleruby+graalvm-24.1.0`. Only the first word
2945
+ * counts; comments and blank lines are skipped.
2946
+ *
2947
+ * @param {string} text file contents
2948
+ * @returns {{engine: string, version?: string, requirement?: string, source: string}|undefined}
2949
+ */
2950
+ export function parseRubyVersionFile(text) {
2951
+ if (typeof text !== "string") {
2952
+ return undefined;
2953
+ }
2954
+ for (const rawLine of text.slice(0, MAX_PIN_FILE_BYTES).split(/\r?\n/)) {
2955
+ const line = rawLine.split("#")[0].trim().split(/\s+/)[0];
2956
+ if (!line) {
2957
+ continue;
2958
+ }
2959
+ const dash = line.indexOf("-");
2960
+ if (dash > 0 && !/^\d/.test(line)) {
2961
+ const engine = line.slice(0, dash).split("+")[0];
2962
+ if (!RUBY_ENGINES.has(engine)) {
2963
+ return undefined;
2964
+ }
2965
+ return rubyPin(engine, line.slice(dash + 1), ".ruby-version");
2966
+ }
2967
+ return rubyPin("ruby", line, ".ruby-version");
2968
+ }
2969
+ return undefined;
2970
+ }
2971
+
2972
+ /**
2973
+ * Read the Ruby an asdf `.tool-versions` file selects: the first version on
2974
+ * the `ruby` line, e.g. `ruby 3.3.6 3.2.2`.
2975
+ *
2976
+ * @param {string} text file contents
2977
+ * @returns {{engine: string, version?: string, requirement?: string, source: string}|undefined}
2978
+ */
2979
+ export function parseToolVersionsRuby(text) {
2980
+ if (typeof text !== "string") {
2981
+ return undefined;
2982
+ }
2983
+ for (const rawLine of text.slice(0, MAX_PIN_FILE_BYTES).split(/\r?\n/)) {
2984
+ const words = rawLine.split("#")[0].trim().split(/\s+/);
2985
+ if (words[0] !== "ruby" || !words[1]) {
2986
+ continue;
2987
+ }
2988
+ const value = words[1];
2989
+ const dash = value.indexOf("-");
2990
+ if (dash > 0 && !/^\d/.test(value)) {
2991
+ const engine = value.slice(0, dash).split("+")[0];
2992
+ return RUBY_ENGINES.has(engine)
2993
+ ? rubyPin(engine, value.slice(dash + 1), ".tool-versions")
2994
+ : undefined;
2995
+ }
2996
+ return rubyPin("ruby", value, ".tool-versions");
2997
+ }
2998
+ return undefined;
2999
+ }
3000
+
3001
+ /**
3002
+ * Read the Ruby a mise configuration selects from its `[tools]` table:
3003
+ * `ruby = "3.3.6"`, `ruby = ["3.3.6", "3.2"]` or `ruby = { version = "3.3.6" }`.
3004
+ *
3005
+ * @param {string} text mise.toml contents
3006
+ * @param {string} [source="mise.toml"] the file name, for the result
3007
+ * @returns {{engine: string, version?: string, requirement?: string, source: string}|undefined}
3008
+ */
3009
+ export function parseMiseRuby(text, source = "mise.toml") {
3010
+ if (typeof text !== "string") {
3011
+ return undefined;
3012
+ }
3013
+ let inTools = false;
3014
+ for (const rawLine of text.slice(0, MAX_PIN_FILE_BYTES).split(/\r?\n/)) {
3015
+ const line = rawLine.trim();
3016
+ if (line.startsWith("[")) {
3017
+ // `[tools]`, `[ tools ]` and `[tools] # pinned` open the same table
3018
+ inTools = line.split("#")[0].replace(/\s+/g, "") === "[tools]";
3019
+ continue;
3020
+ }
3021
+ if (!inTools || !/^["']?ruby["']?\s*=/.test(line)) {
3022
+ continue;
3023
+ }
3024
+ const value = line.slice(line.indexOf("=") + 1).trim();
3025
+ const quoted = /["']([^"']+)["']/.exec(
3026
+ value.startsWith("{") ? value.slice(value.indexOf("version")) : value,
3027
+ );
3028
+ return quoted ? rubyPin("ruby", quoted[1], source) : undefined;
3029
+ }
3030
+ return undefined;
3031
+ }
3032
+
3033
+ /**
3034
+ * Read the `ruby` directive of a Gemfile: `ruby "3.3.6"`, `ruby "~> 3.3"`,
3035
+ * `ruby "3.1.4", engine: "jruby", engine_version: "9.4.8.0"`, or
3036
+ * `ruby file: ".ruby-version"`. The Gemfile is never evaluated, so a
3037
+ * directive built from an expression, such as `ruby RUBY_VERSION`, says
3038
+ * nothing.
3039
+ *
3040
+ * @param {string} text Gemfile contents
3041
+ * @returns {{engine: string, version?: string, requirement?: string,
3042
+ * file?: string, source: string}|undefined}
3043
+ */
3044
+ export function parseGemfileRubyDirective(text) {
3045
+ if (typeof text !== "string") {
3046
+ return undefined;
3047
+ }
3048
+ for (const rawLine of text.slice(0, 1024 * 1024).split(/\r?\n/)) {
3049
+ const line = rawLine.trim();
3050
+ if (!/^ruby[\s(]/.test(line)) {
3051
+ continue;
3052
+ }
3053
+ let rest = line.slice(4).trim();
3054
+ if (rest.startsWith("(")) {
3055
+ rest = rest.slice(1).trim();
3056
+ }
3057
+ const fileMatch = /^file:\s*/.exec(rest);
3058
+ if (fileMatch) {
3059
+ const literal = readRubyStringLiteral(rest.slice(fileMatch[0].length));
3060
+ return literal?.value && !literal.interpolated
3061
+ ? { engine: "ruby", file: literal.value, source: "Gemfile" }
3062
+ : undefined;
3063
+ }
3064
+ const literal = readRubyStringLiteral(rest);
3065
+ if (!literal?.value || literal.interpolated) {
3066
+ return undefined;
3067
+ }
3068
+ // `ruby "3.2.2" if ENV["LEGACY"]` holds only on some machines; a later
3069
+ // unconditional directive, if any, is the project's.
3070
+ if (/(^|[\s,)])(if|unless)\s/.test(literal.rest)) {
3071
+ continue;
3072
+ }
3073
+ const keyword = (name) => {
3074
+ const index = rest.indexOf(`${name}:`);
3075
+ if (index === -1) {
3076
+ return undefined;
3077
+ }
3078
+ const value = readRubyStringLiteral(
3079
+ rest.slice(index + name.length + 1).trim(),
3080
+ );
3081
+ return value && !value.interpolated ? value.value : undefined;
3082
+ };
3083
+ const engine = keyword("engine");
3084
+ const engineVersion = keyword("engine_version");
3085
+ if (engine && engine !== "ruby") {
3086
+ if (!RUBY_ENGINES.has(engine) || !engineVersion) {
3087
+ return undefined;
3088
+ }
3089
+ const pin = rubyPin(engine, engineVersion, "Gemfile");
3090
+ return pin ? { ...pin, rubyVersion: literal.value } : undefined;
3091
+ }
3092
+ const value = literal.value.trim();
3093
+ const exact = parseRubyVersion(value);
3094
+ if (exact) {
3095
+ return { engine: "ruby", version: exact, source: "Gemfile" };
3096
+ }
3097
+ return rubyRequirementToVers([value]) !== undefined
3098
+ ? { engine: "ruby", requirement: value, source: "Gemfile" }
3099
+ : undefined;
3100
+ }
3101
+ return undefined;
3102
+ }
3103
+
2861
3104
  /**
2862
- * Describe the Ruby runtime the gems require as one external `platform`
3105
+ * Read the `RUBY VERSION` section of a Gemfile.lock, which Bundler writes when
3106
+ * the Gemfile has a `ruby` directive: `ruby 3.3.6p108`, `ruby 3.4.0p-1` for a
3107
+ * pre-release, or `ruby 3.1.4p0 (jruby 9.4.8.0)` for another engine.
3108
+ *
3109
+ * @param {string} text Gemfile.lock contents
3110
+ * @returns {{engine: string, version: string, patchlevel?: string,
3111
+ * rubyVersion?: string, source: string}|undefined}
3112
+ */
3113
+ export function parseGemfileLockRubyVersion(text) {
3114
+ if (typeof text !== "string") {
3115
+ return undefined;
3116
+ }
3117
+ const lines = text.split(/\r?\n/);
3118
+ for (let index = 0; index < lines.length; index++) {
3119
+ if (lines[index].trim() !== "RUBY VERSION") {
3120
+ continue;
3121
+ }
3122
+ const value = (lines[index + 1] || "").trim();
3123
+ const match =
3124
+ /^ruby (\d+\.\d+\.\d+)(?:p(-?\d+))?(?: \((jruby|truffleruby) (\d+(?:\.\d+){1,3})\))?$/.exec(
3125
+ value,
3126
+ );
3127
+ if (!match) {
3128
+ return undefined;
3129
+ }
3130
+ if (match[3]) {
3131
+ return {
3132
+ engine: match[3],
3133
+ version: match[4],
3134
+ rubyVersion: match[1],
3135
+ source: "Gemfile.lock",
3136
+ };
3137
+ }
3138
+ const result = {
3139
+ engine: "ruby",
3140
+ version: match[1],
3141
+ source: "Gemfile.lock",
3142
+ };
3143
+ if (match[2] !== undefined && match[2] !== "-1") {
3144
+ result.patchlevel = match[2];
3145
+ } else if (match[2] === "-1") {
3146
+ // A pre-release or development build, whose tag Bundler does not record
3147
+ result.prerelease = true;
3148
+ }
3149
+ return result;
3150
+ }
3151
+ return undefined;
3152
+ }
3153
+
3154
+ // A pin file is read only when it is a regular file inside the project and
3155
+ // within a size cap: a repository can commit `.tool-versions -> /dev/zero` or
3156
+ // a FIFO, which an unbounded read would never finish.
3157
+ function readPinFile(file, projectRoot, maxBytes = MAX_PIN_FILE_BYTES) {
3158
+ return readRegularTextFile(file, maxBytes, projectRoot);
3159
+ }
3160
+
3161
+ /**
3162
+ * Collect every Ruby a project directory declares, from the Gemfile.lock
3163
+ * `RUBY VERSION` section, `.ruby-version`, `.tool-versions`, mise.toml and the
3164
+ * Gemfile `ruby` directive. Each declaration names its source.
3165
+ *
3166
+ * @param {string} projectPath project directory
3167
+ * @param {string} [lockData] Gemfile.lock contents, when already read
3168
+ * @returns {Array<{engine: string, version?: string, requirement?: string,
3169
+ * patchlevel?: string, source: string, path: string}>}
3170
+ */
3171
+ export function collectProjectRubyDeclarations(projectPath, lockData) {
3172
+ const declarations = [];
3173
+ const add = (declaration, path) => {
3174
+ if (declaration) {
3175
+ declarations.push({ ...declaration, path });
3176
+ }
3177
+ };
3178
+ const lockFile = join(projectPath, "Gemfile.lock");
3179
+ add(
3180
+ parseGemfileLockRubyVersion(
3181
+ lockData === undefined
3182
+ ? readPinFile(lockFile, projectPath, MAX_LOCKFILE_BYTES)
3183
+ : lockData,
3184
+ ),
3185
+ lockFile,
3186
+ );
3187
+ const versionFile = join(projectPath, ".ruby-version");
3188
+ add(parseRubyVersionFile(readPinFile(versionFile, projectPath)), versionFile);
3189
+ const toolVersions = join(projectPath, ".tool-versions");
3190
+ add(
3191
+ parseToolVersionsRuby(readPinFile(toolVersions, projectPath)),
3192
+ toolVersions,
3193
+ );
3194
+ for (const name of ["mise.toml", ".mise.toml", ".config/mise.toml"]) {
3195
+ const miseFile = join(projectPath, name);
3196
+ add(parseMiseRuby(readPinFile(miseFile, projectPath), name), miseFile);
3197
+ }
3198
+ const gemfile = join(projectPath, "Gemfile");
3199
+ const directive = parseGemfileRubyDirective(
3200
+ readPinFile(gemfile, projectPath, MAX_GEMFILE_BYTES),
3201
+ );
3202
+ if (directive?.file) {
3203
+ // `ruby file: ".ruby-version"` reads the version from that file, relative
3204
+ // to the Gemfile; a path out of the project is not followed.
3205
+ const target = resolve(projectPath, directive.file);
3206
+ const inside = relative(resolve(projectPath), target);
3207
+ if (inside && !inside.startsWith("..") && !isAbsolute(inside)) {
3208
+ // Bundler reads .tool-versions and mise.toml through `ruby file:` too
3209
+ const text = readPinFile(target, projectPath);
3210
+ const name = basename(target);
3211
+ const fromFile =
3212
+ name === ".tool-versions"
3213
+ ? parseToolVersionsRuby(text)
3214
+ : name.endsWith(".toml")
3215
+ ? parseMiseRuby(text)
3216
+ : parseRubyVersionFile(text);
3217
+ add(fromFile ? { ...fromFile, source: "Gemfile" } : undefined, gemfile);
3218
+ }
3219
+ } else {
3220
+ add(directive, gemfile);
3221
+ }
3222
+ return declarations;
3223
+ }
3224
+
3225
+ // The Ruby a lockfile was resolved with is what the project ran on; the pin
3226
+ // files say what developers select; the Gemfile says what Bundler accepts.
3227
+ const DECLARATION_PRECEDENCE = [
3228
+ "Gemfile.lock",
3229
+ ".ruby-version",
3230
+ ".tool-versions",
3231
+ "mise.toml",
3232
+ ".mise.toml",
3233
+ ".config/mise.toml",
3234
+ "Gemfile",
3235
+ ];
3236
+
3237
+ /**
3238
+ * Settle the Ruby a project runs on from its declarations: the exact version
3239
+ * of the most authoritative source that has one, and the requirements of the
3240
+ * sources that only constrain it.
3241
+ *
3242
+ * @param {Array<Object>} declarations result of collectProjectRubyDeclarations
3243
+ * @returns {{engine: string, version?: string, patchlevel?: string,
3244
+ * prerelease?: boolean, rubyVersion?: string, source?: string,
3245
+ * requirements: string[], conflicts: string[]}|undefined}
3246
+ */
3247
+ export function resolveProjectRuby(declarations) {
3248
+ if (!declarations?.length) {
3249
+ return undefined;
3250
+ }
3251
+ const ordered = [...declarations].sort(
3252
+ (a, b) =>
3253
+ DECLARATION_PRECEDENCE.indexOf(a.source) -
3254
+ DECLARATION_PRECEDENCE.indexOf(b.source),
3255
+ );
3256
+ let exact = ordered.find((declaration) => declaration.version);
3257
+ // Bundler records a pre-release as `ruby 3.4.0p-1`, without its tag. A pin
3258
+ // that names the tag of that same version says which pre-release it is.
3259
+ const lockedPrerelease = exact?.prerelease ? exact : undefined;
3260
+ if (lockedPrerelease) {
3261
+ exact =
3262
+ ordered.find(
3263
+ (declaration) =>
3264
+ declaration.engine === lockedPrerelease.engine &&
3265
+ declaration.version?.startsWith(`${lockedPrerelease.version}-`),
3266
+ ) || lockedPrerelease;
3267
+ }
3268
+ const requirements = ordered
3269
+ .filter((declaration) => declaration.requirement)
3270
+ .map((declaration) => declaration.requirement);
3271
+ const result = {
3272
+ engine: exact?.engine || "ruby",
3273
+ requirements,
3274
+ conflicts: [],
3275
+ };
3276
+ if (exact) {
3277
+ for (const key of [
3278
+ "version",
3279
+ "patchlevel",
3280
+ "prerelease",
3281
+ "rubyVersion",
3282
+ "source",
3283
+ ]) {
3284
+ if (exact[key] !== undefined) {
3285
+ result[key] = exact[key];
3286
+ }
3287
+ }
3288
+ for (const other of ordered) {
3289
+ if (
3290
+ other !== exact &&
3291
+ other !== lockedPrerelease &&
3292
+ other.version &&
3293
+ (other.engine !== exact.engine || other.version !== exact.version)
3294
+ ) {
3295
+ result.conflicts.push(
3296
+ `${other.source}: ${other.engine} ${other.version}`,
3297
+ );
3298
+ }
3299
+ }
3300
+ }
3301
+ return result;
3302
+ }
3303
+
3304
+ /**
3305
+ * Describe the Ruby runtime of a project as one external `platform`
2863
3306
  * component, and make the components that declare a `required_ruby_version`
2864
3307
  * depend on it (discussion 4409).
2865
3308
  *
2866
- * The component stands for the Ruby the bundle as a whole runs on, so its
2867
- * `versionRange` is the intersection of every requirement that could be read,
2868
- * in the `vers` syntax CycloneDX mandates for the field. Each component's own
2869
- * requirement stays in its `cdx:gem:rubyVersionSpecifiers` property. One
2870
- * runtime component per distinct requirement would describe the same
2871
- * interpreter many times over: a modest bundle spells `>= 2.x` a dozen ways.
3309
+ * When the project pins a Ruby (Gemfile.lock `RUBY VERSION`, `.ruby-version`,
3310
+ * `.tool-versions`, mise or the Gemfile `ruby` directive), the component names
3311
+ * that version, `pkg:generic/ruby@3.3.6`, with the CPE a scanner matches
3312
+ * interpreter advisories by. The requirements of the gems then stay in the
3313
+ * `cdx:ruby:requiredVersionRange` property, since CycloneDX allows a
3314
+ * `versionRange` only on a component without a version.
3315
+ *
3316
+ * Without a pin, the component stands for the Ruby the bundle as a whole runs
3317
+ * on, so its `versionRange` is the intersection of every requirement that
3318
+ * could be read, in the `vers` syntax CycloneDX mandates for the field. Each
3319
+ * component's own requirement stays in its `cdx:gem:rubyVersionSpecifiers`
3320
+ * property. One runtime component per distinct requirement would describe the
3321
+ * same interpreter many times over: a modest bundle spells `>= 2.x` a dozen
3322
+ * ways.
2872
3323
  *
2873
3324
  * The type is `platform`, the CycloneDX type for a runtime environment that
2874
3325
  * interprets software, and the one cdxgen gives the Ruby it finds in the
2875
- * build environment. The purl names no version, since a range is not one.
3326
+ * build environment. A range is not a version, so an unpinned purl names none.
2876
3327
  *
2877
- * `isExternal` and `versionRange` are CycloneDX 1.7 fields, and without them
2878
- * the component would claim the product bundles its interpreter, so the caller
2879
- * decides whether the BOM should carry it at all.
3328
+ * `isExternal` and `versionRange` are CycloneDX 1.7 fields. A pinned runtime
3329
+ * also carries the `cdx:ruby:isExternal` property, which says the interpreter
3330
+ * comes with the deployment environment in a BOM of an older spec; the caller
3331
+ * decides whether such a BOM should carry an unpinned runtime at all.
2880
3332
  *
2881
3333
  * @param {Array} pkgList Components
2882
3334
  * @param {object} parentComponent The metadata.component, when the project is
2883
3335
  * itself a gem
2884
3336
  * @param {Array} dependencies Dependency edges
3337
+ * @param {object} [projectRuby] result of resolveProjectRuby
2885
3338
  * @returns {{pkgList: Array, dependencies: Array}} Components and edges with
2886
3339
  * the runtime component added
2887
3340
  */
@@ -2889,6 +3342,7 @@ export function addRubyRuntimeComponent(
2889
3342
  pkgList,
2890
3343
  parentComponent,
2891
3344
  dependencies,
3345
+ projectRuby,
2892
3346
  ) {
2893
3347
  const requirements = [];
2894
3348
  const declaringRefs = new Set();
@@ -2905,29 +3359,85 @@ export function addRubyRuntimeComponent(
2905
3359
  declaringRefs.add(component["bom-ref"]);
2906
3360
  }
2907
3361
  }
2908
- if (!declaringRefs.size) {
3362
+ for (const requirement of projectRuby?.requirements || []) {
3363
+ if (rubyRequirementToVers([requirement]) !== undefined) {
3364
+ requirements.push(requirement);
3365
+ }
3366
+ }
3367
+ const pinned = projectRuby?.version;
3368
+ if (!pinned && !requirements.length) {
2909
3369
  return { pkgList, dependencies };
2910
3370
  }
2911
- const runtimeRef = genericPurl("ruby");
2912
- const runtimeComponent = {
2913
- type: "platform",
2914
- name: "ruby",
2915
- scope: "required",
2916
- // The interpreter comes with the deployment environment, not with the
2917
- // product the BOM describes
2918
- isExternal: true,
2919
- purl: runtimeRef,
2920
- "bom-ref": runtimeRef,
2921
- };
2922
- const versionRange = rubyRequirementToVers(requirements);
2923
- if (versionRange) {
2924
- runtimeComponent.versionRange = versionRange;
3371
+ if (projectRuby && parentComponent?.["bom-ref"]) {
3372
+ declaringRefs.add(parentComponent["bom-ref"]);
3373
+ }
3374
+ const versionRange = requirements.length
3375
+ ? rubyRequirementToVers(requirements)
3376
+ : undefined;
3377
+ let runtimeComponent;
3378
+ if (pinned) {
3379
+ runtimeComponent = createRubyRuntimeComponent(pinned, {
3380
+ engine: projectRuby.engine,
3381
+ // Bundler records no tag for a pre-release, so no CPE can name it
3382
+ withCpe: !projectRuby.prerelease,
3383
+ });
3384
+ runtimeComponent.scope = "required";
3385
+ runtimeComponent.isExternal = true;
3386
+ const properties = [
3387
+ { name: "cdx:ruby:isExternal", value: "true" },
3388
+ { name: "cdx:ruby:versionSource", value: projectRuby.source },
3389
+ ];
3390
+ if (projectRuby.prerelease) {
3391
+ // Not the release of that number, and no tag says which pre-release
3392
+ properties.push({ name: "cdx:ruby:prerelease", value: "true" });
3393
+ }
3394
+ if (projectRuby.patchlevel !== undefined) {
3395
+ properties.push({
3396
+ name: "cdx:ruby:patchlevel",
3397
+ value: projectRuby.patchlevel,
3398
+ });
3399
+ }
3400
+ if (projectRuby.rubyVersion) {
3401
+ properties.push({
3402
+ name: "cdx:ruby:languageVersion",
3403
+ value: projectRuby.rubyVersion,
3404
+ });
3405
+ }
3406
+ if (versionRange) {
3407
+ properties.push({
3408
+ name: "cdx:ruby:requiredVersionRange",
3409
+ value: versionRange,
3410
+ });
3411
+ }
3412
+ if (projectRuby.conflicts?.length) {
3413
+ properties.push({
3414
+ name: "cdx:ruby:conflictingDeclarations",
3415
+ value: projectRuby.conflicts.join(", "),
3416
+ });
3417
+ }
3418
+ runtimeComponent.properties = properties;
2925
3419
  } else {
2926
- thoughtLog(
2927
- "The Ruby version requirements of the gems exclude each other, so the Ruby runtime component carries no version range.",
2928
- { requirements },
2929
- );
3420
+ const runtimeRef = genericPurl("ruby");
3421
+ runtimeComponent = {
3422
+ type: "platform",
3423
+ name: "ruby",
3424
+ scope: "required",
3425
+ // The interpreter comes with the deployment environment, not with the
3426
+ // product the BOM describes
3427
+ isExternal: true,
3428
+ purl: runtimeRef,
3429
+ "bom-ref": runtimeRef,
3430
+ };
3431
+ if (versionRange) {
3432
+ runtimeComponent.versionRange = versionRange;
3433
+ } else {
3434
+ thoughtLog(
3435
+ "The Ruby version requirements of the gems exclude each other, so the Ruby runtime component carries no version range.",
3436
+ { requirements },
3437
+ );
3438
+ }
2930
3439
  }
3440
+ const runtimeRef = runtimeComponent["bom-ref"];
2931
3441
  const describedRefs = new Set(dependencies.map((d) => d.ref));
2932
3442
  const outDependencies = dependencies.map((d) =>
2933
3443
  declaringRefs.has(d.ref)
@@ -14,6 +14,7 @@ import {
14
14
  reportPluginRunFailure,
15
15
  resolvePluginSourceDir,
16
16
  } from "../core/pluginRun.js";
17
+ import { isProjectToolingAllowed } from "../core/projectTooling.js";
17
18
  import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
18
19
  import { resolvePluginBinary } from "../inventory/plugins.js";
19
20
 
@@ -821,6 +822,15 @@ export function runRusiAnalysis(src, outputFile, options = {}) {
821
822
  if (!executable || !src || !outputFile) {
822
823
  return false;
823
824
  }
825
+ // rusi runs cargo and rustc on the project, which follow its
826
+ // `.cargo/config.toml` and `rust-toolchain.toml` and can compile its build
827
+ // scripts, so it waits for permission to run the project's tooling.
828
+ if (!isProjectToolingAllowed(options)) {
829
+ console.log(
830
+ "Skipping rusi since dependency installation is disabled. Rust evidence needs cargo to run on the project.",
831
+ );
832
+ return false;
833
+ }
824
834
 
825
835
  // cdxgen may run with its working directory already inside the project
826
836
  // while the source argument is relative to a repository root; a plain