@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.
- package/README.md +6 -0
- package/bin/cdxgen.js +1 -2
- package/data/rules/ai-agent-governance.yaml +43 -0
- package/data/rules/ci-permissions.yaml +132 -0
- package/data/rules/dependency-sources.yaml +65 -5
- package/data/rules/mcp-servers.yaml +36 -2
- package/data/rules/package-integrity.yaml +22 -0
- package/lib/cli/index.js +436 -56
- package/lib/cli/index.poku.js +875 -2
- package/lib/helpers/agentFormulationParser.js +10 -3
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/aiInventory.js +262 -0
- package/lib/helpers/aiInventory.poku.js +111 -0
- package/lib/helpers/analyzer.js +413 -54
- package/lib/helpers/analyzer.poku.js +117 -0
- package/lib/helpers/auditCategories.js +76 -0
- package/lib/helpers/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +50 -24
- package/lib/helpers/display.poku.js +70 -58
- package/lib/helpers/formulationParsers.js +26 -6
- package/lib/helpers/jsonLike.js +21 -20
- package/lib/helpers/jsonLike.poku.js +34 -0
- package/lib/helpers/mcpConfigParser.js +32 -16
- package/lib/helpers/mcpConfigParser.poku.js +104 -0
- package/lib/helpers/mcpDiscovery.js +13 -23
- package/lib/helpers/mcpDiscovery.poku.js +21 -0
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/utils.js +953 -41
- package/lib/helpers/utils.poku.js +901 -1
- package/lib/managers/binary.js +16 -0
- package/lib/managers/binary.poku.js +1 -0
- package/lib/managers/docker.js +240 -16
- package/lib/managers/docker.poku.js +1142 -2
- package/lib/server/server.js +7 -4
- package/lib/server/server.poku.js +36 -1
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +15 -0
- package/lib/stages/postgen/auditBom.js +12 -6
- package/lib/stages/postgen/auditBom.poku.js +755 -6
- package/lib/stages/postgen/postgen.js +229 -6
- package/lib/stages/postgen/postgen.poku.js +180 -0
- package/package.json +2 -1
- package/types/lib/cli/index.d.ts +1 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/aiInventory.d.ts +23 -0
- package/types/lib/helpers/aiInventory.d.ts.map +1 -0
- package/types/lib/helpers/analyzer.d.ts +5 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/auditCategories.d.ts +12 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/jsonLike.d.ts +4 -0
- package/types/lib/helpers/jsonLike.d.ts.map +1 -0
- package/types/lib/helpers/mcp.d.ts +29 -0
- package/types/lib/helpers/mcp.d.ts.map +1 -0
- package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
- package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
- package/types/lib/helpers/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +31 -0
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
package/lib/cli/index.poku.js
CHANGED
|
@@ -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 {
|
|
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
|
});
|