@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,484 lines (1,338 loc) • 160 kB
JavaScript
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",