@cyclonedx/cdxgen 12.3.0 → 12.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/bin/audit.js +7 -0
- package/bin/cdxgen.js +241 -81
- package/bin/repl.js +138 -0
- package/data/rules/ai-agent-governance.yaml +249 -0
- package/data/rules/dependency-sources.yaml +41 -0
- package/data/rules/mcp-servers.yaml +304 -0
- package/data/rules/package-integrity.yaml +123 -0
- package/lib/audit/index.js +353 -29
- package/lib/audit/index.poku.js +247 -7
- package/lib/audit/reporters.js +26 -0
- package/lib/audit/scoring.js +262 -13
- package/lib/audit/scoring.poku.js +179 -0
- package/lib/audit/targets.js +391 -2
- package/lib/audit/targets.poku.js +416 -3
- package/lib/cli/index.js +588 -45
- package/lib/cli/index.poku.js +735 -1
- package/lib/evinser/evinser.js +8 -5
- package/lib/helpers/agentFormulationParser.js +318 -0
- package/lib/helpers/aiInventory.js +262 -0
- package/lib/helpers/aiInventory.poku.js +111 -0
- package/lib/helpers/analyzer.js +1769 -0
- package/lib/helpers/analyzer.poku.js +284 -3
- package/lib/helpers/auditCategories.js +76 -0
- package/lib/helpers/ciParsers/githubActions.js +140 -16
- package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
- package/lib/helpers/communityAiConfigParser.js +672 -0
- package/lib/helpers/communityAiConfigParser.poku.js +63 -0
- package/lib/helpers/depsUtils.js +108 -0
- package/lib/helpers/depsUtils.poku.js +72 -1
- package/lib/helpers/display.js +325 -3
- package/lib/helpers/display.poku.js +301 -0
- package/lib/helpers/formulationParsers.js +28 -0
- package/lib/helpers/formulationParsers.poku.js +504 -1
- package/lib/helpers/jsonLike.js +102 -0
- package/lib/helpers/jsonLike.poku.js +34 -0
- package/lib/helpers/mcp.js +248 -0
- package/lib/helpers/mcp.poku.js +101 -0
- package/lib/helpers/mcpConfigParser.js +656 -0
- package/lib/helpers/mcpConfigParser.poku.js +126 -0
- package/lib/helpers/mcpDiscovery.js +84 -0
- package/lib/helpers/mcpDiscovery.poku.js +21 -0
- package/lib/helpers/protobom.js +3 -3
- package/lib/helpers/provenanceUtils.js +29 -4
- package/lib/helpers/provenanceUtils.poku.js +29 -3
- package/lib/helpers/registryProvenance.js +210 -0
- package/lib/helpers/registryProvenance.poku.js +144 -0
- package/lib/helpers/rustFormulationParser.js +330 -0
- package/lib/helpers/source.js +21 -2
- package/lib/helpers/source.poku.js +38 -0
- package/lib/helpers/utils.js +1331 -83
- package/lib/helpers/utils.poku.js +599 -188
- package/lib/helpers/vsixutils.js +12 -4
- package/lib/helpers/vsixutils.poku.js +34 -0
- package/lib/managers/binary.js +36 -12
- package/lib/managers/binary.poku.js +68 -0
- package/lib/managers/docker.js +59 -9
- package/lib/managers/docker.poku.js +61 -0
- package/lib/managers/piptree.js +12 -7
- package/lib/managers/piptree.poku.js +44 -0
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +15 -0
- package/lib/stages/postgen/auditBom.js +20 -6
- package/lib/stages/postgen/auditBom.poku.js +694 -1
- package/lib/stages/postgen/postgen.js +262 -11
- package/lib/stages/postgen/postgen.poku.js +306 -2
- package/lib/stages/postgen/ruleEngine.js +49 -1
- package/lib/stages/postgen/spdxConverter.poku.js +70 -0
- package/lib/stages/pregen/pregen.js +6 -4
- package/package.json +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts +12 -0
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -8
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.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 +10 -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/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 +8 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +17 -1
- 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/provenanceUtils.d.ts +5 -3
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
- package/types/lib/helpers/registryProvenance.d.ts +9 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
- package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
- package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +31 -1
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/piptree.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/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
package/lib/cli/index.poku.js
CHANGED
|
@@ -14,7 +14,14 @@ import esmock from "esmock";
|
|
|
14
14
|
import { assert, describe, it } from "poku";
|
|
15
15
|
import sinon from "sinon";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { auditBom } from "../stages/postgen/auditBom.js";
|
|
18
|
+
import { postProcess } from "../stages/postgen/postgen.js";
|
|
19
|
+
import {
|
|
20
|
+
createBom,
|
|
21
|
+
createChromeExtensionBom,
|
|
22
|
+
createNodejsBom,
|
|
23
|
+
createRustBom,
|
|
24
|
+
} from "./index.js";
|
|
18
25
|
|
|
19
26
|
const fixtureDir = join(
|
|
20
27
|
dirname(fileURLToPath(import.meta.url)),
|
|
@@ -24,9 +31,70 @@ const fixtureDir = join(
|
|
|
24
31
|
"data",
|
|
25
32
|
"chrome-extensions",
|
|
26
33
|
);
|
|
34
|
+
const cargoFixtureDir = join(
|
|
35
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
36
|
+
"..",
|
|
37
|
+
"..",
|
|
38
|
+
"test",
|
|
39
|
+
"data",
|
|
40
|
+
"cargo-workspace-repotest",
|
|
41
|
+
);
|
|
42
|
+
const cargoCacheFixtureDir = join(
|
|
43
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
44
|
+
"..",
|
|
45
|
+
"..",
|
|
46
|
+
"test",
|
|
47
|
+
"data",
|
|
48
|
+
"cargo-cache-fixture",
|
|
49
|
+
"registry",
|
|
50
|
+
"cache",
|
|
51
|
+
"index.crates.io-1949cf8c6b5b557f",
|
|
52
|
+
);
|
|
53
|
+
const mcpFixtureDir = join(
|
|
54
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
55
|
+
"..",
|
|
56
|
+
"..",
|
|
57
|
+
"test",
|
|
58
|
+
"data",
|
|
59
|
+
"mcp-repotest",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
function getProp(obj, name) {
|
|
63
|
+
return obj?.properties?.find((property) => property.name === name)?.value;
|
|
64
|
+
}
|
|
27
65
|
|
|
28
66
|
describe("CLI tests", () => {
|
|
29
67
|
describe("submitBom()", () => {
|
|
68
|
+
it("should report blocked Dependency-Track submission during dry-run", async () => {
|
|
69
|
+
const recordActivity = sinon.stub();
|
|
70
|
+
const actualUtils = await import("../helpers/utils.js");
|
|
71
|
+
const { submitBom } = await esmock("./index.js", {
|
|
72
|
+
"../helpers/utils.js": {
|
|
73
|
+
...actualUtils,
|
|
74
|
+
isDryRun: true,
|
|
75
|
+
recordActivity,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const response = await submitBom(
|
|
80
|
+
{
|
|
81
|
+
apiKey: "TEST_API_KEY",
|
|
82
|
+
projectId: "f7cb9f02-8041-4991-9101-b01fa07a6522",
|
|
83
|
+
projectName: "cdxgen-test-project",
|
|
84
|
+
projectVersion: "1.0.0",
|
|
85
|
+
serverUrl: "https://dtrack.example.com",
|
|
86
|
+
},
|
|
87
|
+
{ bom: "test" },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(response, undefined);
|
|
91
|
+
sinon.assert.calledWithMatch(recordActivity, {
|
|
92
|
+
kind: "network",
|
|
93
|
+
status: "blocked",
|
|
94
|
+
target: sinon.match("https://dtrack.example.com"),
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
30
98
|
it("should successfully report the SBOM with given project id, name, version and a single tag", async () => {
|
|
31
99
|
const fakeGotResponse = {
|
|
32
100
|
json: sinon.stub().resolves({ success: true }),
|
|
@@ -488,5 +556,671 @@ describe("CLI tests", () => {
|
|
|
488
556
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
489
557
|
}
|
|
490
558
|
});
|
|
559
|
+
|
|
560
|
+
it("records the specific create*Bom project type for multi-type dry-run activities", async () => {
|
|
561
|
+
let currentActivityContext = {};
|
|
562
|
+
const recordedActivities = [];
|
|
563
|
+
const actualUtils = await import("../helpers/utils.js");
|
|
564
|
+
const { createBom: createBomMocked } = await esmock("./index.js", {
|
|
565
|
+
"../helpers/utils.js": {
|
|
566
|
+
...actualUtils,
|
|
567
|
+
recordActivity: (activity) => {
|
|
568
|
+
recordedActivities.push({
|
|
569
|
+
packageType: currentActivityContext.packageType,
|
|
570
|
+
projectType: currentActivityContext.projectType,
|
|
571
|
+
sourcePath: currentActivityContext.sourcePath,
|
|
572
|
+
...activity,
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
resetActivityContext: () => {
|
|
576
|
+
currentActivityContext = {};
|
|
577
|
+
},
|
|
578
|
+
setActivityContext: (context = {}) => {
|
|
579
|
+
currentActivityContext = {
|
|
580
|
+
...currentActivityContext,
|
|
581
|
+
...context,
|
|
582
|
+
};
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
await createBomMocked(cargoFixtureDir, {
|
|
587
|
+
installDeps: false,
|
|
588
|
+
multiProject: true,
|
|
589
|
+
projectType: ["cargo", "github"],
|
|
590
|
+
specVersion: 1.7,
|
|
591
|
+
});
|
|
592
|
+
const activities = recordedActivities.filter(
|
|
593
|
+
(activity) =>
|
|
594
|
+
activity.kind === "read" &&
|
|
595
|
+
["cargo", "github"].includes(activity.packageType),
|
|
596
|
+
);
|
|
597
|
+
const cargoActivity = activities.find(
|
|
598
|
+
(activity) => activity.packageType === "cargo",
|
|
599
|
+
);
|
|
600
|
+
const githubActivity = activities.find(
|
|
601
|
+
(activity) => activity.packageType === "github",
|
|
602
|
+
);
|
|
603
|
+
assert.strictEqual(cargoActivity?.projectType, "rust");
|
|
604
|
+
assert.strictEqual(githubActivity?.projectType, "github");
|
|
605
|
+
assert.ok(
|
|
606
|
+
activities.every((activity) => activity.projectType !== "cargo,github"),
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("records the python source directory as the activity target when no metadata filename is available", async () => {
|
|
611
|
+
let currentActivityContext = {};
|
|
612
|
+
const recordedActivities = [];
|
|
613
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-python-activity-"));
|
|
614
|
+
const requirementsFile = join(tempDir, "requirements.txt");
|
|
615
|
+
const actualUtils = await import("../helpers/utils.js");
|
|
616
|
+
try {
|
|
617
|
+
writeFileSync(requirementsFile, "flask==3.1.0\n", "utf-8");
|
|
618
|
+
const { createBom: createBomMocked } = await esmock("./index.js", {
|
|
619
|
+
"../helpers/utils.js": {
|
|
620
|
+
...actualUtils,
|
|
621
|
+
recordActivity: (activity) => {
|
|
622
|
+
recordedActivities.push({
|
|
623
|
+
packageType: currentActivityContext.packageType,
|
|
624
|
+
projectType: currentActivityContext.projectType,
|
|
625
|
+
sourcePath: currentActivityContext.sourcePath,
|
|
626
|
+
...activity,
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
resetActivityContext: () => {
|
|
630
|
+
currentActivityContext = {};
|
|
631
|
+
},
|
|
632
|
+
setActivityContext: (context = {}) => {
|
|
633
|
+
currentActivityContext = {
|
|
634
|
+
...currentActivityContext,
|
|
635
|
+
...context,
|
|
636
|
+
};
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
await createBomMocked(tempDir, {
|
|
641
|
+
installDeps: false,
|
|
642
|
+
multiProject: false,
|
|
643
|
+
projectType: ["python"],
|
|
644
|
+
specVersion: 1.7,
|
|
645
|
+
});
|
|
646
|
+
const pythonActivity = recordedActivities.find(
|
|
647
|
+
(activity) =>
|
|
648
|
+
activity.kind === "read" && activity.packageType === "pypi",
|
|
649
|
+
);
|
|
650
|
+
assert.strictEqual(pythonActivity?.projectType, "python");
|
|
651
|
+
assert.strictEqual(pythonActivity?.sourcePath, tempDir);
|
|
652
|
+
assert.strictEqual(pythonActivity?.target, tempDir);
|
|
653
|
+
} finally {
|
|
654
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
describe("createBom() cargo cache support", () => {
|
|
660
|
+
it("catalogs cached cargo crate archives via the cargo-cache project type", async () => {
|
|
661
|
+
const originalCargoCacheDir = process.env.CARGO_CACHE_DIR;
|
|
662
|
+
try {
|
|
663
|
+
process.env.CARGO_CACHE_DIR = cargoCacheFixtureDir;
|
|
664
|
+
const bomNSData = await createBom(cargoCacheFixtureDir, {
|
|
665
|
+
deep: false,
|
|
666
|
+
failOnError: true,
|
|
667
|
+
installDeps: false,
|
|
668
|
+
multiProject: false,
|
|
669
|
+
projectType: ["cargo-cache"],
|
|
670
|
+
specVersion: 1.6,
|
|
671
|
+
});
|
|
672
|
+
const bomJson = bomNSData?.bomJson || {};
|
|
673
|
+
const components = bomJson.components || [];
|
|
674
|
+
const serdeComponent = components.find(
|
|
675
|
+
(component) => component.name === "serde",
|
|
676
|
+
);
|
|
677
|
+
assert.ok(serdeComponent);
|
|
678
|
+
assert.strictEqual(serdeComponent.version, "1.0.217");
|
|
679
|
+
assert.strictEqual(
|
|
680
|
+
serdeComponent.properties.find(
|
|
681
|
+
(property) => property.name === "cdx:cargo:cacheSource",
|
|
682
|
+
)?.value,
|
|
683
|
+
"registry-cache",
|
|
684
|
+
);
|
|
685
|
+
} finally {
|
|
686
|
+
if (originalCargoCacheDir === undefined) {
|
|
687
|
+
delete process.env.CARGO_CACHE_DIR;
|
|
688
|
+
} else {
|
|
689
|
+
process.env.CARGO_CACHE_DIR = originalCargoCacheDir;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("creates a Cargo workspace BOM with workflow signals and matching audit findings", async () => {
|
|
695
|
+
const options = {
|
|
696
|
+
bomAudit: true,
|
|
697
|
+
bomAuditCategories: "package-integrity",
|
|
698
|
+
bomAuditMinSeverity: "low",
|
|
699
|
+
failOnError: true,
|
|
700
|
+
includeFormulation: true,
|
|
701
|
+
installDeps: false,
|
|
702
|
+
multiProject: true,
|
|
703
|
+
projectType: ["cargo", "github"],
|
|
704
|
+
specVersion: 1.7,
|
|
705
|
+
};
|
|
706
|
+
const bomNSData = await createBom(cargoFixtureDir, options);
|
|
707
|
+
const processedBomNSData = postProcess(
|
|
708
|
+
bomNSData,
|
|
709
|
+
options,
|
|
710
|
+
cargoFixtureDir,
|
|
711
|
+
);
|
|
712
|
+
const bomJson = processedBomNSData?.bomJson || {};
|
|
713
|
+
const coreComponent = (bomJson.components || []).find(
|
|
714
|
+
(component) =>
|
|
715
|
+
component.name === "core" &&
|
|
716
|
+
component.properties?.some(
|
|
717
|
+
(property) =>
|
|
718
|
+
property.name === "cdx:cargo:workspaceDependencyResolved" &&
|
|
719
|
+
property.value === "true",
|
|
720
|
+
),
|
|
721
|
+
);
|
|
722
|
+
const buildHelperComponent = (bomJson.components || []).find(
|
|
723
|
+
(component) =>
|
|
724
|
+
component.name === "build-helper" &&
|
|
725
|
+
component.properties?.some(
|
|
726
|
+
(property) =>
|
|
727
|
+
property.name === "cdx:cargo:workspaceDependencyResolved" &&
|
|
728
|
+
property.value === "true",
|
|
729
|
+
),
|
|
730
|
+
);
|
|
731
|
+
const cargoToolchainComponent = (bomJson.components || []).find(
|
|
732
|
+
(component) =>
|
|
733
|
+
component.properties?.some(
|
|
734
|
+
(property) =>
|
|
735
|
+
property.name === "cdx:github:action:role" &&
|
|
736
|
+
property.value === "toolchain",
|
|
737
|
+
),
|
|
738
|
+
);
|
|
739
|
+
const cargoRunComponent = (bomJson.components || []).find((component) =>
|
|
740
|
+
component.properties?.some(
|
|
741
|
+
(property) =>
|
|
742
|
+
property.name === "cdx:github:step:usesCargo" &&
|
|
743
|
+
property.value === "true",
|
|
744
|
+
),
|
|
745
|
+
);
|
|
746
|
+
assert.strictEqual(
|
|
747
|
+
coreComponent?.properties?.find(
|
|
748
|
+
(property) =>
|
|
749
|
+
property.name === "cdx:cargo:workspaceDependencyResolved",
|
|
750
|
+
)?.value,
|
|
751
|
+
"true",
|
|
752
|
+
);
|
|
753
|
+
assert.strictEqual(
|
|
754
|
+
buildHelperComponent?.properties?.find(
|
|
755
|
+
(property) => property.name === "cdx:cargo:dependencyKind",
|
|
756
|
+
)?.value,
|
|
757
|
+
"build",
|
|
758
|
+
);
|
|
759
|
+
assert.strictEqual(
|
|
760
|
+
buildHelperComponent?.properties?.find(
|
|
761
|
+
(property) => property.name === "cdx:cargo:resolvedWorkspaceMember",
|
|
762
|
+
)?.value,
|
|
763
|
+
"build-helper",
|
|
764
|
+
);
|
|
765
|
+
assert.strictEqual(
|
|
766
|
+
cargoToolchainComponent?.properties?.find(
|
|
767
|
+
(property) => property.name === "cdx:github:action:ecosystem",
|
|
768
|
+
)?.value,
|
|
769
|
+
"cargo",
|
|
770
|
+
);
|
|
771
|
+
assert.strictEqual(
|
|
772
|
+
cargoRunComponent?.properties?.find(
|
|
773
|
+
(property) => property.name === "cdx:github:step:cargoSubcommands",
|
|
774
|
+
)?.value,
|
|
775
|
+
"build,test",
|
|
776
|
+
);
|
|
777
|
+
const findings = await auditBom(bomJson, {
|
|
778
|
+
bomAuditCategories: "package-integrity",
|
|
779
|
+
bomAuditMinSeverity: "low",
|
|
780
|
+
});
|
|
781
|
+
assert.ok(findings.some((finding) => finding.ruleId === "INT-012"));
|
|
782
|
+
assert.ok(findings.some((finding) => finding.ruleId === "INT-013"));
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("nests only manifest package components under the Rust parent component", async () => {
|
|
786
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-rust-parent-"));
|
|
787
|
+
const helperDir = join(tmpDir, "crates", "helper");
|
|
788
|
+
mkdirSync(helperDir, { recursive: true });
|
|
789
|
+
writeFileSync(
|
|
790
|
+
join(tmpDir, "Cargo.toml"),
|
|
791
|
+
`[package]
|
|
792
|
+
name = "demo-app"
|
|
793
|
+
version = "1.0.0"
|
|
794
|
+
|
|
795
|
+
[workspace]
|
|
796
|
+
members = ["crates/helper"]
|
|
797
|
+
|
|
798
|
+
[dependencies]
|
|
799
|
+
helper = { path = "crates/helper" }
|
|
800
|
+
serde = "1.0.0"
|
|
801
|
+
`,
|
|
802
|
+
);
|
|
803
|
+
writeFileSync(
|
|
804
|
+
join(helperDir, "Cargo.toml"),
|
|
805
|
+
`[package]
|
|
806
|
+
name = "helper"
|
|
807
|
+
version = "0.1.0"
|
|
808
|
+
|
|
809
|
+
[dependencies]
|
|
810
|
+
serde = "1.0.0"
|
|
811
|
+
`,
|
|
812
|
+
);
|
|
813
|
+
writeFileSync(
|
|
814
|
+
join(tmpDir, "Cargo.lock"),
|
|
815
|
+
`version = 3
|
|
816
|
+
|
|
817
|
+
[[package]]
|
|
818
|
+
name = "demo-app"
|
|
819
|
+
version = "1.0.0"
|
|
820
|
+
dependencies = ["helper", "serde"]
|
|
821
|
+
|
|
822
|
+
[[package]]
|
|
823
|
+
name = "helper"
|
|
824
|
+
version = "0.1.0"
|
|
825
|
+
dependencies = ["serde"]
|
|
826
|
+
|
|
827
|
+
[[package]]
|
|
828
|
+
name = "serde"
|
|
829
|
+
version = "1.0.0"
|
|
830
|
+
checksum = "${"a".repeat(64)}"
|
|
831
|
+
`,
|
|
832
|
+
);
|
|
833
|
+
try {
|
|
834
|
+
const bomData = await createRustBom(tmpDir, {
|
|
835
|
+
installDeps: false,
|
|
836
|
+
multiProject: true,
|
|
837
|
+
specVersion: 1.7,
|
|
838
|
+
});
|
|
839
|
+
const parentComponent = bomData.parentComponent;
|
|
840
|
+
const nestedComponentNames = parentComponent.components.map(
|
|
841
|
+
(component) => component.name,
|
|
842
|
+
);
|
|
843
|
+
assert.strictEqual(parentComponent.name, "demo-app");
|
|
844
|
+
assert.deepStrictEqual(nestedComponentNames, ["helper"]);
|
|
845
|
+
} finally {
|
|
846
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
describe("createBom() MCP inventory support", () => {
|
|
852
|
+
it("catalogs MCP services, primitives, and audit findings for JavaScript projects", async () => {
|
|
853
|
+
const options = {
|
|
854
|
+
bomAudit: true,
|
|
855
|
+
bomAuditCategories: "mcp-server",
|
|
856
|
+
bomAuditMinSeverity: "low",
|
|
857
|
+
failOnError: true,
|
|
858
|
+
installDeps: false,
|
|
859
|
+
multiProject: false,
|
|
860
|
+
projectType: ["js"],
|
|
861
|
+
specVersion: 1.7,
|
|
862
|
+
};
|
|
863
|
+
const bomNSData = await createBom(mcpFixtureDir, options);
|
|
864
|
+
const processedBomNSData = postProcess(bomNSData, options, mcpFixtureDir);
|
|
865
|
+
const bomJson = processedBomNSData?.bomJson || {};
|
|
866
|
+
const officialSdk = (bomJson.components || []).find(
|
|
867
|
+
(component) =>
|
|
868
|
+
component.purl ===
|
|
869
|
+
"pkg:npm/%40modelcontextprotocol/server@2.0.0-alpha.0",
|
|
870
|
+
);
|
|
871
|
+
const wrapperSdk = (bomJson.components || []).find(
|
|
872
|
+
(component) => component.purl === "pkg:npm/%40acme/mcp-server@0.1.0",
|
|
873
|
+
);
|
|
874
|
+
assert.ok(officialSdk);
|
|
875
|
+
assert.ok(
|
|
876
|
+
officialSdk.tags?.includes("official-mcp-sdk"),
|
|
877
|
+
"expected official MCP SDK tags",
|
|
878
|
+
);
|
|
879
|
+
assert.ok(wrapperSdk);
|
|
880
|
+
assert.ok(
|
|
881
|
+
wrapperSdk.properties?.some(
|
|
882
|
+
(property) =>
|
|
883
|
+
property.name === "cdx:mcp:official" && property.value === "false",
|
|
884
|
+
),
|
|
885
|
+
"expected non-official MCP wrapper signal",
|
|
886
|
+
);
|
|
887
|
+
assert.strictEqual((bomJson.services || []).length, 2);
|
|
888
|
+
const unsafeService = (bomJson.services || []).find(
|
|
889
|
+
(service) => service.name === "unsafe-http-server",
|
|
890
|
+
);
|
|
891
|
+
const authService = (bomJson.services || []).find(
|
|
892
|
+
(service) => service.name === "auth-http-server",
|
|
893
|
+
);
|
|
894
|
+
assert.ok(unsafeService);
|
|
895
|
+
assert.strictEqual(unsafeService.authenticated, false);
|
|
896
|
+
assert.ok(authService);
|
|
897
|
+
assert.strictEqual(authService.authenticated, true);
|
|
898
|
+
assert.ok(
|
|
899
|
+
(bomJson.dependencies || []).some(
|
|
900
|
+
(dependency) =>
|
|
901
|
+
dependency.ref === unsafeService["bom-ref"] &&
|
|
902
|
+
dependency.provides.length >= 1,
|
|
903
|
+
),
|
|
904
|
+
);
|
|
905
|
+
const findings = await auditBom(bomJson, {
|
|
906
|
+
bomAuditCategories: "mcp-server",
|
|
907
|
+
bomAuditMinSeverity: "low",
|
|
908
|
+
});
|
|
909
|
+
assert.ok(findings.some((finding) => finding.ruleId === "MCP-001"));
|
|
910
|
+
assert.ok(findings.some((finding) => finding.ruleId === "MCP-002"));
|
|
911
|
+
assert.ok(findings.some((finding) => finding.ruleId === "MCP-003"));
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it("supports the ai-inventory audit category alias for MCP discovery", async () => {
|
|
915
|
+
const options = {
|
|
916
|
+
bomAudit: true,
|
|
917
|
+
bomAuditCategories: "ai-inventory",
|
|
918
|
+
bomAuditMinSeverity: "low",
|
|
919
|
+
failOnError: true,
|
|
920
|
+
installDeps: false,
|
|
921
|
+
multiProject: false,
|
|
922
|
+
projectType: ["js"],
|
|
923
|
+
specVersion: 1.7,
|
|
924
|
+
};
|
|
925
|
+
const bomNSData = await createBom(mcpFixtureDir, options);
|
|
926
|
+
const processedBomNSData = postProcess(bomNSData, options, mcpFixtureDir);
|
|
927
|
+
const bomJson = processedBomNSData?.bomJson || {};
|
|
928
|
+
assert.ok(
|
|
929
|
+
(bomJson.services || []).some(
|
|
930
|
+
(service) => service.name === "unsafe-http-server",
|
|
931
|
+
),
|
|
932
|
+
);
|
|
933
|
+
const findings = await auditBom(bomJson, {
|
|
934
|
+
bomAuditCategories: "ai-inventory",
|
|
935
|
+
bomAuditMinSeverity: "low",
|
|
936
|
+
});
|
|
937
|
+
assert.ok(findings.some((finding) => finding.ruleId === "MCP-001"));
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it("supports the dedicated mcp project type alias", async () => {
|
|
941
|
+
const options = {
|
|
942
|
+
bomAudit: false,
|
|
943
|
+
failOnError: true,
|
|
944
|
+
installDeps: false,
|
|
945
|
+
multiProject: false,
|
|
946
|
+
projectType: ["mcp"],
|
|
947
|
+
specVersion: 1.7,
|
|
948
|
+
};
|
|
949
|
+
const bomNSData = await createBom(mcpFixtureDir, options);
|
|
950
|
+
const processedBomNSData = postProcess(bomNSData, options, mcpFixtureDir);
|
|
951
|
+
const bomJson = processedBomNSData?.bomJson || {};
|
|
952
|
+
assert.ok(
|
|
953
|
+
(bomJson.services || []).some(
|
|
954
|
+
(service) => service.name === "unsafe-http-server",
|
|
955
|
+
),
|
|
956
|
+
);
|
|
957
|
+
assert.ok(
|
|
958
|
+
(bomJson.components || []).some(
|
|
959
|
+
(component) =>
|
|
960
|
+
component.purl ===
|
|
961
|
+
"pkg:npm/%40modelcontextprotocol/server@2.0.0-alpha.0",
|
|
962
|
+
),
|
|
963
|
+
);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it("supports exact AI skill scans and js exclude-type filtering for AI inventory", async () => {
|
|
967
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-ai-inventory-"));
|
|
968
|
+
mkdirSync(join(tmpDir, ".claude", "skills", "release"), {
|
|
969
|
+
recursive: true,
|
|
970
|
+
});
|
|
971
|
+
mkdirSync(join(tmpDir, ".vscode"), { recursive: true });
|
|
972
|
+
writeFileSync(
|
|
973
|
+
join(tmpDir, "package.json"),
|
|
974
|
+
JSON.stringify(
|
|
975
|
+
{
|
|
976
|
+
dependencies: {
|
|
977
|
+
"left-pad": "1.3.0",
|
|
978
|
+
},
|
|
979
|
+
name: "ai-inventory-demo",
|
|
980
|
+
version: "1.0.0",
|
|
981
|
+
},
|
|
982
|
+
null,
|
|
983
|
+
2,
|
|
984
|
+
),
|
|
985
|
+
);
|
|
986
|
+
writeFileSync(
|
|
987
|
+
join(tmpDir, "package-lock.json"),
|
|
988
|
+
JSON.stringify(
|
|
989
|
+
{
|
|
990
|
+
lockfileVersion: 3,
|
|
991
|
+
name: "ai-inventory-demo",
|
|
992
|
+
packages: {
|
|
993
|
+
"": {
|
|
994
|
+
dependencies: {
|
|
995
|
+
"left-pad": "1.3.0",
|
|
996
|
+
},
|
|
997
|
+
name: "ai-inventory-demo",
|
|
998
|
+
version: "1.0.0",
|
|
999
|
+
},
|
|
1000
|
+
"node_modules/left-pad": {
|
|
1001
|
+
resolved:
|
|
1002
|
+
"https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
|
|
1003
|
+
version: "1.3.0",
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
requires: true,
|
|
1007
|
+
version: "1.0.0",
|
|
1008
|
+
},
|
|
1009
|
+
null,
|
|
1010
|
+
2,
|
|
1011
|
+
),
|
|
1012
|
+
);
|
|
1013
|
+
writeFileSync(
|
|
1014
|
+
join(tmpDir, "CLAUDE.md"),
|
|
1015
|
+
"Use the release skill before publishing artifacts.",
|
|
1016
|
+
);
|
|
1017
|
+
writeFileSync(
|
|
1018
|
+
join(tmpDir, ".claude", "skills", "release", "SKILL.md"),
|
|
1019
|
+
[
|
|
1020
|
+
"---",
|
|
1021
|
+
"name: release",
|
|
1022
|
+
"description: Prepare release artifacts",
|
|
1023
|
+
"---",
|
|
1024
|
+
"Use this skill before shipping.",
|
|
1025
|
+
].join("\n"),
|
|
1026
|
+
);
|
|
1027
|
+
writeFileSync(
|
|
1028
|
+
join(tmpDir, ".vscode", "mcp.json"),
|
|
1029
|
+
JSON.stringify(
|
|
1030
|
+
{
|
|
1031
|
+
mcpServers: {
|
|
1032
|
+
releaseDocs: {
|
|
1033
|
+
endpoint: "https://example.com/mcp",
|
|
1034
|
+
transport: "http",
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
null,
|
|
1039
|
+
2,
|
|
1040
|
+
),
|
|
1041
|
+
);
|
|
1042
|
+
writeFileSync(
|
|
1043
|
+
join(tmpDir, "pyproject.toml"),
|
|
1044
|
+
[
|
|
1045
|
+
"[project]",
|
|
1046
|
+
'name = "demo-python-app"',
|
|
1047
|
+
'version = "0.1.0"',
|
|
1048
|
+
'requires-python = ">=3.10"',
|
|
1049
|
+
].join("\n"),
|
|
1050
|
+
);
|
|
1051
|
+
writeFileSync(
|
|
1052
|
+
join(tmpDir, "server.py"),
|
|
1053
|
+
[
|
|
1054
|
+
"import mcp.server.stdio",
|
|
1055
|
+
"import mcp.types as mtypes",
|
|
1056
|
+
"from mcp.server import Server",
|
|
1057
|
+
"",
|
|
1058
|
+
'server = Server("python-release-docs", version="0.2.0")',
|
|
1059
|
+
"",
|
|
1060
|
+
"@server.list_tools()",
|
|
1061
|
+
"async def handle_list_tools():",
|
|
1062
|
+
' return [mtypes.Tool(name="summarize_vulns", description="Summarize vulns", inputSchema={"type": "object"})]',
|
|
1063
|
+
"",
|
|
1064
|
+
"async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):",
|
|
1065
|
+
" await server.run(read_stream, write_stream, None)",
|
|
1066
|
+
].join("\n"),
|
|
1067
|
+
);
|
|
1068
|
+
try {
|
|
1069
|
+
const baseOptions = {
|
|
1070
|
+
installDeps: false,
|
|
1071
|
+
multiProject: false,
|
|
1072
|
+
specVersion: 1.7,
|
|
1073
|
+
};
|
|
1074
|
+
const jsOptions = {
|
|
1075
|
+
...baseOptions,
|
|
1076
|
+
projectType: ["js"],
|
|
1077
|
+
};
|
|
1078
|
+
const jsBomJson = postProcess(
|
|
1079
|
+
await createBom(tmpDir, jsOptions),
|
|
1080
|
+
jsOptions,
|
|
1081
|
+
tmpDir,
|
|
1082
|
+
).bomJson;
|
|
1083
|
+
assert.ok(
|
|
1084
|
+
(jsBomJson.components || []).some(
|
|
1085
|
+
(component) =>
|
|
1086
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
1087
|
+
getProp(component, "cdx:skill:name") === "release",
|
|
1088
|
+
),
|
|
1089
|
+
"expected skill file in js scan",
|
|
1090
|
+
);
|
|
1091
|
+
assert.ok(
|
|
1092
|
+
(jsBomJson.components || []).some(
|
|
1093
|
+
(component) =>
|
|
1094
|
+
component.name === "CLAUDE.md" &&
|
|
1095
|
+
getProp(component, "cdx:file:kind") === "agent-instructions",
|
|
1096
|
+
),
|
|
1097
|
+
"expected CLAUDE.md in js scan",
|
|
1098
|
+
);
|
|
1099
|
+
assert.ok(
|
|
1100
|
+
(jsBomJson.components || []).some(
|
|
1101
|
+
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1102
|
+
),
|
|
1103
|
+
"expected MCP config in js scan",
|
|
1104
|
+
);
|
|
1105
|
+
assert.ok(
|
|
1106
|
+
(jsBomJson.services || []).some(
|
|
1107
|
+
(service) =>
|
|
1108
|
+
service.name === "releaseDocs" &&
|
|
1109
|
+
getProp(service, "cdx:mcp:inventorySource") === "config-file",
|
|
1110
|
+
),
|
|
1111
|
+
"expected MCP config service in js scan",
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
const dockerOptions = {
|
|
1115
|
+
...baseOptions,
|
|
1116
|
+
projectType: ["js", "docker"],
|
|
1117
|
+
};
|
|
1118
|
+
const dockerBomJson = postProcess(
|
|
1119
|
+
await createNodejsBom(tmpDir, dockerOptions),
|
|
1120
|
+
dockerOptions,
|
|
1121
|
+
tmpDir,
|
|
1122
|
+
).bomJson;
|
|
1123
|
+
assert.ok(
|
|
1124
|
+
(dockerBomJson.components || []).some(
|
|
1125
|
+
(component) =>
|
|
1126
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
1127
|
+
getProp(component, "cdx:skill:name") === "release",
|
|
1128
|
+
),
|
|
1129
|
+
"expected skill file in docker js scan",
|
|
1130
|
+
);
|
|
1131
|
+
assert.ok(
|
|
1132
|
+
(dockerBomJson.components || []).some(
|
|
1133
|
+
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1134
|
+
),
|
|
1135
|
+
"expected MCP config in docker js scan",
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
const exactAiSkillOptions = {
|
|
1139
|
+
...baseOptions,
|
|
1140
|
+
projectType: ["ai-skill"],
|
|
1141
|
+
};
|
|
1142
|
+
const aiSkillBomJson = postProcess(
|
|
1143
|
+
await createBom(tmpDir, exactAiSkillOptions),
|
|
1144
|
+
exactAiSkillOptions,
|
|
1145
|
+
tmpDir,
|
|
1146
|
+
).bomJson;
|
|
1147
|
+
assert.ok(
|
|
1148
|
+
(aiSkillBomJson.components || []).some(
|
|
1149
|
+
(component) =>
|
|
1150
|
+
component.name === "CLAUDE.md" &&
|
|
1151
|
+
getProp(component, "cdx:file:kind") === "agent-instructions",
|
|
1152
|
+
),
|
|
1153
|
+
"expected CLAUDE.md in exact ai-skill scan",
|
|
1154
|
+
);
|
|
1155
|
+
assert.ok(
|
|
1156
|
+
!(aiSkillBomJson.components || []).some(
|
|
1157
|
+
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1158
|
+
),
|
|
1159
|
+
"did not expect MCP configs in exact ai-skill scan",
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
const filteredOptions = {
|
|
1163
|
+
...baseOptions,
|
|
1164
|
+
excludeType: ["ai-skill", "mcp"],
|
|
1165
|
+
projectType: ["js"],
|
|
1166
|
+
};
|
|
1167
|
+
const filteredBomJson = postProcess(
|
|
1168
|
+
await createBom(tmpDir, filteredOptions),
|
|
1169
|
+
filteredOptions,
|
|
1170
|
+
tmpDir,
|
|
1171
|
+
).bomJson;
|
|
1172
|
+
assert.ok(
|
|
1173
|
+
!(filteredBomJson.components || []).some((component) =>
|
|
1174
|
+
["agent-instructions", "mcp-config", "skill-file"].includes(
|
|
1175
|
+
getProp(component, "cdx:file:kind"),
|
|
1176
|
+
),
|
|
1177
|
+
),
|
|
1178
|
+
"did not expect AI inventory components after exclude-type filtering",
|
|
1179
|
+
);
|
|
1180
|
+
assert.ok(
|
|
1181
|
+
!(filteredBomJson.services || []).some((service) =>
|
|
1182
|
+
service.properties?.some((property) =>
|
|
1183
|
+
property.name.startsWith("cdx:mcp:"),
|
|
1184
|
+
),
|
|
1185
|
+
),
|
|
1186
|
+
"did not expect MCP services after exclude-type filtering",
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
const pyOptions = {
|
|
1190
|
+
...baseOptions,
|
|
1191
|
+
projectType: ["py"],
|
|
1192
|
+
};
|
|
1193
|
+
const pyBomJson = postProcess(
|
|
1194
|
+
await createBom(tmpDir, pyOptions),
|
|
1195
|
+
pyOptions,
|
|
1196
|
+
tmpDir,
|
|
1197
|
+
).bomJson;
|
|
1198
|
+
assert.ok(
|
|
1199
|
+
(pyBomJson.components || []).some(
|
|
1200
|
+
(component) =>
|
|
1201
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
1202
|
+
getProp(component, "cdx:skill:name") === "release",
|
|
1203
|
+
),
|
|
1204
|
+
"expected skill file in python scan",
|
|
1205
|
+
);
|
|
1206
|
+
assert.ok(
|
|
1207
|
+
(pyBomJson.components || []).some(
|
|
1208
|
+
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1209
|
+
),
|
|
1210
|
+
"expected MCP config in python scan",
|
|
1211
|
+
);
|
|
1212
|
+
assert.ok(
|
|
1213
|
+
(pyBomJson.services || []).some(
|
|
1214
|
+
(service) =>
|
|
1215
|
+
service.name === "python-release-docs" &&
|
|
1216
|
+
getProp(service, "cdx:mcp:inventorySource") ===
|
|
1217
|
+
"source-code-analysis",
|
|
1218
|
+
),
|
|
1219
|
+
"expected Python MCP service in python scan",
|
|
1220
|
+
);
|
|
1221
|
+
} finally {
|
|
1222
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
491
1225
|
});
|
|
492
1226
|
});
|