@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,218 @@
1
+ // Bounded Node.js build-version reader for unpackaged `node` binaries.
2
+ //
3
+ // The Node.js the Docker Official images, the release tarballs, nvm, fnm and
4
+ // volta install is not owned by any package manager, so a BOM sees it only as
5
+ // a file. Its version decides whether the runtime is affected by advisories
6
+ // such as CVE-2026-111497 (fixed in 22.23.4, 24.22.1 and 26.12.1).
7
+ //
8
+ // Two markers are compiled into the binary:
9
+ // - the release URLs, `https://nodejs.org/download/<channel>/v<version>/`,
10
+ // which a build configured with release URLs (every nodejs.org build)
11
+ // carries for process.release.sourceUrl and headersUrl
12
+ // https://github.com/nodejs/node/blob/v24.22.0/src/node_metadata.cc#L191-L197
13
+ // - `node.js/v<version>`, NUL-terminated, the inspector's /json/version
14
+ // Browser string, which every build with the inspector carries; it is the
15
+ // fallback for a build without release URLs.
16
+ // https://github.com/nodejs/node/blob/v24.22.0/src/inspector_socket_server.cc#L117
17
+ //
18
+ // process.versions.brotli is not a usable signal: Node.js backports the
19
+ // decoder fix without changing it (24.22.1 still reports 1.2.0). The decoder
20
+ // state is read separately, by brotliDecoder.js.
21
+
22
+ import { basename } from "node:path";
23
+
24
+ import { scanFileMarkers } from "../core/boundedFile.js";
25
+ import { applyPurl, tryBuildPurl } from "./purl.js";
26
+
27
+ const RELEASE_URL_MARKER = Buffer.from("https://nodejs.org/download/");
28
+ const USER_AGENT_MARKER = Buffer.from("node.js/v");
29
+ const MAX_VERSION_LENGTH = 64;
30
+ const VERSION_RE = /^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z][0-9A-Za-z.]*))?$/;
31
+ const CHANNELS = new Set(["release", "rc", "nightly", "test", "v8-canary"]);
32
+
33
+ /**
34
+ * The most node-named files one scan opens in search of a build version.
35
+ *
36
+ * @type {number}
37
+ */
38
+ export const MAX_NODE_PROBES = 64;
39
+
40
+ /**
41
+ * Whether a file name is one the Node.js runtime is installed under.
42
+ *
43
+ * @param {string} fileName basename or path of the candidate
44
+ * @returns {boolean} true for `node`, `nodejs` and `node.exe`
45
+ */
46
+ export function isNodeBinaryName(fileName) {
47
+ if (typeof fileName !== "string" || !fileName) {
48
+ return false;
49
+ }
50
+ const name = basename(fileName.replaceAll("\\", "/"));
51
+ return name === "node" || name === "nodejs" || name === "node.exe";
52
+ }
53
+
54
+ /**
55
+ * Parse a Node.js version in the grammar the release tooling produces:
56
+ * `24.22.1`, `25.0.0-rc.1`, `26.0.0-nightly20260301abcdef`.
57
+ *
58
+ * @param {string} text candidate version, without the leading `v`
59
+ * @returns {string|undefined} the version, or undefined
60
+ */
61
+ export function parseNodeVersion(text) {
62
+ if (typeof text !== "string" || !text || text.length > MAX_VERSION_LENGTH) {
63
+ return undefined;
64
+ }
65
+ return VERSION_RE.test(text) ? text : undefined;
66
+ }
67
+
68
+ function versionFromReleaseUrl(after) {
69
+ // <channel>/v<version>/...
70
+ const text = after.toString("latin1");
71
+ const slash = text.indexOf("/");
72
+ if (slash <= 0) {
73
+ return undefined;
74
+ }
75
+ const channel = text.slice(0, slash);
76
+ if (!CHANNELS.has(channel) || text[slash + 1] !== "v") {
77
+ return undefined;
78
+ }
79
+ const end = text.indexOf("/", slash + 2);
80
+ if (end === -1) {
81
+ return undefined;
82
+ }
83
+ const version = parseNodeVersion(text.slice(slash + 2, end));
84
+ return version ? { version, channel } : undefined;
85
+ }
86
+
87
+ function versionFromUserAgent(after) {
88
+ const end = after.indexOf(0);
89
+ if (end <= 0) {
90
+ return undefined;
91
+ }
92
+ return parseNodeVersion(after.toString("latin1", 0, end));
93
+ }
94
+
95
+ /**
96
+ * Read the Node.js version a `node` binary was built as.
97
+ *
98
+ * @param {string} absPath absolute path of the binary
99
+ * @param {string|undefined} rootDir extracted root, when the file came from one
100
+ * @returns {{version: string, source: string, channel?: string}|undefined}
101
+ * source is `release-url` or `binary`
102
+ */
103
+ export function readNodeBuildVersion(absPath, rootDir) {
104
+ const scan = scanFileMarkers(
105
+ absPath,
106
+ rootDir,
107
+ [RELEASE_URL_MARKER, USER_AGENT_MARKER],
108
+ { contextBytes: MAX_VERSION_LENGTH + 32, maxHits: 8 },
109
+ );
110
+ if (!scan || scan.skipped) {
111
+ return undefined;
112
+ }
113
+ const [releaseHits, userAgentHits] = scan.hits;
114
+ const fromBinary = userAgentHits
115
+ .map((hit) => versionFromUserAgent(hit.after))
116
+ .find(Boolean);
117
+ for (const hit of releaseHits) {
118
+ const release = versionFromReleaseUrl(hit.after);
119
+ // The headers URL names the release a binary was published as. A binary
120
+ // whose own version string disagrees is not that release, so the URL is
121
+ // used only when the two agree or the version string is missing.
122
+ if (release && (!fromBinary || fromBinary === release.version)) {
123
+ return {
124
+ version: release.version,
125
+ source: "release-url",
126
+ channel: release.channel,
127
+ };
128
+ }
129
+ }
130
+ return fromBinary ? { version: fromBinary, source: "binary" } : undefined;
131
+ }
132
+
133
+ /**
134
+ * The CPE that lets a downstream scanner match a Node.js version. NVD names the
135
+ * product node.js and keeps a pre-release in the update field.
136
+ *
137
+ * @param {string} version Node.js version
138
+ * @returns {string} CPE 2.3 string
139
+ */
140
+ export function nodeCpe(version) {
141
+ const match = VERSION_RE.exec(version || "");
142
+ if (!match) {
143
+ return `cpe:2.3:a:nodejs:node.js:${version}:*:*:*:*:*:*:*`;
144
+ }
145
+ const update = match[2] ? match[2].replaceAll(".", "").toLowerCase() : "*";
146
+ return `cpe:2.3:a:nodejs:node.js:${match[1]}:${update}:*:*:*:*:*:*`;
147
+ }
148
+
149
+ /**
150
+ * A Node.js runtime is one `platform` component per distinct version, like the
151
+ * PHP and Go stdlib components. Its identity is `pkg:generic/node`: it is only
152
+ * used where no package manager owns the runtime.
153
+ *
154
+ * @param {string} version Node.js version
155
+ * @returns {Object} component with `purl`, `bom-ref` and `cpe`
156
+ */
157
+ export function createNodeRuntimeComponent(version) {
158
+ return applyPurl(
159
+ { type: "platform", name: "node", version, cpe: nodeCpe(version) },
160
+ tryBuildPurl({ type: "generic", name: "node", version }),
161
+ );
162
+ }
163
+
164
+ /**
165
+ * The properties a node file component carries once its build is known.
166
+ *
167
+ * @param {{version: string, source: string, channel?: string}} build result of readNodeBuildVersion
168
+ * @returns {{name: string, value: string}[]} properties to append
169
+ */
170
+ export function nodeBuildProperties(build) {
171
+ const properties = [
172
+ { name: "cdx:node:buildVersion", value: build.version },
173
+ { name: "cdx:node:buildVersionSource", value: build.source },
174
+ ];
175
+ if (build.channel) {
176
+ properties.push({ name: "cdx:node:releaseChannel", value: build.channel });
177
+ }
178
+ return properties;
179
+ }
180
+
181
+ /**
182
+ * Start collecting the runtime components and edges of one scan.
183
+ *
184
+ * @returns {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}}
185
+ */
186
+ export function createNodeRuntimeInventory() {
187
+ return { runtimeComponents: [], dependencies: [], runtimeRefs: new Set() };
188
+ }
189
+
190
+ /**
191
+ * Tag a node file component with its build facts and link it to the runtime
192
+ * component of its version, creating that component on first use. Mutates the
193
+ * component in place.
194
+ *
195
+ * @param {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}} inventory
196
+ * accumulator from createNodeRuntimeInventory
197
+ * @param {Object} component file component, with a `properties` array
198
+ * @param {{version: string, source: string, channel?: string}} build result of readNodeBuildVersion
199
+ */
200
+ export function attachNodeBuild(inventory, component, build) {
201
+ component.properties.push(...nodeBuildProperties(build));
202
+ const runtime = createNodeRuntimeComponent(build.version);
203
+ const runtimeRef = runtime["bom-ref"];
204
+ if (!runtimeRef) {
205
+ return;
206
+ }
207
+ if (!inventory.runtimeRefs.has(runtimeRef)) {
208
+ inventory.runtimeRefs.add(runtimeRef);
209
+ inventory.runtimeComponents.push(runtime);
210
+ inventory.dependencies.push({ ref: runtimeRef, dependsOn: [] });
211
+ }
212
+ if (component["bom-ref"]) {
213
+ inventory.dependencies.push({
214
+ ref: component["bom-ref"],
215
+ dependsOn: [runtimeRef],
216
+ });
217
+ }
218
+ }
@@ -0,0 +1,202 @@
1
+ // Bounded, read-only probe for the Node.js no package manager owns on a live
2
+ // host, for `-t os` / obom. The release tarballs, the nodejs.org installer and
3
+ // the version managers (nvm, fnm, volta, asdf, mise, n) put Node.js where no
4
+ // package database records it, so an OBOM sees neither the runtime nor the
5
+ // Brotli decoder compiled into it.
6
+ //
7
+ // Candidates come from fixed directories and from each version manager's
8
+ // tree under the home directories, one wildcard level at a time, never from a
9
+ // walk of the file system. The ownership rules, caps and degradations are the
10
+ // shared host-runtime probe's.
11
+
12
+ import { join } from "node:path";
13
+
14
+ import {
15
+ isDryRun,
16
+ readEnvironmentVariable,
17
+ recordActivity,
18
+ } from "../core/activity.js";
19
+ import { recordDegradation } from "../core/buildLedger.js";
20
+ import {
21
+ attachBrotliDecoder,
22
+ createBrotliInventory,
23
+ finishBrotliInventory,
24
+ readBrotliDecoder,
25
+ } from "./brotliDecoder.js";
26
+ import {
27
+ isDirectory,
28
+ listDirectory,
29
+ probeHostRuntime,
30
+ } from "./hostRuntimeProbe.js";
31
+ import {
32
+ attachNodeBuild,
33
+ createNodeRuntimeInventory,
34
+ isNodeBinaryName,
35
+ MAX_NODE_PROBES,
36
+ readNodeBuildVersion,
37
+ } from "./nodeBuildInfo.js";
38
+
39
+ // The most directories one wildcard pattern may expand to.
40
+ const MAX_EXPANDED_DIRECTORIES = 256;
41
+
42
+ // Relative to the root.
43
+ const POSIX_DIRECTORIES = {
44
+ linux: ["usr/local/bin", "opt/*/bin", "usr/local/n/versions/node/*/bin"],
45
+ darwin: ["usr/local/bin", "opt/homebrew/bin", "opt/*/bin"],
46
+ };
47
+
48
+ // Relative to a home directory.
49
+ const VERSION_MANAGER_DIRECTORIES = [
50
+ ".nvm/versions/node/*/bin",
51
+ ".local/share/fnm/node-versions/*/installation/bin",
52
+ ".fnm/node-versions/*/installation/bin",
53
+ "Library/Application Support/fnm/node-versions/*/installation/bin",
54
+ ".volta/tools/image/node/*/bin",
55
+ ".asdf/installs/nodejs/*/bin",
56
+ ".local/share/mise/installs/node/*/bin",
57
+ ];
58
+
59
+ const HOME_PARENTS = {
60
+ linux: ["home"],
61
+ darwin: ["Users"],
62
+ };
63
+
64
+ /**
65
+ * Expand a relative directory pattern whose `*` segments match one directory
66
+ * name each.
67
+ *
68
+ * @param {string} base absolute directory the pattern hangs off
69
+ * @param {string} pattern `/`-separated relative pattern
70
+ * @returns {string[]} existing directories, at most MAX_EXPANDED_DIRECTORIES
71
+ */
72
+ function expandDirectories(base, pattern) {
73
+ let current = [base];
74
+ for (const segment of pattern.split("/")) {
75
+ const next = [];
76
+ for (const directory of current) {
77
+ if (segment !== "*") {
78
+ next.push(join(directory, segment));
79
+ continue;
80
+ }
81
+ for (const name of listDirectory(directory)) {
82
+ const child = join(directory, name);
83
+ if (isDirectory(child)) {
84
+ next.push(child);
85
+ }
86
+ if (next.length >= MAX_EXPANDED_DIRECTORIES) {
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ current = next.slice(0, MAX_EXPANDED_DIRECTORIES);
92
+ }
93
+ return current.filter(isDirectory);
94
+ }
95
+
96
+ function homeDirectories(context) {
97
+ const { rootPath, platform } = context;
98
+ const homes = [];
99
+ if (platform === "linux") {
100
+ homes.push(join(rootPath, "root"));
101
+ }
102
+ for (const parent of HOME_PARENTS[platform] || []) {
103
+ for (const name of listDirectory(join(rootPath, parent))) {
104
+ const home = join(rootPath, parent, name);
105
+ if (isDirectory(home)) {
106
+ homes.push(home);
107
+ }
108
+ }
109
+ }
110
+ return homes.slice(0, MAX_EXPANDED_DIRECTORIES);
111
+ }
112
+
113
+ // Where the Node.js MSI installs, by itself or for Chocolatey's nodejs.install.
114
+ function windowsInstallDirectory(context) {
115
+ const programFiles =
116
+ context.readEnv("ProgramFiles") || join(context.rootPath, "Program Files");
117
+ return join(programFiles, "nodejs");
118
+ }
119
+
120
+ function candidateDirectories(context) {
121
+ const { rootPath, platform } = context;
122
+ const directories = [];
123
+ if (platform === "win32") {
124
+ directories.push({ directory: windowsInstallDirectory(context) });
125
+ const appData = context.readEnv("APPDATA");
126
+ if (appData) {
127
+ for (const directory of expandDirectories(appData, "nvm/*")) {
128
+ directories.push({ directory });
129
+ }
130
+ }
131
+ return directories;
132
+ }
133
+ for (const pattern of POSIX_DIRECTORIES[platform] || []) {
134
+ for (const directory of expandDirectories(rootPath, pattern)) {
135
+ directories.push({ directory });
136
+ }
137
+ }
138
+ for (const home of homeDirectories(context)) {
139
+ for (const pattern of VERSION_MANAGER_DIRECTORIES) {
140
+ for (const directory of expandDirectories(home, pattern)) {
141
+ directories.push({ directory });
142
+ }
143
+ }
144
+ }
145
+ return directories;
146
+ }
147
+
148
+ function readBuild(context, candidate) {
149
+ const node = readNodeBuildVersion(candidate.real, context.realRoot);
150
+ if (!node) {
151
+ return undefined;
152
+ }
153
+ return { node, brotli: readBrotliDecoder(candidate.real, context.realRoot) };
154
+ }
155
+
156
+ /**
157
+ * Look for Node.js no package manager owns on the host. Components come back
158
+ * only for the files nothing owns: a `file` component for each `node` binary
159
+ * with the cdx:node and cdx:brotli properties, one `platform` component per
160
+ * Node.js version with its CPE, and the brotli component when a binary embeds
161
+ * a decoder.
162
+ *
163
+ * @param {Object} [options] probe options, as probeHostPhp takes them
164
+ * @returns {{components: Object[], dependencies: Object[]}}
165
+ */
166
+ export function probeHostNode(options = {}) {
167
+ return probeHostRuntime(options, {
168
+ label: "Node.js",
169
+ ecosystem: "npm",
170
+ capability: "node-host-probe",
171
+ remediationId: "npm.host-probe.package-db-unreadable",
172
+ maxProbes: MAX_NODE_PROBES,
173
+ chocolateyPackages: ["nodejs", "nodejs-lts", "nodejs.install"],
174
+ chocolateyDirectories: (context) => [windowsInstallDirectory(context)],
175
+ directories: candidateDirectories,
176
+ classify: (name) => (isNodeBinaryName(name) ? "node" : undefined),
177
+ readBuild,
178
+ createInventory: () => ({
179
+ node: createNodeRuntimeInventory(),
180
+ brotli: createBrotliInventory(),
181
+ }),
182
+ attach: (inventory, component, build) => {
183
+ attachNodeBuild(inventory.node, component, build.node);
184
+ if (build.brotli) {
185
+ attachBrotliDecoder(inventory.brotli, component, build.brotli);
186
+ }
187
+ },
188
+ finish: (inventory) => {
189
+ const brotli = finishBrotliInventory(inventory.brotli);
190
+ return {
191
+ components: [...inventory.node.runtimeComponents, ...brotli.components],
192
+ dependencies: [...inventory.node.dependencies, ...brotli.dependencies],
193
+ };
194
+ },
195
+ hooks: {
196
+ isDryRun,
197
+ readEnvironmentVariable,
198
+ recordActivity,
199
+ recordDegradation,
200
+ },
201
+ });
202
+ }
@@ -493,3 +493,185 @@ function collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths) {
493
493
  }
494
494
  }
495
495
  }
