@cyclonedx/cdxgen 12.3.3 → 12.4.1

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 (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -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/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -56,17 +56,25 @@ import { getTreeWithPlugin } from "../managers/piptree.js";
56
56
  import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
57
57
  import Arborist from "../third-party/arborist/lib/index.js";
58
58
  import { analyzeSuspiciousJsFile } from "./analyzer.js";
59
+ import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "./auditCategories.js";
59
60
  import { parseWorkflowFile } from "./ciParsers/githubActions.js";
60
61
  import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
62
+ import {
63
+ createOccurrenceEvidence,
64
+ parseOccurrenceEvidenceLocation,
65
+ } from "./evidenceUtils.js";
66
+ import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
61
67
  import { thoughtLog, traceLog } from "./logger.js";
62
68
  import { createLolbasProperties } from "./lolbas.js";
63
69
  import {
70
+ createOsQueryFallbackBomRef,
64
71
  createOsQueryPurl,
65
72
  deriveOsQueryDescription,
66
73
  deriveOsQueryName,
67
74
  deriveOsQueryPublisher,
68
75
  deriveOsQueryVersion,
69
76
  sanitizeOsQueryIdentity,
77
+ shouldCreateOsQueryPurl,
70
78
  } from "./osqueryTransform.js";
71
79
  import {
72
80
  collectPyLockFileComponents,
@@ -117,6 +125,596 @@ export const DRY_RUN_ERROR_CODE = "CDXGEN_DRY_RUN";
117
125
  const activityLedger = [];
118
126
  let activityCounter = 0;
119
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
+ }
120
718
 
121
719
  export function setDryRunMode(enabled) {
122
720
  isDryRun = !!enabled;
@@ -161,10 +759,8 @@ export function recordActivity(activity) {
161
759
  const identifier = `ACT-${String(++activityCounter).padStart(4, "0")}`;
162
760
  const entry = {
163
761
  identifier,
762
+ ...currentActivityContext,
164
763
  timestamp: new Date().toISOString(),
165
- projectType: currentActivityContext.projectType,
166
- packageType: currentActivityContext.packageType,
167
- sourcePath: currentActivityContext.sourcePath,
168
764
  ...activity,
169
765
  };
170
766
  activityLedger.push(entry);
@@ -172,6 +768,8 @@ export function recordActivity(activity) {
172
768
  return entry;
173
769
  }
174
770
 
771
+ dryRunReadTraceState.recordActivity = recordActivity;
772
+
175
773
  export function getRecordedActivities() {
176
774
  return [...activityLedger];
177
775
  }
@@ -179,11 +777,21 @@ export function getRecordedActivities() {
179
777
  export function resetRecordedActivities() {
180
778
  activityLedger.length = 0;
181
779
  activityCounter = 0;
780
+ dryRunReadTraceState.environmentReads.clear();
781
+ dryRunReadTraceState.observations.clear();
782
+ dryRunReadTraceState.sensitiveFileReads.clear();
182
783
  }
183
784
 
184
- function recordFilesystemActivity(kind, target, status, reason = undefined) {
785
+ function recordFilesystemActivity(
786
+ kind,
787
+ target,
788
+ status,
789
+ reason = undefined,
790
+ metadata = {},
791
+ ) {
185
792
  return recordActivity({
186
793
  kind,
794
+ ...metadata,
187
795
  reason,
188
796
  status,
189
797
  target,
@@ -218,13 +826,40 @@ function hasWritePermission(filePath) {
218
826
  * @Boolean True if the path exists. False otherwise
219
827
  */
220
828
  export function safeExistsSync(filePath) {
829
+ const pathMetadata = classifyActivityPath(filePath);
221
830
  if (!hasReadPermission(filePath)) {
222
831
  if (DEBUG_MODE) {
223
832
  console.log("cdxgen lacks read permission for a requested path.");
224
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
+ }
225
845
  return false;
226
846
  }
227
- 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;
228
863
  }
229
864
 
230
865
  export function safeWriteSync(filePath, data, options) {
@@ -249,9 +884,8 @@ export function safeWriteSync(filePath, data, options) {
249
884
  );
250
885
  return undefined;
251
886
  }
252
- const result = writeFileSync(filePath, data, options);
887
+ writeFileSync(filePath, data, options);
253
888
  recordFilesystemActivity("write", filePath, "completed");
254
- return result;
255
889
  }
256
890
 
257
891
  /**
@@ -283,24 +917,32 @@ export function safeMkdirSync(filePath, options) {
283
917
  );
284
918
  return undefined;
285
919
  }
286
- const result = mkdirSync(filePath, options);
920
+ mkdirSync(filePath, options);
287
921
  recordFilesystemActivity("mkdir", filePath, "completed");
288
- return result;
289
922
  }
290
923
 
291
924
  export function safeMkdtempSync(prefix, options = undefined) {
925
+ const resourceType =
926
+ typeof prefix === "string" && prefix.toLowerCase().includes("cache")
927
+ ? "cache"
928
+ : "temporary-workspace";
292
929
  if (isDryRun) {
293
930
  const tempPath = `${prefix}${randomUUID().replaceAll("-", "").slice(0, 6)}`;
294
931
  recordFilesystemActivity(
295
932
  "temp-dir",
296
933
  tempPath,
297
934
  "blocked",
298
- "Dry run mode blocks temporary directory creation.",
935
+ `Dry run mode blocks temporary directory creation for ${resourceType}.`,
936
+ {
937
+ resourceType,
938
+ },
299
939
  );
300
940
  return tempPath;
301
941
  }
302
942
  const tempPath = mkdtempSync(prefix, options);
303
- recordFilesystemActivity("temp-dir", tempPath, "completed");
943
+ recordFilesystemActivity("temp-dir", tempPath, "completed", undefined, {
944
+ resourceType,
945
+ });
304
946
  return tempPath;
305
947
  }
306
948
 
@@ -314,9 +956,8 @@ export function safeRmSync(filePath, options = undefined) {
314
956
  );
315
957
  return undefined;
316
958
  }
317
- const result = rmSync(filePath, options);
959
+ rmSync(filePath, options);
318
960
  recordFilesystemActivity("cleanup", filePath, "completed");
319
- return result;
320
961
  }
321
962
 
322
963
  export function safeUnlinkSync(filePath) {
@@ -329,9 +970,8 @@ export function safeUnlinkSync(filePath) {
329
970
  );
330
971
  return undefined;
331
972
  }
332
- const result = unlinkSync(filePath);
973
+ unlinkSync(filePath);
333
974
  recordFilesystemActivity("cleanup", filePath, "completed");
334
- return result;
335
975
  }
336
976
 
337
977
  export function safeCopyFileSync(src, dest, mode = undefined) {
@@ -357,32 +997,69 @@ export async function safeExtractArchive(
357
997
  targetPath,
358
998
  extractor,
359
999
  kind = "unzip",
1000
+ options = undefined,
360
1001
  ) {
1002
+ const traceArchiveStats = isDryRun || DEBUG_MODE;
1003
+ const sourceBytes = traceArchiveStats
1004
+ ? getArchiveSourceByteSize(sourcePath)
1005
+ : undefined;
361
1006
  if (isDryRun) {
362
1007
  recordActivity({
1008
+ archiveKind: kind,
1009
+ capability: "archive-extraction",
363
1010
  kind,
364
- 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}.`,
365
1016
  status: "blocked",
366
1017
  target: `${sourcePath} -> ${targetPath}`,
367
1018
  });
368
1019
  return false;
369
1020
  }
370
- await extractor();
371
- recordActivity({
372
- kind,
373
- status: "completed",
374
- target: `${sourcePath} -> ${targetPath}`,
375
- });
376
- 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
+ }
377
1049
  }
378
1050
 
379
1051
  export const commandsExecuted = new Set();
380
- const ALLOW_COMMANDS = (process.env.CDXGEN_ALLOWED_COMMANDS || "").split(",");
381
- function isAllowedCommand(command) {
382
- if (!process.env.CDXGEN_ALLOWED_COMMANDS) {
1052
+ function isAllowedCommand(
1053
+ command,
1054
+ allowedCommandsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS"),
1055
+ ) {
1056
+ if (!allowedCommandsEnv) {
383
1057
  return true;
384
1058
  }
385
- return ALLOW_COMMANDS.includes(command.trim());
1059
+ return allowedCommandsEnv
1060
+ .split(",")
1061
+ .map((entry) => entry.trim())
1062
+ .includes(command.trim());
386
1063
  }
387
1064
 
388
1065
  const ALLOWED_WRAPPERS = new Set(["gradlew", "mvnw"]);
@@ -429,6 +1106,71 @@ function isWindowsShellHijackRisk(command, options) {
429
1106
  return false;
430
1107
  }
431
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
+
432
1174
  /**
433
1175
  * Safe wrapper around spawnSync that enforces permission checks, injects default
434
1176
  * options (maxBuffer, encoding, timeout), warns about unsafe Python and pip/uv
@@ -440,17 +1182,49 @@ function isWindowsShellHijackRisk(command, options) {
440
1182
  * @returns {Object} spawnSync result object with status, stdout, stderr, and error fields
441
1183
  */
442
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
+ }
443
1216
  if (isDryRun) {
444
1217
  const error = createDryRunError(
445
1218
  "execute",
446
1219
  command,
447
- "Dry run mode blocks child process execution.",
1220
+ activityDescriptor.blockedReason,
448
1221
  );
449
1222
  recordActivity({
450
- kind: "execute",
1223
+ kind: activityDescriptor.kind,
1224
+ ...activityDescriptor.metadata,
451
1225
  reason: error.message,
452
1226
  status: "blocked",
453
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1227
+ target: activityDescriptor.target,
454
1228
  });
455
1229
  return {
456
1230
  status: 1,
@@ -461,16 +1235,17 @@ export function safeSpawnSync(command, args, options) {
461
1235
  }
462
1236
  if (
463
1237
  (isSecureMode && process.permission && !process.permission.has("child")) ||
464
- !isAllowedCommand(command)
1238
+ !commandAllowed
465
1239
  ) {
466
1240
  if (DEBUG_MODE) {
467
1241
  console.log(`cdxgen lacks execute permission for ${command}`);
468
1242
  }
469
1243
  recordActivity({
470
- kind: "execute",
1244
+ kind: activityDescriptor.kind,
1245
+ ...activityDescriptor.metadata,
471
1246
  reason: "cdxgen lacks execute permission for this command.",
472
1247
  status: "blocked",
473
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1248
+ target: activityDescriptor.target,
474
1249
  });
475
1250
  return {
476
1251
  status: 1,
@@ -484,10 +1259,11 @@ export function safeSpawnSync(command, args, options) {
484
1259
  const blockedReason = `${command} matches local file in cwd (Windows shell hijack risk)`;
485
1260
  console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
486
1261
  recordActivity({
487
- kind: "execute",
1262
+ kind: activityDescriptor.kind,
1263
+ ...activityDescriptor.metadata,
488
1264
  reason: blockedReason,
489
1265
  status: "blocked",
490
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1266
+ target: activityDescriptor.target,
491
1267
  });
492
1268
  return {
493
1269
  status: 1,
@@ -506,6 +1282,11 @@ export function safeSpawnSync(command, args, options) {
506
1282
  }
507
1283
  if (!options) {
508
1284
  options = {};
1285
+ } else if (options.cdxgenActivity) {
1286
+ options = {
1287
+ ...options,
1288
+ };
1289
+ delete options.cdxgenActivity;
509
1290
  }
510
1291
  // Inject maxBuffer
511
1292
  if (!options.maxBuffer) {
@@ -605,10 +1386,13 @@ export function safeSpawnSync(command, args, options) {
605
1386
  }
606
1387
  const result = spawnSync(command, args, options);
607
1388
  recordActivity({
608
- kind: "execute",
1389
+ kind: activityDescriptor.kind,
1390
+ ...activityDescriptor.metadata,
1391
+ stderrBytes: getOutputByteSize(result.stderr, options.encoding),
609
1392
  reason: result.error?.message,
610
1393
  status: result.status === 0 && !result.error ? "completed" : "failed",
611
- target: `${command}${args?.length ? ` ${args.join(" ")}` : ""}`,
1394
+ stdoutBytes: getOutputByteSize(result.stdout, options.encoding),
1395
+ target: activityDescriptor.target,
612
1396
  });
613
1397
  return result;
614
1398
  }
@@ -985,6 +1769,7 @@ export const PROJECT_TYPE_ALIASES = {
985
1769
  c: ["c", "cpp", "c++", "conan", "collider"],
986
1770
  clojure: ["clojure", "edn", "clj", "leiningen"],
987
1771
  github: ["github", "actions"],
1772
+ hbom: ["hbom", "hardware"],
988
1773
  os: ["os", "osquery", "windows", "linux", "mac", "macos", "darwin"],
989
1774
  jenkins: ["jenkins", "hpi"],
990
1775
  helm: ["helm", "charts"],
@@ -1020,12 +1805,14 @@ export const PROJECT_TYPE_ALIASES = {
1020
1805
  scala: ["scala", "scala3", "sbt", "mill"],
1021
1806
  nix: ["nix", "nixos", "flake"],
1022
1807
  caxa: ["caxa"],
1808
+ asar: ["asar", "electron", "electron-asar"],
1023
1809
  "vscode-extension": [
1024
1810
  "vscode-extension",
1025
1811
  "vsix",
1026
1812
  "vscode",
1027
1813
  "openvsx",
1028
1814
  "vscode-extensions",
1815
+ "ide-extension",
1029
1816
  "ide-extensions",
1030
1817
  ],
1031
1818
  "chrome-extension": [
@@ -1155,6 +1942,90 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
1155
1942
  return shouldInclude;
1156
1943
  }
1157
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
+
1158
2029
  /**
1159
2030
  * Convenient method to check if the given package manager is allowed.
1160
2031
  *
@@ -1196,23 +2067,76 @@ function isCacheDisabled() {
1196
2067
  const cache = isCacheDisabled() ? undefined : gotHttpCache;
1197
2068
  export const remoteHostsAccessed = new Set();
1198
2069
 
1199
- function isAllowedHost(hostname) {
1200
- if (!process.env.CDXGEN_ALLOWED_HOSTS) {
2070
+ export function isAllowedHttpHost(
2071
+ hostname,
2072
+ allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS"),
2073
+ ) {
2074
+ if (!allowedHostsEnv) {
1201
2075
  return true;
1202
2076
  }
1203
- 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);
1204
2085
  for (const ahost of allow_hosts) {
1205
- if (!ahost.length) {
1206
- continue;
1207
- }
1208
- if (hostname === ahost) {
2086
+ if (normalizedHostname === ahost) {
1209
2087
  return true;
1210
2088
  }
1211
2089
  // wildcard support
1212
- 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
+ ) {
1213
2095
  return true;
1214
2096
  }
1215
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";
1216
2140
  }
1217
2141
 
1218
2142
  // Custom user-agent for cdxgen
@@ -1228,18 +2152,40 @@ export const cdxgenAgent = got.extend({
1228
2152
  hooks: {
1229
2153
  beforeRequest: [
1230
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
+ );
1231
2162
  options.context = {
1232
2163
  ...options.context,
2164
+ activityIntent: networkIntent,
1233
2165
  activityTarget: options.url.toString(),
1234
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
+ }
1235
2180
  if (isDryRun) {
1236
2181
  const error = createDryRunError(
1237
2182
  "network",
1238
2183
  options.url.toString(),
1239
- "Dry run mode blocks outbound network access.",
2184
+ `Dry run mode blocks outbound network access (${networkIntent}).`,
1240
2185
  );
1241
2186
  recordActivity({
1242
2187
  kind: "network",
2188
+ networkIntent,
1243
2189
  reason: error.message,
1244
2190
  status: "blocked",
1245
2191
  target: options.url.toString(),
@@ -1247,12 +2193,13 @@ export const cdxgenAgent = got.extend({
1247
2193
  options.context.activityBlocked = true;
1248
2194
  throw error;
1249
2195
  }
1250
- if (!isAllowedHost(options.url.hostname)) {
2196
+ if (!hostAllowed) {
1251
2197
  console.log(
1252
2198
  `Access to the remote host '${options.url.hostname}' is not permitted.`,
1253
2199
  );
1254
2200
  recordActivity({
1255
2201
  kind: "network",
2202
+ networkIntent,
1256
2203
  reason: "The remote host is not permitted.",
1257
2204
  status: "blocked",
1258
2205
  target: options.url.toString(),
@@ -1267,6 +2214,7 @@ export const cdxgenAgent = got.extend({
1267
2214
  );
1268
2215
  recordActivity({
1269
2216
  kind: "network",
2217
+ networkIntent,
1270
2218
  reason: `The '${options.url.protocol}' protocol is not permitted in secure mode.`,
1271
2219
  status: "blocked",
1272
2220
  target: options.url.toString(),
@@ -1293,6 +2241,7 @@ export const cdxgenAgent = got.extend({
1293
2241
  response.url;
1294
2242
  recordActivity({
1295
2243
  kind: "network",
2244
+ networkIntent: response.request.options.context?.activityIntent,
1296
2245
  status: "completed",
1297
2246
  target: activityTarget,
1298
2247
  });
@@ -1306,6 +2255,7 @@ export const cdxgenAgent = got.extend({
1306
2255
  }
1307
2256
  recordActivity({
1308
2257
  kind: "network",
2258
+ networkIntent: error.options?.context?.activityIntent,
1309
2259
  reason: error.message,
1310
2260
  status: "failed",
1311
2261
  target:
@@ -1389,6 +2339,10 @@ export function getAllFilesWithIgnore(
1389
2339
  includeDot,
1390
2340
  ignoreList,
1391
2341
  ) {
2342
+ const patternValue = Array.isArray(pattern)
2343
+ ? pattern.join(",")
2344
+ : String(pattern);
2345
+ const discoveryMetadata = classifyDiscoveryPattern(patternValue);
1392
2346
  try {
1393
2347
  const files = globSync(pattern, {
1394
2348
  cwd: dirPath,
@@ -1399,6 +2353,16 @@ export function getAllFilesWithIgnore(
1399
2353
  follow: false,
1400
2354
  ignore: ignoreList,
1401
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
+ });
1402
2366
  if (files.length > 1) {
1403
2367
  thoughtLog(
1404
2368
  `Found ${files.length} files for the pattern '${pattern}' at '${dirPath}'.`,
@@ -1406,6 +2370,14 @@ export function getAllFilesWithIgnore(
1406
2370
  }
1407
2371
  return files;
1408
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
+ });
1409
2381
  if (DEBUG_MODE) {
1410
2382
  console.error(err);
1411
2383
  }
@@ -7089,7 +8061,7 @@ function addComponentProperty(component, name, value) {
7089
8061
  }
7090
8062
 
7091
8063
  const PYTHON_DIRECT_REFERENCE_PATTERN =
7092
- /^([A-Za-z0-9_.-]+)(?:\[[^\]]+\])?\s*@\s*(\S+)$/;
8064
+ /^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?\s*@\s*(\S+)$/;
7093
8065
 
7094
8066
  function isWindowsAbsolutePath(value) {
7095
8067
  return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("\\\\");
@@ -7180,7 +8152,7 @@ function extractPythonDependencyKey(value) {
7180
8152
  }
7181
8153
  const packageMatch =
7182
8154
  typeof value === "string"
7183
- ? value.trim().match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+\])?/)
8155
+ ? value.trim().match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?/)
7184
8156
  : undefined;
7185
8157
  return normalizePythonDependencyKey(packageMatch?.[1]);
7186
8158
  }
@@ -8463,7 +9435,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
8463
9435
 
8464
9436
  // Handle extras (e.g., package[extra1,extra2])
8465
9437
  let extras = null;
8466
- const extrasMatch = l.match(/^([a-zA-Z0-9_\-\.]+)(\[([^\]]+)\])?(.*)$/);
9438
+ const extrasMatch = l.match(/^([a-zA-Z0-9_\-.]+)(\[([^\]]+)])?(.*)$/);
8467
9439
  if (extrasMatch) {
8468
9440
  const [, packageName, , extrasStr, versionSpecifiers] = extrasMatch;
8469
9441
  const name = packageName;
@@ -8532,7 +9504,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
8532
9504
  }
8533
9505
  pkgList.push(apkg);
8534
9506
  } else {
8535
- const match = l.match(/^([a-zA-Z0-9_\-\.]+)(.*)$/);
9507
+ const match = l.match(/^([a-zA-Z0-9_\-.]+)(.*)$/);
8536
9508
  if (!match) {
8537
9509
  continue;
8538
9510
  }
@@ -13599,7 +14571,7 @@ export function parseFlakeNix(flakeNixFile) {
13599
14571
  const flakeContent = readFileSync(flakeNixFile, "utf-8");
13600
14572
 
13601
14573
  // Extract inputs from flake.nix using regex
13602
- const inputsRegex = /inputs\s*=\s*\{[^}]*\}/g;
14574
+ const inputsRegex = /inputs\s*=\s*\{[^}]*}/g;
13603
14575
  let match;
13604
14576
  while ((match = inputsRegex.exec(flakeContent)) !== null) {
13605
14577
  const inputBlock = match[0];
@@ -13607,7 +14579,7 @@ export function parseFlakeNix(flakeNixFile) {
13607
14579
  // Match different input patterns including nested inputs
13608
14580
  const inputPatterns = [
13609
14581
  /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
13610
- /([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,
13611
14583
  ];
13612
14584
 
13613
14585
  const addedPackages = new Set();
@@ -15549,16 +16521,22 @@ export function convertOSQueryResults(
15549
16521
  if (name) {
15550
16522
  name = sanitizeOsQueryIdentity(name);
15551
16523
  group = sanitizeOsQueryIdentity(group);
15552
- const purl = createOsQueryPurl(
15553
- queryObj.purlType,
15554
- group,
15555
- name,
15556
- version,
15557
- qualifiers,
15558
- subpath,
15559
- );
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;
15560
16535
  const props = [{ name: "cdx:osquery:category", value: queryCategory }];
15561
16536
  props.push(...createLolbasProperties(queryCategory, res));
16537
+ if (platform() === "linux") {
16538
+ props.push(...createGtfoBinsPropertiesFromRow(queryCategory, res));
16539
+ }
15562
16540
  let providesList;
15563
16541
  if (enhance) {
15564
16542
  switch (queryObj.purlType) {
@@ -15584,25 +16562,39 @@ export function convertOSQueryResults(
15584
16562
  if (providesList) {
15585
16563
  props.push({ name: "PkgProvides", value: providesList.join(", ") });
15586
16564
  }
16565
+ const cryptoProperties = isCryptoAsset
16566
+ ? createOsQueryCryptoProperties(queryCategory, res, version)
16567
+ : undefined;
16568
+ const hashes = createOsQueryComponentHashes(res);
15587
16569
  const apkg = {
15588
16570
  name,
15589
16571
  group,
15590
16572
  version: version || "",
15591
16573
  description,
15592
16574
  publisher,
15593
- "bom-ref": decodeURIComponent(purl),
16575
+ "bom-ref": createOsQueryBomRef(
16576
+ queryCategory,
16577
+ queryObj.componentType,
16578
+ res,
16579
+ name,
16580
+ version,
16581
+ purl,
16582
+ ),
15594
16583
  purl,
15595
16584
  scope,
15596
16585
  type: queryObj.componentType,
15597
16586
  };
16587
+ if (hashes?.length) {
16588
+ apkg.hashes = hashes;
16589
+ }
16590
+ if (cryptoProperties) {
16591
+ apkg.cryptoProperties = cryptoProperties;
16592
+ }
15598
16593
  for (const k of Object.keys(res).filter((p) => {
15599
16594
  if (["version", "description", "publisher"].includes(p)) {
15600
16595
  return false;
15601
16596
  }
15602
- if (queryObj.purlType !== "chrome-extension" && p === "name") {
15603
- return false;
15604
- }
15605
- return true;
16597
+ return !(queryObj.purlType !== "chrome-extension" && p === "name");
15606
16598
  })) {
15607
16599
  if (res[k] && res[k] !== "null") {
15608
16600
  props.push({
@@ -15619,6 +16611,182 @@ export function convertOSQueryResults(
15619
16611
  return pkgList;
15620
16612
  }
15621
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
+
15622
16790
  /**
15623
16791
  * Create a PackageURL object from a repository URL string, package type, and version.
15624
16792
  *
@@ -17085,6 +18253,14 @@ export function getGradleCommand(srcPath, rootPath) {
17085
18253
  // continue regardless of error
17086
18254
  }
17087
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
+ });
17088
18264
  } else if (rootPath && safeExistsSync(join(rootPath, findGradleFile))) {
17089
18265
  // Check if the root directory has a wrapper script
17090
18266
  try {
@@ -17093,10 +18269,43 @@ export function getGradleCommand(srcPath, rootPath) {
17093
18269
  // continue regardless of error
17094
18270
  }
17095
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
+ });
17096
18280
  } else if (process.env.GRADLE_CMD) {
17097
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
+ });
17098
18290
  } else if (process.env.GRADLE_HOME) {
17099
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
+ });
17100
18309
  }
17101
18310
  return gradleCmd;
17102
18311
  }
@@ -17974,6 +19183,14 @@ export function getMavenCommand(srcPath, rootPath) {
17974
19183
  }
17975
19184
  mavenWrapperCmd = resolve(join(srcPath, findMavenFile));
17976
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
+ });
17977
19194
  } else if (rootPath && safeExistsSync(join(rootPath, findMavenFile))) {
17978
19195
  // Check if the root directory has a wrapper script
17979
19196
  try {
@@ -17983,6 +19200,14 @@ export function getMavenCommand(srcPath, rootPath) {
17983
19200
  }
17984
19201
  mavenWrapperCmd = resolve(join(rootPath, findMavenFile));
17985
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
+ });
17986
19211
  }
17987
19212
  if (isWrapperFound) {
17988
19213
  if (DEBUG_MODE) {
@@ -17991,25 +19216,74 @@ export function getMavenCommand(srcPath, rootPath) {
17991
19216
  );
17992
19217
  }
17993
19218
  const result = safeSpawnSync(mavenWrapperCmd, ["wrapper:wrapper"], {
19219
+ cdxgenActivity: {
19220
+ kind: "probe",
19221
+ metadata: {
19222
+ tool: "maven",
19223
+ },
19224
+ probeType: "wrapper-readiness",
19225
+ },
17994
19226
  cwd: rootPath,
17995
19227
  shell: isWin,
17996
19228
  });
17997
19229
  if (!result.error && !result.status) {
17998
19230
  isWrapperReady = true;
17999
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
+ });
18000
19240
  } else {
18001
19241
  if (DEBUG_MODE) {
18002
19242
  console.log(
18003
19243
  "Maven wrapper script test has failed. Will use the installed version of maven.",
18004
19244
  );
18005
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
+ });
18006
19255
  }
18007
19256
  }
18008
19257
  if (!isWrapperFound || !isWrapperReady) {
18009
19258
  if (process.env.MVN_CMD || process.env.MAVEN_CMD) {
18010
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
+ });
18011
19268
  } else if (process.env.MAVEN_HOME) {
18012
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
+ });
18013
19287
  }
18014
19288
  }
18015
19289
  return mavenCmd;
@@ -19344,16 +20618,17 @@ export async function addEvidenceForImports(
19344
20618
  const evidences = allImports[subevidence];
19345
20619
  for (const evidence of evidences) {
19346
20620
  if (evidence && Object.keys(evidence).length && evidence.fileName) {
19347
- pkg.evidence = pkg.evidence || {};
19348
- pkg.evidence.occurrences = pkg.evidence.occurrences || [];
19349
- const occurrenceLocation = `${evidence.fileName}${
19350
- evidence.lineNumber ? `#${evidence.lineNumber}` : ""
19351
- }`;
19352
- if (!seenOccurrenceLocations.has(occurrenceLocation)) {
19353
- pkg.evidence.occurrences.push({
19354
- location: occurrenceLocation,
19355
- });
19356
- 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
+ }
19357
20632
  }
19358
20633
  importedModules.add(evidence.importedAs);
19359
20634
  for (const importedSm of evidence.importedModules || []) {
@@ -20578,14 +21853,16 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
20578
21853
  purlLocationMap[apkg.purl],
20579
21854
  ).sort();
20580
21855
  // Add the occurrences evidence
20581
- apkg.evidence.occurrences = locationOccurrences.map((l) => ({
20582
- location: l,
20583
- }));
21856
+ apkg.evidence = apkg.evidence || {};
21857
+ apkg.evidence.occurrences = locationOccurrences.map((l) =>
21858
+ parseOccurrenceEvidenceLocation(l),
21859
+ );
20584
21860
  // Set the package scope
20585
21861
  apkg.scope = "required";
20586
21862
  }
20587
21863
  // Add the imported modules to properties
20588
21864
  if (purlModulesMap[apkg.purl]) {
21865
+ apkg.properties = apkg.properties || [];
20589
21866
  apkg.properties.push({
20590
21867
  name: "ImportedModules",
20591
21868
  value: Array.from(purlModulesMap[apkg.purl]).sort().join(", "),
@@ -20593,6 +21870,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
20593
21870
  }
20594
21871
  // Add the called methods to properties
20595
21872
  if (purlMethodsMap[apkg.purl]) {
21873
+ apkg.properties = apkg.properties || [];
20596
21874
  apkg.properties.push({
20597
21875
  name: "CalledMethods",
20598
21876
  value: Array.from(purlMethodsMap[apkg.purl]).sort().join(", "),
@@ -20831,6 +22109,14 @@ export function extractPathEnv(envValues) {
20831
22109
  expandedBinPaths.push(apath);
20832
22110
  }
20833
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
+ });
20834
22120
  return expandedBinPaths;
20835
22121
  }
20836
22122
 
@@ -20839,13 +22125,17 @@ export function extractPathEnv(envValues) {
20839
22125
  *
20840
22126
  * @param basePath Base directory
20841
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
20842
22129
  * @return {Array[String]} List of executables
20843
22130
  */
20844
- export function collectExecutables(basePath, binPaths) {
22131
+ export function collectExecutables(basePath, binPaths, excludePaths = []) {
20845
22132
  if (!binPaths) {
20846
22133
  return [];
20847
22134
  }
20848
22135
  const executablesByResolvedPath = new Map();
22136
+ const excludedPathSet = new Set(
22137
+ (excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
22138
+ );
20849
22139
  const ignoreList = [
20850
22140
  "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
20851
22141
  "[",
@@ -20865,12 +22155,38 @@ export function collectExecutables(basePath, binPaths) {
20865
22155
  let resolvedFile = file;
20866
22156
  try {
20867
22157
  resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
20868
- } catch (_err) {
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
+ });
20869
22177
  // Broken symlinks or permission errors can prevent realpath resolution.
20870
22178
  if (DEBUG_MODE) {
20871
22179
  console.log(`Unable to resolve executable path alias for ${file}`);
20872
22180
  }
20873
22181
  }
22182
+ if (
22183
+ excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
22184
+ excludedPathSet.has(
22185
+ resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
22186
+ )
22187
+ ) {
22188
+ continue;
22189
+ }
20874
22190
  const existingFile = executablesByResolvedPath.get(resolvedFile);
20875
22191
  if (shouldPreferUsrMergedExecutablePath(file, existingFile)) {
20876
22192
  executablesByResolvedPath.set(resolvedFile, file);
@@ -20902,6 +22218,7 @@ function shouldPreferUsrMergedExecutablePath(file, existingFile) {
20902
22218
  * @param libPaths {Array[String]} Paths containing potential libraries
20903
22219
  * @param ldConf {String} Config file used by ldconfig to locate additional paths
20904
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
20905
22222
  *
20906
22223
  * @return {Array[String]} List of executables
20907
22224
  */
@@ -20910,11 +22227,15 @@ export function collectSharedLibs(
20910
22227
  libPaths,
20911
22228
  ldConf,
20912
22229
  ldConfDirPattern,
22230
+ excludePaths = [],
20913
22231
  ) {
20914
22232
  if (!libPaths) {
20915
22233
  return [];
20916
22234
  }
20917
- let sharedLibs = [];
22235
+ const sharedLibs = [];
22236
+ const excludedPathSet = new Set(
22237
+ (excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
22238
+ );
20918
22239
  const ignoreList = [
20919
22240
  "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
20920
22241
  ];
@@ -20946,7 +22267,39 @@ export function collectSharedLibs(
20946
22267
  follow: true,
20947
22268
  ignore: ignoreList,
20948
22269
  });
20949
- 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
+ }
20950
22303
  } catch (_err) {
20951
22304
  // ignore
20952
22305
  }
@@ -21080,11 +22433,7 @@ export function hasDangerousUnicode(str) {
21080
22433
 
21081
22434
  // Check for control characters (except common ones like \n, \r, \t)
21082
22435
  const controlChars = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/;
21083
- if (controlChars.test(str)) {
21084
- return true;
21085
- }
21086
-
21087
- return false;
22436
+ return controlChars.test(str);
21088
22437
  }
21089
22438
  // biome-ignore-end lint/suspicious/noControlCharactersInRegex: validation
21090
22439
 
@@ -21119,11 +22468,7 @@ export function isValidDriveRoot(root) {
21119
22468
  }
21120
22469
 
21121
22470
  // Backslash (optional) must be exactly ASCII backslash (0x5C)
21122
- if (backslash && backslash.charCodeAt(0) !== 0x5c) {
21123
- return false;
21124
- }
21125
-
21126
- return true;
22471
+ return !(backslash && backslash.charCodeAt(0) !== 0x5c);
21127
22472
  }
21128
22473
 
21129
22474
  /**