UNPKG

@cyclonedx/cdxgen

Version:

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

333 lines (326 loc) 10.9 kB
import { DEBUG_MODE } from "./utils.js"; /** * Merges two CycloneDX dependency arrays into a single deduplicated list. * For each unique ref, the dependsOn and provides sets from both arrays are * combined. Self-referential entries pointing to the parent component are * removed from all dependsOn and provides lists. * * @param {Object[]} dependencies First array of dependency objects * @param {Object[]} newDependencies Second array of dependency objects to merge * @param {Object} parentComponent Parent component whose bom-ref is used to filter self-references * @returns {Object[]} Merged and deduplicated array of dependency objects */ export function mergeDependencies( dependencies, newDependencies, parentComponent = {}, ) { if (!parentComponent && DEBUG_MODE) { console.log( "Unable to determine parent component. Dependencies will be flattened.", ); } let providesFound = false; const deps_map = {}; const provides_map = {}; const parentRef = parentComponent?.["bom-ref"] ? parentComponent["bom-ref"] : undefined; const combinedDeps = dependencies.concat(newDependencies || []); for (const adep of combinedDeps) { if (!deps_map[adep.ref]) { deps_map[adep.ref] = new Set(); } if (!provides_map[adep.ref]) { provides_map[adep.ref] = new Set(); } if (adep["dependsOn"]) { for (const eachDepends of adep["dependsOn"]) { if (!eachDepends) { continue; } if (parentRef) { if (eachDepends.toLowerCase() !== parentRef.toLowerCase()) { deps_map[adep.ref].add(eachDepends); } } else { deps_map[adep.ref].add(eachDepends); } } } if (adep["provides"]) { providesFound = true; for (const eachProvides of adep["provides"]) { // Add the entry unless it is the parent itself: // when there is no parentRef every entry is kept (!parentRef is true), // when parentRef exists only entries that differ from it are kept. if ( !parentRef || eachProvides?.toLowerCase() !== parentRef?.toLowerCase() ) { provides_map[adep.ref].add(eachProvides); } } } } const retlist = []; for (const akey of Object.keys(deps_map)) { if (providesFound) { retlist.push({ ref: akey, dependsOn: Array.from(deps_map[akey]).sort(), provides: Array.from(provides_map[akey]).sort(), }); } else { retlist.push({ ref: akey, dependsOn: Array.from(deps_map[akey]).sort(), }); } } return retlist; } function serviceIdentityKey(service) { if (service?.["bom-ref"]) { return service["bom-ref"].toLowerCase(); } return `${service?.group || ""}:${service?.name || ""}:${service?.version || ""}`.toLowerCase(); } function mergeServiceProperties(existingProps = [], newProps = []) { const merged = [...existingProps]; for (const newProp of newProps) { if ( !merged.find( (prop) => prop?.name === newProp?.name && prop?.value === newProp?.value, ) ) { merged.push(newProp); } } return merged; } function normalizeServiceEndpoints(endpoints) { if (Array.isArray(endpoints)) { return endpoints.filter( (endpoint) => typeof endpoint === "string" && endpoint, ); } if (typeof endpoints === "string" && endpoints) { return [endpoints]; } return []; } /** * Merge CycloneDX services using bom-ref or group/name/version identity. * * @param {Object[]|Object} services Existing service list * @param {Object[]|Object} newServices New service list * @returns {Object[]} Merged and deduplicated services */ export function mergeServices(services, newServices) { const combined = [] .concat(services || []) .concat(newServices || []) .filter(Boolean); const serviceMap = new Map(); for (const service of combined) { const key = serviceIdentityKey(service); if (!serviceMap.has(key)) { serviceMap.set(key, { ...service, endpoints: Array.from( new Set(normalizeServiceEndpoints(service.endpoints)), ), properties: mergeServiceProperties([], service.properties || []), services: Array.isArray(service.services) ? mergeServices([], service.services) : undefined, }); continue; } const existing = serviceMap.get(key); existing.description = existing.description || service.description; existing.group = existing.group || service.group; existing.name = existing.name || service.name; existing.version = existing.version || service.version; existing.provider = existing.provider || service.provider; existing.trustZone = existing.trustZone || service.trustZone; if (service.authenticated === true) { existing.authenticated = true; } else if ( typeof existing.authenticated === "undefined" && typeof service.authenticated !== "undefined" ) { existing.authenticated = service.authenticated; } if (service["x-trust-boundary"] === true) { existing["x-trust-boundary"] = true; } else if ( typeof existing["x-trust-boundary"] === "undefined" && typeof service["x-trust-boundary"] !== "undefined" ) { existing["x-trust-boundary"] = service["x-trust-boundary"]; } const incomingEndpoints = normalizeServiceEndpoints(service.endpoints); if (incomingEndpoints.length) { existing.endpoints = Array.from( new Set([ ...normalizeServiceEndpoints(existing.endpoints), ...incomingEndpoints, ]), ); } existing.properties = mergeServiceProperties( existing.properties || [], service.properties || [], ); if (Array.isArray(service.services) && service.services.length) { existing.services = mergeServices( existing.services || [], service.services, ); } } return Array.from(serviceMap.values()); } /** * Trim duplicate components by retaining all the properties * * @param {Array} components Components * * @returns {Array} Filtered components */ export function trimComponents(components) { const keyCache = {}; const filteredComponents = []; for (const comp of components) { const key = ( comp.purl || comp["bom-ref"] || comp.name + comp.version ).toLowerCase(); if (!keyCache[key]) { keyCache[key] = comp; } else { const existingComponent = keyCache[key]; // We need to retain any properties that differ if (comp.properties) { if (existingComponent.properties) { for (const newprop of comp.properties) { if ( !existingComponent.properties.find( (prop) => prop.name === newprop.name && prop.value === newprop.value, ) ) { existingComponent.properties.push(newprop); } } } else { existingComponent.properties = comp.properties; } } if (comp.hashes) { if (existingComponent.hashes) { for (const newhash of comp.hashes) { if ( !existingComponent.hashes.find( (hash) => hash.alg === newhash.alg && hash.content === newhash.content, ) ) { existingComponent.hashes.push(newhash); } } } else { existingComponent.hashes = comp.hashes; } } // Retain all component.evidence.identity if (comp?.evidence?.identity) { if (!existingComponent.evidence) { existingComponent.evidence = { identity: [] }; } else if (!existingComponent?.evidence?.identity) { existingComponent.evidence.identity = []; } else if ( existingComponent?.evidence?.identity && !Array.isArray(existingComponent.evidence.identity) ) { existingComponent.evidence.identity = [ existingComponent.evidence.identity, ]; } // comp.evidence.identity can be an array or object // Merge the evidence.identity based on methods or objects const isIdentityArray = Array.isArray(comp.evidence.identity); const identities = isIdentityArray ? comp.evidence.identity : [comp.evidence.identity]; for (const aident of identities) { let methodBasedMerge = false; if (aident?.methods?.length) { for (const amethod of aident.methods) { for (const existIdent of existingComponent.evidence.identity) { if (existIdent.field === aident.field) { if (!existIdent.methods) { existIdent.methods = []; } if (aident.tools?.length) { existIdent.tools = Array.from( new Set([...(existIdent.tools || []), ...aident.tools]), ); } let isDup = false; for (const emethod of existIdent.methods) { if (emethod?.value === amethod?.value) { isDup = true; break; } } if (!isDup) { existIdent.methods.push(amethod); } methodBasedMerge = true; } } } } if (!methodBasedMerge && aident.field && aident.confidence) { existingComponent.evidence.identity.push(aident); } } if (!isIdentityArray) { const firstIdentity = existingComponent.evidence.identity[0]; let identConfidence = firstIdentity?.confidence; // We need to set the confidence to the max of all confidences if (firstIdentity?.methods?.length > 1) { for (const aidentMethod of firstIdentity.methods) { if ( aidentMethod?.confidence && aidentMethod.confidence > identConfidence ) { identConfidence = aidentMethod.confidence; } } } firstIdentity.confidence = identConfidence; existingComponent.evidence = { identity: firstIdentity, }; } } // If the component is required in any of the child projects, then make it required if ( existingComponent?.scope !== "required" && comp?.scope === "required" ) { existingComponent.scope = "required"; } } } for (const akey of Object.keys(keyCache)) { filteredComponents.push(keyCache[akey]); } return filteredComponents; }