496
+
497
+ // Native modules that language package managers install outside the library
498
+ // directories the loader searches: CPython and PyPy extension modules, PHP
499
+ // extensions, Ruby gem extensions, Node.js addons, and the native half of a
500
+ // .NET runtime. A Brotli decoder compiled into one of them is invisible unless
501
+ // the file is read.
502
+ const NATIVE_EXTENSION_GLOBS = [
503
+ "**/site-packages/**/*.{so,pyd}",
504
+ "**/dist-packages/**/*.so",
505
+ "**/lib/php/**/*.so",
506
+ "**/lib/php*/modules/*.so",
507
+ "**/php*/ext/php_*.dll",
508
+ "**/gems/*/lib/**/*.{so,bundle}",
509
+ "**/gems/*/extensions/**/*.{so,bundle}",
510
+ "**/bundle/extensions/**/*.{so,bundle}",
511
+ "**/node_modules/**/*.node",
512
+ "**/dotnet/shared/*/*/*.so",
513
+ "**/.dotnet/shared/*/*/*.so",
514
+ // Of a Windows runtime's many DLLs, only this one is native and holds the
515
+ // decoder.
516
+ "**/dotnet/shared/*/*/System.IO.Compression.Native.dll",
517
+ ];
518
+ const NATIVE_EXTENSION_IGNORES = ["proc/**", "sys/**", "dev/**"];
519
+
520
+ /**
521
+ * The most native extension candidates one scan returns.
522
+ *
523
+ * @type {number}
524
+ */
525
+ export const MAX_NATIVE_EXTENSIONS = 4096;
526
+
527
+ /**
528
+ * Collect native extension modules under a root file system that the library
529
+ * collector does not see, leaving out paths a package database owns and paths
530
+ * already collected. Symlinks are not followed, so a link pointing out of the
531
+ * root is never listed.
532
+ *
533
+ * @param {string} basePath extracted root file system
534
+ * @param {string[]} [excludePaths] root-relative paths to leave out
535
+ * @returns {string[]} root-relative paths, sorted, capped at MAX_NATIVE_EXTENSIONS
536
+ */
537
+ export function collectNativeExtensions(basePath, excludePaths = []) {
538
+ if (!basePath) {
539
+ return [];
540
+ }
541
+ const excluded = new Set(
542
+ (excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
543
+ );
544
+ let files = [];
545
+ try {
546
+ files = globSync(NATIVE_EXTENSION_GLOBS, {
547
+ cwd: basePath,
548
+ absolute: false,
549
+ nodir: true,
550
+ dot: true,
551
+ follow: false,
552
+ ignore: NATIVE_EXTENSION_IGNORES,
553
+ });
554
+ } catch (_err) {
555
+ return [];
556
+ }
557
+ const result = new Set();
558
+ for (const file of files) {
559
+ const normalized = file.replace(/\\/g, "/").replace(/^\/+/, "");
560
+ if (!excluded.has(normalized)) {
561
+ result.add(normalized);
562
+ }
563
+ }
564
+ return Array.from(result).sort().slice(0, MAX_NATIVE_EXTENSIONS);
565
+ }
566
+
567
+ const OS_PACKAGE_PURL_PREFIXES = [
568
+ "pkg:deb/",
569
+ "pkg:apk/",
570
+ "pkg:rpm/",
571
+ "pkg:alpm/",
572
+ "pkg:qpkg/",
573
+ "pkg:generic/",
574
+ ];
575
+
576
+ /**
577
+ * Whether a source file is the installation record of a language package,
578
+ * the file a package manager writes when it installs the package: dist-info
579
+ * or egg-info metadata, an installed gemspec, a package.json inside
580
+ * node_modules, or a jar. A lock file or manifest only names packages, so an
581
+ * OS package owning one says nothing about who installed them.
582
+ *
583
+ * @param {string} filePath root-relative path with forward slashes
584
+ * @returns {boolean}
585
+ */
586
+ function isInstallRecord(filePath) {
587
+ const parts = filePath.split("/");
588
+ const name = parts.at(-1) || "";
589
+ const parent = parts.at(-2) || "";
590
+ if (parent.endsWith(".dist-info") || parent.endsWith(".egg-info")) {
591
+ return true;
592
+ }
593
+ if (name.endsWith(".egg-info") || name.endsWith(".jar")) {
594
+ return true;
595
+ }
596
+ if (name.endsWith(".gemspec") && parts.includes("specifications")) {
597
+ return true;
598
+ }
599
+ return name === "package.json" && parts.includes("node_modules");
600
+ }
601
+
602
+ /**
603
+ * Mark language packages that an OS package installed. Alpine's py3-brotli
604
+ * ships a Brotli-1.1.0.dist-info that a Python scan reports as
605
+ * pkg:pypi/brotli@1.1.0, but a distro fixes it by backporting under the same
606
+ * upstream version, so a PyPI advisory range keeps matching after the fix. The
607
+ * `cdx:os:installedBy` property names the OS package that owns the
608
+ * installation record, so a consumer can judge the component by that package
609
+ * instead.
610
+ *
611
+ * A component is marked only when every installation record it was read from
612
+ * belongs to an OS package: a copy that pip put in a virtual environment
613
+ * beside the distro's is not the distro's to fix. Run it after duplicates
614
+ * merge, so that a merged component carries all of its records.
615
+ *
616
+ * @param {Object[]} components BOM components, mutated in place
617
+ * @param {Map<string, string>|undefined} ownerByPath root-relative file path to
618
+ * owning OS package bom-ref
619
+ * @param {string} [rootDir] extracted root, stripped from absolute source paths
620
+ * @returns {number} how many components were marked
621
+ */
622
+ export function markOsInstalledLanguagePackages(
623
+ components,
624
+ ownerByPath,
625
+ rootDir,
626
+ ) {
627
+ if (!ownerByPath?.size || !Array.isArray(components)) {
628
+ return 0;
629
+ }
630
+ const rootPrefix = rootDir
631
+ ? rootDir.replaceAll("\\", "/").replace(/\/+$/, "")
632
+ : undefined;
633
+ let marked = 0;
634
+ for (const component of components) {
635
+ const purl = component?.purl;
636
+ if (
637
+ typeof purl !== "string" ||
638
+ !purl.startsWith("pkg:") ||
639
+ OS_PACKAGE_PURL_PREFIXES.some((prefix) => purl.startsWith(prefix)) ||
640
+ !Array.isArray(component.properties) ||
641
+ component.properties.some((prop) => prop.name === "cdx:os:installedBy")
642
+ ) {
643
+ continue;
644
+ }
645
+ const owners = new Set();
646
+ let records = 0;
647
+ let unowned = 0;
648
+ for (const prop of component.properties) {
649
+ if (prop.name !== "internal:SrcFile" || typeof prop.value !== "string") {
650
+ continue;
651
+ }
652
+ let filePath = prop.value.replaceAll("\\", "/");
653
+ if (rootPrefix && filePath.startsWith(`${rootPrefix}/`)) {
654
+ filePath = filePath.slice(rootPrefix.length);
655
+ }
656
+ filePath = filePath.replace(/^\/+/, "");
657
+ if (!isInstallRecord(filePath)) {
658
+ continue;
659
+ }
660
+ records += 1;
661
+ const owner = ownerByPath.get(filePath);
662
+ if (owner) {
663
+ owners.add(owner);
664
+ } else {
665
+ unowned += 1;
666
+ }
667
+ }
668
+ if (!records || unowned) {
669
+ continue;
670
+ }
671
+ for (const owner of Array.from(owners).sort()) {
672
+ component.properties.push({ name: "cdx:os:installedBy", value: owner });
673
+ }
674
+ marked += 1;
675
+ }
676
+ return marked;
677
+ }
@@ -20,16 +20,10 @@
20
20
  // The runtime component helpers at the end are the one place a PHP version
21
21
  // becomes a component, for the container scan, the OS scan and formulation.
22
22
 
23
- import {
24
- closeSync,
25
- fstatSync,
26
- lstatSync,
27
- openSync,
28
- readSync,
29
- realpathSync,
30
- } from "node:fs";
23
+ import { closeSync, readSync } from "node:fs";
31
24
  import { basename, join, resolve, sep } from "node:path";
32
25
 
26
+ import { openRegularFile } from "../core/boundedFile.js";
33
27
  import { applyPurl, tryBuildPurl } from "./purl.js";
34
28
 
35
29
  // The version grammar the contract accepts: M.m.p, optionally with -dev or an
@@ -187,53 +181,6 @@ export function classifyPhpSapi(fileName) {
187
181
  return undefined;
188
182
  }
189
183
 
190
- /**
191
- * Open a path only when it is a regular file that does not resolve outside
192
- * rootDir. lstat runs first so a FIFO is rejected without opening it (an
193
- * open on a FIFO blocks until a writer appears) and a symlink is never
194
- * followed.
195
- *
196
- * @param {string} absPath absolute path
197
- * @param {string|undefined} rootDir extracted root the path must stay under
198
- * @returns {{fd: number, size: number}|undefined}
199
- */
200
- function openRegularFile(absPath, rootDir) {
201
- let fd;
202
- try {
203
- if (typeof absPath !== "string" || !absPath) {
204
- return undefined;
205
- }
206
- const lst = lstatSync(absPath);
207
- if (!lst.isFile()) {
208
- return undefined;
209
- }
210
- if (rootDir) {
211
- const root = realpathSync(resolve(rootDir));
212
- const real = realpathSync(absPath);
213
- const rootPrefix = root.endsWith(sep) ? root : root + sep;
214
- if (real !== root && !real.startsWith(rootPrefix)) {
215
- return undefined;
216
- }
217
- }
218
- fd = openSync(absPath, "r");
219
- const stats = fstatSync(fd);
220
- if (!stats.isFile()) {
221
- closeSync(fd);
222
- return undefined;
223
- }
224
- return { fd, size: stats.size };
225
- } catch {
226
- if (fd !== undefined) {
227
- try {
228
- closeSync(fd);
229
- } catch {
230
- // already closed
231
- }
232
- }
233
- return undefined;
234
- }
235
- }
236
-
237
184
  function matchVersionAt(buffer, start, readLength) {
238
185
  if (start >= readLength) {
239
186
  return undefined;
@@ -266,15 +213,19 @@ function scanBinaryVersion(fd, size) {
266
213
  return undefined;
267
214
  }
268
215
  // Only matches starting inside this window are new; the overlap exists so
269
- // a version split at the window edge is readable.
270
- const searchLimit = Math.min(READ_CHUNK_BYTES, read);
271
- let offset = buffer.indexOf(MARKER);
216
+ // a version split at the window edge is readable. The last window has no
217
+ // next one, so everything it read is searched.
218
+ const isLast = position + read >= size;
219
+ const searchLimit = isLast ? read : Math.min(READ_CHUNK_BYTES, read);
220
+ // Bytes a previous, longer read left past `read` are never searched.
221
+ const window = buffer.subarray(0, read);
222
+ let offset = window.indexOf(MARKER);
272
223
  while (offset !== -1 && offset < searchLimit) {
273
224
  const version = matchVersionAt(buffer, offset + MARKER.length, read);
274
225
  if (version) {
275
226
  return version;
276
227
  }
277
- offset = buffer.indexOf(MARKER, offset + 1);
228
+ offset = window.indexOf(MARKER, offset + 1);
278
229
  }
279
230
  if (read < bufferSize) {
280
231
  return undefined;