UNPKG

@cyclonedx/cdxgen

Version:

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

334 lines (315 loc) 10.6 kB
import { assert, describe, it } from "poku"; import { mergeDependencies, mergeServices, trimComponents, } from "./depsUtils.js"; describe("mergeDependencies()", () => { it("merges two non-overlapping dependency arrays", () => { const a = [{ ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/b@1"] }]; const b = [{ ref: "pkg:npm/c@1", dependsOn: ["pkg:npm/d@1"] }]; const result = mergeDependencies(a, b); assert.strictEqual(result.length, 2); const aEntry = result.find((d) => d.ref === "pkg:npm/a@1"); assert.ok(aEntry); assert.deepStrictEqual(aEntry.dependsOn, ["pkg:npm/b@1"]); }); it("merges dependsOn sets for the same ref", () => { const a = [{ ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/b@1"] }]; const b = [{ ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/c@1"] }]; const result = mergeDependencies(a, b); assert.strictEqual(result.length, 1); const entry = result[0]; assert.ok(entry.dependsOn.includes("pkg:npm/b@1")); assert.ok(entry.dependsOn.includes("pkg:npm/c@1")); }); it("deduplicates identical dependsOn entries", () => { const a = [{ ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/b@1"] }]; const b = [ { ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/b@1", "pkg:npm/c@1"] }, ]; const result = mergeDependencies(a, b); assert.strictEqual(result.length, 1); assert.strictEqual( result[0].dependsOn.filter((x) => x === "pkg:npm/b@1").length, 1, ); }); it("handles undefined newDependencies gracefully", () => { const a = [{ ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/b@1"] }]; const result = mergeDependencies(a, undefined); assert.strictEqual(result.length, 1); assert.strictEqual(result[0].ref, "pkg:npm/a@1"); }); it("handles empty arrays", () => { assert.deepStrictEqual(mergeDependencies([], []), []); assert.deepStrictEqual(mergeDependencies([], undefined), []); }); it("merges a single dependency object (non-array)", () => { const a = [{ ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/b@1"] }]; const single = { ref: "pkg:npm/c@1", dependsOn: ["pkg:npm/d@1"] }; const result = mergeDependencies(a, single); assert.strictEqual(result.length, 2); }); it("handles the provides field for OmniBOR / ADG links", () => { const a = [ { ref: "gitoid:commit:sha1:abc", dependsOn: [], provides: ["gitoid:commit:sha1:def"], }, ]; const b = [ { ref: "gitoid:commit:sha1:def", provides: ["gitoid:blob:sha1:001", "gitoid:blob:sha1:002"], }, ]; const result = mergeDependencies(a, b); assert.ok( result.every((d) => Array.isArray(d.provides)), "all entries should have provides", ); const defEntry = result.find((d) => d.ref === "gitoid:commit:sha1:def"); assert.ok(defEntry); assert.ok(defEntry.provides.includes("gitoid:blob:sha1:001")); assert.ok(defEntry.provides.includes("gitoid:blob:sha1:002")); }); it("excludes parent component from dependsOn", () => { const parentComponent = { "bom-ref": "pkg:npm/myapp@1.0.0" }; const a = [ { ref: "pkg:npm/a@1", dependsOn: ["pkg:npm/myapp@1.0.0", "pkg:npm/b@1"], }, ]; const result = mergeDependencies(a, [], parentComponent); const entry = result.find((d) => d.ref === "pkg:npm/a@1"); assert.ok( !entry.dependsOn.includes("pkg:npm/myapp@1.0.0"), "parent should be excluded", ); assert.ok(entry.dependsOn.includes("pkg:npm/b@1")); }); it("merges parser-returned dependencies into BOM dependencies", () => { const bomDeps = [{ ref: "pkg:npm/app@1", dependsOn: ["pkg:npm/lib@2"] }]; const parserDeps = [ { ref: "workflow-bom-ref-1", dependsOn: ["task-bom-ref-1", "task-bom-ref-2"], }, { ref: "task-bom-ref-1", dependsOn: ["pkg:github/actions/checkout@v4"] }, ]; const result = mergeDependencies(bomDeps, parserDeps); assert.strictEqual(result.length, 3); const wfEntry = result.find((d) => d.ref === "workflow-bom-ref-1"); assert.ok(wfEntry); assert.ok(wfEntry.dependsOn.includes("task-bom-ref-1")); assert.ok(wfEntry.dependsOn.includes("task-bom-ref-2")); }); it("filters out null and undefined entries from dependsOn", () => { const deps = [ { ref: "pkg:composer/foo/bar", dependsOn: [null, undefined, "pkg:composer/vendor/lib@1.0"], }, ]; const result = mergeDependencies(deps, []); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0].dependsOn, [ "pkg:composer/vendor/lib@1.0", ]); assert.ok(!result[0].dependsOn.includes(null), "null must be filtered"); assert.ok( !result[0].dependsOn.includes(undefined), "undefined must be filtered", ); }); it("filters out null and undefined from dependsOn even with a parentComponent", () => { const parent = { "bom-ref": "pkg:composer/foo/bar" }; const deps = [ { ref: "pkg:composer/foo/bar", dependsOn: [null, "pkg:composer/vendor/lib@1.0"], }, ]; const result = mergeDependencies(deps, [], parent); const entry = result.find((d) => d.ref === "pkg:composer/foo/bar"); assert.ok(entry); assert.deepStrictEqual(entry.dependsOn, ["pkg:composer/vendor/lib@1.0"]); assert.ok(!entry.dependsOn.includes(null), "null must be filtered"); }); }); describe("trimComponents()", () => { it("retains hashes from duplicate components", () => { const components = [ { name: "jquery", version: "3.5.1", purl: "pkg:npm/jquery@3.5.1", type: "library", properties: [{ name: "SrcFile", value: "Scripts/jquery.min.js" }], }, { name: "jquery", version: "3.5.1", purl: "pkg:npm/jquery@3.5.1", type: "framework", hashes: [{ alg: "SHA-512", content: "abc123" }], properties: [{ name: "SrcFile", value: "package-lock.json" }], }, ]; const result = trimComponents(components); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0].hashes, [ { alg: "SHA-512", content: "abc123" }, ]); }); it("merges and deduplicates hashes from duplicate components", () => { const components = [ { name: "jquery", version: "3.5.1", purl: "pkg:npm/jquery@3.5.1", type: "library", hashes: [{ alg: "SHA-512", content: "abc123" }], properties: [{ name: "SrcFile", value: "Scripts/jquery.min.js" }], }, { name: "jquery", version: "3.5.1", purl: "pkg:npm/jquery@3.5.1", type: "framework", hashes: [ { alg: "SHA-512", content: "abc123" }, { alg: "SHA-256", content: "def456" }, ], properties: [{ name: "SrcFile", value: "package-lock.json" }], }, ]; const result = trimComponents(components); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0].hashes, [ { alg: "SHA-512", content: "abc123" }, { alg: "SHA-256", content: "def456" }, ]); }); it("retains identity tool references when merging duplicate components", () => { const components = [ { name: "openssl", version: "3.0.0", purl: "pkg:rpm/redhat/openssl@3.0.0", type: "library", evidence: { identity: [ { field: "purl", confidence: 1, methods: [ { technique: "binary-analysis", confidence: 1, value: "openssl", }, ], tools: ["pkg:generic/trivy@0.1.0"], }, ], }, }, { name: "openssl", version: "3.0.0", purl: "pkg:rpm/redhat/openssl@3.0.0", type: "library", evidence: { identity: [ { field: "purl", confidence: 1, methods: [ { technique: "binary-analysis", confidence: 1, value: "openssl", }, ], tools: ["pkg:generic/blint@1.2.3"], }, ], }, }, ]; const result = trimComponents(components); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0].evidence.identity[0].tools, [ "pkg:generic/trivy@0.1.0", "pkg:generic/blint@1.2.3", ]); }); }); describe("mergeServices()", () => { it("merges matching services and deduplicates endpoints and properties", () => { const result = mergeServices( [ { "bom-ref": "urn:service:mcp:demo:1.0.0", name: "demo", version: "1.0.0", endpoints: ["/mcp"], authenticated: false, properties: [{ name: "cdx:mcp:transport", value: "streamable-http" }], }, ], [ { "bom-ref": "urn:service:mcp:demo:1.0.0", name: "demo", version: "1.0.0", endpoints: ["/mcp", "/.well-known/oauth-authorization-server"], authenticated: true, "x-trust-boundary": true, properties: [ { name: "cdx:mcp:transport", value: "streamable-http" }, { name: "cdx:mcp:capabilities:tools", value: "true" }, ], }, ], ); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0].endpoints, [ "/mcp", "/.well-known/oauth-authorization-server", ]); assert.strictEqual(result[0].authenticated, true); assert.strictEqual(result[0]["x-trust-boundary"], true); assert.strictEqual(result[0].properties.length, 2); }); it("retains distinct services", () => { const result = mergeServices( [{ "bom-ref": "urn:service:mcp:stdio:1.0.0", name: "stdio" }], [{ "bom-ref": "urn:service:mcp:http:1.0.0", name: "http" }], ); assert.strictEqual(result.length, 2); }); it("normalizes string endpoints when merging matching services", () => { const result = mergeServices( [ { "bom-ref": "urn:service:mcp:demo:1.0.0", endpoints: "/mcp", name: "demo", }, ], [ { "bom-ref": "urn:service:mcp:demo:1.0.0", endpoints: ["/mcp", "/health"], name: "demo", }, ], ); assert.deepStrictEqual(result[0].endpoints, ["/mcp", "/health"]); }); });