@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
@@ -0,0 +1,217 @@
1
+ // Bounded reader for the .NET shared frameworks installed in a root file
2
+ // system.
3
+ //
4
+ // The .NET Docker images, dotnet-install.sh and the tarballs put the runtime
5
+ // under <dotnet root>/shared/<framework>/<version>/ without a package manager,
6
+ // so a BOM sees only its files. Each framework directory carries a
7
+ // <framework>.deps.json whose runtime target names the runtime pack it was
8
+ // built from, e.g. `Microsoft.NETCore.App.Runtime.linux-arm64/8.0.32`. That pack
9
+ // is a NuGet package, and it is the identity .NET security advisories list, so
10
+ // it becomes the component's purl.
11
+ // https://learn.microsoft.com/dotnet/core/dependency-loading/default-probing#how-are-the-properties-populated
12
+
13
+ import { closeSync, readSync } from "node:fs";
14
+ import { join } from "node:path";
15
+
16
+ import { globSync } from "glob";
17
+
18
+ import { openRegularFile } from "../core/boundedFile.js";
19
+ import { applyPurl, tryBuildPurl } from "./purl.js";
20
+
21
+ const MAX_DEPS_JSON_BYTES = 8 * 1024 * 1024;
22
+ const MAX_FRAMEWORKS = 64;
23
+ const FRAMEWORKS = new Set([
24
+ "Microsoft.NETCore.App",
25
+ "Microsoft.AspNetCore.App",
26
+ "Microsoft.WindowsDesktop.App",
27
+ ]);
28
+ // Where the .NET images, dotnet-install.sh, the tarballs and the distro
29
+ // packages put the dotnet root.
30
+ const DOTNET_ROOTS = [
31
+ "usr/share/dotnet",
32
+ "usr/lib/dotnet",
33
+ "usr/lib64/dotnet",
34
+ "usr/local/share/dotnet",
35
+ "opt/dotnet",
36
+ "root/.dotnet",
37
+ "home/*/.dotnet",
38
+ ];
39
+ const RUNTIME_PACK_RE =
40
+ /^(Microsoft\.[A-Za-z]+(?:\.[A-Za-z]+)*\.App\.Runtime\.[A-Za-z0-9.-]+)\/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
41
+
42
+ function readJsonBounded(absPath, rootDir) {
43
+ const opened = openRegularFile(absPath, rootDir);
44
+ if (!opened) {
45
+ return undefined;
46
+ }
47
+ try {
48
+ if (opened.size <= 0 || opened.size > MAX_DEPS_JSON_BYTES) {
49
+ return undefined;
50
+ }
51
+ const buffer = Buffer.allocUnsafe(opened.size);
52
+ const read = readSync(opened.fd, buffer, 0, opened.size, 0);
53
+ return JSON.parse(buffer.toString("utf8", 0, read));
54
+ } catch {
55
+ return undefined;
56
+ } finally {
57
+ try {
58
+ closeSync(opened.fd);
59
+ } catch {
60
+ // already closed
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Read the runtime pack a shared framework's deps.json was built from.
67
+ *
68
+ * @param {Object} depsJson parsed <framework>.deps.json
69
+ * @param {string} framework framework directory name
70
+ * @returns {{pack: string, version: string, rid: string}|undefined}
71
+ */
72
+ export function runtimePackFromDepsJson(depsJson, framework) {
73
+ const targetName = depsJson?.runtimeTarget?.name;
74
+ if (typeof targetName !== "string") {
75
+ return undefined;
76
+ }
77
+ const target = depsJson?.targets?.[targetName];
78
+ if (!target || typeof target !== "object") {
79
+ return undefined;
80
+ }
81
+ for (const key of Object.keys(target)) {
82
+ const match = RUNTIME_PACK_RE.exec(key);
83
+ if (!match?.[1].startsWith(`${framework}.Runtime.`)) {
84
+ continue;
85
+ }
86
+ return {
87
+ pack: match[1],
88
+ version: match[2],
89
+ rid: match[1].slice(`${framework}.Runtime.`.length),
90
+ };
91
+ }
92
+ return undefined;
93
+ }
94
+
95
+ /**
96
+ * The CPE NVD uses for a shared framework version: `.net` from 5.0 on,
97
+ * `.net_core` before it, and `asp.net_core` for ASP.NET Core.
98
+ *
99
+ * @param {string} framework framework directory name
100
+ * @param {string} version runtime version
101
+ * @returns {string|undefined} CPE 2.3 string
102
+ */
103
+ export function dotnetCpe(framework, version) {
104
+ const base = /^(\d+)\.\d+\.\d+/.exec(version || "");
105
+ if (!base) {
106
+ return undefined;
107
+ }
108
+ const plain = base[0];
109
+ if (framework === "Microsoft.AspNetCore.App") {
110
+ return `cpe:2.3:a:microsoft:asp.net_core:${plain}:*:*:*:*:*:*:*`;
111
+ }
112
+ if (framework === "Microsoft.NETCore.App") {
113
+ const product = Number(base[1]) >= 5 ? ".net" : ".net_core";
114
+ return `cpe:2.3:a:microsoft:${product}:${plain}:*:*:*:*:*:*:*`;
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ /**
120
+ * Create the platform component for one installed shared framework.
121
+ *
122
+ * @param {string} framework framework directory name
123
+ * @param {{pack: string, version: string, rid: string}} runtime runtime pack
124
+ * @param {string} location root-relative path of the deps.json
125
+ * @returns {Object|undefined} component, or undefined when no purl can be built
126
+ */
127
+ export function createDotnetRuntimeComponent(framework, runtime, location) {
128
+ const component = {
129
+ type: "platform",
130
+ name: runtime.pack,
131
+ version: runtime.version,
132
+ properties: [
133
+ { name: "cdx:dotnet:sharedFramework", value: framework },
134
+ { name: "cdx:dotnet:runtimeIdentifier", value: runtime.rid },
135
+ ],
136
+ evidence: {
137
+ occurrences: [{ location: `/${location.replace(/^\/+/, "")}` }],
138
+ },
139
+ };
140
+ const cpe = dotnetCpe(framework, runtime.version);
141
+ if (cpe) {
142
+ component.cpe = cpe;
143
+ }
144
+ const purl = tryBuildPurl({
145
+ type: "nuget",
146
+ name: runtime.pack,
147
+ version: runtime.version,
148
+ });
149
+ if (!purl) {
150
+ return undefined;
151
+ }
152
+ return applyPurl(component, purl);
153
+ }
154
+
155
+ /**
156
+ * Find the .NET shared frameworks installed under a root that no package
157
+ * manager owns, and return one platform component per runtime pack.
158
+ *
159
+ * @param {string} rootDir extracted root file system
160
+ * @param {string[]} [ownedPaths] root-relative paths a package database owns
161
+ * @returns {Object[]} platform components
162
+ */
163
+ export function collectDotnetRuntimes(rootDir, ownedPaths = []) {
164
+ if (!rootDir) {
165
+ return [];
166
+ }
167
+ const owned = new Set(
168
+ (ownedPaths || []).map((p) => p.replace(/^\/+/, "").replaceAll("\\", "/")),
169
+ );
170
+ let depsFiles = [];
171
+ try {
172
+ depsFiles = globSync(
173
+ DOTNET_ROOTS.map((root) => `${root}/shared/*/*/*.deps.json`),
174
+ {
175
+ cwd: rootDir,
176
+ absolute: false,
177
+ nodir: true,
178
+ dot: true,
179
+ follow: false,
180
+ },
181
+ )
182
+ .map((file) => file.replaceAll("\\", "/"))
183
+ .sort();
184
+ } catch {
185
+ return [];
186
+ }
187
+ const components = [];
188
+ const seen = new Set();
189
+ for (const file of depsFiles.slice(0, MAX_FRAMEWORKS)) {
190
+ if (owned.has(file)) {
191
+ continue;
192
+ }
193
+ const parts = file.split("/");
194
+ const framework = parts[parts.length - 3];
195
+ if (
196
+ !FRAMEWORKS.has(framework) ||
197
+ parts.at(-1) !== `${framework}.deps.json`
198
+ ) {
199
+ continue;
200
+ }
201
+ const runtime = runtimePackFromDepsJson(
202
+ readJsonBounded(join(rootDir, file), rootDir),
203
+ framework,
204
+ );
205
+ // The directory a runtime lives in is named after its version.
206
+ if (!runtime || runtime.version !== parts[parts.length - 2]) {
207
+ continue;
208
+ }
209
+ const component = createDotnetRuntimeComponent(framework, runtime, file);
210
+ if (!component || seen.has(component["bom-ref"])) {
211
+ continue;
212
+ }
213
+ seen.add(component["bom-ref"]);
214
+ components.push(component);
215
+ }
216
+ return components;
217
+ }
@@ -1,6 +1,6 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { arch, homedir } from "node:os";
3
- import { basename, delimiter, dirname, join } from "node:path";
3
+ import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
4
4
  import process from "node:process";
5
5
 
6
6
  import { compareLoose } from "semver";
@@ -19,10 +19,12 @@ import {
19
19
  recordLedgerEvent,
20
20
  } from "../core/buildLedger.js";
21
21
  import {
22
+ BUNDLE_CMD,
22
23
  CARGO_CMD,
23
24
  COMPOSER_CMD,
24
25
  DOTNET_CMD,
25
26
  GCC_CMD,
27
+ GEM_CMD,
26
28
  GO_CMD,
27
29
  getJavaCommand,
28
30
  getPythonCommand,
@@ -44,12 +46,19 @@ import {
44
46
  import { diagnosticStream } from "../core/logger.js";
45
47
  import { dirNameStr, isMac, isWin } from "../core/paths.js";
46
48
  import { isProjectToolingAllowed } from "../core/projectTooling.js";
49
+ import { brotliDecoderProperties, readBrotliDecoder } from "./brotliDecoder.js";
47
50
  import { isValidSdkmanVersion } from "./jvmToolEnv.js";
51
+ import { createNodeRuntimeComponent } from "./nodeBuildInfo.js";
48
52
  import {
49
53
  createPhpRuntimeComponent,
54
+ packageStoreOf,
50
55
  parsePhpReportedVersion,
51
56
  } from "./phpBuildInfo.js";
52
57
  import { applyPurl, tryBuildPurl } from "./purl.js";
58
+ import {
59
+ createRubyRuntimeComponent,
60
+ parseRubyDescription,
61
+ } from "./rubyBuildInfo.js";
53
62
  import {
54
63
  classifySpawnRestriction,
55
64
  describeSpawnRestriction,
@@ -137,6 +146,11 @@ export const SDKMAN_JAVA_TOOL_ALIASES = {
137
146
  java24: readEnvironmentVariable("JAVA24_TOOL") || "24.0.2-tem",
138
147
  java25: readEnvironmentVariable("JAVA25_TOOL") || "25.0.4-tem",
139
148
  java26: readEnvironmentVariable("JAVA26_TOOL") || "26.0.2-tem",
149
+ // The broker serves no Java 27 build yet (every vendor id 404s), so java27
150
+ // has an alias only when JAVA27_TOOL names one that is downloadable.
151
+ ...(readEnvironmentVariable("JAVA27_TOOL")
152
+ ? { java27: readEnvironmentVariable("JAVA27_TOOL") }
153
+ : {}),
140
154
  };
141
155
 
142
156
  /**
@@ -486,6 +500,16 @@ export function collectNodeInfo(dir) {
486
500
  version: runtimeInfo.version,
487
501
  components: runtimeInfo.components,
488
502
  };
503
+ if (runtimeInfo.runtime === "Node.js" && runtimeInfo.version) {
504
+ // The same identity a scanned image's Node.js gets, without a bom-ref, as
505
+ // for the other toolchains.
506
+ const runtime = formulationToolchain(
507
+ createNodeRuntimeComponent(runtimeInfo.version),
508
+ );
509
+ nodeInfo.purl = runtime.purl;
510
+ nodeInfo.cpe = runtime.cpe;
511
+ annotateNodeBrotli(nodeInfo.components);
512
+ }
489
513
  const moduleDesc = getCommandOutput(NPM_CMD, dir, ["--version"]);
490
514
  if (moduleDesc) {
491
515
  nodeInfo.description = `npm: ${moduleDesc}`;
@@ -496,6 +520,36 @@ export function collectNodeInfo(dir) {
496
520
  return nodeInfo;
497
521
  }
498
522
 
523
+ /**
524
+ * Record whether the Brotli decoder the running Node.js uses carries the
525
+ * CVE-2026-111497 fix. process.versions.brotli cannot say: Node.js backports
526
+ * the fix without changing it. The decoder is read where it lives, in the node
527
+ * binary or in the shared libbrotlidec a `--shared-brotli` build loads.
528
+ *
529
+ * @param {Object[]|undefined} components bundled components of the runtime
530
+ */
531
+ function annotateNodeBrotli(components) {
532
+ const brotli = (components || []).find(
533
+ (component) => component.name === "brotli",
534
+ );
535
+ if (!brotli) {
536
+ return;
537
+ }
538
+ let decoderFile = globalThis.process?.execPath;
539
+ if (nodeComponentLinkage("brotli") === "shared") {
540
+ decoderFile = (
541
+ globalThis.process?.report?.getReport?.()?.sharedObjects || []
542
+ ).find((path) => basename(path).startsWith("libbrotlidec"));
543
+ }
544
+ const decoder = decoderFile ? readBrotliDecoder(decoderFile) : undefined;
545
+ if (decoder) {
546
+ brotli.properties = [
547
+ ...(brotli.properties || []),
548
+ ...brotliDecoderProperties(decoder),
549
+ ];
550
+ }
551
+ }
552
+
499
553
  /**
500
554
  * Collect gcc version
501
555
  *
@@ -585,23 +639,215 @@ export function collectSwiftInfo(dir) {
585
639
  }
586
640
 
587
641
  /**
588
- * Collect Ruby version
642
+ * The real path of the executable a command name runs, looked up on PATH as
643
+ * the shell would, or undefined when it cannot be found.
644
+ *
645
+ * @param {string} command command name or path
646
+ * @returns {string|undefined}
647
+ */
648
+ function resolveExecutable(command) {
649
+ const name = `${command || ""}`.trim().split(" ")[0];
650
+ if (!name) {
651
+ return undefined;
652
+ }
653
+ const extensions = isWin
654
+ ? ["", ...`${process.env.PATHEXT || ".EXE;.CMD;.BAT"}`.split(";")]
655
+ : [""];
656
+ const directories = isAbsolute(name)
657
+ ? [dirname(name)]
658
+ : `${process.env.PATH || ""}`.split(delimiter).filter(Boolean);
659
+ const base = isAbsolute(name) ? basename(name) : name;
660
+ for (const directory of directories) {
661
+ for (const extension of extensions) {
662
+ const candidate = join(directory, `${base}${extension}`);
663
+ try {
664
+ if (statSync(candidate).isFile()) {
665
+ return realpathSync(candidate);
666
+ }
667
+ } catch {
668
+ // not here
669
+ }
670
+ }
671
+ }
672
+ return undefined;
673
+ }
674
+
675
+ /**
676
+ * Whether a Ruby executable is one the operating system or a package manager
677
+ * that patches it owns. Distros backport fixes under the old upstream version,
678
+ * so its version must not be matched against upstream advisories by CPE.
679
+ *
680
+ * @param {string|undefined} realPath real path of the executable
681
+ * @returns {boolean}
682
+ */
683
+ export function isPackagedRubyPath(realPath) {
684
+ if (!realPath || isWin) {
685
+ return false;
686
+ }
687
+ // Homebrew builds the upstream release, so it keeps the CPE, as it does in
688
+ // an OBOM; Nix and Guix patch theirs.
689
+ const store = packageStoreOf(realPath);
690
+ if (store) {
691
+ return store !== "homebrew";
692
+ }
693
+ return ["/usr/bin/", "/bin/", "/usr/lib/", "/usr/libexec/", "/System/"].some(
694
+ (prefix) => realPath.startsWith(prefix),
695
+ );
696
+ }
697
+
698
+ // The shims of rbenv, asdf and mise run whichever Ruby is selected, which can
699
+ // be the distro's (`rbenv global system`).
700
+ function isVersionManagerShim(realPath) {
701
+ return `${realPath}`.split(/[\\/]/).includes("shims");
702
+ }
703
+
704
+ // Whether the Ruby `ruby --version` described may carry its release's CPE.
705
+ // A shim is followed to the interpreter it runs; one that cannot be followed
706
+ // keeps no CPE, since it may be the distro's.
707
+ function rubyKeepsCpe(dir) {
708
+ let executable = resolveExecutable(RUBY_CMD);
709
+ if (executable && isVersionManagerShim(executable)) {
710
+ const target = isWin
711
+ ? ""
712
+ : firstOutputLine(
713
+ getCommandOutput(RUBY_CMD, dir, ["-e", "print RbConfig.ruby"]),
714
+ );
715
+ if (!isAbsolute(target)) {
716
+ return false;
717
+ }
718
+ try {
719
+ executable = realpathSync(target);
720
+ } catch {
721
+ executable = target;
722
+ }
723
+ }
724
+ return !isPackagedRubyPath(executable);
725
+ }
726
+
727
+ /**
728
+ * Collect the Ruby on the PATH.
729
+ *
730
+ * `ruby --version` prints the interpreter's description, such as
731
+ * `ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [x86_64-linux]`, which is kept
732
+ * as the component description. The identity is `pkg:generic/ruby@3.3.6`
733
+ * (or jruby, truffleruby), with the CPE of the release unless a package
734
+ * manager owns the executable: a distro backports fixes under the old upstream
735
+ * version.
589
736
  *
590
737
  * @param {string} dir Working directory
591
738
  * @returns Object containing Ruby details
592
739
  */
593
740
  export function collectRubyInfo(dir) {
594
741
  const versionDesc = getCommandOutput(RUBY_CMD, dir, ["--version"]);
595
- if (versionDesc) {
596
- const info = {
597
- type: "platform",
598
- name: "ruby",
599
- version: versionDesc.trim(),
600
- };
601
- recordToolProbeResolved("ruby", "ruby", info.version);
742
+ const banner = firstOutputLine(versionDesc);
743
+ const parsed = parseRubyDescription(banner);
744
+ if (parsed) {
745
+ const info = formulationToolchain(
746
+ createRubyRuntimeComponent(parsed.version, {
747
+ engine: parsed.engine,
748
+ withCpe: rubyKeepsCpe(dir),
749
+ }),
750
+ );
751
+ info.description = banner;
752
+ const properties = [];
753
+ if (parsed.patchlevel !== undefined) {
754
+ properties.push({
755
+ name: "cdx:ruby:patchlevel",
756
+ value: parsed.patchlevel,
757
+ });
758
+ }
759
+ if (parsed.rubyVersion) {
760
+ properties.push({
761
+ name: "cdx:ruby:languageVersion",
762
+ value: parsed.rubyVersion,
763
+ });
764
+ }
765
+ if (parsed.platform) {
766
+ properties.push({ name: "cdx:ruby:platform", value: parsed.platform });
767
+ }
768
+ if (properties.length) {
769
+ info.properties = properties;
770
+ }
771
+ recordToolProbeResolved("ruby", "ruby", banner);
602
772
  return info;
603
773
  }
604
- recordToolProbeFailure("ruby", "ruby", RUBY_CMD);
774
+ recordToolProbeFailure(
775
+ "ruby",
776
+ "ruby",
777
+ RUBY_CMD,
778
+ versionDesc
779
+ ? "The ruby version probe did not print a Ruby version banner."
780
+ : undefined,
781
+ );
782
+ return undefined;
783
+ }
784
+
785
+ /**
786
+ * Whether the Ruby collectRubyInfo found is the MRI release a project needs.
787
+ *
788
+ * @param {Object|undefined} info result of collectRubyInfo
789
+ * @param {string} version wanted MRI version
790
+ * @returns {boolean}
791
+ */
792
+ export function isRubyVersionActive(info, version) {
793
+ return Boolean(info && info.name === "ruby" && info.version === version);
794
+ }
795
+
796
+ /**
797
+ * Collect the RubyGems version. `gem --version` prints the bare version.
798
+ *
799
+ * @param {string} dir Working directory
800
+ * @returns {Object|undefined} the RubyGems component
801
+ */
802
+ export function collectRubyGemsInfo(dir) {
803
+ const token = firstOutputLine(getCommandOutput(GEM_CMD, dir, ["--version"]));
804
+ if (/^\d+\.\d+\.\d+$/.test(token)) {
805
+ recordToolProbeResolved("gem", "ruby", token);
806
+ return formulationToolchain(
807
+ applyPurl(
808
+ {
809
+ type: "platform",
810
+ name: "rubygems",
811
+ version: token,
812
+ cpe: `cpe:2.3:a:rubygems:rubygems:${token}:*:*:*:*:*:*:*`,
813
+ },
814
+ tryBuildPurl({ type: "generic", name: "rubygems", version: token }),
815
+ ),
816
+ );
817
+ }
818
+ recordToolProbeFailure("gem", "ruby", GEM_CMD);
819
+ return undefined;
820
+ }
821
+
822
+ /**
823
+ * Collect the Bundler version. `bundle --version` prints `Bundler version
824
+ * 2.5.22`, or the bare version from Bundler 2.6 on.
825
+ *
826
+ * @param {string} dir Working directory
827
+ * @returns {Object|undefined} the Bundler component
828
+ */
829
+ export function collectBundlerInfo(dir) {
830
+ const line = firstOutputLine(
831
+ getCommandOutput(BUNDLE_CMD, dir, ["--version"]),
832
+ );
833
+ const token = line.startsWith("Bundler version ")
834
+ ? line.slice("Bundler version ".length).trim()
835
+ : line;
836
+ if (/^\d+\.\d+\.\d+$/.test(token)) {
837
+ recordToolProbeResolved("bundler", "ruby", token);
838
+ return formulationToolchain(
839
+ applyPurl(
840
+ {
841
+ type: "platform",
842
+ name: "bundler",
843
+ version: token,
844
+ cpe: `cpe:2.3:a:bundler:bundler:${token}:*:*:*:*:*:*:*`,
845
+ },
846
+ tryBuildPurl({ type: "gem", name: "bundler", version: token }),
847
+ ),
848
+ );
849
+ }
850
+ recordToolProbeFailure("bundler", "ruby", BUNDLE_CMD);
605
851
  return undefined;
606
852
  }
607
853
 
@@ -787,6 +1033,14 @@ export function collectEnvInfo(dir, options = undefined) {
787
1033
  cmp = collectRubyInfo(dir);
788
1034
  if (cmp) {
789
1035
  infoComponents.push(cmp);
1036
+ cmp = collectRubyGemsInfo(dir);
1037
+ if (cmp) {
1038
+ infoComponents.push(cmp);
1039
+ }
1040
+ cmp = collectBundlerInfo(dir);
1041
+ if (cmp) {
1042
+ infoComponents.push(cmp);
1043
+ }
790
1044
  }
791
1045
  cmp = collectPhpInfo(dir);
792
1046
  if (cmp) {
@@ -1218,7 +1472,7 @@ export function installRubyVersion(rubyVersion, filePath) {
1218
1472
  return { fullToolBinDir: undefined, status: false };
1219
1473
  }
1220
1474
  const existingRuby = collectRubyInfo(filePath);
1221
- if (existingRuby?.version?.startsWith(`ruby ${rubyVersion} `)) {
1475
+ if (isRubyVersionActive(existingRuby, rubyVersion)) {
1222
1476
  return { fullToolBinDir: undefined, status: true };
1223
1477
  }
1224
1478
  const fullToolBinDir = rubyVersionDir(rubyVersion);
@@ -1984,6 +2238,36 @@ export function gitRevertsAndHotfixes(dir, maxCount = 20) {
1984
2238
  return entries;
1985
2239
  }
1986
2240
 
2241
+ // process.versions keys whose configure flag is named differently.
2242
+ const NODE_SHARED_FLAG_NAMES = {
2243
+ ares: "cares",
2244
+ libffi: "ffi",
2245
+ llhttp: "http_parser",
2246
+ uv: "libuv",
2247
+ };
2248
+
2249
+ /**
2250
+ * Whether the running Node.js links a bundled dependency as a shared library,
2251
+ * from the `node_shared_<name>` configure variables it reports.
2252
+ *
2253
+ * @param {string} name key of process.versions
2254
+ * @param {Object} [variables] configure variables, process.config.variables
2255
+ * by default
2256
+ * @returns {string|undefined} `shared`, `bundled`, or undefined when the build
2257
+ * does not say
2258
+ */
2259
+ export function nodeComponentLinkage(
2260
+ name,
2261
+ variables = globalThis.process?.config?.variables,
2262
+ ) {
2263
+ const flag = `node_shared_${NODE_SHARED_FLAG_NAMES[name] || name}`;
2264
+ if (!variables || !(flag in variables)) {
2265
+ return undefined;
2266
+ }
2267
+ const value = variables[flag];
2268
+ return value === true || value === "true" ? "shared" : "bundled";
2269
+ }
2270
+
1987
2271
  /**
1988
2272
  * Get information about the runtime.
1989
2273
  *
@@ -2016,12 +2300,22 @@ export function getRuntimeInformation() {
2016
2300
  name,
2017
2301
  version: version || null,
2018
2302
  });
2303
+ // A distro or Homebrew build links some of these as shared libraries
2304
+ // (`configure --shared-brotli`); the version is then the shared
2305
+ // library's, and updating Node.js alone does not update it.
2306
+ const linkage = nodeComponentLinkage(name);
2019
2307
  const apkg = {
2020
2308
  name,
2021
2309
  version,
2022
- description: `Bundled with Node.js ${runtimeInfo.version}`,
2310
+ description:
2311
+ linkage === "shared"
2312
+ ? `Linked by Node.js ${runtimeInfo.version} as a shared library`
2313
+ : `Bundled with Node.js ${runtimeInfo.version}`,
2023
2314
  type: "library",
2024
2315
  scope: "excluded",
2316
+ ...(linkage
2317
+ ? { properties: [{ name: "cdx:node:linkage", value: linkage }] }
2318
+ : {}),
2025
2319
  };
2026
2320
  applyPurl(apkg, bundledPurl);
2027
2321
  if (nodeSourceUrl) {
@@ -6,6 +6,7 @@ import { build } from "@cdxgen/cdx-purl";
6
6
  import { safeExistsSync } from "../core/fs.js";
7
7
  import { thoughtLog } from "../core/logger.js";
8
8
  import { sanitizeBomUrl } from "../core/propertySanitizer.js";
9
+ import { vscodeExtensionPlatformFromPath } from "../parsers/vscodeExtensionId.js";
9
10
  import { isAgentSkillSpecValid } from "./agentSkillSpec.js";
10
11
  import {
11
12
  addDosaiReachabilityProperties,
@@ -619,6 +620,13 @@ export function convertOSQueryResults(
619
620
  // The profile an add-on was found in is not part of its purl, so the
620
621
  // add-on ID is what tells apart two add-ons that share a name.
621
622
  qualifiers = { tag_id: firefoxAddonTagId(res.identifier) };
623
+ } else if (queryObj.purlType === "vscode-extension") {
624
+ // A platform-specific build is a different artefact from the one for
625
+ // another platform, and the extension scan names it the same way.
626
+ const targetPlatform = vscodeExtensionPlatformFromPath(res.path);
627
+ if (targetPlatform) {
628
+ qualifiers = { platform: targetPlatform };
629
+ }
622
630
  }
623
631
  if (name) {
624
632
  name = sanitizeOsQueryIdentity(name);
@@ -788,11 +796,14 @@ export function convertOSQueryResults(
788
796
  // profile. Where it was found is not part of what it is, so the path is an
789
797
  // occurrence rather than a subpath: a home directory stays out of the purl, and
790
798
  // one package installed in two places is one component with two occurrences.
799
+ // Chocolatey rows carry the absolute path of the package's .nuspec, which a
800
+ // purl subpath cannot hold, so it is kept as an occurrence like these.
791
801
  const LOCATED_PACKAGE_PURL_TYPES = new Set([
792
802
  "npm",
793
803
  "pypi",
794
804
  "chrome-extension",
795
805
  "vscode-extension",
806
+ "chocolatey",
796
807
  ]);
797
808
  const LOCATED_PACKAGE_CATEGORIES = new Set(["firefox_addons"]);
798
809
 
@@ -221,7 +221,7 @@ export function openHostCircuit(
221
221
  );
222
222
  } else if (breaker === "maven-central") {
223
223
  console.warn(
224
- `${host} ${what}, so cdxgen will not contact it for the next ${minutes} minute(s) and Maven metadata comes from local caches only. Set MAVEN_CENTRAL_URL to a mirror or repository manager, for example https://maven-central.storage-download.googleapis.com/maven2/, to keep license enrichment working.`,
224
+ `${host} ${what}, so cdxgen will not contact it for the next ${minutes} minute(s) and Maven metadata comes from local caches only. Set MAVEN_CENTRAL_MIRROR=true to use Google's Maven Central mirror, or name a repository manager in MAVEN_CENTRAL_URL, to keep license enrichment working.`,
225
225
  );
226
226
  } else {
227
227
  console.warn(