UNPKG

@cyclonedx/cdxgen

Version:

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

1,071 lines (1,056 loc) 34.9 kB
#!/usr/bin/env node import { Buffer } from "node:buffer"; import crypto from "node:crypto"; import fs from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; import process from "node:process"; import { findUpSync } from "find-up"; import globalAgent from "global-agent"; import jws from "jws"; import { parse as _load } from "yaml"; import { createBom, submitBom } from "../lib/cli/index.js"; import { printCallStack, printDependencyTree, printFormulation, printOccurrences, printReachables, printServices, printSponsorBanner, printSummary, printTable, } from "../lib/helpers/display.js"; import { thoughtEnd, thoughtLog } from "../lib/helpers/logger.js"; import { ATOM_DB, DEBUG_MODE, dirNameStr, getTmpDir, isMac, isSecureMode, isWin, safeExistsSync, } from "../lib/helpers/utils.js"; import { validateBom } from "../lib/helpers/validator.js"; import { postProcess } from "../lib/stages/postgen/postgen.js"; import { prepareEnv } from "../lib/stages/pregen/pregen.js"; // Support for config files const configPath = findUpSync([ ".cdxgenrc", ".cdxgen.json", ".cdxgen.yml", ".cdxgen.yaml", ]); let config = {}; if (configPath) { try { if (configPath.endsWith(".yml") || configPath.endsWith(".yaml")) { config = _load(fs.readFileSync(configPath, "utf-8")); } else { config = JSON.parse(fs.readFileSync(configPath, "utf-8")); } } catch (e) { console.log("Invalid config file", configPath); } } const dirName = dirNameStr; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; const args = yargs(hideBin(process.argv)) .env("CDXGEN") .parserConfiguration({ "greedy-arrays": false, "short-option-groups": false, }) .option("output", { alias: "o", description: "Output file. Default bom.json", default: "bom.json", }) .option("evinse-output", { description: "Create bom with evidence as a separate file. Default bom.json", hidden: true, }) .option("type", { alias: "t", description: "Project type. Please refer to https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES for supported languages/platforms.", }) .option("exclude-type", { description: "Project types to exclude. Please refer to https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES for supported languages/platforms.", }) .option("recurse", { alias: "r", type: "boolean", default: true, description: "Recurse mode suitable for mono-repos. Defaults to true. Pass --no-recurse to disable.", }) .option("print", { alias: "p", type: "boolean", description: "Print the SBOM as a table with tree.", }) .option("resolve-class", { alias: "c", type: "boolean", description: "Resolve class names for packages. jars only for now.", }) .option("deep", { type: "boolean", description: "Perform deep searches for components. Useful while scanning C/C++ apps, live OS and oci images.", }) .option("server-url", { description: "Dependency track url. Eg: https://deptrack.cyclonedx.io", }) .option("skip-dt-tls-check", { type: "boolean", default: false, description: "Skip TLS certificate check when calling Dependency-Track. ", }) .option("api-key", { description: "Dependency track api key", }) .option("project-group", { description: "Dependency track project group", }) .option("project-name", { description: "Dependency track project name. Default use the directory name", }) .option("project-version", { description: "Dependency track project version", default: "", type: "string", }) .option("project-id", { description: "Dependency track project id. Either provide the id or the project name and version together", type: "string", }) .option("parent-project-id", { description: "Dependency track parent project id", type: "string", }) .option("required-only", { type: "boolean", description: "Include only the packages with required scope on the SBOM. Would set compositions.aggregate to incomplete unless --no-auto-compositions is passed.", }) .option("fail-on-error", { type: "boolean", default: isSecureMode, description: "Fail if any dependency extractor fails.", }) .option("no-babel", { type: "boolean", description: "Do not use babel to perform usage analysis for JavaScript/TypeScript projects.", }) .option("generate-key-and-sign", { type: "boolean", description: "Generate an RSA public/private key pair and then sign the generated SBOM using JSON Web Signatures.", }) .option("server", { type: "boolean", description: "Run cdxgen as a server", }) .option("server-host", { description: "Listen address", default: "127.0.0.1", }) .option("server-port", { description: "Listen port", default: "9090", }) .option("install-deps", { type: "boolean", default: !isSecureMode, description: "Install dependencies automatically for some projects. Defaults to true but disabled for containers and oci scans. Use --no-install-deps to disable this feature.", }) .option("validate", { type: "boolean", default: true, description: "Validate the generated SBOM using json schema. Defaults to true. Pass --no-validate to disable.", }) .option("evidence", { type: "boolean", default: false, description: "Generate SBOM with evidence for supported languages.", }) .option("deps-slices-file", { description: "Path for the parsedeps slice file created by atom.", default: "deps.slices.json", hidden: true, }) .option("usages-slices-file", { description: "Path for the usages slices file created by atom.", hidden: true, }) .option("data-flow-slices-file", { description: "Path for the data-flow slices file created by atom.", hidden: true, }) .option("reachables-slices-file", { description: "Path for the reachables slices file created by atom.", hidden: true, }) .option("semantics-slices-file", { description: "Path for the semantics slices file.", default: "semantics.slices.json", hidden: true, }) .option("openapi-spec-file", { description: "Path for the openapi specification file (SaaSBOM).", hidden: true, }) .option("spec-version", { description: "CycloneDX Specification version to use. Defaults to 1.6", default: 1.6, type: "number", choices: [1.4, 1.5, 1.6], }) .option("filter", { description: "Filter components containing this word in purl or component.properties.value. Multiple values allowed.", }) .option("only", { description: "Include components only containing this word in purl. Useful to generate BOM with first party components alone. Multiple values allowed.", }) .option("author", { description: "The person(s) who created the BOM. Set this value if you're intending the modify the BOM and claim authorship.", default: "OWASP Foundation", }) .option("profile", { description: "BOM profile to use for generation. Default generic.", default: "generic", choices: [ "appsec", "research", "operational", "threat-modeling", "license-compliance", "generic", "machine-learning", "ml", "deep-learning", "ml-deep", "ml-tiny", ], }) .option("lifecycle", { description: "Product lifecycle for the generated BOM.", hidden: true, choices: ["pre-build", "build", "post-build"], }) .option("exclude", { description: "Additional glob pattern(s) to ignore", }) .option("export-proto", { type: "boolean", default: false, description: "Serialize and export BOM as protobuf binary.", hidden: true, }) .option("proto-bin-file", { description: "Path for the serialized protobuf binary.", default: "bom.cdx", hidden: true, }) .option("include-formulation", { type: "boolean", default: false, description: "Generate formulation section with git metadata and build tools. Defaults to false.", }) .option("include-crypto", { type: "boolean", default: false, description: "Include crypto libraries as components.", }) .option("standard", { description: "The list of standards which may consist of regulations, industry or organizational-specific standards, maturity models, best practices, or any other requirements which can be evaluated against or attested to.", choices: [ "asvs-5.0", "asvs-4.0.3", "bsimm-v13", "masvs-2.0.0", "nist_ssdf-1.1", "pcissc-secure-slc-1.1", "scvs-1.0.0", "ssaf-DRAFT-2023-11", ], }) .option("no-banner", { type: "boolean", default: false, hidden: true, description: "Do not show the donation banner. Set this attribute if you are an active sponsor for OWASP CycloneDX.", }) .option("json-pretty", { type: "boolean", default: DEBUG_MODE, description: "Pretty-print the generated BOM json.", }) .option("feature-flags", { description: "Experimental feature flags to enable. Advanced users only.", hidden: true, choices: ["safe-pip-install", "suggest-build-tools", "ruby-docker-install"], }) .option("min-confidence", { description: "Minimum confidence needed for the identity of a component from 0 - 1, where 1 is 100% confidence.", default: 0, type: "number", }) .option("technique", { description: "Analysis technique to use", choices: [ "auto", "source-code-analysis", "binary-analysis", "manifest-analysis", "hash-comparison", "instrumentation", "filename", ], }) .completion("completion", "Generate bash/zsh completion") .array("type") .array("excludeType") .array("filter") .array("only") .array("author") .array("exclude") .array("standard") .array("feature-flags") .array("technique") .option("auto-compositions", { type: "boolean", default: true, description: "Automatically set compositions when the BOM was filtered. Defaults to true", }) .example([ ["$0 -t java .", "Generate a Java SBOM for the current directory"], [ "$0 -t java -t js .", "Generate a SBOM for Java and JavaScript in the current directory", ], [ "$0 -t java --profile ml .", "Generate a Java SBOM for machine learning purposes.", ], [ "$0 -t python --profile research .", "Generate a Python SBOM for appsec research.", ], ["$0 --server", "Run cdxgen as a server"], ]) .epilogue("for documentation, visit https://cyclonedx.github.io/cdxgen") .config(config) .scriptName("cdxgen") .version() .alias("v", "version") .help("h") .alias("h", "help") .wrap(Math.min(120, yargs().terminalWidth())).argv; if (process.env?.CDXGEN_NODE_OPTIONS) { process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ""} ${process.env.CDXGEN_NODE_OPTIONS}`; } if (args.version) { const packageJsonAsString = fs.readFileSync( join(dirName, "..", "package.json"), "utf-8", ); const packageJson = JSON.parse(packageJsonAsString); console.log(packageJson.version); process.exit(0); } if (process.env.GLOBAL_AGENT_HTTP_PROXY || process.env.HTTP_PROXY) { // Support standard HTTP_PROXY variable if the user doesn't override the namespace if (!process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE) { process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = ""; } globalAgent.bootstrap(); } const filePath = args._[0] || process.cwd(); if (!args.projectName) { if (filePath !== ".") { args.projectName = basename(filePath); } else { args.projectName = basename(resolve(filePath)); } } thoughtLog(`Let's try to generate a CycloneDX BOM for the path '${filePath}'`); if ( filePath.includes(" ") || filePath.includes("\r") || filePath.includes("\n") ) { console.log( `'${filePath}' contains spaces. This could lead to bugs when invoking external build tools.`, ); if (isSecureMode) { process.exit(1); } } // Support for obom/cbom aliases if (process.argv[1].includes("obom") && !args.type) { args.type = "os"; thoughtLog( "Ok, the user wants to generate an Operations Bill-of-Materials (OBOM).", ); } /** * Command line options */ const options = Object.assign({}, args, { projectType: args.type, multiProject: args.recurse, noBabel: args.noBabel || args.babel === false, project: args.projectId, deep: args.deep || args.evidence, output: isSecureMode && args.output === "bom.json" ? resolve(join(filePath, args.output)) : args.output, }); // Should we create the output directory? const outputDirectory = dirname(options.output); if ( outputDirectory && outputDirectory !== process.cwd() && !safeExistsSync(outputDirectory) ) { fs.mkdirSync(outputDirectory, { recursive: true }); } // Filter duplicate types. Eg: -t gradle -t gradle if (options.projectType && Array.isArray(options.projectType)) { options.projectType = Array.from(new Set(options.projectType)); } if (!options.projectType) { thoughtLog( "Ok, the user wants me to identify all the project types and generate a consolidated BOM document.", ); } // Handle dedicated cbom and saasbom commands if (["cbom", "saasbom"].includes(process.argv[1])) { if (process.argv[1].includes("cbom")) { thoughtLog( "Ok, the user wants to generate Cryptographic Bill-of-Materials (CBOM).", ); options.includeCrypto = true; } else if (process.argv[1].includes("saasbom")) { thoughtLog( "Ok, the user wants to generate a Software as a Service Bill-of-Materials (SaaSBOM). I should carefully collect the services, endpoints, and data flows.", ); if (process.env?.CDXGEN_IN_CONTAINER !== "true") { thoughtLog( "Wait, I'm not running in a container. This means the chances of successfully collecting this inventory are quite low. Perhaps this is an advanced user who has set up atom and atom-tools already 🤔?", ); } } options.evidence = true; options.specVersion = 1.6; options.deep = true; } if (process.argv[1].includes("cdxgen-secure")) { thoughtLog( "Ok, the user wants cdxgen to run in secure mode by default. Let's try and use the permissions api.", ); console.log( "NOTE: Secure mode only restricts cdxgen from performing certain activities such as package installation. It does not provide security guarantees in the presence of malicious code.", ); options.installDeps = false; process.env.CDXGEN_SECURE_MODE = true; } if (options.standard) { options.specVersion = 1.6; } if (options.includeFormulation) { thoughtLog( "Wait, the user wants to include formulation information. Let's warn about accidentally disclosing sensitive data via the BOM files.", ); console.log( "NOTE: Formulation section could include sensitive data such as emails and secrets.\nPlease review the generated SBOM before distribution.\n", ); } /** * Method to apply advanced options such as profile and lifecycles * * @param {object} options CLI options */ const applyAdvancedOptions = (options) => { if (options?.profile !== "generic") { thoughtLog(`BOM profile to use is '${options.profile}'.`); } else { thoughtLog( "The user hasn't specified a profile. Should I suggest one to optimize the BOM for a specific use case or persona 🤔?", ); } switch (options.profile) { case "appsec": options.deep = true; break; case "research": options.deep = true; options.evidence = true; options.includeCrypto = true; process.env.CDX_MAVEN_INCLUDE_TEST_SCOPE = "true"; process.env.ASTGEN_IGNORE_DIRS = ""; process.env.ASTGEN_IGNORE_FILE_PATTERN = ""; break; case "operational": if (options?.projectType) { options.projectType.push("os"); } else { options.projectType = ["os"]; } break; case "threat-modeling": options.deep = true; options.evidence = true; break; case "license-compliance": process.env.FETCH_LICENSE = "true"; break; case "ml-tiny": process.env.FETCH_LICENSE = "true"; options.deep = false; options.evidence = false; options.includeCrypto = false; options.installDeps = false; break; case "machine-learning": case "ml": process.env.FETCH_LICENSE = "true"; options.deep = true; options.evidence = false; options.includeCrypto = false; options.installDeps = !isSecureMode; break; case "deep-learning": case "ml-deep": process.env.FETCH_LICENSE = "true"; options.deep = true; options.evidence = true; options.includeCrypto = true; options.installDeps = !isSecureMode; break; default: break; } if (options.lifecycle) { thoughtLog( `BOM must be generated for the lifecycle '${options.lifecycle}'.`, ); } switch (options.lifecycle) { case "pre-build": options.installDeps = false; break; case "post-build": if ( !options.projectType || (Array.isArray(options.projectType) && options.projectType.length > 1) || ![ "csharp", "dotnet", "container", "docker", "podman", "oci", "android", "apk", "aab", "go", "golang", "rust", "rust-lang", "cargo", ].includes(options.projectType[0]) ) { console.log( "PREVIEW: post-build lifecycle SBOM generation is supported only for android, dotnet, go, and Rust projects. Please specify the type using the -t argument.", ); process.exit(1); } options.installDeps = true; break; default: break; } // When the user specifies source-code-analysis as a technique, then enable deep and evidence mode. if (options?.technique && Array.isArray(options.technique)) { if (options?.technique?.includes("source-code-analysis")) { options.deep = true; options.evidence = true; } if (options.technique.length === 1) { thoughtLog( `Wait, the user wants me to use only the following technique: '${options.technique.join(", ")}'.`, ); } else { thoughtLog( `Alright, I will use only the following techniques: '${options.technique.join(", ")}' for the final BOM.`, ); } } if (!options.installDeps) { thoughtLog( "I must avoid any package installations and focus solely on the available artefacts, such as lock files.", ); } return options; }; applyAdvancedOptions(options); /** * Check for node >= 20 permissions * * @param {string} filePath File path * @param {Object} options CLI Options * @returns */ const checkPermissions = (filePath, options) => { const fullFilePath = resolve(filePath); if ( process.getuid && process.getuid() === 0 && process.env?.CDXGEN_IN_CONTAINER !== "true" ) { console.log( "\x1b[1;35mSECURE MODE: DO NOT run cdxgen with root privileges.\x1b[0m", ); } if (!process.permission) { if (isSecureMode) { console.log( "\x1b[1;35mSecure mode requires permission-related arguments. These can be passed as CLI arguments directly to the node runtime or set the NODE_OPTIONS environment variable as shown below.\x1b[0m", ); const childProcessArgs = options?.lifecycle !== "pre-build" ? " --allow-child-process" : ""; const nodeOptionsVal = `--permission --allow-fs-read="${getTmpDir()}/*" --allow-fs-write="${getTmpDir()}/*" --allow-fs-read="${fullFilePath}/*" --allow-fs-write="${options.output}"${childProcessArgs}`; console.log( `${isWin ? "$env:" : "export "}NODE_OPTIONS='${nodeOptionsVal}'`, ); if (process.env?.CDXGEN_IN_CONTAINER !== "true") { console.log( "TIP: Run cdxgen using the secure container image 'ghcr.io/cyclonedx/cdxgen-secure' for best experience.", ); } } return true; } // Secure mode checks if (isSecureMode) { if (process.env?.GITHUB_TOKEN) { console.log( "Ensure that the GitHub token provided to cdxgen is restricted to read-only scopes.", ); } if (process.permission.has("fs.read", "*")) { console.log( "\x1b[1;35mSECURE MODE: DO NOT run cdxgen with FileSystemRead permission set to wildcard.\x1b[0m", ); } if (process.permission.has("fs.write", "*")) { console.log( "\x1b[1;35mSECURE MODE: DO NOT run cdxgen with FileSystemWrite permission set to wildcard.\x1b[0m", ); } if (process.permission.has("worker")) { console.log( "SECURE MODE: DO NOT run cdxgen with worker thread permission! Remove `--allow-worker` argument.", ); } if (filePath !== fullFilePath) { console.log( `\x1b[1;35mSECURE MODE: Invoke cdxgen with an absolute path to improve security. Use '${fullFilePath}' instead of '${filePath}'\x1b[0m`, ); if (fullFilePath.includes(" ")) { console.log( "\x1b[1;35mSECURE MODE: Directory names containing spaces are known to cause issues. Rename the directories by replacing spaces with hyphens or underscores.\x1b[0m", ); } else if (fullFilePath.length > 255 && isWin) { console.log( "Ensure 'Enable Win32 Long paths' is set to 'Enabled' by using Group Policy Editor.", ); } return false; } } if (!process.permission.has("fs.read", filePath)) { console.log( `\x1b[1;35mSECURE MODE: FileSystemRead permission required. Please invoke cdxgen with the argument --allow-fs-read="${resolve( filePath, )}"\x1b[0m`, ); return false; } if (!process.permission.has("fs.write", options.output)) { console.log( `\x1b[1;35mSECURE MODE: FileSystemWrite permission is required to create the output BOM file. Please invoke cdxgen with the argument --allow-fs-write="${options.output}"\x1b[0m`, ); } if (options.evidence) { const slicesFilesKeys = [ "deps-slices-file", "usages-slices-file", "reachables-slices-file", ]; if (options?.type?.includes("swift") || options?.type?.includes("scala")) { slicesFilesKeys.push("semantics-slices-file"); } for (const sf of slicesFilesKeys) { let issueFound = false; if (!process.permission.has("fs.write", options[sf])) { console.log( `SECURE MODE: FileSystemWrite permission is required to create the output slices file. Please invoke cdxgen with the argument --allow-fs-write="${options[sf]}"`, ); if (!issueFound) { issueFound = true; } } if (issueFound) { return false; } } if (!process.permission.has("fs.write", process.env.ATOM_DB || ATOM_DB)) { console.log( `SECURE MODE: FileSystemWrite permission is required to create the output slices file. Please invoke cdxgen with the argument --allow-fs-write="${process.env.ATOM_DB || ATOM_DB}"`, ); return false; } console.log( "TIP: Invoke cdxgen with `--allow-addons` to allow the use of sqlite3 native addon. This addon is required for evidence mode.", ); } else { if (process.permission.has("fs.write", process.env.ATOM_DB || ATOM_DB)) { console.log( `SECURE MODE: FileSystemWrite permission is not required for the directory "${process.env.ATOM_DB || ATOM_DB}" in non-evidence mode. Consider removing the argument --allow-fs-write="${process.env.ATOM_DB || ATOM_DB}".`, ); return false; } } if (!process.permission.has("fs.write", getTmpDir())) { console.log( `FileSystemWrite permission may be required for the TEMP directory. Please invoke cdxgen with the argument --allow-fs-write="${join(getTmpDir(), "*")}" in case of any crashes.`, ); if (isMac) { console.log( "TIP: macOS doesn't use the `/tmp` prefix for TEMP directories. Use the argument shown above.", ); } } if (!process.permission.has("child") && !isSecureMode) { console.log( "ChildProcess permission is missing. This is required to spawn commands for some languages. Please invoke cdxgen with the argument --allow-child-process in case of issues.", ); } if (process.permission.has("child") && options?.lifecycle === "pre-build") { console.log( "SECURE MODE: ChildProcess permission is not required for pre-build SBOM generation. Please invoke cdxgen without the argument --allow-child-process.", ); return false; } return true; }; const needsBomSigning = ({ generateKeyAndSign }) => generateKeyAndSign || (process.env.SBOM_SIGN_ALGORITHM && process.env.SBOM_SIGN_ALGORITHM !== "none" && process.env.SBOM_SIGN_PRIVATE_KEY && safeExistsSync(process.env.SBOM_SIGN_PRIVATE_KEY)); /** * Method to start the bom creation process */ (async () => { // Display the sponsor banner printSponsorBanner(options); // Start SBOM server if (options.server) { const serverModule = await import("../lib/server/server.js"); return serverModule.start(options); } // Check if cdxgen has the required permissions if (!checkPermissions(filePath, options)) { if (isSecureMode) { process.exit(1); } return; } prepareEnv(filePath, options); thoughtLog("Getting ready to generate the BOM ⚡️."); let bomNSData = (await createBom(filePath, options)) || {}; if (bomNSData?.bomJson) { thoughtLog( "Tweaking the generated BOM data with useful annotations and properties.", ); } // Add extra metadata and annotations with post processing bomNSData = postProcess(bomNSData, options); if ( options.output && (typeof options.output === "string" || options.output instanceof String) ) { const jsonFile = options.output; // Create bom json file if (bomNSData.bomJson) { let jsonPayload = undefined; if ( typeof bomNSData.bomJson === "string" || bomNSData.bomJson instanceof String ) { fs.writeFileSync(jsonFile, bomNSData.bomJson); jsonPayload = bomNSData.bomJson; } else { jsonPayload = JSON.stringify( bomNSData.bomJson, null, options.jsonPretty ? 2 : null, ); fs.writeFileSync(jsonFile, jsonPayload); if (jsonFile.endsWith("bom.json")) { thoughtLog( `Let's save the file to "${jsonFile}". Should I suggest the '.cdx.json' file extension for better semantics?`, ); } else { thoughtLog(`Let's save the file to "${jsonFile}".`); } } if (jsonPayload && needsBomSigning(options)) { let alg = process.env.SBOM_SIGN_ALGORITHM || "RS512"; if (alg.includes("none")) { alg = "RS512"; } let privateKeyToUse = undefined; let jwkPublicKey = undefined; let publicKeyFile = undefined; if (options.generateKeyAndSign) { const jdirName = dirname(jsonFile); publicKeyFile = join(jdirName, "public.key"); const privateKeyFile = join(jdirName, "private.key"); const privateKeyB64File = join(jdirName, "private.key.base64"); const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 4096, publicKeyEncoding: { type: "spki", format: "pem", }, privateKeyEncoding: { type: "pkcs8", format: "pem", }, }); fs.writeFileSync(publicKeyFile, publicKey); fs.writeFileSync(privateKeyFile, privateKey); fs.writeFileSync( privateKeyB64File, Buffer.from(privateKey, "utf8").toString("base64"), ); console.log( "Created public/private key pairs for testing purposes", publicKeyFile, privateKeyFile, privateKeyB64File, ); privateKeyToUse = privateKey; jwkPublicKey = crypto .createPublicKey(publicKey) .export({ format: "jwk" }); } else { privateKeyToUse = fs.readFileSync( process.env.SBOM_SIGN_PRIVATE_KEY, "utf8", ); if ( process.env.SBOM_SIGN_PUBLIC_KEY && safeExistsSync(process.env.SBOM_SIGN_PUBLIC_KEY) ) { jwkPublicKey = crypto .createPublicKey( fs.readFileSync(process.env.SBOM_SIGN_PUBLIC_KEY, "utf8"), ) .export({ format: "jwk" }); } } try { // Sign the individual components // Let's leave the services unsigned for now since it might require additional cleansing const bomJsonUnsignedObj = JSON.parse(jsonPayload); for (const comp of bomJsonUnsignedObj.components) { const compSignature = jws.sign({ header: { alg }, payload: comp, privateKey: privateKeyToUse, }); const compSignatureBlock = { algorithm: alg, value: compSignature, }; if (jwkPublicKey) { compSignatureBlock.publicKey = jwkPublicKey; } comp.signature = compSignatureBlock; } const signature = jws.sign({ header: { alg }, payload: JSON.stringify(bomJsonUnsignedObj, null, 2), privateKey: privateKeyToUse, }); if (signature) { const signatureBlock = { algorithm: alg, value: signature, }; if (jwkPublicKey) { signatureBlock.publicKey = jwkPublicKey; } bomJsonUnsignedObj.signature = signatureBlock; fs.writeFileSync( jsonFile, JSON.stringify( bomJsonUnsignedObj, null, options.jsonPretty ? 2 : null, ), ); thoughtLog(`Signing the BOM file "${jsonFile}".`); if (publicKeyFile) { // Verifying this signature const signatureVerification = jws.verify( signature, alg, fs.readFileSync(publicKeyFile, "utf8"), ); if (signatureVerification) { console.log( "SBOM signature is verifiable with the public key and the algorithm", publicKeyFile, alg, ); } else { console.log("SBOM signature verification was unsuccessful"); console.log( "Check if the public key was exported in PEM format", ); } } } } catch (ex) { console.log("SBOM signing was unsuccessful", ex); console.log("Check if the private key was exported in PEM format"); } } } // bom ns mapping if (bomNSData.nsMapping && Object.keys(bomNSData.nsMapping).length) { const nsFile = `${jsonFile}.map`; fs.writeFileSync(nsFile, JSON.stringify(bomNSData.nsMapping)); } } else if (!options.print) { if (bomNSData.bomJson) { console.log( JSON.stringify(bomNSData.bomJson, null, options.jsonPretty ? 2 : null), ); } else { console.log("Unable to produce BOM for", filePath); console.log("Try running the command with -t <type> or -r argument"); } } // Evidence generation if (options.evidence || options.includeCrypto) { // Set the evinse output file to be the same as output file if (!options.evinseOutput) { options.evinseOutput = options.output; } const evinserModule = await import("../lib/evinser/evinser.js"); options.projectType = options.projectType || ["java"]; const evinseOptions = { _: args._, input: options.output, output: options.evinseOutput, language: options.projectType, dbPath: process.env.ATOM_DB || ATOM_DB, skipMavenCollector: false, force: false, withReachables: options.deep, usagesSlicesFile: options.usagesSlicesFile, dataFlowSlicesFile: options.dataFlowSlicesFile, reachablesSlicesFile: options.reachablesSlicesFile, semanticsSlicesFile: options.semanticsSlicesFile, openapiSpecFile: options.openapiSpecFile, includeCrypto: options.includeCrypto, specVersion: options.specVersion, profile: options.profile, jsonPretty: options.jsonPretty, }; const dbObjMap = await evinserModule.prepareDB(evinseOptions); if (dbObjMap) { const sliceArtefacts = await evinserModule.analyzeProject( dbObjMap, evinseOptions, ); const evinseJson = evinserModule.createEvinseFile( sliceArtefacts, evinseOptions, ); bomNSData.bomJson = evinseJson; if (options.print && evinseJson) { printOccurrences(evinseJson); printCallStack(evinseJson); printReachables(sliceArtefacts); printServices(evinseJson); } } } // Perform automatic validation if (options.validate && bomNSData?.bomJson) { thoughtLog("Wait, let's check the generated BOM file for any issues."); if (!validateBom(bomNSData.bomJson)) { process.exit(1); } thoughtLog("BOM file looks valid. Thank you for using cdxgen!"); } thoughtEnd(); // Automatically submit the bom data // biome-ignore lint/suspicious/noDoubleEquals: yargs passes true for empty values if (options.serverUrl && options.serverUrl != true && options.apiKey) { try { await submitBom(options, bomNSData.bomJson); } catch (err) { console.log(err); process.exit(1); } } // Protobuf serialization if (options.exportProto) { const protobomModule = await import("../lib/helpers/protobom.js"); protobomModule.writeBinary(bomNSData.bomJson, options.protoBinFile); } if (options.print && bomNSData.bomJson && bomNSData.bomJson.components) { printSummary(bomNSData.bomJson); if (options.includeFormulation) { printFormulation(bomNSData.bomJson); } printDependencyTree(bomNSData.bomJson); printTable(bomNSData.bomJson); // CBOM related print if (options.includeCrypto) { printTable(bomNSData.bomJson, ["cryptographic-asset"]); printDependencyTree(bomNSData.bomJson, "provides"); } } })();