@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
@@ -14,19 +14,7 @@
14
14
  // A package database that exists but cannot be read leaves ownership unknown.
15
15
  // Unknown is not unpackaged: it emits nothing and records a degradation.
16
16
 
17
- import { readdirSync, realpathSync, statSync } from "node:fs";
18
- import {
19
- basename,
20
- dirname,
21
- isAbsolute,
22
- join,
23
- posix,
24
- relative,
25
- resolve,
26
- sep,
27
- win32,
28
- } from "node:path";
29
- import process from "node:process";
17
+ import { dirname, join } from "node:path";
30
18
 
31
19
  import {
32
20
  isDryRun,
@@ -34,28 +22,18 @@ import {
34
22
  recordActivity,
35
23
  } from "../core/activity.js";
36
24
  import { recordDegradation } from "../core/buildLedger.js";
37
- import { safeExistsSync, safeReadFileSync } from "../core/fs.js";
38
- import { thoughtLog } from "../core/logger.js";
39
25
  import {
40
- APK_INSTALLED_DB_PATHS,
41
- readApkInstalledDb,
42
- } from "./apkInstalledDb.js";
26
+ isDirectory,
27
+ listDirectory,
28
+ probeHostRuntime,
29
+ } from "./hostRuntimeProbe.js";
43
30
  import {
44
31
  attachPhpBuild,
45
32
  classifyPhpSapi,
46
33
  createPhpRuntimeInventory,
47
34
  MAX_PHP_PROBES,
48
- packageStoreOf,
49
35
  readPhpBuildVersion,
50
36
  } from "./phpBuildInfo.js";
51
- import { tryBuildPurl } from "./purl.js";
52
-
53
- // One dpkg list, and every dpkg list together. A database past these caps
54
- // counts as unreadable instead of being read in part; the apk database has the
55
- // same cap in its reader.
56
- const MAX_DATABASE_FILE_BYTES = 64 * 1024 * 1024;
57
- const MAX_DATABASE_TOTAL_BYTES = 256 * 1024 * 1024;
58
- const MAX_DPKG_LISTS = 100000;
59
37
 
60
38
  // Where the PHP of a source build is installed, relative to the root. `only`
61
39
  // narrows a directory to one SAPI. `opt/*/bin` and `opt/*/sbin` read /opt one
@@ -98,193 +76,6 @@ function isWindowsCoreLibrary(name) {
98
76
  return /^\d+$/.test(stem.endsWith("ts") ? stem.slice(0, -2) : stem);
99
77
  }
100
78
 
