@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
996 lines (957 loc) • 29.8 kB
JavaScript
import {
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { assert, it } from "poku";
import {
getRecordedActivities,
resetRecordedActivities,
setDryRunMode,
} from "../../helpers/utils.js";
import {
cleanupEnv,
cleanupTmpDir,
filterBom,
postProcess,
} from "./postgen.js";
it("filter bom tests", () => {
const bomJson = JSON.parse(
readFileSync("./test/data/bom-postgen-test.json", "utf-8"),
);
let newBom = filterBom(bomJson, {});
assert.deepStrictEqual(bomJson, newBom);
assert.deepStrictEqual(newBom.components.length, 1060);
newBom = filterBom(bomJson, { requiredOnly: true });
for (const comp of newBom.components) {
if (comp.scope && comp.scope !== "required") {
throw new Error(`${comp.scope} is unexpected`);
}
}
assert.deepStrictEqual(newBom.components.length, 345);
});
it("filter bom tests2", () => {
const bomJson = JSON.parse(
readFileSync("./test/data/bom-postgen-test2.json", "utf-8"),
);
let newBom = filterBom(bomJson, {});
assert.deepStrictEqual(bomJson, newBom);
assert.deepStrictEqual(newBom.components.length, 199);
newBom = filterBom(bomJson, { requiredOnly: true });
for (const comp of newBom.components) {
if (comp.scope && comp.scope !== "required") {
throw new Error(`${comp.scope} is unexpected`);
}
}
assert.deepStrictEqual(newBom.components.length, 199);
newBom = filterBom(bomJson, { filter: [""] });
assert.deepStrictEqual(newBom.components.length, 199);
newBom = filterBom(bomJson, { filter: ["apache"] });
for (const comp of newBom.components) {
if (comp.purl.includes("apache")) {
throw new Error(`${comp.purl} is unexpected`);
}
}
assert.deepStrictEqual(newBom.components.length, 158);
newBom = filterBom(bomJson, { filter: ["apache", "json"] });
for (const comp of newBom.components) {
if (comp.purl.includes("apache") || comp.purl.includes("json")) {
throw new Error(`${comp.purl} is unexpected`);
}
}
assert.deepStrictEqual(newBom.components.length, 135);
assert.deepStrictEqual(newBom.compositions, undefined);
newBom = filterBom(bomJson, {
only: ["org.springframework"],
specVersion: 1.5,
autoCompositions: true,
});
for (const comp of newBom.components) {
if (!comp.purl.includes("org.springframework")) {
throw new Error(`${comp.purl} is unexpected`);
}
}
assert.deepStrictEqual(newBom.components.length, 29);
assert.deepStrictEqual(newBom.compositions, [
{
aggregate: "incomplete_first_party_only",
"bom-ref": "pkg:maven/sec/java-sec-code@1.0.0?type=jar",
},
]);
});
it("exclude-type mcp removes inventory artifacts but retains MCP SDK packages", () => {
const bomJson = {
components: [
{
"bom-ref": "pkg:npm/%40modelcontextprotocol/server-filesystem@1.0.0",
name: "@modelcontextprotocol/server-filesystem",
purl: "pkg:npm/%40modelcontextprotocol/server-filesystem@1.0.0",
},
{
"bom-ref": "file:/repo/.vscode/mcp.json",
name: "mcp.json",
properties: [{ name: "cdx:file:kind", value: "mcp-config" }],
type: "file",
},
{
"bom-ref": "urn:mcp:tool:docs:search",
name: "search",
properties: [
{ name: "cdx:mcp:role", value: "tool" },
{
name: "cdx:mcp:serviceRef",
value: "urn:service:mcp:docs:latest",
},
],
type: "application",
},
],
dependencies: [
{
dependsOn: ["urn:mcp:tool:docs:search"],
ref: "urn:service:mcp:docs:latest",
},
{
provides: ["urn:mcp:tool:docs:search"],
ref: "pkg:npm/%40modelcontextprotocol/server-filesystem@1.0.0",
},
],
metadata: { properties: [] },
services: [
{
"bom-ref": "urn:service:mcp:docs:latest",
group: "mcp",
name: "docs",
properties: [{ name: "cdx:mcp:inventorySource", value: "config-file" }],
},
],
};
const filteredBom = filterBom(bomJson, { excludeType: ["mcp"] });
assert.deepStrictEqual(
filteredBom.components.map((component) => component["bom-ref"]),
["pkg:npm/%40modelcontextprotocol/server-filesystem@1.0.0"],
);
assert.deepStrictEqual(filteredBom.services, []);
assert.deepStrictEqual(filteredBom.dependencies, [
{
dependsOn: [],
provides: [],
ref: "pkg:npm/%40modelcontextprotocol/server-filesystem@1.0.0",
},
]);
});
it("postProcess adds formulation exactly once when includeFormulation is true", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.5",
components: [],
dependencies: [],
metadata: { properties: [] },
},
};
const options = { includeFormulation: true, specVersion: 1.5 };
const result = postProcess(bomNSData, options);
assert.ok(
Array.isArray(result.bomJson.formulation),
"formulation must be an array",
);
assert.ok(
result.bomJson.formulation.length > 0,
"formulation must have at least one entry",
);
});
it("postProcess does not add formulation when includeFormulation is false", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.5",
components: [],
dependencies: [],
metadata: { properties: [] },
},
};
const options = { includeFormulation: false, specVersion: 1.5 };
const result = postProcess(bomNSData, options);
assert.strictEqual(
result.bomJson.formulation,
undefined,
"formulation must not be added when disabled",
);
});
it("postProcess preserves existing formulation and does not overwrite it", () => {
const sentinel = [{ "bom-ref": "already-present" }];
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.5",
components: [],
dependencies: [],
metadata: { properties: [] },
formulation: sentinel,
},
};
const options = { includeFormulation: true, specVersion: 1.5 };
const result = postProcess(bomNSData, options);
assert.strictEqual(
result.bomJson.formulation[0]["bom-ref"],
"already-present",
"existing formulation must not be overwritten",
);
});
it("postProcess passes formulationList from bomNSData into the formulation section", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.5",
components: [],
dependencies: [],
metadata: { properties: [] },
},
formulationList: [{ type: "library", name: "pixi-pkg", version: "1.0.0" }],
};
const options = { includeFormulation: true, specVersion: 1.5 };
const result = postProcess(bomNSData, options);
assert.ok(
Array.isArray(result.bomJson.formulation),
"formulation must be present",
);
// The formulationList item should be reflected somewhere in the formulation components
const allComponents = result.bomJson.formulation.flatMap(
(f) => f.components ?? [],
);
assert.ok(
allComponents.some((c) => c.name === "pixi-pkg"),
"pixi-pkg from formulationList should appear in formulation components",
);
});
it("postProcess finalizes CycloneDX 2.0-dev root fields and strips legacy fields", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
metadata: {
manufacture: { name: "Legacy Factory" },
component: "not-a-component-object",
tools: [
{
author: "OWASP Foundation",
name: "cdxgen",
vendor: "OWASP Foundation",
version: "12.4.0",
components: [
{
author: "Nested Tool Author",
modified: true,
name: "cdxgen-plugin",
type: "library",
},
],
},
],
},
components: [
{
author: "Jane Doe",
modified: false,
name: "demo-lib",
type: "library",
version: "1.0.0",
components: [
{
author: "Nested Author",
modified: true,
name: "nested-lib",
type: "library",
},
],
},
],
},
};
const result = postProcess(bomNSData, { specVersion: 2.0 });
const [toolComponent] = result.bomJson.metadata.tools.components;
const [component] = result.bomJson.components;
assert.strictEqual(result.bomJson.specFormat, "CycloneDX");
assert.strictEqual(result.bomJson.bomFormat, undefined);
assert.strictEqual(result.bomJson.specVersion, "2.0");
assert.strictEqual(result.bomJson.metadata.manufacture, undefined);
assert.deepStrictEqual(result.bomJson.metadata.manufacturer, {
name: "Legacy Factory",
});
assert.strictEqual(
result.bomJson.metadata.component,
"not-a-component-object",
);
assert.strictEqual(toolComponent.publisher, "OWASP Foundation");
assert.deepStrictEqual(toolComponent.authors, [{ name: "OWASP Foundation" }]);
assert.strictEqual(toolComponent.author, undefined);
assert.deepStrictEqual(toolComponent.components[0].authors, [
{ name: "Nested Tool Author" },
]);
assert.strictEqual(toolComponent.components[0].author, undefined);
assert.strictEqual(toolComponent.components[0].modified, undefined);
assert.deepStrictEqual(component.authors, [{ name: "Jane Doe" }]);
assert.strictEqual(component.author, undefined);
assert.strictEqual(component.modified, undefined);
assert.deepStrictEqual(component.components[0].authors, [
{ name: "Nested Author" },
]);
assert.strictEqual(component.components[0].author, undefined);
assert.strictEqual(component.components[0].modified, undefined);
});
it("postProcess preserves malformed explicit specVersion values instead of coercing them to 1.7", () => {
const malformedBomResult = postProcess(
{
bomJson: {
bomFormat: "CycloneDX",
specVersion: "2.0.1",
components: [],
dependencies: [],
metadata: { properties: [] },
},
},
{},
);
assert.strictEqual(malformedBomResult.bomJson.specVersion, "2.0.1");
assert.strictEqual(malformedBomResult.bomJson.bomFormat, "CycloneDX");
assert.strictEqual(malformedBomResult.bomJson.specFormat, undefined);
const malformedOptionResult = postProcess(
{
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
components: [],
dependencies: [],
metadata: { properties: [] },
},
},
{ specVersion: "2.0.1" },
);
assert.strictEqual(malformedOptionResult.bomJson.specVersion, "1.7");
assert.strictEqual(malformedOptionResult.bomJson.bomFormat, "CycloneDX");
assert.strictEqual(malformedOptionResult.bomJson.specFormat, undefined);
});
it("postProcess migrates CycloneDX 2.0 metadata manufacture and tool services without broad recursion", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
metadata: {
manufacture: { name: "Component Factory" },
component: {
name: "demo-app",
type: "application",
},
tools: {
components: [],
services: [
{
author: "Legacy Author",
name: "scanner-service",
vendor: "Scanner Vendor",
},
],
},
},
components: [],
dependencies: [{ ref: "pkg:generic/demo@1.0.0", dependsOn: [] }],
unrelatedInventory: {
components: [
{
author: "Should Not Be Traversed",
name: "not-a-component-list",
},
],
},
},
};
const result = postProcess(bomNSData, { specVersion: 2.0 });
const [toolService] = result.bomJson.metadata.tools.services;
assert.strictEqual(result.bomJson.metadata.manufacture, undefined);
assert.deepStrictEqual(result.bomJson.metadata.component.manufacturer, {
name: "Component Factory",
});
assert.deepStrictEqual(toolService.provider, { name: "Scanner Vendor" });
assert.strictEqual(toolService.vendor, undefined);
assert.strictEqual(toolService.author, undefined);
assert.strictEqual(
result.bomJson.unrelatedInventory.components[0].author,
"Should Not Be Traversed",
);
});
it("postProcess downgrades certificate crypto properties for spec version 1.6", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.6",
components: [
{
type: "cryptographic-asset",
name: "demo-cert",
cryptoProperties: {
assetType: "certificate",
certificateProperties: {
serialNumber: "1234",
subjectName: "CN=demo",
issuerName: "CN=demo",
notValidBefore: "2024-01-01T00:00:00.000Z",
notValidAfter: "2034-01-01T00:00:00.000Z",
certificateFormat: "X.509",
certificateFileExtension: "crt",
fingerprint: { alg: "SHA-1", content: "a".repeat(40) },
},
},
},
],
dependencies: [],
formulation: [
{
components: [
{
type: "cryptographic-asset",
name: "formulation-cert",
cryptoProperties: {
assetType: "certificate",
certificateProperties: {
serialNumber: "5678",
subjectName: "CN=formulation",
certificateFileExtension: "pem",
fingerprint: { alg: "SHA-1", content: "b".repeat(40) },
},
},
},
],
},
],
metadata: {
properties: [],
tools: {
components: [{ name: "cdxgen" }],
},
},
},
};
const result = postProcess(bomNSData, { specVersion: 1.6 });
const componentCert =
result.bomJson.components[0].cryptoProperties.certificateProperties;
const formulationCert =
result.bomJson.formulation[0].components[0].cryptoProperties
.certificateProperties;
assert.deepStrictEqual(componentCert, {
subjectName: "CN=demo",
issuerName: "CN=demo",
notValidBefore: "2024-01-01T00:00:00.000Z",
notValidAfter: "2034-01-01T00:00:00.000Z",
certificateFormat: "X.509",
certificateExtension: "crt",
});
assert.deepStrictEqual(formulationCert, {
subjectName: "CN=formulation",
certificateExtension: "pem",
});
});
it("postProcess removes remaining 1.7-only fields from metadata, components, and formulation inventories for spec version 1.6", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.6",
components: [
{
type: "library",
name: "demo-lib",
version: "1.0.0",
isExternal: true,
patentAssertions: [{ patentNumber: "US-123" }],
versionRange: "vers:npm/>=1.0.0|<2.0.0",
},
],
dependencies: [],
formulation: [
{
components: [
{
type: "library",
name: "formulation-lib",
version: "2.0.0",
isExternal: true,
versionRange: "vers:npm/>=2.0.0|<3.0.0",
},
],
services: [
{
name: "formulation-service",
patentAssertions: [{ patentNumber: "US-456" }],
},
],
},
],
metadata: {
distributionConstraints: { tlp: "GREEN" },
component: {
type: "application",
name: "demo-app",
version: "1.0.0",
isExternal: true,
versionRange: "vers:npm/>=1.0.0|<2.0.0",
},
properties: [],
tools: {
components: [{ name: "cdxgen" }],
},
},
services: [
{
name: "demo-service",
patentAssertions: [{ patentNumber: "US-789" }],
},
],
},
};
const result = postProcess(bomNSData, { specVersion: 1.6 });
const rootComponent = result.bomJson.components[0];
const formulationComponent = result.bomJson.formulation[0].components[0];
const rootService = result.bomJson.services[0];
const formulationService = result.bomJson.formulation[0].services[0];
const metadataComponent = result.bomJson.metadata.component;
assert.strictEqual(
result.bomJson.metadata.distributionConstraints,
undefined,
);
assert.strictEqual(rootComponent.isExternal, undefined);
assert.strictEqual(rootComponent.patentAssertions, undefined);
assert.strictEqual(rootComponent.versionRange, undefined);
assert.strictEqual(formulationComponent.isExternal, undefined);
assert.strictEqual(formulationComponent.versionRange, undefined);
assert.strictEqual(rootService.patentAssertions, undefined);
assert.strictEqual(formulationService.patentAssertions, undefined);
assert.strictEqual(metadataComponent.isExternal, undefined);
assert.strictEqual(metadataComponent.versionRange, undefined);
});
it("postProcess removes remaining 1.6-only fields from metadata, components, and formulation inventories for spec version 1.5", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.5",
components: [
{
type: "library",
name: "demo-lib",
version: "1.0.0",
authors: [{ name: "Alice" }],
manufacturer: { name: "Acme" },
omniborId: ["gitoid:blob:sha1:abc"],
swhid: ["swh:1:rev:def"],
tags: ["demo"],
},
],
dependencies: [],
formulation: [
{
components: [
{
type: "library",
name: "formulation-lib",
version: "2.0.0",
authors: [{ name: "Bob" }],
manufacturer: { name: "Builder" },
omniborId: ["gitoid:blob:sha1:ghi"],
swhid: ["swh:1:dir:jkl"],
tags: ["workflow"],
},
],
services: [
{
name: "formulation-service",
tags: ["ci"],
},
],
},
],
metadata: {
manufacturer: { name: "BOM Factory" },
component: {
type: "application",
name: "demo-app",
version: "1.0.0",
authors: [{ name: "Carol" }],
manufacturer: { name: "Acme" },
tags: ["root"],
},
properties: [],
},
services: [
{
name: "demo-service",
tags: ["runtime"],
},
],
},
};
const result = postProcess(bomNSData, { specVersion: 1.5 });
const rootComponent = result.bomJson.components[0];
const formulationComponent = result.bomJson.formulation[0].components[0];
const rootService = result.bomJson.services[0];
const formulationService = result.bomJson.formulation[0].services[0];
const metadataComponent = result.bomJson.metadata.component;
assert.strictEqual(result.bomJson.metadata.manufacturer, undefined);
assert.strictEqual(rootComponent.authors, undefined);
assert.strictEqual(rootComponent.manufacturer, undefined);
assert.strictEqual(rootComponent.omniborId, undefined);
assert.strictEqual(rootComponent.swhid, undefined);
assert.strictEqual(rootComponent.tags, undefined);
assert.strictEqual(formulationComponent.authors, undefined);
assert.strictEqual(formulationComponent.manufacturer, undefined);
assert.strictEqual(formulationComponent.omniborId, undefined);
assert.strictEqual(formulationComponent.swhid, undefined);
assert.strictEqual(formulationComponent.tags, undefined);
assert.strictEqual(rootService.tags, undefined);
assert.strictEqual(formulationService.tags, undefined);
assert.strictEqual(metadataComponent.authors, undefined);
assert.strictEqual(metadataComponent.manufacturer, undefined);
assert.strictEqual(metadataComponent.tags, undefined);
});
it("postProcess removes unsupported evidence occurrence details for spec version 1.5", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.5",
components: [
{
type: "file",
name: "deviceTypeManager.js",
evidence: {
occurrences: [
{
location: "source/microservices/lib/deviceTypeManager.js",
line: 11,
offset: 2,
symbol: "deviceTypeManager",
additionalContext: "source-import",
},
],
},
},
],
dependencies: [],
metadata: { properties: [] },
},
};
const result = postProcess(bomNSData, { specVersion: 1.5 });
assert.deepStrictEqual(result.bomJson.components[0].evidence.occurrences, [
{
location: "source/microservices/lib/deviceTypeManager.js",
},
]);
});
it("postProcess merges formulation-discovered MCP config services into bomJson.services", () => {
const tmpDir = join(tmpdir(), `cdxgen-postgen-${Date.now()}`);
mkdirSync(join(tmpDir, ".vscode"), { recursive: true });
writeFileSync(
join(tmpDir, ".vscode", "mcp.json"),
JSON.stringify({
mcpServers: {
gateway: {
endpoint: "https://demo.ngrok-free.app/mcp",
transport: "http",
},
},
}),
);
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
components: [],
dependencies: [],
metadata: {
properties: [],
tools: {
components: [
{ group: "@cyclonedx", name: "cdxgen", version: "test" },
],
},
},
},
};
const options = { includeFormulation: true, specVersion: 1.7 };
try {
const result = postProcess(bomNSData, options, tmpDir);
assert.ok(
result.bomJson.services?.some(
(service) =>
service.name === "gateway" &&
service.properties?.some(
(property) =>
property.name === "cdx:mcp:inventorySource" &&
property.value === "config-file",
),
),
"expected config-discovered MCP service to be merged into bomJson.services",
);
} finally {
rmSync(tmpDir, { force: true, recursive: true });
}
});
it("postProcess labels formulation execute activities with the Formulation type", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.5",
components: [],
dependencies: [],
metadata: { properties: [] },
},
};
const options = { includeFormulation: true, specVersion: 1.5 };
setDryRunMode(true);
resetRecordedActivities();
try {
postProcess(bomNSData, options, "/home/runner/work/cdxgen/cdxgen");
const executeActivities = getRecordedActivities().filter(
(activity) => activity.kind === "execute",
);
assert.ok(
executeActivities.length > 0,
"expected formulation generation to record execute activities in dry-run mode",
);
assert.ok(
executeActivities.every(
(activity) => activity.projectType === "Formulation",
),
"formulation execute activities should be labeled with the Formulation type",
);
} finally {
setDryRunMode(false);
resetRecordedActivities();
}
});
it("postProcess attaches releaseNotes to cdxgen metadata tool component", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
components: [],
dependencies: [],
metadata: {
tools: {
components: [
{
group: "@cyclonedx",
name: "cdxgen",
version: "12.3.0",
type: "application",
},
],
},
properties: [],
},
},
};
const options = {
includeReleaseNotes: true,
releaseNotesCurrentTag: "v1.0.0",
releaseNotesPreviousTag: "v0.9.0",
specVersion: 1.7,
failOnError: true,
};
const result = postProcess(bomNSData, options);
const cdxTool = result.bomJson.metadata.tools.components[0];
assert.strictEqual(cdxTool.releaseNotes.title, "Release notes for v1.0.0");
assert.strictEqual(
cdxTool.releaseNotes.description,
"Changes between v0.9.0 and v1.0.0.",
);
assert.ok(cdxTool.releaseNotes.timestamp);
assert.deepStrictEqual(cdxTool.releaseNotes.tags, ["v1.0.0", "v0.9.0"]);
assert.ok(Array.isArray(cdxTool.releaseNotes.resolves));
for (const aresolve of cdxTool.releaseNotes.resolves) {
assert.ok(aresolve.type);
assert.ok(aresolve.id);
assert.ok(aresolve.name);
assert.ok(aresolve.description);
}
});
it("postProcess refreshes unpackaged native file inventory counts from the final BOM", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
components: [
{
name: "demo",
type: "file",
properties: [{ name: "internal:is_executable", value: "true" }],
},
{
name: "libdemo.so",
type: "file",
properties: [{ name: "internal:is_shared_library", value: "true" }],
},
],
dependencies: [],
metadata: {
properties: [
{ name: "cdx:container:unpackagedExecutableCount", value: "0" },
{
name: "cdx:container:unpackagedSharedLibraryCount",
value: "0",
},
],
tools: {
components: [
{ group: "@cyclonedx", name: "cdxgen", version: "test" },
],
},
},
},
};
const result = postProcess(bomNSData, { specVersion: 1.7 });
assert.deepStrictEqual(
result.bomJson.metadata.properties.filter((property) =>
property.name.startsWith("cdx:container:unpackaged"),
),
[
{ name: "cdx:container:unpackagedExecutableCount", value: "1" },
{ name: "cdx:container:unpackagedSharedLibraryCount", value: "1" },
],
);
});
it("postProcess fails for weak TLP when sensitive property values are present", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
components: [
{
"bom-ref": "urn:service:mcp:gateway:latest",
name: "gateway",
properties: [
{
name: "cdx:mcp:configuredEndpoints",
value:
"https://user:pass@example.com/mcp?access_token=abc123456789",
},
],
type: "application",
},
],
dependencies: [],
metadata: {
distributionConstraints: { tlp: "CLEAR" },
properties: [],
tools: {
components: [
{ group: "@cyclonedx", name: "cdxgen", version: "test" },
],
},
},
},
};
assert.throws(
() => postProcess(bomNSData, { failOnError: true, specVersion: 1.7 }),
/TLP classification 'CLEAR'/,
);
});
it("postProcess allows sensitive property values when TLP is strong", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
components: [
{
"bom-ref": "urn:service:mcp:gateway:latest",
name: "gateway",
properties: [
{
name: "cdx:mcp:command",
value: "Authorization: Bearer super-secret-token-value",
},
],
type: "application",
},
],
dependencies: [],
metadata: {
distributionConstraints: { tlp: "RED" },
properties: [],
tools: {
components: [
{ group: "@cyclonedx", name: "cdxgen", version: "test" },
],
},
},
},
};
const result = postProcess(bomNSData, {
failOnError: true,
specVersion: 1.7,
});
assert.strictEqual(
result.bomJson.metadata.distributionConstraints.tlp,
"RED",
);
});
it("postProcess does not enforce TLP validation when no TLP is set", () => {
const bomNSData = {
bomJson: {
bomFormat: "CycloneDX",
specVersion: "1.7",
components: [
{
"bom-ref": "urn:service:mcp:gateway:latest",
name: "gateway",
properties: [
{
name: "cdx:mcp:resourceUri",
value: "https://user:pass@example.com/private#fragment",
},
],
type: "application",
},
],
dependencies: [],
metadata: {
properties: [],
tools: {
components: [
{ group: "@cyclonedx", name: "cdxgen", version: "test" },
],
},
},
},
};
const result = postProcess(bomNSData, {
failOnError: true,
specVersion: 1.7,
});
assert.strictEqual(
result.bomJson.metadata.distributionConstraints,
undefined,
);
});
it("cleanup helpers do not delete directories in dry-run mode", () => {
const pipTarget = join(tmpdir(), `cdxgen-pip-${Date.now()}`);
const tmpDir = join(tmpdir(), `cdxgen-tmp-${Date.now()}`);
mkdirSync(pipTarget, { recursive: true });
mkdirSync(tmpDir, { recursive: true });
process.env.PIP_TARGET = pipTarget;
process.env.CDXGEN_TMP_DIR = tmpDir;
setDryRunMode(true);
try {
cleanupEnv({});
cleanupTmpDir();
assert.ok(existsSync(pipTarget));
assert.ok(existsSync(tmpDir));
} finally {
setDryRunMode(false);
delete process.env.PIP_TARGET;
delete process.env.CDXGEN_TMP_DIR;
rmSync(pipTarget, { recursive: true, force: true });
rmSync(tmpDir, { recursive: true, force: true });
}
});