UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

1,484 lines (1,338 loc) 160 kB
import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { PackageURL } from "packageurl-js"; import { assert, describe, it } from "poku"; import { githubActionsParser } from "../../helpers/ciParsers/githubActions.js"; import { createLolbasProperties } from "../../helpers/lolbas.js"; import { auditBom, formatAnnotations, formatDryRunSupportSummary, getBomAuditDryRunSupportSummary, hasCriticalFindings, } from "./auditBom.js"; import { evaluateRule, evaluateRules, loadRules } from "./ruleEngine.js"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); const RULES_DIR = join(__dirname, "..", "..", "..", "data", "rules"); const WORKFLOWS_DIR = join( __dirname, "..", "..", "..", "test", "data", "workflows", ); function makeBom( components = [], workflows = [], formulationComponents = [], services = [], ) { const formulationEntry = {}; if (formulationComponents.length) { formulationEntry.components = formulationComponents; } if (workflows.length) { formulationEntry.workflows = workflows; } return { bomFormat: "CycloneDX", specVersion: "1.6", serialNumber: "urn:uuid:test-bom", metadata: { tools: { components: [ { type: "application", name: "cdxgen", version: "11.0.0", "bom-ref": "pkg:npm/%40cyclonedx/cdxgen@11.0.0", }, ], }, component: { name: "test-project", type: "application", "bom-ref": "pkg:npm/test-project@1.0.0", }, }, components, services, formulation: workflows.length || formulationComponents.length ? [formulationEntry] : undefined, }; } function makeComponent(name, version, properties) { return { type: "library", name, version, purl: `pkg:npm/${name}@${version}`, "bom-ref": `pkg:npm/${name}@${version}`, properties: properties.map(([k, v]) => ({ name: k, value: v })), }; } function makeChromeExtensionComponent(name, version, properties) { const purl = new PackageURL( "chrome-extension", null, name, version, ).toString(); return { type: "application", name, version, purl, "bom-ref": purl, properties: properties.map(([k, v]) => ({ name: k, value: v })), }; } function makeHbomComponent(name, hardwareClass, properties = [], extra = {}) { return { type: "device", name, version: extra.version, "bom-ref": extra.bomRef || `urn:uuid:${hardwareClass}:${name}`, properties: [["cdx:hbom:hardwareClass", hardwareClass], ...properties].map( ([k, v]) => ({ name: k, value: v }), ), ...extra, }; } function makeHbomBom( components = [], metadataProperties = [], bomProperties = [], ) { return { bomFormat: "CycloneDX", specVersion: "1.7", serialNumber: "urn:uuid:test-hbom", metadata: { tools: { components: [ { type: "application", name: "cdxgen", version: "12.4.0", "bom-ref": "pkg:npm/%40cyclonedx/cdxgen@12.4.0", }, ], }, component: { name: "test-host", type: "device", "bom-ref": "urn:uuid:test-host", properties: metadataProperties.map(([k, v]) => ({ name: k, value: v })), }, }, components, properties: bomProperties.map(([k, v]) => ({ name: k, value: v })), }; } function makeBomFromWorkflowFixture(filename) { const workflowFile = join(WORKFLOWS_DIR, filename); const result = githubActionsParser.parse([workflowFile], { specVersion: 1.7, }); return makeBom([], result.workflows, result.components); } describe("loadRules", () => { it("should load built-in rules from the data/rules directory", async () => { const rules = await loadRules(RULES_DIR); assert.ok(rules.length > 0, "Should load at least one rule"); for (const rule of rules) { assert.ok(rule.id, "Each rule must have an id"); assert.ok(rule.condition, "Each rule must have a condition"); assert.ok(rule.message, "Each rule must have a message"); assert.ok( ["critical", "high", "medium", "low"].includes(rule.severity), `Rule ${rule.id} severity must be valid`, ); assert.ok( ["no", "partial", "full"].includes(rule.dryRunSupport), `Rule ${rule.id} dry-run support must be valid`, ); } }); it("should return empty array for non-existent directory", async () => { const rules = await loadRules("/tmp/non-existent-rules-dir-12345"); assert.deepStrictEqual(rules, []); }); it("should load rules with all required fields", async () => { const rules = await loadRules(RULES_DIR); const ciRules = rules.filter((r) => r.category === "ci-permission"); assert.ok(ciRules.length > 0, "Should have CI permission rules"); const depRules = rules.filter((r) => r.category === "dependency-source"); assert.ok(depRules.length > 0, "Should have dependency source rules"); const intRules = rules.filter((r) => r.category === "package-integrity"); assert.ok(intRules.length > 0, "Should have package integrity rules"); const chromeExtensionRules = rules.filter( (r) => r.category === "chrome-extension", ); assert.ok(chromeExtensionRules.length > 0, "Should have extension rules"); const containerRiskRules = rules.filter( (r) => r.category === "container-risk", ); assert.ok( containerRiskRules.length > 0, "Should have container risk rules", ); const mcpRules = rules.filter((r) => r.category === "mcp-server"); assert.ok(mcpRules.length > 0, "Should have MCP server rules"); const agentRules = rules.filter((r) => r.category === "ai-agent"); assert.ok(agentRules.length > 0, "Should have AI agent rules"); const asarRules = rules.filter((r) => r.category === "asar-archive"); assert.ok(asarRules.length > 0, "Should have ASAR archive rules"); const hbomSecurityRules = rules.filter( (r) => r.category === "hbom-security", ); assert.ok(hbomSecurityRules.length > 0, "Should have HBOM security rules"); const hbomPerformanceRules = rules.filter( (r) => r.category === "hbom-performance", ); assert.ok( hbomPerformanceRules.length > 0, "Should have HBOM performance rules", ); const hbomComplianceRules = rules.filter( (r) => r.category === "hbom-compliance", ); assert.ok( hbomComplianceRules.length > 0, "Should have HBOM compliance rules", ); const hostTopologyRules = rules.filter( (r) => r.category === "host-topology", ); assert.ok(hostTopologyRules.length > 0, "Should have host-topology rules"); }); it("should assign explicit dry-run support metadata to built-in rules", async () => { const rules = await loadRules(RULES_DIR); assert.strictEqual( rules.find((rule) => rule.id === "CI-001")?.dryRunSupport, "full", ); assert.strictEqual( rules.find((rule) => rule.id === "INT-003")?.dryRunSupport, "no", ); assert.strictEqual( rules.find((rule) => rule.id === "INT-005")?.dryRunSupport, "partial", ); assert.strictEqual( rules.find((rule) => rule.id === "HBS-001")?.dryRunSupport, "full", ); }); }); describe("evaluateRule", () => { it("should detect unpinned action with write permissions (CI-001)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "CI-001"); assert.ok(rule, "CI-001 rule should exist"); const bom = makeBom([ makeComponent("actions/setup-node", "v3", [ ["cdx:github:action:isShaPinned", "false"], ["cdx:github:workflow:hasWritePermissions", "true"], ["cdx:github:action:uses", "actions/setup-node@v3"], ["cdx:github:action:versionPinningType", "tag"], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should find unpinned action"); assert.strictEqual(findings[0].ruleId, "CI-001"); assert.strictEqual(findings[0].severity, "high"); }); it("should not flag SHA-pinned actions for CI-001", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "CI-001"); const bom = makeBom([ makeComponent("actions/setup-node", "v3", [ ["cdx:github:action:isShaPinned", "true"], ["cdx:github:workflow:hasWritePermissions", "true"], ["cdx:github:action:uses", "actions/setup-node@abc123"], ]), ]); const findings = await evaluateRule(rule, bom); assert.strictEqual( findings.length, 0, "SHA-pinned action should not trigger", ); }); it("should detect npm install script from direct manifest source (PKG-001)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-001"); assert.ok(rule, "PKG-001 rule should exist"); const bom = makeBom([ makeComponent("sketchy-pkg", "1.0.0", [ ["cdx:npm:hasInstallScript", "true"], ["cdx:npm:manifestSourceType", "git"], [ "cdx:npm:manifestSource", "git+https://github.com/acme/sketchy-pkg.git", ], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect install script risk"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect npm install scripts from url and path manifest sources for PKG-001", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-001"); assert.ok(rule, "PKG-001 rule should exist"); for (const manifestSourceType of ["url", "path"]) { const bom = makeBom([ makeComponent(`sketchy-${manifestSourceType}`, "1.0.0", [ ["cdx:npm:hasInstallScript", "true"], ["cdx:npm:manifestSourceType", manifestSourceType], ["cdx:npm:manifestSource", `${manifestSourceType}:example`], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0); } }); it("should not detect npm install script without manifest source evidence for PKG-001", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-001"); assert.ok(rule, "PKG-001 rule should exist"); const bom = makeBom([ makeComponent("registry-pkg", "1.0.0", [ ["cdx:npm:hasInstallScript", "true"], ["cdx:npm:isRegistryDependency", "false"], ]), ]); const findings = await evaluateRule(rule, bom); assert.strictEqual(findings.length, 0); }); it("should detect Collider packages from insecure HTTP origins (PKG-009)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-009"); assert.ok(rule, "PKG-009 rule should exist"); const bom = makeBom([ makeComponent("fmt", "11.0.2", [ ["cdx:collider:dependencyKind", "direct"], ["cdx:collider:origin", "http://mirror.example.com/collider/v2/"], ["cdx:collider:originScheme", "http"], ["cdx:collider:originHost", "mirror.example.com"], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect insecure Collider origin"); assert.strictEqual(findings[0].ruleId, "PKG-009"); assert.strictEqual(findings[0].severity, "medium"); }); it("should detect Collider origins that required sanitization (PKG-010)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-010"); assert.ok(rule, "PKG-010 rule should exist"); const bom = makeBom([ makeComponent("spdlog", "1.15.0", [ ["cdx:collider:dependencyKind", "direct"], ["cdx:collider:origin", "https://example.com/collider/v2/"], ["cdx:collider:originScheme", "https"], ["cdx:collider:originSanitized", "true"], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect sanitized Collider origin"); assert.strictEqual(findings[0].ruleId, "PKG-010"); assert.strictEqual(findings[0].severity, "low"); }); it("should detect python dependency from direct manifest source (PKG-011)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-011"); assert.ok(rule, "PKG-011 rule should exist"); const bom = makeBom([ makeComponent("suspicious-python-pkg", "1.0.0", [ ["cdx:pypi:manifestSourceType", "url"], [ "cdx:pypi:manifestSource", "https://example.com/suspicious-python-pkg.whl", ], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect python direct source risk"); assert.strictEqual(findings[0].ruleId, "PKG-011"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect HBOM security findings from synthetic device inventory", async () => { const bom = makeHbomBom( [ makeHbomComponent("Main SSD", "storage", [ ["cdx:hbom:isEncrypted", "false"], ["cdx:hbom:deviceSerial", "ABC123456789"], ]), makeHbomComponent("wifi0", "wireless-adapter", [ ["cdx:hbom:connected", "true"], ["cdx:hbom:securityMode", "open"], ]), makeHbomComponent("USB Stick", "storage-volume", [ ["cdx:hbom:isRemovable", "true"], ["cdx:hbom:isLocked", "false"], ]), makeHbomComponent("USB4 Dock", "bus", [ ["cdx:hbom:securityLevel", "none"], ["cdx:hbom:iommuProtection", "false"], ["cdx:hbom:policy", "auto"], ]), makeHbomComponent("LTE Modem", "modem", [ ["cdx:hbom:imei", "490154203237518"], ["cdx:hbom:ownNumbers", "+15551234567"], ]), ], [ ["cdx:hbom:platform", "linux"], ["cdx:hbom:architecture", "amd64"], ["cdx:hbom:identifierPolicy", "full"], ["cdx:hbom:serialNumber", "HOST-SERIAL-001"], ], [["cdx:hbom:collectorProfile", "linux-amd64"]], ); const findings = await auditBom(bom, { bomAuditCategories: "hbom-security", }); const ruleIds = new Set(findings.map((finding) => finding.ruleId)); assert.ok(ruleIds.has("HBS-001")); assert.ok(ruleIds.has("HBS-002")); assert.ok(ruleIds.has("HBS-003")); assert.ok(ruleIds.has("HBS-004")); assert.ok(ruleIds.has("HBS-005")); assert.ok(ruleIds.has("HBS-006")); }); it("should detect HBOM performance findings from synthetic device inventory", async () => { const bom = makeHbomBom([ makeHbomComponent("rootfs", "storage-volume", [ ["cdx:hbom:capacityBytes", "1000"], ["cdx:hbom:freeBytes", "100"], ]), makeHbomComponent("nvme0", "storage", [ ["cdx:hbom:wearPercentageUsed", "91"], ["cdx:hbom:smartStatus", "Failing"], ]), makeHbomComponent("CPU Thermal Zone", "thermal-zone", [ ["cdx:hbom:temperatureCelsius", "92"], ]), makeHbomComponent("Battery", "power", [ ["cdx:hbom:maximumCapacity", "71%"], ["cdx:hbom:cycleCount", "1204"], ["cdx:hbom:designCapacityPercent", "62"], ]), makeHbomComponent("eth0", "network-interface", [ ["cdx:hbom:operState", "up"], ["cdx:hbom:duplex", "half"], ["cdx:hbom:speedMbps", "100"], ]), makeHbomComponent("DIMM Bank", "memory", [ ["cdx:hbom:sizeBytes", "1000"], ["cdx:hbom:memoryOnlineSize", "800"], ]), makeHbomComponent("USB Camera", "usb-device", [ ["cdx:hbom:currentRequired", "900"], ["cdx:hbom:currentAvailable", "500"], ]), makeHbomComponent("LTE Modem", "modem", [ ["cdx:hbom:signalQuality", "18"], ["cdx:hbom:operatorName", "ExampleTel"], ]), ]); const findings = await auditBom(bom, { bomAuditCategories: "hbom-performance", }); const ruleIds = new Set(findings.map((finding) => finding.ruleId)); assert.ok(ruleIds.has("HBP-001")); assert.ok(ruleIds.has("HBP-002")); assert.ok(ruleIds.has("HBP-003")); assert.ok(ruleIds.has("HBP-004")); assert.ok(ruleIds.has("HBP-005")); assert.ok(ruleIds.has("HBP-006")); assert.ok(ruleIds.has("HBP-007")); assert.ok(ruleIds.has("HBP-008")); assert.ok(ruleIds.has("HBP-009")); }); it("should detect HBOM compliance findings from synthetic device inventory", async () => { const bom = makeHbomBom( [ makeHbomComponent("rootfs", "storage-volume", [ ["cdx:hbom:capacityBytes", "1000"], ]), makeHbomComponent("HDMI-A-1", "display-connector", [ ["cdx:hbom:displayConnectorType", "HDMI-A"], ]), ], [ ["cdx:hbom:platform", "linux"], ["cdx:hbom:identifierPolicy", "full"], ], [ ["cdx:hbom:collectorProfile", "linux-amd64"], ["cdx:hbom:analysis:missingCommandCount", "2"], ["cdx:hbom:analysis:missingCommands", "lspci,lsusb"], [ "cdx:hbom:analysis:missingCommandIds", "fwupdmgr-devices-json,edid-decode", ], ["cdx:hbom:analysis:installHintCount", "2"], ["cdx:hbom:analysis:permissionDeniedCount", "1"], ["cdx:hbom:analysis:permissionDeniedCommands", "drm_info"], [ "cdx:hbom:analysis:permissionDeniedIds", "dmidecode-firmware-board,drm-info-json", ], ["cdx:hbom:analysis:privilegeHintCount", "1"], ["cdx:hbom:analysis:requiresPrivileged", "true"], ], ); const findings = await auditBom(bom, { bomAuditCategories: "hbom-compliance", }); const ruleIds = new Set(findings.map((finding) => finding.ruleId)); assert.ok(ruleIds.has("HBC-001")); assert.ok(ruleIds.has("HBC-002")); assert.ok(ruleIds.has("HBC-003")); assert.ok(ruleIds.has("HBC-004")); assert.ok(ruleIds.has("HBC-005")); assert.ok(ruleIds.has("HBC-006")); assert.ok(ruleIds.has("HBC-007")); assert.ok(ruleIds.has("HBC-008")); assert.ok(ruleIds.has("HBC-009")); assert.ok(ruleIds.has("HBC-010")); }); it("should not flag redacted-by-default HBOM identifier policy as a compliance finding", async () => { const bom = makeHbomBom( [], [ ["cdx:hbom:platform", "darwin"], ["cdx:hbom:architecture", "arm64"], ["cdx:hbom:identifierPolicy", "redacted-by-default"], ["cdx:hbom:serialNumber", "redacted:serialNumber"], ], [ ["cdx:hbom:collectorProfile", "darwin-arm64"], ["cdx:hbom:evidence:commandCount", "1"], ["cdx:hbom:evidence:command", "system_profiler"], ], ); const findings = await auditBom(bom, { bomAuditCategories: "hbom-compliance", }); assert.ok( !findings.some((finding) => finding.ruleId === "HBC-005"), "redacted-by-default should not trigger HBC-005", ); }); it("should not flag HBOM collector evidence as incomplete when BOM command evidence is present", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "HBC-003"); const bom = makeHbomBom( [], [ ["cdx:hbom:platform", "darwin"], ["cdx:hbom:architecture", "arm64"], ["cdx:hbom:identifierPolicy", "redacted-by-default"], ], [ ["cdx:hbom:collectorProfile", "darwin-arm64"], ["cdx:hbom:evidence:commandCount", "2"], [ "cdx:hbom:evidence:command", "system-profiler-json|platform|/usr/sbin/system_profiler SPHardwareDataType -json", ], [ "cdx:hbom:evidence:command", "battery-status|power|/usr/bin/pmset -g batt", ], ], ); const findings = await evaluateRule(rule, bom); assert.strictEqual(findings.length, 0); }); it("should detect HBOM command diagnostics for missing utilities and permission-denied enrichments", async () => { const rules = await loadRules(RULES_DIR); const missingCommandsRule = rules.find((r) => r.id === "HBC-006"); const permissionDeniedRule = rules.find((r) => r.id === "HBC-007"); const firmwareRule = rules.find((r) => r.id === "HBC-008"); const boardRule = rules.find((r) => r.id === "HBC-009"); const displayRule = rules.find((r) => r.id === "HBC-010"); const bom = makeHbomBom( [ makeHbomComponent("eDP-1", "display-connector", [ ["cdx:hbom:displayConnectorType", "eDP"], ]), ], [ ["cdx:hbom:platform", "linux"], ["cdx:hbom:architecture", "amd64"], ], [ ["cdx:hbom:collectorProfile", "linux-amd64-v1"], ["cdx:hbom:analysis:missingCommandCount", "1"], ["cdx:hbom:analysis:missingCommands", "lsusb"], [ "cdx:hbom:analysis:missingCommandIds", "fwupdmgr-devices-json,edid-decode", ], ["cdx:hbom:analysis:permissionDeniedCount", "1"], ["cdx:hbom:analysis:permissionDeniedCommands", "drm_info"], [ "cdx:hbom:analysis:permissionDeniedIds", "dmidecode-firmware-board,drm-info-json", ], ["cdx:hbom:analysis:requiresPrivileged", "true"], ], ); const missingCommandsFindings = await evaluateRule( missingCommandsRule, bom, ); const permissionDeniedFindings = await evaluateRule( permissionDeniedRule, bom, ); const firmwareFindings = await evaluateRule(firmwareRule, bom); const boardFindings = await evaluateRule(boardRule, bom); const displayFindings = await evaluateRule(displayRule, bom); assert.strictEqual(missingCommandsFindings.length, 1); assert.strictEqual(missingCommandsFindings[0].ruleId, "HBC-006"); assert.strictEqual(permissionDeniedFindings.length, 1); assert.strictEqual(permissionDeniedFindings[0].ruleId, "HBC-007"); assert.strictEqual(firmwareFindings.length, 1); assert.strictEqual(firmwareFindings[0].ruleId, "HBC-008"); assert.strictEqual(boardFindings.length, 1); assert.strictEqual(boardFindings[0].ruleId, "HBC-009"); assert.strictEqual(displayFindings.length, 1); assert.strictEqual(displayFindings[0].ruleId, "HBC-010"); }); it("should not flag redacted HBOM identifiers for the raw-identifier exposure rule", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "HBS-004"); const bom = makeHbomBom( [ makeHbomComponent("wifi0", "network-interface", [ ["cdx:hbom:macAddress", "redacted:macAddress"], ]), ], [ ["cdx:hbom:platform", "darwin"], ["cdx:hbom:architecture", "arm64"], ["cdx:hbom:identifierPolicy", "redacted-by-default"], ["cdx:hbom:serialNumber", "redacted:serialNumber"], ["cdx:hbom:platformUuid", "redacted:platformUuid"], ], [["cdx:hbom:collectorProfile", "darwin-arm64"]], ); const findings = await evaluateRule(rule, bom); assert.strictEqual(findings.length, 0); }); it("should not flag redacted modem identifiers for the cellular exposure rule", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "HBS-006"); const bom = makeHbomBom( [ makeHbomComponent("LTE Modem", "modem", [ ["cdx:hbom:imei", "redacted:imei"], ["cdx:hbom:ownNumbers", "redacted:ownNumbers"], ]), ], [["cdx:hbom:identifierPolicy", "redacted-by-default"]], ); const findings = await evaluateRule(rule, bom); assert.strictEqual(findings.length, 0); }); it("should not flag healthy storage telemetry for the degraded-storage HBOM rule", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "HBP-002"); const bom = makeHbomBom([ makeHbomComponent("nvme0", "storage", [ ["cdx:hbom:wearPercentageUsed", "12"], ["cdx:hbom:smartStatus", "ok"], ]), ]); const findings = await evaluateRule(rule, bom); assert.strictEqual(findings.length, 0); }); it("should safely handle human-readable online memory size values for the HBOM memory rule", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "HBP-006"); const bom = makeHbomBom([ makeHbomComponent("System Memory", "memory", [ ["cdx:hbom:sizeBytes", "32899006464"], ["cdx:hbom:memoryOnlineSize", "32 GB"], ]), ]); const findings = await evaluateRule(rule, bom); assert.strictEqual(findings.length, 0); }); it("should expand the hbom alias to all HBOM audit categories", async () => { const bom = makeHbomBom( [ makeHbomComponent("Main SSD", "storage", [ ["cdx:hbom:isEncrypted", "false"], ["cdx:hbom:wearPercentageUsed", "90"], ]), ], [ ["cdx:hbom:platform", "linux"], ["cdx:hbom:identifierPolicy", "full"], ], [["cdx:hbom:collectorProfile", "linux-amd64"]], ); const findings = await auditBom(bom, { bomAuditCategories: "hbom", }); const categories = new Set(findings.map((finding) => finding.category)); assert.ok(categories.has("hbom-security")); assert.ok(categories.has("hbom-performance")); assert.ok(categories.has("hbom-compliance")); }); it("should detect Collider packages missing valid wrap hashes (INT-014)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "INT-014"); assert.ok(rule, "INT-014 rule should exist"); const bom = makeBom([ makeComponent("fast_float", "8.0.2", [ ["cdx:collider:dependencyKind", "transitive"], ["cdx:collider:hasWrapHash", "false"], ["cdx:collider:wrapHash", "not-a-sha256"], ["cdx:collider:wrapHashInvalid", "true"], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect missing Collider wrap hash"); assert.strictEqual(findings[0].ruleId, "INT-014"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect OIDC token issuance to a non-official action (CI-002)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "CI-002"); assert.ok(rule, "CI-002 rule should exist"); const bom = makeBom( [], [], [ { type: "application", name: "deploy-action", version: "v1", purl: "pkg:github/vendor/deploy-action@v1", "bom-ref": "pkg:github/vendor/deploy-action@v1", properties: [ { name: "cdx:github:action:uses", value: "vendor/deploy-action@v1", }, { name: "cdx:github:workflow:hasIdTokenWrite", value: "true" }, { name: "cdx:github:job:hasIdTokenWrite", value: "true" }, { name: "cdx:actions:isOfficial", value: "false" }, { name: "cdx:actions:isVerified", value: "false" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect third-party OIDC exposure"); assert.deepStrictEqual(findings[0].attackTechniques, ["T1528"]); }); it("should detect unauthenticated MCP tool exposure (MCP-001)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "MCP-001"); assert.ok(rule, "MCP-001 rule should exist"); const bom = makeBom( [], [], [], [ { "bom-ref": "urn:service:mcp:unsafe-http:1.0.0", name: "unsafe-http", version: "1.0.0", endpoints: ["/mcp-unsafe"], authenticated: false, properties: [ { name: "SrcFile", value: "src/unsafe.js" }, { name: "cdx:mcp:transport", value: "streamable-http" }, { name: "cdx:mcp:capabilities:tools", value: "true" }, { name: "cdx:mcp:toolCount", value: "1" }, { name: "cdx:mcp:officialSdk", value: "false" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect unauthenticated MCP tools"); assert.strictEqual(findings[0].severity, "critical"); }); it("should detect revoked Secure Boot certificates (OBOM-LNX-012)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "OBOM-LNX-012"); assert.ok(rule, "OBOM-LNX-012 rule should exist"); const bom = makeBom([ makeComponent("dbx-entry", "key-id-1", [ ["cdx:osquery:category", "secureboot_certificates"], ["revoked", "1"], ["subject", "CN=Legacy Bootloader"], ["issuer", "CN=Platform DBX"], ["serial", "42"], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect revoked Secure Boot cert"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect expiring Secure Boot certificates (OBOM-LNX-013)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "OBOM-LNX-013"); assert.ok(rule, "OBOM-LNX-013 rule should exist"); const bom = makeBom([ makeComponent("db-entry", "key-id-2", [ ["cdx:osquery:category", "secureboot_certificates"], ["not_valid_after", `${Math.floor(Date.now() / 1000) + 86400}`], ["not_valid_before", `${Math.floor(Date.now() / 1000) - 86400}`], ["subject", "CN=Current Platform Key"], ["issuer", "CN=Firmware CA"], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect expiring Secure Boot cert"); assert.strictEqual(findings[0].severity, "medium"); }); it("should detect a network-exposed non-official MCP server (MCP-003)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "MCP-003"); assert.ok(rule, "MCP-003 rule should exist"); const bom = makeBom( [], [], [], [ { "bom-ref": "urn:service:mcp:custom-wrapper:0.1.0", name: "custom-wrapper", version: "0.1.0", endpoints: ["http://localhost:4000/mcp"], authenticated: true, properties: [ { name: "SrcFile", value: "src/custom.js" }, { name: "cdx:mcp:transport", value: "streamable-http" }, { name: "cdx:mcp:officialSdk", value: "false" }, { name: "cdx:mcp:toolCount", value: "2" }, { name: "cdx:mcp:sdkImports", value: "@acme/mcp-server" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect non-official MCP wrapper"); assert.strictEqual(findings[0].severity, "medium"); }); it("should detect hidden Unicode in AI agent files (AGT-001)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "AGT-001"); assert.ok(rule, "AGT-001 rule should exist"); const bom = makeBom( [], [], [ { "bom-ref": "file:/repo/AGENTS.md", name: "AGENTS.md", type: "file", properties: [ { name: "SrcFile", value: "/repo/AGENTS.md" }, { name: "cdx:agent:inventorySource", value: "agent-file" }, { name: "cdx:file:hasHiddenUnicode", value: "true" }, { name: "cdx:file:hiddenUnicodeCodePoints", value: "U+200B" }, { name: "cdx:file:hiddenUnicodeLineNumbers", value: "4" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok( findings.length > 0, "Should detect hidden Unicode in agent file", ); assert.ok(findings[0].standards?.["owasp-ai-top-10"]?.length); }); it("should detect public MCP endpoint references in AI agent files (AGT-002)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "AGT-002"); assert.ok(rule, "AGT-002 rule should exist"); const bom = makeBom( [], [], [ { "bom-ref": "file:/repo/.github/copilot-instructions.md", name: "copilot-instructions.md", type: "file", properties: [ { name: "SrcFile", value: "/repo/.github/copilot-instructions.md", }, { name: "cdx:agent:inventorySource", value: "agent-file" }, { name: "cdx:agent:hasPublicMcpEndpoint", value: "true" }, { name: "cdx:agent:hiddenMcpUrls", value: "https://demo.ngrok-free.app/mcp", }, { name: "cdx:agent:hiddenMcpHosts", value: "demo.ngrok-free.app", }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect public MCP endpoint risk"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect undeclared MCP references in AI agent files (AGT-003)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "AGT-003"); assert.ok(rule, "AGT-003 rule should exist"); const bom = makeBom( [], [], [ { "bom-ref": "file:/repo/AGENTS.md", name: "AGENTS.md", type: "file", properties: [ { name: "SrcFile", value: "/repo/AGENTS.md" }, { name: "cdx:agent:inventorySource", value: "agent-file" }, { name: "cdx:agent:hasMcpReferences", value: "true" }, { name: "cdx:agent:mcpPackageRefs", value: "@acme/mcp-server", }, { name: "cdx:agent:hiddenMcpUrls", value: "http://localhost:3000/mcp", }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect undeclared MCP references"); }); it("should detect tunneled MCP references in AI agent files (AGT-004)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "AGT-004"); assert.ok(rule, "AGT-004 rule should exist"); const bom = makeBom( [], [], [ { "bom-ref": "file:/repo/AGENTS.md", name: "AGENTS.md", type: "file", properties: [ { name: "SrcFile", value: "/repo/AGENTS.md" }, { name: "cdx:agent:inventorySource", value: "agent-file" }, { name: "cdx:agent:hasTunnelReference", value: "true" }, { name: "cdx:agent:hiddenMcpUrls", value: "https://demo.ngrok-free.app/mcp", }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect tunnel exposure"); }); it("should detect inline credentials in AI agent files (AGT-006)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "AGT-006"); assert.ok(rule, "AGT-006 rule should exist"); const bom = makeBom( [], [], [ { "bom-ref": "file:/repo/AGENTS.md", name: "AGENTS.md", type: "file", properties: [ { name: "SrcFile", value: "/repo/AGENTS.md" }, { name: "cdx:agent:inventorySource", value: "agent-file" }, { name: "cdx:agent:credentialExposure", value: "true" }, { name: "cdx:agent:credentialRiskIndicators", value: "generic-secret,bearer-token", }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect inline credentials"); assert.strictEqual(findings[0].severity, "critical"); }); it("should detect unauthenticated configured MCP endpoints (MCP-004)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "MCP-004"); assert.ok(rule, "MCP-004 rule should exist"); const bom = makeBom( [], [], [], [ { "bom-ref": "urn:service:mcp:gateway:latest", name: "gateway", version: "latest", endpoints: ["https://demo.ngrok-free.app/mcp"], authenticated: false, properties: [ { name: "SrcFile", value: "/repo/.vscode/mcp.json" }, { name: "cdx:mcp:inventorySource", value: "config-file" }, { name: "cdx:mcp:transport", value: "streamable-http" }, { name: "cdx:mcp:configFormat", value: "vscode" }, { name: "cdx:mcp:configKey", value: "mcpServers.gateway" }, { name: "cdx:mcp:trustProfile", value: "review-needed" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok( findings.length > 0, "Should detect unauthenticated config endpoint", ); }); it("should detect inline credential exposure in MCP config services (MCP-005)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "MCP-005"); assert.ok(rule, "MCP-005 rule should exist"); const bom = makeBom( [], [], [], [ { "bom-ref": "urn:service:mcp:gateway:latest", name: "gateway", version: "latest", properties: [ { name: "SrcFile", value: "/repo/.vscode/mcp.json" }, { name: "cdx:mcp:inventorySource", value: "config-file" }, { name: "cdx:mcp:credentialExposure", value: "true" }, { name: "cdx:mcp:credentialExposureFieldCount", value: "2", }, { name: "cdx:mcp:credentialIndicatorCount", value: "2", }, { name: "cdx:mcp:credentialReferenceCount", value: "1" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect config credential exposure"); assert.strictEqual(findings[0].severity, "critical"); }); it("should detect confused-deputy risk in MCP config services (MCP-006)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "MCP-006"); assert.ok(rule, "MCP-006 rule should exist"); const bom = makeBom( [], [], [], [ { "bom-ref": "urn:service:mcp:gateway:latest", name: "gateway", version: "latest", properties: [ { name: "SrcFile", value: "/repo/.vscode/mcp.json" }, { name: "cdx:mcp:inventorySource", value: "config-file" }, { name: "cdx:mcp:security:confusedDeputyRisk", value: "high" }, { name: "cdx:mcp:auth:supportsDCR", value: "true" }, { name: "cdx:mcp:authPosture", value: "oauth" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect confused-deputy risk"); }); it("should detect token passthrough risk in MCP config services (MCP-007)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "MCP-007"); assert.ok(rule, "MCP-007 rule should exist"); const bom = makeBom( [], [], [], [ { "bom-ref": "urn:service:mcp:gateway:latest", name: "gateway", version: "latest", properties: [ { name: "SrcFile", value: "/repo/.vscode/mcp.json" }, { name: "cdx:mcp:inventorySource", value: "config-file" }, { name: "cdx:mcp:security:tokenPassthroughRisk", value: "high" }, { name: "cdx:mcp:authPosture", value: "bearer" }, { name: "cdx:mcp:trustProfile", value: "official-sdk+networked+auth", }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect token passthrough risk"); }); it("should flag shipped AI instruction files in build/post-build BOMs (AGT-007)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "AGT-007"); assert.ok(rule, "AGT-007 rule should exist"); const bom = makeBom([ { "bom-ref": "file:/repo/CLAUDE.md", name: "CLAUDE.md", type: "file", properties: [ { name: "SrcFile", value: "/repo/CLAUDE.md" }, { name: "cdx:file:kind", value: "agent-instructions" }, { name: "cdx:agent:inventorySource", value: "agent-file" }, ], }, ]); bom.metadata.lifecycles = [{ phase: "build" }, { phase: "post-build" }]; const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect shipped AI instructions"); assert.strictEqual(findings[0].severity, "medium"); }); it("should flag shipped MCP config files in build/post-build BOMs (MCP-008)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "MCP-008"); assert.ok(rule, "MCP-008 rule should exist"); const bom = makeBom([ { "bom-ref": "file:/repo/.vscode/mcp.json", name: "mcp.json", type: "file", properties: [ { name: "SrcFile", value: "/repo/.vscode/mcp.json" }, { name: "cdx:file:kind", value: "mcp-config" }, { name: "cdx:mcp:configFormat", value: "vscode" }, { name: "cdx:mcp:configuredServiceCount", value: "1" }, { name: "cdx:mcp:configuredServiceNames", value: "releaseDocs" }, ], }, ]); bom.metadata.lifecycles = [{ phase: "build" }]; const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect shipped MCP config"); assert.strictEqual(findings[0].severity, "medium"); }); it("should detect npm name mismatch (INT-002)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "INT-002"); assert.ok(rule, "INT-002 rule should exist"); const bom = makeBom([ makeComponent("suspicious-pkg", "1.0.0", [ [ "cdx:npm:nameMismatchError", "Expected 'real-pkg', found 'suspicious-pkg'", ], ]), ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect name mismatch"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect yanked Ruby gem (INT-004)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "INT-004"); assert.ok(rule, "INT-004 rule should exist"); const bom = makeBom([ { type: "library", name: "bad-gem", version: "0.5.0", purl: "pkg:gem/bad-gem@0.5.0", "bom-ref": "pkg:gem/bad-gem@0.5.0", properties: [{ name: "cdx:gem:yanked", value: "true" }], }, ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect yanked gem"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect Cargo git dependency without immutable pin (PKG-007)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-007"); assert.ok(rule, "PKG-007 rule should exist"); const bom = makeBom([ { type: "library", name: "git-crate", version: "git+https://example.com/git-crate", purl: "pkg:cargo/git-crate@git+https://example.com/git-crate", "bom-ref": "pkg:cargo/git-crate@git+https://example.com/git-crate", properties: [ { name: "cdx:cargo:git", value: "https://example.com/git-crate" }, { name: "cdx:cargo:dependencyKind", value: "runtime" }, ], }, ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect mutable Cargo git source"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect Cargo local path dependency (PKG-008)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "PKG-008"); assert.ok(rule, "PKG-008 rule should exist"); const bom = makeBom([ { type: "library", name: "path-crate", version: "path+../path-crate", purl: "pkg:cargo/path-crate@path+../path-crate", "bom-ref": "pkg:cargo/path-crate@path+../path-crate", properties: [ { name: "cdx:cargo:path", value: "../path-crate" }, { name: "cdx:cargo:dependencyKind", value: "build" }, ], }, ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect Cargo path dependency"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect yanked Cargo crate (INT-010)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "INT-010"); assert.ok(rule, "INT-010 rule should exist"); const bom = makeBom([ { type: "library", name: "yanked-crate", version: "1.2.3", purl: "pkg:cargo/yanked-crate@1.2.3", "bom-ref": "pkg:cargo/yanked-crate@1.2.3", properties: [ { name: "cdx:cargo:yanked", value: "true" }, { name: "cdx:cargo:publisher", value: "publisher" }, ], }, ]); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect yanked Cargo crate"); assert.strictEqual(findings[0].severity, "high"); }); it("should detect native Cargo build surface in formulation (INT-011)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "INT-011"); assert.ok(rule, "INT-011 rule should exist"); const bom = makeBom( [], [], [ { type: "application", name: "cargo-demo", version: "config", "bom-ref": "urn:cdxgen:formulation:cargo:test", properties: [ { name: "SrcFile", value: "/tmp/Cargo.toml" }, { name: "cdx:rust:buildTool", value: "cargo" }, { name: "cdx:cargo:hasNativeBuild", value: "true" }, { name: "cdx:cargo:buildScript", value: "/tmp/build.rs" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok(findings.length > 0, "Should detect native Cargo build surface"); assert.strictEqual(findings[0].severity, "medium"); }); it("should detect mutable Cargo toolchain setup for native builds (INT-012)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "INT-012"); assert.ok(rule, "INT-012 rule should exist"); const bom = makeBom( [ { type: "application", name: "rust-toolchain", version: "stable", purl: "pkg:github/dtolnay/rust-toolchain@stable", "bom-ref": "pkg:github/dtolnay/rust-toolchain@stable", properties: [ { name: "cdx:github:action:ecosystem", value: "cargo" }, { name: "cdx:github:action:role", value: "toolchain" }, { name: "cdx:github:action:versionPinningType", value: "tag", }, { name: "cdx:github:action:uses", value: "dtolnay/rust-toolchain@stable", }, ], }, ], [], [ { type: "application", name: "cargo-demo", version: "config", "bom-ref": "urn:cdxgen:formulation:cargo:int012", properties: [ { name: "SrcFile", value: "/tmp/Cargo.toml" }, { name: "cdx:rust:buildTool", value: "cargo" }, { name: "cdx:cargo:hasNativeBuild", value: "true" }, { name: "cdx:cargo:buildScript", value: "/tmp/build.rs" }, ], }, ], ); const findings = await evaluateRule(rule, bom); assert.ok( findings.length > 0, "Should detect mutable Cargo toolchain setup for native builds", ); assert.strictEqual(findings[0].severity, "medium"); }); it("should detect Cargo build workflow steps against native build surfaces (INT-013)", async () => { const rules = await loadRules(RULES_DIR); const rule = rules.find((r) => r.id === "INT-013"); assert.ok(rule, "INT-013 rule should exist"); const bom = makeBom( [ { type: "application", name: "cargo build", "bom-ref": "urn:cdxgen:workflow:cargo-build", properties: [ { name: "cdx:github:step:type", value: "run" }, { name: "cdx:github:step:usesCargo", value: "true" }, { name: "cdx:github:step:cargoSubcommands",