UNPKG

@cyclonedx/cdxgen

Version:

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

244 lines (221 loc) 7.36 kB
import { readFileSync } from "node:fs"; import { createBom, decodeBomBinary, decodeBomJson, encodeBomBinary, encodeBomJson, parseBomBinary, parseBomJson, supportedSpecVersions, } from "@appthreat/cdx-proto"; import { toCycloneDxSpecVersionString } from "./bomUtils.js"; import { safeExistsSync, safeWriteSync } from "./utils.js"; const JSON_READ_OPTIONS = { ignoreUnknownFields: true, }; const BINARY_READ_OPTIONS = { readUnknownFields: true, }; const BINARY_WRITE_OPTIONS = { writeUnknownFields: true, }; const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"]; const DEFAULT_SPEC_VERSION = supportedSpecVersions[supportedSpecVersions.length - 1]; const PROTO_SUPPORTED_SPEC_VERSIONS = new Set( supportedSpecVersions.map((specVersion) => toCycloneDxSpecVersionString(specVersion), ), ); const isProtoMessageBom = (bom) => Boolean( bom && typeof bom === "object" && !Array.isArray(bom) && typeof bom.$typeName === "string" && bom.specVersion, ); const hasExplicitSpecVersion = (bomJson) => Boolean( bomJson && typeof bomJson === "object" && !Array.isArray(bomJson) && (bomJson.specVersion !== undefined || bomJson.spec_version !== undefined), ); const resolveExplicitSpecVersion = (bomJson) => bomJson?.specVersion ?? bomJson?.spec_version; const hasProvidedSpecVersion = (specVersion) => specVersion !== undefined && specVersion !== null && `${specVersion}`.trim() !== ""; export const isProtoSupportedSpecVersion = (specVersion) => { if (!hasProvidedSpecVersion(specVersion)) { return true; } const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion); return ( normalizedSpecVersion !== undefined && PROTO_SUPPORTED_SPEC_VERSIONS.has(normalizedSpecVersion) ); }; export const assertProtoSupportedSpecVersion = ( specVersion, operation = "protobuf operations", ) => { if (!hasProvidedSpecVersion(specVersion)) { return; } const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion); if (isProtoSupportedSpecVersion(specVersion)) { return; } const displaySpecVersion = normalizedSpecVersion || `${specVersion}`.trim(); throw new Error( `CycloneDX ${displaySpecVersion} is not currently supported for ${operation}. @appthreat/cdx-proto supports ${supportedSpecVersions.join(", ")} only.`, ); }; const OBJECT_WRAPPED_LIST_FIELDS = ["declarations", "definitions"]; const isPlainObject = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value)); const normalizeObjectWrappedListsForProto = (bomJson) => { if (!isPlainObject(bomJson)) { return bomJson; } const normalizedBomJson = { ...bomJson }; for (const fieldName of OBJECT_WRAPPED_LIST_FIELDS) { if (isPlainObject(normalizedBomJson[fieldName])) { normalizedBomJson[fieldName] = [normalizedBomJson[fieldName]]; } } return normalizedBomJson; }; const mergeObjectWrappedListEntries = (entries) => { const mergedEntry = {}; for (const entry of entries) { if (!isPlainObject(entry)) { continue; } for (const [key, value] of Object.entries(entry)) { if (value === undefined) { continue; } if (Array.isArray(value)) { mergedEntry[key] = [...(mergedEntry[key] || []), ...value]; continue; } if (isPlainObject(value) && isPlainObject(mergedEntry[key])) { mergedEntry[key] = { ...mergedEntry[key], ...value }; continue; } if (mergedEntry[key] === undefined) { mergedEntry[key] = value; } } } return Object.keys(mergedEntry).length ? mergedEntry : undefined; }; const normalizeObjectWrappedListsFromProto = (bomJson) => { if (!isPlainObject(bomJson)) { return bomJson; } const normalizedBomJson = { ...bomJson }; for (const fieldName of OBJECT_WRAPPED_LIST_FIELDS) { if (!Array.isArray(normalizedBomJson[fieldName])) { continue; } const mergedEntry = mergeObjectWrappedListEntries( normalizedBomJson[fieldName], ); if (mergedEntry) { normalizedBomJson[fieldName] = mergedEntry; } else { delete normalizedBomJson[fieldName]; } } return normalizedBomJson; }; const resolveBomMessage = (bomJson, specVersion = DEFAULT_SPEC_VERSION) => { if (isProtoMessageBom(bomJson)) { return bomJson; } if (typeof bomJson === "string" || bomJson instanceof String) { const parsedBomJson = normalizeObjectWrappedListsForProto( JSON.parse(`${bomJson}`), ); if (hasExplicitSpecVersion(parsedBomJson)) { assertProtoSupportedSpecVersion( resolveExplicitSpecVersion(parsedBomJson), "protobuf serialization", ); return parseBomJson(parsedBomJson, JSON_READ_OPTIONS); } assertProtoSupportedSpecVersion(specVersion, "protobuf serialization"); return decodeBomJson(specVersion, parsedBomJson, JSON_READ_OPTIONS); } if (bomJson && typeof bomJson === "object" && !Array.isArray(bomJson)) { const normalizedBomJson = normalizeObjectWrappedListsForProto(bomJson); if (hasExplicitSpecVersion(normalizedBomJson)) { assertProtoSupportedSpecVersion( resolveExplicitSpecVersion(normalizedBomJson), "protobuf serialization", ); return parseBomJson(normalizedBomJson, JSON_READ_OPTIONS); } assertProtoSupportedSpecVersion(specVersion, "protobuf serialization"); return decodeBomJson(specVersion, normalizedBomJson, JSON_READ_OPTIONS); } return createBom(specVersion); }; /** * Determine whether a path looks like a CycloneDX protobuf file. * * @param {string} filePath File path * @returns {boolean} true when the path looks like a protobuf BOM file */ export const isProtoBomFile = (filePath) => { const normalizedPath = `${filePath || ""}`.toLowerCase(); return PROTO_BOM_FILE_EXTENSIONS.some((extension) => normalizedPath.endsWith(extension), ); }; /** * Method to convert the given bom json to proto binary * * @param {string | Object} bomJson BOM Json * @param {string} binFile Binary file name * @param {string | number} [specVersion] CycloneDX spec version fallback for BOMs without specVersion */ export const writeBinary = ( bomJson, binFile, specVersion = DEFAULT_SPEC_VERSION, ) => { if (bomJson && binFile) { const bomMessage = resolveBomMessage(bomJson, specVersion); safeWriteSync(binFile, encodeBomBinary(bomMessage, BINARY_WRITE_OPTIONS)); } }; /** * Method to read a serialized binary * * @param {string} binFile Binary file name * @param {boolean} asJson Convert to JSON * @param {string | number} [specVersion] Optional specification version. When omitted, cdxgen auto-detects the matching schema. */ export const readBinary = (binFile, asJson, specVersion) => { asJson = asJson ?? true; assertProtoSupportedSpecVersion(specVersion, "protobuf decoding"); if (!safeExistsSync(binFile)) { return undefined; } const binaryData = readFileSync(binFile); const bomObject = specVersion !== undefined && specVersion !== null && specVersion !== "" ? decodeBomBinary(specVersion, binaryData, BINARY_READ_OPTIONS) : parseBomBinary(binaryData, BINARY_READ_OPTIONS); if (asJson) { return normalizeObjectWrappedListsFromProto(encodeBomJson(bomObject)); } return bomObject; };