@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
334 lines (315 loc) • 10.6 kB
JavaScript
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"]);
});
});