@cyclonedx/cdxgen 12.3.2 → 12.4.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 +70 -22
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +238 -116
  4. package/bin/convert.js +28 -13
  5. package/bin/hbom.js +490 -0
  6. package/bin/repl.js +580 -29
  7. package/bin/validate.js +34 -4
  8. package/bin/verify.js +40 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/predictive-audit-allowlist.json +11 -0
  13. package/data/queries-darwin.json +12 -1
  14. package/data/queries-win.json +7 -1
  15. package/data/queries.json +39 -2
  16. package/data/rules/ai-agent-governance.yaml +16 -0
  17. package/data/rules/asar-archives.yaml +150 -0
  18. package/data/rules/chrome-extensions.yaml +8 -0
  19. package/data/rules/ci-permissions.yaml +171 -15
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +76 -5
  22. package/data/rules/hbom-compliance.yaml +325 -0
  23. package/data/rules/hbom-performance.yaml +307 -0
  24. package/data/rules/hbom-security.yaml +248 -0
  25. package/data/rules/host-topology.yaml +165 -0
  26. package/data/rules/mcp-servers.yaml +18 -3
  27. package/data/rules/obom-runtime.yaml +907 -22
  28. package/data/rules/package-integrity.yaml +36 -0
  29. package/data/rules/rootfs-hardening.yaml +179 -0
  30. package/data/rules/vscode-extensions.yaml +9 -0
  31. package/lib/audit/index.js +209 -8
  32. package/lib/audit/index.poku.js +332 -0
  33. package/lib/audit/reporters.js +222 -0
  34. package/lib/audit/targets.js +146 -1
  35. package/lib/audit/targets.poku.js +186 -0
  36. package/lib/cli/asar.poku.js +328 -0
  37. package/lib/cli/index.js +647 -127
  38. package/lib/cli/index.poku.js +1905 -187
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/agentFormulationParser.js +6 -2
  41. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  42. package/lib/helpers/analyzer.js +1444 -38
  43. package/lib/helpers/analyzer.poku.js +409 -0
  44. package/lib/helpers/analyzerScope.js +712 -0
  45. package/lib/helpers/asarutils.js +1556 -0
  46. package/lib/helpers/asarutils.poku.js +443 -0
  47. package/lib/helpers/auditCategories.js +12 -0
  48. package/lib/helpers/auditCategories.poku.js +32 -0
  49. package/lib/helpers/cbomutils.js +271 -1
  50. package/lib/helpers/cbomutils.poku.js +248 -5
  51. package/lib/helpers/chromextutils.js +25 -3
  52. package/lib/helpers/chromextutils.poku.js +68 -0
  53. package/lib/helpers/ciParsers/githubActions.js +79 -0
  54. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  55. package/lib/helpers/communityAiConfigParser.js +15 -5
  56. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  57. package/lib/helpers/depsUtils.js +5 -0
  58. package/lib/helpers/depsUtils.poku.js +55 -0
  59. package/lib/helpers/display.js +336 -23
  60. package/lib/helpers/display.poku.js +179 -43
  61. package/lib/helpers/evidenceUtils.js +58 -0
  62. package/lib/helpers/evidenceUtils.poku.js +54 -0
  63. package/lib/helpers/exportUtils.js +9 -0
  64. package/lib/helpers/gtfobins.js +142 -8
  65. package/lib/helpers/gtfobins.poku.js +24 -1
  66. package/lib/helpers/hbom.js +710 -0
  67. package/lib/helpers/hbom.poku.js +496 -0
  68. package/lib/helpers/hbomAnalysis.js +268 -0
  69. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  70. package/lib/helpers/hbomLoader.js +35 -0
  71. package/lib/helpers/hostTopology.js +803 -0
  72. package/lib/helpers/hostTopology.poku.js +363 -0
  73. package/lib/helpers/inventoryStats.js +69 -0
  74. package/lib/helpers/inventoryStats.poku.js +86 -0
  75. package/lib/helpers/lolbas.js +19 -1
  76. package/lib/helpers/lolbas.poku.js +23 -0
  77. package/lib/helpers/mcpConfigParser.js +21 -5
  78. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  79. package/lib/helpers/osqueryTransform.js +47 -0
  80. package/lib/helpers/osqueryTransform.poku.js +47 -0
  81. package/lib/helpers/plugins.js +349 -0
  82. package/lib/helpers/plugins.poku.js +57 -0
  83. package/lib/helpers/propertySanitizer.js +121 -0
  84. package/lib/helpers/protobom.js +156 -45
  85. package/lib/helpers/protobom.poku.js +140 -5
  86. package/lib/helpers/remote/dependency-track.js +36 -3
  87. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  88. package/lib/helpers/source.js +24 -0
  89. package/lib/helpers/source.poku.js +32 -0
  90. package/lib/helpers/utils.js +2454 -198
  91. package/lib/helpers/utils.poku.js +1798 -74
  92. package/lib/managers/binary.e2e.poku.js +367 -0
  93. package/lib/managers/binary.js +2306 -350
  94. package/lib/managers/binary.poku.js +1700 -1
  95. package/lib/managers/docker.js +441 -95
  96. package/lib/managers/docker.poku.js +1479 -14
  97. package/lib/server/server.js +2 -24
  98. package/lib/server/server.poku.js +36 -1
  99. package/lib/stages/postgen/annotator.js +38 -0
  100. package/lib/stages/postgen/annotator.poku.js +107 -1
  101. package/lib/stages/postgen/auditBom.js +121 -18
  102. package/lib/stages/postgen/auditBom.poku.js +2967 -990
  103. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  104. package/lib/stages/postgen/postgen.js +192 -1
  105. package/lib/stages/postgen/postgen.poku.js +321 -0
  106. package/lib/stages/postgen/ruleEngine.js +116 -0
  107. package/lib/stages/pregen/envAudit.js +14 -3
  108. package/package.json +24 -21
  109. package/types/bin/hbom.d.ts +3 -0
  110. package/types/bin/hbom.d.ts.map +1 -0
  111. package/types/bin/repl.d.ts.map +1 -1
  112. package/types/lib/audit/index.d.ts +44 -0
  113. package/types/lib/audit/index.d.ts.map +1 -1
  114. package/types/lib/audit/reporters.d.ts +16 -0
  115. package/types/lib/audit/reporters.d.ts.map +1 -1
  116. package/types/lib/audit/targets.d.ts.map +1 -1
  117. package/types/lib/cli/index.d.ts +16 -0
  118. package/types/lib/cli/index.d.ts.map +1 -1
  119. package/types/lib/evinser/evinser.d.ts +4 -0
  120. package/types/lib/evinser/evinser.d.ts.map +1 -1
  121. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  122. package/types/lib/helpers/analyzer.d.ts +33 -0
  123. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  124. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  125. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  126. package/types/lib/helpers/asarutils.d.ts +34 -0
  127. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  128. package/types/lib/helpers/auditCategories.d.ts +5 -0
  129. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  130. package/types/lib/helpers/cbomutils.d.ts +3 -2
  131. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  132. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  133. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  134. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  135. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  136. package/types/lib/helpers/display.d.ts +1 -0
  137. package/types/lib/helpers/display.d.ts.map +1 -1
  138. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  139. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  141. package/types/lib/helpers/gtfobins.d.ts +8 -0
  142. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  143. package/types/lib/helpers/hbom.d.ts +49 -0
  144. package/types/lib/helpers/hbom.d.ts.map +1 -0
  145. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  146. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  147. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  148. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  149. package/types/lib/helpers/hostTopology.d.ts +12 -0
  150. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  151. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  152. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  153. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  154. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  155. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  156. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  157. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  158. package/types/lib/helpers/plugins.d.ts +58 -0
  159. package/types/lib/helpers/plugins.d.ts.map +1 -0
  160. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  161. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  162. package/types/lib/helpers/protobom.d.ts +3 -4
  163. package/types/lib/helpers/protobom.d.ts.map +1 -1
  164. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  165. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  166. package/types/lib/helpers/source.d.ts.map +1 -1
  167. package/types/lib/helpers/utils.d.ts +74 -8
  168. package/types/lib/helpers/utils.d.ts.map +1 -1
  169. package/types/lib/managers/binary.d.ts +5 -0
  170. package/types/lib/managers/binary.d.ts.map +1 -1
  171. package/types/lib/managers/docker.d.ts +3 -0
  172. package/types/lib/managers/docker.d.ts.map +1 -1
  173. package/types/lib/server/server.d.ts +2 -0
  174. package/types/lib/server/server.d.ts.map +1 -1
  175. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  176. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  177. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  178. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  179. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  180. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  182. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -11,6 +11,7 @@ import {
11
11
  mkdirSync,
12
12
  mkdtempSync,
13
13
  readFileSync,
14
+ realpathSync,
14
15
  rmSync,
15
16
  unlinkSync,
16
17
  writeFileSync,
@@ -55,17 +56,25 @@ import { getTreeWithPlugin } from "../managers/piptree.js";
55
56
  import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
56
57
  import Arborist from "../third-party/arborist/lib/index.js";
57
58
  import { analyzeSuspiciousJsFile } from "./analyzer.js";
59
+ import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "./auditCategories.js";
58
60
  import { parseWorkflowFile } from "./ciParsers/githubActions.js";
59
61
  import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
62
+ import {
63
+ createOccurrenceEvidence,
64
+ parseOccurrenceEvidenceLocation,
65
+ } from "./evidenceUtils.js";
66
+ import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
60
67
  import { thoughtLog, traceLog } from "./logger.js";
61
68
  import { createLolbasProperties } from "./lolbas.js";
62
69
  import {
70
+ createOsQueryFallbackBomRef,
63
71
  createOsQueryPurl,
64
72
  deriveOsQueryDescription,
65
73
  deriveOsQueryName,
66
74
  deriveOsQueryPublisher,
67
75
  deriveOsQueryVersion,
68
76
  sanitizeOsQueryIdentity,
77
+ shouldCreateOsQueryPurl,
69
78
  } from "./osqueryTransform.js";
70
79
  import {
71
80
  collectPyLockFileComponents,
@@ -116,6 +125,596 @@ export const DRY_RUN_ERROR_CODE = "CDXGEN_DRY_RUN";
116
125
  const activityLedger = [];
117
126
  let activityCounter = 0;
118
127
  let currentActivityContext = {};
128
+ const dryRunReadTraceState =
129
+ globalThis.__cdxgenDryRunReadTraceState ||
130
+ (globalThis.__cdxgenDryRunReadTraceState = {
131
+ environmentReads: new Map(),
132
+ observations: new Map(),
133
+ recordActivity: undefined,
134
+ sensitiveFileReads: new Map(),
135
+ });
136
+ const SENSITIVE_ENV_VAR_PATTERN =
137
+ /(^|_)(?:token|key|secret|pass(?:word)?|credential(?:s)?|cred|auth|session|cookie|email|user)$/i;
138
+ const DIRECTORY_DISCOVERY_NAMES = new Set([
139
+ ".cargo",
140
+ ".docker",
141
+ ".gem",
142
+ ".github",
143
+ ".m2",
144
+ ".nuget",
145
+ ".venv",
146
+ ".yarn",
147
+ "blobs",
148
+ "extensions",
149
+ "node_modules",
150
+ "target",
151
+ "vendor",
152
+ ]);
153
+ const LOCKFILE_ACTIVITY_HINTS = new Map([
154
+ [
155
+ "bun.lock",
156
+ { classification: "lockfile", ecosystem: "bun", label: "Bun lockfile" },
157
+ ],
158
+ [
159
+ "cargo.lock",
160
+ { classification: "lockfile", ecosystem: "cargo", label: "Cargo lockfile" },
161
+ ],
162
+ [
163
+ "composer.lock",
164
+ {
165
+ classification: "lockfile",
166
+ ecosystem: "composer",
167
+ label: "Composer lockfile",
168
+ },
169
+ ],
170
+ [
171
+ "gemfile.lock",
172
+ {
173
+ classification: "lockfile",
174
+ ecosystem: "rubygems",
175
+ label: "Bundler lockfile",
176
+ },
177
+ ],
178
+ [
179
+ "package-lock.json",
180
+ { classification: "lockfile", ecosystem: "npm", label: "npm lockfile" },
181
+ ],
182
+ [
183
+ "packages.lock.json",
184
+ { classification: "lockfile", ecosystem: "nuget", label: "NuGet lockfile" },
185
+ ],
186
+ [
187
+ "pdm.lock",
188
+ { classification: "lockfile", ecosystem: "python", label: "PDM lockfile" },
189
+ ],
190
+ [
191
+ "pnpm-lock.yaml",
192
+ { classification: "lockfile", ecosystem: "pnpm", label: "pnpm lockfile" },
193
+ ],
194
+ [
195
+ "poetry.lock",
196
+ {
197
+ classification: "lockfile",
198
+ ecosystem: "python",
199
+ label: "Poetry lockfile",
200
+ },
201
+ ],
202
+ [
203
+ "podfile.lock",
204
+ {
205
+ classification: "lockfile",
206
+ ecosystem: "cocoapods",
207
+ label: "CocoaPods lockfile",
208
+ },
209
+ ],
210
+ [
211
+ "pylock.toml",
212
+ {
213
+ classification: "lockfile",
214
+ ecosystem: "python",
215
+ label: "PEP 751 lockfile",
216
+ },
217
+ ],
218
+ [
219
+ "uv.lock",
220
+ { classification: "lockfile", ecosystem: "python", label: "uv lockfile" },
221
+ ],
222
+ [
223
+ "yarn.lock",
224
+ { classification: "lockfile", ecosystem: "yarn", label: "Yarn lockfile" },
225
+ ],
226
+ ]);
227
+ const MANIFEST_ACTIVITY_HINTS = new Map([
228
+ [
229
+ "cargo.toml",
230
+ { classification: "manifest", ecosystem: "cargo", label: "Cargo manifest" },
231
+ ],
232
+ [
233
+ "composer.json",
234
+ {
235
+ classification: "manifest",
236
+ ecosystem: "composer",
237
+ label: "Composer manifest",
238
+ },
239
+ ],
240
+ [
241
+ "gemfile",
242
+ {
243
+ classification: "manifest",
244
+ ecosystem: "rubygems",
245
+ label: "Gem manifest",
246
+ },
247
+ ],
248
+ [
249
+ "package.json",
250
+ { classification: "manifest", ecosystem: "npm", label: "package manifest" },
251
+ ],
252
+ [
253
+ "pom.xml",
254
+ { classification: "manifest", ecosystem: "maven", label: "Maven manifest" },
255
+ ],
256
+ [
257
+ "pyproject.toml",
258
+ {
259
+ classification: "manifest",
260
+ ecosystem: "python",
261
+ label: "Python project manifest",
262
+ },
263
+ ],
264
+ [
265
+ "requirements.txt",
266
+ {
267
+ classification: "manifest",
268
+ ecosystem: "python",
269
+ label: "Python requirements manifest",
270
+ },
271
+ ],
272
+ [
273
+ "setup.py",
274
+ {
275
+ classification: "manifest",
276
+ ecosystem: "python",
277
+ label: "Python setup manifest",
278
+ },
279
+ ],
280
+ ]);
281
+ const SENSITIVE_CONFIG_ACTIVITY_HINTS = [
282
+ {
283
+ matcher: (lowerPath, _baseName) =>
284
+ lowerPath.includes("/.cargo/config.toml") ||
285
+ lowerPath.endsWith("/.cargo/credentials") ||
286
+ lowerPath.endsWith("/.cargo/credentials.toml"),
287
+ metadata: {
288
+ classification: "config",
289
+ ecosystem: "cargo",
290
+ label: "Cargo registry configuration",
291
+ sensitive: true,
292
+ },
293
+ },
294
+ {
295
+ matcher: (lowerPath, baseName) =>
296
+ lowerPath.includes("/.docker/config.json") ||
297
+ (baseName === "config.json" && lowerPath.includes("/docker")),
298
+ metadata: {
299
+ classification: "credential",
300
+ ecosystem: "oci",
301
+ label: "Docker credential file",
302
+ sensitive: true,
303
+ },
304
+ },
305
+ {
306
+ matcher: (lowerPath) => lowerPath.endsWith("/.gem/credentials"),
307
+ metadata: {
308
+ classification: "credential",
309
+ ecosystem: "rubygems",
310
+ label: "RubyGems credentials file",
311
+ sensitive: true,
312
+ },
313
+ },
314
+ {
315
+ matcher: (_lowerPath, baseName) =>
316
+ baseName === ".npmrc" || baseName === ".pnpmrc" || baseName === ".yarnrc",
317
+ metadata: {
318
+ classification: "config",
319
+ ecosystem: "npm",
320
+ label: "JavaScript package manager configuration",
321
+ sensitive: true,
322
+ },
323
+ },
324
+ {
325
+ matcher: (_lowerPath, baseName) => baseName === ".yarnrc.yml",
326
+ metadata: {
327
+ classification: "config",
328
+ ecosystem: "yarn",
329
+ label: "Yarn configuration",
330
+ sensitive: true,
331
+ },
332
+ },
333
+ {
334
+ matcher: (_lowerPath, baseName) =>
335
+ baseName === ".pypirc" || baseName === "pip.conf",
336
+ metadata: {
337
+ classification: "config",
338
+ ecosystem: "python",
339
+ label: "Python package publishing configuration",
340
+ sensitive: true,
341
+ },
342
+ },
343
+ {
344
+ matcher: (_lowerPath, baseName) =>
345
+ baseName === "uv.toml" || baseName === "poetry.toml",
346
+ metadata: {
347
+ classification: "config",
348
+ ecosystem: "python",
349
+ label: "Python package manager configuration",
350
+ sensitive: true,
351
+ },
352
+ },
353
+ {
354
+ matcher: (_lowerPath, baseName) => baseName === "nuget.config",
355
+ metadata: {
356
+ classification: "config",
357
+ ecosystem: "nuget",
358
+ label: "NuGet configuration",
359
+ sensitive: true,
360
+ },
361
+ },
362
+ {
363
+ matcher: (_lowerPath, baseName) => baseName === "settings.xml",
364
+ metadata: {
365
+ classification: "config",
366
+ ecosystem: "maven",
367
+ label: "Maven settings.xml",
368
+ sensitive: true,
369
+ },
370
+ },
371
+ ];
372
+ const CERTIFICATE_FILE_EXTENSIONS = new Set([".crt", ".cer", ".pem"]);
373
+ const KEY_FILE_EXTENSIONS = new Set([
374
+ ".key",
375
+ ".jks",
376
+ ".keystore",
377
+ ".p12",
378
+ ".pfx",
379
+ ]);
380
+
381
+ const buildReadCountSuffix = (count) => (count > 1 ? ` (${count} times)` : "");
382
+
383
+ const buildEnvironmentReadReason = (varName, count, sensitive) =>
384
+ `Read ${sensitive ? "sensitive " : ""}environment variable ${varName}${buildReadCountSuffix(count)}.`;
385
+
386
+ const buildSensitiveFileReadReason = (filePath, count, label) =>
387
+ `Read ${label} ${filePath}${buildReadCountSuffix(count)}.`;
388
+
389
+ function emitActivity(activity) {
390
+ if (typeof dryRunReadTraceState.recordActivity !== "function") {
391
+ return undefined;
392
+ }
393
+ return dryRunReadTraceState.recordActivity(activity);
394
+ }
395
+
396
+ function classifyActivityPath(filePath) {
397
+ if (typeof filePath !== "string" || !filePath.length) {
398
+ return undefined;
399
+ }
400
+ const normalizedPath = filePath.replaceAll("\\", "/");
401
+ const lowerPath = normalizedPath.toLowerCase();
402
+ const baseName = basename(lowerPath);
403
+ if (LOCKFILE_ACTIVITY_HINTS.has(baseName)) {
404
+ return LOCKFILE_ACTIVITY_HINTS.get(baseName);
405
+ }
406
+ if (MANIFEST_ACTIVITY_HINTS.has(baseName)) {
407
+ return MANIFEST_ACTIVITY_HINTS.get(baseName);
408
+ }
409
+ for (const { matcher, metadata } of SENSITIVE_CONFIG_ACTIVITY_HINTS) {
410
+ if (matcher(lowerPath, baseName)) {
411
+ return metadata;
412
+ }
413
+ }
414
+ if (
415
+ lowerPath.includes("/cache/") ||
416
+ lowerPath.includes("/.cache/") ||
417
+ lowerPath.includes("/caches/")
418
+ ) {
419
+ return {
420
+ classification: "cache",
421
+ label: "cache path",
422
+ };
423
+ }
424
+ if (
425
+ CERTIFICATE_FILE_EXTENSIONS.has(extname(baseName)) ||
426
+ baseName === "cert.pem"
427
+ ) {
428
+ return {
429
+ classification: "certificate",
430
+ label: "certificate file",
431
+ sensitive: true,
432
+ };
433
+ }
434
+ if (
435
+ KEY_FILE_EXTENSIONS.has(extname(baseName)) ||
436
+ baseName === "key.pem" ||
437
+ baseName.startsWith("id_")
438
+ ) {
439
+ return {
440
+ classification: "key",
441
+ label: "private key file",
442
+ sensitive: true,
443
+ };
444
+ }
445
+ const trimmedPath = normalizedPath.endsWith("/")
446
+ ? normalizedPath.slice(0, -1)
447
+ : normalizedPath;
448
+ const directoryName = basename(trimmedPath.toLowerCase());
449
+ if (DIRECTORY_DISCOVERY_NAMES.has(directoryName)) {
450
+ return {
451
+ classification: "directory",
452
+ label: "directory discovery path",
453
+ };
454
+ }
455
+ return undefined;
456
+ }
457
+
458
+ function classifyDiscoveryPattern(pattern) {
459
+ const patternValue = Array.isArray(pattern)
460
+ ? pattern.join(",")
461
+ : String(pattern);
462
+ const lowerPattern = patternValue.toLowerCase();
463
+ if (
464
+ lowerPattern.includes("package-lock.json") ||
465
+ lowerPattern.includes("pnpm-lock.yaml") ||
466
+ lowerPattern.includes("yarn.lock") ||
467
+ lowerPattern.includes("poetry.lock") ||
468
+ lowerPattern.includes("uv.lock") ||
469
+ lowerPattern.includes("cargo.lock") ||
470
+ lowerPattern.includes("gemfile.lock")
471
+ ) {
472
+ return {
473
+ discoveryType: "lockfile-discovery",
474
+ label: "lockfile discovery",
475
+ };
476
+ }
477
+ if (
478
+ lowerPattern.includes("package.json") ||
479
+ lowerPattern.includes("pom.xml") ||
480
+ lowerPattern.includes("pyproject.toml") ||
481
+ lowerPattern.includes("cargo.toml") ||
482
+ lowerPattern.includes("composer.json")
483
+ ) {
484
+ return {
485
+ discoveryType: "manifest-discovery",
486
+ label: "manifest discovery",
487
+ };
488
+ }
489
+ return {
490
+ discoveryType: "directory-enumeration",
491
+ label: "directory enumeration",
492
+ };
493
+ }
494
+
495
+ function recordDeduplicatedRead(traceMap, traceKey, activity, createReason) {
496
+ const existingTrace = traceMap.get(traceKey);
497
+ if (existingTrace) {
498
+ existingTrace.count += 1;
499
+ if (existingTrace.entry) {
500
+ existingTrace.entry.count = existingTrace.count;
501
+ existingTrace.entry.reason = createReason(existingTrace.count);
502
+ }
503
+ return existingTrace.entry;
504
+ }
505
+ const entry = emitActivity({
506
+ ...activity,
507
+ reason: createReason(1),
508
+ });
509
+ if (entry) {
510
+ entry.count = 1;
511
+ }
512
+ traceMap.set(traceKey, {
513
+ count: 1,
514
+ entry,
515
+ });
516
+ return entry;
517
+ }
518
+
519
+ export function isSensitiveEnvironmentVariableName(varName) {
520
+ return typeof varName === "string" && SENSITIVE_ENV_VAR_PATTERN.test(varName);
521
+ }
522
+
523
+ export function recordObservedActivity(kind, target, options = {}) {
524
+ if (!(isDryRun || DEBUG_MODE) || !kind || !target) {
525
+ return undefined;
526
+ }
527
+ const status = options.status || "completed";
528
+ const traceKey =
529
+ options.traceKey ||
530
+ `${kind}:${status}:${target}:${options.traceDetail || ""}`;
531
+ const metadata = options.metadata || {};
532
+ const reasonBuilder =
533
+ options.reasonBuilder ||
534
+ ((count) =>
535
+ options.reason
536
+ ? `${options.reason}${buildReadCountSuffix(count)}`
537
+ : `Recorded ${kind} activity for ${target}${buildReadCountSuffix(count)}.`);
538
+ return recordDeduplicatedRead(
539
+ dryRunReadTraceState.observations,
540
+ traceKey,
541
+ {
542
+ kind,
543
+ status,
544
+ target,
545
+ ...metadata,
546
+ },
547
+ reasonBuilder,
548
+ );
549
+ }
550
+
551
+ export function recordDecisionActivity(target, options = {}) {
552
+ return recordObservedActivity(options.kind || "decision", target, options);
553
+ }
554
+
555
+ export function recordDiscoveryActivity(target, options = {}) {
556
+ return recordObservedActivity(options.kind || "discover", target, options);
557
+ }
558
+
559
+ export function recordPolicyActivity(target, options = {}) {
560
+ return recordObservedActivity(options.kind || "policy", target, options);
561
+ }
562
+
563
+ function normalizeRecordedPathForComparison(
564
+ candidatePath,
565
+ basePath = undefined,
566
+ ) {
567
+ if (typeof candidatePath !== "string" || !candidatePath.length) {
568
+ return undefined;
569
+ }
570
+ let normalizedPath = candidatePath.replaceAll("\\", "/");
571
+ if (basePath && path.isAbsolute(candidatePath)) {
572
+ const resolvedBasePath = resolve(basePath);
573
+ const normalizedBasePath = resolvedBasePath.replaceAll("\\", "/");
574
+ const isWithinBasePath = (candidate) => {
575
+ const normalizedCandidate = candidate.replaceAll("\\", "/");
576
+ return (
577
+ normalizedCandidate === normalizedBasePath ||
578
+ normalizedCandidate.startsWith(`${normalizedBasePath}/`)
579
+ );
580
+ };
581
+
582
+ const resolvedCandidatePath = resolve(candidatePath);
583
+ if (isWithinBasePath(resolvedCandidatePath)) {
584
+ normalizedPath = relative(
585
+ resolvedBasePath,
586
+ resolvedCandidatePath,
587
+ ).replaceAll("\\", "/");
588
+ } else {
589
+ const rebasedCandidatePath = resolve(
590
+ resolvedBasePath,
591
+ candidatePath.replace(/^([A-Za-z]:)?[\\/]+/, ""),
592
+ );
593
+ if (isWithinBasePath(rebasedCandidatePath)) {
594
+ normalizedPath = relative(
595
+ resolvedBasePath,
596
+ rebasedCandidatePath,
597
+ ).replaceAll("\\", "/");
598
+ }
599
+ }
600
+ }
601
+ return normalizedPath;
602
+ }
603
+
604
+ export function recordSymlinkResolution(
605
+ sourcePath,
606
+ resolvedPath,
607
+ options = {},
608
+ ) {
609
+ const normalizedSourcePath = normalizeRecordedPathForComparison(
610
+ sourcePath,
611
+ options.basePath,
612
+ );
613
+ const normalizedResolvedPath = normalizeRecordedPathForComparison(
614
+ resolvedPath,
615
+ options.basePath,
616
+ );
617
+ const status = options.status || "completed";
618
+ if (
619
+ !normalizedSourcePath ||
620
+ (status === "completed" &&
621
+ (!normalizedResolvedPath ||
622
+ normalizedSourcePath === normalizedResolvedPath))
623
+ ) {
624
+ return undefined;
625
+ }
626
+ const metadata = {
627
+ capability: "symlink-resolution",
628
+ ...(normalizedResolvedPath ? { resolvedPath: normalizedResolvedPath } : {}),
629
+ ...(options.errorCode ? { errorCode: options.errorCode } : {}),
630
+ ...(options.metadata || {}),
631
+ };
632
+ return recordObservedActivity("symlink-resolution", normalizedSourcePath, {
633
+ metadata,
634
+ reason:
635
+ options.reason ||
636
+ (status === "failed"
637
+ ? `Failed to resolve symlink ${normalizedSourcePath}.`
638
+ : `Resolved symlink ${normalizedSourcePath} to ${normalizedResolvedPath}.`),
639
+ status,
640
+ });
641
+ }
642
+
643
+ function getArchiveSourceByteSize(sourcePath) {
644
+ if (!sourcePath || !safeExistsSync(sourcePath)) {
645
+ return undefined;
646
+ }
647
+ try {
648
+ const sourceStats = lstatSync(sourcePath);
649
+ return sourceStats.isFile() ? sourceStats.size : undefined;
650
+ } catch {
651
+ return undefined;
652
+ }
653
+ }
654
+
655
+ export function recordEnvironmentRead(varName, options = {}) {
656
+ // Read tracing intentionally mirrors the activity ledger's dry-run/debug behavior.
657
+ if (!(isDryRun || DEBUG_MODE) || !varName) {
658
+ return undefined;
659
+ }
660
+ const source = options.source || "process.env";
661
+ const sensitive =
662
+ options.sensitive ?? isSensitiveEnvironmentVariableName(varName);
663
+ const status = options.status || "completed";
664
+ const traceKey = `${source}:${varName}:${status}`;
665
+ const target = `${source}:${varName}`;
666
+ return recordDeduplicatedRead(
667
+ dryRunReadTraceState.environmentReads,
668
+ traceKey,
669
+ {
670
+ kind: "env",
671
+ redacted: sensitive,
672
+ secretCategory: sensitive ? "environment-variable" : undefined,
673
+ sensitive,
674
+ status,
675
+ target,
676
+ },
677
+ (count) =>
678
+ options.reason || buildEnvironmentReadReason(varName, count, sensitive),
679
+ );
680
+ }
681
+
682
+ export function recordSensitiveFileRead(filePath, options = {}) {
683
+ // Read tracing intentionally mirrors the activity ledger's dry-run/debug behavior.
684
+ if (!(isDryRun || DEBUG_MODE) || !filePath) {
685
+ return undefined;
686
+ }
687
+ const kind = options.kind || "read";
688
+ const pathMetadata = classifyActivityPath(filePath) || {};
689
+ const label = options.label || pathMetadata.label || "sensitive file";
690
+ const status = options.status || "completed";
691
+ const traceKey = `${kind}:${status}:${filePath}`;
692
+ return recordDeduplicatedRead(
693
+ dryRunReadTraceState.sensitiveFileReads,
694
+ traceKey,
695
+ {
696
+ classification: pathMetadata.classification,
697
+ ecosystem: pathMetadata.ecosystem,
698
+ kind,
699
+ redacted: pathMetadata.sensitive ?? true,
700
+ secretCategory:
701
+ pathMetadata.classification === "key"
702
+ ? "private-key"
703
+ : pathMetadata.classification === "certificate"
704
+ ? "certificate"
705
+ : "credential-file",
706
+ status,
707
+ target: filePath,
708
+ },
709
+ (count) =>
710
+ options.reason || buildSensitiveFileReadReason(filePath, count, label),
711
+ );
712
+ }
713
+
714
+ export function readEnvironmentVariable(varName, options = {}) {
715
+ recordEnvironmentRead(varName, options);
716
+ return process.env[varName];
717
+ }
119
718
 
120
719
  export function setDryRunMode(enabled) {
121
720
  isDryRun = !!enabled;
@@ -160,10 +759,8 @@ export function recordActivity(activity) {
160
759
  const identifier = `ACT-${String(++activityCounter).padStart(4, "0")}`;
161
760
  const entry = {
162
761
  identifier,
762
+ ...currentActivityContext,
163
763
  timestamp: new Date().toISOString(),
164
- projectType: currentActivityContext.projectType,
165
- packageType: currentActivityContext.packageType,
166
- sourcePath: currentActivityContext.sourcePath,
167
764
  ...activity,
168
765
  };
169
766
  activityLedger.push(entry);
@@ -171,6 +768,8 @@ export function recordActivity(activity) {
171
768
  return entry;
172
769
  }
173
770
 
771
+ dryRunReadTraceState.recordActivity = recordActivity;
772
+
174
773
  export function getRecordedActivities() {
175
774
  return [...activityLedger];
176
775
  }
@@ -178,11 +777,21 @@ export function getRecordedActivities() {
178
777
  export function resetRecordedActivities() {
179
778
  activityLedger.length = 0;
180
779
  activityCounter = 0;
780
+ dryRunReadTraceState.environmentReads.clear();
781
+ dryRunReadTraceState.observations.clear();
782
+ dryRunReadTraceState.sensitiveFileReads.clear();
181
783
  }
182
784
 
183
- function recordFilesystemActivity(kind, target, status, reason = undefined) {
785
+ function recordFilesystemActivity(
786
+ kind,
787
+ target,
788
+ status,
789
+ reason = undefined,
790
+ metadata = {},
791
+ ) {
184
792
  return recordActivity({
185
793
  kind,
794
+ ...metadata,
186
795
  reason,
187
796
  status,
188
797
  target,
@@ -217,13 +826,40 @@ function hasWritePermission(filePath) {
217
826
  * @Boolean True if the path exists. False otherwise
218
827
  */
219
828
  export function safeExistsSync(filePath) {
829
+ const pathMetadata = classifyActivityPath(filePath);
220
830
  if (!hasReadPermission(filePath)) {
221
831
  if (DEBUG_MODE) {
222
832
  console.log("cdxgen lacks read permission for a requested path.");
223
833
  }
834
+ if (pathMetadata) {
835
+ recordPolicyActivity(filePath, {
836
+ metadata: {
837
+ classification: pathMetadata.classification,
838
+ ecosystem: pathMetadata.ecosystem,
839
+ policyType: "fs.read",
840
+ },
841
+ reason: `Denied inspection of ${pathMetadata.label} ${filePath} due to missing fs.read permission.`,
842
+ status: "blocked",
843
+ });
844
+ }
224
845
  return false;
225
846
  }
226
- return existsSync(filePath);
847
+ const exists = existsSync(filePath);
848
+ if (pathMetadata) {
849
+ const inspectionKind =
850
+ pathMetadata.classification === "directory" ? "discover" : "inspect";
851
+ recordObservedActivity(inspectionKind, filePath, {
852
+ metadata: {
853
+ classification: pathMetadata.classification,
854
+ ecosystem: pathMetadata.ecosystem,
855
+ exists,
856
+ redacted: pathMetadata.sensitive ?? false,
857
+ },
858
+ reasonBuilder: (count) =>
859
+ `${exists ? "Inspected" : "Checked for"} ${pathMetadata.label} ${filePath}${buildReadCountSuffix(count)}.`,
860
+ });
861
+ }
862
+ return exists;
227
863
  }
228
864
 
229
865
  export function safeWriteSync(filePath, data, options) {
@@ -248,9 +884,8 @@ export function safeWriteSync(filePath, data, options) {
248
884
  );
249
885
  return undefined;
250
886
  }
251
- const result = writeFileSync(filePath, data, options);
887
+ writeFileSync(filePath, data, options);
252
888
  recordFilesystemActivity("write", filePath, "completed");
253
- return result;
254
889
  }
255
890
 
256
891
  /**
@@ -282,24 +917,32 @@ export function safeMkdirSync(filePath, options) {
282
917
  );
283
918
  return undefined;
284
919
  }
285
- const result = mkdirSync(filePath, options);
920
+ mkdirSync(filePath, options);
286
921
  recordFilesystemActivity("mkdir", filePath, "completed");
287
- return result;
288
922
  }
289
923
 
290
924
  export function safeMkdtempSync(prefix, options = undefined) {
925
+ const resourceType =
926
+ typeof prefix === "string" && prefix.toLowerCase().includes("cache")
927
+ ? "cache"
928
+ : "temporary-workspace";
291
929
  if (isDryRun) {
292
930
  const tempPath = `${prefix}${randomUUID().replaceAll("-", "").slice(0, 6)}`;
293
931
  recordFilesystemActivity(
294
932
  "temp-dir",
295
933
  tempPath,
296
934
  "blocked",
297
- "Dry run mode blocks temporary directory creation.",
935
+ `Dry run mode blocks temporary directory creation for ${resourceType}.`,
936
+ {
937
+ resourceType,
938
+ },
298
939
  );
299
940
  return tempPath;
300
941
  }
301
942
  const tempPath = mkdtempSync(prefix, options);
302
- recordFilesystemActivity("temp-dir", tempPath, "completed");
943
+ recordFilesystemActivity("temp-dir", tempPath, "completed", undefined, {
944
+ resourceType,
945
+ });
303
946
  return tempPath;
304
947
  }
305
948
 
@@ -313,9 +956,8 @@ export function safeRmSync(filePath, options = undefined) {
313
956
  );
314
957
  return undefined;
315
958
  }
316
- const result = rmSync(filePath, options);
959
+ rmSync(filePath, options);
317
960
  recordFilesystemActivity("cleanup", filePath, "completed");
318
- return result;
319
961
  }
320
962
 
321
963
  export function safeUnlinkSync(filePath) {
@@ -328,9 +970,8 @@ export function safeUnlinkSync(filePath) {
328
970
  );
329
971
  return undefined;
330
972
  }
331
- const result = unlinkSync(filePath);
973
+ unlinkSync(filePath);
332
974
  recordFilesystemActivity("cleanup", filePath, "completed");
333
- return result;
334
975
  }
335
976
 
336
977
  export function safeCopyFileSync(src, dest, mode = undefined) {
@@ -356,32 +997,69 @@ export async function safeExtractArchive(
356
997
  targetPath,
357
998
  extractor,
358
999
  kind = "unzip",
1000
+ options = undefined,
359
1001
  ) {
1002
+ const traceArchiveStats = isDryRun || DEBUG_MODE;
1003
+ const sourceBytes = traceArchiveStats
1004
+ ? getArchiveSourceByteSize(sourcePath)
1005
+ : undefined;
360
1006
  if (isDryRun) {
361
1007
  recordActivity({
1008
+ archiveKind: kind,
1009
+ capability: "archive-extraction",
362
1010
  kind,
363
- reason: "Dry run mode blocks archive extraction and decompression.",
1011
+ ...(options?.metadata || {}),
1012
+ ...(sourceBytes !== undefined ? { sourceBytes } : {}),
1013
+ reason:
1014
+ options?.blockedReason ||
1015
+ `Dry run mode blocks ${kind} extraction from ${sourcePath} into ${targetPath}.`,
364
1016
  status: "blocked",
365
1017
  target: `${sourcePath} -> ${targetPath}`,
366
1018
  });
367
1019
  return false;
368
1020
  }
369
- await extractor();
370
- recordActivity({
371
- kind,
372
- status: "completed",
373
- target: `${sourcePath} -> ${targetPath}`,
374
- });
375
- return true;
1021
+ try {
1022
+ await extractor();
1023
+ recordActivity({
1024
+ archiveKind: kind,
1025
+ capability: "archive-extraction",
1026
+ kind,
1027
+ ...(options?.metadata || {}),
1028
+ ...(sourceBytes !== undefined ? { sourceBytes } : {}),
1029
+ status: "completed",
1030
+ target: `${sourcePath} -> ${targetPath}`,
1031
+ });
1032
+ return true;
1033
+ } catch (error) {
1034
+ recordActivity({
1035
+ archiveKind: kind,
1036
+ capability: "archive-extraction",
1037
+ kind,
1038
+ ...(options?.metadata || {}),
1039
+ ...(sourceBytes !== undefined ? { sourceBytes } : {}),
1040
+ ...(error?.code ? { errorCode: error.code } : {}),
1041
+ reason:
1042
+ options?.failureReason ||
1043
+ `Failed ${kind} extraction from ${sourcePath} into ${targetPath}: ${error.message}`,
1044
+ status: "failed",
1045
+ target: `${sourcePath} -> ${targetPath}`,
1046
+ });
1047
+ throw error;
1048
+ }
376
1049
  }
377
1050
 
378
1051
  export const commandsExecuted = new Set();
379
- const ALLOW_COMMANDS = (process.env.CDXGEN_ALLOWED_COMMANDS || "").split(",");
380
- function isAllowedCommand(command) {
381
- if (!process.env.CDXGEN_ALLOWED_COMMANDS) {
1052
+ function isAllowedCommand(
1053
+ command,
1054
+ allowedCommandsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS"),
1055
+ ) {
1056
+ if (!allowedCommandsEnv) {
382
1057
  return true;
383
1058
  }
384
- return ALLOW_COMMANDS.includes(command.trim());
1059
+ return allowedCommandsEnv
1060
+ .split(",")
1061
+ .map((entry) => entry.trim())
1062
+ .includes(command.trim());
385
1063
  }
386
1064
 
387
1065
  const ALLOWED_WRAPPERS = new Set(["gradlew", "mvnw"]);
@@ -428,6 +1106,71 @@ function isWindowsShellHijackRisk(command, options) {
428
1106
  return false;
429
1107
  }
430
1108
 
1109
+ const VERSION_PROBE_ARGS = new Set(["--version", "-version", "version"]);
1110
+
1111
+ function detectProbeType(command, args = []) {
1112
+ const normalizedCommand = basename(String(command || "")).toLowerCase();
1113
+ const normalizedArgs = (args || []).map((arg) => String(arg).toLowerCase());
1114
+ if (
1115
+ normalizedArgs.some((arg) => VERSION_PROBE_ARGS.has(arg)) ||
1116
+ (normalizedArgs.length === 1 && normalizedArgs[0] === "-v")
1117
+ ) {
1118
+ return "version-check";
1119
+ }
1120
+ if (normalizedCommand === "which" || normalizedArgs.includes("--help")) {
1121
+ return "capability-probe";
1122
+ }
1123
+ if (
1124
+ normalizedCommand.startsWith("python") &&
1125
+ normalizedArgs.includes("-c") &&
1126
+ normalizedArgs.some((arg) => arg.includes("import"))
1127
+ ) {
1128
+ return "runtime-probe";
1129
+ }
1130
+ return undefined;
1131
+ }
1132
+
1133
+ function buildCommandActivityDescriptor(command, args, options) {
1134
+ const target = `${command}${args?.length ? ` ${args.join(" ")}` : ""}`;
1135
+ const cdxgenActivity = options?.cdxgenActivity || {};
1136
+ const probeType = cdxgenActivity.probeType || detectProbeType(command, args);
1137
+ const metadata = {
1138
+ ...(cdxgenActivity.metadata || {}),
1139
+ };
1140
+ if (probeType) {
1141
+ metadata.capability = metadata.capability || "tool-runtime-probe";
1142
+ metadata.probeType = probeType;
1143
+ }
1144
+ if (cdxgenActivity.gitOperation) {
1145
+ metadata.gitOperation = cdxgenActivity.gitOperation;
1146
+ }
1147
+ return {
1148
+ blockedReason:
1149
+ cdxgenActivity.blockedReason ||
1150
+ (probeType
1151
+ ? `Dry run mode blocks ${probeType.replaceAll("-", " ")} command execution.`
1152
+ : "Dry run mode blocks child process execution."),
1153
+ kind: cdxgenActivity.kind || "execute",
1154
+ metadata,
1155
+ target: cdxgenActivity.target || target,
1156
+ };
1157
+ }
1158
+
1159
+ function getOutputByteSize(value, encoding = "utf-8") {
1160
+ if (value === undefined || value === null) {
1161
+ return 0;
1162
+ }
1163
+ if (Buffer.isBuffer(value)) {
1164
+ return value.length;
1165
+ }
1166
+ if (ArrayBuffer.isView(value)) {
1167
+ return value.byteLength;
1168
+ }
1169
+ const safeEncoding =
1170
+ typeof encoding === "string" && encoding !== "buffer" ? encoding : "utf8";
1171
+ return Buffer.byteLength(String(value), safeEncoding);
1172
+ }
1173
+
431
1174
  /**
432
1175
  * Safe wrapper around spawnSync that enforces permission checks, injects default
433
1176
  * options (maxBuffer, encoding, timeout), warns about unsafe Python and pip/uv
@@ -439,17 +1182,49 @@ function isWindowsShellHijackRisk(command, options) {
439
1182
  * @returns {Object} spawnSync result object with status, stdout, stderr, and error fields
440
1183
  */
441
1184
  export function safeSpawnSync(command, args, options) {
1185
+ const activityDescriptor = buildCommandActivityDescriptor(
1186
+ command,
1187
+ args,
1188
+ options,
1189
+ );
1190
+ const allowedCommandsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS");
1191
+ const commandAllowed = isAllowedCommand(command, allowedCommandsEnv);
1192
+ if (allowedCommandsEnv) {
1193
+ recordPolicyActivity(command, {
1194
+ metadata: {
1195
+ allowed: commandAllowed,
1196
+ allowlist: allowedCommandsEnv,
1197
+ policyType: "command-allowlist",
1198
+ },
1199
+ reason: `${commandAllowed ? "Allowed" : "Blocked"} command ${command} against CDXGEN_ALLOWED_COMMANDS.`,
1200
+ status: commandAllowed ? "completed" : "blocked",
1201
+ traceDetail: "allowlist",
1202
+ });
1203
+ }
1204
+ if (isSecureMode && process.permission) {
1205
+ const hasChildPermission = process.permission.has("child");
1206
+ recordPolicyActivity(command, {
1207
+ metadata: {
1208
+ allowed: hasChildPermission,
1209
+ policyType: "child-process",
1210
+ },
1211
+ reason: `${hasChildPermission ? "Confirmed" : "Denied"} child-process permission for ${command}.`,
1212
+ status: hasChildPermission ? "completed" : "blocked",
1213
+ traceDetail: "child-permission",
1214
+ });
1215
+ }
442
1216
  if (isDryRun) {
443
1217
  const error = createDryRunError(
444
1218
  "execute",
445
1219
  command,
446
- "Dry run mode blocks child process execution.",
1220
+ activityDescriptor.blockedReason,
447
1221
  );
448
1222
  recordActivity({
449
- kind: "execute",
1223
+ kind: activityDescriptor.kind,
1224
+ ...activityDescriptor.metadata,
450
1225
  reason: error.message,
451
1226
  status: "blocked",
452
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1227
+ target: activityDescriptor.target,
453
1228
  });
454
1229
  return {
455
1230
  status: 1,
@@ -460,16 +1235,17 @@ export function safeSpawnSync(command, args, options) {
460
1235
  }
461
1236
  if (
462
1237
  (isSecureMode && process.permission && !process.permission.has("child")) ||
463
- !isAllowedCommand(command)
1238
+ !commandAllowed
464
1239
  ) {
465
1240
  if (DEBUG_MODE) {
466
1241
  console.log(`cdxgen lacks execute permission for ${command}`);
467
1242
  }
468
1243
  recordActivity({
469
- kind: "execute",
1244
+ kind: activityDescriptor.kind,
1245
+ ...activityDescriptor.metadata,
470
1246
  reason: "cdxgen lacks execute permission for this command.",
471
1247
  status: "blocked",
472
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1248
+ target: activityDescriptor.target,
473
1249
  });
474
1250
  return {
475
1251
  status: 1,
@@ -483,10 +1259,11 @@ export function safeSpawnSync(command, args, options) {
483
1259
  const blockedReason = `${command} matches local file in cwd (Windows shell hijack risk)`;
484
1260
  console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
485
1261
  recordActivity({
486
- kind: "execute",
1262
+ kind: activityDescriptor.kind,
1263
+ ...activityDescriptor.metadata,
487
1264
  reason: blockedReason,
488
1265
  status: "blocked",
489
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1266
+ target: activityDescriptor.target,
490
1267
  });
491
1268
  return {
492
1269
  status: 1,
@@ -505,6 +1282,11 @@ export function safeSpawnSync(command, args, options) {
505
1282
  }
506
1283
  if (!options) {
507
1284
  options = {};
1285
+ } else if (options.cdxgenActivity) {
1286
+ options = {
1287
+ ...options,
1288
+ };
1289
+ delete options.cdxgenActivity;
508
1290
  }
509
1291
  // Inject maxBuffer
510
1292
  if (!options.maxBuffer) {
@@ -604,10 +1386,13 @@ export function safeSpawnSync(command, args, options) {
604
1386
  }
605
1387
  const result = spawnSync(command, args, options);
606
1388
  recordActivity({
607
- kind: "execute",
1389
+ kind: activityDescriptor.kind,
1390
+ ...activityDescriptor.metadata,
1391
+ stderrBytes: getOutputByteSize(result.stderr, options.encoding),
608
1392
  reason: result.error?.message,
609
1393
  status: result.status === 0 && !result.error ? "completed" : "failed",
610
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1394
+ stdoutBytes: getOutputByteSize(result.stdout, options.encoding),
1395
+ target: activityDescriptor.target,
611
1396
  });
612
1397
  return result;
613
1398
  }
@@ -981,9 +1766,10 @@ export const PROJECT_TYPE_ALIASES = {
981
1766
  dart: ["dart", "flutter", "pub"],
982
1767
  haskell: ["haskell", "hackage", "cabal"],
983
1768
  elixir: ["elixir", "hex", "mix"],
984
- c: ["c", "cpp", "c++", "conan"],
1769
+ c: ["c", "cpp", "c++", "conan", "collider"],
985
1770
  clojure: ["clojure", "edn", "clj", "leiningen"],
986
1771
  github: ["github", "actions"],
1772
+ hbom: ["hbom", "hardware"],
987
1773
  os: ["os", "osquery", "windows", "linux", "mac", "macos", "darwin"],
988
1774
  jenkins: ["jenkins", "hpi"],
989
1775
  helm: ["helm", "charts"],
@@ -1014,17 +1800,19 @@ export const PROJECT_TYPE_ALIASES = {
1014
1800
  "visionos",
1015
1801
  ],
1016
1802
  binary: ["binary", "blint"],
1017
- oci: ["docker", "oci", "container", "podman"],
1803
+ oci: ["docker", "oci", "container", "podman", "rootfs", "oci-dir"],
1018
1804
  cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
1019
1805
  scala: ["scala", "scala3", "sbt", "mill"],
1020
1806
  nix: ["nix", "nixos", "flake"],
1021
1807
  caxa: ["caxa"],
1808
+ asar: ["asar", "electron", "electron-asar"],
1022
1809
  "vscode-extension": [
1023
1810
  "vscode-extension",
1024
1811
  "vsix",
1025
1812
  "vscode",
1026
1813
  "openvsx",
1027
1814
  "vscode-extensions",
1815
+ "ide-extension",
1028
1816
  "ide-extensions",
1029
1817
  ],
1030
1818
  "chrome-extension": [
@@ -1154,6 +1942,90 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
1154
1942
  return shouldInclude;
1155
1943
  }
1156
1944
 
1945
+ /**
1946
+ * Determine whether the predictive dependency audit should run for the current
1947
+ * CLI invocation.
1948
+ *
1949
+ * OBOM-focused runs (`obom` or explicit `-t os` / OS aliases only) should keep
1950
+ * the direct BOM audit findings but skip the predictive dependency audit.
1951
+ *
1952
+ * @param {object} options CLI options
1953
+ * @param {string} [commandPath] Invoked command path or name
1954
+ * @returns {boolean} True when predictive dependency audit should run
1955
+ */
1956
+ export function shouldRunPredictiveBomAudit(options, commandPath) {
1957
+ const normalizedCommandPath = `${commandPath || ""}`.toLowerCase();
1958
+ if (normalizedCommandPath.includes("obom")) {
1959
+ return false;
1960
+ }
1961
+ if (normalizedCommandPath.includes("hbom")) {
1962
+ return false;
1963
+ }
1964
+ const projectTypes = Array.isArray(options?.projectType)
1965
+ ? options.projectType
1966
+ : typeof options?.projectType === "string"
1967
+ ? options.projectType.split(",")
1968
+ : [];
1969
+ const normalizedProjectTypes = projectTypes
1970
+ .map((projectType) => `${projectType || ""}`.trim().toLowerCase())
1971
+ .filter(Boolean);
1972
+ if (!normalizedProjectTypes.length) {
1973
+ return true;
1974
+ }
1975
+ const hbomProjectTypes = new Set(["hbom", "hardware"]);
1976
+ if (
1977
+ normalizedProjectTypes.every((projectType) =>
1978
+ hbomProjectTypes.has(projectType),
1979
+ )
1980
+ ) {
1981
+ return false;
1982
+ }
1983
+ const osProjectTypes = new Set(["os", ...(PROJECT_TYPE_ALIASES.os || [])]);
1984
+ return !normalizedProjectTypes.every((projectType) =>
1985
+ osProjectTypes.has(projectType),
1986
+ );
1987
+ }
1988
+
1989
+ /**
1990
+ * Determine the default BOM audit categories for the current CLI invocation.
1991
+ *
1992
+ * OBOM-focused runs should default to the runtime-specific rule pack unless the
1993
+ * user explicitly requests other categories.
1994
+ *
1995
+ * @param {object} options CLI options
1996
+ * @param {string} [commandPath] Invoked command path or name
1997
+ * @returns {string | undefined} Default category string, if any
1998
+ */
1999
+ export function getDefaultBomAuditCategories(options, commandPath) {
2000
+ const normalizedCommandPath = `${commandPath || ""}`.toLowerCase();
2001
+ const defaultHbomCategories = options?.includeRuntime
2002
+ ? `${DEFAULT_HBOM_AUDIT_CATEGORIES},host-topology`
2003
+ : DEFAULT_HBOM_AUDIT_CATEGORIES;
2004
+ if (normalizedCommandPath.includes("hbom")) {
2005
+ return defaultHbomCategories;
2006
+ }
2007
+ const projectTypes = Array.isArray(options?.projectType)
2008
+ ? options.projectType
2009
+ : typeof options?.projectType === "string"
2010
+ ? options.projectType.split(",")
2011
+ : [];
2012
+ const normalizedProjectTypes = projectTypes
2013
+ .map((projectType) => `${projectType || ""}`.trim().toLowerCase())
2014
+ .filter(Boolean);
2015
+ if (
2016
+ normalizedProjectTypes.length &&
2017
+ normalizedProjectTypes.every((projectType) =>
2018
+ ["hbom", "hardware"].includes(projectType),
2019
+ )
2020
+ ) {
2021
+ return defaultHbomCategories;
2022
+ }
2023
+ if (!shouldRunPredictiveBomAudit(options, commandPath)) {
2024
+ return "obom-runtime";
2025
+ }
2026
+ return undefined;
2027
+ }
2028
+
1157
2029
  /**
1158
2030
  * Convenient method to check if the given package manager is allowed.
1159
2031
  *
@@ -1195,23 +2067,76 @@ function isCacheDisabled() {
1195
2067
  const cache = isCacheDisabled() ? undefined : gotHttpCache;
1196
2068
  export const remoteHostsAccessed = new Set();
1197
2069
 
1198
- function isAllowedHost(hostname) {
1199
- if (!process.env.CDXGEN_ALLOWED_HOSTS) {
2070
+ export function isAllowedHttpHost(
2071
+ hostname,
2072
+ allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS"),
2073
+ ) {
2074
+ if (!allowedHostsEnv) {
1200
2075
  return true;
1201
2076
  }
1202
- const allow_hosts = (process.env.CDXGEN_ALLOWED_HOSTS || "").split(",");
2077
+ if (!hostname || hasDangerousUnicode(hostname)) {
2078
+ return false;
2079
+ }
2080
+ const normalizedHostname = hostname.toLowerCase();
2081
+ const allow_hosts = allowedHostsEnv
2082
+ .split(",")
2083
+ .map((host) => host.trim().toLowerCase())
2084
+ .filter(Boolean);
1203
2085
  for (const ahost of allow_hosts) {
1204
- if (!ahost.length) {
1205
- continue;
1206
- }
1207
- if (hostname === ahost) {
2086
+ if (normalizedHostname === ahost) {
1208
2087
  return true;
1209
2088
  }
1210
2089
  // wildcard support
1211
- if (ahost.startsWith("*.") && hostname.endsWith(ahost.replace("*", ""))) {
2090
+ if (
2091
+ ahost.startsWith("*.") &&
2092
+ normalizedHostname.length > ahost.length - 1 &&
2093
+ normalizedHostname.endsWith(`.${ahost.slice(2)}`)
2094
+ ) {
1212
2095
  return true;
1213
2096
  }
1214
2097
  }
2098
+ return false;
2099
+ }
2100
+
2101
+ function hostnameMatches(hostname, candidateHost) {
2102
+ return hostname === candidateHost || hostname.endsWith(`.${candidateHost}`);
2103
+ }
2104
+
2105
+ function inferNetworkIntent(requestUrl) {
2106
+ const hostname = requestUrl?.hostname?.toLowerCase() || "";
2107
+ const pathname = requestUrl?.pathname?.toLowerCase() || "";
2108
+ if (pathname.includes("/api/v1/bom")) {
2109
+ return "sbom-submit";
2110
+ }
2111
+ if (pathname.includes("/manifests/")) {
2112
+ return "oci-manifest-access";
2113
+ }
2114
+ if (pathname.includes("/blobs/")) {
2115
+ return "oci-layer-access";
2116
+ }
2117
+ if (
2118
+ pathname.includes("license") ||
2119
+ hostnameMatches(hostname, "spdx.org") ||
2120
+ hostnameMatches(hostname, "opensource.org")
2121
+ ) {
2122
+ return "license-fetch";
2123
+ }
2124
+ if (
2125
+ hostnameMatches(hostname, "registry.npmjs.org") ||
2126
+ hostnameMatches(hostname, "pypi.org") ||
2127
+ hostnameMatches(hostname, "rubygems.org") ||
2128
+ hostnameMatches(hostname, "repo.maven.apache.org") ||
2129
+ hostnameMatches(hostname, "repo1.maven.org") ||
2130
+ hostnameMatches(hostname, "crates.io") ||
2131
+ hostnameMatches(hostname, "pub.dev") ||
2132
+ hostnameMatches(hostname, "nuget.org")
2133
+ ) {
2134
+ return "registry-lookup";
2135
+ }
2136
+ if (hostnameMatches(hostname, "github.com") && pathname.endsWith(".git")) {
2137
+ return "git-fetch";
2138
+ }
2139
+ return "metadata-fetch";
1215
2140
  }
1216
2141
 
1217
2142
  // Custom user-agent for cdxgen
@@ -1227,18 +2152,40 @@ export const cdxgenAgent = got.extend({
1227
2152
  hooks: {
1228
2153
  beforeRequest: [
1229
2154
  (options) => {
2155
+ const networkIntent =
2156
+ options.context?.activityIntent || inferNetworkIntent(options.url);
2157
+ const allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS");
2158
+ const hostAllowed = isAllowedHttpHost(
2159
+ options.url.hostname,
2160
+ allowedHostsEnv,
2161
+ );
1230
2162
  options.context = {
1231
2163
  ...options.context,
2164
+ activityIntent: networkIntent,
1232
2165
  activityTarget: options.url.toString(),
1233
2166
  };
2167
+ if (allowedHostsEnv) {
2168
+ recordPolicyActivity(options.url.hostname, {
2169
+ metadata: {
2170
+ allowed: hostAllowed,
2171
+ allowlist: allowedHostsEnv,
2172
+ networkIntent,
2173
+ policyType: "host-allowlist",
2174
+ },
2175
+ reason: `${hostAllowed ? "Allowed" : "Blocked"} host ${options.url.hostname} against CDXGEN_ALLOWED_HOSTS.`,
2176
+ status: hostAllowed ? "completed" : "blocked",
2177
+ traceDetail: "host-allowlist",
2178
+ });
2179
+ }
1234
2180
  if (isDryRun) {
1235
2181
  const error = createDryRunError(
1236
2182
  "network",
1237
2183
  options.url.toString(),
1238
- "Dry run mode blocks outbound network access.",
2184
+ `Dry run mode blocks outbound network access (${networkIntent}).`,
1239
2185
  );
1240
2186
  recordActivity({
1241
2187
  kind: "network",
2188
+ networkIntent,
1242
2189
  reason: error.message,
1243
2190
  status: "blocked",
1244
2191
  target: options.url.toString(),
@@ -1246,12 +2193,13 @@ export const cdxgenAgent = got.extend({
1246
2193
  options.context.activityBlocked = true;
1247
2194
  throw error;
1248
2195
  }
1249
- if (!isAllowedHost(options.url.hostname)) {
2196
+ if (!hostAllowed) {
1250
2197
  console.log(
1251
2198
  `Access to the remote host '${options.url.hostname}' is not permitted.`,
1252
2199
  );
1253
2200
  recordActivity({
1254
2201
  kind: "network",
2202
+ networkIntent,
1255
2203
  reason: "The remote host is not permitted.",
1256
2204
  status: "blocked",
1257
2205
  target: options.url.toString(),
@@ -1266,6 +2214,7 @@ export const cdxgenAgent = got.extend({
1266
2214
  );
1267
2215
  recordActivity({
1268
2216
  kind: "network",
2217
+ networkIntent,
1269
2218
  reason: `The '${options.url.protocol}' protocol is not permitted in secure mode.`,
1270
2219
  status: "blocked",
1271
2220
  target: options.url.toString(),
@@ -1292,6 +2241,7 @@ export const cdxgenAgent = got.extend({
1292
2241
  response.url;
1293
2242
  recordActivity({
1294
2243
  kind: "network",
2244
+ networkIntent: response.request.options.context?.activityIntent,
1295
2245
  status: "completed",
1296
2246
  target: activityTarget,
1297
2247
  });
@@ -1305,6 +2255,7 @@ export const cdxgenAgent = got.extend({
1305
2255
  }
1306
2256
  recordActivity({
1307
2257
  kind: "network",
2258
+ networkIntent: error.options?.context?.activityIntent,
1308
2259
  reason: error.message,
1309
2260
  status: "failed",
1310
2261
  target:
@@ -1388,6 +2339,10 @@ export function getAllFilesWithIgnore(
1388
2339
  includeDot,
1389
2340
  ignoreList,
1390
2341
  ) {
2342
+ const patternValue = Array.isArray(pattern)
2343
+ ? pattern.join(",")
2344
+ : String(pattern);
2345
+ const discoveryMetadata = classifyDiscoveryPattern(patternValue);
1391
2346
  try {
1392
2347
  const files = globSync(pattern, {
1393
2348
  cwd: dirPath,
@@ -1398,6 +2353,16 @@ export function getAllFilesWithIgnore(
1398
2353
  follow: false,
1399
2354
  ignore: ignoreList,
1400
2355
  });
2356
+ recordDiscoveryActivity(`${dirPath} :: ${patternValue}`, {
2357
+ metadata: {
2358
+ discoveryType: discoveryMetadata.discoveryType,
2359
+ matchedCount: files.length,
2360
+ pattern: patternValue,
2361
+ },
2362
+ traceDetail: `matched:${files.length}`,
2363
+ reasonBuilder: (count) =>
2364
+ `Scanned ${dirPath} with glob '${patternValue}' for ${discoveryMetadata.label}; matched ${files.length} path(s)${buildReadCountSuffix(count)}.`,
2365
+ });
1401
2366
  if (files.length > 1) {
1402
2367
  thoughtLog(
1403
2368
  `Found ${files.length} files for the pattern '${pattern}' at '${dirPath}'.`,
@@ -1405,6 +2370,14 @@ export function getAllFilesWithIgnore(
1405
2370
  }
1406
2371
  return files;
1407
2372
  } catch (err) {
2373
+ recordDiscoveryActivity(`${dirPath} :: ${patternValue}`, {
2374
+ metadata: {
2375
+ discoveryType: discoveryMetadata.discoveryType,
2376
+ pattern: patternValue,
2377
+ },
2378
+ reason: `File discovery failed for glob '${patternValue}' under ${dirPath}: ${err.message}`,
2379
+ status: "failed",
2380
+ });
1408
2381
  if (DEBUG_MODE) {
1409
2382
  console.error(err);
1410
2383
  }
@@ -2585,6 +3558,23 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
2585
3558
  value: "true",
2586
3559
  });
2587
3560
  }
3561
+ const npmManifestSources = collectNpmManifestSources(node);
3562
+ if (npmManifestSources.length) {
3563
+ addComponentProperty(
3564
+ pkg,
3565
+ "cdx:npm:manifestSourceType",
3566
+ npmManifestSources
3567
+ .map((manifestSource) => manifestSource.type)
3568
+ .join(","),
3569
+ );
3570
+ addComponentProperty(
3571
+ pkg,
3572
+ "cdx:npm:manifestSource",
3573
+ npmManifestSources
3574
+ .map((manifestSource) => manifestSource.value)
3575
+ .join(","),
3576
+ );
3577
+ }
2588
3578
  // This getter method could fail with errors at times.
2589
3579
  // Example Error: Invalid tag name "^>=6.0.0" of package "^>=6.0.0": Tags may not have any characters that encodeURIComponent encodes.
2590
3580
  try {
@@ -6811,8 +7801,17 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
6811
7801
  value: origName,
6812
7802
  });
6813
7803
  }
6814
- if (body.releases?.[p.version] && body.releases[p.version].length) {
6815
- const digest = body.releases[p.version][0].digests;
7804
+ const releaseEntries = body.releases?.[p.version]?.length
7805
+ ? body.releases[p.version]
7806
+ : Array.isArray(body.urls)
7807
+ ? body.urls
7808
+ : [];
7809
+ mergeExternalReferences(
7810
+ p,
7811
+ collectPypiReleaseExternalReferences(releaseEntries),
7812
+ );
7813
+ if (releaseEntries.length) {
7814
+ const digest = releaseEntries[0].digests;
6816
7815
  if (digest["sha256"]) {
6817
7816
  p._integrity = `sha256-${digest["sha256"]}`;
6818
7817
  } else if (digest["md5"]) {
@@ -7001,46 +8000,327 @@ export function parseBdistMetadata(mDataFile, rawMetadata = undefined) {
7001
8000
  break;
7002
8001
  }
7003
8002
  }
7004
- if (mDataFile) {
7005
- pkg.evidence = {
7006
- identity: {
7007
- field: "purl",
7008
- confidence: 0.5,
7009
- methods: [
7010
- {
7011
- technique: "manifest-analysis",
7012
- confidence: 0.5,
7013
- value: mDataFile,
7014
- },
7015
- ],
7016
- },
8003
+ if (mDataFile) {
8004
+ pkg.evidence = {
8005
+ identity: {
8006
+ field: "purl",
8007
+ confidence: 0.5,
8008
+ methods: [
8009
+ {
8010
+ technique: "manifest-analysis",
8011
+ confidence: 0.5,
8012
+ value: mDataFile,
8013
+ },
8014
+ ],
8015
+ },
8016
+ };
8017
+ }
8018
+ pkg.purl = `pkg:pypi/${pkg.name.toLowerCase().replaceAll("_", "-")}@${pkg.version}`;
8019
+ pkg["bom-ref"] = pkg.purl;
8020
+ return [pkg];
8021
+ }
8022
+
8023
+ /**
8024
+ * Method to parse pipfile.lock data
8025
+ *
8026
+ * @param {Object} lockData JSON data from Pipfile.lock
8027
+ */
8028
+ export async function parsePiplockData(lockData) {
8029
+ const pkgList = [];
8030
+ Object.keys(lockData)
8031
+ .filter((i) => i !== "_meta")
8032
+ .forEach((k) => {
8033
+ const depBlock = lockData[k];
8034
+ Object.keys(depBlock).forEach((p) => {
8035
+ const pkg = depBlock[p];
8036
+ if (Object.hasOwn(pkg, "version")) {
8037
+ const versionStr = pkg.version.replace("==", "");
8038
+ pkgList.push({ name: p, version: versionStr });
8039
+ }
8040
+ });
8041
+ });
8042
+ return await getPyMetadata(pkgList, false);
8043
+ }
8044
+
8045
+ function addComponentProperty(component, name, value) {
8046
+ if (value === undefined || value === null || value === "" || !component) {
8047
+ return;
8048
+ }
8049
+ component.properties = component.properties || [];
8050
+ if (
8051
+ component.properties.some(
8052
+ (property) => property.name === name && property.value === value,
8053
+ )
8054
+ ) {
8055
+ return;
8056
+ }
8057
+ component.properties.push({
8058
+ name,
8059
+ value,
8060
+ });
8061
+ }
8062
+
8063
+ const PYTHON_DIRECT_REFERENCE_PATTERN =
8064
+ /^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?\s*@\s*(\S+)$/;
8065
+
8066
+ function isWindowsAbsolutePath(value) {
8067
+ return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("\\\\");
8068
+ }
8069
+
8070
+ function createExternalReferenceKey(reference) {
8071
+ return JSON.stringify([
8072
+ reference.type,
8073
+ reference.url,
8074
+ reference.comment || "",
8075
+ ]);
8076
+ }
8077
+
8078
+ function classifyNpmManifestSource(spec) {
8079
+ if (typeof spec !== "string" || !spec.trim()) {
8080
+ return undefined;
8081
+ }
8082
+ const normalizedSpec = spec.trim();
8083
+ const lowerSpec = normalizedSpec.toLowerCase();
8084
+ if (
8085
+ lowerSpec.startsWith("git+") ||
8086
+ lowerSpec.startsWith("git://") ||
8087
+ lowerSpec.startsWith("github:") ||
8088
+ lowerSpec.startsWith("gitlab:") ||
8089
+ lowerSpec.startsWith("bitbucket:") ||
8090
+ lowerSpec.startsWith("gist:")
8091
+ ) {
8092
+ return {
8093
+ type: "git",
8094
+ value: normalizedSpec,
8095
+ };
8096
+ }
8097
+ if (lowerSpec.startsWith("http://") || lowerSpec.startsWith("https://")) {
8098
+ return {
8099
+ type: "url",
8100
+ value: normalizedSpec,
8101
+ };
8102
+ }
8103
+ if (
8104
+ lowerSpec.startsWith("file:") ||
8105
+ lowerSpec.startsWith("link:") ||
8106
+ lowerSpec.startsWith("workspace:") ||
8107
+ normalizedSpec.startsWith("./") ||
8108
+ normalizedSpec.startsWith("../") ||
8109
+ normalizedSpec.startsWith("/") ||
8110
+ isWindowsAbsolutePath(normalizedSpec)
8111
+ ) {
8112
+ return {
8113
+ type: "path",
8114
+ value: normalizedSpec,
8115
+ };
8116
+ }
8117
+ return undefined;
8118
+ }
8119
+
8120
+ function collectNpmManifestSources(node) {
8121
+ const manifestSources = [];
8122
+ const seen = new Set();
8123
+ if (!node?.edgesIn) {
8124
+ return manifestSources;
8125
+ }
8126
+ for (const edge of node.edgesIn) {
8127
+ const manifestSource = classifyNpmManifestSource(edge?.spec);
8128
+ if (!manifestSource) {
8129
+ continue;
8130
+ }
8131
+ const dedupeKey = `${manifestSource.type}|${manifestSource.value}`;
8132
+ if (seen.has(dedupeKey)) {
8133
+ continue;
8134
+ }
8135
+ seen.add(dedupeKey);
8136
+ manifestSources.push(manifestSource);
8137
+ }
8138
+ return manifestSources;
8139
+ }
8140
+
8141
+ function normalizePythonDependencyKey(value) {
8142
+ if (typeof value !== "string" || !value.trim()) {
8143
+ return undefined;
8144
+ }
8145
+ return value.trim().toLowerCase().replaceAll("_", "-");
8146
+ }
8147
+
8148
+ function extractPythonDependencyKey(value) {
8149
+ const manifestSource = parsePyProjectDependencySourceString(value);
8150
+ if (manifestSource?.name) {
8151
+ return normalizePythonDependencyKey(manifestSource.name);
8152
+ }
8153
+ const packageMatch =
8154
+ typeof value === "string"
8155
+ ? value.trim().match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?/)
8156
+ : undefined;
8157
+ return normalizePythonDependencyKey(packageMatch?.[1]);
8158
+ }
8159
+ function classifyPythonManifestSourceValue(value) {
8160
+ if (typeof value !== "string" || !value.trim()) {
8161
+ return undefined;
8162
+ }
8163
+ const normalizedValue = value.trim();
8164
+ const lowerValue = normalizedValue.toLowerCase();
8165
+ if (
8166
+ lowerValue.startsWith("git+") ||
8167
+ lowerValue.startsWith("git://") ||
8168
+ lowerValue.startsWith("git@") ||
8169
+ lowerValue.startsWith("ssh://git@")
8170
+ ) {
8171
+ return {
8172
+ type: "git",
8173
+ value: normalizedValue,
8174
+ };
8175
+ }
8176
+ if (
8177
+ lowerValue.startsWith("http://") ||
8178
+ lowerValue.startsWith("https://") ||
8179
+ lowerValue.startsWith("ftp://")
8180
+ ) {
8181
+ return {
8182
+ type: "url",
8183
+ value: normalizedValue,
8184
+ };
8185
+ }
8186
+ if (
8187
+ lowerValue.startsWith("file:") ||
8188
+ normalizedValue.startsWith("./") ||
8189
+ normalizedValue.startsWith("../") ||
8190
+ normalizedValue.startsWith("/") ||
8191
+ isWindowsAbsolutePath(normalizedValue)
8192
+ ) {
8193
+ return {
8194
+ type: "path",
8195
+ value: normalizedValue,
8196
+ };
8197
+ }
8198
+ return undefined;
8199
+ }
8200
+
8201
+ function applyManifestSourceProperties(
8202
+ component,
8203
+ propertyPrefix,
8204
+ manifestSource,
8205
+ ) {
8206
+ if (!manifestSource?.type || !manifestSource?.value) {
8207
+ return;
8208
+ }
8209
+ addComponentProperty(
8210
+ component,
8211
+ `${propertyPrefix}:manifestSourceType`,
8212
+ manifestSource.type,
8213
+ );
8214
+ addComponentProperty(
8215
+ component,
8216
+ `${propertyPrefix}:manifestSource`,
8217
+ manifestSource.value,
8218
+ );
8219
+ }
8220
+
8221
+ function recordPythonDependencySource(
8222
+ dependencySourceMap,
8223
+ dependencyName,
8224
+ sourceType,
8225
+ sourceValue,
8226
+ ) {
8227
+ const normalizedKey = normalizePythonDependencyKey(dependencyName);
8228
+ if (!normalizedKey || !sourceType || !sourceValue) {
8229
+ return;
8230
+ }
8231
+ dependencySourceMap[normalizedKey] = {
8232
+ type: sourceType,
8233
+ value: sourceValue,
8234
+ };
8235
+ }
8236
+
8237
+ function parsePyProjectDependencySourceString(value) {
8238
+ if (typeof value !== "string" || !value.includes("@")) {
8239
+ return undefined;
8240
+ }
8241
+ const directReferenceMatch = value
8242
+ .trim()
8243
+ .match(PYTHON_DIRECT_REFERENCE_PATTERN);
8244
+ if (!directReferenceMatch) {
8245
+ return undefined;
8246
+ }
8247
+ const manifestSource = classifyPythonManifestSourceValue(
8248
+ directReferenceMatch[2],
8249
+ );
8250
+ if (!manifestSource) {
8251
+ return undefined;
8252
+ }
8253
+ return {
8254
+ name: directReferenceMatch[1],
8255
+ ...manifestSource,
8256
+ };
8257
+ }
8258
+
8259
+ function collectPythonManifestSource(pkg) {
8260
+ const sourceCandidates = [
8261
+ { kind: "git", value: pkg?.source?.git },
8262
+ { kind: "git", value: pkg?.vcs?.git },
8263
+ { kind: "url", value: pkg?.vcs?.url },
8264
+ { kind: "url", value: pkg?.source?.url },
8265
+ { kind: "path", value: pkg?.source?.path },
8266
+ { kind: "path", value: pkg?.source?.editable },
8267
+ { kind: "path", value: pkg?.source?.virtual },
8268
+ ];
8269
+ for (const candidate of sourceCandidates) {
8270
+ if (typeof candidate.value !== "string" || !candidate.value.trim()) {
8271
+ continue;
8272
+ }
8273
+ const normalizedValue = candidate.value.trim();
8274
+ if (candidate.kind === "git") {
8275
+ return {
8276
+ type: "git",
8277
+ value: normalizedValue.startsWith("git+")
8278
+ ? normalizedValue
8279
+ : `git+${normalizedValue}`,
8280
+ };
8281
+ }
8282
+ const manifestSource = classifyPythonManifestSourceValue(normalizedValue);
8283
+ if (manifestSource) {
8284
+ return manifestSource;
8285
+ }
8286
+ return {
8287
+ type: candidate.kind,
8288
+ value: normalizedValue,
7017
8289
  };
7018
8290
  }
7019
- pkg.purl = `pkg:pypi/${pkg.name.toLowerCase().replaceAll("_", "-")}@${pkg.version}`;
7020
- pkg["bom-ref"] = pkg.purl;
7021
- return [pkg];
8291
+ return undefined;
7022
8292
  }
7023
8293
 
7024
- /**
7025
- * Method to parse pipfile.lock data
7026
- *
7027
- * @param {Object} lockData JSON data from Pipfile.lock
7028
- */
7029
- export async function parsePiplockData(lockData) {
7030
- const pkgList = [];
7031
- Object.keys(lockData)
7032
- .filter((i) => i !== "_meta")
7033
- .forEach((k) => {
7034
- const depBlock = lockData[k];
7035
- Object.keys(depBlock).forEach((p) => {
7036
- const pkg = depBlock[p];
7037
- if (Object.hasOwn(pkg, "version")) {
7038
- const versionStr = pkg.version.replace("==", "");
7039
- pkgList.push({ name: p, version: versionStr });
7040
- }
7041
- });
7042
- });
7043
- return await getPyMetadata(pkgList, false);
8294
+ function parsePythonRequirementManifestSource(value) {
8295
+ if (typeof value !== "string" || !value.trim()) {
8296
+ return undefined;
8297
+ }
8298
+ const normalizedValue = value.trim();
8299
+ const directReferenceMatch = normalizedValue.match(
8300
+ PYTHON_DIRECT_REFERENCE_PATTERN,
8301
+ );
8302
+ if (directReferenceMatch) {
8303
+ const manifestSource = classifyPythonManifestSourceValue(
8304
+ directReferenceMatch[2],
8305
+ );
8306
+ if (manifestSource) {
8307
+ return {
8308
+ name: directReferenceMatch[1],
8309
+ ...manifestSource,
8310
+ };
8311
+ }
8312
+ }
8313
+ const vcsRequirementMatch = normalizedValue.match(
8314
+ /^(git\+\S+?)(?:#.*egg=([A-Za-z0-9_.-]+))?$/,
8315
+ );
8316
+ if (vcsRequirementMatch?.[2]) {
8317
+ return {
8318
+ name: vcsRequirementMatch[2],
8319
+ type: "git",
8320
+ value: vcsRequirementMatch[1],
8321
+ };
8322
+ }
8323
+ return undefined;
7044
8324
  }
7045
8325
 
7046
8326
  /**
@@ -7102,6 +8382,7 @@ export function parsePyProjectTomlFile(tomlFile) {
7102
8382
  let tomlData;
7103
8383
  const directDepsKeys = {};
7104
8384
  const groupDepsKeys = {};
8385
+ const dependencySourceMap = {};
7105
8386
  try {
7106
8387
  tomlData = toml.parse(readFileSync(tomlFile, { encoding: "utf-8" }));
7107
8388
  } catch (err) {
@@ -7173,8 +8454,19 @@ export function parsePyProjectTomlFile(tomlFile) {
7173
8454
  }
7174
8455
  if (tomlData?.project?.dependencies) {
7175
8456
  for (const adep of tomlData.project.dependencies) {
7176
- // Example: bcrypt>=4.2.0
7177
- directDepsKeys[adep.split(/[\s<>=]/)[0]] = true;
8457
+ const dependencyKey = extractPythonDependencyKey(adep);
8458
+ if (dependencyKey) {
8459
+ directDepsKeys[dependencyKey] = true;
8460
+ }
8461
+ const manifestSource = parsePyProjectDependencySourceString(adep);
8462
+ if (manifestSource) {
8463
+ recordPythonDependencySource(
8464
+ dependencySourceMap,
8465
+ manifestSource.name,
8466
+ manifestSource.type,
8467
+ manifestSource.value,
8468
+ );
8469
+ }
7178
8470
  }
7179
8471
  }
7180
8472
  if (tomlData["dependency-groups"]) {
@@ -7186,6 +8478,15 @@ export function parsePyProjectTomlFile(tomlFile) {
7186
8478
  groupDepsKeys[pname] = [];
7187
8479
  }
7188
8480
  groupDepsKeys[pname].push(agroup);
8481
+ const manifestSource = parsePyProjectDependencySourceString(p);
8482
+ if (manifestSource) {
8483
+ recordPythonDependencySource(
8484
+ dependencySourceMap,
8485
+ manifestSource.name,
8486
+ manifestSource.type,
8487
+ manifestSource.value,
8488
+ );
8489
+ }
7189
8490
  } else {
7190
8491
  return;
7191
8492
  }
@@ -7206,6 +8507,29 @@ export function parsePyProjectTomlFile(tomlFile) {
7206
8507
  ].includes(adep)
7207
8508
  ) {
7208
8509
  directDepsKeys[adep] = true;
8510
+ const poetryDependency = tomlData.tool.poetry.dependencies[adep];
8511
+ if (poetryDependency?.git) {
8512
+ recordPythonDependencySource(
8513
+ dependencySourceMap,
8514
+ adep,
8515
+ "git",
8516
+ poetryDependency.git,
8517
+ );
8518
+ } else if (poetryDependency?.url) {
8519
+ recordPythonDependencySource(
8520
+ dependencySourceMap,
8521
+ adep,
8522
+ "url",
8523
+ poetryDependency.url,
8524
+ );
8525
+ } else if (poetryDependency?.path) {
8526
+ recordPythonDependencySource(
8527
+ dependencySourceMap,
8528
+ adep,
8529
+ "path",
8530
+ poetryDependency.path,
8531
+ );
8532
+ }
7209
8533
  }
7210
8534
  } // for
7211
8535
  if (tomlData?.tool?.poetry?.group) {
@@ -7217,10 +8541,63 @@ export function parsePyProjectTomlFile(tomlFile) {
7217
8541
  groupDepsKeys[adep] = [];
7218
8542
  }
7219
8543
  groupDepsKeys[adep].push(agroup);
8544
+ const poetryDependency =
8545
+ tomlData.tool.poetry.group[agroup]?.dependencies?.[adep];
8546
+ if (poetryDependency?.git) {
8547
+ recordPythonDependencySource(
8548
+ dependencySourceMap,
8549
+ adep,
8550
+ "git",
8551
+ poetryDependency.git,
8552
+ );
8553
+ } else if (poetryDependency?.url) {
8554
+ recordPythonDependencySource(
8555
+ dependencySourceMap,
8556
+ adep,
8557
+ "url",
8558
+ poetryDependency.url,
8559
+ );
8560
+ } else if (poetryDependency?.path) {
8561
+ recordPythonDependencySource(
8562
+ dependencySourceMap,
8563
+ adep,
8564
+ "path",
8565
+ poetryDependency.path,
8566
+ );
8567
+ }
7220
8568
  }
7221
8569
  } // for
7222
8570
  }
7223
8571
  }
8572
+ if (tomlData?.tool?.uv?.sources) {
8573
+ for (const adep of Object.keys(tomlData.tool.uv.sources)) {
8574
+ const uvSource = Array.isArray(tomlData.tool.uv.sources[adep])
8575
+ ? tomlData.tool.uv.sources[adep][0]
8576
+ : tomlData.tool.uv.sources[adep];
8577
+ if (uvSource?.git) {
8578
+ recordPythonDependencySource(
8579
+ dependencySourceMap,
8580
+ adep,
8581
+ "git",
8582
+ uvSource.git,
8583
+ );
8584
+ } else if (uvSource?.url) {
8585
+ recordPythonDependencySource(
8586
+ dependencySourceMap,
8587
+ adep,
8588
+ "url",
8589
+ uvSource.url,
8590
+ );
8591
+ } else if (uvSource?.path) {
8592
+ recordPythonDependencySource(
8593
+ dependencySourceMap,
8594
+ adep,
8595
+ "path",
8596
+ uvSource.path,
8597
+ );
8598
+ }
8599
+ }
8600
+ }
7224
8601
  return {
7225
8602
  parentComponent: pkg,
7226
8603
  poetryMode,
@@ -7229,9 +8606,146 @@ export function parsePyProjectTomlFile(tomlFile) {
7229
8606
  workspacePaths,
7230
8607
  directDepsKeys,
7231
8608
  groupDepsKeys,
8609
+ dependencySourceMap,
7232
8610
  };
7233
8611
  }
7234
8612
 
8613
+ function collectPythonLockDistributionReferences(pkg) {
8614
+ const externalReferences = [];
8615
+ const seen = new Set();
8616
+
8617
+ function addExternalReference(type, url, comment) {
8618
+ if (typeof url !== "string" || !url.trim()) {
8619
+ return;
8620
+ }
8621
+ const normalizedUrl = url.trim();
8622
+ const reference = {
8623
+ type,
8624
+ url: normalizedUrl,
8625
+ comment,
8626
+ };
8627
+ const referenceKey = createExternalReferenceKey(reference);
8628
+ if (seen.has(referenceKey)) {
8629
+ return;
8630
+ }
8631
+ seen.add(referenceKey);
8632
+ externalReferences.push(reference);
8633
+ }
8634
+
8635
+ addExternalReference("distribution", pkg?.archive?.url, "archive");
8636
+ addExternalReference("distribution", pkg?.sdist?.url, "sdist");
8637
+ if (Array.isArray(pkg?.wheels)) {
8638
+ for (const wheel of pkg.wheels) {
8639
+ addExternalReference(
8640
+ "distribution",
8641
+ wheel?.url,
8642
+ wheel?.file || wheel?.name || wheel?.filename || "wheel",
8643
+ );
8644
+ }
8645
+ }
8646
+ const vcsSource = [
8647
+ { kind: "url", value: pkg?.vcs?.url },
8648
+ { kind: "git", value: pkg?.vcs?.git },
8649
+ { kind: "git", value: pkg?.source?.git },
8650
+ ].find(
8651
+ (entry) => typeof entry.value === "string" && entry.value.trim().length > 0,
8652
+ );
8653
+ if (vcsSource) {
8654
+ const vcsUrl = vcsSource.value.trim();
8655
+ const normalizedVcsUrl =
8656
+ vcsSource.kind === "git" && !vcsUrl.startsWith("git+")
8657
+ ? `git+${vcsUrl}`
8658
+ : vcsUrl;
8659
+ addExternalReference("vcs", normalizedVcsUrl, "vcs");
8660
+ }
8661
+ if (pkg?.source?.url) {
8662
+ const manifestSource = classifyPythonManifestSourceValue(pkg.source.url);
8663
+ addExternalReference(
8664
+ manifestSource?.type === "git" ? "vcs" : "distribution",
8665
+ pkg.source.url,
8666
+ "source",
8667
+ );
8668
+ }
8669
+ return externalReferences;
8670
+ }
8671
+
8672
+ function collectPythonLockMetadataFileEntries(lockTomlObj, pkg) {
8673
+ if (!lockTomlObj?.metadata?.files || !pkg?.name) {
8674
+ return [];
8675
+ }
8676
+ const expectedKeys = new Set([normalizePythonDependencyKey(pkg.name)]);
8677
+ if (pkg.version) {
8678
+ expectedKeys.add(
8679
+ `${normalizePythonDependencyKey(pkg.name)} ${`${pkg.version}`.trim().toLowerCase()}`,
8680
+ );
8681
+ }
8682
+ const matchingEntries = [];
8683
+ for (const [entryKey, entryValues] of Object.entries(
8684
+ lockTomlObj.metadata.files,
8685
+ )) {
8686
+ if (!Array.isArray(entryValues)) {
8687
+ continue;
8688
+ }
8689
+ if (expectedKeys.has(normalizePythonDependencyKey(entryKey))) {
8690
+ matchingEntries.push(...entryValues);
8691
+ }
8692
+ }
8693
+ return matchingEntries;
8694
+ }
8695
+
8696
+ function mergeExternalReferences(component, references) {
8697
+ if (!references?.length) {
8698
+ return;
8699
+ }
8700
+ const existingReferences = component.externalReferences || [];
8701
+ const seen = new Set(
8702
+ existingReferences.map((reference) =>
8703
+ createExternalReferenceKey(reference),
8704
+ ),
8705
+ );
8706
+ for (const reference of references) {
8707
+ const dedupeKey = createExternalReferenceKey(reference);
8708
+ if (seen.has(dedupeKey)) {
8709
+ continue;
8710
+ }
8711
+ seen.add(dedupeKey);
8712
+ existingReferences.push(reference);
8713
+ }
8714
+ if (existingReferences.length) {
8715
+ component.externalReferences = existingReferences;
8716
+ }
8717
+ }
8718
+
8719
+ function collectPythonLockMetadataDistributionReferences(fileEntries) {
8720
+ const distributionReferences = [];
8721
+ for (const fileEntry of fileEntries || []) {
8722
+ if (typeof fileEntry?.url !== "string" || !fileEntry.url.trim()) {
8723
+ continue;
8724
+ }
8725
+ distributionReferences.push({
8726
+ type: "distribution",
8727
+ url: fileEntry.url.trim(),
8728
+ comment: fileEntry.file,
8729
+ });
8730
+ }
8731
+ return distributionReferences;
8732
+ }
8733
+
8734
+ function collectPypiReleaseExternalReferences(releaseEntries) {
8735
+ const externalReferences = [];
8736
+ for (const releaseEntry of releaseEntries || []) {
8737
+ if (typeof releaseEntry?.url !== "string" || !releaseEntry.url.trim()) {
8738
+ continue;
8739
+ }
8740
+ externalReferences.push({
8741
+ type: "distribution",
8742
+ url: releaseEntry.url.trim(),
8743
+ comment: releaseEntry.filename || releaseEntry.packagetype,
8744
+ });
8745
+ }
8746
+ return externalReferences;
8747
+ }
8748
+
7235
8749
  /**
7236
8750
  * Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
7237
8751
  *
@@ -7248,6 +8762,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7248
8762
  const pkgBomRefMap = {};
7249
8763
  let directDepsKeys = {};
7250
8764
  let groupDepsKeys = {};
8765
+ let dependencySourceMap = {};
7251
8766
  let parentComponent;
7252
8767
  let workspacePaths;
7253
8768
  let workspaceWarningShown = false;
@@ -7255,6 +8770,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7255
8770
  let pyLockProperties = [];
7256
8771
  // Keep track of any workspace components to be added to the parent component
7257
8772
  const workspaceComponentMap = {};
8773
+ const workspaceDependencySourceMap = {};
7258
8774
  const workspacePyProjMap = {};
7259
8775
  const workspaceRefPyProjMap = {};
7260
8776
  const pkgParentMap = {};
@@ -7274,6 +8790,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7274
8790
  const pyProjMap = parsePyProjectTomlFile(pyProjectFile);
7275
8791
  directDepsKeys = pyProjMap.directDepsKeys || {};
7276
8792
  groupDepsKeys = pyProjMap.groupDepsKeys || {};
8793
+ dependencySourceMap = pyProjMap.dependencySourceMap || {};
7277
8794
  parentComponent = pyProjMap.parentComponent;
7278
8795
  workspacePaths = pyProjMap.workspacePaths;
7279
8796
  if (workspacePaths?.length) {
@@ -7337,6 +8854,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7337
8854
  }
7338
8855
  }
7339
8856
  const wparentComponentRef = wcompMap.parentComponent["bom-ref"];
8857
+ if (wcompMap?.dependencySourceMap) {
8858
+ Object.assign(
8859
+ workspaceDependencySourceMap,
8860
+ wcompMap.dependencySourceMap,
8861
+ );
8862
+ }
7340
8863
  // Track the parents of workspace direct dependencies
7341
8864
  if (wcompMap?.directDepsKeys) {
7342
8865
  for (const wdd of Object.keys(wcompMap?.directDepsKeys)) {
@@ -7408,6 +8931,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7408
8931
  value: workspacePyProjMap[apkg.name] || pyProjectFile,
7409
8932
  });
7410
8933
  }
8934
+ const manifestSource =
8935
+ dependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
8936
+ workspaceDependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
8937
+ collectPythonManifestSource(apkg);
8938
+ applyManifestSourceProperties(pkg, "cdx:pypi", manifestSource);
7411
8939
  if (apkg.optional) {
7412
8940
  pkg.scope = "optional";
7413
8941
  }
@@ -7449,6 +8977,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7449
8977
  });
7450
8978
  }
7451
8979
  }
8980
+ mergeExternalReferences(pkg, collectPythonLockDistributionReferences(apkg));
7452
8981
  if (pyLockMode) {
7453
8982
  pkg.properties = pkg.properties.concat(
7454
8983
  collectPyLockPackageProperties(apkg),
@@ -7511,9 +9040,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7511
9040
  }
7512
9041
  }
7513
9042
  }
7514
- if (lockTomlObj?.metadata?.files?.[pkg.name]?.length) {
9043
+ const metadataFileEntries = collectPythonLockMetadataFileEntries(
9044
+ lockTomlObj,
9045
+ pkg,
9046
+ );
9047
+ mergeExternalReferences(
9048
+ pkg,
9049
+ collectPythonLockMetadataDistributionReferences(metadataFileEntries),
9050
+ );
9051
+ if (metadataFileEntries.length) {
7515
9052
  pkg.components = [];
7516
- for (const afileObj of lockTomlObj.metadata.files[pkg.name]) {
9053
+ for (const afileObj of metadataFileEntries) {
7517
9054
  const hashParts = afileObj?.hash?.split(":");
7518
9055
  let hashes;
7519
9056
  if (hashParts?.length === 2) {
@@ -7547,8 +9084,9 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7547
9084
  pkg.components = (pkg.components || []).concat(pylockFileComponents);
7548
9085
  }
7549
9086
  }
9087
+ const normalizedPkgName = normalizePythonDependencyKey(pkg.name);
7550
9088
  if (
7551
- directDepsKeys[pkg.name] ||
9089
+ directDepsKeys[normalizedPkgName] ||
7552
9090
  (hasWorkspaces && !Object.keys(workspaceComponentMap).length)
7553
9091
  ) {
7554
9092
  rootList.push(pkg);
@@ -7743,6 +9281,23 @@ export async function parseReqFile(reqFile, fetchDepsInfo = false) {
7743
9281
  const LICENSE_ID_COMMENTS_PATTERN =
7744
9282
  /^(Apache-2\.0|MIT|ISC|GPL-|LGPL-|BSD-[23]-Clause)/i;
7745
9283
 
9284
+ function parseLicenseComment(comment) {
9285
+ if (!comment) {
9286
+ return undefined;
9287
+ }
9288
+ const licenses = comment
9289
+ .split("/")
9290
+ .map((value) => {
9291
+ const licenseId = value.trim();
9292
+ if (!licenseId.match(LICENSE_ID_COMMENTS_PATTERN)) {
9293
+ return undefined;
9294
+ }
9295
+ return { license: { id: licenseId } };
9296
+ })
9297
+ .filter((value) => value !== undefined);
9298
+ return licenses.length ? licenses : undefined;
9299
+ }
9300
+
7746
9301
  /**
7747
9302
  * Method to parse requirements.txt file. Must only be used internally.
7748
9303
  *
@@ -7780,11 +9335,16 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7780
9335
  const lines = normalizedData.split("\n");
7781
9336
  for (const line of lines) {
7782
9337
  let l = line.trim();
9338
+ let editableRequirement = false;
7783
9339
  if (l.includes("# Basic requirements")) {
7784
9340
  compScope = "required";
7785
9341
  } else if (l.includes("added by pip freeze")) {
7786
9342
  compScope = undefined;
7787
9343
  }
9344
+ if (l.startsWith("-e ") || l.startsWith("--editable ")) {
9345
+ editableRequirement = true;
9346
+ l = l.replace(/^--editable\s+|^-e\s+/, "").trim();
9347
+ }
7788
9348
  if (l.startsWith("Skipping line") || l.startsWith("(add")) {
7789
9349
  continue;
7790
9350
  }
@@ -7833,10 +9393,49 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7833
9393
  markers = parts.slice(1).join(";").trim();
7834
9394
  structuredMarkers = parseReqEnvMarkers(markers);
7835
9395
  }
9396
+ const requirementManifestSource = parsePythonRequirementManifestSource(l);
9397
+ if (requirementManifestSource?.name) {
9398
+ const apkg = {
9399
+ name: requirementManifestSource.name,
9400
+ version: null,
9401
+ scope: compScope,
9402
+ evidence,
9403
+ };
9404
+ if (hashes.length > 0) {
9405
+ apkg.hashes = hashes;
9406
+ }
9407
+ const licenses = parseLicenseComment(comment);
9408
+ if (licenses) {
9409
+ apkg.licenses = licenses;
9410
+ }
9411
+ applyManifestSourceProperties(
9412
+ apkg,
9413
+ "cdx:pypi",
9414
+ requirementManifestSource,
9415
+ );
9416
+ if (editableRequirement) {
9417
+ addComponentProperty(apkg, "cdx:pypi:editable", "true");
9418
+ }
9419
+ if (markers) {
9420
+ addComponentProperty(apkg, "cdx:pip:markers", markers);
9421
+ if (structuredMarkers?.length > 0) {
9422
+ addComponentProperty(
9423
+ apkg,
9424
+ "cdx:pip:structuredMarkers",
9425
+ JSON.stringify(structuredMarkers),
9426
+ );
9427
+ }
9428
+ }
9429
+ if (reqFile) {
9430
+ addComponentProperty(apkg, "SrcFile", reqFile);
9431
+ }
9432
+ pkgList.push(apkg);
9433
+ continue;
9434
+ }
7836
9435
 
7837
9436
  // Handle extras (e.g., package[extra1,extra2])
7838
9437
  let extras = null;
7839
- const extrasMatch = l.match(/^([a-zA-Z0-9_\-\.]+)(\[([^\]]+)\])?(.*)$/);
9438
+ const extrasMatch = l.match(/^([a-zA-Z0-9_\-.]+)(\[([^\]]+)])?(.*)$/);
7840
9439
  if (extrasMatch) {
7841
9440
  const [, packageName, , extrasStr, versionSpecifiers] = extrasMatch;
7842
9441
  const name = packageName;
@@ -7866,20 +9465,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7866
9465
  if (hashes.length > 0) {
7867
9466
  apkg.hashes = hashes;
7868
9467
  }
7869
- if (comment) {
7870
- apkg.licenses = comment
7871
- .split("/")
7872
- .map((c) => {
7873
- const licenseObj = {};
7874
- const cId = c.trim();
7875
- if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
7876
- licenseObj.id = cId;
7877
- } else {
7878
- return undefined;
7879
- }
7880
- return { license: licenseObj };
7881
- })
7882
- .filter((l) => l !== undefined);
9468
+ const licenses = parseLicenseComment(comment);
9469
+ if (licenses) {
9470
+ apkg.licenses = licenses;
7883
9471
  }
7884
9472
  if (extras && extras.length > 0) {
7885
9473
  properties.push({
@@ -7905,12 +9493,18 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7905
9493
  });
7906
9494
  }
7907
9495
  }
9496
+ if (editableRequirement) {
9497
+ properties.push({
9498
+ name: "cdx:pypi:editable",
9499
+ value: "true",
9500
+ });
9501
+ }
7908
9502
  if (properties.length) {
7909
9503
  apkg.properties = properties;
7910
9504
  }
7911
9505
  pkgList.push(apkg);
7912
9506
  } else {
7913
- const match = l.match(/^([a-zA-Z0-9_\-\.]+)(.*)$/);
9507
+ const match = l.match(/^([a-zA-Z0-9_\-.]+)(.*)$/);
7914
9508
  if (!match) {
7915
9509
  continue;
7916
9510
  }
@@ -7932,20 +9526,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7932
9526
  scope: compScope,
7933
9527
  evidence,
7934
9528
  };
7935
- if (comment) {
7936
- apkg.licenses = comment
7937
- .split("/")
7938
- .map((c) => {
7939
- const licenseObj = {};
7940
- const cId = c.trim();
7941
- if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
7942
- licenseObj.id = cId;
7943
- } else {
7944
- return undefined;
7945
- }
7946
- return { license: licenseObj };
7947
- })
7948
- .filter((l) => l !== undefined);
9529
+ const licenses = parseLicenseComment(comment);
9530
+ if (licenses) {
9531
+ apkg.licenses = licenses;
7949
9532
  }
7950
9533
  if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
7951
9534
  properties.push({
@@ -7965,6 +9548,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7965
9548
  });
7966
9549
  }
7967
9550
  }
9551
+ if (editableRequirement) {
9552
+ properties.push({
9553
+ name: "cdx:pypi:editable",
9554
+ value: "true",
9555
+ });
9556
+ }
7968
9557
  if (properties.length) {
7969
9558
  apkg.properties = properties;
7970
9559
  }
@@ -12621,48 +14210,230 @@ export function parseConanLockData(conanLockData) {
12621
14210
  }
12622
14211
  }
12623
14212
  }
12624
- return { pkgList, dependencies, parentComponentDependencies };
14213
+ return { pkgList, dependencies, parentComponentDependencies };
14214
+ }
14215
+
14216
+ /**
14217
+ * Parse a Conan conanfile.txt and extract required and optional packages.
14218
+ *
14219
+ * @param {string} conanData Raw text contents of a conanfile.txt
14220
+ * @returns {Object[]} Array of package objects with purl, name, version, and scope
14221
+ */
14222
+ export function parseConanData(conanData) {
14223
+ const pkgList = [];
14224
+ if (!conanData) {
14225
+ return pkgList;
14226
+ }
14227
+ let scope = "required";
14228
+ conanData.split("\n").forEach((l) => {
14229
+ l = l.replace("\r", "");
14230
+ if (l.includes("[build_requires]")) {
14231
+ scope = "optional";
14232
+ }
14233
+ if (l.includes("[requires]")) {
14234
+ scope = "required";
14235
+ }
14236
+
14237
+ // The line must start with sequence non-whitespace characters, followed by a slash,
14238
+ // followed by at least one more non-whitespace character.
14239
+ // Provides a heuristic for locating Conan package references inside conanfile.txt files.
14240
+ if (l.match(/^[^\s\/]+\/\S+/)) {
14241
+ const [purl, name, version] =
14242
+ mapConanPkgRefToPurlStringAndNameAndVersion(l);
14243
+ if (purl !== null) {
14244
+ pkgList.push({
14245
+ name,
14246
+ version,
14247
+ purl,
14248
+ "bom-ref": decodeURIComponent(purl),
14249
+ scope,
14250
+ });
14251
+ }
14252
+ }
14253
+ });
14254
+ return pkgList;
14255
+ }
14256
+
14257
+ /**
14258
+ * Construct a generic package component object for collider-managed packages.
14259
+ *
14260
+ * @param {string} name Package name
14261
+ * @param {Object} pkgData Locked package entry from collider.lock
14262
+ * @param {string} lockFile Source lock file path
14263
+ * @param {string} dependencyKind Whether the package is direct or transitive
14264
+ * @returns {Object|undefined} Package component
14265
+ */
14266
+ function buildColliderComponent(name, pkgData, lockFile, dependencyKind) {
14267
+ if (!name) {
14268
+ return undefined;
14269
+ }
14270
+ pkgData = pkgData || {};
14271
+ dependencyKind = dependencyKind || "transitive";
14272
+ const version = pkgData?.version || "";
14273
+ const purl = new PackageURL(
14274
+ "generic",
14275
+ "",
14276
+ name,
14277
+ version || undefined,
14278
+ null,
14279
+ null,
14280
+ ).toString();
14281
+ const properties = [
14282
+ {
14283
+ name: "cdx:collider:dependencyKind",
14284
+ value: dependencyKind,
14285
+ },
14286
+ ];
14287
+ if (lockFile) {
14288
+ properties.unshift({
14289
+ name: "SrcFile",
14290
+ value: lockFile,
14291
+ });
14292
+ }
14293
+ const wrapHash =
14294
+ typeof pkgData?.wrap_hash === "string" ? pkgData.wrap_hash.trim() : "";
14295
+ const wrapHashMatch = wrapHash.match(/^sha256:([0-9A-Fa-f]{64})$/);
14296
+ if (wrapHash) {
14297
+ properties.push({
14298
+ name: "cdx:collider:wrapHash",
14299
+ value: wrapHash,
14300
+ });
14301
+ }
14302
+ properties.push({
14303
+ name: "cdx:collider:hasWrapHash",
14304
+ value: wrapHashMatch ? "true" : "false",
14305
+ });
14306
+ if (wrapHash && !wrapHashMatch) {
14307
+ properties.push({
14308
+ name: "cdx:collider:wrapHashInvalid",
14309
+ value: "true",
14310
+ });
14311
+ }
14312
+ let originReference;
14313
+ if (typeof pkgData?.origin === "string" && pkgData.origin.trim()) {
14314
+ try {
14315
+ const originUrl = new URL(pkgData.origin.trim());
14316
+ const originHadSensitiveParts = Boolean(
14317
+ originUrl.username ||
14318
+ originUrl.password ||
14319
+ originUrl.search ||
14320
+ originUrl.hash,
14321
+ );
14322
+ originUrl.username = "";
14323
+ originUrl.password = "";
14324
+ originUrl.search = "";
14325
+ originUrl.hash = "";
14326
+ originReference = originUrl.toString();
14327
+ properties.push({
14328
+ name: "cdx:collider:origin",
14329
+ value: originReference,
14330
+ });
14331
+ properties.push({
14332
+ name: "cdx:collider:originScheme",
14333
+ value: originUrl.protocol.replace(":", ""),
14334
+ });
14335
+ if (originUrl.host) {
14336
+ properties.push({
14337
+ name: "cdx:collider:originHost",
14338
+ value: originUrl.host,
14339
+ });
14340
+ }
14341
+ if (originHadSensitiveParts) {
14342
+ properties.push({
14343
+ name: "cdx:collider:originSanitized",
14344
+ value: "true",
14345
+ });
14346
+ }
14347
+ } catch {
14348
+ thoughtLog("Ignoring invalid Collider origin URL");
14349
+ }
14350
+ }
14351
+ const component = {
14352
+ name,
14353
+ version,
14354
+ purl,
14355
+ "bom-ref": decodeURIComponent(purl),
14356
+ properties,
14357
+ };
14358
+ if (dependencyKind === "direct") {
14359
+ component.scope = "required";
14360
+ }
14361
+ if (wrapHashMatch) {
14362
+ component.hashes = [
14363
+ {
14364
+ alg: "SHA-256",
14365
+ content: wrapHashMatch[1].toLowerCase(),
14366
+ },
14367
+ ];
14368
+ }
14369
+ if (originReference) {
14370
+ component.externalReferences = [
14371
+ {
14372
+ type: "distribution",
14373
+ url: originReference,
14374
+ },
14375
+ ];
14376
+ }
14377
+ return component;
12625
14378
  }
12626
14379
 
12627
14380
  /**
12628
- * Parse a Conan conanfile.txt and extract required and optional packages.
14381
+ * Parse Collider lock file data (collider.lock) and return the package list and
14382
+ * parent component dependencies.
12629
14383
  *
12630
- * @param {string} conanData Raw text contents of a conanfile.txt
12631
- * @returns {Object[]} Array of package objects with purl, name, version, and scope
14384
+ * @param {string} colliderLockData Raw JSON string of the Collider lock file
14385
+ * @param {string} lockFile Source lock file path
14386
+ * @returns {{ pkgList: Object[], dependencies: Object, parentComponentDependencies: string[] }}
12632
14387
  */
12633
- export function parseConanData(conanData) {
14388
+ export function parseColliderLockData(colliderLockData, lockFile) {
12634
14389
  const pkgList = [];
12635
- if (!conanData) {
12636
- return pkgList;
14390
+ const dependencies = {};
14391
+ const parentComponentDependencies = [];
14392
+ if (!colliderLockData) {
14393
+ return { pkgList, dependencies, parentComponentDependencies };
12637
14394
  }
12638
- let scope = "required";
12639
- conanData.split("\n").forEach((l) => {
12640
- l = l.replace("\r", "");
12641
- if (l.includes("[build_requires]")) {
12642
- scope = "optional";
14395
+ let parsedLockFile;
14396
+ try {
14397
+ parsedLockFile = JSON.parse(colliderLockData);
14398
+ } catch {
14399
+ return { pkgList, dependencies, parentComponentDependencies };
14400
+ }
14401
+ const addedBomRefs = new Set();
14402
+ const directDependencies = parsedLockFile.dependencies || {};
14403
+ const packages = parsedLockFile.packages || {};
14404
+ for (const [name, pkgData] of Object.entries(directDependencies)) {
14405
+ const component = buildColliderComponent(name, pkgData, lockFile, "direct");
14406
+ if (!component) {
14407
+ continue;
12643
14408
  }
12644
- if (l.includes("[requires]")) {
12645
- scope = "required";
14409
+ if (!addedBomRefs.has(component["bom-ref"])) {
14410
+ pkgList.push(component);
14411
+ addedBomRefs.add(component["bom-ref"]);
12646
14412
  }
12647
-
12648
- // The line must start with sequence non-whitespace characters, followed by a slash,
12649
- // followed by at least one more non-whitespace character.
12650
- // Provides a heuristic for locating Conan package references inside conanfile.txt files.
12651
- if (l.match(/^[^\s\/]+\/\S+/)) {
12652
- const [purl, name, version] =
12653
- mapConanPkgRefToPurlStringAndNameAndVersion(l);
12654
- if (purl !== null) {
12655
- pkgList.push({
12656
- name,
12657
- version,
12658
- purl,
12659
- "bom-ref": decodeURIComponent(purl),
12660
- scope,
12661
- });
12662
- }
14413
+ if (!parentComponentDependencies.includes(component["bom-ref"])) {
14414
+ parentComponentDependencies.push(component["bom-ref"]);
12663
14415
  }
12664
- });
12665
- return pkgList;
14416
+ if (!(component["bom-ref"] in dependencies)) {
14417
+ dependencies[component["bom-ref"]] = [];
14418
+ }
14419
+ }
14420
+ for (const [name, pkgData] of Object.entries(packages)) {
14421
+ const component = buildColliderComponent(
14422
+ name,
14423
+ pkgData,
14424
+ lockFile,
14425
+ "transitive",
14426
+ );
14427
+ if (!component || addedBomRefs.has(component["bom-ref"])) {
14428
+ continue;
14429
+ }
14430
+ pkgList.push(component);
14431
+ addedBomRefs.add(component["bom-ref"]);
14432
+ if (!(component["bom-ref"] in dependencies)) {
14433
+ dependencies[component["bom-ref"]] = [];
14434
+ }
14435
+ }
14436
+ return { pkgList, dependencies, parentComponentDependencies };
12666
14437
  }
12667
14438
 
12668
14439
  /**
@@ -12800,7 +14571,7 @@ export function parseFlakeNix(flakeNixFile) {
12800
14571
  const flakeContent = readFileSync(flakeNixFile, "utf-8");
12801
14572
 
12802
14573
  // Extract inputs from flake.nix using regex
12803
- const inputsRegex = /inputs\s*=\s*\{[^}]*\}/g;
14574
+ const inputsRegex = /inputs\s*=\s*\{[^}]*}/g;
12804
14575
  let match;
12805
14576
  while ((match = inputsRegex.exec(flakeContent)) !== null) {
12806
14577
  const inputBlock = match[0];
@@ -12808,7 +14579,7 @@ export function parseFlakeNix(flakeNixFile) {
12808
14579
  // Match different input patterns including nested inputs
12809
14580
  const inputPatterns = [
12810
14581
  /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
12811
- /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*\}/gs,
14582
+ /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*}/gs,
12812
14583
  ];
12813
14584
 
12814
14585
  const addedPackages = new Set();
@@ -14750,16 +16521,22 @@ export function convertOSQueryResults(
14750
16521
  if (name) {
14751
16522
  name = sanitizeOsQueryIdentity(name);
14752
16523
  group = sanitizeOsQueryIdentity(group);
14753
- const purl = createOsQueryPurl(
14754
- queryObj.purlType,
14755
- group,
14756
- name,
14757
- version,
14758
- qualifiers,
14759
- subpath,
14760
- );
16524
+ const isCryptoAsset = queryObj.componentType === "cryptographic-asset";
16525
+ const purl = shouldCreateOsQueryPurl(queryObj.componentType)
16526
+ ? createOsQueryPurl(
16527
+ queryObj.purlType,
16528
+ group,
16529
+ name,
16530
+ version,
16531
+ qualifiers,
16532
+ subpath,
16533
+ )
16534
+ : undefined;
14761
16535
  const props = [{ name: "cdx:osquery:category", value: queryCategory }];
14762
16536
  props.push(...createLolbasProperties(queryCategory, res));
16537
+ if (platform() === "linux") {
16538
+ props.push(...createGtfoBinsPropertiesFromRow(queryCategory, res));
16539
+ }
14763
16540
  let providesList;
14764
16541
  if (enhance) {
14765
16542
  switch (queryObj.purlType) {
@@ -14785,25 +16562,39 @@ export function convertOSQueryResults(
14785
16562
  if (providesList) {
14786
16563
  props.push({ name: "PkgProvides", value: providesList.join(", ") });
14787
16564
  }
16565
+ const cryptoProperties = isCryptoAsset
16566
+ ? createOsQueryCryptoProperties(queryCategory, res, version)
16567
+ : undefined;
16568
+ const hashes = createOsQueryComponentHashes(res);
14788
16569
  const apkg = {
14789
16570
  name,
14790
16571
  group,
14791
16572
  version: version || "",
14792
16573
  description,
14793
16574
  publisher,
14794
- "bom-ref": decodeURIComponent(purl),
16575
+ "bom-ref": createOsQueryBomRef(
16576
+ queryCategory,
16577
+ queryObj.componentType,
16578
+ res,
16579
+ name,
16580
+ version,
16581
+ purl,
16582
+ ),
14795
16583
  purl,
14796
16584
  scope,
14797
16585
  type: queryObj.componentType,
14798
16586
  };
16587
+ if (hashes?.length) {
16588
+ apkg.hashes = hashes;
16589
+ }
16590
+ if (cryptoProperties) {
16591
+ apkg.cryptoProperties = cryptoProperties;
16592
+ }
14799
16593
  for (const k of Object.keys(res).filter((p) => {
14800
16594
  if (["version", "description", "publisher"].includes(p)) {
14801
16595
  return false;
14802
16596
  }
14803
- if (queryObj.purlType !== "chrome-extension" && p === "name") {
14804
- return false;
14805
- }
14806
- return true;
16597
+ return !(queryObj.purlType !== "chrome-extension" && p === "name");
14807
16598
  })) {
14808
16599
  if (res[k] && res[k] !== "null") {
14809
16600
  props.push({
@@ -14820,6 +16611,182 @@ export function convertOSQueryResults(
14820
16611
  return pkgList;
14821
16612
  }
14822
16613
 
16614
+ function createOsQueryComponentHashes(res) {
16615
+ const hashes = [];
16616
+ if (res?.md5) {
16617
+ hashes.push({ alg: "MD5", content: res.md5 });
16618
+ }
16619
+ if (res?.sha1) {
16620
+ hashes.push({ alg: "SHA-1", content: res.sha1 });
16621
+ }
16622
+ if (res?.sha256) {
16623
+ hashes.push({ alg: "SHA-256", content: res.sha256 });
16624
+ }
16625
+ return hashes.length ? hashes : undefined;
16626
+ }
16627
+
16628
+ function createOsQueryBomRef(
16629
+ queryCategory,
16630
+ componentType,
16631
+ res,
16632
+ name,
16633
+ version,
16634
+ purl,
16635
+ ) {
16636
+ if (purl) {
16637
+ return decodeURIComponent(purl);
16638
+ }
16639
+ if (componentType === "cryptographic-asset") {
16640
+ return createOsQueryCryptoBomRef(queryCategory, res, name, version);
16641
+ }
16642
+ const identityEntry = [
16643
+ ["path", res?.path],
16644
+ ["key_file", res?.key_file],
16645
+ ["history_file", res?.history_file],
16646
+ ["fragment_path", res?.fragment_path],
16647
+ ["source_path", res?.source_path],
16648
+ ["source", res?.source],
16649
+ ["key", res?.key],
16650
+ ["label", res?.label],
16651
+ ["identifier", res?.identifier],
16652
+ ["uuid", res?.uuid],
16653
+ ["device_id", res?.device_id],
16654
+ ["sid", res?.sid],
16655
+ ["logon_id", res?.logon_id],
16656
+ ["pid", res?.pid],
16657
+ ["uid", res?.uid],
16658
+ ].find(
16659
+ ([, value]) =>
16660
+ value !== undefined && value !== null && String(value).length,
16661
+ );
16662
+ return createOsQueryFallbackBomRef(
16663
+ queryCategory,
16664
+ componentType,
16665
+ name,
16666
+ version,
16667
+ identityEntry?.[0],
16668
+ identityEntry?.[1],
16669
+ );
16670
+ }
16671
+
16672
+ function createOsQueryCryptoBomRef(queryCategory, res, name, version) {
16673
+ const encodedName = encodeURIComponent(
16674
+ name || queryCategory || "crypto-asset",
16675
+ );
16676
+ switch (queryCategory) {
16677
+ case "trusted_gpg_keys":
16678
+ return `crypto/related-crypto-material/public-key/${encodedName}@sha256:${res?.sha256 || version || "unknown"}`;
16679
+ case "kernel_keys":
16680
+ return `crypto/related-crypto-material/key/${encodedName}@${version || res?.serial_number || "unknown"}`;
16681
+ case "certificates":
16682
+ case "secureboot_certificates":
16683
+ return `crypto/certificate/${encodedName}@${res?.sha256 ? `sha256:${res.sha256}` : version || "unknown"}`;
16684
+ default:
16685
+ return `crypto/related-crypto-material/unknown/${encodedName}@${version || "unknown"}`;
16686
+ }
16687
+ }
16688
+
16689
+ function createOsQueryCryptoProperties(queryCategory, res, version) {
16690
+ switch (queryCategory) {
16691
+ case "trusted_gpg_keys":
16692
+ return {
16693
+ assetType: "related-crypto-material",
16694
+ relatedCryptoMaterialProperties: {
16695
+ type: "public-key",
16696
+ id: res?.sha256 || version || res?.path || res?.name || "unknown",
16697
+ state: "active",
16698
+ },
16699
+ };
16700
+ case "kernel_keys":
16701
+ return {
16702
+ assetType: "related-crypto-material",
16703
+ relatedCryptoMaterialProperties: {
16704
+ type: "key",
16705
+ id:
16706
+ res?.serial_number ||
16707
+ version ||
16708
+ res?.description ||
16709
+ res?.name ||
16710
+ "unknown",
16711
+ state: res?.timeout === "expd" ? "deactivated" : "active",
16712
+ },
16713
+ };
16714
+ case "certificates":
16715
+ case "secureboot_certificates": {
16716
+ const certificateFileExtension = extname(res?.path || "")
16717
+ .replace(/^\./, "")
16718
+ .toLowerCase();
16719
+ const certificateFormat =
16720
+ queryCategory === "secureboot_certificates"
16721
+ ? "X.509"
16722
+ : deriveCertificateFormat(certificateFileExtension);
16723
+ return {
16724
+ assetType: "certificate",
16725
+ algorithmProperties: {
16726
+ executionEnvironment: "unknown",
16727
+ implementationPlatform: "unknown",
16728
+ },
16729
+ certificateProperties: {
16730
+ serialNumber: res?.serial || undefined,
16731
+ subjectName: res?.subject || res?.common_name || undefined,
16732
+ issuerName: res?.issuer || undefined,
16733
+ notValidBefore: normalizeCertificateDate(res?.not_valid_before),
16734
+ notValidAfter: normalizeCertificateDate(res?.not_valid_after),
16735
+ certificateFormat,
16736
+ certificateFileExtension: certificateFileExtension || undefined,
16737
+ fingerprint: res?.sha1
16738
+ ? { alg: "SHA-1", content: res.sha1 }
16739
+ : undefined,
16740
+ },
16741
+ };
16742
+ }
16743
+ default:
16744
+ return {
16745
+ assetType: "related-crypto-material",
16746
+ relatedCryptoMaterialProperties: {
16747
+ type: "unknown",
16748
+ id: version || res?.path || res?.name || "unknown",
16749
+ },
16750
+ };
16751
+ }
16752
+ }
16753
+
16754
+ function deriveCertificateFormat(certificateFileExtension) {
16755
+ switch ((certificateFileExtension || "").toLowerCase()) {
16756
+ case "pem":
16757
+ return "PEM";
16758
+ case "der":
16759
+ return "DER";
16760
+ case "cer":
16761
+ case "crt":
16762
+ return "X.509";
16763
+ default:
16764
+ return undefined;
16765
+ }
16766
+ }
16767
+
16768
+ function normalizeCertificateDate(value) {
16769
+ if (value === undefined || value === null || value === "") {
16770
+ return undefined;
16771
+ }
16772
+ const stringValue = `${value}`.trim();
16773
+ if (!stringValue) {
16774
+ return undefined;
16775
+ }
16776
+ const numericValue = Number(stringValue);
16777
+ if (Number.isFinite(numericValue)) {
16778
+ const millis = stringValue.length > 10 ? numericValue : numericValue * 1000;
16779
+ const parsedDate = new Date(millis);
16780
+ return Number.isNaN(parsedDate.getTime())
16781
+ ? undefined
16782
+ : parsedDate.toISOString();
16783
+ }
16784
+ const parsedDate = new Date(stringValue);
16785
+ return Number.isNaN(parsedDate.getTime())
16786
+ ? undefined
16787
+ : parsedDate.toISOString();
16788
+ }
16789
+
14823
16790
  /**
14824
16791
  * Create a PackageURL object from a repository URL string, package type, and version.
14825
16792
  *
@@ -16286,6 +18253,14 @@ export function getGradleCommand(srcPath, rootPath) {
16286
18253
  // continue regardless of error
16287
18254
  }
16288
18255
  gradleCmd = resolve(join(srcPath, findGradleFile));
18256
+ recordDecisionActivity(gradleCmd, {
18257
+ metadata: {
18258
+ decisionType: "path-resolution",
18259
+ selectedSource: "project-wrapper",
18260
+ tool: "gradle",
18261
+ },
18262
+ reason: `Selected project-local Gradle wrapper ${gradleCmd}.`,
18263
+ });
16289
18264
  } else if (rootPath && safeExistsSync(join(rootPath, findGradleFile))) {
16290
18265
  // Check if the root directory has a wrapper script
16291
18266
  try {
@@ -16294,10 +18269,43 @@ export function getGradleCommand(srcPath, rootPath) {
16294
18269
  // continue regardless of error
16295
18270
  }
16296
18271
  gradleCmd = resolve(join(rootPath, findGradleFile));
18272
+ recordDecisionActivity(gradleCmd, {
18273
+ metadata: {
18274
+ decisionType: "path-resolution",
18275
+ selectedSource: "root-wrapper",
18276
+ tool: "gradle",
18277
+ },
18278
+ reason: `Selected root-level Gradle wrapper ${gradleCmd}.`,
18279
+ });
16297
18280
  } else if (process.env.GRADLE_CMD) {
16298
18281
  gradleCmd = process.env.GRADLE_CMD;
18282
+ recordDecisionActivity(gradleCmd, {
18283
+ metadata: {
18284
+ decisionType: "path-resolution",
18285
+ selectedSource: "GRADLE_CMD",
18286
+ tool: "gradle",
18287
+ },
18288
+ reason: `Selected Gradle command from GRADLE_CMD (${gradleCmd}).`,
18289
+ });
16299
18290
  } else if (process.env.GRADLE_HOME) {
16300
18291
  gradleCmd = join(process.env.GRADLE_HOME, "bin", "gradle");
18292
+ recordDecisionActivity(gradleCmd, {
18293
+ metadata: {
18294
+ decisionType: "path-resolution",
18295
+ selectedSource: "GRADLE_HOME",
18296
+ tool: "gradle",
18297
+ },
18298
+ reason: `Selected Gradle command from GRADLE_HOME (${gradleCmd}).`,
18299
+ });
18300
+ } else {
18301
+ recordDecisionActivity(gradleCmd, {
18302
+ metadata: {
18303
+ decisionType: "path-resolution",
18304
+ selectedSource: "PATH",
18305
+ tool: "gradle",
18306
+ },
18307
+ reason: "Falling back to Gradle from PATH.",
18308
+ });
16301
18309
  }
16302
18310
  return gradleCmd;
16303
18311
  }
@@ -17175,6 +19183,14 @@ export function getMavenCommand(srcPath, rootPath) {
17175
19183
  }
17176
19184
  mavenWrapperCmd = resolve(join(srcPath, findMavenFile));
17177
19185
  isWrapperFound = true;
19186
+ recordDecisionActivity(mavenWrapperCmd, {
19187
+ metadata: {
19188
+ decisionType: "path-resolution",
19189
+ selectedSource: "project-wrapper-candidate",
19190
+ tool: "maven",
19191
+ },
19192
+ reason: `Found Maven wrapper candidate ${mavenWrapperCmd}.`,
19193
+ });
17178
19194
  } else if (rootPath && safeExistsSync(join(rootPath, findMavenFile))) {
17179
19195
  // Check if the root directory has a wrapper script
17180
19196
  try {
@@ -17184,6 +19200,14 @@ export function getMavenCommand(srcPath, rootPath) {
17184
19200
  }
17185
19201
  mavenWrapperCmd = resolve(join(rootPath, findMavenFile));
17186
19202
  isWrapperFound = true;
19203
+ recordDecisionActivity(mavenWrapperCmd, {
19204
+ metadata: {
19205
+ decisionType: "path-resolution",
19206
+ selectedSource: "root-wrapper-candidate",
19207
+ tool: "maven",
19208
+ },
19209
+ reason: `Found root-level Maven wrapper candidate ${mavenWrapperCmd}.`,
19210
+ });
17187
19211
  }
17188
19212
  if (isWrapperFound) {
17189
19213
  if (DEBUG_MODE) {
@@ -17192,25 +19216,74 @@ export function getMavenCommand(srcPath, rootPath) {
17192
19216
  );
17193
19217
  }
17194
19218
  const result = safeSpawnSync(mavenWrapperCmd, ["wrapper:wrapper"], {
19219
+ cdxgenActivity: {
19220
+ kind: "probe",
19221
+ metadata: {
19222
+ tool: "maven",
19223
+ },
19224
+ probeType: "wrapper-readiness",
19225
+ },
17195
19226
  cwd: rootPath,
17196
19227
  shell: isWin,
17197
19228
  });
17198
19229
  if (!result.error && !result.status) {
17199
19230
  isWrapperReady = true;
17200
19231
  mavenCmd = mavenWrapperCmd;
19232
+ recordDecisionActivity(mavenCmd, {
19233
+ metadata: {
19234
+ decisionType: "path-resolution",
19235
+ selectedSource: "wrapper",
19236
+ tool: "maven",
19237
+ },
19238
+ reason: `Selected Maven wrapper ${mavenCmd} after readiness probe.`,
19239
+ });
17201
19240
  } else {
17202
19241
  if (DEBUG_MODE) {
17203
19242
  console.log(
17204
19243
  "Maven wrapper script test has failed. Will use the installed version of maven.",
17205
19244
  );
17206
19245
  }
19246
+ recordDecisionActivity(mavenWrapperCmd, {
19247
+ metadata: {
19248
+ decisionType: "fallback",
19249
+ selectedSource: "PATH",
19250
+ skippedSource: "wrapper",
19251
+ tool: "maven",
19252
+ },
19253
+ reason: `Maven wrapper readiness probe failed for ${mavenWrapperCmd}; falling back to installed Maven.`,
19254
+ });
17207
19255
  }
17208
19256
  }
17209
19257
  if (!isWrapperFound || !isWrapperReady) {
17210
19258
  if (process.env.MVN_CMD || process.env.MAVEN_CMD) {
17211
19259
  mavenCmd = process.env.MVN_CMD || process.env.MAVEN_CMD;
19260
+ recordDecisionActivity(mavenCmd, {
19261
+ metadata: {
19262
+ decisionType: "path-resolution",
19263
+ selectedSource: process.env.MVN_CMD ? "MVN_CMD" : "MAVEN_CMD",
19264
+ tool: "maven",
19265
+ },
19266
+ reason: `Selected Maven command from environment (${mavenCmd}).`,
19267
+ });
17212
19268
  } else if (process.env.MAVEN_HOME) {
17213
19269
  mavenCmd = join(process.env.MAVEN_HOME, "bin", "mvn");
19270
+ recordDecisionActivity(mavenCmd, {
19271
+ metadata: {
19272
+ decisionType: "path-resolution",
19273
+ selectedSource: "MAVEN_HOME",
19274
+ tool: "maven",
19275
+ },
19276
+ reason: `Selected Maven command from MAVEN_HOME (${mavenCmd}).`,
19277
+ });
19278
+ } else {
19279
+ recordDecisionActivity(mavenCmd, {
19280
+ metadata: {
19281
+ decisionType: "path-resolution",
19282
+ selectedSource: "PATH",
19283
+ tool: "maven",
19284
+ },
19285
+ reason: "Falling back to Maven from PATH.",
19286
+ });
17214
19287
  }
17215
19288
  }
17216
19289
  return mavenCmd;
@@ -18363,6 +20436,92 @@ export function parsePackageJsonName(name) {
18363
20436
  return returnObject;
18364
20437
  }
18365
20438
 
20439
+ /**
20440
+ * Collect bom-refs from metadata.tools entries.
20441
+ *
20442
+ * @param {Object[]|Object} tools CycloneDX metadata.tools section
20443
+ * @param {Function} predicate Optional filter function
20444
+ * @returns {string[]} Unique tool bom-refs
20445
+ */
20446
+ export function extractToolRefs(tools, predicate) {
20447
+ if (!tools) {
20448
+ return [];
20449
+ }
20450
+ const toolRefs = new Set();
20451
+ const toolList = Array.isArray(tools)
20452
+ ? tools
20453
+ : [...(tools.components || []), ...(tools.services || [])];
20454
+ for (const tool of toolList) {
20455
+ let toolRef = tool?.["bom-ref"];
20456
+ if (!toolRef && tool?.purl) {
20457
+ toolRef = decodeURIComponent(tool.purl);
20458
+ }
20459
+ if (!toolRef && tool?.name) {
20460
+ try {
20461
+ toolRef = new PackageURL(
20462
+ "generic",
20463
+ tool.group || tool.publisher || tool.manufacturer?.name || undefined,
20464
+ tool.name,
20465
+ tool.version || undefined,
20466
+ null,
20467
+ null,
20468
+ ).toString();
20469
+ } catch (_err) {
20470
+ thoughtLog("Unable to derive bom-ref for external tool", {
20471
+ group: tool.group,
20472
+ manufacturer: tool.manufacturer?.name,
20473
+ name: tool.name,
20474
+ publisher: tool.publisher,
20475
+ version: tool.version,
20476
+ });
20477
+ toolRef = undefined;
20478
+ }
20479
+ }
20480
+ if (!toolRef) {
20481
+ continue;
20482
+ }
20483
+ if (!tool["bom-ref"]) {
20484
+ tool["bom-ref"] = toolRef;
20485
+ }
20486
+ if (predicate && !predicate(tool)) {
20487
+ continue;
20488
+ }
20489
+ toolRefs.add(toolRef);
20490
+ }
20491
+ return Array.from(toolRefs);
20492
+ }
20493
+
20494
+ /**
20495
+ * Attach evidence.identity.tools references to the supplied subjects.
20496
+ *
20497
+ * @param {Object|Object[]} subjects Component or service objects
20498
+ * @param {string[]} toolRefs Tool bom-refs
20499
+ * @returns {Object|Object[]} The same mutated subject(s)
20500
+ */
20501
+ export function attachIdentityTools(subjects, toolRefs) {
20502
+ if (!subjects || !toolRefs?.length) {
20503
+ return subjects;
20504
+ }
20505
+ const uniqueToolRefs = Array.from(new Set(toolRefs.filter(Boolean)));
20506
+ if (!uniqueToolRefs.length) {
20507
+ return subjects;
20508
+ }
20509
+ const subjectList = Array.isArray(subjects) ? subjects : [subjects];
20510
+ for (const subject of subjectList) {
20511
+ const identities = Array.isArray(subject?.evidence?.identity)
20512
+ ? subject.evidence.identity
20513
+ : subject?.evidence?.identity
20514
+ ? [subject.evidence.identity]
20515
+ : [];
20516
+ for (const identity of identities) {
20517
+ identity.tools = Array.from(
20518
+ new Set([...(identity.tools || []), ...uniqueToolRefs]),
20519
+ );
20520
+ }
20521
+ }
20522
+ return subjects;
20523
+ }
20524
+
18366
20525
  /**
18367
20526
  * Method to add occurrence evidence for components based on import statements. Currently useful for js
18368
20527
  *
@@ -18459,16 +20618,17 @@ export async function addEvidenceForImports(
18459
20618
  const evidences = allImports[subevidence];
18460
20619
  for (const evidence of evidences) {
18461
20620
  if (evidence && Object.keys(evidence).length && evidence.fileName) {
18462
- pkg.evidence = pkg.evidence || {};
18463
- pkg.evidence.occurrences = pkg.evidence.occurrences || [];
18464
- const occurrenceLocation = `${evidence.fileName}${
18465
- evidence.lineNumber ? `#${evidence.lineNumber}` : ""
18466
- }`;
18467
- if (!seenOccurrenceLocations.has(occurrenceLocation)) {
18468
- pkg.evidence.occurrences.push({
18469
- location: occurrenceLocation,
18470
- });
18471
- seenOccurrenceLocations.add(occurrenceLocation);
20621
+ const occurrence = createOccurrenceEvidence(evidence.fileName, {
20622
+ ...(evidence.lineNumber ? { line: evidence.lineNumber } : {}),
20623
+ });
20624
+ if (occurrence) {
20625
+ pkg.evidence = pkg.evidence || {};
20626
+ pkg.evidence.occurrences = pkg.evidence.occurrences || [];
20627
+ const occurrenceLocation = `${occurrence.location}${occurrence.line ? `#${occurrence.line}` : ""}`;
20628
+ if (!seenOccurrenceLocations.has(occurrenceLocation)) {
20629
+ pkg.evidence.occurrences.push(occurrence);
20630
+ seenOccurrenceLocations.add(occurrenceLocation);
20631
+ }
18472
20632
  }
18473
20633
  importedModules.add(evidence.importedAs);
18474
20634
  for (const importedSm of evidence.importedModules || []) {
@@ -19693,14 +21853,16 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
19693
21853
  purlLocationMap[apkg.purl],
19694
21854
  ).sort();
19695
21855
  // Add the occurrences evidence
19696
- apkg.evidence.occurrences = locationOccurrences.map((l) => ({
19697
- location: l,
19698
- }));
21856
+ apkg.evidence = apkg.evidence || {};
21857
+ apkg.evidence.occurrences = locationOccurrences.map((l) =>
21858
+ parseOccurrenceEvidenceLocation(l),
21859
+ );
19699
21860
  // Set the package scope
19700
21861
  apkg.scope = "required";
19701
21862
  }
19702
21863
  // Add the imported modules to properties
19703
21864
  if (purlModulesMap[apkg.purl]) {
21865
+ apkg.properties = apkg.properties || [];
19704
21866
  apkg.properties.push({
19705
21867
  name: "ImportedModules",
19706
21868
  value: Array.from(purlModulesMap[apkg.purl]).sort().join(", "),
@@ -19708,6 +21870,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
19708
21870
  }
19709
21871
  // Add the called methods to properties
19710
21872
  if (purlMethodsMap[apkg.purl]) {
21873
+ apkg.properties = apkg.properties || [];
19711
21874
  apkg.properties.push({
19712
21875
  name: "CalledMethods",
19713
21876
  value: Array.from(purlMethodsMap[apkg.purl]).sort().join(", "),
@@ -19946,6 +22109,14 @@ export function extractPathEnv(envValues) {
19946
22109
  expandedBinPaths.push(apath);
19947
22110
  }
19948
22111
  }
22112
+ recordObservedActivity("path-resolution", "PATH", {
22113
+ metadata: {
22114
+ capability: "path-lookup",
22115
+ pathCount: expandedBinPaths.length,
22116
+ },
22117
+ reasonBuilder: (count) =>
22118
+ `Expanded PATH into ${expandedBinPaths.length} executable search path(s)${buildReadCountSuffix(count)}.`,
22119
+ });
19949
22120
  return expandedBinPaths;
19950
22121
  }
19951
22122
 
@@ -19954,13 +22125,17 @@ export function extractPathEnv(envValues) {
19954
22125
  *
19955
22126
  * @param basePath Base directory
19956
22127
  * @param binPaths {Array[String]} Paths containing potential binaries
22128
+ * @param excludePaths {Array[String]} Container-relative paths that should be excluded from the result set
19957
22129
  * @return {Array[String]} List of executables
19958
22130
  */
19959
- export function collectExecutables(basePath, binPaths) {
22131
+ export function collectExecutables(basePath, binPaths, excludePaths = []) {
19960
22132
  if (!binPaths) {
19961
22133
  return [];
19962
22134
  }
19963
- let executables = [];
22135
+ const executablesByResolvedPath = new Map();
22136
+ const excludedPathSet = new Set(
22137
+ (excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
22138
+ );
19964
22139
  const ignoreList = [
19965
22140
  "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
19966
22141
  "[",
@@ -19976,12 +22151,64 @@ export function collectExecutables(basePath, binPaths) {
19976
22151
  follow: true,
19977
22152
  ignore: ignoreList,
19978
22153
  });
19979
- executables = executables.concat(files);
22154
+ for (const file of files) {
22155
+ let resolvedFile = file;
22156
+ try {
22157
+ resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
22158
+ if (resolvedFile !== file) {
22159
+ recordSymlinkResolution(join(basePath, file), resolvedFile, {
22160
+ basePath,
22161
+ metadata: {
22162
+ resolutionKind: "executable",
22163
+ },
22164
+ reason: `Resolved executable candidate ${file} to ${resolvedFile}.`,
22165
+ });
22166
+ }
22167
+ } catch (err) {
22168
+ recordSymlinkResolution(join(basePath, file), undefined, {
22169
+ basePath,
22170
+ errorCode: err?.code || err?.name,
22171
+ metadata: {
22172
+ resolutionKind: "executable",
22173
+ },
22174
+ reason: `Failed to resolve executable candidate ${file}.`,
22175
+ status: "failed",
22176
+ });
22177
+ // Broken symlinks or permission errors can prevent realpath resolution.
22178
+ if (DEBUG_MODE) {
22179
+ console.log(`Unable to resolve executable path alias for ${file}`);
22180
+ }
22181
+ }
22182
+ if (
22183
+ excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
22184
+ excludedPathSet.has(
22185
+ resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
22186
+ )
22187
+ ) {
22188
+ continue;
22189
+ }
22190
+ const existingFile = executablesByResolvedPath.get(resolvedFile);
22191
+ if (shouldPreferUsrMergedExecutablePath(file, existingFile)) {
22192
+ executablesByResolvedPath.set(resolvedFile, file);
22193
+ }
22194
+ }
19980
22195
  } catch (_err) {
19981
22196
  // ignore
19982
22197
  }
19983
22198
  }
19984
- return Array.from(new Set(executables)).sort();
22199
+ return Array.from(executablesByResolvedPath.values()).sort();
22200
+ }
22201
+
22202
+ function shouldPreferUsrMergedExecutablePath(file, existingFile) {
22203
+ if (!existingFile) {
22204
+ return true;
22205
+ }
22206
+ const fileUsesUsrPrefix = file.startsWith("usr/");
22207
+ const existingFileUsesUsrPrefix = existingFile.startsWith("usr/");
22208
+ if (fileUsesUsrPrefix !== existingFileUsesUsrPrefix) {
22209
+ return fileUsesUsrPrefix;
22210
+ }
22211
+ return file < existingFile;
19985
22212
  }
19986
22213
 
19987
22214
  /**
@@ -19991,6 +22218,7 @@ export function collectExecutables(basePath, binPaths) {
19991
22218
  * @param libPaths {Array[String]} Paths containing potential libraries
19992
22219
  * @param ldConf {String} Config file used by ldconfig to locate additional paths
19993
22220
  * @param ldConfDirPattern {String} Config directory that can contain more .conf files for ldconfig
22221
+ * @param excludePaths {Array[String]} Container-relative paths that should be excluded from the result set
19994
22222
  *
19995
22223
  * @return {Array[String]} List of executables
19996
22224
  */
@@ -19999,11 +22227,15 @@ export function collectSharedLibs(
19999
22227
  libPaths,
20000
22228
  ldConf,
20001
22229
  ldConfDirPattern,
22230
+ excludePaths = [],
20002
22231
  ) {
20003
22232
  if (!libPaths) {
20004
22233
  return [];
20005
22234
  }
20006
- let sharedLibs = [];
22235
+ const sharedLibs = [];
22236
+ const excludedPathSet = new Set(
22237
+ (excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
22238
+ );
20007
22239
  const ignoreList = [
20008
22240
  "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
20009
22241
  ];
@@ -20035,7 +22267,39 @@ export function collectSharedLibs(
20035
22267
  follow: true,
20036
22268
  ignore: ignoreList,
20037
22269
  });
20038
- sharedLibs = sharedLibs.concat(files);
22270
+ for (const file of files) {
22271
+ let resolvedFile = file;
22272
+ try {
22273
+ resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
22274
+ recordSymlinkResolution(join(basePath, file), resolvedFile, {
22275
+ basePath,
22276
+ metadata: {
22277
+ resolutionKind: "shared-library",
22278
+ },
22279
+ reason: `Resolved shared library candidate ${file} to ${resolvedFile}.`,
22280
+ });
22281
+ } catch (err) {
22282
+ recordSymlinkResolution(join(basePath, file), undefined, {
22283
+ basePath,
22284
+ errorCode: err?.code || err?.name,
22285
+ metadata: {
22286
+ resolutionKind: "shared-library",
22287
+ },
22288
+ reason: `Failed to resolve shared library candidate ${file}.`,
22289
+ status: "failed",
22290
+ });
22291
+ // Broken symlinks or permission errors can prevent realpath resolution.
22292
+ }
22293
+ if (
22294
+ excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
22295
+ excludedPathSet.has(
22296
+ resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
22297
+ )
22298
+ ) {
22299
+ continue;
22300
+ }
22301
+ sharedLibs.push(file);
22302
+ }
20039
22303
  } catch (_err) {
20040
22304
  // ignore
20041
22305
  }
@@ -20169,11 +22433,7 @@ export function hasDangerousUnicode(str) {
20169
22433
 
20170
22434
  // Check for control characters (except common ones like \n, \r, \t)
20171
22435
  const controlChars = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/;
20172
- if (controlChars.test(str)) {
20173
- return true;
20174
- }
20175
-
20176
- return false;
22436
+ return controlChars.test(str);
20177
22437
  }
20178
22438
  // biome-ignore-end lint/suspicious/noControlCharactersInRegex: validation
20179
22439
 
@@ -20208,11 +22468,7 @@ export function isValidDriveRoot(root) {
20208
22468
  }
20209
22469
 
20210
22470
  // Backslash (optional) must be exactly ASCII backslash (0x5C)
20211
- if (backslash && backslash.charCodeAt(0) !== 0x5c) {
20212
- return false;
20213
- }
20214
-
20215
- return true;
22471
+ return !(backslash && backslash.charCodeAt(0) !== 0x5c);
20216
22472
  }
20217
22473
 
20218
22474
  /**