UNPKG

@cyclonedx/cdxgen

Version:

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

213 lines (205 loc) 6.8 kB
#!/usr/bin/env node import { writeFileSync } from "node:fs"; import { dirname } from "node:path"; import process from "node:process"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { finalizeAuditReport, runAudit } from "../lib/audit/index.js"; import { createProgressTracker } from "../lib/audit/progress.js"; import { retrieveCdxgenVersion, safeExistsSync, safeMkdirSync, } from "../lib/helpers/utils.js"; const args = yargs(hideBin(process.argv)) .option("bom", { description: "Path to a CycloneDX JSON BOM file.", type: "string", }) .option("bom-dir", { description: "Directory containing one or more CycloneDX JSON BOM files.", type: "string", }) .option("workspace-dir", { description: "Optional directory to reuse git clones for purl-to-source enrichment.", type: "string", }) .option("reports-dir", { description: "Optional directory to store generated per-purl SBOMs and findings.", type: "string", }) .option("direct-bom-audit", { default: false, description: "Evaluate audit rules directly against the supplied BOM(s) instead of running only the predictive dependency audit.", type: "boolean", }) .option("rules-dir", { description: "Directory containing additional YAML audit rules (merged with built-in). Applies to direct BOM audit and predictive child-SBOM rule evaluation.", type: "string", }) .option("report", { choices: ["console", "json", "sarif"], default: "console", description: "Output format.", }) .option("report-file", { alias: "o", description: "Write the report to this file. Defaults to stdout.", type: "string", }) .option("categories", { description: "Comma-separated rule categories. In predictive mode this applies to generated child SBOMs (default: ai-agent, ci-permission, dependency-source, package-integrity). In direct BOM audit mode it applies to the supplied BOM(s) themselves (default: obom-runtime for OBOMs, all categories otherwise).", type: "string", }) .option("min-severity", { choices: ["low", "medium", "high", "critical"], default: "low", description: "Minimum final target severity to include in console or SARIF output.", type: "string", }) .option("fail-severity", { choices: ["low", "medium", "high", "critical"], default: "high", description: "Exit with code 3 when any target reaches this final severity or above.", type: "string", }) .option("max-targets", { description: "Optional safety limit for the number of unique npm/PyPI purls to analyze.", type: "number", }) .option("scope", { choices: ["all", "required"], default: "all", description: "Target selection scope. Use 'required' to scan only components with CycloneDX scope=required (missing scope is treated as required).", type: "string", }) .option("include-trusted", { default: false, description: "Include packages already marked with trusted publishing metadata in predictive audit target selection.", type: "boolean", }) .option("only-trusted", { default: false, description: "Restrict predictive audit target selection to packages marked with trusted publishing metadata.", type: "boolean", }) .option("prioritize-direct-runtime", { default: true, description: "Prioritize direct runtime dependencies ahead of optional, development-only, or platform-specific transitive packages during target selection.", type: "boolean", }) .option("allowlist-file", { description: "Optional JSON array or newline-delimited file of purl prefixes to exclude from predictive audit target selection in addition to the built-in well-known allowlist.", type: "string", }) .check((argv) => { if (!argv.bom && !argv.bomDir) { throw new Error("Specify --bom or --bom-dir."); } if (argv.bom && argv.bomDir) { throw new Error("Use either --bom or --bom-dir, not both."); } if (argv.output && argv.reportFile) { throw new Error("Use either --report-file or --output, not both."); } if (argv.includeTrusted && argv.onlyTrusted) { throw new Error( "Use either --include-trusted or --only-trusted, not both.", ); } return true; }) .completion("completion", "Generate bash/zsh completion") .epilogue("for documentation, visit https://cdxgen.github.io/cdxgen") .scriptName("cdx-audit") .version(retrieveCdxgenVersion()) .help() .wrap(Math.min(120, yargs().terminalWidth())).argv; /** * Split a CSV option into a normalized string array. * * @param {string | undefined} value CSV string * @returns {string[] | undefined} parsed values */ function splitCsv(value) { if (!value) { return undefined; } return value .split(",") .map((entry) => entry.trim()) .filter(Boolean); } /** * Print or write rendered report output. * * @param {string} output rendered output * @param {string | undefined} outputPath optional file path * @returns {void} */ function writeOrPrint(output, outputPath) { if (!outputPath) { process.stdout.write(output); return; } const parentDir = dirname(outputPath); if (!safeExistsSync(parentDir)) { safeMkdirSync(parentDir, { recursive: true }); } writeFileSync(outputPath, output); } (async () => { const progressTracker = createProgressTracker(); try { const reportFile = args.reportFile || args.output; const report = await runAudit({ allowlistFile: args.allowlistFile, bom: args.bom, bomDir: args.bomDir, categories: splitCsv(args.categories), directBomAudit: args.directBomAudit, failSeverity: args.failSeverity, maxTargets: args.maxTargets, minSeverity: args.minSeverity, onProgress: progressTracker.onProgress, prioritizeDirectRuntime: args.prioritizeDirectRuntime, report: args.report, reportsDir: args.reportsDir, rulesDir: args.rulesDir, scope: args.scope === "required" ? "required" : undefined, trusted: args.onlyTrusted ? "only" : args.includeTrusted ? "include" : undefined, trustedSelectionHelp: "Use --include-trusted to include them or --only-trusted to audit just those packages.", workspaceDir: args.workspaceDir, }); const finalized = finalizeAuditReport(report, { failSeverity: args.failSeverity, minSeverity: args.minSeverity, report: args.report, }); writeOrPrint(finalized.output, reportFile); process.exit(finalized.exitCode); } catch (error) { console.error(error.message); process.exit(1); } finally { progressTracker.stop(); } })();