101
- function listDirectory(directory) {
102
- try {
103
- return readdirSync(directory).sort();
104
- } catch {
105
- return [];
106
- }
107
- }
108
-
109
- function isDirectory(path) {
110
- try {
111
- return statSync(path).isDirectory();
112
- } catch {
113
- return false;
114
- }
115
- }
116
-
117
- function realPathOf(path) {
118
- try {
119
- return realpathSync(path);
120
- } catch {
121
- return undefined;
122
- }
123
- }
124
-
125
- function quoteForOsquery(value) {
126
- return `'${value.replaceAll("'", "''")}'`;
127
- }
128
-
129
- function byBomRef(a, b) {
130
- const left = a["bom-ref"];
131
- const right = b["bom-ref"];
132
- return left < right ? -1 : left > right ? 1 : 0;
133
- }
134
-
135
- function readBounded(file, maxBytes) {
136
- try {
137
- const stats = statSync(file);
138
- if (!stats.isFile() || stats.size > maxBytes) {
139
- return undefined;
140
- }
141
- } catch {
142
- return undefined;
143
- }
144
- const text = safeReadFileSync(file, "utf-8");
145
- return typeof text === "string" ? text : undefined;
146
- }
147
-
148
- // A package database: its name and path, `absent` when the host has none,
149
- // `unreadable` when it has one that could not be read in full, and the paths of
150
- // the candidates it lists.
151
- function database(name, path, state = "ok") {
152
- return { name, path, state, owned: new Set() };
153
- }
154
-
155
- // dpkg keeps one <package>.list per package, naming each path as it is on the
156
- // system.
157
- function scanDpkg(infoDirectory, wanted) {
158
- const result = database("dpkg", infoDirectory);
159
- if (!safeExistsSync(infoDirectory)) {
160
- result.state = "absent";
161
- return result;
162
- }
163
- const lists = listDirectory(infoDirectory).filter((name) =>
164
- name.endsWith(".list"),
165
- );
166
- if (!isDirectory(infoDirectory) || lists.length > MAX_DPKG_LISTS) {
167
- result.state = "unreadable";
168
- }
169
- let total = 0;
170
- for (const name of lists.slice(0, MAX_DPKG_LISTS)) {
171
- const text = readBounded(
172
- join(infoDirectory, name),
173
- Math.min(MAX_DATABASE_FILE_BYTES, MAX_DATABASE_TOTAL_BYTES - total),
174
- );
175
- if (text === undefined) {
176
- result.state = "unreadable";
177
- continue;
178
- }
179
- total += text.length;
180
- for (const line of text.split("\n")) {
181
- if (wanted.has(line)) {
182
- result.owned.add(line);
183
- }
184
- }
185
- }
186
- return result;
187
- }
188
-
189
- // apk's installed database is read by the same reader the OS inventory uses, so
190
- // the packages an OBOM lists and the files this probe treats as owned follow one
191
- // parser. A dry run never gets here: probeHostPhp returns before it.
192
- function scanApk(context, wanted) {
193
- const { apkInstalledFile } = context.databases;
194
- const apk = readApkInstalledDb({
195
- rootPath: context.rootPath,
196
- databaseFile: apkInstalledFile,
197
- wantedFiles: wanted,
198
- platform: context.platform,
199
- });
200
- const result = database(
201
- "apk",
202
- apk.path ||
203
- apkInstalledFile ||
204
- join(context.rootPath, APK_INSTALLED_DB_PATHS[0]),
205
- );
206
- result.state = { ok: "ok", unreadable: "unreadable" }[apk.state] || "absent";
207
- for (const pkg of apk.packages) {
208
- for (const file of pkg.files) {
209
- result.owned.add(file);
210
- }
211
- }
212
- return result;
213
- }
214
-
215
- // osquery's rpm_package_files answers for a path without being told the
216
- // package.
217
- function queryRpm(databaseDirectories, wanted, executeOsQuery) {
218
- const result = database("rpm", databaseDirectories[0]);
219
- if (!databaseDirectories.some((directory) => safeExistsSync(directory))) {
220
- result.state = "absent";
221
- return result;
222
- }
223
- if (!wanted.size) {
224
- return result;
225
- }
226
- const rows =
227
- typeof executeOsQuery === "function"
228
- ? executeOsQuery(
229
- `SELECT package, path FROM rpm_package_files WHERE path IN (${[
230
- ...wanted,
231
- ]
232
- .map(quoteForOsquery)
233
- .join(", ")})`,
234
- )
235
- : undefined;
236
- if (!Array.isArray(rows)) {
237
- result.state = "unreadable";
238
- return result;
239
- }
240
- for (const row of rows) {
241
- if (typeof row?.path === "string") {
242
- result.owned.add(row.path);
243
- }
244
- }
245
- return result;
246
- }
247
-
248
- // Chocolatey's database is the list of packages the OS scan has already made.
249
- // The client is itself a package, so a Chocolatey with none listed was not
250
- // listed: the query failed.
251
- function readChocolatey(context) {
252
- const configured =
253
- context.readEnv("ChocolateyInstall") ||
254
- join(context.rootPath, "ProgramData", "chocolatey");
255
- const result = database("chocolatey", configured);
256
- if (!safeExistsSync(configured)) {
257
- result.state = "absent";
258
- return result;
259
- }
260
- const packages = (context.packageComponents || []).filter((component) =>
261
- component?.purl?.startsWith("pkg:chocolatey/"),
262
- );
263
- if (!packages.length) {
264
- result.state = "unreadable";
265
- return result;
266
- }
267
- result.directory = realPathOf(configured) || resolve(configured);
268
- result.hasPhp = packages.some(
269
- (component) => `${component.name}`.toLowerCase() === "php",
270
- );
271
- return result;
272
- }
273
-
274
- // Chocolatey's php package unpacks under C:\tools by default, outside the
275
- // package's own folder, so what makes a file Chocolatey's is the package being
276
- // installed and the file being where the package puts it.
277
- function chocolateyOwns(chocolatey, context, candidate) {
278
- if (!chocolatey.hasPhp) {
279
- return false;
280
- }
281
- const first = relative(context.realRoot, candidate.real).split(sep)[0];
282
- return (
283
- first.toLowerCase() === "tools" ||
284
- candidate.real.startsWith(`${chocolatey.directory}${sep}`)
285
- );
286
- }
287
-
288
79
  // The directories to look in, as absolute paths.
