UNPKG

@cyclonedx/cdxgen

Version:

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

281 lines (267 loc) 7.96 kB
#!/usr/bin/env node import { basename, resolve } from "node:path"; import process from "node:process"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { createDynamicBom } from "../lib/cli/index.js"; import { DEBUG_MODE, retrieveCdxgenVersion, safeWriteSync, } from "../lib/helpers/utils.js"; const _yargs = yargs(hideBin(process.argv)); const args = _yargs .parserConfiguration({ "boolean-negation": true, "greedy-arrays": false, "parse-numbers": true, "short-option-groups": false, }) .option("cmd", { description: "Command to execute and trace for dynamic library SBOM generation.", type: "string", }) .option("working-dir", { alias: "d", description: "Working directory for the traced process.", type: "string", }) .option("output", { alias: "o", description: "Output SBOM file path.", default: "bom.json", type: "string", }) .option("spec-version", { description: "CycloneDX specification version.", default: 1.7, type: "number", }) .option("project-name", { description: "Override component name.", type: "string", }) .option("project-version", { description: "Override component version.", type: "string", }) .option("read-paths", { description: "Comma-separated extra filesystem read paths for the sandbox.", type: "string", }) .option("write-paths", { description: "Comma-separated sandbox write paths (overrides default of OS tmpdir).", type: "string", }) .option("max-memory", { description: "Max memory MB for sandbox.", default: 512, type: "number", }) .option("max-processes", { description: "Max process count for sandbox.", default: 64, type: "number", }) .option("timeout", { description: "Trace timeout in milliseconds.", default: 60000, type: "number", }) .option("disable-network", { description: "Disable network inside sandbox. Automatically disabled when --trace-http-urls is set.", default: true, type: "boolean", }) .option("trace-http-urls", { description: "Enable eBPF-based HTTP URL tracing (Linux only, kernel >= 5.8). Requires CAP_BPF.", default: false, type: "boolean", }) .option("trace-period", { description: "Stop tracing after N seconds (for long-running or persistent commands).", type: "number", }) .option("max-cpu", { description: "Max CPU cores as fractional number (e.g. 0.5 for half a core).", type: "number", }) .option("allow-envs", { description: "Comma-separated list of host environment variables allowed to pass through the sandbox.", type: "string", }) .option("allow-hidden", { description: "Allow reading and writing to hidden files and directories.", default: true, type: "boolean", }) .option("allow-listen", { description: "Comma-separated IP addresses or ip:port strings to allow the sandboxed process to bind/listen to.", type: "string", }) .option("crypto-probe-mode", { description: "Crypto probe mode controlling tracing depth: tls-only (default) or operations (digest, encrypt, sign).", default: "tls-only", type: "string", }) .option("diff", { description: "Enable filesystem mutation diffing (tracks created/modified/deleted files).", default: false, type: "boolean", }) .option("strict", { description: "Treat sandbox setup warnings as hard errors.", default: false, type: "boolean", }) .option("allow-host", { description: "Comma-separated hostnames to allow network access to (when network is enabled).", type: "string", }) .option("allow-port", { description: "Comma-separated TCP ports to allow network access to.", type: "string", }) .option("allow-url", { description: "Comma-separated URL allow rules for fine-grained HTTP access control (Linux only, requires --trace-http-urls).", type: "string", }) .option("block-fork", { description: "Prevent the traced process from forking new processes.", default: false, type: "boolean", }) .option("trace-exec", { description: "Log every child process spawned by the traced command.", default: false, type: "boolean", }) .option("allow-exec", { description: "Comma-separated list of executables the traced command is allowed to run.", type: "string", }) .option("block-exec", { description: "Comma-separated list of executables to block from running.", type: "string", }) .option("trace-crypto", { description: "Enable eBPF-based cryptographic library and cipher suite tracing (Linux only).", default: true, type: "boolean", }) .option("print", { description: "Print BOM to stdout.", default: false, type: "boolean", }) .scriptName( basename(process.argv[1] || "tracebom").replace(/\.(?:[cm]?js|exe)$/u, ""), ) .version(retrieveCdxgenVersion()) .alias("v", "version") .help(false) .option("help", { alias: "h", description: "Show help", type: "boolean", }) .wrap(Math.min(120, yargs().terminalWidth())).argv; if (args.help) { console.log(`${retrieveCdxgenVersion()}\n`); _yargs.showHelp(); process.exit(0); } const workingDir = args.workingDir || process.cwd(); const options = { traceCmd: args.cmd, traceWorkingDir: workingDir, specVersion: args.specVersion, projectName: args.projectName, projectVersion: args.projectVersion, traceReadPaths: args.readPaths ? args.readPaths.split(",").filter(Boolean) : [], traceWritePaths: args.writePaths ? args.writePaths.split(",").filter(Boolean) : [], traceMaxMemoryMB: args.maxMemory, traceMaxProcesses: args.maxProcesses, traceTimeoutMs: args.timeout, traceDisableNetwork: args.traceHttpUrls ? false : (args.disableNetwork ?? true), traceHTTPURLs: args.traceHttpUrls ?? false, tracePeriod: args.tracePeriod, traceMaxCPUCores: args.maxCpu, traceAllowEnvs: args.allowEnvs ? args.allowEnvs.split(",").filter(Boolean) : [], traceAllowHidden: args.allowHidden ?? true, traceAllowListen: args.allowListen ? args.allowListen.split(",").filter(Boolean) : [], traceCryptoProbeMode: args.cryptoProbeMode || "tls-only", traceEnableDiff: args.diff ?? false, traceStrict: args.strict ?? false, traceAllowHosts: args.allowHost ? args.allowHost.split(",").filter(Boolean) : [], traceAllowPorts: args.allowPort ? args.allowPort .split(",") .map(Number) .filter((n) => !Number.isNaN(n)) : [], traceAllowUrls: args.allowUrl ? args.allowUrl.split(",").filter(Boolean) : [], traceBlockFork: args.blockFork ?? false, traceTraceExec: args.traceExec ?? false, traceAllowExec: args.allowExec ? args.allowExec.split(",").filter(Boolean) : [], traceBlockExec: args.blockExec ? args.blockExec.split(",").filter(Boolean) : [], traceCrypto: args.traceCrypto ?? true, cbom: undefined, projectType: ["dynamic"], output: resolve(args.output), }; (async () => { const { bomJson } = await createDynamicBom(workingDir, options); if (!bomJson) { console.error("Dynamic SBOM generation failed: no output was produced."); process.exit(1); } const jsonPayload = JSON.stringify(bomJson, null, DEBUG_MODE ? 2 : null); if (args.print) { console.log(jsonPayload); } safeWriteSync(options.output, jsonPayload); const serviceCount = bomJson?.services?.length || 0; const componentCount = bomJson?.components?.length || 0; const summaryParts = [ `SBOM written to ${options.output}`, `${componentCount} component(s)`, ]; if (serviceCount > 0) { summaryParts.push(`${serviceCount} service(s)`); } console.log(summaryParts.join(", ")); })().catch((error) => { console.error(error.message || error); process.exit(1); });