@cdxgen/cdxgen 13.5.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 (182) hide show
  1. package/README.md +6 -6
  2. package/bin/cdxgen.js +4 -0
  3. package/bin/evinse.js +3 -0
  4. package/bin/validate.js +6 -0
  5. package/data/license-aliases.json +24 -2
  6. package/data/remediations.json +124 -0
  7. package/data/rules/ai-bom.yaml +1 -1
  8. package/data/rules/brotli-decoder.yaml +107 -0
  9. package/data/rules/vscode-extensions.yaml +75 -0
  10. package/data/spdx-license-list.json +121 -7
  11. package/data/spdx-licenses.json +69 -0
  12. package/data/spdx.schema.json +845 -2
  13. package/lib/cli/bomAssembly.js +92 -3
  14. package/lib/cli/index.js +108 -1
  15. package/lib/cli/jsBom.js +153 -64
  16. package/lib/cli/jvmBom.js +237 -46
  17. package/lib/cli/managedBom.js +142 -16
  18. package/lib/cli/nativeBom.js +42 -5
  19. package/lib/core/boundedFile.js +344 -0
  20. package/lib/core/brotliProbe.js +182 -0
  21. package/lib/core/env.js +8 -0
  22. package/lib/core/fs.js +12 -0
  23. package/lib/core/httpClient.js +98 -1
  24. package/lib/core/lockPaths.js +38 -0
  25. package/lib/core/paths.js +24 -1
  26. package/lib/core/processTree.js +5 -0
  27. package/lib/core/toolRegistry.js +170 -0
  28. package/lib/ecosystems/cppEvidence.js +263 -106
  29. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  30. package/lib/ecosystems/ecosystems.js +25 -10
  31. package/lib/ecosystems/gemHostProbe.js +301 -0
  32. package/lib/ecosystems/gradleutils.js +828 -224
  33. package/lib/ecosystems/parsers-bazel.js +107 -6
  34. package/lib/ecosystems/parsers-js.js +505 -22
  35. package/lib/ecosystems/parsers-misc.js +209 -20
  36. package/lib/ecosystems/parsers-python.js +89 -2
  37. package/lib/ecosystems/pythonutils.js +128 -2
  38. package/lib/ecosystems/rubyutils.js +538 -31
  39. package/lib/ecosystems/scalacliutils.js +2 -0
  40. package/lib/ecosystems/scalasem.js +796 -151
  41. package/lib/ecosystems/vendoredCode.js +237 -3
  42. package/lib/ecosystems/venvProbe.js +360 -0
  43. package/lib/evinser/evinser.js +346 -43
  44. package/lib/helpers/agenticToolutils.js +413 -93
  45. package/lib/helpers/vsixutils.js +1574 -315
  46. package/lib/inventory/atomUtils.js +77 -0
  47. package/lib/inventory/brotliDecoder.js +142 -0
  48. package/lib/inventory/buildToolRateLimit.js +1 -1
  49. package/lib/inventory/cbomutils.js +56 -7
  50. package/lib/inventory/deps.js +35 -0
  51. package/lib/inventory/dosai.js +139 -5
  52. package/lib/inventory/dotnetRuntime.js +217 -0
  53. package/lib/inventory/envcontext.js +307 -13
  54. package/lib/inventory/evidenceUtils.js +11 -0
  55. package/lib/inventory/fetchBatch.js +1 -1
  56. package/lib/inventory/formulationParsers.js +31 -3
  57. package/lib/inventory/gtfobins.js +13 -0
  58. package/lib/inventory/hostRuntimeProbe.js +686 -0
  59. package/lib/inventory/jvmLocalRepos.js +9 -2
  60. package/lib/inventory/jvmToolEnv.js +29 -0
  61. package/lib/inventory/licenseEnhancer.js +12 -0
  62. package/lib/inventory/mavenCentralMirror.js +534 -0
  63. package/lib/inventory/nodeBuildInfo.js +218 -0
  64. package/lib/inventory/nodeHostProbe.js +202 -0
  65. package/lib/inventory/osPackageResolver.js +182 -0
  66. package/lib/inventory/phpBuildInfo.js +10 -59
  67. package/lib/inventory/phpHostProbe.js +32 -488
  68. package/lib/inventory/rubyBuildInfo.js +972 -0
  69. package/lib/inventory/rubyHostProbe.js +218 -0
  70. package/lib/inventory/scalaCoords.js +4 -3
  71. package/lib/inventory/scalaDedupe.js +440 -0
  72. package/lib/inventory/source.js +49 -2
  73. package/lib/managers/binary.js +337 -3
  74. package/lib/managers/docker.js +79 -0
  75. package/lib/parsers/largeJson.js +28 -11
  76. package/lib/parsers/vscodeExtensionId.js +183 -0
  77. package/lib/server/server.js +12 -1
  78. package/lib/stages/postgen/ruleEngine.js +13 -5
  79. package/lib/stages/pregen/envAudit.js +1 -0
  80. package/lib/stages/pregen/pregen.js +114 -57
  81. package/lib/validator/bomValidator.js +6 -1
  82. package/lib/validator/generated/validate-1.6.mjs +1 -1
  83. package/lib/validator/generated/validate-1.7.mjs +1 -1
  84. package/lib/validator/generated/validate-2.0.mjs +1 -1
  85. package/lib/validator/index.js +1 -1
  86. package/package.json +3 -2
  87. package/types/lib/cli/bomAssembly.d.ts +13 -1
  88. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/cli/jsBom.d.ts +1 -17
  91. package/types/lib/cli/jsBom.d.ts.map +1 -1
  92. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  93. package/types/lib/cli/managedBom.d.ts.map +1 -1
  94. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  95. package/types/lib/core/boundedFile.d.ts +70 -0
  96. package/types/lib/core/boundedFile.d.ts.map +1 -0
  97. package/types/lib/core/brotliProbe.d.ts +37 -0
  98. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  99. package/types/lib/core/env.d.ts +4 -0
  100. package/types/lib/core/env.d.ts.map +1 -1
  101. package/types/lib/core/fs.d.ts.map +1 -1
  102. package/types/lib/core/httpClient.d.ts +24 -0
  103. package/types/lib/core/httpClient.d.ts.map +1 -1
  104. package/types/lib/core/lockPaths.d.ts +19 -0
  105. package/types/lib/core/lockPaths.d.ts.map +1 -0
  106. package/types/lib/core/processTree.d.ts +2 -0
  107. package/types/lib/core/processTree.d.ts.map +1 -1
  108. package/types/lib/core/toolRegistry.d.ts +36 -0
  109. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  110. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  111. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  112. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  113. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  114. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  115. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  116. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  117. package/types/lib/ecosystems/gradleutils.d.ts +47 -13
  118. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  121. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  123. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  125. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  126. package/types/lib/ecosystems/pythonutils.d.ts +20 -2
  127. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  128. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  129. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  131. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  132. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  133. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  134. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  135. package/types/lib/evinser/evinser.d.ts +63 -8
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/inventory/atomUtils.d.ts +19 -0
  138. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  139. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  140. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  141. package/types/lib/inventory/cbomutils.d.ts +17 -0
  142. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  143. package/types/lib/inventory/deps.d.ts.map +1 -1
  144. package/types/lib/inventory/dosai.d.ts +17 -1
  145. package/types/lib/inventory/dosai.d.ts.map +1 -1
  146. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  147. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  148. package/types/lib/inventory/envcontext.d.ts +52 -6
  149. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  150. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  151. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  152. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  153. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  154. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  155. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  156. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  157. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  158. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  159. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  160. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  161. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  162. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  163. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  164. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  165. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  166. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  167. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  168. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  169. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  170. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  171. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  172. package/types/lib/inventory/source.d.ts +10 -0
  173. package/types/lib/inventory/source.d.ts.map +1 -1
  174. package/types/lib/managers/binary.d.ts.map +1 -1
  175. package/types/lib/managers/docker.d.ts +12 -0
  176. package/types/lib/managers/docker.d.ts.map +1 -1
  177. package/types/lib/parsers/largeJson.d.ts +4 -3
  178. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  179. package/types/lib/server/server.d.ts.map +1 -1
  180. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  182. package/types/lib/stages/pregen/pregen.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,11 @@ 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";