289
80
  function candidateDirectories(context) {
290
81
  const { rootPath, platform } = context;
@@ -318,197 +109,6 @@ function candidateDirectories(context) {
318
109
  return directories;
319
110
  }
320
111
 
321
- // A path as the system being described names it, and as a purl subpath. `base`
322
- // is the directory the path hangs off, so that a symlinked root, as a temporary
323
- // directory often is, does not matter.
324
- function locate(context, base, absolutePath) {
325
- const parts = relative(base, absolutePath).split(sep);
326
- return {
327
- logical: context.pathApi.join(context.logicalRoot, ...parts),
328
- subpath: parts.join("/"),
329
- };
330
- }
331
-
332
- /**
333
- * The distinct PHP files of the fixed directories, at most `maxProbes`. A
334
- * symlink is read through to the file it names, which must itself be a PHP
335
- * file under the root, and two names for one file are one candidate.
336
- *
337
- * @param {Object} context probe context
338
- * @returns {Object[]} candidates with their real path, SAPI and locations
339
- */
340
- function collectCandidates(context) {
341
- const { realRoot, platform, maxProbes } = context;
342
- const classify =
343
- platform === "win32"
344
- ? (name) => WINDOWS_SAPIS[name.toLowerCase()]
345
- : classifyPhpSapi;
346
- const candidates = [];
347
- const seen = new Set();
348
- for (const { directory, only } of candidateDirectories(context)) {
349
- for (const name of listDirectory(directory)) {
350
- if (!classify(name)) {
351
- continue;
352
- }
353
- const listed = join(directory, name);
354
- const real = realPathOf(listed);
355
- if (!real || seen.has(real)) {
356
- continue;
357
- }
358
- const sapi = classify(basename(real));
359
- const inside = relative(realRoot, real);
360
- if (
361
- !sapi ||
362
- (only && sapi !== only) ||
363
- !inside ||
364
- inside.startsWith("..") ||
365
- isAbsolute(inside)
366
- ) {
367
- continue;
368
- }
369
- try {
370
- if (!statSync(real).isFile()) {
371
- continue;
372
- }
373
- } catch {
374
- continue;
375
- }
376
- if (candidates.length >= maxProbes) {
377
- thoughtLog(
378
- `The PHP host probe looked at ${maxProbes} files and left any others unread.`,
379
- );
380
- return candidates;
381
- }
382
- seen.add(real);
383
- candidates.push({
384
- real,
385
- sapi,
386
- name: basename(real),
387
- ...locate(context, realRoot, real),
388
- listedLogical: locate(context, context.rootPath, listed).logical,
389
- });
390
- }
391
- }
392
- return candidates;
393
- }
394
-
395
- function recordUnreadable(unreadable, recorded) {
396
- if (recorded.has(unreadable.name)) {
397
- return;
398
- }
399
- recorded.add(unreadable.name);
400
- recordDegradation("php.host-probe.package-db-unreadable", {
401
- ecosystem: "php",
402
- tool: unreadable.name,
403
- impact: "components",
404
- path: unreadable.path,
405
- detail: `The ${unreadable.name} package database could not be read in full, so PHP files it may own were not identified as unpackaged PHP.`,
406
- });
407
- }
408
-
409
- /**
410
- * Sort the candidates into those a package owns, those whose ownership could
411
- * not be settled, and the unpackaged rest.
412
- *
413
- * @param {Object[]} candidates candidates from collectCandidates
414
- * @param {Object} context probe context
415
- * @returns {{unpackaged: Object[], owned: number, unknown: number}}
416
- */
417
- function classifyOwnership(candidates, context) {
418
- const unpackaged = [];
419
- const recorded = new Set();
420
- let owned = 0;
421
- let unknown = 0;
422
- const pending = candidates.filter((candidate) => {
423
- if (packageStoreOf(candidate.logical)) {
424
- owned++;
425
- return false;
426
- }
427
- return true;
428
- });
429
- if (!pending.length) {
430
- return { unpackaged, owned, unknown };
431
- }
432
- const wanted = new Set(
433
- pending.flatMap((candidate) => [
434
- candidate.logical,
435
- candidate.listedLogical,
436
- ]),
437
- );
438
- const databases = [];
439
- if (context.platform === "win32") {
440
- databases.push(readChocolatey(context));
441
- } else {
442
- const { dpkgInfoDirectory, rpmDatabaseDirectories } = context.databases;
443
- databases.push(
444
- scanDpkg(dpkgInfoDirectory, wanted),
445
- scanApk(context, wanted),
446
- );
447
- // Only what dpkg and apk did not claim is asked of osquery.
448
- const claimed = new Set(databases.flatMap((db) => [...db.owned]));
449
- databases.push(
450
- queryRpm(
451
- rpmDatabaseDirectories,
452
- new Set([...wanted].filter((path) => !claimed.has(path))),
453
- context.executeOsQuery,
454
- ),
455
- );
456
- }
457
- for (const candidate of pending) {
458
- const paths = [candidate.logical, candidate.listedLogical];
459
- const isOwned = databases.some(
460
- (db) =>
461
- db.state === "ok" &&
462
- (db.name === "chocolatey"
463
- ? chocolateyOwns(db, context, candidate)
464
- : paths.some((path) => db.owned.has(path))),
465
- );
466
- if (isOwned) {
467
- owned++;
468
- continue;
469
- }
470
- const unreadable = databases.filter((db) => db.state === "unreadable");
471
- if (!unreadable.length) {
472
- unpackaged.push(candidate);
473
- continue;
474
- }
475
- unknown++;
476
- for (const db of unreadable) {
477
- recordUnreadable(db, recorded);
478
- }
479
- }
480
- return { unpackaged, owned, unknown };
481
- }
482
-
483
- // Like a container's unpackaged files, but named by where they are: a host can
484
- // hold several binaries of one name.
485
- function fileComponent(candidate) {
486
- const purl = tryBuildPurl({
487
- type: "generic",
488
- name: candidate.name,
489
- subpath: candidate.subpath,
490
- });
491
- return {
492
- name: candidate.name,
493
- type: "file",
494
- ...(purl ? { purl } : {}),
495
- "bom-ref": purl ? decodeURIComponent(purl) : `file:${candidate.logical}`,
496
- properties: [{ name: "internal:SrcFile", value: candidate.logical }],
497
- evidence: {
498
- identity: [
499
- {
500
- field: "purl",
501
- confidence: 0,
502
- methods: [
503
- { technique: "filename", confidence: 0, value: candidate.logical },
504
- ],
505
- concludedValue: candidate.logical,
506
- },
507
- ],
508
- },
509
- };
510
- }
511
-
512
112
  // On Windows the CGI and debugger executables hold no version; the core DLL in
513
113
  // the same directory does.
514
114
  function readBuild(context, candidate) {
@@ -531,36 +131,6 @@ function readBuild(context, candidate) {
531
131
  return undefined;
532
132
  }
533
133
 
534
- function createContext(options, platform, readEnv, logicalRoot) {
535
- const rootPath = options.rootPath || logicalRoot;
536
- const realRoot = realPathOf(rootPath);
537
- if (!realRoot) {
538
- return undefined;
539
- }
540
- const databases = options.packageDatabases || {};
541
- return {
542
- platform,
543
- readEnv,
544
- rootPath,
545
- realRoot,
546
- logicalRoot,
547
- pathApi: platform === "win32" ? win32 : posix,
548
- maxProbes: options.maxProbes || MAX_PHP_PROBES,
549
- executeOsQuery: options.executeOsQuery,
550
- packageComponents: options.packageComponents,
551
- databases: {
552
- dpkgInfoDirectory:
553
- databases.dpkgInfoDirectory || join(rootPath, "var/lib/dpkg/info"),
554
- // Unset, the reader looks where apk keeps the database under the root.
555
- apkInstalledFile: databases.apkInstalledFile,
556
- rpmDatabaseDirectories: databases.rpmDatabaseDirectories || [
557
- join(rootPath, "var/lib/rpm"),
558
- join(rootPath, "usr/lib/sysimage/rpm"),
559
- ],
560
- },
561
- };
562
- }
563
-
564
134
  /**
565
135
  * Look for PHP no package manager owns, in the fixed directories of the host.
566
136
  * Components come back only for the files nothing owns: a `file` component for
@@ -583,59 +153,33 @@ function createContext(options, platform, readEnv, logicalRoot) {
583
153
  * @returns {{components: Object[], dependencies: Object[]}}
584
154
  */
585
155
  export function probeHostPhp(options = {}) {
586
- const empty = { components: [], dependencies: [] };
587
- const platform = options.platform || process.platform;
588
- const readEnv = (name) =>
589
- options.env ? options.env[name] : readEnvironmentVariable(name);
590
- const logicalRoot =
591
- platform === "win32" ? `${readEnv("SystemDrive") || "C:"}\\` : "/";
592
- if (isDryRun) {
593
- recordActivity({
594
- capability: "php-host-probe",
595
- kind: "read",
596
- reason:
597
- "Dry run mode blocks the PHP host probe, which opens executables and package databases outside the project.",
598
- status: "blocked",
599
- target: options.rootPath || logicalRoot,
600
- });
601
- return empty;
602
- }
603
- const context = createContext(options, platform, readEnv, logicalRoot);
604
- if (!context) {
605
- return empty;
606
- }
607
- const candidates = collectCandidates(context);
608
- if (!candidates.length) {
609
- return empty;
610
- }
611
- const { unpackaged, owned, unknown } = classifyOwnership(candidates, context);
612
- const inventory = createPhpRuntimeInventory();
613
- const files = [];
614
- for (const candidate of unpackaged) {
615
- const build = readBuild(context, candidate);
616
- if (build) {
617
- const component = fileComponent(candidate);
618
- attachPhpBuild(inventory, component, build, candidate.sapi);
619
- files.push(component);
620
- }
621
- }
622
- recordActivity({
156
+ return probeHostRuntime(options, {
157
+ label: "PHP",
158
+ ecosystem: "php",
623
159
  capability: "php-host-probe",
624
- kind: "read",
625
- reason: `Looked at ${candidates.length} PHP file(s) in the fixed directories: ${owned} owned by a package, ${unknown} of unknown ownership, ${files.length} unpackaged PHP identified.`,
626
- status: "completed",
627
- target: context.rootPath,
160
+ remediationId: "php.host-probe.package-db-unreadable",
161
+ maxProbes: MAX_PHP_PROBES,
162
+ chocolateyPackages: ["php"],
163
+ // Chocolatey's php package unpacks under C:\tools by default.
164
+ chocolateyDirectories: (context) => [join(context.rootPath, "tools")],
165
+ directories: candidateDirectories,
166
+ classify: (name, platform) =>
167
+ platform === "win32"
168
+ ? WINDOWS_SAPIS[name.toLowerCase()]
169
+ : classifyPhpSapi(name),
170
+ readBuild,
171
+ createInventory: createPhpRuntimeInventory,
172
+ attach: (inventory, component, build, candidate) =>
173
+ attachPhpBuild(inventory, component, build, candidate.kind),
174
+ finish: (inventory) => ({
175
+ components: inventory.runtimeComponents,
176
+ dependencies: inventory.dependencies,
177
+ }),
178
+ hooks: {
179
+ isDryRun,
180
+ readEnvironmentVariable,
181
+ recordActivity,
182
+ recordDegradation,
183
+ },
628
184
  });
629
- if (files.length) {
630
- thoughtLog(
631
- `I found ${files.length} unpackaged PHP file(s) on this host, from ${inventory.runtimeComponents.length} version(s).`,
632
- );
633
- }
634
- return {
635
- components: [
636
- ...files.sort(byBomRef),
637
- ...inventory.runtimeComponents.sort(byBomRef),
638
- ],
639
- dependencies: inventory.dependencies,
640
- };
641
185
  }