@cyclonedx/cdxgen 12.3.0 → 12.3.2

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 (121) hide show
  1. package/README.md +15 -5
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +241 -81
  4. package/bin/repl.js +138 -0
  5. package/data/rules/ai-agent-governance.yaml +249 -0
  6. package/data/rules/dependency-sources.yaml +41 -0
  7. package/data/rules/mcp-servers.yaml +304 -0
  8. package/data/rules/package-integrity.yaml +123 -0
  9. package/lib/audit/index.js +353 -29
  10. package/lib/audit/index.poku.js +247 -7
  11. package/lib/audit/reporters.js +26 -0
  12. package/lib/audit/scoring.js +262 -13
  13. package/lib/audit/scoring.poku.js +179 -0
  14. package/lib/audit/targets.js +391 -2
  15. package/lib/audit/targets.poku.js +416 -3
  16. package/lib/cli/index.js +588 -45
  17. package/lib/cli/index.poku.js +735 -1
  18. package/lib/evinser/evinser.js +8 -5
  19. package/lib/helpers/agentFormulationParser.js +318 -0
  20. package/lib/helpers/aiInventory.js +262 -0
  21. package/lib/helpers/aiInventory.poku.js +111 -0
  22. package/lib/helpers/analyzer.js +1769 -0
  23. package/lib/helpers/analyzer.poku.js +284 -3
  24. package/lib/helpers/auditCategories.js +76 -0
  25. package/lib/helpers/ciParsers/githubActions.js +140 -16
  26. package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
  27. package/lib/helpers/communityAiConfigParser.js +672 -0
  28. package/lib/helpers/communityAiConfigParser.poku.js +63 -0
  29. package/lib/helpers/depsUtils.js +108 -0
  30. package/lib/helpers/depsUtils.poku.js +72 -1
  31. package/lib/helpers/display.js +325 -3
  32. package/lib/helpers/display.poku.js +301 -0
  33. package/lib/helpers/formulationParsers.js +28 -0
  34. package/lib/helpers/formulationParsers.poku.js +504 -1
  35. package/lib/helpers/jsonLike.js +102 -0
  36. package/lib/helpers/jsonLike.poku.js +34 -0
  37. package/lib/helpers/mcp.js +248 -0
  38. package/lib/helpers/mcp.poku.js +101 -0
  39. package/lib/helpers/mcpConfigParser.js +656 -0
  40. package/lib/helpers/mcpConfigParser.poku.js +126 -0
  41. package/lib/helpers/mcpDiscovery.js +84 -0
  42. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  43. package/lib/helpers/protobom.js +3 -3
  44. package/lib/helpers/provenanceUtils.js +29 -4
  45. package/lib/helpers/provenanceUtils.poku.js +29 -3
  46. package/lib/helpers/registryProvenance.js +210 -0
  47. package/lib/helpers/registryProvenance.poku.js +144 -0
  48. package/lib/helpers/rustFormulationParser.js +330 -0
  49. package/lib/helpers/source.js +21 -2
  50. package/lib/helpers/source.poku.js +38 -0
  51. package/lib/helpers/utils.js +1331 -83
  52. package/lib/helpers/utils.poku.js +599 -188
  53. package/lib/helpers/vsixutils.js +12 -4
  54. package/lib/helpers/vsixutils.poku.js +34 -0
  55. package/lib/managers/binary.js +36 -12
  56. package/lib/managers/binary.poku.js +68 -0
  57. package/lib/managers/docker.js +59 -9
  58. package/lib/managers/docker.poku.js +61 -0
  59. package/lib/managers/piptree.js +12 -7
  60. package/lib/managers/piptree.poku.js +44 -0
  61. package/lib/stages/postgen/annotator.js +2 -1
  62. package/lib/stages/postgen/annotator.poku.js +15 -0
  63. package/lib/stages/postgen/auditBom.js +20 -6
  64. package/lib/stages/postgen/auditBom.poku.js +694 -1
  65. package/lib/stages/postgen/postgen.js +262 -11
  66. package/lib/stages/postgen/postgen.poku.js +306 -2
  67. package/lib/stages/postgen/ruleEngine.js +49 -1
  68. package/lib/stages/postgen/spdxConverter.poku.js +70 -0
  69. package/lib/stages/pregen/pregen.js +6 -4
  70. package/package.json +1 -1
  71. package/types/bin/repl.d.ts.map +1 -1
  72. package/types/lib/audit/index.d.ts.map +1 -1
  73. package/types/lib/audit/reporters.d.ts.map +1 -1
  74. package/types/lib/audit/scoring.d.ts.map +1 -1
  75. package/types/lib/audit/targets.d.ts +12 -0
  76. package/types/lib/audit/targets.d.ts.map +1 -1
  77. package/types/lib/cli/index.d.ts +2 -8
  78. package/types/lib/cli/index.d.ts.map +1 -1
  79. package/types/lib/evinser/evinser.d.ts.map +1 -1
  80. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  81. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  82. package/types/lib/helpers/aiInventory.d.ts +23 -0
  83. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  84. package/types/lib/helpers/analyzer.d.ts +10 -0
  85. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  86. package/types/lib/helpers/auditCategories.d.ts +12 -0
  87. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  88. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  89. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  90. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  91. package/types/lib/helpers/depsUtils.d.ts +8 -0
  92. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  93. package/types/lib/helpers/display.d.ts +17 -1
  94. package/types/lib/helpers/display.d.ts.map +1 -1
  95. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  96. package/types/lib/helpers/jsonLike.d.ts +4 -0
  97. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  98. package/types/lib/helpers/mcp.d.ts +29 -0
  99. package/types/lib/helpers/mcp.d.ts.map +1 -0
  100. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  101. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  102. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  103. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  104. package/types/lib/helpers/provenanceUtils.d.ts +5 -3
  105. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
  106. package/types/lib/helpers/registryProvenance.d.ts +9 -0
  107. package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
  108. package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
  109. package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
  110. package/types/lib/helpers/source.d.ts.map +1 -1
  111. package/types/lib/helpers/utils.d.ts +31 -1
  112. package/types/lib/helpers/utils.d.ts.map +1 -1
  113. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  114. package/types/lib/managers/binary.d.ts.map +1 -1
  115. package/types/lib/managers/docker.d.ts.map +1 -1
  116. package/types/lib/managers/piptree.d.ts.map +1 -1
  117. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  118. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  119. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  120. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  121. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
