@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,436 lines (1,285 loc) • 183 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 makeAiModel(name, version, properties = [], extra = {}) {
return {
type: "machine-learning-model",
name,
version,
purl: `pkg:generic/${name}@${version}`,
"bom-ref": `pkg:generic/${name}@${version}`,
properties: properties.map(([k, v]) => ({ name: k, value: v })),
...extra,
};
}
function makeAiService(name, version, endpoint, properties = [], extra = {}) {
return {
"bom-ref": `urn:service:ai:${name}:${version}`,
name,
version,
endpoints: endpoint ? [endpoint] : [],
authenticated: true,
properties: [["cdx:ai:kind", "inference-service"], ...properties].map(
([k, v]) => ({ name: k, value: v }),
),
...extra,
};
}
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 aiGovernanceRules = rules.filter(
(r) => r.category === "ai-governance",
);
assert.ok(aiGovernanceRules.length > 0, "Should have AI governance rules");
const aiSecurityRules = rules.filter((r) => r.category === "ai-security");
assert.ok(aiSecurityRules.length > 0, "Should have AI security rules");
const aiPerformanceRules = rules.filter(
(r) => r.category === "ai-performance",
);
assert.ok(
aiPerformanceRules.length > 0,
"Should have AI performance 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");
const golemSecurityRules = rules.filter(
(r) => r.category === "golem-security",
);
assert.ok(
golemSecurityRules.length > 0,
"Should have Golem security rules",
);
const golemPerformanceRules = rules.filter(
(r) => r.category === "golem-performance",
);
assert.ok(
golemPerformanceRules.length > 0,
"Should have Golem performance rules",
);
const cbomSecurityRules = rules.filter(
(r) => r.category === "cbom-security",
);
assert.ok(cbomSecurityRules.length > 0, "Should have CBOM security rules");
const cbomComplianceRules = rules.filter(
(r) => r.category === "cbom-compliance",
);
assert.ok(
cbomComplianceRules.length > 0,
"Should have CBOM compliance 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 high-severity Golem semantic security signals (GOLEM-SEC-001)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-SEC-001");
assert.ok(rule, "GOLEM-SEC-001 rule should exist");
const bom = makeBom([
makeComponent("github.com/acme/crypto", "1.0.0", [
["cdx:golem:securitySignalCategory", "weak-crypto"],
["cdx:golem:securitySignalSeverity", "high"],
["cdx:golem:usageScopes", "runtime"],
["cdx:golem:occurrenceEvidenceKinds", "import,symbolCall"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect Golem security signal");
assert.strictEqual(findings[0].ruleId, "GOLEM-SEC-001");
assert.strictEqual(findings[0].severity, "medium");
});
it("should detect Golem crypto data-flow evidence (GOLEM-SEC-002)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-SEC-002");
assert.ok(rule, "GOLEM-SEC-002 rule should exist");
const bom = makeBom([
makeComponent("github.com/acme/app", "1.0.0", [
["cdx:golem:cryptoDataFlow", "true"],
["cdx:golem:cryptoDataFlowCategories", "http-request->crypto"],
["cdx:golem:cryptoDataFlowRuleId", "GOLEM-DATAFLOW-CRYPTO-MATERIAL"],
["cdx:golem:cryptoDataFlowTaintKinds", "crypto,user-input"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect Golem crypto flow");
assert.strictEqual(findings[0].ruleId, "GOLEM-SEC-002");
assert.strictEqual(findings[0].severity, "low");
});
it("should detect Golem crypto findings (GOLEM-SEC-003)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-SEC-003");
assert.ok(rule, "GOLEM-SEC-003 rule should exist");
const bom = makeBom([
makeComponent("github.com/acme/crypto", "1.0.0", [
["cdx:golem:cryptoFinding", "GOLEM-CRYPTO-WEAK-MD5"],
["cdx:golem:cryptoFindingSeverity", "high"],
["cdx:golem:cryptoAlgorithm", "md5"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect Golem crypto finding");
assert.strictEqual(findings[0].ruleId, "GOLEM-SEC-003");
assert.strictEqual(findings[0].severity, "low");
});
it("should detect local Go replacements (GOLEM-SEC-004)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-SEC-004");
assert.ok(rule, "GOLEM-SEC-004 rule should exist");
const bom = makeBom([
makeComponent("github.com/acme/local", "1.0.0", [
["cdx:golem:localReplacement", "true"],
["cdx:golem:replacementModule", "../local"],
["cdx:golem:modulePath", "github.com/acme/local"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect local replacement");
assert.strictEqual(findings[0].ruleId, "GOLEM-SEC-004");
assert.strictEqual(findings[0].severity, "low");
});
it("should detect Golem native code boundaries (GOLEM-PERF-001)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-PERF-001");
assert.ok(rule, "GOLEM-PERF-001 rule should exist");
const bom = makeBom([]);
bom.metadata.component.properties = [
{ name: "cdx:golem:nativeArtifactCount", value: "2" },
{ name: "cdx:golem:nativeArtifactKinds", value: "cgo,assembly" },
];
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect Golem native boundary");
assert.strictEqual(findings[0].ruleId, "GOLEM-PERF-001");
assert.strictEqual(findings[0].severity, "low");
});
it("should detect Golem generated or embedded build inputs (GOLEM-PERF-002)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-PERF-002");
assert.ok(rule, "GOLEM-PERF-002 rule should exist");
const bom = makeBom([]);
bom.metadata.component.properties = [
{ name: "cdx:golem:nativeArtifactCount", value: "1" },
{ name: "cdx:golem:goGenerateCount", value: "1" },
{ name: "cdx:golem:goEmbedCount", value: "0" },
];
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect Golem build surface");
assert.strictEqual(findings[0].ruleId, "GOLEM-PERF-002");
assert.strictEqual(findings[0].severity, "low");
});
it("should detect truncated Golem data-flow evidence (GOLEM-PERF-003)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-PERF-003");
assert.ok(rule, "GOLEM-PERF-003 rule should exist");
const bom = makeBom([]);
bom.metadata.component.properties = [
{ name: "cdx:golem:dataFlowTruncated", value: "true" },
{ name: "cdx:golem:dataFlowTruncationReasons", value: "max-slices" },
{ name: "cdx:golem:dataFlowSliceCount", value: "250" },
];
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect truncated data-flow");
assert.strictEqual(findings[0].ruleId, "GOLEM-PERF-003");
assert.strictEqual(findings[0].severity, "low");
});
it("should detect private Go module candidates (GOLEM-COMP-001)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-COMP-001");
assert.ok(rule, "GOLEM-COMP-001 rule should exist");
const bom = makeBom([
makeComponent("internal.example.com/platform/auth", "1.0.0", [
["cdx:golem:privateModuleCandidate", "true"],
["cdx:golem:modulePath", "internal.example.com/platform/auth"],
["cdx:golem:usageScopes", "runtime"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect private module candidate");
assert.strictEqual(findings[0].ruleId, "GOLEM-COMP-001");
assert.strictEqual(findings[0].severity, "low");
});
it("should detect vendored Go modules without license evidence (GOLEM-COMP-002)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-COMP-002");
assert.ok(rule, "GOLEM-COMP-002 rule should exist");
const bom = makeBom([
makeComponent("github.com/acme/vendor-only", "1.0.0", [
["cdx:golem:vendored", "true"],
["cdx:golem:licenseFileCount", "0"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(
findings.length > 0,
"Should detect missing vendored license evidence",
);
assert.strictEqual(findings[0].ruleId, "GOLEM-COMP-002");
assert.strictEqual(findings[0].severity, "medium");
});
it("should detect Go module exclude directives (GOLEM-COMP-003)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "GOLEM-COMP-003");
assert.ok(rule, "GOLEM-COMP-003 rule should exist");
const bom = makeBom([]);
bom.metadata.component.properties = [
{ name: "cdx:golem:goModExcludeCount", value: "1" },
{ name: "cdx:golem:excludeModule", value: "github.com/acme/unsafe" },
];
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect go.mod excludes");
assert.strictEqual(findings[0].ruleId, "GOLEM-COMP-003");
assert.strictEqual(findings[0].severity, "low");
});
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 shipped prompt config files in build BOMs (AIG-001)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "AIG-001");
assert.ok(rule, "AIG-001 rule should exist");
const bom = makeBom([
{
"bom-ref": "file:/repo/prompts/release-prompt.yaml",
name: "release-prompt.yaml",
type: "file",
properties: [
{ name: "SrcFile", value: "/repo/prompts/release-prompt.yaml" },
{ name: "cdx:file:kind", value: "prompt-config-file" },
{ name: "cdx:ai:source", value: "prompt-config" },
],
},
]);
bom.metadata.lifecycles = [{ phase: "build" }];
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect shipped prompt config");
assert.strictEqual(findings[0].severity, "medium");
assert.ok(findings[0].standards?.["eu-ai-act"]?.length);
});
it("should detect AI services with implicit model selection (AIG-002)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "AIG-002");
assert.ok(rule, "AIG-002 rule should exist");
const bom = makeBom(
[],
[],
[],
[
makeAiService("openai", "latest", "https://api.openai.com/v1", [
["cdx:ai:provider", "openai"],
["cdx:ai:modelSelection", "implicit"],
["cdx:ai:modelCount", "0"],
["cdx:ai:deployment", "remote"],
["cdx:ai:source", "source-import"],
]),
],
);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect implicit model selection");
assert.strictEqual(findings[0].severity, "medium");
});
it("should detect insecure remote AI endpoints (AIS-001)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "AIS-001");
assert.ok(rule, "AIS-001 rule should exist");
const bom = makeBom(
[],
[],
[],
[
makeAiService("openai-proxy", "latest", "http://proxy.example.com/v1", [
["cdx:ai:provider", "openai-compatible"],
["cdx:ai:modelSelection", "explicit"],
["cdx:ai:modelCount", "1"],
["cdx:ai:deployment", "remote"],
["cdx:ai:transportSecurity", "insecure-http"],
]),
],
);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect insecure AI transport");
assert.strictEqual(findings[0].severity, "high");
});
it("should detect large local AI context windows (AIP-001)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "AIP-001");
assert.ok(rule, "AIP-001 rule should exist");
const bom = makeBom([
makeAiModel("llama3-70b", "1.0.0", [
["cdx:ai:runtime", "ollama"],
["cdx:ai:contextWindow", "131072"],
["cdx:ai:artifactFormat", "modelfile"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect large local context window");
assert.strictEqual(findings[0].severity, "medium");
});
it("should detect large local AI models missing quantization metadata (AIP-002)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "AIP-002");
assert.ok(rule, "AIP-002 rule should exist");
const bom = makeBom([
makeAiModel("local-llama3-8b", "1.0.0", [
["cdx:ai:source", "local-artifact"],
["cdx:ai:runtime", "llama.cpp"],
["cdx:ai:parameterCount", "8000000000"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect missing quantization");
assert.strictEqual(findings[0].severity, "medium");
});
it("should detect modified AI model variants (AIG-003)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "AIG-003");
assert.ok(rule, "AIG-003 rule should exist");
const bom = makeBom([
makeAiModel("DeepSeek-R1-Distill-Qwen-7B", "1.0.0", [
["cdx:ai:provider", "huggingface"],
["cdx:ai:source", "huggingface-api"],
["cdx:ai:variant", "distilled"],
["cdx:ai:variant", "quantized"],
]),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(findings.length > 0, "Should detect modified AI model variants");
assert.strictEqual(findings[0].severity, "medium");
});
it("should detect unlocked or abliterated AI models (AIS-002)", async () => {
const rules = await loadRules(RULES_DIR);
const rule = rules.find((r) => r.id === "AIS-002");
assert.ok(rule, "AIS-002 rule should exist");
const bom = makeBom([
makeAiModel(
"Llama-3-8B-Abliterated-Unlocked",
"1.0.0",
[
["cdx:ai:provider", "huggingface"],
["cdx:ai:source", "huggingface-api"],
["cdx:ai:variant", "abliterated"],
["cdx:ai:variant", "unlocked"],
],
{
pedigree: {
notes:
"Hugging Face relation: finetune; model marked unlocked and abliterated",
},
},
),
]);
const findings = await evaluateRule(rule, bom);
assert.ok(
findings.length > 0,
"Should detect unlocked or abliterated models",
);
assert.strictEqual(findings[0].severity, "high");
});
it("should expand the ai-bom alias to AI governance, security, and performance categories", async () => {
const bom = makeBom(
[
makeAiModel("local-llama3-8b", "1.0.0", [
["cdx:ai:source", "local-artifact"],
["cdx:ai:runtime", "llama.cpp"],
["cdx:ai:parameterCount", "8000000000"],
["cdx:ai:variant", "distilled"],
]),
],
[],
[],
[
makeAiService("openai-proxy", "latest", "http://proxy.example.com/v1", [
["cdx:ai:provider", "openai-compatible"],
["cdx:ai:modelSelection", "implicit"],
["cdx:ai:modelCount", "0"],
["cdx:ai:deployment", "remote"],
["cdx:ai:transportSecurity", "insecure-http"],
["cdx:ai:source", "source-import"],
]),
],
);
const findings = await auditBom(bom, {
bomAuditCategories: "ai-bom",
});
const categories = new Set(findings.map((finding) => finding.category));
assert.ok(categories.has("ai-governance"));
assert.ok(categories.has("ai-security"));
assert.ok(categories.has("ai-performance"));
});
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");
assert.ok(findings[0].standards?.["owasp-agentic-top-10-2026"]?.length);
assert.ok(findings[0].standards?.["eu-cra"]?.length);
});
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);
assert.ok(findings[0].standards?.["owasp-agentic-top-10-2026"]?.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",