32
38
  import { isValidIdnEmail } from "../parsers/email.js";
33
39
 
34
40
  // FIXME. This has to get removed, once we improve the module detection one-liner.
@@ -1563,6 +1569,40 @@ function listGemBindirFiles(gemspecFile, bindir = "bin") {
1563
1569
  }
1564
1570
  }
1565
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
+
1566
1606
  /**
1567
1607
  * Method to parse Gemspec file contents.
1568
1608
  *
@@ -1594,7 +1634,7 @@ export async function parseGemspecData(gemspecData, gemspecFile, options = {}) {
1594
1634
  if (gemspecFile) {
1595
1635
  pkg.name = basename(gemspecFile).replace(".gemspec", "");
1596
1636
  }
1597
- if (!gemspecData) {
1637
+ if (!gemspecData || isMarshalData(gemspecData)) {
1598
1638
  return pkgList;
1599
1639
  }
1600
1640
  let versionHackMatch = false;
@@ -1949,6 +1989,12 @@ export async function parseGemspecData(gemspecData, gemspecFile, options = {}) {
1949
1989
  }
1950
1990
  if (gemspecFile) {
1951
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
+ }
1952
1998
  // Did we find the version number from the directory name? Let's reduce the confidence and set the correct technique
1953
1999
  pkg.evidence = {
1954
2000
  identity: {
@@ -2861,30 +2907,434 @@ export function rubyRequirementToVers(requirements) {
2861
2907
  return `vers:gem/${constraints.map((c) => c.text).join("|")}`;
2862
2908
  }
2863
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
+
2864
2941
  /**
2865
- * Describe the Ruby runtime the gems require as one external `platform`
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
+
3104
+ /**
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`
2866
3306
  * component, and make the components that declare a `required_ruby_version`
2867
3307
  * depend on it (discussion 4409).
2868
3308
  *
2869
- * The component stands for the Ruby the bundle as a whole runs on, so its
2870
- * `versionRange` is the intersection of every requirement that could be read,
2871
- * in the `vers` syntax CycloneDX mandates for the field. Each component's own
2872
- * requirement stays in its `cdx:gem:rubyVersionSpecifiers` property. One
2873
- * runtime component per distinct requirement would describe the same
2874
- * 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.
2875
3323
  *
2876
3324
  * The type is `platform`, the CycloneDX type for a runtime environment that
2877
3325
  * interprets software, and the one cdxgen gives the Ruby it finds in the
2878
- * 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.
2879
3327
  *
2880
- * `isExternal` and `versionRange` are CycloneDX 1.7 fields, and without them
2881
- * the component would claim the product bundles its interpreter, so the caller
2882
- * 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.
2883
3332
  *
2884
3333
  * @param {Array} pkgList Components
2885
3334
  * @param {object} parentComponent The metadata.component, when the project is
2886
3335
  * itself a gem
2887
3336
  * @param {Array} dependencies Dependency edges
3337
+ * @param {object} [projectRuby] result of resolveProjectRuby
2888
3338
  * @returns {{pkgList: Array, dependencies: Array}} Components and edges with
2889
3339
  * the runtime component added
2890
3340
  */
