@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
@@ -54,9 +54,20 @@ function applySpec(value, spec) {
54
54
  return value;
55
55
  }
56
56
  if (Array.isArray(value)) {
57
+ let elements = value;
58
+ if (spec.map) {
59
+ const mapped = [];
60
+ for (const element of elements) {
61
+ const transformed = spec.map(element);
62
+ if (transformed !== undefined) {
63
+ mapped.push(transformed);
64
+ }
65
+ }
66
+ elements = mapped;
67
+ }
57
68
  return spec.filter
58
- ? value.filter((element) => spec.filter(element))
59
- : value;
69
+ ? elements.filter((element) => spec.filter(element))
70
+ : elements;
60
71
  }
61
72
  if (!spec.members) {
62
73
  return value;
@@ -294,9 +305,10 @@ function scanObjectMembers(cursor, stopAt = undefined) {
294
305
  }
295
306
  }
296
307
 
297
- // Decode the array at the cursor in runs of whole elements, keeping those that
298
- // `filter` accepts.
299
- function readArrayElements(cursor, filter, batchBytes) {
308
+ // Decode the array at the cursor in runs of whole elements, mapping each one
309
+ // through `spec.map` (an undefined result drops it) and keeping those that
310
+ // `spec.filter` accepts.
311
+ function readArrayElements(cursor, spec, batchBytes) {
300
312
  if (cursor.nextToken() !== OPEN_BRACKET) {
301
313
  cursor.fail("Expected an array");
302
314
  }
@@ -310,8 +322,12 @@ function readArrayElements(cursor, filter, batchBytes) {
310
322
  for (const element of JSON.parse(
311
323
  `[${cursor.endCapture(cursor.pos - 1)}]`,
312
324
  )) {
313
- if (!filter || filter(element)) {
314
- elements.push(element);
325
+ const mapped = spec?.map ? spec.map(element) : element;
326
+ if (mapped === undefined) {
327
+ continue;
328
+ }
329
+ if (!spec?.filter || spec.filter(mapped)) {
330
+ elements.push(mapped);
315
331
  }
316
332
  }
317
333
  };
@@ -367,7 +383,7 @@ function decodeRange(fd, start, end, spec, options) {
367
383
  );
368
384
  }
369
385
  } else if (first === OPEN_BRACKET) {
370
- value = readArrayElements(cursor, spec?.filter, options.batchBytes);
386
+ value = readArrayElements(cursor, spec, options.batchBytes);
371
387
  } else {
372
388
  // A string or number this large has no smaller parts; JSON.parse reports
373
389
  // it the way it would for the whole file.
@@ -449,9 +465,10 @@ export function readJsonMember(filePath, name, options = {}) {
449
465
  * more than a bounded run of it in one string.
450
466
  *
451
467
  * `spec` narrows what is kept. `{ members: { name: spec } }` keeps only the
452
- * named members of an object, each narrowed by its own spec. `{ filter }`
453
- * keeps the elements of an array that `filter(element)` accepts. Anything else
454
- * keeps the whole value.
468
+ * named members of an object, each narrowed by its own spec. `{ map }`
469
+ * transforms each element of an array, an undefined result dropping it, and
470
+ * `{ filter }` keeps the mapped elements that `filter(element)` accepts.
471
+ * Anything else keeps the whole value.
455
472
  *
456
473
  * @param {string} filePath JSON file
457
474
  * @param {{start: number, end: number}} range Byte range of the value, such as one returned by indexJsonObject
@@ -0,0 +1,183 @@
1
+ // The one place that reads the identity of an installed VS Code-family
2
+ // extension from the name of its folder.
3
+ //
4
+ // An editor installs an extension into `<publisher>.<name>-<version>`, and a
5
+ // platform-specific build into `<publisher>.<name>-<version>-<platform>`, for
6
+ // example `anthropic.claude-code-2.1.313-darwin-arm64`. The extension scan and
7
+ // the osquery `vscode_extensions` rows both name the extension from this, so an
8
+ // extension found by `cdxgen -t vscode-extension` and by `obom` is the same
9
+ // component with the same purl.
10
+
11
+ /**
12
+ * Target platforms a VS Code-family marketplace publishes an extension for.
13
+ * `universal` and `web` builds are the same artefact on every platform and are
14
+ * not part of the extension's identity.
15
+ */
16
+ export const VSCODE_TARGET_PLATFORMS = new Set([
17
+ "win32-x64",
18
+ "win32-arm64",
19
+ "win32-ia32",
20
+ "linux-x64",
21
+ "linux-arm64",
22
+ "linux-armhf",
23
+ "alpine-x64",
24
+ "alpine-arm64",
25
+ "darwin-x64",
26
+ "darwin-arm64",
27
+ ]);
28
+
29
+ // Folder suffixes for builds that run on every platform.
30
+ const NEUTRAL_PLATFORMS = ["universal", "web"];
31
+
32
+ /**
33
+ * Normalize a target platform value from a manifest, an `extensions.json`
34
+ * record, or a folder name. Values that do not name a specific platform, such
35
+ * as `universal`, `web`, `undefined`, or an empty string, return an empty
36
+ * string.
37
+ *
38
+ * @param {string} value Raw target platform
39
+ * @returns {string} A value from VSCODE_TARGET_PLATFORMS, or an empty string
40
+ */
41
+ export function normalizeVscodeTargetPlatform(value) {
42
+ const platform = `${value || ""}`.trim().toLowerCase();
43
+ return VSCODE_TARGET_PLATFORMS.has(platform) ? platform : "";
44
+ }
45
+
46
+ /**
47
+ * Split an installed extension folder name into its publisher, name, version
48
+ * and target platform.
49
+ *
50
+ * @param {string} dirName Folder name, such as `ms-python.python-2025.4.0` or
51
+ * `anthropic.claude-code-2.1.313-darwin-arm64`
52
+ * @returns {{publisher: string, name: string, version: string, platform: string}|undefined}
53
+ * The parts, or undefined when the name does not follow the pattern
54
+ */
55
+ export function parseVscodeExtensionDirName(dirName) {
56
+ let value = `${dirName || ""}`.trim();
57
+ while (value.endsWith("/") || value.endsWith("\\")) {
58
+ value = value.slice(0, -1);
59
+ }
60
+ const dotIdx = value.indexOf(".");
61
+ if (dotIdx < 1) {
62
+ return undefined;
63
+ }
64
+ const publisher = value.substring(0, dotIdx);
65
+ let rest = value.substring(dotIdx + 1);
66
+ let platform = "";
67
+ for (const candidate of [...VSCODE_TARGET_PLATFORMS, ...NEUTRAL_PLATFORMS]) {
68
+ if (rest.toLowerCase().endsWith(`-${candidate}`)) {
69
+ platform = normalizeVscodeTargetPlatform(candidate);
70
+ rest = rest.substring(0, rest.length - candidate.length - 1);
71
+ break;
72
+ }
73
+ }
74
+ // Marketplaces require semver versions, so the version starts at the first
75
+ // hyphen whose remainder is a whole semver: `python-3.11-tools-1.0.0` is the
76
+ // name `python-3.11-tools` at 1.0.0, and `1.0.0-beta-2` stays one version.
77
+ // A folder whose version is not semver falls back to the first hyphen
78
+ // followed by digits and a dot.
79
+ let versionStart = -1;
80
+ let fallbackStart = -1;
81
+ for (let i = 1; i < rest.length - 2; i++) {
82
+ if (rest[i] !== "-" || !isDigit(rest[i + 1])) {
83
+ continue;
84
+ }
85
+ if (isSemver(rest.substring(i + 1))) {
86
+ versionStart = i;
87
+ break;
88
+ }
89
+ let j = i + 1;
90
+ while (j < rest.length && isDigit(rest[j])) {
91
+ j++;
92
+ }
93
+ if (fallbackStart < 0 && rest[j] === ".") {
94
+ fallbackStart = i;
95
+ }
96
+ }
97
+ if (versionStart < 0) {
98
+ versionStart = fallbackStart;
99
+ }
100
+ if (versionStart < 1) {
101
+ return undefined;
102
+ }
103
+ const name = rest.substring(0, versionStart);
104
+ const version = rest.substring(versionStart + 1);
105
+ if (!name || !version) {
106
+ return undefined;
107
+ }
108
+ return { publisher, name, version, platform };
109
+ }
110
+
111
+ function isDigit(character) {
112
+ return character >= "0" && character <= "9";
113
+ }
114
+
115
+ function isIdentifierList(text, numericNoLeadingZero) {
116
+ if (!text) {
117
+ return false;
118
+ }
119
+ return text.split(".").every((part) => {
120
+ if (!part.length) {
121
+ return false;
122
+ }
123
+ for (const character of part) {
124
+ const ok =
125
+ isDigit(character) ||
126
+ character === "-" ||
127
+ (character >= "a" && character <= "z") ||
128
+ (character >= "A" && character <= "Z");
129
+ if (!ok) {
130
+ return false;
131
+ }
132
+ }
133
+ if (numericNoLeadingZero && [...part].every(isDigit)) {
134
+ return part === "0" || part[0] !== "0";
135
+ }
136
+ return true;
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Check a string is a semantic version: three numeric parts, then an optional
142
+ * `-prerelease` and `+build`.
143
+ *
144
+ * @param {string} text Candidate version
145
+ * @returns {boolean} true for a semver
146
+ */
147
+ export function isSemver(text) {
148
+ const plus = text.indexOf("+");
149
+ const core = plus >= 0 ? text.substring(0, plus) : text;
150
+ if (plus >= 0 && !isIdentifierList(text.substring(plus + 1), false)) {
151
+ return false;
152
+ }
153
+ const dash = core.indexOf("-");
154
+ const numbers = dash >= 0 ? core.substring(0, dash) : core;
155
+ if (dash >= 0 && !isIdentifierList(core.substring(dash + 1), true)) {
156
+ return false;
157
+ }
158
+ const parts = numbers.split(".");
159
+ return (
160
+ parts.length === 3 &&
161
+ parts.every(
162
+ (part) =>
163
+ part.length > 0 &&
164
+ [...part].every(isDigit) &&
165
+ (part === "0" || part[0] !== "0"),
166
+ )
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Read the target platform from the path of an installed extension folder.
172
+ *
173
+ * @param {string} extensionPath Path to the extension folder, with either
174
+ * separator
175
+ * @returns {string} The target platform, or an empty string
176
+ */
177
+ export function vscodeExtensionPlatformFromPath(extensionPath) {
178
+ const segments = `${extensionPath || ""}`
179
+ .split(/[/\\]/u)
180
+ .filter((segment) => segment.length);
181
+ const dirName = segments[segments.length - 1];
182
+ return parseVscodeExtensionDirName(dirName)?.platform || "";
183
+ }
@@ -659,7 +659,18 @@ const start = (options) => {
659
659
  );
660
660
  }
661
661
  }
662
- cloneDir = gitClone(sourcePath, gitRef);
662
+ try {
663
+ cloneDir = gitClone(sourcePath, gitRef);
664
+ } catch (_err) {
665
+ console.log("Unable to clone repository. See the server log.");
666
+ res.writeHead(500, { "Content-Type": "application/json" });
667
+ return res.end(
668
+ JSON.stringify({
669
+ error: "Unable to clone repository",
670
+ details: "Cloning the repository failed. See the server log.",
671
+ }),
672
+ );
673
+ }
663
674
  srcDir = cloneDir;
664
675
  if (purlResolution?.type === "npm") {
665
676
  const cloneRootDir = cloneDir;
@@ -672,16 +672,21 @@ async function interpolateTemplate(template, context, options = {}) {
672
672
  return template;
673
673
  }
674
674
  const templateRegex = /\{\{\s*([^}]+)\s*}}/g;
675
- let result = template;
676
- const matches = [...template.matchAll(templateRegex)];
677
- 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)) {
678
681
  const [fullMatch, expr] = match;
682
+ let replacement = fullMatch;
679
683
  try {
680
684
  const expression = jsonata(expr.trim());
681
685
  registerCdxHelpers(expression, options);
682
686
  const value = await safeEvaluate(expression, context);
683
- const replacement = value !== undefined ? String(value) : fullMatch;
684
- result = result.replace(fullMatch, replacement);
687
+ if (value !== undefined) {
688
+ replacement = String(value);
689
+ }
685
690
  } catch (err) {
686
691
  if (DEBUG_MODE) {
687
692
  console.warn(
@@ -690,7 +695,10 @@ async function interpolateTemplate(template, context, options = {}) {
690
695
  );
691
696
  }
692
697
  }
698
+ result += template.slice(cursor, match.index) + replacement;
699
+ cursor = match.index + fullMatch.length;
693
700
  }
701
+ result += template.slice(cursor);
694
702
  return result;
695
703
  }
696
704
 
@@ -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",
@@ -29,6 +29,8 @@ import {
29
29
  } from "../../core/fs.js";
30
30
  import { isMac, isWin } from "../../core/paths.js";
31
31
  import { isProjectToolingAllowed } from "../../core/projectTooling.js";
32
+ import { recordToolVersion } from "../../core/toolRegistry.js";
33
+ import { collectProjectRubyDeclarations } from "../../ecosystems/rubyutils.js";
32
34
  import {
33
35
  dosaiDotnetRequirement,
34
36
  dosaiDotnetRequirementAdvice,
@@ -45,6 +47,7 @@ import {
45
47
  installSdkmanTool,
46
48
  isNvmAvailable,
47
49
  isRbenvAvailable,
50
+ isRubyVersionActive,
48
51
  isSdkmanAvailable,
49
52
  isSdkmanToolAvailable,
50
53
  performBundleInstall,
@@ -65,6 +68,7 @@ import {
65
68
  readSbtBuildPropertiesVersion,
66
69
  readToolVersionEnvHint,
67
70
  readToolVersionOverride,
71
+ resolveGradleVersionForJava,
68
72
  resolvePartialVersion,
69
73
  } from "../../inventory/jvmToolEnv.js";
70
74
  import { resolvePluginBinary } from "../../inventory/plugins.js";
@@ -316,6 +320,27 @@ const JVM_TOOL_DEFAULT_PREFIXES = {
316
320
  maven: "3.9",
317
321
  };
318
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
+
319
344
  /** sdkman candidates cdxgen can provision for JVM builds. */
320
345
  const JVM_BUILD_TOOLS = ["maven", "gradle", "sbt", "scala"];
321
346
 
@@ -400,10 +425,26 @@ export function prepareJvmBuildEnv(filePath, options) {
400
425
  return;
401
426
  }
402
427
  // Resolve partial versions (e.g. maven3.9 -> 3.9.16) before touching the JDK
403
- // 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;
404
445
  const resolvedPins = [];
405
446
  for (const pin of installPins) {
406
- const resolved = resolveJvmToolPinVersion(pin);
447
+ const resolved = resolveJvmToolPinVersion(pin, gradleJavaMajor);
407
448
  if (resolved) {
408
449
  resolvedPins.push(resolved);
409
450
  }
@@ -422,6 +463,11 @@ export function prepareJvmBuildEnv(filePath, options) {
422
463
  const installed = provisionJvmTool(pin, options);
423
464
  if (installed) {
424
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
+ }
425
471
  if (pin.source === "cli") {
426
472
  pinnedToolSelected = true;
427
473
  }
@@ -731,11 +777,14 @@ function recordDetectedJvmPins(installPins, wrapperVersions) {
731
777
  * and to pick defaults.
732
778
  *
733
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.
734
783
  *
735
784
  * @returns {Object|undefined} Pin with an exact version, or undefined when no
736
785
  * version could be determined.
737
786
  */
738
- export function resolveJvmToolPinVersion(pin) {
787
+ export function resolveJvmToolPinVersion(pin, javaMajor) {
739
788
  if (pin.version && isSdkmanToolAvailable(pin.tool, pin.version)) {
740
789
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
741
790
  ecosystem: "java",
@@ -766,20 +815,30 @@ export function resolveJvmToolPinVersion(pin) {
766
815
  return undefined;
767
816
  }
768
817
  const prefix = pin.version || JVM_TOOL_DEFAULT_PREFIXES[pin.tool] || "";
769
- const resolved = resolvePartialVersion(versionList, prefix);
818
+ const resolved =
819
+ pin.tool === "gradle" && !pin.version
820
+ ? resolveGradleVersionForJava(versionList, javaMajor)
821
+ : resolvePartialVersion(versionList, prefix);
770
822
  if (!resolved) {
771
823
  if (pin.version) {
772
824
  return pin;
773
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}'.`;
774
833
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
775
834
  ecosystem: "java",
776
835
  tool: pin.tool,
777
- wanted: prefix,
836
+ wanted: gradleMinimum ? `>=${gradleMinimum}` : prefix,
778
837
  source: "sdkman",
779
- detail: `No stable ${pin.tool} version matched the default prefix '${prefix}'.`,
838
+ detail: missingDetail,
780
839
  });
781
840
  console.log(
782
- `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>.`,
783
842
  );
784
843
  return undefined;
785
844
  }
@@ -1582,6 +1641,17 @@ export function prepareSwiftEnv(filePath, options) {
1582
1641
  * @param {String} filePath Path
1583
1642
  * @param {Object} options CLI Options
1584
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
+
1585
1655
  export function prepareRubyEnv(filePath, options) {
1586
1656
  // Skip preparation early
1587
1657
  if (
@@ -1627,53 +1697,39 @@ export function prepareRubyEnv(filePath, options) {
1627
1697
  readEnvironmentVariable("GEM_HOME") ||
1628
1698
  safeMkdtempSync(join(getTmpDir(), "cdxgen-gem-home-"));
1629
1699
  process.env.CDXGEN_GEM_HOME = cdxgenGemHome;
1630
- // Is there a .ruby-version file in the project?
1631
- if (safeExistsSync(join(filePath, ".ruby-version"))) {
1632
- rubyVersionNeeded = readFileSync(join(filePath, ".ruby-version"), {
1633
- encoding: "utf-8",
1634
- })
1635
- .trim()
1636
- .replace("ruby-", "");
1637
- if (rubyVersionNeeded) {
1638
- recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
1639
- ecosystem: "ruby",
1640
- tool: "ruby",
1641
- wanted: rubyVersionNeeded,
1642
- source: ".ruby-version",
1643
- path: join(filePath, ".ruby-version"),
1644
- });
1645
- }
1646
- } else if (safeExistsSync(join(filePath, "Gemfile.lock"))) {
1647
- // 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"))) {
1648
1730
  const gemlockData = readFileSync(join(filePath, "Gemfile.lock"), {
1649
1731
  encoding: "utf-8",
1650
1732
  });
1651
- let rubyVersionMarker = false;
1652
- for (let l of gemlockData.split("\n")) {
1653
- l = l.replaceAll("\r", "").trim();
1654
- if (l.includes("RUBY VERSION")) {
1655
- rubyVersionMarker = true;
1656
- }
1657
- if (rubyVersionMarker && l.includes("ruby ")) {
1658
- const possibleVersion = l
1659
- .split("ruby ")
1660
- .pop()
1661
- .split("p")[0]
1662
- .split("d")[0];
1663
- if (/^\d+/.test(possibleVersion)) {
1664
- rubyVersionNeeded = possibleVersion;
1665
- break;
1666
- }
1667
- }
1668
- }
1669
- if (rubyVersionNeeded) {
1670
- recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
1671
- ecosystem: "ruby",
1672
- tool: "ruby",
1673
- wanted: rubyVersionNeeded,
1674
- source: "Gemfile.lock",
1675
- });
1676
- }
1677
1733
  const bundlerVersion = parseGemfileLockBundlerVersion(gemlockData);
1678
1734
  if (bundlerVersion) {
1679
1735
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_EXPECTED, {
@@ -1683,7 +1739,8 @@ export function prepareRubyEnv(filePath, options) {
1683
1739
  source: "Gemfile.lock",
1684
1740
  });
1685
1741
  }
1686
- } else {
1742
+ }
1743
+ if (!installable) {
1687
1744
  // Is the user invoking with a ruby with custom version type. Eg: -t ruby2.5.4
1688
1745
  let projectTypes = options.projectType;
1689
1746
  if (
@@ -1716,13 +1773,13 @@ export function prepareRubyEnv(filePath, options) {
1716
1773
  const existingRuby = collectRubyInfo(filePath);
1717
1774
  if (
1718
1775
  rubyVersionNeeded &&
1719
- existingRuby?.version?.startsWith(`ruby ${rubyVersionNeeded} `)
1776
+ isRubyVersionActive(existingRuby, rubyVersionNeeded)
1720
1777
  ) {
1721
1778
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
1722
1779
  ecosystem: "ruby",
1723
1780
  tool: "ruby",
1724
1781
  wanted: rubyVersionNeeded,
1725
- found: existingRuby.version,
1782
+ found: existingRuby.description || existingRuby.version,
1726
1783
  source: "env",
1727
1784
  detail: `The active interpreter is the required Ruby ${rubyVersionNeeded}.`,
1728
1785
  });
@@ -1754,15 +1811,15 @@ export function prepareRubyEnv(filePath, options) {
1754
1811
  });
1755
1812
  } else if (
1756
1813
  rubyVersionNeeded &&
1757
- !existingRuby.version.startsWith(`ruby ${rubyVersionNeeded} `)
1814
+ !isRubyVersionActive(existingRuby, rubyVersionNeeded)
1758
1815
  ) {
1759
1816
  recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
1760
1817
  ecosystem: "ruby",
1761
1818
  tool: "ruby",
1762
1819
  wanted: rubyVersionNeeded,
1763
- found: existingRuby.version,
1820
+ found: existingRuby.description || existingRuby.version,
1764
1821
  source: "env",
1765
- 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}.`,
1766
1823
  });
1767
1824
  }
1768
1825
  if (rubyVersionNeeded && !rbenvPresent) {
@@ -263,10 +263,12 @@ const getSpdxExportSchemaValidator = () => {
263
263
  * Validate the generated bom using jsonschema
264
264
  *
265
265
  * @param {object} bomJson content
266
+ * @param {object} [options] Validation options
267
+ * @param {boolean} [options.deep] Run the deep validators after the schema check. Defaults to true.
266
268
  *
267
269
  * @returns {Promise<Boolean>} true if the BOM is valid. false otherwise.
268
270
  */
269
- export const validateBom = async (bomJson) => {
271
+ export const validateBom = async (bomJson, options = {}) => {
270
272
  if (!bomJson) {
271
273
  return true;
272
274
  }
@@ -292,6 +294,9 @@ export const validateBom = async (bomJson) => {
292
294
  console.log(validate.errors);
293
295
  return false;
294
296
  }
297
+ if (options.deep === false) {
298
+ return true;
299
+ }
295
300
  // Deep validation tests
296
301
  return (
297
302
  validateMetadata(bomJson) &&