@cyclonedx/cdxgen 12.3.2 → 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/data/rules/ci-permissions.yaml +132 -0
- package/data/rules/dependency-sources.yaml +65 -5
- package/data/rules/package-integrity.yaml +22 -0
- package/lib/cli/index.js +141 -39
- package/lib/cli/index.poku.js +579 -1
- package/lib/helpers/agentFormulationParser.js +6 -2
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/analyzer.js +38 -9
- package/lib/helpers/analyzer.poku.js +67 -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 +45 -22
- package/lib/helpers/display.poku.js +47 -60
- package/lib/helpers/mcpConfigParser.js +21 -5
- package/lib/helpers/mcpConfigParser.poku.js +39 -2
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/utils.js +951 -40
- package/lib/helpers/utils.poku.js +882 -0
- 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/auditBom.poku.js +644 -2
- package/package.json +2 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- 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.map +1 -1
- 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/mcpConfigParser.d.ts +1 -1
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
- 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 +29 -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/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
|
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
createBom,
|
|
21
22
|
createChromeExtensionBom,
|
|
22
23
|
createNodejsBom,
|
|
24
|
+
createPHPBom,
|
|
23
25
|
createRustBom,
|
|
24
26
|
} from "./index.js";
|
|
25
27
|
|
|
@@ -58,12 +60,115 @@ const mcpFixtureDir = join(
|
|
|
58
60
|
"data",
|
|
59
61
|
"mcp-repotest",
|
|
60
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)), "..", "..");
|
|
61
79
|
|
|
62
80
|
function getProp(obj, name) {
|
|
63
81
|
return obj?.properties?.find((property) => property.name === name)?.value;
|
|
64
82
|
}
|
|
65
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
|
+
}
|
|
151
|
+
|
|
66
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
|
+
|
|
67
172
|
describe("submitBom()", () => {
|
|
68
173
|
it("should report blocked Dependency-Track submission during dry-run", async () => {
|
|
69
174
|
const recordActivity = sinon.stub();
|
|
@@ -146,6 +251,7 @@ describe("CLI tests", () => {
|
|
|
146
251
|
|
|
147
252
|
assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
|
|
148
253
|
assert.equal(options.method, "PUT");
|
|
254
|
+
assert.equal(options.followRedirect, false);
|
|
149
255
|
assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
|
|
150
256
|
assert.equal(options.headers["X-Api-Key"], apiKey);
|
|
151
257
|
assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
|
|
@@ -206,6 +312,7 @@ describe("CLI tests", () => {
|
|
|
206
312
|
// Assert call arguments against expectations
|
|
207
313
|
assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
|
|
208
314
|
assert.equal(options.method, "PUT");
|
|
315
|
+
assert.equal(options.followRedirect, false);
|
|
209
316
|
assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
|
|
210
317
|
assert.equal(options.headers["X-Api-Key"], apiKey);
|
|
211
318
|
assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
|
|
@@ -262,6 +369,7 @@ describe("CLI tests", () => {
|
|
|
262
369
|
|
|
263
370
|
assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
|
|
264
371
|
assert.equal(options.method, "PUT");
|
|
372
|
+
assert.equal(options.followRedirect, false);
|
|
265
373
|
assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
|
|
266
374
|
assert.equal(options.headers["X-Api-Key"], apiKey);
|
|
267
375
|
assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
|
|
@@ -328,6 +436,41 @@ describe("CLI tests", () => {
|
|
|
328
436
|
assert.equal(response, undefined);
|
|
329
437
|
sinon.assert.notCalled(gotStub);
|
|
330
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
|
+
});
|
|
331
474
|
});
|
|
332
475
|
|
|
333
476
|
describe("createCocoaBom()", () => {
|
|
@@ -654,6 +797,103 @@ describe("CLI tests", () => {
|
|
|
654
797
|
rmSync(tempDir, { recursive: true, force: true });
|
|
655
798
|
}
|
|
656
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
|
+
});
|
|
657
897
|
});
|
|
658
898
|
|
|
659
899
|
describe("createBom() cargo cache support", () => {
|
|
@@ -848,6 +1088,102 @@ checksum = "${"a".repeat(64)}"
|
|
|
848
1088
|
});
|
|
849
1089
|
});
|
|
850
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
|
+
|
|
851
1187
|
describe("createBom() MCP inventory support", () => {
|
|
852
1188
|
it("catalogs MCP services, primitives, and audit findings for JavaScript projects", async () => {
|
|
853
1189
|
const options = {
|
|
@@ -963,6 +1299,128 @@ checksum = "${"a".repeat(64)}"
|
|
|
963
1299
|
);
|
|
964
1300
|
});
|
|
965
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
|
+
|
|
966
1424
|
it("supports exact AI skill scans and js exclude-type filtering for AI inventory", async () => {
|
|
967
1425
|
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-ai-inventory-"));
|
|
968
1426
|
mkdirSync(join(tmpDir, ".claude", "skills", "release"), {
|
|
@@ -1223,4 +1681,124 @@ checksum = "${"a".repeat(64)}"
|
|
|
1223
1681
|
}
|
|
1224
1682
|
});
|
|
1225
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
|
+
});
|
|
1803
|
+
});
|
|
1226
1804
|
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
providerNamesForText,
|
|
8
8
|
sanitizeMcpRefToken,
|
|
9
9
|
} from "./mcpDiscovery.js";
|
|
10
|
+
import { sanitizeBomUrl } from "./propertySanitizer.js";
|
|
10
11
|
import { scanTextForHiddenUnicode } from "./unicodeScan.js";
|
|
11
12
|
|
|
12
13
|
const AGENT_FILE_PATTERNS = [
|
|
@@ -125,7 +126,7 @@ function buildInferredMcpServices(filePath, mcpUrls, authHints, providerNames) {
|
|
|
125
126
|
"bom-ref": `urn:service:agent-mcp:${sanitizeMcpRefToken(hostname || basename(filePath))}:${index + 1}`,
|
|
126
127
|
group: "mcp",
|
|
127
128
|
name: hostname || `${basename(filePath)}-mcp-endpoint`,
|
|
128
|
-
endpoints: [urlValue],
|
|
129
|
+
endpoints: [sanitizeBomUrl(urlValue)],
|
|
129
130
|
properties,
|
|
130
131
|
version: "inferred",
|
|
131
132
|
};
|
|
@@ -233,10 +234,13 @@ export const agentFormulationParser = {
|
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
if (mcpUrls.length) {
|
|
237
|
+
const sanitizedMcpUrls = mcpUrls.map((urlValue) =>
|
|
238
|
+
sanitizeBomUrl(urlValue),
|
|
239
|
+
);
|
|
236
240
|
properties.push(
|
|
237
241
|
{
|
|
238
242
|
name: "cdx:agent:hiddenMcpUrls",
|
|
239
|
-
value:
|
|
243
|
+
value: sanitizedMcpUrls.join(","),
|
|
240
244
|
},
|
|
241
245
|
{
|
|
242
246
|
name: "cdx:agent:hiddenMcpHosts",
|