@cyclonedx/cdxgen 12.3.1 → 12.3.3

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 (87) hide show
  1. package/README.md +6 -0
  2. package/bin/cdxgen.js +1 -2
  3. package/data/rules/ai-agent-governance.yaml +43 -0
  4. package/data/rules/ci-permissions.yaml +132 -0
  5. package/data/rules/dependency-sources.yaml +65 -5
  6. package/data/rules/mcp-servers.yaml +36 -2
  7. package/data/rules/package-integrity.yaml +22 -0
  8. package/lib/cli/index.js +436 -56
  9. package/lib/cli/index.poku.js +875 -2
  10. package/lib/helpers/agentFormulationParser.js +10 -3
  11. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  12. package/lib/helpers/aiInventory.js +262 -0
  13. package/lib/helpers/aiInventory.poku.js +111 -0
  14. package/lib/helpers/analyzer.js +413 -54
  15. package/lib/helpers/analyzer.poku.js +117 -0
  16. package/lib/helpers/auditCategories.js +76 -0
  17. package/lib/helpers/chromextutils.js +25 -3
  18. package/lib/helpers/chromextutils.poku.js +68 -0
  19. package/lib/helpers/ciParsers/githubActions.js +79 -0
  20. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  21. package/lib/helpers/communityAiConfigParser.js +15 -5
  22. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  23. package/lib/helpers/depsUtils.js +5 -0
  24. package/lib/helpers/depsUtils.poku.js +55 -0
  25. package/lib/helpers/display.js +50 -24
  26. package/lib/helpers/display.poku.js +70 -58
  27. package/lib/helpers/formulationParsers.js +26 -6
  28. package/lib/helpers/jsonLike.js +21 -20
  29. package/lib/helpers/jsonLike.poku.js +34 -0
  30. package/lib/helpers/mcpConfigParser.js +32 -16
  31. package/lib/helpers/mcpConfigParser.poku.js +104 -0
  32. package/lib/helpers/mcpDiscovery.js +13 -23
  33. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  34. package/lib/helpers/propertySanitizer.js +121 -0
  35. package/lib/helpers/utils.js +953 -41
  36. package/lib/helpers/utils.poku.js +901 -1
  37. package/lib/managers/binary.js +16 -0
  38. package/lib/managers/binary.poku.js +1 -0
  39. package/lib/managers/docker.js +240 -16
  40. package/lib/managers/docker.poku.js +1142 -2
  41. package/lib/server/server.js +7 -4
  42. package/lib/server/server.poku.js +36 -1
  43. package/lib/stages/postgen/annotator.js +2 -1
  44. package/lib/stages/postgen/annotator.poku.js +15 -0
  45. package/lib/stages/postgen/auditBom.js +12 -6
  46. package/lib/stages/postgen/auditBom.poku.js +755 -6
  47. package/lib/stages/postgen/postgen.js +229 -6
  48. package/lib/stages/postgen/postgen.poku.js +180 -0
  49. package/package.json +2 -1
  50. package/types/lib/cli/index.d.ts +1 -0
  51. package/types/lib/cli/index.d.ts.map +1 -1
  52. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  53. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  54. package/types/lib/helpers/aiInventory.d.ts +23 -0
  55. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  56. package/types/lib/helpers/analyzer.d.ts +5 -0
  57. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  58. package/types/lib/helpers/auditCategories.d.ts +12 -0
  59. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  60. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  61. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  62. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  63. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  64. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  65. package/types/lib/helpers/display.d.ts +1 -0
  66. package/types/lib/helpers/display.d.ts.map +1 -1
  67. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  68. package/types/lib/helpers/jsonLike.d.ts +4 -0
  69. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  70. package/types/lib/helpers/mcp.d.ts +29 -0
  71. package/types/lib/helpers/mcp.d.ts.map +1 -0
  72. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  73. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  74. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  75. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  76. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  77. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  78. package/types/lib/helpers/utils.d.ts +31 -0
  79. package/types/lib/helpers/utils.d.ts.map +1 -1
  80. package/types/lib/managers/binary.d.ts.map +1 -1
  81. package/types/lib/managers/docker.d.ts +3 -0
  82. package/types/lib/managers/docker.d.ts.map +1 -1
  83. package/types/lib/server/server.d.ts +1 -0
  84. package/types/lib/server/server.d.ts.map +1 -1
  85. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  86. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  87. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import {
2
3
  mkdirSync,
3
4
  mkdtempSync,
@@ -6,7 +7,7 @@ import {
6
7
  writeFileSync,
7
8
  } from "node:fs";
8
9
  import { tmpdir } from "node:os";
9
- import { dirname, join } from "node:path";
10
+ import { dirname, join, normalize, sep } from "node:path";
10
11
  import process from "node:process";
11
12
  import { fileURLToPath } from "node:url";
12
13
 
@@ -16,7 +17,13 @@ import sinon from "sinon";
16
17
 
17
18
  import { auditBom } from "../stages/postgen/auditBom.js";
18
19
  import { postProcess } from "../stages/postgen/postgen.js";
19
- import { createBom, createChromeExtensionBom, createRustBom } from "./index.js";
20
+ import {
21
+ createBom,
22
+ createChromeExtensionBom,
23
+ createNodejsBom,
24
+ createPHPBom,
25
+ createRustBom,
26
+ } from "./index.js";
20
27
 
21
28
  const fixtureDir = join(
22
29
  dirname(fileURLToPath(import.meta.url)),
@@ -53,8 +60,115 @@ const mcpFixtureDir = join(
53
60
  "data",
54
61
  "mcp-repotest",
55
62
  );
63
+ const cacheDisableFixtureDir = join(
64
+ dirname(fileURLToPath(import.meta.url)),
65
+ "..",
66
+ "..",
67
+ "test",
68
+ "data",
69
+ "cache-disable-repotest",
70
+ );
71
+ const composerFixtureDir = join(
72
+ dirname(fileURLToPath(import.meta.url)),
73
+ "..",
74
+ "..",
75
+ "test",
76
+ "data",
77
+ );
78
+ const repoDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
79
+
80
+ function getProp(obj, name) {
81
+ return obj?.properties?.find((property) => property.name === name)?.value;
82
+ }
83
+
84
+ function createComposerNodeModulesFixture() {
85
+ const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-composer-node-modules-"));
86
+ const packageDir = join(tmpDir, "node_modules", "moment-timezone");
87
+ mkdirSync(packageDir, { recursive: true });
88
+ writeFileSync(
89
+ join(packageDir, "composer.json"),
90
+ readFileSync(join(composerFixtureDir, "composer.json"), "utf-8"),
91
+ );
92
+ writeFileSync(
93
+ join(packageDir, "composer.lock"),
94
+ readFileSync(join(composerFixtureDir, "composer.lock"), "utf-8"),
95
+ );
96
+ return tmpDir;
97
+ }
98
+
99
+ function createJarNodeModulesFixture() {
100
+ const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-jar-node-modules-"));
101
+ const packageDir = join(tmpDir, "node_modules", "font-mfizz");
102
+ mkdirSync(packageDir, { recursive: true });
103
+ writeFileSync(join(packageDir, "blaze.jar"), "fake jar content");
104
+ return tmpDir;
105
+ }
106
+
107
+ const stubbedJarPackage = {
108
+ group: "org.slf4j",
109
+ name: "slf4j-simple",
110
+ version: "2.0.17",
111
+ purl: "pkg:maven/org.slf4j/slf4j-simple@2.0.17?type=jar",
112
+ "bom-ref": "pkg:maven/org.slf4j/slf4j-simple@2.0.17?type=jar",
113
+ };
114
+
115
+ async function loadStubbedCreateJarBom() {
116
+ const actualUtils = await import("../helpers/utils.js");
117
+ const extractJarArchive = sinon.stub().resolves([stubbedJarPackage]);
118
+ const getMvnMetadata = sinon.stub().callsFake(async (pkgList) => pkgList);
119
+ const mockedIndex = await esmock("./index.js", {
120
+ "../helpers/utils.js": {
121
+ ...actualUtils,
122
+ extractJarArchive,
123
+ getMvnMetadata,
124
+ },
125
+ });
126
+ return mockedIndex.createJarBom;
127
+ }
128
+
129
+ function toPortablePath(filePath) {
130
+ return normalize(filePath).split(sep).join("/");
131
+ }
132
+
133
+ function getNpmPackFilePaths() {
134
+ const command =
135
+ process.platform === "win32"
136
+ ? {
137
+ args: ["/c", "npm", "pack", "--dry-run", "--json"],
138
+ file: process.env.ComSpec || "cmd.exe",
139
+ }
140
+ : {
141
+ args: ["pack", "--dry-run", "--json"],
142
+ file: "npm",
143
+ };
144
+ const packOutput = execFileSync(command.file, command.args, {
145
+ cwd: repoDir,
146
+ encoding: "utf8",
147
+ });
148
+ const [packSummary] = JSON.parse(packOutput);
149
+ return packSummary.files.map((file) => toPortablePath(file.path));
150
+ }
56
151
 
57
152
  describe("CLI tests", () => {
153
+ describe("distribution filters", () => {
154
+ it("keeps npm types while excluding poku tests from npm pack output", () => {
155
+ const packedPaths = getNpmPackFilePaths();
156
+
157
+ assert.ok(
158
+ packedPaths.some((path) => path.startsWith("types/")),
159
+ "expected npm pack output to keep generated type definitions",
160
+ );
161
+ assert.ok(
162
+ packedPaths.every((path) => !path.endsWith(".poku.js")),
163
+ "expected npm pack output to exclude co-located poku tests",
164
+ );
165
+ assert.ok(
166
+ packedPaths.every((path) => !path.startsWith("test/")),
167
+ "expected npm pack output to exclude test fixtures",
168
+ );
169
+ });
170
+ });
171
+
58
172
  describe("submitBom()", () => {
59
173
  it("should report blocked Dependency-Track submission during dry-run", async () => {
60
174
  const recordActivity = sinon.stub();
@@ -137,6 +251,7 @@ describe("CLI tests", () => {
137
251
 
138
252
  assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
139
253
  assert.equal(options.method, "PUT");
254
+ assert.equal(options.followRedirect, false);
140
255
  assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
141
256
  assert.equal(options.headers["X-Api-Key"], apiKey);
142
257
  assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
@@ -197,6 +312,7 @@ describe("CLI tests", () => {
197
312
  // Assert call arguments against expectations
198
313
  assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
199
314
  assert.equal(options.method, "PUT");
315
+ assert.equal(options.followRedirect, false);
200
316
  assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
201
317
  assert.equal(options.headers["X-Api-Key"], apiKey);
202
318
  assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
@@ -253,6 +369,7 @@ describe("CLI tests", () => {
253
369
 
254
370
  assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
255
371
  assert.equal(options.method, "PUT");
372
+ assert.equal(options.followRedirect, false);
256
373
  assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
257
374
  assert.equal(options.headers["X-Api-Key"], apiKey);
258
375
  assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
@@ -319,6 +436,41 @@ describe("CLI tests", () => {
319
436
  assert.equal(response, undefined);
320
437
  sinon.assert.notCalled(gotStub);
321
438
  });
439
+
440
+ it("disables redirects for the POST fallback request too", async () => {
441
+ const putError = new Error("Method not allowed");
442
+ putError.response = { statusCode: 405 };
443
+ const gotStub = sinon.stub();
444
+ gotStub
445
+ .onFirstCall()
446
+ .returns({
447
+ json: sinon.stub().rejects(putError),
448
+ })
449
+ .onSecondCall()
450
+ .returns({
451
+ json: sinon.stub().resolves({ success: true }),
452
+ });
453
+ gotStub.extend = sinon.stub().returns(gotStub);
454
+
455
+ const { submitBom } = await esmock("./index.js", {
456
+ got: { default: gotStub },
457
+ });
458
+
459
+ await submitBom(
460
+ {
461
+ serverUrl: "https://dtrack.example.com",
462
+ projectName: "cdxgen-test-project",
463
+ apiKey: "TEST_API_KEY\r\n",
464
+ },
465
+ { bom: "test6" },
466
+ );
467
+
468
+ assert.equal(gotStub.callCount, 2);
469
+ const [, postOptions] = gotStub.secondCall.args;
470
+ assert.equal(postOptions.method, "POST");
471
+ assert.equal(postOptions.followRedirect, false);
472
+ assert.equal(postOptions.headers["X-Api-Key"], "TEST_API_KEY");
473
+ });
322
474
  });
323
475
 
324
476
  describe("createCocoaBom()", () => {
@@ -645,6 +797,103 @@ describe("CLI tests", () => {
645
797
  rmSync(tempDir, { recursive: true, force: true });
646
798
  }
647
799
  });
800
+
801
+ it("treats an existing local directory as a staged rootfs for docker scans", async () => {
802
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-rootfs-"));
803
+ const exportImage = sinon.stub().resolves(undefined);
804
+ const getPkgPathList = sinon.stub().returns([]);
805
+ try {
806
+ const { createBom: createBomMocked } = await esmock("./index.js", {
807
+ "../managers/binary.js": {
808
+ executeOsQuery: sinon.stub(),
809
+ getBinaryBom: sinon.stub(),
810
+ getDotnetSlices: sinon.stub(),
811
+ getOSPackages: sinon.stub().resolves({
812
+ allTypes: [],
813
+ binPaths: [],
814
+ bundledRuntimes: [],
815
+ bundledSdks: [],
816
+ dependenciesList: [],
817
+ executables: [],
818
+ osPackages: [],
819
+ sharedLibs: [],
820
+ }),
821
+ },
822
+ "../managers/docker.js": {
823
+ addSkippedSrcFiles: sinon.stub(),
824
+ exportArchive: sinon.stub(),
825
+ exportImage,
826
+ getPkgPathList,
827
+ parseImageName: sinon.stub(),
828
+ },
829
+ });
830
+ const bomNSData = await createBomMocked(tempDir, {
831
+ failOnError: true,
832
+ installDeps: false,
833
+ multiProject: false,
834
+ projectType: ["docker"],
835
+ specVersion: 1.6,
836
+ });
837
+ sinon.assert.notCalled(exportImage);
838
+ sinon.assert.calledOnce(getPkgPathList);
839
+ assert.ok(bomNSData?.bomJson);
840
+ assert.strictEqual(bomNSData?.parentComponent?.type, "container");
841
+ } finally {
842
+ rmSync(tempDir, { recursive: true, force: true });
843
+ }
844
+ });
845
+
846
+ it("prefers an all-layers subdirectory when scanning staged rootfs inputs", async () => {
847
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-rootfs-"));
848
+ const allLayersDir = join(tempDir, "all-layers");
849
+ const exportImage = sinon.stub().resolves(undefined);
850
+ const getPkgPathList = sinon.stub().returns([]);
851
+ mkdirSync(allLayersDir);
852
+ try {
853
+ const { createBom: createBomMocked } = await esmock("./index.js", {
854
+ "../managers/binary.js": {
855
+ executeOsQuery: sinon.stub(),
856
+ getBinaryBom: sinon.stub(),
857
+ getDotnetSlices: sinon.stub(),
858
+ getOSPackages: sinon.stub().resolves({
859
+ allTypes: [],
860
+ binPaths: [],
861
+ bundledRuntimes: [],
862
+ bundledSdks: [],
863
+ dependenciesList: [],
864
+ executables: [],
865
+ osPackages: [],
866
+ sharedLibs: [],
867
+ }),
868
+ },
869
+ "../managers/docker.js": {
870
+ addSkippedSrcFiles: sinon.stub(),
871
+ exportArchive: sinon.stub(),
872
+ exportImage,
873
+ getPkgPathList,
874
+ parseImageName: sinon.stub(),
875
+ },
876
+ });
877
+ await createBomMocked(tempDir, {
878
+ failOnError: true,
879
+ installDeps: false,
880
+ multiProject: false,
881
+ projectType: ["docker"],
882
+ specVersion: 1.6,
883
+ });
884
+ sinon.assert.calledOnce(getPkgPathList);
885
+ assert.strictEqual(
886
+ getPkgPathList.firstCall.args[0].allLayersDir,
887
+ tempDir,
888
+ );
889
+ assert.strictEqual(
890
+ getPkgPathList.firstCall.args[0].allLayersExplodedDir,
891
+ allLayersDir,
892
+ );
893
+ } finally {
894
+ rmSync(tempDir, { recursive: true, force: true });
895
+ }
896
+ });
648
897
  });
649
898
 
650
899
  describe("createBom() cargo cache support", () => {
@@ -839,6 +1088,102 @@ checksum = "${"a".repeat(64)}"
839
1088
  });
840
1089
  });
841
1090
 
1091
+ describe("createBom() Collider lock support", () => {
1092
+ it("preserves Collider integrity metadata and dependency nodes in the BOM", async () => {
1093
+ const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-collider-"));
1094
+ writeFileSync(
1095
+ join(tmpDir, "collider.lock"),
1096
+ JSON.stringify(
1097
+ {
1098
+ version: 1,
1099
+ dependencies: {
1100
+ fmt: {
1101
+ version: "11.0.2",
1102
+ wrap_hash: `sha256:${"a".repeat(64)}`,
1103
+ origin: "https://packages.example.com/collider/v2/",
1104
+ },
1105
+ },
1106
+ packages: {
1107
+ fast_float: {
1108
+ version: "8.0.2",
1109
+ wrap_hash: `sha256:${"b".repeat(64)}`,
1110
+ origin: "https://wrapdb.mesonbuild.com/v2/",
1111
+ },
1112
+ },
1113
+ },
1114
+ null,
1115
+ 2,
1116
+ ),
1117
+ );
1118
+ try {
1119
+ const bomNSData = await createBom(tmpDir, {
1120
+ failOnError: true,
1121
+ installDeps: false,
1122
+ multiProject: false,
1123
+ projectType: ["collider"],
1124
+ specVersion: 1.7,
1125
+ });
1126
+ const bomJson = bomNSData?.bomJson || {};
1127
+ const fmtComponent = (bomJson.components || []).find(
1128
+ (component) => component.name === "fmt",
1129
+ );
1130
+ const transitiveComponent = (bomJson.components || []).find(
1131
+ (component) => component.name === "fast_float",
1132
+ );
1133
+ assert.ok(fmtComponent);
1134
+ assert.ok(transitiveComponent);
1135
+ assert.deepStrictEqual(
1136
+ getProp(fmtComponent, "cdx:collider:origin"),
1137
+ "https://packages.example.com/collider/v2/",
1138
+ );
1139
+ assert.deepStrictEqual(
1140
+ getProp(fmtComponent, "cdx:collider:hasWrapHash"),
1141
+ "true",
1142
+ );
1143
+ assert.deepStrictEqual(
1144
+ getProp(transitiveComponent, "cdx:collider:dependencyKind"),
1145
+ "transitive",
1146
+ );
1147
+ assert.deepStrictEqual(fmtComponent.hashes, [
1148
+ {
1149
+ alg: "SHA-256",
1150
+ content: "a".repeat(64),
1151
+ },
1152
+ ]);
1153
+ assert.deepStrictEqual(fmtComponent.externalReferences, [
1154
+ {
1155
+ type: "distribution",
1156
+ url: "https://packages.example.com/collider/v2/",
1157
+ },
1158
+ ]);
1159
+ const parentDependency = (bomJson.dependencies || []).find(
1160
+ (dependency) =>
1161
+ dependency.ref === bomJson.metadata.component["bom-ref"],
1162
+ );
1163
+ assert.ok(parentDependency);
1164
+ assert.deepStrictEqual(parentDependency.dependsOn, [
1165
+ "pkg:generic/fmt@11.0.2",
1166
+ ]);
1167
+ assert.ok(
1168
+ (bomJson.dependencies || []).some(
1169
+ (dependency) =>
1170
+ dependency.ref === "pkg:generic/fmt@11.0.2" &&
1171
+ dependency.dependsOn.length === 0,
1172
+ ),
1173
+ );
1174
+ assert.ok(
1175
+ (bomJson.dependencies || []).some(
1176
+ (dependency) =>
1177
+ dependency.ref === "pkg:generic/fast_float@8.0.2" &&
1178
+ dependency.dependsOn.length === 0,
1179
+ ),
1180
+ );
1181
+ } finally {
1182
+ rmSync(tmpDir, { force: true, recursive: true });
1183
+ }
1184
+ });
1185
+ });
1186
+
842
1187
  describe("createBom() MCP inventory support", () => {
843
1188
  it("catalogs MCP services, primitives, and audit findings for JavaScript projects", async () => {
844
1189
  const options = {
@@ -902,6 +1247,32 @@ checksum = "${"a".repeat(64)}"
902
1247
  assert.ok(findings.some((finding) => finding.ruleId === "MCP-003"));
903
1248
  });
904
1249
 
1250
+ it("supports the ai-inventory audit category alias for MCP discovery", async () => {
1251
+ const options = {
1252
+ bomAudit: true,
1253
+ bomAuditCategories: "ai-inventory",
1254
+ bomAuditMinSeverity: "low",
1255
+ failOnError: true,
1256
+ installDeps: false,
1257
+ multiProject: false,
1258
+ projectType: ["js"],
1259
+ specVersion: 1.7,
1260
+ };
1261
+ const bomNSData = await createBom(mcpFixtureDir, options);
1262
+ const processedBomNSData = postProcess(bomNSData, options, mcpFixtureDir);
1263
+ const bomJson = processedBomNSData?.bomJson || {};
1264
+ assert.ok(
1265
+ (bomJson.services || []).some(
1266
+ (service) => service.name === "unsafe-http-server",
1267
+ ),
1268
+ );
1269
+ const findings = await auditBom(bomJson, {
1270
+ bomAuditCategories: "ai-inventory",
1271
+ bomAuditMinSeverity: "low",
1272
+ });
1273
+ assert.ok(findings.some((finding) => finding.ruleId === "MCP-001"));
1274
+ });
1275
+
905
1276
  it("supports the dedicated mcp project type alias", async () => {
906
1277
  const options = {
907
1278
  bomAudit: false,
@@ -927,5 +1298,507 @@ checksum = "${"a".repeat(64)}"
927
1298
  ),
928
1299
  );
929
1300
  });
1301
+
1302
+ it("flags disabled setup caches for npm, Python, and Cargo fixtures", async () => {
1303
+ const options = {
1304
+ bomAudit: true,
1305
+ bomAuditCategories: "ci-permission",
1306
+ bomAuditMinSeverity: "low",
1307
+ failOnError: true,
1308
+ includeFormulation: true,
1309
+ installDeps: false,
1310
+ multiProject: true,
1311
+ projectType: ["js", "python", "cargo", "github"],
1312
+ specVersion: 1.7,
1313
+ };
1314
+ const bomNSData = await createBom(cacheDisableFixtureDir, options);
1315
+ const processedBomNSData = postProcess(
1316
+ bomNSData,
1317
+ options,
1318
+ cacheDisableFixtureDir,
1319
+ );
1320
+ const bomJson = processedBomNSData?.bomJson || {};
1321
+ const setupNodeComponent = (bomJson.components || []).find(
1322
+ (component) =>
1323
+ getProp(component, "cdx:github:action:uses") ===
1324
+ "actions/setup-node@v4",
1325
+ );
1326
+ const setupPythonComponent = (bomJson.components || []).find(
1327
+ (component) =>
1328
+ getProp(component, "cdx:github:action:uses") ===
1329
+ "actions/setup-python@v5",
1330
+ );
1331
+ const setupRustComponent = (bomJson.components || []).find(
1332
+ (component) =>
1333
+ getProp(component, "cdx:github:action:uses") ===
1334
+ "moonrepo/setup-rust@v1",
1335
+ );
1336
+ const npmComponent = (bomJson.components || []).find((component) =>
1337
+ component.purl?.startsWith("pkg:npm/left-pad@1.3.0"),
1338
+ );
1339
+ const pythonComponent = (bomJson.components || []).find((component) =>
1340
+ component.purl?.startsWith("pkg:pypi/anyio@4.6.0"),
1341
+ );
1342
+ const cargoComponent = (bomJson.components || []).find(
1343
+ (component) =>
1344
+ component.name === "git-crate" &&
1345
+ getProp(component, "cdx:cargo:git") ===
1346
+ "https://github.com/acme/git-crate.git",
1347
+ );
1348
+ const cargoRunComponent = (bomJson.components || []).find((component) =>
1349
+ component.properties?.some(
1350
+ (property) =>
1351
+ property.name === "cdx:github:step:cargoSubcommands" &&
1352
+ property.value === "build",
1353
+ ),
1354
+ );
1355
+ assert.ok(setupNodeComponent, "expected setup-node workflow component");
1356
+ assert.ok(
1357
+ setupPythonComponent,
1358
+ "expected setup-python workflow component",
1359
+ );
1360
+ assert.ok(setupRustComponent, "expected setup-rust workflow component");
1361
+ assert.strictEqual(
1362
+ getProp(setupNodeComponent, "cdx:github:action:disablesBuildCache"),
1363
+ "true",
1364
+ );
1365
+ assert.strictEqual(
1366
+ getProp(setupPythonComponent, "cdx:github:action:disablesBuildCache"),
1367
+ "true",
1368
+ );
1369
+ assert.strictEqual(
1370
+ getProp(setupRustComponent, "cdx:github:action:disablesBuildCache"),
1371
+ "true",
1372
+ );
1373
+ assert.strictEqual(
1374
+ getProp(setupRustComponent, "cdx:github:action:buildCacheEcosystem"),
1375
+ "cargo",
1376
+ );
1377
+ assert.strictEqual(
1378
+ getProp(setupRustComponent, "cdx:github:action:buildCacheDisableInput"),
1379
+ "cache",
1380
+ );
1381
+ assert.ok(npmComponent, "expected npm dependency from package-lock");
1382
+ assert.ok(pythonComponent, "expected PyPI dependency from uv.lock");
1383
+ assert.ok(cargoComponent, "expected Cargo dependency from Cargo.toml");
1384
+ assert.ok(cargoRunComponent, "expected Cargo run step component");
1385
+ assert.strictEqual(
1386
+ getProp(npmComponent, "cdx:npm:manifestSourceType"),
1387
+ "url",
1388
+ );
1389
+ assert.strictEqual(
1390
+ getProp(pythonComponent, "cdx:pypi:manifestSourceType"),
1391
+ "url",
1392
+ );
1393
+ assert.strictEqual(
1394
+ getProp(cargoComponent, "cdx:cargo:git"),
1395
+ "https://github.com/acme/git-crate.git",
1396
+ );
1397
+ assert.strictEqual(
1398
+ getProp(cargoComponent, "cdx:cargo:gitBranch"),
1399
+ "main",
1400
+ );
1401
+ assert.strictEqual(
1402
+ getProp(cargoRunComponent, "cdx:github:step:usesCargo"),
1403
+ "true",
1404
+ );
1405
+
1406
+ const findings = await auditBom(bomJson, {
1407
+ bomAuditCategories: "ci-permission",
1408
+ bomAuditMinSeverity: "low",
1409
+ });
1410
+ assert.ok(
1411
+ findings.some((finding) => finding.ruleId === "CI-022"),
1412
+ "expected npm disabled cache finding",
1413
+ );
1414
+ assert.ok(
1415
+ findings.some((finding) => finding.ruleId === "CI-023"),
1416
+ "expected Python disabled cache finding",
1417
+ );
1418
+ assert.ok(
1419
+ findings.some((finding) => finding.ruleId === "CI-024"),
1420
+ "expected Cargo disabled cache finding",
1421
+ );
1422
+ });
1423
+
1424
+ it("supports exact AI skill scans and js exclude-type filtering for AI inventory", async () => {
1425
+ const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-ai-inventory-"));
1426
+ mkdirSync(join(tmpDir, ".claude", "skills", "release"), {
1427
+ recursive: true,
1428
+ });
1429
+ mkdirSync(join(tmpDir, ".vscode"), { recursive: true });
1430
+ writeFileSync(
1431
+ join(tmpDir, "package.json"),
1432
+ JSON.stringify(
1433
+ {
1434
+ dependencies: {
1435
+ "left-pad": "1.3.0",
1436
+ },
1437
+ name: "ai-inventory-demo",
1438
+ version: "1.0.0",
1439
+ },
1440
+ null,
1441
+ 2,
1442
+ ),
1443
+ );
1444
+ writeFileSync(
1445
+ join(tmpDir, "package-lock.json"),
1446
+ JSON.stringify(
1447
+ {
1448
+ lockfileVersion: 3,
1449
+ name: "ai-inventory-demo",
1450
+ packages: {
1451
+ "": {
1452
+ dependencies: {
1453
+ "left-pad": "1.3.0",
1454
+ },
1455
+ name: "ai-inventory-demo",
1456
+ version: "1.0.0",
1457
+ },
1458
+ "node_modules/left-pad": {
1459
+ resolved:
1460
+ "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
1461
+ version: "1.3.0",
1462
+ },
1463
+ },
1464
+ requires: true,
1465
+ version: "1.0.0",
1466
+ },
1467
+ null,
1468
+ 2,
1469
+ ),
1470
+ );
1471
+ writeFileSync(
1472
+ join(tmpDir, "CLAUDE.md"),
1473
+ "Use the release skill before publishing artifacts.",
1474
+ );
1475
+ writeFileSync(
1476
+ join(tmpDir, ".claude", "skills", "release", "SKILL.md"),
1477
+ [
1478
+ "---",
1479
+ "name: release",
1480
+ "description: Prepare release artifacts",
1481
+ "---",
1482
+ "Use this skill before shipping.",
1483
+ ].join("\n"),
1484
+ );
1485
+ writeFileSync(
1486
+ join(tmpDir, ".vscode", "mcp.json"),
1487
+ JSON.stringify(
1488
+ {
1489
+ mcpServers: {
1490
+ releaseDocs: {
1491
+ endpoint: "https://example.com/mcp",
1492
+ transport: "http",
1493
+ },
1494
+ },
1495
+ },
1496
+ null,
1497
+ 2,
1498
+ ),
1499
+ );
1500
+ writeFileSync(
1501
+ join(tmpDir, "pyproject.toml"),
1502
+ [
1503
+ "[project]",
1504
+ 'name = "demo-python-app"',
1505
+ 'version = "0.1.0"',
1506
+ 'requires-python = ">=3.10"',
1507
+ ].join("\n"),
1508
+ );
1509
+ writeFileSync(
1510
+ join(tmpDir, "server.py"),
1511
+ [
1512
+ "import mcp.server.stdio",
1513
+ "import mcp.types as mtypes",
1514
+ "from mcp.server import Server",
1515
+ "",
1516
+ 'server = Server("python-release-docs", version="0.2.0")',
1517
+ "",
1518
+ "@server.list_tools()",
1519
+ "async def handle_list_tools():",
1520
+ ' return [mtypes.Tool(name="summarize_vulns", description="Summarize vulns", inputSchema={"type": "object"})]',
1521
+ "",
1522
+ "async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):",
1523
+ " await server.run(read_stream, write_stream, None)",
1524
+ ].join("\n"),
1525
+ );
1526
+ try {
1527
+ const baseOptions = {
1528
+ installDeps: false,
1529
+ multiProject: false,
1530
+ specVersion: 1.7,
1531
+ };
1532
+ const jsOptions = {
1533
+ ...baseOptions,
1534
+ projectType: ["js"],
1535
+ };
1536
+ const jsBomJson = postProcess(
1537
+ await createBom(tmpDir, jsOptions),
1538
+ jsOptions,
1539
+ tmpDir,
1540
+ ).bomJson;
1541
+ assert.ok(
1542
+ (jsBomJson.components || []).some(
1543
+ (component) =>
1544
+ getProp(component, "cdx:file:kind") === "skill-file" &&
1545
+ getProp(component, "cdx:skill:name") === "release",
1546
+ ),
1547
+ "expected skill file in js scan",
1548
+ );
1549
+ assert.ok(
1550
+ (jsBomJson.components || []).some(
1551
+ (component) =>
1552
+ component.name === "CLAUDE.md" &&
1553
+ getProp(component, "cdx:file:kind") === "agent-instructions",
1554
+ ),
1555
+ "expected CLAUDE.md in js scan",
1556
+ );
1557
+ assert.ok(
1558
+ (jsBomJson.components || []).some(
1559
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1560
+ ),
1561
+ "expected MCP config in js scan",
1562
+ );
1563
+ assert.ok(
1564
+ (jsBomJson.services || []).some(
1565
+ (service) =>
1566
+ service.name === "releaseDocs" &&
1567
+ getProp(service, "cdx:mcp:inventorySource") === "config-file",
1568
+ ),
1569
+ "expected MCP config service in js scan",
1570
+ );
1571
+
1572
+ const dockerOptions = {
1573
+ ...baseOptions,
1574
+ projectType: ["js", "docker"],
1575
+ };
1576
+ const dockerBomJson = postProcess(
1577
+ await createNodejsBom(tmpDir, dockerOptions),
1578
+ dockerOptions,
1579
+ tmpDir,
1580
+ ).bomJson;
1581
+ assert.ok(
1582
+ (dockerBomJson.components || []).some(
1583
+ (component) =>
1584
+ getProp(component, "cdx:file:kind") === "skill-file" &&
1585
+ getProp(component, "cdx:skill:name") === "release",
1586
+ ),
1587
+ "expected skill file in docker js scan",
1588
+ );
1589
+ assert.ok(
1590
+ (dockerBomJson.components || []).some(
1591
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1592
+ ),
1593
+ "expected MCP config in docker js scan",
1594
+ );
1595
+
1596
+ const exactAiSkillOptions = {
1597
+ ...baseOptions,
1598
+ projectType: ["ai-skill"],
1599
+ };
1600
+ const aiSkillBomJson = postProcess(
1601
+ await createBom(tmpDir, exactAiSkillOptions),
1602
+ exactAiSkillOptions,
1603
+ tmpDir,
1604
+ ).bomJson;
1605
+ assert.ok(
1606
+ (aiSkillBomJson.components || []).some(
1607
+ (component) =>
1608
+ component.name === "CLAUDE.md" &&
1609
+ getProp(component, "cdx:file:kind") === "agent-instructions",
1610
+ ),
1611
+ "expected CLAUDE.md in exact ai-skill scan",
1612
+ );
1613
+ assert.ok(
1614
+ !(aiSkillBomJson.components || []).some(
1615
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1616
+ ),
1617
+ "did not expect MCP configs in exact ai-skill scan",
1618
+ );
1619
+
1620
+ const filteredOptions = {
1621
+ ...baseOptions,
1622
+ excludeType: ["ai-skill", "mcp"],
1623
+ projectType: ["js"],
1624
+ };
1625
+ const filteredBomJson = postProcess(
1626
+ await createBom(tmpDir, filteredOptions),
1627
+ filteredOptions,
1628
+ tmpDir,
1629
+ ).bomJson;
1630
+ assert.ok(
1631
+ !(filteredBomJson.components || []).some((component) =>
1632
+ ["agent-instructions", "mcp-config", "skill-file"].includes(
1633
+ getProp(component, "cdx:file:kind"),
1634
+ ),
1635
+ ),
1636
+ "did not expect AI inventory components after exclude-type filtering",
1637
+ );
1638
+ assert.ok(
1639
+ !(filteredBomJson.services || []).some((service) =>
1640
+ service.properties?.some((property) =>
1641
+ property.name.startsWith("cdx:mcp:"),
1642
+ ),
1643
+ ),
1644
+ "did not expect MCP services after exclude-type filtering",
1645
+ );
1646
+
1647
+ const pyOptions = {
1648
+ ...baseOptions,
1649
+ projectType: ["py"],
1650
+ };
1651
+ const pyBomJson = postProcess(
1652
+ await createBom(tmpDir, pyOptions),
1653
+ pyOptions,
1654
+ tmpDir,
1655
+ ).bomJson;
1656
+ assert.ok(
1657
+ (pyBomJson.components || []).some(
1658
+ (component) =>
1659
+ getProp(component, "cdx:file:kind") === "skill-file" &&
1660
+ getProp(component, "cdx:skill:name") === "release",
1661
+ ),
1662
+ "expected skill file in python scan",
1663
+ );
1664
+ assert.ok(
1665
+ (pyBomJson.components || []).some(
1666
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1667
+ ),
1668
+ "expected MCP config in python scan",
1669
+ );
1670
+ assert.ok(
1671
+ (pyBomJson.services || []).some(
1672
+ (service) =>
1673
+ service.name === "python-release-docs" &&
1674
+ getProp(service, "cdx:mcp:inventorySource") ===
1675
+ "source-code-analysis",
1676
+ ),
1677
+ "expected Python MCP service in python scan",
1678
+ );
1679
+ } finally {
1680
+ rmSync(tmpDir, { force: true, recursive: true });
1681
+ }
1682
+ });
1683
+ });
1684
+
1685
+ describe("node_modules multi-ecosystem filtering", () => {
1686
+ it("ignores composer manifests in node_modules during mixed npm/php scans", () => {
1687
+ const tmpDir = createComposerNodeModulesFixture();
1688
+ try {
1689
+ const bomData = createPHPBom(tmpDir, {
1690
+ installDeps: false,
1691
+ multiProject: true,
1692
+ projectType: ["js", "php"],
1693
+ specVersion: 1.7,
1694
+ });
1695
+ assert.deepStrictEqual(bomData, {});
1696
+ } finally {
1697
+ rmSync(tmpDir, { force: true, recursive: true });
1698
+ }
1699
+ });
1700
+
1701
+ it("still allows explicit php scans to inspect composer manifests in node_modules", () => {
1702
+ const tmpDir = createComposerNodeModulesFixture();
1703
+ try {
1704
+ const bomData = createPHPBom(tmpDir, {
1705
+ installDeps: false,
1706
+ multiProject: true,
1707
+ projectType: ["php"],
1708
+ specVersion: 1.7,
1709
+ });
1710
+ assert.ok(bomData?.bomJson?.components?.length);
1711
+ } finally {
1712
+ rmSync(tmpDir, { force: true, recursive: true });
1713
+ }
1714
+ });
1715
+
1716
+ it("still allows direct php scans without projectType to inspect composer manifests in node_modules", () => {
1717
+ const tmpDir = createComposerNodeModulesFixture();
1718
+ try {
1719
+ const bomData = createPHPBom(tmpDir, {
1720
+ installDeps: false,
1721
+ multiProject: true,
1722
+ specVersion: 1.7,
1723
+ });
1724
+ assert.ok(bomData?.bomJson?.components?.length);
1725
+ } finally {
1726
+ rmSync(tmpDir, { force: true, recursive: true });
1727
+ }
1728
+ });
1729
+
1730
+ it("still allows explicit php alias combinations to inspect composer manifests in node_modules", () => {
1731
+ const tmpDir = createComposerNodeModulesFixture();
1732
+ try {
1733
+ const bomData = createPHPBom(tmpDir, {
1734
+ installDeps: false,
1735
+ multiProject: true,
1736
+ projectType: ["php", "composer"],
1737
+ specVersion: 1.7,
1738
+ });
1739
+ assert.ok(bomData?.bomJson?.components?.length);
1740
+ } finally {
1741
+ rmSync(tmpDir, { force: true, recursive: true });
1742
+ }
1743
+ });
1744
+
1745
+ it("ignores jar artifacts in node_modules during mixed npm/jar scans", async () => {
1746
+ const tmpDir = createJarNodeModulesFixture();
1747
+ try {
1748
+ const createJarBom = await loadStubbedCreateJarBom();
1749
+ const bomData = await createJarBom(tmpDir, {
1750
+ multiProject: true,
1751
+ projectType: ["js", "jar"],
1752
+ specVersion: 1.7,
1753
+ });
1754
+ assert.strictEqual(bomData?.bomJson?.components?.length || 0, 0);
1755
+ } finally {
1756
+ rmSync(tmpDir, { force: true, recursive: true });
1757
+ }
1758
+ });
1759
+
1760
+ it("still allows explicit jar scans to inspect node_modules artifacts", async () => {
1761
+ const tmpDir = createJarNodeModulesFixture();
1762
+ try {
1763
+ const createJarBom = await loadStubbedCreateJarBom();
1764
+ const bomData = await createJarBom(tmpDir, {
1765
+ multiProject: true,
1766
+ projectType: ["jar"],
1767
+ specVersion: 1.7,
1768
+ });
1769
+ assert.ok(bomData?.bomJson?.components?.length);
1770
+ } finally {
1771
+ rmSync(tmpDir, { force: true, recursive: true });
1772
+ }
1773
+ });
1774
+
1775
+ it("still allows direct jar scans without projectType to inspect node_modules artifacts", async () => {
1776
+ const tmpDir = createJarNodeModulesFixture();
1777
+ try {
1778
+ const createJarBom = await loadStubbedCreateJarBom();
1779
+ const bomData = await createJarBom(tmpDir, {
1780
+ multiProject: true,
1781
+ specVersion: 1.7,
1782
+ });
1783
+ assert.ok(bomData?.bomJson?.components?.length);
1784
+ } finally {
1785
+ rmSync(tmpDir, { force: true, recursive: true });
1786
+ }
1787
+ });
1788
+
1789
+ it("still allows explicit jar alias combinations to inspect node_modules artifacts", async () => {
1790
+ const tmpDir = createJarNodeModulesFixture();
1791
+ try {
1792
+ const createJarBom = await loadStubbedCreateJarBom();
1793
+ const bomData = await createJarBom(tmpDir, {
1794
+ multiProject: true,
1795
+ projectType: ["jar", "war"],
1796
+ specVersion: 1.7,
1797
+ });
1798
+ assert.ok(bomData?.bomJson?.components?.length);
1799
+ } finally {
1800
+ rmSync(tmpDir, { force: true, recursive: true });
1801
+ }
1802
+ });
930
1803
  });
931
1804
  });