@@ -2892,6 +3342,7 @@ export function addRubyRuntimeComponent(
2892
3342
  pkgList,
2893
3343
  parentComponent,
2894
3344
  dependencies,
3345
+ projectRuby,
2895
3346
  ) {
2896
3347
  const requirements = [];
2897
3348
  const declaringRefs = new Set();
@@ -2908,29 +3359,85 @@ export function addRubyRuntimeComponent(
2908
3359
  declaringRefs.add(component["bom-ref"]);
2909
3360
  }
2910
3361
  }
2911
- 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) {
2912
3369
  return { pkgList, dependencies };
2913
3370
  }
2914
- const runtimeRef = genericPurl("ruby");
2915
- const runtimeComponent = {
2916
- type: "platform",
2917
- name: "ruby",
2918
- scope: "required",
2919
- // The interpreter comes with the deployment environment, not with the
2920
- // product the BOM describes
2921
- isExternal: true,
2922
- purl: runtimeRef,
2923
- "bom-ref": runtimeRef,
2924
- };
2925
- const versionRange = rubyRequirementToVers(requirements);
2926
- if (versionRange) {
2927
- 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;
2928
3419
  } else {
2929
- thoughtLog(
2930
- "The Ruby version requirements of the gems exclude each other, so the Ruby runtime component carries no version range.",
2931
- { requirements },
2932
- );
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
+ }
2933
3439
  }
3440
+ const runtimeRef = runtimeComponent["bom-ref"];
2934
3441
  const describedRefs = new Set(dependencies.map((d) => d.ref));
2935
3442
  const outDependencies = dependencies.map((d) =>
2936
3443
  declaringRefs.has(d.ref)
@@ -7,6 +7,7 @@ import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
7
7
  import { isWin } from "../core/paths.js";
8
8
  import { isProjectToolingAllowed } from "../core/projectTooling.js";
9
9
  import { noteBuildToolRateLimit } from "../inventory/buildToolRateLimit.js";
10
+ import { coursierMirrorEnv } from "../inventory/mavenCentralMirror.js";
10
11
  import { parseScalaArtifact } from "../inventory/scalaCoords.js";
11
12
  import { completeComponent } from "./parsers-jvm.js";
12
13
 
@@ -297,6 +298,7 @@ export function collectScalaCliComponents(dirPath, options = {}) {
297
298
  shell: isWin,
298
299
  maxBuffer: 100 * 1024 * 1024,
299
300
  timeout: 600_000,
301
+ env: { ...process.env, ...coursierMirrorEnv() },
300
302
  });
301
303
  // scala-cli resolves through Coursier, so the class path fails with its
302
304
  // downloader message when the repository rate limits the machine.