1
+ import { readdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, join, resolve } from "node:path";
4
4
  import process from "node:process";
@@ -13,6 +13,9 @@ import {
13
13
  isMac,
14
14
  isWin,
15
15
  safeExistsSync,
16
+ safeExtractArchive,
17
+ safeMkdtempSync,
18
+ safeRmSync,
16
19
  } from "./utils.js";
17
20
  import { toVersRange } from "./versutils.js";
18
21
 
@@ -804,9 +807,14 @@ export async function extractVsixToTempDir(vsixFile) {
804
807
  let tempDir;
805
808
  let zip;
806
809
  try {
807
- tempDir = mkdtempSync(join(getTmpDir(), "vsix-deps-"));
810
+ tempDir = safeMkdtempSync(join(getTmpDir(), "vsix-deps-"));
808
811
  zip = new StreamZip.async({ file: vsixFile });
809
- await zip.extract(null, tempDir);
812
+ const extracted = await safeExtractArchive(vsixFile, tempDir, async () => {
813
+ await zip.extract(null, tempDir);
814
+ });
815
+ if (!extracted) {
816
+ return undefined;
817
+ }
810
818
  // Most vsix files have content under extension/ subdirectory
811
819
  const extensionSubDir = join(tempDir, "extension");
812
820
  if (safeExistsSync(extensionSubDir)) {
@@ -854,7 +862,7 @@ export function cleanupTempDir(tempDir) {
854
862
  dirBaseName.startsWith("vsix-deps-") &&
855
863
  resolve(dirToRemove, "..") === expectedBase
856
864
  ) {
857
- rmSync(dirToRemove, { recursive: true, force: true });
865
+ safeRmSync(dirToRemove, { recursive: true, force: true });
858
866
  }
859
867
  } catch (_e) {
860
868
  // Best effort cleanup
@@ -9,7 +9,9 @@ import {
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
 
12
+ import esmock from "esmock";
12
13
  import { describe, it } from "poku";
14
+ import sinon from "sinon";
13
15
 
14
16
  import {
15
17
  cleanupTempDir,
@@ -40,6 +42,38 @@ describe("VSCODE_EXTENSION_PURL_TYPE", () => {
40
42
  });
41
43
  });
42
44
 
45
+ describe("extractVsixToTempDir()", () => {
46
+ it("returns undefined when dry-run blocks vsix extraction", async () => {
47
+ const safeExtractArchive = sinon.stub().resolves(false);
48
+ const zipClose = sinon.stub().resolves();
49
+ const { extractVsixToTempDir } = await esmock("./vsixutils.js", {
50
+ "node-stream-zip": {
51
+ default: {
52
+ async: sinon.stub().returns({
53
+ close: zipClose,
54
+ }),
55
+ },
56
+ },
57
+ "./utils.js": {
58
+ DEBUG_MODE: false,
59
+ getTmpDir: sinon.stub().returns("/tmp"),
60
+ isMac: false,
61
+ isWin: false,
62
+ safeExistsSync: sinon.stub().returns(false),
63
+ safeExtractArchive,
64
+ safeMkdtempSync: sinon.stub().returns("/tmp/vsix-deps-test"),
65
+ safeRmSync: sinon.stub(),
66
+ },
67
+ });
68
+
69
+ const extractedDir = await extractVsixToTempDir("/tmp/sample.vsix");
70
+
71
+ assert.strictEqual(extractedDir, undefined);
72
+ sinon.assert.calledOnce(safeExtractArchive);
73
+ sinon.assert.calledOnce(zipClose);
74
+ });
75
+ });
76
+
43
77
  describe("getIdeExtensionDirs", () => {
44
78
  it("should return an array of IDE configurations", () => {
45
79
  const ides = getIdeExtensionDirs();
@@ -1,11 +1,4 @@
1
- import {
2
- lstatSync,
3
- mkdtempSync,
4
- readFileSync,
5
- realpathSync,
6
- rmSync,
7
- statSync,
8
- } from "node:fs";
1
+ import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
9
2
  import { arch as _arch, platform as _platform, homedir } from "node:os";
10
3
  import {
11
4
  basename,
@@ -30,11 +23,15 @@ import {
30
23
  extractPathEnv,
31
24
  findLicenseId,
32
25
  getTmpDir,
26
+ isDryRun,
33
27
  isSpdxLicenseExpression,
34
28
  multiChecksumFile,
29
+ recordActivity,
35
30
  retrieveCdxgenPluginVersion,
36
31
  safeExistsSync,
37
32
  safeMkdirSync,
33
+ safeMkdtempSync,
34
+ safeRmSync,
38
35
  safeSpawnSync,
39
36
  } from "../helpers/utils.js";
40
37
  import { getDirs } from "./containerutils.js";
@@ -451,6 +448,25 @@ export function executeSourcekitten(args) {
451
448
  * @returns {Object} Metadata containing packages, dependencies, etc
452
449
  */
453
450
  export async function getOSPackages(src, imageConfig) {
451
+ if (isDryRun) {
452
+ recordActivity({
453
+ kind: "container",
454
+ reason:
455
+ "Dry run mode blocks Trivy-based OS package generation because it executes external tools and writes temporary output.",
456
+ status: "blocked",
457
+ target: src,
458
+ });
459
+ return {
460
+ allTypes: new Set(),
461
+ binPaths: [],
462
+ bundledRuntimes: new Set(),
463
+ bundledSdks: new Set(),
464
+ dependenciesList: [],
465
+ executables: [],
466
+ osPackages: [],
467
+ sharedLibs: [],
468
+ };
469
+ }
454
470
  const pkgList = [];
455
471
  const dependenciesList = [];
456
472
  const allTypes = new Set();
@@ -491,7 +507,7 @@ export async function getOSPackages(src, imageConfig) {
491
507
  if (safeExistsSync(src)) {
492
508
  imageType = "rootfs";
493
509
  }
494
- const tempDir = mkdtempSync(join(getTmpDir(), "trivy-cdxgen-"));
510
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "trivy-cdxgen-"));
495
511
  const bomJsonFile = join(tempDir, "trivy-bom.json");
496
512
  const args = [
497
513
  imageType,
@@ -541,9 +557,7 @@ export async function getOSPackages(src, imageConfig) {
541
557
  if (DEBUG_MODE) {
542
558
  console.log(`Cleaning up ${tempDir}`);
543
559
  }
544
- if (rmSync) {
545
- rmSync(tempDir, { recursive: true, force: true });
546
- }
560
+ safeRmSync(tempDir, { recursive: true, force: true });
547
561
  }
548
562
  const osReleaseData = {};
549
563
  let osReleaseFile;
@@ -1026,6 +1040,16 @@ const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
1026
1040
  };
1027
1041
 
1028
1042
  export function executeOsQuery(query) {
1043
+ if (isDryRun) {
1044
+ recordActivity({
1045
+ kind: "osquery",
1046
+ reason:
1047
+ "Dry run mode blocks osquery execution and reports the query instead.",
1048
+ status: "blocked",
1049
+ target: query,
1050
+ });
1051
+ return undefined;
1052
+ }
1029
1053
  if (OSQUERY_BIN) {
1030
1054
  if (!query.endsWith(";")) {
1031
1055
  query = `${query};`;
@@ -0,0 +1,68 @@
1
+ import esmock from "esmock";
2
+ import { assert, it } from "poku";
3
+ import sinon from "sinon";
4
+
5
+ async function loadBinaryModule({ utilsOverrides } = {}) {
6
+ return esmock("./binary.js", {
7
+ "../helpers/utils.js": {
8
+ adjustLicenseInformation: sinon.stub(),
9
+ collectExecutables: sinon.stub().returns([]),
10
+ collectSharedLibs: sinon.stub().returns([]),
11
+ DEBUG_MODE: false,
12
+ dirNameStr: "/tmp",
13
+ extractPathEnv: sinon.stub().returns([]),
14
+ findLicenseId: sinon.stub(),
15
+ getTmpDir: sinon.stub().returns("/tmp"),
16
+ isDryRun: false,
17
+ isSpdxLicenseExpression: sinon.stub().returns(false),
18
+ multiChecksumFile: sinon.stub(),
19
+ recordActivity: sinon.stub(),
20
+ retrieveCdxgenPluginVersion: sinon.stub().returns("1.0.0"),
21
+ safeExistsSync: sinon.stub().returns(false),
22
+ safeMkdirSync: sinon.stub(),
23
+ safeMkdtempSync: sinon.stub().returns("/tmp/trivy-cdxgen-test"),
24
+ safeRmSync: sinon.stub(),
25
+ safeSpawnSync: sinon
26
+ .stub()
27
+ .returns({ status: 1, stdout: "", stderr: "" }),
28
+ ...utilsOverrides,
29
+ },
30
+ });
31
+ }
32
+
33
+ it("executeOsQuery() reports a blocked dry-run activity", async () => {
34
+ const recordActivity = sinon.stub();
35
+ const { executeOsQuery } = await loadBinaryModule({
36
+ utilsOverrides: {
37
+ isDryRun: true,
38
+ recordActivity,
39
+ },
40
+ });
41
+ const result = executeOsQuery("select * from processes");
42
+ assert.strictEqual(result, undefined);
43
+ sinon.assert.calledWithMatch(recordActivity, {
44
+ kind: "osquery",
45
+ status: "blocked",
46
+ target: "select * from processes",
47
+ });
48
+ });
49
+
50
+ it("getOSPackages() returns empty collections and reports a blocked dry-run activity", async () => {
51
+ const recordActivity = sinon.stub();
52
+ const { getOSPackages } = await loadBinaryModule({
53
+ utilsOverrides: {
54
+ isDryRun: true,
55
+ recordActivity,
56
+ },
57
+ });
58
+ const result = await getOSPackages("/tmp/rootfs", {});
59
+ assert.deepStrictEqual(result.osPackages, []);
60
+ assert.deepStrictEqual(result.dependenciesList, []);
61
+ assert.deepStrictEqual(result.binPaths, []);
62
+ assert.deepStrictEqual(Array.from(result.allTypes), []);
63
+ sinon.assert.calledWithMatch(recordActivity, {
64
+ kind: "container",
65
+ status: "blocked",
66
+ target: "/tmp/rootfs",
67
+ });
68
+ });
@@ -2,11 +2,8 @@ import { Buffer } from "node:buffer";
2
2
  import {
3
3
  createReadStream,
4
4
  lstatSync,
5
- mkdtempSync,
6
5
  readdirSync,
7
6
  readFileSync,
8
- rmSync,
9
- writeFileSync,
10
7
  } from "node:fs";
11
8
  import { platform as _platform, userInfo as _userInfo, homedir } from "node:os";
12
9
  import { basename, join, resolve, win32 } from "node:path";
@@ -22,9 +19,14 @@ import {
22
19
  extractPathEnv,
23
20
  getAllFiles,
24
21
  getTmpDir,
22
+ isDryRun,
23
+ recordActivity,
25
24
  safeExistsSync,
26
25
  safeMkdirSync,
26
+ safeMkdtempSync,
27
+ safeRmSync,
27
28
  safeSpawnSync,
29
+ safeWriteSync,
28
30
  } from "../helpers/utils.js";
29
31
  import { getDirs, getOnlyDirs } from "./containerutils.js";
30
32
 
@@ -393,6 +395,16 @@ const getDefaultOptions = (forRegistry) => {
393
395
  * daemon base URL, or `undefined`
394
396
  */
395
397
  export const getConnection = async (options, forRegistry) => {
398
+ if (isDryRun) {
399
+ recordActivity({
400
+ kind: "network",
401
+ reason:
402
+ "Dry run mode blocks container daemon and registry HTTP requests.",
403
+ status: "blocked",
404
+ target: forRegistry || "container-daemon",
405
+ });
406
+ return undefined;
407
+ }
396
408
  if (isContainerd || isNerdctl) {
397
409
  return undefined;
398
410
  }
@@ -507,6 +519,16 @@ export const getConnection = async (options, forRegistry) => {
507
519
  * requests, raw Buffer for other methods, or `undefined` if no client is available
508
520
  */
509
521
  export const makeRequest = async (path, method, forRegistry) => {
522
+ if (isDryRun) {
523
+ recordActivity({
524
+ kind: "network",
525
+ reason:
526
+ "Dry run mode blocks container daemon and registry HTTP requests.",
527
+ status: "blocked",
528
+ target: `${method} ${forRegistry || "container-daemon"}/${path}`,
529
+ });
530
+ return undefined;
531
+ }
510
532
  const client = await getConnection({}, forRegistry);
511
533
  if (!client) {
512
534
  return undefined;
@@ -959,6 +981,16 @@ const EXTRACT_EXCLUDE_TYPES = new Set([
959
981
  * empty or a non-fatal error was encountered
960
982
  */
961
983
  export const extractTar = async (fullImageName, dir, options) => {
984
+ if (isDryRun) {
985
+ recordActivity({
986
+ kind: "untar",
987
+ reason:
988
+ "Dry run mode blocks untar and layer extraction operations because they create files on disk.",
989
+ status: "blocked",
990
+ target: `${fullImageName} -> ${dir}`,
991
+ });
992
+ return false;
993
+ }
962
994
  try {
963
995
  await stream.pipeline(
964
996
  createReadStream(fullImageName),
@@ -1126,12 +1158,22 @@ const discoverManifestFromBlobs = (tempDir) => {
1126
1158
  * Returns the location of the layers with additional packages related metadata
1127
1159
  */
1128
1160
  export const exportArchive = async (fullImageName, options = {}) => {
1161
+ if (isDryRun) {
1162
+ recordActivity({
1163
+ kind: "container",
1164
+ reason:
1165
+ "Dry run mode blocks container archive expansion and layer materialization.",
1166
+ status: "blocked",
1167
+ target: fullImageName,
1168
+ });
1169
+ return undefined;
1170
+ }
1129
1171
  if (!safeExistsSync(fullImageName)) {
1130
1172
  console.log(`Unable to find container image archive ${fullImageName}`);
1131
1173
  return undefined;
1132
1174
  }
1133
1175
  const manifest = {};
1134
- const tempDir = mkdtempSync(join(getTmpDir(), "docker-images-"));
1176
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "docker-images-"));
1135
1177
  const allLayersExplodedDir = join(tempDir, "all-layers");
1136
1178
  const blobsDir = join(tempDir, "blobs", "sha256");
1137
1179
  safeMkdirSync(allLayersExplodedDir);
@@ -1163,7 +1205,7 @@ export const exportArchive = async (fullImageName, options = {}) => {
1163
1205
  if (safeExistsSync(blobsDir)) {
1164
1206
  const discoveredManifest = discoverManifestFromBlobs(tempDir);
1165
1207
  if (discoveredManifest?.length) {
1166
- writeFileSync(
1208
+ safeWriteSync(
1167
1209
  synthesizedManifestFile,
1168
1210
  JSON.stringify(discoveredManifest),
1169
1211
  "utf-8",
@@ -1340,6 +1382,16 @@ export const extractFromManifest = async (
1340
1382
  * Returns the location of the layers with additional packages related metadata
1341
1383
  */
1342
1384
  export const exportImage = async (fullImageName, options) => {
1385
+ if (isDryRun) {
1386
+ recordActivity({
1387
+ kind: "container",
1388
+ reason:
1389
+ "Dry run mode blocks container image pull, save, and export operations.",
1390
+ status: "blocked",
1391
+ target: fullImageName,
1392
+ });
1393
+ return undefined;
1394
+ }
1343
1395
  // Safely ignore local directories
1344
1396
  if (
1345
1397
  !fullImageName ||
@@ -1358,7 +1410,7 @@ export const exportImage = async (fullImageName, options) => {
1358
1410
  if (tag === "" && digest === "") {
1359
1411
  fullImageName = `${fullImageName}:latest`;
1360
1412
  }
1361
- const tempDir = mkdtempSync(join(getTmpDir(), "docker-images-"));
1413
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "docker-images-"));
1362
1414
  const allLayersExplodedDir = join(tempDir, "all-layers");
1363
1415
  let manifestFile = join(tempDir, "manifest.json");
1364
1416
  // Windows containers use index.json
@@ -1386,9 +1438,7 @@ export const exportImage = async (fullImageName, options) => {
1386
1438
  if (DEBUG_MODE) {
1387
1439
  console.log(`Cleaning up ${imageTarFile}`);
1388
1440
  }
1389
- if (rmSync) {
1390
- rmSync(imageTarFile, { force: true });
1391
- }
1441
+ safeRmSync(imageTarFile, { force: true });
1392
1442
  } else {
1393
1443
  const client = await getConnection({}, registry);
1394
1444
  try {
@@ -135,12 +135,18 @@ async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
135
135
  };
136
136
  const utilsStub = {
137
137
  DEBUG_MODE: false,
138
+ createDryRunError: sinon.stub(),
138
139
  extractPathEnv: sinon.stub().returns([]),
139
140
  getAllFiles: sinon.stub().returns([]),
140
141
  getTmpDir: sinon.stub().returns("/tmp"),
142
+ isDryRun: false,
143
+ recordActivity: sinon.stub(),
141
144
  safeExistsSync: sinon.stub().returns(false),
142
145
  safeMkdirSync: sinon.stub(),
146
+ safeMkdtempSync: sinon.stub().returns("/tmp/docker-images-test"),
147
+ safeRmSync: sinon.stub(),
143
148
  safeSpawnSync: sinon.stub().returns({ status: 1, stdout: "", stderr: "" }),
149
+ safeWriteSync: sinon.stub(),
144
150
  ...utilsOverrides,
145
151
  };
146
152
  const dockerModule = await esmock("./docker.js", {
@@ -257,6 +263,61 @@ await it("docker getImage uses nerdctl when DOCKER_CMD is configured", async ()
257
263
  }
258
264
  });
259
265
 
266
+ await it("docker getConnection reports blocked network activity in dry-run mode", async () => {
267
+ const recordActivity = sinon.stub();
268
+ const { dockerModule } = await loadDockerModule({
269
+ utilsOverrides: {
270
+ isDryRun: true,
271
+ recordActivity,
272
+ },
273
+ });
274
+ const conn = await dockerModule.getConnection({}, "docker.io");
275
+ assert.strictEqual(conn, undefined);
276
+ sinon.assert.calledWithMatch(recordActivity, {
277
+ kind: "network",
278
+ status: "blocked",
279
+ target: "docker.io",
280
+ });
281
+ });
282
+
283
+ await it("docker extractTar reports a blocked untar activity in dry-run mode", async () => {
284
+ const recordActivity = sinon.stub();
285
+ const { dockerModule } = await loadDockerModule({
286
+ utilsOverrides: {
287
+ isDryRun: true,
288
+ recordActivity,
289
+ },
290
+ });
291
+ const result = await dockerModule.extractTar(
292
+ "/tmp/image.tar",
293
+ "/tmp/out",
294
+ {},
295
+ );
296
+ assert.strictEqual(result, false);
297
+ sinon.assert.calledWithMatch(recordActivity, {
298
+ kind: "untar",
299
+ status: "blocked",
300
+ target: "/tmp/image.tar -> /tmp/out",
301
+ });
302
+ });
303
+
304
+ await it("docker exportImage reports a blocked container activity in dry-run mode", async () => {
305
+ const recordActivity = sinon.stub();
306
+ const { dockerModule } = await loadDockerModule({
307
+ utilsOverrides: {
308
+ isDryRun: true,
309
+ recordActivity,
310
+ },
311
+ });
312
+ const result = await dockerModule.exportImage("alpine:3.20", {});
313
+ assert.strictEqual(result, undefined);
314
+ sinon.assert.calledWithMatch(recordActivity, {
315
+ kind: "container",
316
+ status: "blocked",
317
+ target: "alpine:3.20",
318
+ });
319
+ });
320
+
260
321
  await it("docker exportImage ignores local directories", async () => {
261
322
  const imageData = await exportImage(".");
262
323
  assert.strictEqual(imageData, undefined);
@@ -4,10 +4,17 @@
4
4
  *
5
5
  * We use the internal pip api to construct the dependency tree for modern python + pip environments
6
6
  */
7
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { readFileSync } from "node:fs";
8
8
  import { delimiter, join } from "node:path";
9
9
 
10
- import { getTmpDir, safeExistsSync, safeSpawnSync } from "../helpers/utils.js";
10
+ import {
11
+ getTmpDir,
12
+ safeExistsSync,
13
+ safeMkdtempSync,
14
+ safeRmSync,
15
+ safeSpawnSync,
16
+ safeWriteSync,
17
+ } from "../helpers/utils.js";
11
18
 
12
19
  const PIP_TREE_PLUGIN_CONTENT = `
13
20
  import importlib.metadata as importlib_metadata
@@ -226,11 +233,11 @@ if __name__ == "__main__":
226
233
  */
227
234
  export const getTreeWithPlugin = (env, python_cmd, basePath) => {
228
235
  let tree = [];
229
- const tempDir = mkdtempSync(join(getTmpDir(), "cdxgen-piptree-"));
236
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "cdxgen-piptree-"));
230
237
  const pipPlugin = join(tempDir, "piptree.py");
231
238
  const pipTreeJson = join(tempDir, "piptree.json");
232
239
  const pipPluginArgs = [pipPlugin, pipTreeJson];
233
- writeFileSync(pipPlugin, PIP_TREE_PLUGIN_CONTENT);
240
+ safeWriteSync(pipPlugin, PIP_TREE_PLUGIN_CONTENT);
234
241
  if (env.PIP_TARGET) {
235
242
  if (!env.PYTHONPATH) {
236
243
  env.PYTHONPATH = "";
@@ -255,8 +262,6 @@ export const getTreeWithPlugin = (env, python_cmd, basePath) => {
255
262
  }),
256
263
  );
257
264
  }
258
- if (rmSync) {
259
- rmSync(tempDir, { recursive: true, force: true });
260
- }
265
+ safeRmSync(tempDir, { recursive: true, force: true });
261
266
  return tree;
262
267
  };
@@ -0,0 +1,44 @@
1
+ import path from "node:path";
2
+
3
+ import esmock from "esmock";
4
+ import { assert, it } from "poku";
5
+ import sinon from "sinon";
6
+
7
+ it("getTreeWithPlugin() reports dry-run temp-dir, write, execute, and cleanup activity", async () => {
8
+ const tempDir = path.join(path.sep, "tmp", "cdxgen-piptree-test");
9
+ const pluginFile = path.join(tempDir, "piptree.py");
10
+ const outputFile = path.join(tempDir, "piptree.json");
11
+ const safeMkdtempSync = sinon.stub().returns(tempDir);
12
+ const safeWriteSync = sinon.stub();
13
+ const safeSpawnSync = sinon.stub().returns({
14
+ error: new Error("dry run"),
15
+ status: 1,
16
+ stderr: "",
17
+ stdout: "",
18
+ });
19
+ const safeRmSync = sinon.stub();
20
+ const { getTreeWithPlugin } = await esmock("./piptree.js", {
21
+ "../helpers/utils.js": {
22
+ getTmpDir: sinon.stub().returns("/tmp"),
23
+ safeExistsSync: sinon.stub().returns(false),
24
+ safeMkdtempSync,
25
+ safeRmSync,
26
+ safeSpawnSync,
27
+ safeWriteSync,
28
+ },
29
+ });
30
+
31
+ const result = getTreeWithPlugin({}, "python3", "/repo");
32
+
33
+ assert.deepStrictEqual(result, []);
34
+ sinon.assert.calledOnce(safeMkdtempSync);
35
+ sinon.assert.calledWithMatch(safeWriteSync, pluginFile, sinon.match.string);
36
+ sinon.assert.calledWith(safeSpawnSync, "python3", [pluginFile, outputFile], {
37
+ cwd: "/repo",
38
+ env: {},
39
+ });
40
+ sinon.assert.calledWith(safeRmSync, tempDir, {
41
+ force: true,
42
+ recursive: true,
43
+ });
44
+ });
@@ -285,7 +285,8 @@ export function textualMetadata(bomJson) {
285
285
  const { bomType, bomTypeDescription } = findBomType(bomJson);
286
286
  const metadata = bomJson.metadata;
287
287
  const lifecycles = metadata?.lifecycles || [];
288
- const tlpClassification = metadata.distribution;
288
+ const tlpClassification =
289
+ metadata.distributionConstraints?.tlp || metadata.distribution;
289
290
  const cryptoAssetsCount = bomJson?.components?.filter(
290
291
  (c) => c.type === "cryptographic-asset",
291
292
  ).length;
@@ -311,3 +311,18 @@ it("extractTags tests", () => {
311
311
  "security",
312
312
  ]);
313
313
  });
314
+
315
+ it("textualMetadata includes the CycloneDX 1.7 TLP classification from distributionConstraints", () => {
316
+ assert.match(
317
+ textualMetadata({
318
+ bomFormat: "CycloneDX",
319
+ specVersion: "1.7",
320
+ metadata: {
321
+ distributionConstraints: {
322
+ tlp: "AMBER_AND_STRICT",
323
+ },
324
+ },
325
+ }),
326
+ /TLP\) classification for this document is 'AMBER_AND_STRICT'/,
327
+ );
328
+ });
@@ -6,6 +6,10 @@ import { join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  import { buildAnnotationText } from "../../helpers/annotationFormatter.js";
9
+ import {
10
+ expandBomAuditCategories,
11
+ validateBomAuditCategories,
12
+ } from "../../helpers/auditCategories.js";
9
13
  import { table } from "../../helpers/table.js";
10
14
  import {
11
15
  DEBUG_MODE,
@@ -45,15 +49,17 @@ export async function auditBom(bomJson, options) {
45
49
  }
46
50
  let activeRules = rules;
47
51
  if (options.bomAuditCategories) {
48
- const categories = options.bomAuditCategories
49
- .split(",")
50
- .map((c) => c.trim())
51
- .filter(Boolean);
52
+ const { categories, expandedCategories } = validateBomAuditCategories(
53
+ options.bomAuditCategories,
54
+ rules,
55
+ );
52
56
  if (categories.length > 0) {
53
- activeRules = rules.filter((r) => categories.includes(r.category));
57
+ activeRules = rules.filter((r) =>
58
+ expandedCategories.includes(r.category),
59
+ );
54
60
  if (DEBUG_MODE) {
55
61
  console.log(
56
- `Filtering rules by categories: ${categories.join(", ")} (${activeRules.length} active)`,
62
+ `Filtering rules by categories: ${categories.join(", ")} -> ${expandBomAuditCategories(categories).join(", ")} (${activeRules.length} active)`,
57
63
  );
58
64
  }
59
65
  }
@@ -159,6 +165,14 @@ export function formatAnnotations(findings, bomJson) {
159
165
  value: f.attackTechniques.join(","),
160
166
  });
161
167
  }
168
+ if (f.standards && typeof f.standards === "object") {
169
+ for (const [standardName, entries] of Object.entries(f.standards)) {
170
+ properties.push({
171
+ name: `cdx:audit:standards:${standardName}`,
172
+ value: Array.isArray(entries) ? entries.join(",") : String(entries),
173
+ });
174
+ }
175
+ }
162
176
  if (f?.location?.purl) {
163
177
  properties.push({
164
178
  name: "cdx:audit:location:purl",