UNPKG

@cyclonedx/cdxgen

Version:

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

1,749 lines (1,688 loc) 45.6 kB
import { readFileSync } from "node:fs"; import { cpus } from "node:os"; import { join, resolve } from "node:path"; import { resolvePluginBinary } from "./plugins.js"; import { DEBUG_MODE, dirNameStr, getTmpDir, safeExistsSync, safeMkdtempSync, safeRmSync, safeSpawnSync, } from "./utils.js"; const GO_LANGUAGES = new Set(["go", "golang"]); const GOLEM_CALLGRAPH_MODES = new Set(["none", "static", "cha", "rta", "vta"]); const GOLEM_DATAFLOW_MODES = new Set(["none", "security", "crypto", "all"]); const GOLEM_DATAFLOW_CALLGRAPH_MODES = new Set([ "none", "static", "cha", "rta", "vta", ]); const GOLEM_CRYPTO_OIDS = JSON.parse( readFileSync(join(dirNameStr, "data", "crypto-oid.json"), "utf-8"), ); const GOLEM_CRYPTO_PRIMITIVES = new Set([ "drbg", "mac", "block-cipher", "stream-cipher", "signature", "hash", "pke", "xof", "kdf", "key-agree", "kem", "ae", "combiner", "other", "unknown", ]); function golemBin() { return resolvePluginBinary("golem"); } function appendUniqueProperty(properties, name, value) { if (value === undefined || value === null || value === "") { return; } const propertyValue = String(value); if ( !properties.some( (property) => property.name === name && property.value === propertyValue, ) ) { properties.push({ name, value: propertyValue }); } } function addSetValue(map, key, value) { if (!key || !value) { return; } map[key] ??= new Set(); map[key].add(value); } function addPropertyValue(map, key, name, value) { if (!key || value === undefined || value === null || value === "") { return; } map[key] ??= []; appendUniqueProperty(map[key], name, value); } function rangeLocation(range) { const start = range?.start; if (!start?.filename) { return undefined; } if (start.line && start.line > 0) { return `${start.filename}#${start.line}`; } return start.filename; } function positionLocation(position) { if (!position?.filename) { return undefined; } if (position.line && position.line > 0) { return `${position.filename}#${position.line}`; } return position.filename; } function purlWithoutVersion(purl) { return purl?.split("?")[0].split("#")[0].split("@")[0]; } function modulePurl(module) { return module?.purl || module?.PURL; } function createPurlAliasMap(components = []) { const purlAliasMap = new Map(); for (const component of components) { if (!component?.purl) { continue; } purlAliasMap.set(component.purl, component.purl); const noVersionPurl = purlWithoutVersion(component.purl); if (noVersionPurl && !purlAliasMap.has(noVersionPurl)) { purlAliasMap.set(noVersionPurl, component.purl); } } return purlAliasMap; } function resolveComponentPurl(purl, purlAliasMap) { if (!purl) { return undefined; } return purlAliasMap.get(purl) || purlAliasMap.get(purlWithoutVersion(purl)); } function addResolvedPurl(purls, purl, purlAliasMap) { const resolvedPurl = resolveComponentPurl(purl, purlAliasMap); if (resolvedPurl) { purls.add(resolvedPurl); } } function addResolvedPurls(purls, values, purlAliasMap) { for (const value of values || []) { addResolvedPurl(purls, value, purlAliasMap); } } function symbolModule(symbol, modules = []) { if (!symbol) { return undefined; } let match; for (const module of modules) { if ( module?.path && (symbol === module.path || symbol.startsWith(`${module.path}.`)) && (!match || module.path.length > match.path.length) ) { match = module; } } return match; } function frameFromUsage(usage) { const start = usage?.range?.start; if (!start?.filename) { return undefined; } return { package: usage.enclosing?.id?.split("|")[0] || "", module: usage.enclosing?.kind || "", function: usage.enclosing?.name || "", line: start.line || undefined, column: start.column || undefined, fullFilename: start.filename, }; } function frameFromEdge(edge) { const position = edge?.position; if (!position?.filename) { return undefined; } return { package: edge.sourceId?.split(".").slice(0, -1).join(".") || "", module: edge.callType || "", function: edge.sourceName || edge.sourceId || "", line: position.line || undefined, column: position.column || undefined, fullFilename: position.filename, }; } function frameFromCallGraphNode(node, fallbackName = "") { const position = node?.position; if (!position?.filename) { return undefined; } return { package: node.packagePath || fallbackName.split(".").slice(0, -1).join("."), module: node.kind || "", function: node.label || node.name || fallbackName || "", line: position.line || undefined, column: position.column || undefined, fullFilename: position.filename, }; } function frameLocationKey(frame) { if (!frame?.fullFilename) { return undefined; } return `${frame.fullFilename}#${frame.line || ""}#${frame.column || ""}`; } function dedupeFrames(frames = []) { const seen = new Set(); const out = []; for (const frame of frames) { const key = frameLocationKey(frame); if (!key || seen.has(key)) { continue; } seen.add(key); out.push(frame); } return out; } function frameFromDataFlowNode(node, fallbackFunction) { const position = node?.position; if (!position?.filename) { return undefined; } const functionName = node.function || fallbackFunction || ""; return { package: node.packagePath || functionName.split(".").slice(0, -1).join("."), module: node.kind || node.category || "", function: functionName || node.symbol || node.name || "", line: position.line || undefined, column: position.column || undefined, fullFilename: position.filename, }; } function addFrame(dataFlowFrames, purl, frame) { if (!purl || !frame) { return; } dataFlowFrames[purl] ??= []; dataFlowFrames[purl].push([frame]); } function addCountProperty(properties, name, count) { if (count && count > 0) { appendUniqueProperty(properties, name, count); } } function normalizedPositiveInteger(value) { const intValue = Number.parseInt(value, 10); if (Number.isFinite(intValue) && intValue > 0) { return intValue; } return undefined; } function sortedCsv(values) { const filteredValues = [...new Set((values || []).filter(Boolean))].sort(); return filteredValues.length ? filteredValues.join(",") : undefined; } function incrementNestedCount(map, key, name) { if (!key || !name) { return; } map[key] ??= {}; map[key][name] = (map[key][name] || 0) + 1; } function incrementCount(map, key) { if (!key) { return; } map[key] = (map[key] || 0) + 1; } function isCryptoDataFlowSlice(slice) { return [ slice?.sourceCategory, slice?.sinkCategory, slice?.ruleId, slice?.ruleName, ...(slice?.taintKinds || []), ] .filter(Boolean) .some((value) => String(value).toLowerCase().includes("crypto")); } function cryptoBomRef(kind, name, version) { return `crypto/${kind}/${encodeURIComponent(name)}@${encodeURIComponent(version || name)}`; } function safeCryptoPrimitive(primitive) { if (GOLEM_CRYPTO_PRIMITIVES.has(primitive)) { return primitive; } return primitive ? "other" : undefined; } function cryptoSourceLocation(item) { const start = item?.range?.start; if (!start?.filename) { return undefined; } return `${start.filename}:${start.line || 0}:${start.column || 0}`; } function appendCryptoComponentProperty(component, name, value) { component.properties ??= []; appendUniqueProperty(component.properties, name, value); } function mergeCryptoComponent(componentsByRef, component, item) { const existing = componentsByRef.get(component["bom-ref"]); if (!existing) { componentsByRef.set(component["bom-ref"], component); appendCryptoComponentProperty( component, "cdx:golem:crypto:sourceLocation", cryptoSourceLocation(item), ); return component; } appendCryptoComponentProperty( existing, "cdx:golem:crypto:sourceLocation", cryptoSourceLocation(item), ); return existing; } function cryptoAlgorithmComponent(asset) { const algorithmMetadata = GOLEM_CRYPTO_OIDS[asset.name]; const oid = asset.oid || algorithmMetadata?.oid; if (!oid) { return undefined; } const primitive = safeCryptoPrimitive(asset.primitive); const component = { type: "cryptographic-asset", name: asset.name, "bom-ref": cryptoBomRef("algorithm", asset.name, oid), description: algorithmMetadata?.description || `${asset.primitive || "cryptographic"} algorithm detected by golem`, cryptoProperties: { assetType: "algorithm", oid, ...(primitive ? { algorithmProperties: { primitive } } : {}), }, properties: [], }; appendCryptoComponentProperty( component, "cdx:golem:crypto:strength", asset.strength, ); appendCryptoComponentProperty( component, "cdx:golem:crypto:symbol", asset.symbol, ); appendCryptoComponentProperty( component, "cdx:golem:crypto:usageScope", asset.usageScope, ); return component; } function cryptoCertificateComponent(asset) { const component = { type: "cryptographic-asset", name: asset.name || "X.509 certificate", "bom-ref": cryptoBomRef( "certificate", asset.name || "X.509 certificate", "x509", ), description: "Certificate asset detected by golem source analysis", cryptoProperties: { assetType: "certificate", algorithmProperties: { primitive: "unknown" }, }, properties: [], }; appendCryptoComponentProperty( component, "cdx:golem:crypto:symbol", asset.symbol, ); appendCryptoComponentProperty( component, "cdx:golem:crypto:usageScope", asset.usageScope, ); return component; } function cryptoProtocolComponent(protocol) { const protocolType = protocol.type || "unknown"; const component = { type: "cryptographic-asset", name: protocol.name || protocolType.toUpperCase(), "bom-ref": cryptoBomRef( "protocol", protocol.name || protocolType, protocol.version || protocolType, ), description: "Cryptographic protocol detected by golem source analysis", cryptoProperties: { assetType: "protocol", protocolProperties: { type: ["tls", "ssh", "ipsec", "ike", "sstp", "wpa"].includes( protocolType, ) ? protocolType : "other", ...(protocol.version ? { version: protocol.version } : {}), }, }, properties: [], }; appendCryptoComponentProperty( component, "cdx:golem:crypto:symbol", protocol.symbol, ); appendCryptoComponentProperty( component, "cdx:golem:crypto:usageScope", protocol.usageScope, ); return component; } function cryptoMaterialComponent(material) { const materialType = material.type || "unknown"; const component = { type: "cryptographic-asset", name: material.name || materialType, "bom-ref": cryptoBomRef( "material", material.name || materialType, materialType, ), description: "Related cryptographic material indicator detected by golem source analysis; raw values are not emitted", cryptoProperties: { assetType: "related-crypto-material", relatedCryptoMaterialProperties: { type: materialType }, }, properties: [], }; appendCryptoComponentProperty( component, "cdx:golem:crypto:symbol", material.symbol, ); appendCryptoComponentProperty( component, "cdx:golem:crypto:usageScope", material.usageScope, ); return component; } function addScopedProperties(componentPropertiesMap, purl, scopeCounts = {}) { const scopes = Object.keys(scopeCounts).filter( (scope) => scopeCounts[scope] > 0, ); if (!purl || !scopes.length) { return; } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:usageScopes", sortedCsv(scopes), ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:testOnly", scopes.length > 0 && scopes.every((scope) => scope !== "runtime"), ); for (const [scope, count] of Object.entries(scopeCounts)) { addCountProperty( (componentPropertiesMap[purl] ??= []), `cdx:golem:${scope}UsageCount`, count, ); } } function addOccurrenceKindProperties( componentPropertiesMap, purl, kindCounts = {}, ) { const kinds = Object.keys(kindCounts).filter((kind) => kindCounts[kind] > 0); if (!purl || !kinds.length) { return; } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:occurrenceEvidenceKinds", sortedCsv(kinds), ); for (const [kind, count] of Object.entries(kindCounts)) { addCountProperty( (componentPropertiesMap[purl] ??= []), `cdx:golem:${kind}OccurrenceCount`, count, ); } } function addMetadataProperties(properties, golemReport = {}) { appendUniqueProperty( properties, "cdx:golem:toolVersion", golemReport.tool?.version, ); appendUniqueProperty( properties, "cdx:golem:callGraphMode", golemReport.callGraph?.mode || golemReport.options?.callGraphMode, ); appendUniqueProperty( properties, "cdx:golem:dataFlowMode", golemReport.dataFlow?.mode || golemReport.options?.dataFlowMode, ); appendUniqueProperty( properties, "cdx:golem:dataFlowCallGraphMode", golemReport.options?.dataFlowCallGraphMode, ); appendUniqueProperty( properties, "cdx:golem:noRecurse", golemReport.options?.noRecurse, ); appendUniqueProperty( properties, "cdx:golem:includeAllFlows", golemReport.options?.includeAllFlows, ); appendUniqueProperty( properties, "cdx:golem:dataFlowPacks", sortedCsv(golemReport.options?.dataFlowPacks), ); addCountProperty( properties, "cdx:golem:packageCount", golemReport.stats?.packageCount, ); addCountProperty( properties, "cdx:golem:moduleCount", golemReport.stats?.moduleCount, ); addCountProperty( properties, "cdx:golem:fileCount", golemReport.stats?.fileCount, ); addCountProperty( properties, "cdx:golem:generatedFileCount", golemReport.stats?.generatedFileCount, ); addCountProperty( properties, "cdx:golem:importCount", golemReport.stats?.importCount, ); addCountProperty( properties, "cdx:golem:declarationCount", golemReport.stats?.declarationCount, ); addCountProperty( properties, "cdx:golem:usageCount", golemReport.stats?.usageCount, ); for (const scope of ["runtime", "test", "benchmark", "fuzz", "example"]) { addCountProperty( properties, `cdx:golem:${scope}UsageCount`, golemReport.stats?.[`${scope}UsageCount`], ); } addCountProperty( properties, "cdx:golem:buildDirectiveCount", golemReport.stats?.buildDirectiveCount, ); addCountProperty( properties, "cdx:golem:nativeArtifactCount", golemReport.stats?.nativeArtifactCount, ); addCountProperty( properties, "cdx:golem:securitySignalCount", golemReport.stats?.securitySignalCount, ); addCountProperty( properties, "cdx:golem:dataFlowSourceCount", golemReport.stats?.dataFlowSourceCount, ); addCountProperty( properties, "cdx:golem:dataFlowSinkCount", golemReport.stats?.dataFlowSinkCount, ); addCountProperty( properties, "cdx:golem:dataFlowSliceCount", golemReport.stats?.dataFlowSliceCount, ); addCountProperty( properties, "cdx:golem:diagnosticCount", golemReport.stats?.diagnosticCount, ); addCountProperty( properties, "cdx:golem:goModReplaceCount", golemReport.stats?.goModReplaceCount, ); addCountProperty( properties, "cdx:golem:goModExcludeCount", golemReport.stats?.goModExcludeCount, ); addCountProperty( properties, "cdx:golem:vendorModuleCount", golemReport.stats?.vendorModuleCount, ); addCountProperty( properties, "cdx:golem:workspaceModuleCount", golemReport.stats?.workspaceModuleCount, ); addCountProperty( properties, "cdx:golem:privateModuleHintCount", golemReport.stats?.privateModuleHintCount, ); addCountProperty( properties, "cdx:golem:licenseFileModuleCount", golemReport.stats?.licenseFileModuleCount, ); addCountProperty( properties, "cdx:golem:callGraphNodeCount", golemReport.callGraph?.stats?.nodeCount, ); addCountProperty( properties, "cdx:golem:callGraphEdgeCount", golemReport.callGraph?.stats?.edgeCount, ); addCountProperty( properties, "cdx:golem:dataFlowNodeCount", golemReport.dataFlow?.stats?.nodeCount, ); addCountProperty( properties, "cdx:golem:dataFlowEdgeCount", golemReport.dataFlow?.stats?.edgeCount, ); addCountProperty( properties, "cdx:golem:dataFlowSummaryCount", golemReport.dataFlow?.stats?.summaryCount, ); addCountProperty( properties, "cdx:golem:dataFlowCandidateFunctionCount", golemReport.dataFlow?.stats?.candidateFunctionCount, ); addCountProperty( properties, "cdx:golem:dataFlowFunctionCount", golemReport.dataFlow?.stats?.functionCount, ); addCountProperty( properties, "cdx:golem:dataFlowSkippedFunctionCount", golemReport.dataFlow?.stats?.skippedFunctionCount, ); addCountProperty( properties, "cdx:golem:dataFlowInstructionCount", golemReport.dataFlow?.stats?.instructionCount, ); addCountProperty( properties, "cdx:golem:dataFlowWorkerCount", golemReport.dataFlow?.stats?.workerCount, ); addCountProperty( properties, "cdx:golem:dataFlowElapsedMillis", golemReport.dataFlow?.stats?.elapsedMillis, ); addCountProperty( properties, "cdx:golem:dataFlowUniqueFlowCount", golemReport.dataFlow?.stats?.uniqueFlowCount, ); addCountProperty( properties, "cdx:golem:dataFlowDuplicateSliceCount", golemReport.dataFlow?.stats?.duplicateSliceCount, ); addCountProperty( properties, "cdx:golem:dataFlowDuplicateGroupCount", golemReport.dataFlow?.stats?.duplicateGroupCount, ); addCountProperty( properties, "cdx:golem:dataFlowMaxPathLength", golemReport.dataFlow?.stats?.maxPathLength, ); addCountProperty( properties, "cdx:golem:dataFlowSanitizedSliceCount", golemReport.dataFlow?.stats?.sanitizedSliceCount, ); appendUniqueProperty( properties, "cdx:golem:dataFlowTruncated", golemReport.dataFlow?.stats?.truncated, ); appendUniqueProperty( properties, "cdx:golem:dataFlowTruncationReasons", sortedCsv(golemReport.dataFlow?.stats?.truncationReasons), ); appendUniqueProperty( properties, "cdx:golem:buildDirectiveKinds", sortedCsv( (golemReport.buildDirectives || []).map((directive) => directive.kind), ), ); appendUniqueProperty( properties, "cdx:golem:nativeArtifactKinds", sortedCsv( (golemReport.nativeArtifacts || []).map((artifact) => artifact.kind), ), ); appendUniqueProperty( properties, "cdx:golem:securitySignalCategories", sortedCsv( (golemReport.securitySignals || []).map((signal) => signal.category), ), ); appendUniqueProperty( properties, "cdx:golem:securitySignalSeverities", sortedCsv( (golemReport.securitySignals || []).map((signal) => signal.severity), ), ); appendUniqueProperty( properties, "cdx:golem:generatorKinds", sortedCsv((golemReport.files || []).map((file) => file.generatedBy)), ); appendUniqueProperty( properties, "cdx:golem:goDirectiveVersion", golemReport.supplyChain?.goDirectiveVersion, ); appendUniqueProperty( properties, "cdx:golem:toolchainDirective", golemReport.supplyChain?.toolchainDirective, ); appendUniqueProperty( properties, "cdx:golem:goWorkPresent", golemReport.supplyChain?.goWorkPresent, ); appendUniqueProperty( properties, "cdx:golem:vendorDirectoryPresent", golemReport.supplyChain?.vendorDirectoryPresent, ); addCountProperty( properties, "cdx:golem:goGenerateCount", (golemReport.buildDirectives || []).filter( (directive) => directive.kind === "go-generate", ).length, ); addCountProperty( properties, "cdx:golem:goEmbedCount", (golemReport.buildDirectives || []).filter( (directive) => directive.kind === "go-embed", ).length, ); } function addSupplyChainProperties( componentPropertiesMap, metadataProperties, purlAliasMap, supplyChain = {}, ) { for (const directive of supplyChain.replaces || []) { appendUniqueProperty( metadataProperties, "cdx:golem:replaceModule", directive.modulePath, ); appendUniqueProperty( metadataProperties, "cdx:golem:replaceTargetPathKind", directive.targetPathKind, ); appendUniqueProperty( metadataProperties, "cdx:golem:localReplacementPresent", directive.localReplacement, ); } for (const directive of supplyChain.excludes || []) { appendUniqueProperty( metadataProperties, "cdx:golem:excludeModule", directive.modulePath, ); } for (const module of supplyChain.modules || []) { const purl = resolveComponentPurl(module.purl || module.PURL, purlAliasMap); if (!purl) { continue; } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:vendored", module.vendored, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:privateModuleCandidate", module.privateModuleCandidate, ); addCountProperty( (componentPropertiesMap[purl] ??= []), "cdx:golem:licenseFileCount", module.licenseFiles?.length, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:licenseFiles", sortedCsv(module.licenseFiles), ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:replacementModule", module.properties?.replacementModule, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:localReplacement", module.properties?.localReplacement, ); } } function addModuleProperties( componentPropertiesMap, purlAliasMap, modules = [], ) { for (const module of modules) { const purl = resolveComponentPurl(modulePurl(module), purlAliasMap); if (!purl) { continue; } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:modulePath", module.path, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:goVersion", module.goVersion, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:mainModule", module.main, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:replacementModule", module.replace?.path, ); } } function addSignalProperties( componentPropertiesMap, purlAliasMap, golemReport = {}, ) { const modules = golemReport.modules || []; for (const signal of golemReport.securitySignals || []) { const module = symbolModule(signal.packagePath, modules); const purl = resolveComponentPurl(modulePurl(module), purlAliasMap); if (!purl) { continue; } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:securitySignalCategory", signal.category, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:securitySignalSeverity", signal.severity, ); } } function addCryptoEvidence( componentPropertiesMap, metadataProperties, purlAliasMap, golemReport, cryptoComponentsByRef, cryptoGeneratePurls, ) { const crypto = golemReport?.crypto; if (!crypto) { return; } addCountProperty( metadataProperties, "cdx:golem:cryptoLibraryCount", crypto.libraries?.length, ); addCountProperty( metadataProperties, "cdx:golem:cryptoAssetCount", crypto.assets?.length, ); addCountProperty( metadataProperties, "cdx:golem:cryptoOperationCount", crypto.operations?.length, ); addCountProperty( metadataProperties, "cdx:golem:cryptoMaterialCount", crypto.materials?.length, ); addCountProperty( metadataProperties, "cdx:golem:cryptoProtocolCount", crypto.protocols?.length, ); addCountProperty( metadataProperties, "cdx:golem:cryptoFindingCount", crypto.findings?.length, ); appendUniqueProperty( metadataProperties, "cdx:golem:cryptoAlgorithms", sortedCsv( (crypto.assets || []).map((asset) => asset.assetType === "algorithm" ? asset.name : undefined, ), ), ); appendUniqueProperty( metadataProperties, "cdx:golem:cryptoMaterialTypes", sortedCsv((crypto.materials || []).map((material) => material.type)), ); appendUniqueProperty( metadataProperties, "cdx:golem:cryptoProtocols", sortedCsv((crypto.protocols || []).map((protocol) => protocol.type)), ); for (const asset of crypto.assets || []) { let component; if (asset.assetType === "algorithm") { component = cryptoAlgorithmComponent(asset); } else if (asset.assetType === "certificate") { component = cryptoCertificateComponent(asset); } if (component) { mergeCryptoComponent(cryptoComponentsByRef, component, asset); } } for (const protocol of crypto.protocols || []) { mergeCryptoComponent( cryptoComponentsByRef, cryptoProtocolComponent(protocol), protocol, ); } for (const material of crypto.materials || []) { mergeCryptoComponent( cryptoComponentsByRef, cryptoMaterialComponent(material), material, ); } const componentRefByAssetId = new Map(); for (const asset of crypto.assets || []) { const component = asset.assetType === "algorithm" ? cryptoAlgorithmComponent(asset) : asset.assetType === "certificate" ? cryptoCertificateComponent(asset) : undefined; if (component) { componentRefByAssetId.set(asset.id, component["bom-ref"]); } } const modules = golemReport.modules || []; for (const operation of crypto.operations || []) { const module = symbolModule(operation.packagePath, modules); const purl = resolveComponentPurl(modulePurl(module), purlAliasMap); if (!purl) { continue; } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoOperationType", operation.operationType, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoAlgorithm", operation.algorithm, ); const assetRef = componentRefByAssetId.get(operation.assetId); if (assetRef) { cryptoGeneratePurls[purl] ??= new Set(); cryptoGeneratePurls[purl].add(assetRef); } } for (const finding of crypto.findings || []) { const module = symbolModule(finding.packagePath, modules); const purl = resolveComponentPurl(modulePurl(module), purlAliasMap); if (!purl) { continue; } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoFinding", finding.ruleId, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoFindingSeverity", finding.severity, ); } for (const slice of golemReport.dataFlow?.slices || []) { if (!isCryptoDataFlowSlice(slice)) { continue; } const module = symbolModule(slice.sinkPackagePath, modules); const purl = resolveComponentPurl(slice.sinkPurl, purlAliasMap) || resolveComponentPurl(modulePurl(module), purlAliasMap); if (!purl) { continue; } const asset = (crypto.assets || []).find( (item) => item.symbol === slice.sinkSymbol || (item.name && slice.sinkSymbol?.toLowerCase().includes(item.name)), ); const assetRef = componentRefByAssetId.get(asset?.id); if (assetRef) { cryptoGeneratePurls[purl] ??= new Set(); cryptoGeneratePurls[purl].add(assetRef); } } } function dataFlowNodeCache(golemReport = {}) { const nodeCache = new Map(); for (const node of golemReport.dataFlow?.nodes || []) { nodeCache.set(node.id, node); } return nodeCache; } function resolveDataFlowSlicePurls(slice, nodeCache, modules, purlAliasMap) { const purls = new Set(); addResolvedPurls(purls, slice.purls, purlAliasMap); addResolvedPurls(purls, [slice.sourcePurl, slice.sinkPurl], purlAliasMap); for (const packagePath of [slice.sourcePackagePath, slice.sinkPackagePath]) { const resolvedPurl = resolveComponentPurl( modulePurl(symbolModule(packagePath, modules)), purlAliasMap, ); if (resolvedPurl) { purls.add(resolvedPurl); } } for (const nodeId of slice.nodeIds || []) { const node = nodeCache.get(nodeId); const resolvedPurl = resolveComponentPurl(node?.purl, purlAliasMap) || resolveComponentPurl(modulePurl(node?.module), purlAliasMap) || resolveComponentPurl( modulePurl(symbolModule(node?.packagePath, modules)), purlAliasMap, ); if (resolvedPurl) { purls.add(resolvedPurl); } } return purls; } function dataFlowSliceFrames(slice, nodeCache) { const frames = []; for (const nodeId of slice.nodeIds || []) { const node = nodeCache.get(nodeId); const frame = frameFromDataFlowNode(node, slice.sinkFunction); if (frame) { frames.push(frame); } } if (!frames.length) { for (const nodeId of [slice.sourceId, slice.sinkId]) { const node = nodeCache.get(nodeId); const frame = frameFromDataFlowNode(node, slice.sinkFunction); if (frame) { frames.push(frame); } } } return frames; } function addDataFlowEvidence( golemReport, purlAliasMap, purlLocationMap, dataFlowFrames, componentPropertiesMap, scopeCountsMap, occurrenceKindCountsMap, metadataProperties, ) { const dataFlow = golemReport.dataFlow; if (!dataFlow) { return; } const nodeCache = dataFlowNodeCache(golemReport); const modules = golemReport.modules || []; const dataFlowCounts = {}; const cryptoDataFlowCounts = {}; const slices = [...(dataFlow.slices || [])].sort( (left, right) => Number(isCryptoDataFlowSlice(right)) - Number(isCryptoDataFlowSlice(left)), ); for (const slice of slices) { const purls = resolveDataFlowSlicePurls( slice, nodeCache, modules, purlAliasMap, ); if (!purls.size) { continue; } const frames = dataFlowSliceFrames(slice, nodeCache); const category = [slice.sourceCategory, slice.sinkCategory] .filter(Boolean) .join("->"); const taintKinds = sortedCsv(slice.taintKinds); const sourceNode = nodeCache.get(slice.sourceId); const sinkNode = nodeCache.get(slice.sinkId); for (const purl of purls) { incrementCount(dataFlowCounts, purl); incrementNestedCount(occurrenceKindCountsMap, purl, "dataFlowSlice"); incrementNestedCount(scopeCountsMap, purl, slice.sinkScope || "runtime"); addSetValue( purlLocationMap, purl, positionLocation(sourceNode?.position), ); addSetValue(purlLocationMap, purl, positionLocation(sinkNode?.position)); if (frames.length) { dataFlowFrames[purl] ??= []; dataFlowFrames[purl].push(frames); } addPropertyValue( componentPropertiesMap, purl, "cdx:golem:dataFlowCategories", category, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:dataFlowRuleId", slice.ruleId, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:dataFlowSeverity", slice.severity, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:dataFlowConfidence", slice.confidence, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:dataFlowTaintKinds", taintKinds, ); if (isCryptoDataFlowSlice(slice)) { incrementCount(cryptoDataFlowCounts, purl); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoDataFlow", true, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoDataFlowCategories", category, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoDataFlowRuleId", slice.ruleId, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:cryptoDataFlowTaintKinds", taintKinds, ); } } } addCountProperty( metadataProperties, "cdx:golem:cryptoDataFlowCount", Object.values(cryptoDataFlowCounts).reduce((sum, count) => sum + count, 0), ); for (const [purl, count] of Object.entries(dataFlowCounts)) { addCountProperty( (componentPropertiesMap[purl] ??= []), "cdx:golem:dataFlowSliceCount", count, ); } for (const [purl, count] of Object.entries(cryptoDataFlowCounts)) { addCountProperty( (componentPropertiesMap[purl] ??= []), "cdx:golem:cryptoDataFlowCount", count, ); } } function normalizedGolemDataFlowMode(options = {}) { let dataFlowMode = String( options.golemDataflow || options.golemDataFlow || "", ).toLowerCase(); if (!dataFlowMode) { if (options.withDataFlow || options.profile === "research") { dataFlowMode = "all"; } else if (options.deep) { dataFlowMode = "all"; } else { dataFlowMode = "none"; } } if (!GOLEM_DATAFLOW_MODES.has(dataFlowMode)) { return "none"; } return dataFlowMode; } function normalizedGolemCallGraphMode(dataFlowMode, options = {}) { let callgraphMode = String(options.golemCallgraph || "static").toLowerCase(); if (!GOLEM_CALLGRAPH_MODES.has(callgraphMode)) { callgraphMode = dataFlowMode || "static"; } return callgraphMode; } function normalizedGolemDataFlowCallGraphMode(options = {}) { let callgraphMode = String( options.golemDataflowCallgraph || "static", ).toLowerCase(); if (!GOLEM_DATAFLOW_CALLGRAPH_MODES.has(callgraphMode)) { callgraphMode = "static"; } return callgraphMode; } function appendGolemDataFlowArgs(args, dataFlowMode, options = {}) { if (dataFlowMode === "none") { return; } const cpuCount = cpus()?.length || 1; const performanceWorkerCount = Math.max(1, Math.min(cpuCount, 4)); const dataFlowCallgraph = normalizedGolemDataFlowCallGraphMode(options); const dataFlowPacks = options.golemDataflowPatternPacks || (dataFlowMode === "crypto" ? "crypto" : "all"); const dataFlowMaxSlices = normalizedPositiveInteger(options.golemDataflowMaxSlices) || (options.deep ? 250 : 1000); const dataFlowWorkers = normalizedPositiveInteger(options.golemDataflowWorkers) || performanceWorkerCount; const maxProcs = normalizedPositiveInteger(options.golemMaxProcs) || performanceWorkerCount; args.push("--dataflow", dataFlowMode); args.push("--dataflow-callgraph", dataFlowCallgraph); args.push("--dataflow-pattern-packs", String(dataFlowPacks)); args.push("--dataflow-max-slices", String(dataFlowMaxSlices)); args.push("--dataflow-workers", String(dataFlowWorkers)); args.push("--max-procs", String(maxProcs)); args.push( "--dataflow-large-repo-functions", String( normalizedPositiveInteger(options.golemDataflowLargeRepoFunctions) || 1000, ), ); args.push( "--dataflow-max-function-instructions", String( normalizedPositiveInteger(options.golemDataflowMaxFunctionInstructions) || 200, ), ); args.push( "--dataflow-max-trace-nodes", String(normalizedPositiveInteger(options.golemDataflowMaxTraceNodes) || 64), ); args.push( "--dataflow-max-trace-edges", String( normalizedPositiveInteger(options.golemDataflowMaxTraceEdges) || 128, ), ); if (options.golemDataflowPatterns) { args.push("--dataflow-patterns", String(options.golemDataflowPatterns)); } if (options.golemMemoryLimit) { args.push("--memory-limit", String(options.golemMemoryLimit)); } if (options.golemDataflowSkipGenerated ?? true) { args.push("--dataflow-skip-generated"); } if (options.golemDataflowSkipTests ?? !options.golemTests) { args.push("--dataflow-skip-tests"); } if (options.golemProgress) { args.push("--progress"); } } function addImportEvidence( golemReport, purlAliasMap, purlLocationMap, componentPropertiesMap, scopeCountsMap, occurrenceKindCountsMap, ) { for (const importUsage of golemReport.imports || []) { const purl = resolveComponentPurl( modulePurl(importUsage.module), purlAliasMap, ); if (!purl) { continue; } addSetValue(purlLocationMap, purl, rangeLocation(importUsage.range)); incrementNestedCount( scopeCountsMap, purl, importUsage.usageScope || "runtime", ); incrementNestedCount(occurrenceKindCountsMap, purl, "import"); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:importDirect", importUsage.direct, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:importAliasKind", importUsage.aliasKind, ); } } function addUsageEvidence( golemReport, purlAliasMap, purlLocationMap, dataFlowFrames, componentPropertiesMap, scopeCountsMap, occurrenceKindCountsMap, ) { for (const usage of golemReport.usages || []) { const purl = resolveComponentPurl(modulePurl(usage.module), purlAliasMap); if (!purl) { continue; } addSetValue(purlLocationMap, purl, rangeLocation(usage.range)); addFrame(dataFlowFrames, purl, frameFromUsage(usage)); incrementNestedCount(scopeCountsMap, purl, usage.usageScope || "runtime"); incrementNestedCount( occurrenceKindCountsMap, purl, usage.call ? "symbolCall" : "symbolReference", ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:symbolKind", usage.symbolKind, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:usageKind", usage.kind, ); addPropertyValue( componentPropertiesMap, purl, "cdx:golem:usageScope", usage.usageScope || "runtime", ); } } function addCallGraphEvidence( golemReport, purlAliasMap, purlLocationMap, dataFlowFrames, ) { const modules = golemReport.modules || []; const callGraphNodeMap = new Map(); for (const node of golemReport.callGraph?.nodes || []) { if (node?.id) { callGraphNodeMap.set(node.id, node); } } const localModules = new Set( modules.filter((module) => module.main).map((module) => module.path), ); const localPurls = new Set( modules .filter((module) => module.main) .map((module) => resolveComponentPurl(modulePurl(module), purlAliasMap)) .filter(Boolean), ); for (const edge of golemReport.callGraph?.edges || []) { const sourceModule = symbolModule(edge.sourceId, modules); const targetModule = symbolModule(edge.targetId, modules); const sourcePurl = resolveComponentPurl(edge.sourcePurl, purlAliasMap); const resolvedSourcePurl = sourcePurl || resolveComponentPurl(modulePurl(sourceModule), purlAliasMap); if ( resolvedSourcePurl ? !localPurls.has(resolvedSourcePurl) : !sourceModule?.path || !localModules.has(sourceModule.path) ) { continue; } const purls = new Set(); addResolvedPurls( purls, [edge.sinkPurl, edge.targetPurl, modulePurl(targetModule)], purlAliasMap, ); addResolvedPurls(purls, edge.purls, purlAliasMap); purls.delete(resolvedSourcePurl); if (!purls.size && resolvedSourcePurl) { purls.add(resolvedSourcePurl); } if (!purls.size) { continue; } for (const purl of purls) { addSetValue( purlLocationMap, purl, rangeLocation({ start: edge.position }), ); const sourceFrame = frameFromEdge(edge) || frameFromCallGraphNode( callGraphNodeMap.get(edge.sourceId), edge.sourceName, ); const targetFrame = frameFromCallGraphNode( callGraphNodeMap.get(edge.targetId), edge.targetName, ); const edgeFrames = dedupeFrames([sourceFrame, targetFrame]); if (edgeFrames.length) { dataFlowFrames[purl] ??= []; dataFlowFrames[purl].push(edgeFrames); } } } } export function isGolemGoLanguage(language) { return GO_LANGUAGES.has(String(language || "").toLowerCase()); } export function readGolemJsonFile(jsonFile) { if (!jsonFile || !safeExistsSync(jsonFile)) { return undefined; } try { return JSON.parse(readFileSync(jsonFile, "utf-8")); } catch (_err) { return undefined; } } export function runGolemAnalysis(src, outputFile, options = {}) { const executable = options.golemCommand || golemBin(); if (!executable || !src || !outputFile) { return false; } const dataFlowMode = normalizedGolemDataFlowMode(options); const callgraphMode = normalizedGolemCallGraphMode(dataFlowMode, options); const args = [ "analyze", "--dir", resolve(src), "--format", "json", "--callgraph", callgraphMode, "--out", resolve(outputFile), ]; appendGolemDataFlowArgs(args, dataFlowMode, options); if (options.golemPatterns) { args.push("--patterns", String(options.golemPatterns)); } if (options.golemTags || options.tags) { args.push("--tags", String(options.golemTags || options.tags)); } if (options.golemTests || options.tests) { args.push("--tests"); } if (options.golemIncludeStdlib) { args.push("--include-stdlib"); } if (DEBUG_MODE) { console.log("Executing", executable, args.join(" ")); } const result = safeSpawnSync(executable, args, { cwd: resolve(src), shell: false, }); if (result?.status !== 0 || result?.error || !safeExistsSync(outputFile)) { if (DEBUG_MODE) { if (result?.stdout || result?.stderr) { console.error(result.stdout, result.stderr); } else { console.log("Check if the golem plugin was installed successfully."); } } return false; } return true; } export function analyzeGolemProject(src, options = {}) { const tempDir = safeMkdtempSync(join(getTmpDir(), "golem-")); const outputFile = join(tempDir, "golem.json"); try { if (!runGolemAnalysis(src, outputFile, options)) { return undefined; } return readGolemJsonFile(outputFile); } finally { if (tempDir?.startsWith(getTmpDir())) { safeRmSync(tempDir, { recursive: true, force: true }); } } } export function collectGolemEvidence(golemReport = {}, components = []) { const purlAliasMap = createPurlAliasMap(components); const purlLocationMap = {}; const dataFlowFrames = {}; const componentPropertiesMap = {}; const metadataProperties = []; const scopeCountsMap = {}; const occurrenceKindCountsMap = {}; const cryptoComponentsByRef = new Map(); const cryptoGeneratePurls = {}; addMetadataProperties(metadataProperties, golemReport); addSupplyChainProperties( componentPropertiesMap, metadataProperties, purlAliasMap, golemReport.supplyChain, ); addModuleProperties( componentPropertiesMap, purlAliasMap, golemReport.modules || [], ); addImportEvidence( golemReport, purlAliasMap, purlLocationMap, componentPropertiesMap, scopeCountsMap, occurrenceKindCountsMap, ); addUsageEvidence( golemReport, purlAliasMap, purlLocationMap, dataFlowFrames, componentPropertiesMap, scopeCountsMap, occurrenceKindCountsMap, ); addCallGraphEvidence( golemReport, purlAliasMap, purlLocationMap, dataFlowFrames, ); addDataFlowEvidence( golemReport, purlAliasMap, purlLocationMap, dataFlowFrames, componentPropertiesMap, scopeCountsMap, occurrenceKindCountsMap, metadataProperties, ); addSignalProperties(componentPropertiesMap, purlAliasMap, golemReport); addCryptoEvidence( componentPropertiesMap, metadataProperties, purlAliasMap, golemReport, cryptoComponentsByRef, cryptoGeneratePurls, ); for (const [purl, scopeCounts] of Object.entries(scopeCountsMap)) { addScopedProperties(componentPropertiesMap, purl, scopeCounts); } for (const [purl, kindCounts] of Object.entries(occurrenceKindCountsMap)) { addOccurrenceKindProperties(componentPropertiesMap, purl, kindCounts); } return { componentPropertiesMap, cryptoComponents: Array.from(cryptoComponentsByRef.values()).sort( (left, right) => `${left.name}:${left["bom-ref"]}`.localeCompare( `${right.name}:${right["bom-ref"]}`, ), ), cryptoGeneratePurls, dataFlowFrames, metadataProperties, purlLocationMap, }; }