UNPKG

@cyclonedx/cdxgen

Version:

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

359 lines (330 loc) 10.8 kB
import { assert, describe, it } from "poku"; import { __test, getAllComplianceRules, getCraRules, getScvsRules, } from "./complianceRules.js"; const { componentLicenseId, inventoryComponents, looksLikeSpdx, collectReferencedRefs, } = __test; function baseBom(overrides = {}) { return { bomFormat: "CycloneDX", specVersion: "1.6", serialNumber: "urn:uuid:1b671687-395b-41f5-a30f-a58921a69b79", metadata: { timestamp: "2024-01-02T03:04:05Z", tools: { components: [ { type: "application", name: "cdxgen", version: "12.0.0" }, ], }, component: { name: "demo", version: "1.0.0", type: "application", "bom-ref": "pkg:generic/demo@1.0.0", }, supplier: { name: "Acme", contact: [{ email: "psirt@example.com" }], }, }, components: [ { type: "library", name: "lodash", version: "4.17.21", purl: "pkg:npm/lodash@4.17.21", "bom-ref": "pkg:npm/lodash@4.17.21", licenses: [{ license: { id: "MIT" } }], hashes: [{ alg: "SHA-256", content: "abc" }], copyright: "Copyright (c) OpenJS", }, ], dependencies: [ { ref: "pkg:generic/demo@1.0.0", dependsOn: ["pkg:npm/lodash@4.17.21"] }, { ref: "pkg:npm/lodash@4.17.21", dependsOn: [] }, ], ...overrides, }; } describe("complianceRules catalog", () => { it("exposes SCVS + CRA rules with stable ids", () => { const all = getAllComplianceRules(); assert.ok(all.length >= 80, `expected >= 80 rules, got ${all.length}`); const scvs = getScvsRules(); const cra = getCraRules(); assert.strictEqual(scvs.length + cra.length, all.length); for (const r of all) { assert.ok(typeof r.id === "string" && r.id.length > 0, `bad id ${r.id}`); assert.ok( typeof r.evaluate === "function", `rule ${r.id} missing evaluate`, ); assert.ok( Array.isArray(r.standardRefs), `rule ${r.id} missing standardRefs`, ); } const ids = new Set(all.map((r) => r.id)); assert.strictEqual(ids.size, all.length, "rule ids must be unique"); }); it("SCVS rule ids match the SCVS-X.Y convention", () => { for (const r of getScvsRules()) { assert.match(r.id, /^SCVS-\d+\.\d+$/); assert.strictEqual(r.standard, "SCVS"); } }); it("CRA rule ids match the CRA-MIN-XXX convention", () => { for (const r of getCraRules()) { assert.match(r.id, /^CRA-MIN-\d+$/); assert.strictEqual(r.standard, "CRA"); } }); }); describe("complianceRules helpers", () => { it("inventoryComponents filters non-inventory types", () => { const bom = { components: [ { type: "library", name: "a" }, { type: "cryptographic-asset", name: "b" }, { type: "framework", name: "c" }, ], }; assert.deepStrictEqual( inventoryComponents(bom).map((c) => c.name), ["a", "c"], ); }); it("looksLikeSpdx accepts common forms and rejects garbage", () => { assert.ok(looksLikeSpdx("MIT")); assert.ok(looksLikeSpdx("Apache-2.0")); assert.ok(looksLikeSpdx("Apache-2.0 OR MIT")); assert.ok(looksLikeSpdx("(MIT AND LGPL-2.1-only)")); assert.ok(!looksLikeSpdx("NOASSERTION")); assert.ok(!looksLikeSpdx("unknown")); assert.ok(!looksLikeSpdx("")); assert.ok(!looksLikeSpdx(null)); }); it("componentLicenseId prefers id then name then expression", () => { assert.strictEqual( componentLicenseId({ licenses: [{ license: { id: "MIT" } }] }), "MIT", ); assert.strictEqual( componentLicenseId({ licenses: [{ license: { name: "Custom" } }] }), "Custom", ); assert.strictEqual( componentLicenseId({ licenses: [{ expression: "MIT OR Apache-2.0" }] }), "MIT OR Apache-2.0", ); assert.strictEqual(componentLicenseId({}), null); assert.strictEqual(componentLicenseId({ licenses: [] }), null); }); it("collectReferencedRefs gathers all refs", () => { const bom = baseBom(); const refs = collectReferencedRefs(bom); assert.ok(refs.has("pkg:generic/demo@1.0.0")); assert.ok(refs.has("pkg:npm/lodash@4.17.21")); }); }); describe("SCVS automatable rules on a clean BOM", () => { const bom = baseBom(); const rules = getScvsRules().filter((r) => r.automatable); it("SCVS-1.1, 1.3, 1.7, 2.1, 2.3, 2.7, 2.9, 2.11, 2.12, 2.14, 3.20 all pass", () => { const expected = [ "SCVS-1.1", "SCVS-1.3", "SCVS-1.7", "SCVS-2.1", "SCVS-2.3", "SCVS-2.7", "SCVS-2.9", "SCVS-2.11", "SCVS-2.12", "SCVS-2.14", "SCVS-3.20", ]; for (const id of expected) { const r = rules.find((x) => x.id === id); assert.ok(r, `missing rule ${id}`); const res = r.evaluate(bom); assert.strictEqual(res.status, "pass", `${id}: ${res.message}`); } }); it("SCVS-2.4 fails when BOM is not signed, passes when signed", () => { const rule = rules.find((r) => r.id === "SCVS-2.4"); assert.strictEqual(rule.evaluate(bom).status, "fail"); const signed = baseBom({ signature: { algorithm: "RS512", value: "xxx" }, }); assert.strictEqual(rule.evaluate(signed).status, "pass"); }); it("SCVS-1.1 fails when a component has no version", () => { const rule = rules.find((r) => r.id === "SCVS-1.1"); const bad = baseBom({ components: [{ type: "library", name: "no-version", purl: "pkg:npm/x" }], }); const res = rule.evaluate(bad); assert.strictEqual(res.status, "fail"); assert.match(res.message, /missing a version/); }); it("SCVS-2.3 fails when serialNumber is missing", () => { const rule = rules.find((r) => r.id === "SCVS-2.3"); assert.strictEqual( rule.evaluate(baseBom({ serialNumber: undefined })).status, "fail", ); assert.strictEqual( rule.evaluate(baseBom({ serialNumber: "garbage" })).status, "fail", ); }); it("SCVS-2.11 fails when root name or version is missing", () => { const rule = rules.find((r) => r.id === "SCVS-2.11"); const noVer = baseBom({ metadata: { ...baseBom().metadata, component: { name: "x", "bom-ref": "x", type: "application" }, }, }); assert.strictEqual(rule.evaluate(noVer).status, "fail"); const noName = baseBom({ metadata: { ...baseBom().metadata, component: {}, }, }); assert.strictEqual(rule.evaluate(noName).status, "fail"); }); it("SCVS-2.12 fails when a purl is unparseable", () => { const rule = rules.find((r) => r.id === "SCVS-2.12"); const bom = baseBom({ components: [ { type: "library", name: "bad", version: "1.0.0", purl: "not-a-purl", "bom-ref": "bad", }, ], }); const res = rule.evaluate(bom); assert.strictEqual(res.status, "fail"); assert.ok(res.locations.length > 0); }); it("SCVS-2.15 rejects NOASSERTION-style license ids", () => { const rule = rules.find((r) => r.id === "SCVS-2.15"); const bom = baseBom({ components: [ { type: "library", name: "x", version: "1.0.0", purl: "pkg:npm/x@1.0.0", "bom-ref": "x", licenses: [{ license: { id: "NOASSERTION" } }], }, ], }); assert.strictEqual(rule.evaluate(bom).status, "fail"); }); it("SCVS-3.20 flags orphan components not in dep graph", () => { const rule = rules.find((r) => r.id === "SCVS-3.20"); const orphan = baseBom({ components: [ ...baseBom().components, { type: "library", name: "orphan", version: "0.0.1", purl: "pkg:npm/orphan@0.0.1", "bom-ref": "pkg:npm/orphan@0.0.1", licenses: [{ license: { id: "MIT" } }], hashes: [{ alg: "SHA-256", content: "1" }], }, ], }); const res = rule.evaluate(orphan); assert.strictEqual(res.status, "fail"); assert.match(res.message, /not referenced/); }); it("SCVS-6.3 passes when no modified components exist", () => { const rule = rules.find((r) => r.id === "SCVS-6.3"); assert.strictEqual(rule.evaluate(baseBom()).status, "pass"); }); }); describe("SCVS manual controls with predictive audit assistance", () => { it("includes cdx-audit guidance for mapped manual-review controls", () => { const mappedRuleIds = [ "SCVS-2.8", "SCVS-3.3", "SCVS-3.6", "SCVS-4.10", "SCVS-4.11", "SCVS-6.1", "SCVS-6.2", ]; mappedRuleIds.forEach((ruleId) => { const rule = getScvsRules().find((entry) => entry.id === ruleId); assert.ok(rule, `missing rule ${ruleId}`); const result = rule.evaluate(baseBom()); assert.strictEqual(result.status, "manual"); assert.match( result.mitigation, /cdx-audit --bom bom\.json --scope required/, ); assert.strictEqual( result.evidence?.suggestedCommand, "cdx-audit --bom bom.json --scope required", ); assert.strictEqual(result.evidence?.reviewMode, "manual-with-cdx-audit"); }); }); }); describe("CRA rules", () => { const rules = getCraRules(); it("all pass on the well-formed baseline BOM", () => { for (const r of rules) { const res = r.evaluate(baseBom()); assert.strictEqual( res.status, "pass", `${r.id} expected pass, got ${res.status}: ${res.message}`, ); } }); it("CRA-MIN-001 fails when supplier is missing", () => { const rule = rules.find((r) => r.id === "CRA-MIN-001"); const bom = baseBom(); bom.metadata.supplier = undefined; assert.strictEqual(rule.evaluate(bom).status, "fail"); }); it("CRA-MIN-002 fails when contact is empty", () => { const rule = rules.find((r) => r.id === "CRA-MIN-002"); const bom = baseBom(); bom.metadata.supplier = { name: "Acme" }; assert.strictEqual(rule.evaluate(bom).status, "fail"); }); it("CRA-MIN-004 fails when dependency graph is empty", () => { const rule = rules.find((r) => r.id === "CRA-MIN-004"); const bom = baseBom({ dependencies: [] }); assert.strictEqual(rule.evaluate(bom).status, "fail"); }); it("CRA-MIN-008 supports both array (1.4) and object (1.5+) tool shapes", () => { const rule = rules.find((r) => r.id === "CRA-MIN-008"); const legacy = baseBom(); legacy.metadata.tools = [{ name: "old" }]; assert.strictEqual(rule.evaluate(legacy).status, "pass"); const missing = baseBom(); missing.metadata.tools = undefined; assert.strictEqual(rule.evaluate(missing).status, "fail"); }); });