@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
422 lines (413 loc) • 12.7 kB
JavaScript
import { readFileSync } from "node:fs";
import { join } from "node:path";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { PackageURL } from "packageurl-js";
import { DEBUG_MODE, dirNameStr, isPartialTree } from "./utils.js";
import { URL } from "node:url";
import { thoughtLog } from "./logger.js";
const dirName = dirNameStr;
/**
* Validate the generated bom using jsonschema
*
* @param {object} bomJson content
*
* @returns {Boolean} true if the BOM is valid. false otherwise.
*/
export const validateBom = (bomJson) => {
if (!bomJson) {
return true;
}
const schema = JSON.parse(
readFileSync(
join(dirName, "data", `bom-${bomJson.specVersion}.schema.json`),
"utf-8",
),
);
const defsSchema = JSON.parse(
readFileSync(join(dirName, "data", "jsf-0.82.schema.json"), "utf-8"),
);
const spdxSchema = JSON.parse(
readFileSync(join(dirName, "data", "spdx.schema.json"), "utf-8"),
);
const ajv = new Ajv({
schemas: [schema, defsSchema, spdxSchema],
strict: false,
logger: false,
verbose: true,
code: {
source: true,
lines: true,
optimize: true,
},
});
addFormats(ajv);
const validate = ajv.getSchema(
`http://cyclonedx.org/schema/bom-${bomJson.specVersion}.schema.json`,
);
const isValid = validate(bomJson);
if (!isValid) {
console.log(
`Schema validation failed for ${bomJson.metadata.component.name}`,
);
console.log(validate.errors);
return false;
}
// Deep validation tests
return (
validateMetadata(bomJson) &&
validatePurls(bomJson) &&
validateRefs(bomJson) &&
validateProps(bomJson)
);
};
/**
* Validate the metadata object
*
* @param {object} bomJson Bom json object
*/
export const validateMetadata = (bomJson) => {
const errorList = [];
const warningsList = [];
if (bomJson?.metadata) {
if (
!bomJson.metadata.component ||
!Object.keys(bomJson.metadata.component).length
) {
warningsList.push(
"metadata.component is missing. Run cdxgen with both --project-name and --project-version argument.",
);
}
if (bomJson.metadata.component) {
// Do we have a purl and bom-ref for metadata.component
if (!bomJson.metadata.component.purl) {
warningsList.push("purl is missing for metadata.component");
}
if (!bomJson.metadata.component["bom-ref"]) {
warningsList.push("bom-ref is missing for metadata.component");
}
// Do we have a version for metadata.component
if (!bomJson.metadata.component.version) {
warningsList.push(
"Version is missing for metadata.component. Pass the version using --project-version argument.",
);
}
// Is the same component getting repeated inside the components block
if (bomJson.metadata.component.components?.length) {
for (const comp of bomJson.metadata.component.components) {
if (comp["bom-ref"] === bomJson.metadata.component["bom-ref"]) {
warningsList.push(
`Found parent component with ref ${comp["bom-ref"]} in metadata.component.components`,
);
} else if (
(!comp["bom-ref"] || !bomJson.metadata.component["bom-ref"]) &&
comp["name"] === bomJson.metadata.component["name"]
) {
warningsList.push(
`Found parent component with name ${comp["name"]} in metadata.component.components`,
);
}
}
}
}
}
if (DEBUG_MODE && warningsList.length !== 0) {
console.log("===== WARNINGS =====");
console.log(warningsList);
thoughtLog(
"**VALIDATION**: There are some warnings regarding the BOM Metadata.",
);
}
if (errorList.length !== 0) {
console.log(errorList);
return false;
}
return true;
};
/**
* Validate the format of all purls
*
* @param {object} bomJson Bom json object
*/
export const validatePurls = (bomJson) => {
const errorList = [];
const warningsList = [];
let frameworksCount = 0;
if (bomJson?.components) {
for (const comp of bomJson.components) {
if (comp.type === "framework") {
frameworksCount += 1;
}
if (comp.type === "cryptographic-asset") {
if (comp.purl?.length) {
errorList.push(
`purl should not be defined for cryptographic-asset ${comp.purl}`,
);
}
if (!comp.cryptoProperties) {
errorList.push(
`cryptoProperties is missing for cryptographic-asset ${comp.purl}`,
);
} else if (
comp.cryptoProperties.assetType === "algorithm" &&
!comp.cryptoProperties.oid
) {
errorList.push(
`cryptoProperties.oid is missing for cryptographic-asset of type algorithm ${comp.purl}`,
);
} else if (
comp.cryptoProperties.assetType === "certificate" &&
!comp.cryptoProperties.algorithmProperties
) {
errorList.push(
`cryptoProperties.algorithmProperties is missing for cryptographic-asset of type certificate ${comp.purl}`,
);
}
} else {
try {
const purlObj = PackageURL.fromString(comp.purl);
if (purlObj.type && purlObj.type !== purlObj.type.toLowerCase()) {
warningsList.push(
`purl type is not normalized to lower case ${comp.purl}`,
);
}
if (
["npm", "golang"].includes(purlObj.type) &&
purlObj.name.includes("%2F") &&
!purlObj.namespace
) {
errorList.push(
`purl does not include namespace but includes encoded slash in name for npm type. ${comp.purl}`,
);
}
} catch (ex) {
errorList.push(`Invalid purl ${comp.purl}`);
}
}
}
}
if (frameworksCount > 20) {
warningsList.push(
`BOM likey has too many framework components. Count: ${frameworksCount}`,
);
}
if (DEBUG_MODE && warningsList.length !== 0) {
console.log("===== WARNINGS =====");
console.log(warningsList);
thoughtLog(
"**VALIDATION**: There are some warnings regarding the purls in our SBOM. These could be bugs.",
);
}
if (errorList.length !== 0) {
console.log(errorList);
return false;
}
return true;
};
const buildRefs = (bomJson) => {
const refMap = {};
if (bomJson) {
if (bomJson.metadata) {
if (bomJson.metadata.component) {
refMap[bomJson.metadata.component["bom-ref"]] = true;
if (bomJson.metadata.component.components) {
for (const comp of bomJson.metadata.component.components) {
refMap[comp["bom-ref"]] = true;
}
}
}
}
if (bomJson.components) {
for (const comp of bomJson.components) {
refMap[comp["bom-ref"]] = true;
}
}
}
return refMap;
};
/**
* Validate the refs in dependencies block
*
* @param {object} bomJson Bom json object
*/
export const validateRefs = (bomJson) => {
const errorList = [];
const warningsList = [];
const refMap = buildRefs(bomJson);
const parentComponentRef = bomJson?.metadata?.component?.["bom-ref"];
if (bomJson?.dependencies) {
if (isPartialTree(bomJson.dependencies, bomJson?.components?.length)) {
warningsList.push(
"Dependency tree has multiple empty dependsOn attributes.",
);
}
for (const dep of bomJson.dependencies) {
if (
dep.ref.includes("%40") ||
dep.ref.includes("%3A") ||
dep.ref.includes("%2F")
) {
errorList.push(`Invalid encoded ref in dependencies ${dep.ref}`);
}
if (!refMap[dep.ref]) {
warningsList.push(`Invalid ref in dependencies ${dep.ref}`);
}
let parentPurlType;
try {
const purlObj = PackageURL.fromString(dep.ref);
parentPurlType = purlObj.type;
} catch (e) {
// pass
}
if (
parentComponentRef &&
dep.ref === parentComponentRef &&
dep.dependsOn.length === 0 &&
bomJson.dependencies.length > 1
) {
warningsList.push(
`Parent component ${parentComponentRef} doesn't have any children. The dependency tree must contain dangling nodes, which are unsupported by tools such as Dependency-Track.`,
);
}
if (dep.dependsOn) {
for (const don of dep.dependsOn) {
if (!refMap[don]) {
warningsList.push(
`Invalid ref in dependencies.dependsOn ${don}. Parent: ${dep.ref}`,
);
}
let childPurlType;
try {
const purlObj = PackageURL.fromString(don);
childPurlType = purlObj.type;
} catch (e) {
// pass
}
if (
parentPurlType &&
childPurlType &&
parentPurlType !== childPurlType &&
!["oci", "generic", "container"].includes(parentPurlType)
) {
warningsList.push(
`The parent package '${dep.ref}' (type ${parentPurlType}) depends on the child package '${don}' (type ${childPurlType}). This is a bug in cdxgen if this project is not a monorepo.`,
);
}
}
}
if (dep.provides) {
for (const don of dep.provides) {
if (!refMap[don]) {
warningsList.push(`Invalid ref in dependencies.provides ${don}`);
}
}
}
}
}
if (DEBUG_MODE && warningsList.length !== 0) {
console.log("===== WARNINGS =====");
console.log(warningsList);
thoughtLog(
"**VALIDATION**: There are some warnings regarding the dependency tree in our BOM.",
);
}
if (errorList.length !== 0) {
console.log(errorList);
return false;
}
return true;
};
/**
* Validate the component properties
*
* @param {object} bomJson Bom json object
*/
export function validateProps(bomJson) {
const errorList = [];
const warningsList = [];
let isWorkspaceMode = false;
let lacksProperties = false;
let lacksEvidence = false;
let lacksRelativePath = false;
if (
!["application", "framework", "library"].includes(
bomJson?.metadata?.component?.type,
)
) {
return true;
}
if (bomJson?.components) {
for (const comp of bomJson.components) {
if (!["library", "framework"].includes(comp.type)) {
continue;
}
// Limit to only npm and pypi for now
if (
!comp.purl?.startsWith("pkg:npm") &&
!comp.purl?.startsWith("pkg:pypi")
) {
continue;
}
if (!comp.properties) {
if (!lacksProperties) {
warningsList.push(`${comp["bom-ref"]} lacks properties.`);
lacksProperties = true;
}
} else {
let srcFilePropFound = false;
let workspacePropFound = false;
for (const p of comp.properties) {
if (p.name === "SrcFile") {
srcFilePropFound = true;
// Quick linux/unix only check for relative paths.
if (!lacksRelativePath && p.value?.startsWith("/")) {
lacksRelativePath = true;
}
}
if (p.name === "internal:workspaceRef") {
isWorkspaceMode = true;
workspacePropFound = true;
}
}
if (
isWorkspaceMode &&
!workspacePropFound &&
!srcFilePropFound &&
comp?.scope !== "optional"
) {
warningsList.push(
`${comp["bom-ref"]} lacks workspace-related properties.`,
);
}
if (!srcFilePropFound && !lacksProperties) {
warningsList.push(`${comp["bom-ref"]} lacks SrcFile property.`);
lacksProperties = true;
}
}
if (!comp.evidence && !lacksEvidence) {
lacksEvidence = true;
warningsList.push(`${comp["bom-ref"]} lacks evidence.`);
}
}
}
if (lacksRelativePath) {
warningsList.push(
"BOM includes absolute paths for properties like SrcFile.",
);
thoughtLog(
"BOM still includes absolute paths for properties like SrcFile. My postgen optimizations didn't work completely.",
);
}
if (DEBUG_MODE && warningsList.length !== 0) {
console.log("===== WARNINGS =====");
console.log(warningsList);
thoughtLog(
"**VALIDATION**: There are some warnings regarding the evidence attribute in our BOM, which can be safely ignored.",
);
}
if (errorList.length !== 0) {
console.log(errorList);
return false;
}
return true;
}