using System; using System.CommandLine; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using StellaOps.Cli.Configuration; using StellaOps.Cli.Extensions; using StellaOps.Cli.Plugins; using StellaOps.Cli.Services.Models.AdvisoryAi; namespace StellaOps.Cli.Commands; internal static class CommandFactory { public static RootCommand Create( IServiceProvider services, StellaOpsCliOptions options, CancellationToken cancellationToken, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(loggerFactory); var verboseOption = new Option("--verbose", new[] { "-v" }) { Description = "Enable verbose logging output." }; var globalTenantOption = new Option("--tenant", new[] { "-t" }) { Description = "Tenant context for the operation. Overrides profile and STELLAOPS_TENANT environment variable." }; var root = new RootCommand("StellaOps command-line interface") { TreatUnmatchedTokensAsErrors = true }; root.Add(verboseOption); root.Add(globalTenantOption); root.Add(BuildScannerCommand(services, verboseOption, cancellationToken)); root.Add(BuildScanCommand(services, options, verboseOption, cancellationToken)); root.Add(BuildRubyCommand(services, verboseOption, cancellationToken)); root.Add(BuildPhpCommand(services, verboseOption, cancellationToken)); root.Add(BuildPythonCommand(services, verboseOption, cancellationToken)); root.Add(BuildBunCommand(services, verboseOption, cancellationToken)); root.Add(BuildDatabaseCommand(services, verboseOption, cancellationToken)); root.Add(BuildSourcesCommand(services, verboseOption, cancellationToken)); root.Add(BuildAocCommand(services, verboseOption, cancellationToken)); root.Add(BuildAuthCommand(services, options, verboseOption, cancellationToken)); root.Add(BuildTenantsCommand(services, options, verboseOption, cancellationToken)); root.Add(BuildPolicyCommand(services, options, verboseOption, cancellationToken)); root.Add(BuildTaskRunnerCommand(services, verboseOption, cancellationToken)); root.Add(BuildFindingsCommand(services, verboseOption, cancellationToken)); root.Add(BuildAdviseCommand(services, options, verboseOption, cancellationToken)); root.Add(BuildConfigCommand(options)); root.Add(BuildKmsCommand(services, verboseOption, cancellationToken)); root.Add(BuildVulnCommand(services, verboseOption, cancellationToken)); root.Add(BuildVexCommand(services, options, verboseOption, cancellationToken)); root.Add(BuildCryptoCommand(services, verboseOption, cancellationToken)); root.Add(BuildAttestCommand(services, verboseOption, cancellationToken)); root.Add(BuildRiskProfileCommand(verboseOption, cancellationToken)); root.Add(BuildAdvisoryCommand(services, verboseOption, cancellationToken)); root.Add(BuildForensicCommand(services, verboseOption, cancellationToken)); root.Add(BuildPromotionCommand(services, verboseOption, cancellationToken)); root.Add(BuildDetscoreCommand(services, verboseOption, cancellationToken)); root.Add(BuildObsCommand(services, verboseOption, cancellationToken)); root.Add(BuildPackCommand(services, verboseOption, cancellationToken)); root.Add(BuildExceptionsCommand(services, verboseOption, cancellationToken)); root.Add(BuildOrchCommand(services, verboseOption, cancellationToken)); root.Add(BuildSbomCommand(services, verboseOption, cancellationToken)); root.Add(BuildNotifyCommand(services, verboseOption, cancellationToken)); root.Add(BuildSbomerCommand(services, verboseOption, cancellationToken)); root.Add(BuildCvssCommand(services, verboseOption, cancellationToken)); root.Add(BuildRiskCommand(services, verboseOption, cancellationToken)); root.Add(BuildReachabilityCommand(services, verboseOption, cancellationToken)); root.Add(BuildApiCommand(services, verboseOption, cancellationToken)); root.Add(BuildSdkCommand(services, verboseOption, cancellationToken)); root.Add(BuildMirrorCommand(services, verboseOption, cancellationToken)); root.Add(BuildAirgapCommand(services, verboseOption, cancellationToken)); root.Add(SystemCommandBuilder.BuildSystemCommand(services, verboseOption, cancellationToken)); var pluginLogger = loggerFactory.CreateLogger(); var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger); pluginLoader.RegisterModules(root, verboseOption, cancellationToken); return root; } private static Command BuildScannerCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var scanner = new Command("scanner", "Manage scanner artifacts and lifecycle."); var download = new Command("download", "Download the latest scanner bundle."); var channelOption = new Option("--channel", new[] { "-c" }) { Description = "Scanner channel (stable, beta, nightly)." }; var outputOption = new Option("--output") { Description = "Optional output path for the downloaded bundle." }; var overwriteOption = new Option("--overwrite") { Description = "Overwrite existing bundle if present." }; var noInstallOption = new Option("--no-install") { Description = "Skip installing the scanner container after download." }; download.Add(channelOption); download.Add(outputOption); download.Add(overwriteOption); download.Add(noInstallOption); download.SetAction((parseResult, _) => { var channel = parseResult.GetValue(channelOption) ?? "stable"; var output = parseResult.GetValue(outputOption); var overwrite = parseResult.GetValue(overwriteOption); var install = !parseResult.GetValue(noInstallOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleScannerDownloadAsync(services, channel, output, overwrite, install, verbose, cancellationToken); }); scanner.Add(download); return scanner; } private static Command BuildCvssCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var cvss = new Command("cvss", "CVSS v4.0 receipt operations (score, show, history, export)." ); var score = new Command("score", "Create a CVSS v4 receipt for a vulnerability."); var vulnOption = new Option("--vuln") { Description = "Vulnerability identifier (e.g., CVE).", IsRequired = true }; var policyFileOption = new Option("--policy-file") { Description = "Path to CvssPolicy JSON file.", IsRequired = true }; var vectorOption = new Option("--vector") { Description = "CVSS:4.0 vector string.", IsRequired = true }; var jsonOption = new Option("--json") { Description = "Emit JSON output." }; score.Add(vulnOption); score.Add(policyFileOption); score.Add(vectorOption); score.Add(jsonOption); score.SetAction((parseResult, _) => { var vuln = parseResult.GetValue(vulnOption) ?? string.Empty; var policyPath = parseResult.GetValue(policyFileOption) ?? string.Empty; var vector = parseResult.GetValue(vectorOption) ?? string.Empty; var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleCvssScoreAsync(services, vuln, policyPath, vector, json, verbose, cancellationToken); }); var show = new Command("show", "Fetch a CVSS receipt by ID."); var receiptArg = new Argument("receipt-id") { Description = "Receipt identifier." }; show.Add(receiptArg); var showJsonOption = new Option("--json") { Description = "Emit JSON output." }; show.Add(showJsonOption); show.SetAction((parseResult, _) => { var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty; var json = parseResult.GetValue(showJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleCvssShowAsync(services, receiptId, json, verbose, cancellationToken); }); var history = new Command("history", "Show receipt amendment history."); history.Add(receiptArg); var historyJsonOption = new Option("--json") { Description = "Emit JSON output." }; history.Add(historyJsonOption); history.SetAction((parseResult, _) => { var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty; var json = parseResult.GetValue(historyJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleCvssHistoryAsync(services, receiptId, json, verbose, cancellationToken); }); var export = new Command("export", "Export a CVSS receipt to JSON (pdf not yet supported)."); export.Add(receiptArg); var formatOption = new Option("--format") { Description = "json|pdf (json default)." }; var outOption = new Option("--out") { Description = "Output file path." }; export.Add(formatOption); export.Add(outOption); export.SetAction((parseResult, _) => { var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty; var format = parseResult.GetValue(formatOption) ?? "json"; var output = parseResult.GetValue(outOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleCvssExportAsync(services, receiptId, format, output, verbose, cancellationToken); }); cvss.Add(score); cvss.Add(show); cvss.Add(history); cvss.Add(export); return cvss; } private static Command BuildScanCommand(IServiceProvider services, StellaOpsCliOptions options, Option verboseOption, CancellationToken cancellationToken) { var scan = new Command("scan", "Execute scanners and manage scan outputs."); var run = new Command("run", "Execute a scanner bundle with the configured runner."); var runnerOption = new Option("--runner") { Description = "Execution runtime (dotnet, self, docker)." }; var entryOption = new Option("--entry") { Description = "Path to the scanner entrypoint or Docker image.", Required = true }; var targetOption = new Option("--target") { Description = "Directory to scan.", Required = true }; var argsArgument = new Argument("scanner-args") { Arity = ArgumentArity.ZeroOrMore }; run.Add(runnerOption); run.Add(entryOption); run.Add(targetOption); run.Add(argsArgument); run.SetAction((parseResult, _) => { var runner = parseResult.GetValue(runnerOption) ?? options.DefaultRunner; var entry = parseResult.GetValue(entryOption) ?? string.Empty; var target = parseResult.GetValue(targetOption) ?? string.Empty; var forwardedArgs = parseResult.GetValue(argsArgument) ?? Array.Empty(); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleScannerRunAsync(services, runner, entry, target, forwardedArgs, verbose, cancellationToken); }); var upload = new Command("upload", "Upload completed scan results to the backend."); var fileOption = new Option("--file") { Description = "Path to the scan result artifact.", Required = true }; upload.Add(fileOption); upload.SetAction((parseResult, _) => { var file = parseResult.GetValue(fileOption) ?? string.Empty; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleScanUploadAsync(services, file, verbose, cancellationToken); }); var entryTrace = new Command("entrytrace", "Show entry trace summary for a scan."); var scanIdOption = new Option("--scan-id") { Description = "Scan identifier.", Required = true }; var includeNdjsonOption = new Option("--include-ndjson") { Description = "Include raw NDJSON output." }; entryTrace.Add(scanIdOption); entryTrace.Add(includeNdjsonOption); entryTrace.SetAction((parseResult, _) => { var id = parseResult.GetValue(scanIdOption) ?? string.Empty; var includeNdjson = parseResult.GetValue(includeNdjsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleScanEntryTraceAsync(services, id, includeNdjson, verbose, cancellationToken); }); scan.Add(entryTrace); scan.Add(run); scan.Add(upload); return scan; } private static Command BuildRubyCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var ruby = new Command("ruby", "Work with Ruby analyzer outputs."); var inspect = new Command("inspect", "Inspect a local Ruby workspace."); var inspectRootOption = new Option("--root") { Description = "Path to the Ruby workspace (defaults to current directory)." }; var inspectFormatOption = new Option("--format") { Description = "Output format (table or json)." }; inspect.Add(inspectRootOption); inspect.Add(inspectFormatOption); inspect.SetAction((parseResult, _) => { var root = parseResult.GetValue(inspectRootOption); var format = parseResult.GetValue(inspectFormatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRubyInspectAsync( services, root, format, verbose, cancellationToken); }); var resolve = new Command("resolve", "Fetch Ruby packages for a completed scan."); var resolveImageOption = new Option("--image") { Description = "Image reference (digest or tag) used by the scan." }; var resolveScanIdOption = new Option("--scan-id") { Description = "Explicit scan identifier." }; var resolveFormatOption = new Option("--format") { Description = "Output format (table or json)." }; resolve.Add(resolveImageOption); resolve.Add(resolveScanIdOption); resolve.Add(resolveFormatOption); resolve.SetAction((parseResult, _) => { var image = parseResult.GetValue(resolveImageOption); var scanId = parseResult.GetValue(resolveScanIdOption); var format = parseResult.GetValue(resolveFormatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRubyResolveAsync( services, image, scanId, format, verbose, cancellationToken); }); ruby.Add(inspect); ruby.Add(resolve); return ruby; } private static Command BuildPhpCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var php = new Command("php", "Work with PHP analyzer outputs."); var inspect = new Command("inspect", "Inspect a local PHP workspace."); var inspectRootOption = new Option("--root") { Description = "Path to the PHP workspace (defaults to current directory)." }; var inspectFormatOption = new Option("--format") { Description = "Output format (table or json)." }; inspect.Add(inspectRootOption); inspect.Add(inspectFormatOption); inspect.SetAction((parseResult, _) => { var root = parseResult.GetValue(inspectRootOption); var format = parseResult.GetValue(inspectFormatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePhpInspectAsync( services, root, format, verbose, cancellationToken); }); php.Add(inspect); return php; } private static Command BuildPythonCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var python = new Command("python", "Work with Python analyzer outputs."); var inspect = new Command("inspect", "Inspect a local Python workspace or virtual environment."); var inspectRootOption = new Option("--root") { Description = "Path to the Python workspace (defaults to current directory)." }; var inspectFormatOption = new Option("--format") { Description = "Output format (table, json, or aoc)." }; var inspectSitePackagesOption = new Option("--site-packages") { Description = "Additional site-packages directories to scan." }; var inspectIncludeFrameworksOption = new Option("--include-frameworks") { Description = "Include detected framework hints in output." }; var inspectIncludeCapabilitiesOption = new Option("--include-capabilities") { Description = "Include detected capability signals in output." }; inspect.Add(inspectRootOption); inspect.Add(inspectFormatOption); inspect.Add(inspectSitePackagesOption); inspect.Add(inspectIncludeFrameworksOption); inspect.Add(inspectIncludeCapabilitiesOption); inspect.SetAction((parseResult, _) => { var root = parseResult.GetValue(inspectRootOption); var format = parseResult.GetValue(inspectFormatOption) ?? "table"; var sitePackages = parseResult.GetValue(inspectSitePackagesOption); var includeFrameworks = parseResult.GetValue(inspectIncludeFrameworksOption); var includeCapabilities = parseResult.GetValue(inspectIncludeCapabilitiesOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePythonInspectAsync( services, root, format, sitePackages, includeFrameworks, includeCapabilities, verbose, cancellationToken); }); python.Add(inspect); return python; } private static Command BuildBunCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var bun = new Command("bun", "Work with Bun analyzer outputs."); var inspect = new Command("inspect", "Inspect a local Bun workspace."); var inspectRootOption = new Option("--root") { Description = "Path to the Bun workspace (defaults to current directory)." }; var inspectFormatOption = new Option("--format") { Description = "Output format (table or json)." }; inspect.Add(inspectRootOption); inspect.Add(inspectFormatOption); inspect.SetAction((parseResult, _) => { var root = parseResult.GetValue(inspectRootOption); var format = parseResult.GetValue(inspectFormatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleBunInspectAsync( services, root, format, verbose, cancellationToken); }); var resolve = new Command("resolve", "Fetch Bun packages for a completed scan."); var resolveImageOption = new Option("--image") { Description = "Image reference (digest or tag) used by the scan." }; var resolveScanIdOption = new Option("--scan-id") { Description = "Explicit scan identifier." }; var resolveFormatOption = new Option("--format") { Description = "Output format (table or json)." }; resolve.Add(resolveImageOption); resolve.Add(resolveScanIdOption); resolve.Add(resolveFormatOption); resolve.SetAction((parseResult, _) => { var image = parseResult.GetValue(resolveImageOption); var scanId = parseResult.GetValue(resolveScanIdOption); var format = parseResult.GetValue(resolveFormatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleBunResolveAsync( services, image, scanId, format, verbose, cancellationToken); }); bun.Add(inspect); bun.Add(resolve); return bun; } private static Command BuildKmsCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var kms = new Command("kms", "Manage file-backed signing keys."); var export = new Command("export", "Export key material to a portable bundle."); var exportRootOption = new Option("--root") { Description = "Root directory containing file-based KMS material." }; var exportKeyOption = new Option("--key-id") { Description = "Logical KMS key identifier to export.", Required = true }; var exportVersionOption = new Option("--version") { Description = "Key version identifier to export (defaults to active version)." }; var exportOutputOption = new Option("--output") { Description = "Destination file path for exported key material.", Required = true }; var exportForceOption = new Option("--force") { Description = "Overwrite the destination file if it already exists." }; var exportPassphraseOption = new Option("--passphrase") { Description = "File KMS passphrase (falls back to STELLAOPS_KMS_PASSPHRASE or interactive prompt)." }; export.Add(exportRootOption); export.Add(exportKeyOption); export.Add(exportVersionOption); export.Add(exportOutputOption); export.Add(exportForceOption); export.Add(exportPassphraseOption); export.SetAction((parseResult, _) => { var root = parseResult.GetValue(exportRootOption); var keyId = parseResult.GetValue(exportKeyOption) ?? string.Empty; var versionId = parseResult.GetValue(exportVersionOption); var output = parseResult.GetValue(exportOutputOption) ?? string.Empty; var force = parseResult.GetValue(exportForceOption); var passphrase = parseResult.GetValue(exportPassphraseOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleKmsExportAsync( services, root, keyId, versionId, output, force, passphrase, verbose, cancellationToken); }); var import = new Command("import", "Import key material from a bundle."); var importRootOption = new Option("--root") { Description = "Root directory containing file-based KMS material." }; var importKeyOption = new Option("--key-id") { Description = "Logical KMS key identifier to import into.", Required = true }; var importInputOption = new Option("--input") { Description = "Path to exported key material JSON.", Required = true }; var importVersionOption = new Option("--version") { Description = "Override the imported version identifier." }; var importPassphraseOption = new Option("--passphrase") { Description = "File KMS passphrase (falls back to STELLAOPS_KMS_PASSPHRASE or interactive prompt)." }; import.Add(importRootOption); import.Add(importKeyOption); import.Add(importInputOption); import.Add(importVersionOption); import.Add(importPassphraseOption); import.SetAction((parseResult, _) => { var root = parseResult.GetValue(importRootOption); var keyId = parseResult.GetValue(importKeyOption) ?? string.Empty; var input = parseResult.GetValue(importInputOption) ?? string.Empty; var versionOverride = parseResult.GetValue(importVersionOption); var passphrase = parseResult.GetValue(importPassphraseOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleKmsImportAsync( services, root, keyId, input, versionOverride, passphrase, verbose, cancellationToken); }); kms.Add(export); kms.Add(import); return kms; } private static Command BuildDatabaseCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var db = new Command("db", "Trigger Concelier database operations via backend jobs."); var fetch = new Command("fetch", "Trigger connector fetch/parse/map stages."); var sourceOption = new Option("--source") { Description = "Connector source identifier (e.g. redhat, osv, vmware).", Required = true }; var stageOption = new Option("--stage") { Description = "Stage to trigger: fetch, parse, or map." }; var modeOption = new Option("--mode") { Description = "Optional connector-specific mode (init, resume, cursor)." }; fetch.Add(sourceOption); fetch.Add(stageOption); fetch.Add(modeOption); fetch.SetAction((parseResult, _) => { var source = parseResult.GetValue(sourceOption) ?? string.Empty; var stage = parseResult.GetValue(stageOption) ?? "fetch"; var mode = parseResult.GetValue(modeOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleConnectorJobAsync(services, source, stage, mode, verbose, cancellationToken); }); var merge = new Command("merge", "Run canonical merge reconciliation."); merge.SetAction((parseResult, _) => { var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleMergeJobAsync(services, verbose, cancellationToken); }); var export = new Command("export", "Run Concelier export jobs."); var formatOption = new Option("--format") { Description = "Export format: json or trivy-db." }; var deltaOption = new Option("--delta") { Description = "Request a delta export when supported." }; var publishFullOption = new Option("--publish-full") { Description = "Override whether full exports push to ORAS (true/false)." }; var publishDeltaOption = new Option("--publish-delta") { Description = "Override whether delta exports push to ORAS (true/false)." }; var includeFullOption = new Option("--bundle-full") { Description = "Override whether offline bundles include full exports (true/false)." }; var includeDeltaOption = new Option("--bundle-delta") { Description = "Override whether offline bundles include delta exports (true/false)." }; export.Add(formatOption); export.Add(deltaOption); export.Add(publishFullOption); export.Add(publishDeltaOption); export.Add(includeFullOption); export.Add(includeDeltaOption); export.SetAction((parseResult, _) => { var format = parseResult.GetValue(formatOption) ?? "json"; var delta = parseResult.GetValue(deltaOption); var publishFull = parseResult.GetValue(publishFullOption); var publishDelta = parseResult.GetValue(publishDeltaOption); var includeFull = parseResult.GetValue(includeFullOption); var includeDelta = parseResult.GetValue(includeDeltaOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExportJobAsync(services, format, delta, publishFull, publishDelta, includeFull, includeDelta, verbose, cancellationToken); }); db.Add(fetch); db.Add(merge); db.Add(export); return db; } private static Command BuildCryptoCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var crypto = new Command("crypto", "Inspect StellaOps cryptography providers."); var providers = new Command("providers", "List registered crypto providers and keys."); var jsonOption = new Option("--json") { Description = "Emit JSON output." }; var profileOption = new Option("--profile") { Description = "Temporarily override the active registry profile when computing provider order." }; providers.Add(jsonOption); providers.Add(profileOption); providers.SetAction((parseResult, _) => { var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); var profile = parseResult.GetValue(profileOption); return CommandHandlers.HandleCryptoProvidersAsync(services, verbose, json, profile, cancellationToken); }); crypto.Add(providers); return crypto; } private static Command BuildSourcesCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var sources = new Command("sources", "Interact with source ingestion workflows."); var ingest = new Command("ingest", "Validate source documents before ingestion."); var dryRunOption = new Option("--dry-run") { Description = "Evaluate guard rules without writing to persistent storage." }; var sourceOption = new Option("--source") { Description = "Logical source identifier (e.g. redhat, ubuntu, osv).", Required = true }; var inputOption = new Option("--input") { Description = "Path to a local document or HTTPS URI.", Required = true }; var tenantOption = new Option("--tenant") { Description = "Tenant identifier override." }; var formatOption = new Option("--format") { Description = "Output format: table or json." }; var noColorOption = new Option("--no-color") { Description = "Disable ANSI colouring in console output." }; var outputOption = new Option("--output") { Description = "Write the JSON report to the specified file path." }; ingest.Add(dryRunOption); ingest.Add(sourceOption); ingest.Add(inputOption); ingest.Add(tenantOption); ingest.Add(formatOption); ingest.Add(noColorOption); ingest.Add(outputOption); ingest.SetAction((parseResult, _) => { var dryRun = parseResult.GetValue(dryRunOption); var source = parseResult.GetValue(sourceOption) ?? string.Empty; var input = parseResult.GetValue(inputOption) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var format = parseResult.GetValue(formatOption) ?? "table"; var noColor = parseResult.GetValue(noColorOption); var output = parseResult.GetValue(outputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSourcesIngestAsync( services, dryRun, source, input, tenant, format, noColor, output, verbose, cancellationToken); }); sources.Add(ingest); return sources; } private static Command BuildAocCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var aoc = new Command("aoc", "Aggregation-Only Contract verification commands."); var verify = new Command("verify", "Verify stored raw documents against AOC guardrails."); var sinceOption = new Option("--since") { Description = "Verification window start (ISO-8601 timestamp) or relative duration (e.g. 24h, 7d)." }; var limitOption = new Option("--limit") { Description = "Maximum number of violations to include per code (0 = no limit)." }; var sourcesOption = new Option("--sources") { Description = "Comma-separated list of sources (e.g. redhat,ubuntu,osv)." }; var codesOption = new Option("--codes") { Description = "Comma-separated list of violation codes (ERR_AOC_00x)." }; var formatOption = new Option("--format") { Description = "Output format: table or json." }; var exportOption = new Option("--export") { Description = "Write the JSON report to the specified file path." }; var tenantOption = new Option("--tenant") { Description = "Tenant identifier override." }; var noColorOption = new Option("--no-color") { Description = "Disable ANSI colouring in console output." }; verify.Add(sinceOption); verify.Add(limitOption); verify.Add(sourcesOption); verify.Add(codesOption); verify.Add(formatOption); verify.Add(exportOption); verify.Add(tenantOption); verify.Add(noColorOption); verify.SetAction((parseResult, _) => { var since = parseResult.GetValue(sinceOption); var limit = parseResult.GetValue(limitOption); var sources = parseResult.GetValue(sourcesOption); var codes = parseResult.GetValue(codesOption); var format = parseResult.GetValue(formatOption) ?? "table"; var export = parseResult.GetValue(exportOption); var tenant = parseResult.GetValue(tenantOption); var noColor = parseResult.GetValue(noColorOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAocVerifyAsync( services, since, limit, sources, codes, format, export, tenant, noColor, verbose, cancellationToken); }); aoc.Add(verify); return aoc; } private static Command BuildAuthCommand(IServiceProvider services, StellaOpsCliOptions options, Option verboseOption, CancellationToken cancellationToken) { var auth = new Command("auth", "Manage authentication with StellaOps Authority."); var login = new Command("login", "Acquire and cache access tokens using the configured credentials."); var forceOption = new Option("--force") { Description = "Ignore existing cached tokens and force re-authentication." }; login.Add(forceOption); login.SetAction((parseResult, _) => { var verbose = parseResult.GetValue(verboseOption); var force = parseResult.GetValue(forceOption); return CommandHandlers.HandleAuthLoginAsync(services, options, verbose, force, cancellationToken); }); var logout = new Command("logout", "Remove cached tokens for the current credentials."); logout.SetAction((parseResult, _) => { var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAuthLogoutAsync(services, options, verbose, cancellationToken); }); var status = new Command("status", "Display cached token status."); status.SetAction((parseResult, _) => { var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAuthStatusAsync(services, options, verbose, cancellationToken); }); var whoami = new Command("whoami", "Display cached token claims (subject, scopes, expiry)."); whoami.SetAction((parseResult, _) => { var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAuthWhoAmIAsync(services, options, verbose, cancellationToken); }); var revoke = new Command("revoke", "Manage revocation exports."); var export = new Command("export", "Export the revocation bundle and signature to disk."); var outputOption = new Option("--output") { Description = "Directory to write exported revocation files (defaults to current directory)." }; export.Add(outputOption); export.SetAction((parseResult, _) => { var output = parseResult.GetValue(outputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAuthRevokeExportAsync(services, options, output, verbose, cancellationToken); }); revoke.Add(export); var verify = new Command("verify", "Verify a revocation bundle against a detached JWS signature."); var bundleOption = new Option("--bundle") { Description = "Path to the revocation-bundle.json file." }; var signatureOption = new Option("--signature") { Description = "Path to the revocation-bundle.json.jws file." }; var keyOption = new Option("--key") { Description = "Path to the PEM-encoded public/private key used for verification." }; verify.Add(bundleOption); verify.Add(signatureOption); verify.Add(keyOption); verify.SetAction((parseResult, _) => { var bundlePath = parseResult.GetValue(bundleOption) ?? string.Empty; var signaturePath = parseResult.GetValue(signatureOption) ?? string.Empty; var keyPath = parseResult.GetValue(keyOption) ?? string.Empty; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAuthRevokeVerifyAsync(bundlePath, signaturePath, keyPath, verbose, cancellationToken); }); revoke.Add(verify); // CLI-TEN-49-001: Token minting and delegation commands var token = new Command("token", "Service account token operations (CLI-TEN-49-001)."); var mint = new Command("mint", "Mint a service account token."); var serviceAccountOption = new Option("--service-account", new[] { "-s" }) { Description = "Service account identifier to mint token for.", Required = true }; var mintScopesOption = new Option("--scope") { Description = "Scopes to include in the minted token (can be specified multiple times).", AllowMultipleArgumentsPerToken = true }; var mintExpiresOption = new Option("--expires-in") { Description = "Token expiry in seconds (defaults to server default)." }; var mintTenantOption = new Option("--tenant") { Description = "Tenant context for the token." }; var mintReasonOption = new Option("--reason") { Description = "Audit reason for minting the token." }; var mintOutputOption = new Option("--raw") { Description = "Output only the raw token value (for automation)." }; mint.Add(serviceAccountOption); mint.Add(mintScopesOption); mint.Add(mintExpiresOption); mint.Add(mintTenantOption); mint.Add(mintReasonOption); mint.Add(mintOutputOption); mint.SetAction((parseResult, _) => { var serviceAccount = parseResult.GetValue(serviceAccountOption) ?? string.Empty; var scopes = parseResult.GetValue(mintScopesOption) ?? Array.Empty(); var expiresIn = parseResult.GetValue(mintExpiresOption); var tenant = parseResult.GetValue(mintTenantOption); var reason = parseResult.GetValue(mintReasonOption); var raw = parseResult.GetValue(mintOutputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleTokenMintAsync(services, options, serviceAccount, scopes, expiresIn, tenant, reason, raw, verbose, cancellationToken); }); var delegateCmd = new Command("delegate", "Delegate your token to another principal."); var delegateToOption = new Option("--to") { Description = "Principal identifier to delegate to.", Required = true }; var delegateScopesOption = new Option("--scope") { Description = "Scopes to include in the delegation (must be subset of current token).", AllowMultipleArgumentsPerToken = true }; var delegateExpiresOption = new Option("--expires-in") { Description = "Delegation expiry in seconds (defaults to remaining token lifetime)." }; var delegateTenantOption = new Option("--tenant") { Description = "Tenant context for the delegation." }; var delegateReasonOption = new Option("--reason") { Description = "Audit reason for the delegation.", Required = true }; var delegateRawOption = new Option("--raw") { Description = "Output only the raw token value (for automation)." }; delegateCmd.Add(delegateToOption); delegateCmd.Add(delegateScopesOption); delegateCmd.Add(delegateExpiresOption); delegateCmd.Add(delegateTenantOption); delegateCmd.Add(delegateReasonOption); delegateCmd.Add(delegateRawOption); delegateCmd.SetAction((parseResult, _) => { var delegateTo = parseResult.GetValue(delegateToOption) ?? string.Empty; var scopes = parseResult.GetValue(delegateScopesOption) ?? Array.Empty(); var expiresIn = parseResult.GetValue(delegateExpiresOption); var tenant = parseResult.GetValue(delegateTenantOption); var reason = parseResult.GetValue(delegateReasonOption); var raw = parseResult.GetValue(delegateRawOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleTokenDelegateAsync(services, options, delegateTo, scopes, expiresIn, tenant, reason, raw, verbose, cancellationToken); }); token.Add(mint); token.Add(delegateCmd); auth.Add(login); auth.Add(logout); auth.Add(status); auth.Add(whoami); auth.Add(revoke); auth.Add(token); return auth; } private static Command BuildTenantsCommand(IServiceProvider services, StellaOpsCliOptions options, Option verboseOption, CancellationToken cancellationToken) { _ = options; var tenants = new Command("tenants", "Manage tenant contexts (CLI-TEN-47-001)."); var list = new Command("list", "List available tenants for the authenticated principal."); var tenantOption = new Option("--tenant") { Description = "Tenant context to use for the request (required for multi-tenant environments)." }; var jsonOption = new Option("--json") { Description = "Output tenant list in JSON format." }; list.Add(tenantOption); list.Add(jsonOption); list.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleTenantsListAsync(services, options, tenant, json, verbose, cancellationToken); }); var use = new Command("use", "Set the active tenant context for subsequent commands."); var tenantIdArgument = new Argument("tenant-id") { Description = "Tenant identifier to use as the default context." }; use.Add(tenantIdArgument); use.SetAction((parseResult, _) => { var tenantId = parseResult.GetValue(tenantIdArgument) ?? string.Empty; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleTenantsUseAsync(services, options, tenantId, verbose, cancellationToken); }); var current = new Command("current", "Show the currently active tenant context."); var currentJsonOption = new Option("--json") { Description = "Output profile in JSON format." }; current.Add(currentJsonOption); current.SetAction((parseResult, _) => { var json = parseResult.GetValue(currentJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleTenantsCurrentAsync(json, verbose, cancellationToken); }); var clear = new Command("clear", "Clear the active tenant context (use default or require --tenant)."); clear.SetAction((_, _) => { return CommandHandlers.HandleTenantsClearAsync(cancellationToken); }); tenants.Add(list); tenants.Add(use); tenants.Add(current); tenants.Add(clear); return tenants; } private static Command BuildPolicyCommand(IServiceProvider services, StellaOpsCliOptions options, Option verboseOption, CancellationToken cancellationToken) { _ = options; var policy = new Command("policy", "Interact with Policy Engine operations."); var simulate = new Command("simulate", "Simulate a policy revision against selected SBOMs and environment."); var policyIdArgument = new Argument("policy-id") { Description = "Policy identifier (e.g. P-7)." }; simulate.Add(policyIdArgument); var baseOption = new Option("--base") { Description = "Base policy version for diff calculations." }; var candidateOption = new Option("--candidate") { Description = "Candidate policy version. Defaults to latest approved." }; var sbomOption = new Option("--sbom") { Description = "SBOM identifier to include (repeatable).", Arity = ArgumentArity.ZeroOrMore }; sbomOption.AllowMultipleArgumentsPerToken = true; var envOption = new Option("--env") { Description = "Environment override (key=value, repeatable).", Arity = ArgumentArity.ZeroOrMore }; envOption.AllowMultipleArgumentsPerToken = true; var formatOption = new Option("--format") { Description = "Output format: table, json, or markdown." }; var outputOption = new Option("--output") { Description = "Write output to the specified file." }; var explainOption = new Option("--explain") { Description = "Request explain traces for diffed findings." }; var failOnDiffOption = new Option("--fail-on-diff") { Description = "Exit with code 20 when findings are added or removed." }; // CLI-EXC-25-002: Exception preview flags var withExceptionOption = new Option("--with-exception") { Description = "Include exception ID in simulation (repeatable). Shows what-if the exception were applied.", Arity = ArgumentArity.ZeroOrMore }; withExceptionOption.AllowMultipleArgumentsPerToken = true; var withoutExceptionOption = new Option("--without-exception") { Description = "Exclude exception ID from simulation (repeatable). Shows what-if the exception were removed.", Arity = ArgumentArity.ZeroOrMore }; withoutExceptionOption.AllowMultipleArgumentsPerToken = true; // CLI-POLICY-27-003: Enhanced simulation options var modeOption = new Option("--mode") { Description = "Simulation mode: quick (sample SBOMs) or batch (all matching SBOMs)." }; var sbomSelectorOption = new Option("--sbom-selector") { Description = "SBOM selector pattern (e.g. 'registry:docker.io/*', 'tag:production'). Repeatable.", Arity = ArgumentArity.ZeroOrMore }; sbomSelectorOption.AllowMultipleArgumentsPerToken = true; var heatmapOption = new Option("--heatmap") { Description = "Include severity heatmap summary in output." }; var manifestDownloadOption = new Option("--manifest-download") { Description = "Request manifest download URI for offline analysis." }; // CLI-SIG-26-002: Reachability override options var reachabilityStateOption = new Option("--reachability-state") { Description = "Override reachability state for vuln/package (format: 'CVE-XXXX:reachable' or 'pkg:npm/lodash@4.17.0:unreachable'). Repeatable.", Arity = ArgumentArity.ZeroOrMore }; reachabilityStateOption.AllowMultipleArgumentsPerToken = true; var reachabilityScoreOption = new Option("--reachability-score") { Description = "Override reachability score for vuln/package (format: 'CVE-XXXX:0.85' or 'pkg:npm/lodash@4.17.0:0.5'). Repeatable.", Arity = ArgumentArity.ZeroOrMore }; reachabilityScoreOption.AllowMultipleArgumentsPerToken = true; simulate.Add(baseOption); simulate.Add(candidateOption); simulate.Add(sbomOption); simulate.Add(envOption); simulate.Add(formatOption); simulate.Add(outputOption); simulate.Add(explainOption); simulate.Add(failOnDiffOption); simulate.Add(withExceptionOption); simulate.Add(withoutExceptionOption); simulate.Add(modeOption); simulate.Add(sbomSelectorOption); simulate.Add(heatmapOption); simulate.Add(manifestDownloadOption); simulate.Add(reachabilityStateOption); simulate.Add(reachabilityScoreOption); simulate.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(policyIdArgument) ?? string.Empty; var baseVersion = parseResult.GetValue(baseOption); var candidateVersion = parseResult.GetValue(candidateOption); var sbomSet = parseResult.GetValue(sbomOption) ?? Array.Empty(); var environment = parseResult.GetValue(envOption) ?? Array.Empty(); var format = parseResult.GetValue(formatOption); var output = parseResult.GetValue(outputOption); var explain = parseResult.GetValue(explainOption); var failOnDiff = parseResult.GetValue(failOnDiffOption); var withExceptions = parseResult.GetValue(withExceptionOption) ?? Array.Empty(); var withoutExceptions = parseResult.GetValue(withoutExceptionOption) ?? Array.Empty(); var mode = parseResult.GetValue(modeOption); var sbomSelectors = parseResult.GetValue(sbomSelectorOption) ?? Array.Empty(); var heatmap = parseResult.GetValue(heatmapOption); var manifestDownload = parseResult.GetValue(manifestDownloadOption); var reachabilityStates = parseResult.GetValue(reachabilityStateOption) ?? Array.Empty(); var reachabilityScores = parseResult.GetValue(reachabilityScoreOption) ?? Array.Empty(); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicySimulateAsync( services, policyId, baseVersion, candidateVersion, sbomSet, environment, format, output, explain, failOnDiff, withExceptions, withoutExceptions, mode, sbomSelectors, heatmap, manifestDownload, reachabilityStates, reachabilityScores, verbose, cancellationToken); }); policy.Add(simulate); var activate = new Command("activate", "Activate an approved policy revision."); var activatePolicyIdArgument = new Argument("policy-id") { Description = "Policy identifier (e.g. P-7)." }; activate.Add(activatePolicyIdArgument); var activateVersionOption = new Option("--version") { Description = "Revision version to activate.", Arity = ArgumentArity.ExactlyOne }; var activationNoteOption = new Option("--note") { Description = "Optional activation note recorded with the approval." }; var runNowOption = new Option("--run-now") { Description = "Trigger an immediate full policy run after activation." }; var scheduledAtOption = new Option("--scheduled-at") { Description = "Schedule activation at the provided ISO-8601 timestamp." }; var priorityOption = new Option("--priority") { Description = "Optional activation priority label (e.g. low, standard, high)." }; var rollbackOption = new Option("--rollback") { Description = "Indicate that this activation is a rollback to a previous version." }; var incidentOption = new Option("--incident") { Description = "Associate the activation with an incident identifier." }; activate.Add(activateVersionOption); activate.Add(activationNoteOption); activate.Add(runNowOption); activate.Add(scheduledAtOption); activate.Add(priorityOption); activate.Add(rollbackOption); activate.Add(incidentOption); activate.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(activatePolicyIdArgument) ?? string.Empty; var version = parseResult.GetValue(activateVersionOption); var note = parseResult.GetValue(activationNoteOption); var runNow = parseResult.GetValue(runNowOption); var scheduledAt = parseResult.GetValue(scheduledAtOption); var priority = parseResult.GetValue(priorityOption); var rollback = parseResult.GetValue(rollbackOption); var incident = parseResult.GetValue(incidentOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyActivateAsync( services, policyId, version, note, runNow, scheduledAt, priority, rollback, incident, verbose, cancellationToken); }); policy.Add(activate); // lint subcommand - validates policy DSL files locally var lint = new Command("lint", "Validate a policy DSL file locally without contacting the backend."); var lintFileArgument = new Argument("file") { Description = "Path to the policy DSL file to validate." }; var lintFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; var lintOutputOption = new Option("--output", new[] { "-o" }) { Description = "Write JSON output to the specified file." }; lint.Add(lintFileArgument); lint.Add(lintFormatOption); lint.Add(lintOutputOption); lint.SetAction((parseResult, _) => { var file = parseResult.GetValue(lintFileArgument) ?? string.Empty; var format = parseResult.GetValue(lintFormatOption); var output = parseResult.GetValue(lintOutputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyLintAsync(file, format, output, verbose, cancellationToken); }); policy.Add(lint); // edit subcommand - Git-backed DSL file editing with validation and commit var edit = new Command("edit", "Open a policy DSL file in $EDITOR, validate, and optionally commit with SemVer metadata."); var editFileArgument = new Argument("file") { Description = "Path to the policy DSL file to edit." }; var editCommitOption = new Option("--commit", new[] { "-c" }) { Description = "Commit changes after successful validation." }; var editVersionOption = new Option("--version", new[] { "-V" }) { Description = "SemVer version for commit metadata (e.g. 1.2.0)." }; var editMessageOption = new Option("--message", new[] { "-m" }) { Description = "Commit message (auto-generated if not provided)." }; var editNoValidateOption = new Option("--no-validate") { Description = "Skip validation after editing (not recommended)." }; edit.Add(editFileArgument); edit.Add(editCommitOption); edit.Add(editVersionOption); edit.Add(editMessageOption); edit.Add(editNoValidateOption); edit.SetAction((parseResult, _) => { var file = parseResult.GetValue(editFileArgument) ?? string.Empty; var commit = parseResult.GetValue(editCommitOption); var version = parseResult.GetValue(editVersionOption); var message = parseResult.GetValue(editMessageOption); var noValidate = parseResult.GetValue(editNoValidateOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyEditAsync(file, commit, version, message, noValidate, verbose, cancellationToken); }); policy.Add(edit); // test subcommand - run coverage fixtures against a policy DSL file var test = new Command("test", "Run coverage test fixtures against a policy DSL file."); var testFileArgument = new Argument("file") { Description = "Path to the policy DSL file to test." }; var testFixturesOption = new Option("--fixtures", new[] { "-d" }) { Description = "Path to fixtures directory (defaults to tests/policy//cases)." }; var testFilterOption = new Option("--filter") { Description = "Run only fixtures matching this pattern." }; var testFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; var testOutputOption = new Option("--output", new[] { "-o" }) { Description = "Write test results to the specified file." }; var testFailFastOption = new Option("--fail-fast") { Description = "Stop on first test failure." }; test.Add(testFileArgument); test.Add(testFixturesOption); test.Add(testFilterOption); test.Add(testFormatOption); test.Add(testOutputOption); test.Add(testFailFastOption); test.SetAction((parseResult, _) => { var file = parseResult.GetValue(testFileArgument) ?? string.Empty; var fixtures = parseResult.GetValue(testFixturesOption); var filter = parseResult.GetValue(testFilterOption); var format = parseResult.GetValue(testFormatOption); var output = parseResult.GetValue(testOutputOption); var failFast = parseResult.GetValue(testFailFastOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyTestAsync(file, fixtures, filter, format, output, failFast, verbose, cancellationToken); }); policy.Add(test); // CLI-POLICY-20-001: policy new - scaffold new policy from template var newCmd = new Command("new", "Create a new policy file from a template."); var newNameArgument = new Argument("name") { Description = "Name for the new policy (e.g. 'my-org-policy')." }; var newTemplateOption = new Option("--template", new[] { "-t" }) { Description = "Template to use: minimal (default), baseline, vex-precedence, reachability, secret-leak, full." }; var newOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output path for the policy file. Defaults to ./.stella" }; var newDescriptionOption = new Option("--description", new[] { "-d" }) { Description = "Policy description for metadata block." }; var newTagsOption = new Option("--tag") { Description = "Policy tag for metadata block (repeatable).", Arity = ArgumentArity.ZeroOrMore }; newTagsOption.AllowMultipleArgumentsPerToken = true; var newShadowOption = new Option("--shadow") { Description = "Enable shadow mode in settings (default: true)." }; newShadowOption.SetDefaultValue(true); var newFixturesOption = new Option("--fixtures") { Description = "Create test fixtures directory alongside the policy file." }; var newGitInitOption = new Option("--git-init") { Description = "Initialize a Git repository in the output directory." }; var newFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; newCmd.Add(newNameArgument); newCmd.Add(newTemplateOption); newCmd.Add(newOutputOption); newCmd.Add(newDescriptionOption); newCmd.Add(newTagsOption); newCmd.Add(newShadowOption); newCmd.Add(newFixturesOption); newCmd.Add(newGitInitOption); newCmd.Add(newFormatOption); newCmd.Add(verboseOption); newCmd.SetAction((parseResult, _) => { var name = parseResult.GetValue(newNameArgument) ?? string.Empty; var template = parseResult.GetValue(newTemplateOption); var output = parseResult.GetValue(newOutputOption); var description = parseResult.GetValue(newDescriptionOption); var tags = parseResult.GetValue(newTagsOption) ?? Array.Empty(); var shadow = parseResult.GetValue(newShadowOption); var fixtures = parseResult.GetValue(newFixturesOption); var gitInit = parseResult.GetValue(newGitInitOption); var format = parseResult.GetValue(newFormatOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyNewAsync( name, template, output, description, tags, shadow, fixtures, gitInit, format, verbose, cancellationToken); }); policy.Add(newCmd); // CLI-POLICY-23-006: policy history - view policy run history var history = new Command("history", "View policy run history."); var historyPolicyIdArgument = new Argument("policy-id") { Description = "Policy identifier (e.g. P-7)." }; var historyTenantOption = new Option("--tenant") { Description = "Filter by tenant." }; var historyFromOption = new Option("--from") { Description = "Filter runs from this timestamp (ISO-8601)." }; var historyToOption = new Option("--to") { Description = "Filter runs to this timestamp (ISO-8601)." }; var historyStatusOption = new Option("--status") { Description = "Filter by run status (completed, failed, running)." }; var historyLimitOption = new Option("--limit", new[] { "-l" }) { Description = "Maximum number of runs to return." }; var historyCursorOption = new Option("--cursor") { Description = "Pagination cursor for next page." }; var historyFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; history.Add(historyPolicyIdArgument); history.Add(historyTenantOption); history.Add(historyFromOption); history.Add(historyToOption); history.Add(historyStatusOption); history.Add(historyLimitOption); history.Add(historyCursorOption); history.Add(historyFormatOption); history.Add(verboseOption); history.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(historyPolicyIdArgument) ?? string.Empty; var tenant = parseResult.GetValue(historyTenantOption); var from = parseResult.GetValue(historyFromOption); var to = parseResult.GetValue(historyToOption); var status = parseResult.GetValue(historyStatusOption); var limit = parseResult.GetValue(historyLimitOption); var cursor = parseResult.GetValue(historyCursorOption); var format = parseResult.GetValue(historyFormatOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyHistoryAsync( services, policyId, tenant, from, to, status, limit, cursor, format, verbose, cancellationToken); }); policy.Add(history); // CLI-POLICY-23-006: policy explain - show explanation tree for a decision var explain = new Command("explain", "Show explanation tree for a policy decision."); var explainPolicyIdOption = new Option("--policy", new[] { "-p" }) { Description = "Policy identifier (e.g. P-7).", Required = true }; var explainRunIdOption = new Option("--run-id") { Description = "Specific run ID to explain from." }; var explainFindingIdOption = new Option("--finding-id") { Description = "Finding ID to explain." }; var explainSbomIdOption = new Option("--sbom-id") { Description = "SBOM ID for context." }; var explainPurlOption = new Option("--purl") { Description = "Component PURL to explain." }; var explainAdvisoryOption = new Option("--advisory") { Description = "Advisory ID to explain." }; var explainTenantOption = new Option("--tenant") { Description = "Tenant context." }; var explainDepthOption = new Option("--depth") { Description = "Maximum depth of explanation tree." }; var explainFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; explain.Add(explainPolicyIdOption); explain.Add(explainRunIdOption); explain.Add(explainFindingIdOption); explain.Add(explainSbomIdOption); explain.Add(explainPurlOption); explain.Add(explainAdvisoryOption); explain.Add(explainTenantOption); explain.Add(explainDepthOption); explain.Add(explainFormatOption); explain.Add(verboseOption); explain.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(explainPolicyIdOption) ?? string.Empty; var runId = parseResult.GetValue(explainRunIdOption); var findingId = parseResult.GetValue(explainFindingIdOption); var sbomId = parseResult.GetValue(explainSbomIdOption); var purl = parseResult.GetValue(explainPurlOption); var advisory = parseResult.GetValue(explainAdvisoryOption); var tenant = parseResult.GetValue(explainTenantOption); var depth = parseResult.GetValue(explainDepthOption); var format = parseResult.GetValue(explainFormatOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyExplainTreeAsync( services, policyId, runId, findingId, sbomId, purl, advisory, tenant, depth, format, verbose, cancellationToken); }); policy.Add(explain); // CLI-POLICY-27-001: policy init - initialize policy workspace var init = new Command("init", "Initialize a policy workspace directory."); var initPathArgument = new Argument("path") { Description = "Directory path for the workspace (defaults to current directory).", Arity = ArgumentArity.ZeroOrOne }; var initNameOption = new Option("--name", new[] { "-n" }) { Description = "Policy name (defaults to directory name)." }; var initTemplateOption = new Option("--template", new[] { "-t" }) { Description = "Template to use: minimal (default), baseline, vex-precedence, reachability, secret-leak, full." }; var initNoGitOption = new Option("--no-git") { Description = "Skip Git repository initialization." }; var initNoReadmeOption = new Option("--no-readme") { Description = "Skip README.md creation." }; var initNoFixturesOption = new Option("--no-fixtures") { Description = "Skip test fixtures directory creation." }; var initFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; init.Add(initPathArgument); init.Add(initNameOption); init.Add(initTemplateOption); init.Add(initNoGitOption); init.Add(initNoReadmeOption); init.Add(initNoFixturesOption); init.Add(initFormatOption); init.Add(verboseOption); init.SetAction((parseResult, _) => { var path = parseResult.GetValue(initPathArgument); var name = parseResult.GetValue(initNameOption); var template = parseResult.GetValue(initTemplateOption); var noGit = parseResult.GetValue(initNoGitOption); var noReadme = parseResult.GetValue(initNoReadmeOption); var noFixtures = parseResult.GetValue(initNoFixturesOption); var format = parseResult.GetValue(initFormatOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyInitAsync( path, name, template, noGit, noReadme, noFixtures, format, verbose, cancellationToken); }); policy.Add(init); // CLI-POLICY-27-001: policy compile - compile DSL to IR var compile = new Command("compile", "Compile a policy DSL file to IR."); var compileFileArgument = new Argument("file") { Description = "Path to the policy DSL file to compile." }; var compileOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output path for the compiled IR file." }; var compileNoIrOption = new Option("--no-ir") { Description = "Skip IR file generation (validation only)." }; var compileNoDigestOption = new Option("--no-digest") { Description = "Skip SHA-256 digest output." }; var compileOptimizeOption = new Option("--optimize") { Description = "Enable optimization passes on the IR." }; var compileStrictOption = new Option("--strict") { Description = "Treat warnings as errors." }; var compileFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; compile.Add(compileFileArgument); compile.Add(compileOutputOption); compile.Add(compileNoIrOption); compile.Add(compileNoDigestOption); compile.Add(compileOptimizeOption); compile.Add(compileStrictOption); compile.Add(compileFormatOption); compile.Add(verboseOption); compile.SetAction((parseResult, _) => { var file = parseResult.GetValue(compileFileArgument) ?? string.Empty; var output = parseResult.GetValue(compileOutputOption); var noIr = parseResult.GetValue(compileNoIrOption); var noDigest = parseResult.GetValue(compileNoDigestOption); var optimize = parseResult.GetValue(compileOptimizeOption); var strict = parseResult.GetValue(compileStrictOption); var format = parseResult.GetValue(compileFormatOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyCompileAsync( file, output, noIr, noDigest, optimize, strict, format, verbose, cancellationToken); }); policy.Add(compile); // CLI-POLICY-27-002: policy version bump var versionCmd = new Command("version", "Manage policy versions."); var versionBump = new Command("bump", "Bump the policy version (patch, minor, major)."); var bumpPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier (e.g. P-7)." }; var bumpTypeOption = new Option("--type", new[] { "-t" }) { Description = "Bump type: patch (default), minor, major." }; var bumpChangelogOption = new Option("--changelog", new[] { "-m" }) { Description = "Changelog message for this version." }; var bumpFileOption = new Option("--file", new[] { "-f" }) { Description = "Path to policy DSL file to upload." }; var bumpTenantOption = new Option("--tenant") { Description = "Tenant context." }; var bumpJsonOption = new Option("--json") { Description = "Output as JSON." }; versionBump.Add(bumpPolicyIdArg); versionBump.Add(bumpTypeOption); versionBump.Add(bumpChangelogOption); versionBump.Add(bumpFileOption); versionBump.Add(bumpTenantOption); versionBump.Add(bumpJsonOption); versionBump.Add(verboseOption); versionBump.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(bumpPolicyIdArg) ?? string.Empty; var bumpType = parseResult.GetValue(bumpTypeOption); var changelog = parseResult.GetValue(bumpChangelogOption); var filePath = parseResult.GetValue(bumpFileOption); var tenant = parseResult.GetValue(bumpTenantOption); var json = parseResult.GetValue(bumpJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyVersionBumpAsync( services, policyId, bumpType, changelog, filePath, tenant, json, verbose, cancellationToken); }); versionCmd.Add(versionBump); policy.Add(versionCmd); // CLI-POLICY-27-002: policy submit var submit = new Command("submit", "Submit policy for review."); var submitPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier (e.g. P-7)." }; var submitVersionOption = new Option("--version", new[] { "-v" }) { Description = "Specific version to submit (defaults to latest)." }; var submitReviewersOption = new Option("--reviewer", new[] { "-r" }) { Description = "Reviewer username(s) (repeatable).", Arity = ArgumentArity.ZeroOrMore }; submitReviewersOption.AllowMultipleArgumentsPerToken = true; var submitMessageOption = new Option("--message", new[] { "-m" }) { Description = "Submission message." }; var submitUrgentOption = new Option("--urgent") { Description = "Mark submission as urgent." }; var submitTenantOption = new Option("--tenant") { Description = "Tenant context." }; var submitJsonOption = new Option("--json") { Description = "Output as JSON." }; submit.Add(submitPolicyIdArg); submit.Add(submitVersionOption); submit.Add(submitReviewersOption); submit.Add(submitMessageOption); submit.Add(submitUrgentOption); submit.Add(submitTenantOption); submit.Add(submitJsonOption); submit.Add(verboseOption); submit.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(submitPolicyIdArg) ?? string.Empty; var version = parseResult.GetValue(submitVersionOption); var reviewers = parseResult.GetValue(submitReviewersOption) ?? Array.Empty(); var message = parseResult.GetValue(submitMessageOption); var urgent = parseResult.GetValue(submitUrgentOption); var tenant = parseResult.GetValue(submitTenantOption); var json = parseResult.GetValue(submitJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicySubmitAsync( services, policyId, version, reviewers, message, urgent, tenant, json, verbose, cancellationToken); }); policy.Add(submit); // CLI-POLICY-27-002: policy review command group var review = new Command("review", "Manage policy reviews."); // review status var reviewStatus = new Command("status", "Get current review status."); var reviewStatusPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var reviewStatusIdOption = new Option("--review-id") { Description = "Specific review ID (defaults to latest)." }; var reviewStatusTenantOption = new Option("--tenant") { Description = "Tenant context." }; var reviewStatusJsonOption = new Option("--json") { Description = "Output as JSON." }; reviewStatus.Add(reviewStatusPolicyIdArg); reviewStatus.Add(reviewStatusIdOption); reviewStatus.Add(reviewStatusTenantOption); reviewStatus.Add(reviewStatusJsonOption); reviewStatus.Add(verboseOption); reviewStatus.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(reviewStatusPolicyIdArg) ?? string.Empty; var reviewId = parseResult.GetValue(reviewStatusIdOption); var tenant = parseResult.GetValue(reviewStatusTenantOption); var json = parseResult.GetValue(reviewStatusJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyReviewStatusAsync( services, policyId, reviewId, tenant, json, verbose, cancellationToken); }); review.Add(reviewStatus); // review comment var reviewComment = new Command("comment", "Add a review comment."); var commentPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var commentReviewIdOption = new Option("--review-id") { Description = "Review ID to comment on.", Required = true }; var commentTextOption = new Option("--comment", new[] { "-c" }) { Description = "Comment text.", Required = true }; var commentLineOption = new Option("--line") { Description = "Line number in the policy file." }; var commentRuleOption = new Option("--rule") { Description = "Rule name reference." }; var commentBlockingOption = new Option("--blocking") { Description = "Mark comment as blocking (must be addressed before approval)." }; var commentTenantOption = new Option("--tenant") { Description = "Tenant context." }; var commentJsonOption = new Option("--json") { Description = "Output as JSON." }; reviewComment.Add(commentPolicyIdArg); reviewComment.Add(commentReviewIdOption); reviewComment.Add(commentTextOption); reviewComment.Add(commentLineOption); reviewComment.Add(commentRuleOption); reviewComment.Add(commentBlockingOption); reviewComment.Add(commentTenantOption); reviewComment.Add(commentJsonOption); reviewComment.Add(verboseOption); reviewComment.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(commentPolicyIdArg) ?? string.Empty; var reviewId = parseResult.GetValue(commentReviewIdOption) ?? string.Empty; var comment = parseResult.GetValue(commentTextOption) ?? string.Empty; var line = parseResult.GetValue(commentLineOption); var rule = parseResult.GetValue(commentRuleOption); var blocking = parseResult.GetValue(commentBlockingOption); var tenant = parseResult.GetValue(commentTenantOption); var json = parseResult.GetValue(commentJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyReviewCommentAsync( services, policyId, reviewId, comment, line, rule, blocking, tenant, json, verbose, cancellationToken); }); review.Add(reviewComment); // review approve var reviewApprove = new Command("approve", "Approve a policy review."); var approvePolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var approveReviewIdOption = new Option("--review-id") { Description = "Review ID to approve.", Required = true }; var approveCommentOption = new Option("--comment", new[] { "-c" }) { Description = "Approval comment." }; var approveTenantOption = new Option("--tenant") { Description = "Tenant context." }; var approveJsonOption = new Option("--json") { Description = "Output as JSON." }; reviewApprove.Add(approvePolicyIdArg); reviewApprove.Add(approveReviewIdOption); reviewApprove.Add(approveCommentOption); reviewApprove.Add(approveTenantOption); reviewApprove.Add(approveJsonOption); reviewApprove.Add(verboseOption); reviewApprove.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(approvePolicyIdArg) ?? string.Empty; var reviewId = parseResult.GetValue(approveReviewIdOption) ?? string.Empty; var comment = parseResult.GetValue(approveCommentOption); var tenant = parseResult.GetValue(approveTenantOption); var json = parseResult.GetValue(approveJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyReviewApproveAsync( services, policyId, reviewId, comment, tenant, json, verbose, cancellationToken); }); review.Add(reviewApprove); // review reject var reviewReject = new Command("reject", "Reject a policy review."); var rejectPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var rejectReviewIdOption = new Option("--review-id") { Description = "Review ID to reject.", Required = true }; var rejectReasonOption = new Option("--reason", new[] { "-r" }) { Description = "Rejection reason.", Required = true }; var rejectTenantOption = new Option("--tenant") { Description = "Tenant context." }; var rejectJsonOption = new Option("--json") { Description = "Output as JSON." }; reviewReject.Add(rejectPolicyIdArg); reviewReject.Add(rejectReviewIdOption); reviewReject.Add(rejectReasonOption); reviewReject.Add(rejectTenantOption); reviewReject.Add(rejectJsonOption); reviewReject.Add(verboseOption); reviewReject.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(rejectPolicyIdArg) ?? string.Empty; var reviewId = parseResult.GetValue(rejectReviewIdOption) ?? string.Empty; var reason = parseResult.GetValue(rejectReasonOption) ?? string.Empty; var tenant = parseResult.GetValue(rejectTenantOption); var json = parseResult.GetValue(rejectJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyReviewRejectAsync( services, policyId, reviewId, reason, tenant, json, verbose, cancellationToken); }); review.Add(reviewReject); policy.Add(review); // CLI-POLICY-27-004: publish command var publish = new Command("publish", "Publish an approved policy revision."); var publishPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var publishVersionOption = new Option("--version") { Description = "Version to publish.", Required = true }; var publishSignOption = new Option("--sign") { Description = "Sign the policy during publish." }; var publishAlgorithmOption = new Option("--algorithm") { Description = "Signature algorithm (e.g. ecdsa-sha256, ed25519)." }; var publishKeyIdOption = new Option("--key-id") { Description = "Key identifier for signing." }; var publishNoteOption = new Option("--note") { Description = "Publish note." }; var publishTenantOption = new Option("--tenant") { Description = "Tenant context." }; var publishJsonOption = new Option("--json") { Description = "Output as JSON." }; publish.Add(publishPolicyIdArg); publish.Add(publishVersionOption); publish.Add(publishSignOption); publish.Add(publishAlgorithmOption); publish.Add(publishKeyIdOption); publish.Add(publishNoteOption); publish.Add(publishTenantOption); publish.Add(publishJsonOption); publish.Add(verboseOption); publish.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(publishPolicyIdArg) ?? string.Empty; var version = parseResult.GetValue(publishVersionOption); var sign = parseResult.GetValue(publishSignOption); var algorithm = parseResult.GetValue(publishAlgorithmOption); var keyId = parseResult.GetValue(publishKeyIdOption); var note = parseResult.GetValue(publishNoteOption); var tenant = parseResult.GetValue(publishTenantOption); var json = parseResult.GetValue(publishJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyPublishAsync( services, policyId, version, sign, algorithm, keyId, note, tenant, json, verbose, cancellationToken); }); policy.Add(publish); // CLI-POLICY-27-004: promote command var promote = new Command("promote", "Promote a policy to a target environment."); var promotePolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var promoteVersionOption = new Option("--version") { Description = "Version to promote.", Required = true }; var promoteEnvOption = new Option("--env") { Description = "Target environment (e.g. staging, production).", Required = true }; var promoteCanaryOption = new Option("--canary") { Description = "Enable canary deployment." }; var promoteCanaryPercentOption = new Option("--canary-percent") { Description = "Canary traffic percentage (1-99)." }; var promoteNoteOption = new Option("--note") { Description = "Promotion note." }; var promoteTenantOption = new Option("--tenant") { Description = "Tenant context." }; var promoteJsonOption = new Option("--json") { Description = "Output as JSON." }; promote.Add(promotePolicyIdArg); promote.Add(promoteVersionOption); promote.Add(promoteEnvOption); promote.Add(promoteCanaryOption); promote.Add(promoteCanaryPercentOption); promote.Add(promoteNoteOption); promote.Add(promoteTenantOption); promote.Add(promoteJsonOption); promote.Add(verboseOption); promote.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(promotePolicyIdArg) ?? string.Empty; var version = parseResult.GetValue(promoteVersionOption); var env = parseResult.GetValue(promoteEnvOption) ?? string.Empty; var canary = parseResult.GetValue(promoteCanaryOption); var canaryPercent = parseResult.GetValue(promoteCanaryPercentOption); var note = parseResult.GetValue(promoteNoteOption); var tenant = parseResult.GetValue(promoteTenantOption); var json = parseResult.GetValue(promoteJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyPromoteAsync( services, policyId, version, env, canary, canaryPercent, note, tenant, json, verbose, cancellationToken); }); policy.Add(promote); // CLI-POLICY-27-004: rollback command var rollback = new Command("rollback", "Rollback a policy to a previous version."); var rollbackPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var rollbackTargetVersionOption = new Option("--target-version") { Description = "Target version to rollback to. Defaults to previous version." }; var rollbackEnvOption = new Option("--env") { Description = "Environment scope for rollback." }; var rollbackReasonOption = new Option("--reason") { Description = "Reason for rollback." }; var rollbackIncidentOption = new Option("--incident") { Description = "Associated incident ID." }; var rollbackTenantOption = new Option("--tenant") { Description = "Tenant context." }; var rollbackJsonOption = new Option("--json") { Description = "Output as JSON." }; rollback.Add(rollbackPolicyIdArg); rollback.Add(rollbackTargetVersionOption); rollback.Add(rollbackEnvOption); rollback.Add(rollbackReasonOption); rollback.Add(rollbackIncidentOption); rollback.Add(rollbackTenantOption); rollback.Add(rollbackJsonOption); rollback.Add(verboseOption); rollback.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(rollbackPolicyIdArg) ?? string.Empty; var targetVersion = parseResult.GetValue(rollbackTargetVersionOption); var env = parseResult.GetValue(rollbackEnvOption); var reason = parseResult.GetValue(rollbackReasonOption); var incident = parseResult.GetValue(rollbackIncidentOption); var tenant = parseResult.GetValue(rollbackTenantOption); var json = parseResult.GetValue(rollbackJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyRollbackAsync( services, policyId, targetVersion, env, reason, incident, tenant, json, verbose, cancellationToken); }); policy.Add(rollback); // CLI-POLICY-27-004: sign command var sign = new Command("sign", "Sign a policy revision."); var signPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var signVersionOption = new Option("--version") { Description = "Version to sign.", Required = true }; var signKeyIdOption = new Option("--key-id") { Description = "Key identifier for signing." }; var signAlgorithmOption = new Option("--algorithm") { Description = "Signature algorithm (e.g. ecdsa-sha256, ed25519)." }; var signRekorOption = new Option("--rekor") { Description = "Upload signature to Sigstore Rekor transparency log." }; var signTenantOption = new Option("--tenant") { Description = "Tenant context." }; var signJsonOption = new Option("--json") { Description = "Output as JSON." }; sign.Add(signPolicyIdArg); sign.Add(signVersionOption); sign.Add(signKeyIdOption); sign.Add(signAlgorithmOption); sign.Add(signRekorOption); sign.Add(signTenantOption); sign.Add(signJsonOption); sign.Add(verboseOption); sign.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(signPolicyIdArg) ?? string.Empty; var version = parseResult.GetValue(signVersionOption); var keyId = parseResult.GetValue(signKeyIdOption); var algorithm = parseResult.GetValue(signAlgorithmOption); var rekor = parseResult.GetValue(signRekorOption); var tenant = parseResult.GetValue(signTenantOption); var json = parseResult.GetValue(signJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicySignAsync( services, policyId, version, keyId, algorithm, rekor, tenant, json, verbose, cancellationToken); }); policy.Add(sign); // CLI-POLICY-27-004: verify-signature command var verifySignature = new Command("verify-signature", "Verify a policy signature."); var verifyPolicyIdArg = new Argument("policy-id") { Description = "Policy identifier." }; var verifyVersionOption = new Option("--version") { Description = "Version to verify.", Required = true }; var verifySignatureIdOption = new Option("--signature-id") { Description = "Signature ID to verify. Defaults to latest." }; var verifyCheckRekorOption = new Option("--check-rekor") { Description = "Verify against Sigstore Rekor transparency log." }; var verifyTenantOption = new Option("--tenant") { Description = "Tenant context." }; var verifyJsonOption = new Option("--json") { Description = "Output as JSON." }; verifySignature.Add(verifyPolicyIdArg); verifySignature.Add(verifyVersionOption); verifySignature.Add(verifySignatureIdOption); verifySignature.Add(verifyCheckRekorOption); verifySignature.Add(verifyTenantOption); verifySignature.Add(verifyJsonOption); verifySignature.Add(verboseOption); verifySignature.SetAction((parseResult, _) => { var policyId = parseResult.GetValue(verifyPolicyIdArg) ?? string.Empty; var version = parseResult.GetValue(verifyVersionOption); var signatureId = parseResult.GetValue(verifySignatureIdOption); var checkRekor = parseResult.GetValue(verifyCheckRekorOption); var tenant = parseResult.GetValue(verifyTenantOption); var json = parseResult.GetValue(verifyJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyVerifySignatureAsync( services, policyId, version, signatureId, checkRekor, tenant, json, verbose, cancellationToken); }); policy.Add(verifySignature); return policy; } private static Command BuildTaskRunnerCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var taskRunner = new Command("task-runner", "Interact with Task Runner operations."); var simulate = new Command("simulate", "Simulate a task pack and inspect the execution graph."); var manifestOption = new Option("--manifest") { Description = "Path to the task pack manifest (YAML).", Arity = ArgumentArity.ExactlyOne }; var inputsOption = new Option("--inputs") { Description = "Optional JSON file containing Task Pack input values." }; var formatOption = new Option("--format") { Description = "Output format: table or json." }; var outputOption = new Option("--output") { Description = "Write JSON payload to the specified file." }; simulate.Add(manifestOption); simulate.Add(inputsOption); simulate.Add(formatOption); simulate.Add(outputOption); simulate.SetAction((parseResult, _) => { var manifestPath = parseResult.GetValue(manifestOption) ?? string.Empty; var inputsPath = parseResult.GetValue(inputsOption); var selectedFormat = parseResult.GetValue(formatOption); var output = parseResult.GetValue(outputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleTaskRunnerSimulateAsync( services, manifestPath, inputsPath, selectedFormat, output, verbose, cancellationToken); }); taskRunner.Add(simulate); return taskRunner; } private static Command BuildFindingsCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var findings = new Command("findings", "Inspect policy findings."); var list = new Command("ls", "List effective findings that match the provided filters."); var policyOption = new Option("--policy") { Description = "Policy identifier (e.g. P-7).", Required = true }; var sbomOption = new Option("--sbom") { Description = "Filter by SBOM identifier (repeatable).", Arity = ArgumentArity.ZeroOrMore }; sbomOption.AllowMultipleArgumentsPerToken = true; var statusOption = new Option("--status") { Description = "Filter by finding status (repeatable).", Arity = ArgumentArity.ZeroOrMore }; statusOption.AllowMultipleArgumentsPerToken = true; var severityOption = new Option("--severity") { Description = "Filter by severity label (repeatable).", Arity = ArgumentArity.ZeroOrMore }; severityOption.AllowMultipleArgumentsPerToken = true; var sinceOption = new Option("--since") { Description = "Filter by last-updated timestamp (ISO-8601)." }; var cursorOption = new Option("--cursor") { Description = "Resume listing from the provided cursor." }; var pageOption = new Option("--page") { Description = "Page number (starts at 1)." }; var pageSizeOption = new Option("--page-size") { Description = "Results per page (default backend limit applies)." }; var formatOption = new Option("--format") { Description = "Output format: table or json." }; var outputOption = new Option("--output") { Description = "Write JSON payload to the specified file." }; list.Add(policyOption); list.Add(sbomOption); list.Add(statusOption); list.Add(severityOption); list.Add(sinceOption); list.Add(cursorOption); list.Add(pageOption); list.Add(pageSizeOption); list.Add(formatOption); list.Add(outputOption); list.SetAction((parseResult, _) => { var policy = parseResult.GetValue(policyOption) ?? string.Empty; var sboms = parseResult.GetValue(sbomOption) ?? Array.Empty(); var statuses = parseResult.GetValue(statusOption) ?? Array.Empty(); var severities = parseResult.GetValue(severityOption) ?? Array.Empty(); var since = parseResult.GetValue(sinceOption); var cursor = parseResult.GetValue(cursorOption); var page = parseResult.GetValue(pageOption); var pageSize = parseResult.GetValue(pageSizeOption); var selectedFormat = parseResult.GetValue(formatOption); var output = parseResult.GetValue(outputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyFindingsListAsync( services, policy, sboms, statuses, severities, since, cursor, page, pageSize, selectedFormat, output, verbose, cancellationToken); }); var get = new Command("get", "Retrieve a specific finding."); var findingArgument = new Argument("finding-id") { Description = "Finding identifier (e.g. P-7:S-42:pkg:...)." }; var getPolicyOption = new Option("--policy") { Description = "Policy identifier for the finding.", Required = true }; var getFormatOption = new Option("--format") { Description = "Output format: table or json." }; var getOutputOption = new Option("--output") { Description = "Write JSON payload to the specified file." }; get.Add(findingArgument); get.Add(getPolicyOption); get.Add(getFormatOption); get.Add(getOutputOption); get.SetAction((parseResult, _) => { var policy = parseResult.GetValue(getPolicyOption) ?? string.Empty; var finding = parseResult.GetValue(findingArgument) ?? string.Empty; var selectedFormat = parseResult.GetValue(getFormatOption); var output = parseResult.GetValue(getOutputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyFindingsGetAsync( services, policy, finding, selectedFormat, output, verbose, cancellationToken); }); var explain = new Command("explain", "Fetch explain trace for a finding."); var explainFindingArgument = new Argument("finding-id") { Description = "Finding identifier." }; var explainPolicyOption = new Option("--policy") { Description = "Policy identifier.", Required = true }; var modeOption = new Option("--mode") { Description = "Explain mode (for example: verbose)." }; var explainFormatOption = new Option("--format") { Description = "Output format: table or json." }; var explainOutputOption = new Option("--output") { Description = "Write JSON payload to the specified file." }; explain.Add(explainFindingArgument); explain.Add(explainPolicyOption); explain.Add(modeOption); explain.Add(explainFormatOption); explain.Add(explainOutputOption); explain.SetAction((parseResult, _) => { var policy = parseResult.GetValue(explainPolicyOption) ?? string.Empty; var finding = parseResult.GetValue(explainFindingArgument) ?? string.Empty; var mode = parseResult.GetValue(modeOption); var selectedFormat = parseResult.GetValue(explainFormatOption); var output = parseResult.GetValue(explainOutputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicyFindingsExplainAsync( services, policy, finding, mode, selectedFormat, output, verbose, cancellationToken); }); findings.Add(list); findings.Add(get); findings.Add(explain); return findings; } private static Command BuildAdviseCommand(IServiceProvider services, StellaOpsCliOptions options, Option verboseOption, CancellationToken cancellationToken) { var advise = new Command("advise", "Interact with Advisory AI pipelines."); _ = options; var runOptions = CreateAdvisoryOptions(); var runTaskArgument = new Argument("task") { Description = "Task to run (summary, conflict, remediation)." }; var run = new Command("run", "Generate Advisory AI output for the specified task."); run.Add(runTaskArgument); AddAdvisoryOptions(run, runOptions); run.SetAction((parseResult, _) => { var taskValue = parseResult.GetValue(runTaskArgument); var advisoryKey = parseResult.GetValue(runOptions.AdvisoryKey) ?? string.Empty; var artifactId = parseResult.GetValue(runOptions.ArtifactId); var artifactPurl = parseResult.GetValue(runOptions.ArtifactPurl); var policyVersion = parseResult.GetValue(runOptions.PolicyVersion); var profile = parseResult.GetValue(runOptions.Profile) ?? "default"; var sections = parseResult.GetValue(runOptions.Sections) ?? Array.Empty(); var forceRefresh = parseResult.GetValue(runOptions.ForceRefresh); var timeoutSeconds = parseResult.GetValue(runOptions.TimeoutSeconds) ?? 120; var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(runOptions.Format)); var outputPath = parseResult.GetValue(runOptions.Output); var verbose = parseResult.GetValue(verboseOption); if (!Enum.TryParse(taskValue, ignoreCase: true, out var taskType)) { throw new InvalidOperationException($"Unknown advisory task '{taskValue}'. Expected summary, conflict, or remediation."); } return CommandHandlers.HandleAdviseRunAsync( services, taskType, advisoryKey, artifactId, artifactPurl, policyVersion, profile, sections, forceRefresh, timeoutSeconds, outputFormat, outputPath, verbose, cancellationToken); }); var summarizeOptions = CreateAdvisoryOptions(); var summarize = new Command("summarize", "Summarize an advisory with JSON/Markdown outputs and citations."); AddAdvisoryOptions(summarize, summarizeOptions); summarize.SetAction((parseResult, _) => { var advisoryKey = parseResult.GetValue(summarizeOptions.AdvisoryKey) ?? string.Empty; var artifactId = parseResult.GetValue(summarizeOptions.ArtifactId); var artifactPurl = parseResult.GetValue(summarizeOptions.ArtifactPurl); var policyVersion = parseResult.GetValue(summarizeOptions.PolicyVersion); var profile = parseResult.GetValue(summarizeOptions.Profile) ?? "default"; var sections = parseResult.GetValue(summarizeOptions.Sections) ?? Array.Empty(); var forceRefresh = parseResult.GetValue(summarizeOptions.ForceRefresh); var timeoutSeconds = parseResult.GetValue(summarizeOptions.TimeoutSeconds) ?? 120; var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(summarizeOptions.Format)); var outputPath = parseResult.GetValue(summarizeOptions.Output); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAdviseRunAsync( services, AdvisoryAiTaskType.Summary, advisoryKey, artifactId, artifactPurl, policyVersion, profile, sections, forceRefresh, timeoutSeconds, outputFormat, outputPath, verbose, cancellationToken); }); var explainOptions = CreateAdvisoryOptions(); var explain = new Command("explain", "Explain an advisory conflict set with narrative and rationale."); AddAdvisoryOptions(explain, explainOptions); explain.SetAction((parseResult, _) => { var advisoryKey = parseResult.GetValue(explainOptions.AdvisoryKey) ?? string.Empty; var artifactId = parseResult.GetValue(explainOptions.ArtifactId); var artifactPurl = parseResult.GetValue(explainOptions.ArtifactPurl); var policyVersion = parseResult.GetValue(explainOptions.PolicyVersion); var profile = parseResult.GetValue(explainOptions.Profile) ?? "default"; var sections = parseResult.GetValue(explainOptions.Sections) ?? Array.Empty(); var forceRefresh = parseResult.GetValue(explainOptions.ForceRefresh); var timeoutSeconds = parseResult.GetValue(explainOptions.TimeoutSeconds) ?? 120; var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(explainOptions.Format)); var outputPath = parseResult.GetValue(explainOptions.Output); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAdviseRunAsync( services, AdvisoryAiTaskType.Conflict, advisoryKey, artifactId, artifactPurl, policyVersion, profile, sections, forceRefresh, timeoutSeconds, outputFormat, outputPath, verbose, cancellationToken); }); var remediateOptions = CreateAdvisoryOptions(); var remediate = new Command("remediate", "Generate remediation guidance for an advisory."); AddAdvisoryOptions(remediate, remediateOptions); remediate.SetAction((parseResult, _) => { var advisoryKey = parseResult.GetValue(remediateOptions.AdvisoryKey) ?? string.Empty; var artifactId = parseResult.GetValue(remediateOptions.ArtifactId); var artifactPurl = parseResult.GetValue(remediateOptions.ArtifactPurl); var policyVersion = parseResult.GetValue(remediateOptions.PolicyVersion); var profile = parseResult.GetValue(remediateOptions.Profile) ?? "default"; var sections = parseResult.GetValue(remediateOptions.Sections) ?? Array.Empty(); var forceRefresh = parseResult.GetValue(remediateOptions.ForceRefresh); var timeoutSeconds = parseResult.GetValue(remediateOptions.TimeoutSeconds) ?? 120; var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(remediateOptions.Format)); var outputPath = parseResult.GetValue(remediateOptions.Output); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAdviseRunAsync( services, AdvisoryAiTaskType.Remediation, advisoryKey, artifactId, artifactPurl, policyVersion, profile, sections, forceRefresh, timeoutSeconds, outputFormat, outputPath, verbose, cancellationToken); }); var batchOptions = CreateAdvisoryOptions(); var batchKeys = new Argument("advisory-keys") { Description = "One or more advisory identifiers.", Arity = ArgumentArity.OneOrMore }; var batch = new Command("batch", "Run Advisory AI over multiple advisories with a single invocation."); batch.Add(batchKeys); batch.Add(batchOptions.Output); batch.Add(batchOptions.AdvisoryKey); batch.Add(batchOptions.ArtifactId); batch.Add(batchOptions.ArtifactPurl); batch.Add(batchOptions.PolicyVersion); batch.Add(batchOptions.Profile); batch.Add(batchOptions.Sections); batch.Add(batchOptions.ForceRefresh); batch.Add(batchOptions.TimeoutSeconds); batch.Add(batchOptions.Format); batch.SetAction((parseResult, _) => { var advisoryKeys = parseResult.GetValue(batchKeys) ?? Array.Empty(); var artifactId = parseResult.GetValue(batchOptions.ArtifactId); var artifactPurl = parseResult.GetValue(batchOptions.ArtifactPurl); var policyVersion = parseResult.GetValue(batchOptions.PolicyVersion); var profile = parseResult.GetValue(batchOptions.Profile) ?? "default"; var sections = parseResult.GetValue(batchOptions.Sections) ?? Array.Empty(); var forceRefresh = parseResult.GetValue(batchOptions.ForceRefresh); var timeoutSeconds = parseResult.GetValue(batchOptions.TimeoutSeconds) ?? 120; var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(batchOptions.Format)); var outputDirectory = parseResult.GetValue(batchOptions.Output); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAdviseBatchAsync( services, AdvisoryAiTaskType.Summary, advisoryKeys, artifactId, artifactPurl, policyVersion, profile, sections, forceRefresh, timeoutSeconds, outputFormat, outputDirectory, verbose, cancellationToken); }); advise.Add(run); advise.Add(summarize); advise.Add(explain); advise.Add(remediate); advise.Add(batch); return advise; } private static AdvisoryCommandOptions CreateAdvisoryOptions() { var advisoryKey = new Option("--advisory-key") { Description = "Advisory identifier to summarise (required).", Required = true }; var artifactId = new Option("--artifact-id") { Description = "Optional artifact identifier to scope SBOM context." }; var artifactPurl = new Option("--artifact-purl") { Description = "Optional package URL to scope dependency context." }; var policyVersion = new Option("--policy-version") { Description = "Policy revision to evaluate (defaults to current)." }; var profile = new Option("--profile") { Description = "Advisory AI execution profile (default, fips-local, etc.)." }; var sections = new Option("--section") { Description = "Preferred context sections to emphasise (repeatable).", Arity = ArgumentArity.ZeroOrMore }; sections.AllowMultipleArgumentsPerToken = true; var forceRefresh = new Option("--force-refresh") { Description = "Bypass cached plan/output and recompute." }; var timeoutSeconds = new Option("--timeout") { Description = "Seconds to wait for generated output before timing out (0 = single attempt)." }; timeoutSeconds.Arity = ArgumentArity.ZeroOrOne; var format = new Option("--format") { Description = "Output format: table (default), json, or markdown." }; var output = new Option("--output") { Description = "File path to write advisory output when using json/markdown formats." }; return new AdvisoryCommandOptions( advisoryKey, artifactId, artifactPurl, policyVersion, profile, sections, forceRefresh, timeoutSeconds, format, output); } private static void AddAdvisoryOptions(Command command, AdvisoryCommandOptions options) { command.Add(options.AdvisoryKey); command.Add(options.ArtifactId); command.Add(options.ArtifactPurl); command.Add(options.PolicyVersion); command.Add(options.Profile); command.Add(options.Sections); command.Add(options.ForceRefresh); command.Add(options.TimeoutSeconds); command.Add(options.Format); command.Add(options.Output); } private static AdvisoryOutputFormat ParseAdvisoryOutputFormat(string? formatValue) { var normalized = string.IsNullOrWhiteSpace(formatValue) ? "table" : formatValue!.Trim().ToLowerInvariant(); return normalized switch { "json" => AdvisoryOutputFormat.Json, "markdown" => AdvisoryOutputFormat.Markdown, "md" => AdvisoryOutputFormat.Markdown, _ => AdvisoryOutputFormat.Table }; } private sealed record AdvisoryCommandOptions( Option AdvisoryKey, Option ArtifactId, Option ArtifactPurl, Option PolicyVersion, Option Profile, Option Sections, Option ForceRefresh, Option TimeoutSeconds, Option Format, Option Output); private static Command BuildVulnCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var vuln = new Command("vuln", "Explore vulnerability observations and overlays."); var observations = new Command("observations", "List raw advisory observations for overlay consumers."); var tenantOption = new Option("--tenant") { Description = "Tenant identifier.", Required = true }; var observationIdOption = new Option("--observation-id") { Description = "Filter by observation identifier (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var aliasOption = new Option("--alias") { Description = "Filter by vulnerability alias (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var purlOption = new Option("--purl") { Description = "Filter by Package URL (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var cpeOption = new Option("--cpe") { Description = "Filter by CPE value (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var jsonOption = new Option("--json") { Description = "Emit raw JSON payload instead of a table." }; var limitOption = new Option("--limit") { Description = "Maximum number of observations to return (default 200, max 500)." }; var cursorOption = new Option("--cursor") { Description = "Opaque cursor token returned by a previous page." }; observations.Add(tenantOption); observations.Add(observationIdOption); observations.Add(aliasOption); observations.Add(purlOption); observations.Add(cpeOption); observations.Add(limitOption); observations.Add(cursorOption); observations.Add(jsonOption); observations.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption) ?? string.Empty; var observationIds = parseResult.GetValue(observationIdOption) ?? Array.Empty(); var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty(); var purls = parseResult.GetValue(purlOption) ?? Array.Empty(); var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty(); var limit = parseResult.GetValue(limitOption); var cursor = parseResult.GetValue(cursorOption); var emitJson = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVulnObservationsAsync( services, tenant, observationIds, aliases, purls, cpes, limit, cursor, emitJson, verbose, cancellationToken); }); vuln.Add(observations); // CLI-VULN-29-001: Vulnerability explorer list command var list = new Command("list", "List vulnerabilities with grouping, filters, and pagination."); var listVulnIdOption = new Option("--vuln-id") { Description = "Filter by vulnerability identifier (e.g., CVE-2024-1234)." }; var listSeverityOption = new Option("--severity") { Description = "Filter by severity level (critical, high, medium, low)." }; var listStatusOption = new Option("--status") { Description = "Filter by status (open, triaged, accepted, fixed, etc.)." }; var listPurlOption = new Option("--purl") { Description = "Filter by Package URL." }; var listCpeOption = new Option("--cpe") { Description = "Filter by CPE value." }; var listSbomIdOption = new Option("--sbom-id") { Description = "Filter by SBOM identifier." }; var listPolicyIdOption = new Option("--policy-id") { Description = "Filter by policy identifier." }; var listPolicyVersionOption = new Option("--policy-version") { Description = "Filter by policy version." }; var listGroupByOption = new Option("--group-by") { Description = "Group results by field (vuln, package, severity, status)." }; var listLimitOption = new Option("--limit") { Description = "Maximum number of items to return (default 50, max 500)." }; var listOffsetOption = new Option("--offset") { Description = "Number of items to skip for pagination." }; var listCursorOption = new Option("--cursor") { Description = "Opaque cursor token returned by a previous page." }; var listTenantOption = new Option("--tenant") { Description = "Tenant identifier (overrides profile/environment)." }; var listJsonOption = new Option("--json") { Description = "Emit raw JSON payload instead of a table." }; var listCsvOption = new Option("--csv") { Description = "Emit CSV format instead of a table." }; list.Add(listVulnIdOption); list.Add(listSeverityOption); list.Add(listStatusOption); list.Add(listPurlOption); list.Add(listCpeOption); list.Add(listSbomIdOption); list.Add(listPolicyIdOption); list.Add(listPolicyVersionOption); list.Add(listGroupByOption); list.Add(listLimitOption); list.Add(listOffsetOption); list.Add(listCursorOption); list.Add(listTenantOption); list.Add(listJsonOption); list.Add(listCsvOption); list.Add(verboseOption); list.SetAction((parseResult, _) => { var vulnId = parseResult.GetValue(listVulnIdOption); var severity = parseResult.GetValue(listSeverityOption); var status = parseResult.GetValue(listStatusOption); var purl = parseResult.GetValue(listPurlOption); var cpe = parseResult.GetValue(listCpeOption); var sbomId = parseResult.GetValue(listSbomIdOption); var policyId = parseResult.GetValue(listPolicyIdOption); var policyVersion = parseResult.GetValue(listPolicyVersionOption); var groupBy = parseResult.GetValue(listGroupByOption); var limit = parseResult.GetValue(listLimitOption); var offset = parseResult.GetValue(listOffsetOption); var cursor = parseResult.GetValue(listCursorOption); var tenant = parseResult.GetValue(listTenantOption); var emitJson = parseResult.GetValue(listJsonOption); var emitCsv = parseResult.GetValue(listCsvOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVulnListAsync( services, vulnId, severity, status, purl, cpe, sbomId, policyId, policyVersion, groupBy, limit, offset, cursor, tenant, emitJson, emitCsv, verbose, cancellationToken); }); vuln.Add(list); // CLI-VULN-29-002: Vulnerability show command var show = new Command("show", "Display detailed vulnerability information including evidence, rationale, paths, and ledger."); var showVulnIdArg = new Argument("vulnerability-id") { Description = "Vulnerability identifier (e.g., CVE-2024-1234)." }; var showTenantOption = new Option("--tenant") { Description = "Tenant identifier (overrides profile/environment)." }; var showJsonOption = new Option("--json") { Description = "Emit raw JSON payload instead of formatted output." }; show.Add(showVulnIdArg); show.Add(showTenantOption); show.Add(showJsonOption); show.Add(verboseOption); show.SetAction((parseResult, _) => { var vulnIdVal = parseResult.GetValue(showVulnIdArg) ?? string.Empty; var tenantVal = parseResult.GetValue(showTenantOption); var emitJson = parseResult.GetValue(showJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVulnShowAsync( services, vulnIdVal, tenantVal, emitJson, verbose, cancellationToken); }); vuln.Add(show); // CLI-VULN-29-003: Workflow commands // Common options for workflow commands var wfVulnIdsOption = new Option("--vuln-id") { Description = "Vulnerability IDs to operate on (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var wfFilterSeverityOption = new Option("--filter-severity") { Description = "Filter vulnerabilities by severity (critical, high, medium, low)." }; var wfFilterStatusOption = new Option("--filter-status") { Description = "Filter vulnerabilities by current status." }; var wfFilterPurlOption = new Option("--filter-purl") { Description = "Filter vulnerabilities by Package URL." }; var wfFilterSbomOption = new Option("--filter-sbom") { Description = "Filter vulnerabilities by SBOM ID." }; var wfTenantOption = new Option("--tenant") { Description = "Tenant identifier (overrides profile/environment)." }; var wfIdempotencyKeyOption = new Option("--idempotency-key") { Description = "Idempotency key for retry-safe operations." }; var wfJsonOption = new Option("--json") { Description = "Emit raw JSON response." }; // assign command var assign = new Command("assign", "Assign vulnerabilities to a user."); var assignAssigneeArg = new Argument("assignee") { Description = "Username or email to assign to." }; assign.Add(assignAssigneeArg); assign.Add(wfVulnIdsOption); assign.Add(wfFilterSeverityOption); assign.Add(wfFilterStatusOption); assign.Add(wfFilterPurlOption); assign.Add(wfFilterSbomOption); assign.Add(wfTenantOption); assign.Add(wfIdempotencyKeyOption); assign.Add(wfJsonOption); assign.Add(verboseOption); assign.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync( services, "assign", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty(), parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption), parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption), parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption), parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption), parseResult.GetValue(assignAssigneeArg), null, null, null, null, cancellationToken)); vuln.Add(assign); // comment command var comment = new Command("comment", "Add a comment to vulnerabilities."); var commentTextArg = new Argument("text") { Description = "Comment text to add." }; comment.Add(commentTextArg); comment.Add(wfVulnIdsOption); comment.Add(wfFilterSeverityOption); comment.Add(wfFilterStatusOption); comment.Add(wfFilterPurlOption); comment.Add(wfFilterSbomOption); comment.Add(wfTenantOption); comment.Add(wfIdempotencyKeyOption); comment.Add(wfJsonOption); comment.Add(verboseOption); comment.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync( services, "comment", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty(), parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption), parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption), parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption), parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption), null, parseResult.GetValue(commentTextArg), null, null, null, cancellationToken)); vuln.Add(comment); // accept-risk command var acceptRisk = new Command("accept-risk", "Accept risk for vulnerabilities with justification."); var acceptJustificationArg = new Argument("justification") { Description = "Justification for accepting the risk." }; var acceptDueDateOption = new Option("--due-date") { Description = "Due date for risk review (ISO-8601)." }; acceptRisk.Add(acceptJustificationArg); acceptRisk.Add(acceptDueDateOption); acceptRisk.Add(wfVulnIdsOption); acceptRisk.Add(wfFilterSeverityOption); acceptRisk.Add(wfFilterStatusOption); acceptRisk.Add(wfFilterPurlOption); acceptRisk.Add(wfFilterSbomOption); acceptRisk.Add(wfTenantOption); acceptRisk.Add(wfIdempotencyKeyOption); acceptRisk.Add(wfJsonOption); acceptRisk.Add(verboseOption); acceptRisk.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync( services, "accept_risk", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty(), parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption), parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption), parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption), parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption), null, null, parseResult.GetValue(acceptJustificationArg), parseResult.GetValue(acceptDueDateOption), null, cancellationToken)); vuln.Add(acceptRisk); // verify-fix command var verifyFix = new Command("verify-fix", "Mark vulnerabilities as fixed and verified."); var fixVersionOption = new Option("--fix-version") { Description = "Version where the fix was applied." }; var fixCommentOption = new Option("--comment") { Description = "Optional comment about the fix." }; verifyFix.Add(fixVersionOption); verifyFix.Add(fixCommentOption); verifyFix.Add(wfVulnIdsOption); verifyFix.Add(wfFilterSeverityOption); verifyFix.Add(wfFilterStatusOption); verifyFix.Add(wfFilterPurlOption); verifyFix.Add(wfFilterSbomOption); verifyFix.Add(wfTenantOption); verifyFix.Add(wfIdempotencyKeyOption); verifyFix.Add(wfJsonOption); verifyFix.Add(verboseOption); verifyFix.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync( services, "verify_fix", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty(), parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption), parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption), parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption), parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption), null, parseResult.GetValue(fixCommentOption), null, null, parseResult.GetValue(fixVersionOption), cancellationToken)); vuln.Add(verifyFix); // target-fix command var targetFix = new Command("target-fix", "Set a target fix date for vulnerabilities."); var targetDueDateArg = new Argument("due-date") { Description = "Target fix date (ISO-8601 format, e.g., 2024-12-31)." }; var targetCommentOption = new Option("--comment") { Description = "Optional comment about the target." }; targetFix.Add(targetDueDateArg); targetFix.Add(targetCommentOption); targetFix.Add(wfVulnIdsOption); targetFix.Add(wfFilterSeverityOption); targetFix.Add(wfFilterStatusOption); targetFix.Add(wfFilterPurlOption); targetFix.Add(wfFilterSbomOption); targetFix.Add(wfTenantOption); targetFix.Add(wfIdempotencyKeyOption); targetFix.Add(wfJsonOption); targetFix.Add(verboseOption); targetFix.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync( services, "target_fix", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty(), parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption), parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption), parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption), parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption), null, parseResult.GetValue(targetCommentOption), null, parseResult.GetValue(targetDueDateArg), null, cancellationToken)); vuln.Add(targetFix); // reopen command var reopen = new Command("reopen", "Reopen closed or accepted vulnerabilities."); var reopenCommentOption = new Option("--comment") { Description = "Reason for reopening." }; reopen.Add(reopenCommentOption); reopen.Add(wfVulnIdsOption); reopen.Add(wfFilterSeverityOption); reopen.Add(wfFilterStatusOption); reopen.Add(wfFilterPurlOption); reopen.Add(wfFilterSbomOption); reopen.Add(wfTenantOption); reopen.Add(wfIdempotencyKeyOption); reopen.Add(wfJsonOption); reopen.Add(verboseOption); reopen.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync( services, "reopen", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty(), parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption), parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption), parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption), parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption), null, parseResult.GetValue(reopenCommentOption), null, null, null, cancellationToken)); vuln.Add(reopen); // CLI-VULN-29-004: simulate command var simulate = new Command("simulate", "Simulate policy/VEX changes and show delta summaries."); var simPolicyIdOption = new Option("--policy-id") { Description = "Policy ID to simulate (uses different version or a new policy)." }; var simPolicyVersionOption = new Option("--policy-version") { Description = "Policy version to simulate against." }; var simVexOverrideOption = new Option("--vex-override") { Description = "VEX status overrides in format vulnId=status (e.g., CVE-2024-1234=not_affected).", AllowMultipleArgumentsPerToken = true }; var simSeverityThresholdOption = new Option("--severity-threshold") { Description = "Severity threshold for simulation (critical, high, medium, low)." }; var simSbomIdsOption = new Option("--sbom-id") { Description = "SBOM IDs to include in simulation scope.", AllowMultipleArgumentsPerToken = true }; var simOutputMarkdownOption = new Option("--markdown") { Description = "Include Markdown report suitable for CI pipelines." }; var simChangedOnlyOption = new Option("--changed-only") { Description = "Only show items that changed." }; var simTenantOption = new Option("--tenant") { Description = "Tenant identifier for multi-tenant environments." }; var simJsonOption = new Option("--json") { Description = "Output as JSON for automation." }; var simOutputFileOption = new Option("--output") { Description = "Write Markdown report to file instead of console." }; simulate.Add(simPolicyIdOption); simulate.Add(simPolicyVersionOption); simulate.Add(simVexOverrideOption); simulate.Add(simSeverityThresholdOption); simulate.Add(simSbomIdsOption); simulate.Add(simOutputMarkdownOption); simulate.Add(simChangedOnlyOption); simulate.Add(simTenantOption); simulate.Add(simJsonOption); simulate.Add(simOutputFileOption); simulate.Add(verboseOption); simulate.SetAction((parseResult, _) => CommandHandlers.HandleVulnSimulateAsync( services, parseResult.GetValue(simPolicyIdOption), parseResult.GetValue(simPolicyVersionOption), parseResult.GetValue(simVexOverrideOption) ?? Array.Empty(), parseResult.GetValue(simSeverityThresholdOption), parseResult.GetValue(simSbomIdsOption) ?? Array.Empty(), parseResult.GetValue(simOutputMarkdownOption), parseResult.GetValue(simChangedOnlyOption), parseResult.GetValue(simTenantOption), parseResult.GetValue(simJsonOption), parseResult.GetValue(simOutputFileOption), parseResult.GetValue(verboseOption), cancellationToken)); vuln.Add(simulate); // CLI-VULN-29-005: export command with verify subcommand var export = new Command("export", "Export vulnerability evidence bundles."); var expVulnIdsOption = new Option("--vuln-id") { Description = "Vulnerability IDs to include in export.", AllowMultipleArgumentsPerToken = true }; var expSbomIdsOption = new Option("--sbom-id") { Description = "SBOM IDs to include in export scope.", AllowMultipleArgumentsPerToken = true }; var expPolicyIdOption = new Option("--policy-id") { Description = "Policy ID for export filtering." }; var expFormatOption = new Option("--format") { Description = "Export format (ndjson, json).", DefaultValueFactory = _ => "ndjson" }; var expIncludeEvidenceOption = new Option("--include-evidence") { Description = "Include evidence data in export (default: true).", DefaultValueFactory = _ => true }; var expIncludeLedgerOption = new Option("--include-ledger") { Description = "Include workflow ledger in export (default: true).", DefaultValueFactory = _ => true }; var expSignedOption = new Option("--signed") { Description = "Request signed export bundle (default: true).", DefaultValueFactory = _ => true }; var expOutputOption = new Option("--output") { Description = "Output file path for the export bundle.", Required = true }; var expTenantOption = new Option("--tenant") { Description = "Tenant identifier for multi-tenant environments." }; export.Add(expVulnIdsOption); export.Add(expSbomIdsOption); export.Add(expPolicyIdOption); export.Add(expFormatOption); export.Add(expIncludeEvidenceOption); export.Add(expIncludeLedgerOption); export.Add(expSignedOption); export.Add(expOutputOption); export.Add(expTenantOption); export.Add(verboseOption); export.SetAction((parseResult, _) => CommandHandlers.HandleVulnExportAsync( services, parseResult.GetValue(expVulnIdsOption) ?? Array.Empty(), parseResult.GetValue(expSbomIdsOption) ?? Array.Empty(), parseResult.GetValue(expPolicyIdOption), parseResult.GetValue(expFormatOption) ?? "ndjson", parseResult.GetValue(expIncludeEvidenceOption), parseResult.GetValue(expIncludeLedgerOption), parseResult.GetValue(expSignedOption), parseResult.GetValue(expOutputOption) ?? "", parseResult.GetValue(expTenantOption), parseResult.GetValue(verboseOption), cancellationToken)); // verify subcommand var verify = new Command("verify", "Verify signature and digest of an exported vulnerability bundle."); var verifyFileArg = new Argument("file") { Description = "Path to the export bundle file to verify." }; var verifyExpectedDigestOption = new Option("--expected-digest") { Description = "Expected digest to verify (sha256:hex format)." }; var verifyPublicKeyOption = new Option("--public-key") { Description = "Path to public key file for signature verification." }; verify.Add(verifyFileArg); verify.Add(verifyExpectedDigestOption); verify.Add(verifyPublicKeyOption); verify.Add(verboseOption); verify.SetAction((parseResult, _) => CommandHandlers.HandleVulnExportVerifyAsync( services, parseResult.GetValue(verifyFileArg) ?? "", parseResult.GetValue(verifyExpectedDigestOption), parseResult.GetValue(verifyPublicKeyOption), parseResult.GetValue(verboseOption), cancellationToken)); export.Add(verify); vuln.Add(export); return vuln; } // CLI-VEX-30-001: VEX consensus commands private static Command BuildVexCommand(IServiceProvider services, StellaOpsCliOptions options, Option verboseOption, CancellationToken cancellationToken) { var vex = new Command("vex", "Manage VEX (Vulnerability Exploitability eXchange) consensus data."); var consensus = new Command("consensus", "Explore VEX consensus decisions."); var list = new Command("list", "List VEX consensus decisions with filters and pagination."); var vulnIdOption = new Option("--vuln-id") { Description = "Filter by vulnerability identifier (e.g., CVE-2024-1234)." }; var productKeyOption = new Option("--product-key") { Description = "Filter by product key." }; var purlOption = new Option("--purl") { Description = "Filter by Package URL." }; var statusOption = new Option("--status") { Description = "Filter by VEX status (affected, not_affected, fixed, under_investigation)." }; var policyVersionOption = new Option("--policy-version") { Description = "Filter by policy version." }; var limitOption = new Option("--limit") { Description = "Maximum number of results (default 50)." }; var offsetOption = new Option("--offset") { Description = "Number of results to skip for pagination." }; var tenantOption = new Option("--tenant", new[] { "-t" }) { Description = "Tenant identifier. Overrides profile and STELLAOPS_TENANT environment variable." }; var jsonOption = new Option("--json") { Description = "Emit raw JSON payload instead of a table." }; var csvOption = new Option("--csv") { Description = "Emit CSV format instead of a table." }; list.Add(vulnIdOption); list.Add(productKeyOption); list.Add(purlOption); list.Add(statusOption); list.Add(policyVersionOption); list.Add(limitOption); list.Add(offsetOption); list.Add(tenantOption); list.Add(jsonOption); list.Add(csvOption); list.SetAction((parseResult, _) => { var vulnId = parseResult.GetValue(vulnIdOption); var productKey = parseResult.GetValue(productKeyOption); var purl = parseResult.GetValue(purlOption); var status = parseResult.GetValue(statusOption); var policyVersion = parseResult.GetValue(policyVersionOption); var limit = parseResult.GetValue(limitOption); var offset = parseResult.GetValue(offsetOption); var tenant = parseResult.GetValue(tenantOption); var emitJson = parseResult.GetValue(jsonOption); var emitCsv = parseResult.GetValue(csvOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVexConsensusListAsync( services, vulnId, productKey, purl, status, policyVersion, limit, offset, tenant, emitJson, emitCsv, verbose, cancellationToken); }); // CLI-VEX-30-002: show subcommand var show = new Command("show", "Display detailed VEX consensus including quorum, evidence, rationale, and signature status."); var showVulnIdArg = new Argument("vulnerability-id") { Description = "Vulnerability identifier (e.g., CVE-2024-1234)." }; var showProductKeyArg = new Argument("product-key") { Description = "Product key identifying the affected component." }; var showTenantOption = new Option("--tenant", new[] { "-t" }) { Description = "Tenant identifier. Overrides profile and STELLAOPS_TENANT environment variable." }; var showJsonOption = new Option("--json") { Description = "Emit raw JSON payload instead of formatted output." }; show.Add(showVulnIdArg); show.Add(showProductKeyArg); show.Add(showTenantOption); show.Add(showJsonOption); show.SetAction((parseResult, _) => { var vulnId = parseResult.GetValue(showVulnIdArg) ?? string.Empty; var productKey = parseResult.GetValue(showProductKeyArg) ?? string.Empty; var tenant = parseResult.GetValue(showTenantOption); var emitJson = parseResult.GetValue(showJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVexConsensusShowAsync( services, vulnId, productKey, tenant, emitJson, verbose, cancellationToken); }); consensus.Add(list); consensus.Add(show); vex.Add(consensus); // CLI-VEX-30-003: simulate command var simulate = new Command("simulate", "Simulate VEX consensus with trust/threshold overrides to preview changes."); var simVulnIdOption = new Option("--vuln-id") { Description = "Filter by vulnerability identifier." }; var simProductKeyOption = new Option("--product-key") { Description = "Filter by product key." }; var simPurlOption = new Option("--purl") { Description = "Filter by Package URL." }; var simThresholdOption = new Option("--threshold") { Description = "Override the weight threshold for consensus (0.0-1.0)." }; var simQuorumOption = new Option("--quorum") { Description = "Override the minimum quorum requirement." }; var simTrustOption = new Option("--trust", new[] { "-w" }) { Description = "Trust weight override in format provider=weight (repeatable). Example: --trust nvd=1.5 --trust vendor=2.0", Arity = ArgumentArity.ZeroOrMore }; var simExcludeOption = new Option("--exclude") { Description = "Exclude provider from simulation (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var simIncludeOnlyOption = new Option("--include-only") { Description = "Include only these providers (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var simTenantOption = new Option("--tenant", new[] { "-t" }) { Description = "Tenant identifier." }; var simJsonOption = new Option("--json") { Description = "Emit raw JSON output with full diff details." }; var simChangedOnlyOption = new Option("--changed-only") { Description = "Show only items where the status changed." }; simulate.Add(simVulnIdOption); simulate.Add(simProductKeyOption); simulate.Add(simPurlOption); simulate.Add(simThresholdOption); simulate.Add(simQuorumOption); simulate.Add(simTrustOption); simulate.Add(simExcludeOption); simulate.Add(simIncludeOnlyOption); simulate.Add(simTenantOption); simulate.Add(simJsonOption); simulate.Add(simChangedOnlyOption); simulate.SetAction((parseResult, _) => { var vulnId = parseResult.GetValue(simVulnIdOption); var productKey = parseResult.GetValue(simProductKeyOption); var purl = parseResult.GetValue(simPurlOption); var threshold = parseResult.GetValue(simThresholdOption); var quorum = parseResult.GetValue(simQuorumOption); var trustOverrides = parseResult.GetValue(simTrustOption) ?? Array.Empty(); var exclude = parseResult.GetValue(simExcludeOption) ?? Array.Empty(); var includeOnly = parseResult.GetValue(simIncludeOnlyOption) ?? Array.Empty(); var tenant = parseResult.GetValue(simTenantOption); var emitJson = parseResult.GetValue(simJsonOption); var changedOnly = parseResult.GetValue(simChangedOnlyOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVexSimulateAsync( services, vulnId, productKey, purl, threshold, quorum, trustOverrides, exclude, includeOnly, tenant, emitJson, changedOnly, verbose, cancellationToken); }); vex.Add(simulate); // CLI-VEX-30-004: export command var export = new Command("export", "Export VEX consensus data as NDJSON bundle with optional signature."); var expVulnIdsOption = new Option("--vuln-id") { Description = "Filter by vulnerability identifiers (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var expProductKeysOption = new Option("--product-key") { Description = "Filter by product keys (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var expPurlsOption = new Option("--purl") { Description = "Filter by Package URLs (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var expStatusesOption = new Option("--status") { Description = "Filter by VEX statuses (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var expPolicyVersionOption = new Option("--policy-version") { Description = "Filter by policy version." }; var expOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output file path for the NDJSON bundle.", Required = true }; var expUnsignedOption = new Option("--unsigned") { Description = "Generate unsigned export (default is signed)." }; var expTenantOption = new Option("--tenant", new[] { "-t" }) { Description = "Tenant identifier." }; export.Add(expVulnIdsOption); export.Add(expProductKeysOption); export.Add(expPurlsOption); export.Add(expStatusesOption); export.Add(expPolicyVersionOption); export.Add(expOutputOption); export.Add(expUnsignedOption); export.Add(expTenantOption); export.SetAction((parseResult, _) => { var vulnIds = parseResult.GetValue(expVulnIdsOption) ?? Array.Empty(); var productKeys = parseResult.GetValue(expProductKeysOption) ?? Array.Empty(); var purls = parseResult.GetValue(expPurlsOption) ?? Array.Empty(); var statuses = parseResult.GetValue(expStatusesOption) ?? Array.Empty(); var policyVersion = parseResult.GetValue(expPolicyVersionOption); var output = parseResult.GetValue(expOutputOption) ?? string.Empty; var unsigned = parseResult.GetValue(expUnsignedOption); var tenant = parseResult.GetValue(expTenantOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVexExportAsync( services, vulnIds, productKeys, purls, statuses, policyVersion, output, !unsigned, tenant, verbose, cancellationToken); }); // verify subcommand for signature verification var verify = new Command("verify", "Verify signature and digest of a VEX export bundle."); var verifyFileArg = new Argument("file") { Description = "Path to the NDJSON export file to verify." }; var verifyDigestOption = new Option("--digest") { Description = "Expected SHA-256 digest to verify." }; var verifyKeyOption = new Option("--public-key") { Description = "Path to public key file for signature verification." }; verify.Add(verifyFileArg); verify.Add(verifyDigestOption); verify.Add(verifyKeyOption); verify.SetAction((parseResult, _) => { var file = parseResult.GetValue(verifyFileArg) ?? string.Empty; var digest = parseResult.GetValue(verifyDigestOption); var publicKey = parseResult.GetValue(verifyKeyOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVexVerifyAsync( services, file, digest, publicKey, verbose, cancellationToken); }); export.Add(verify); vex.Add(export); // CLI-LNM-22-002: VEX observation commands var obs = new Command("obs", "Query VEX observations (Link-Not-Merge architecture)."); // vex obs get var obsGet = new Command("get", "Get VEX observations with filters."); var obsGetTenantOption = new Option("--tenant", new[] { "-t" }) { Description = "Tenant identifier.", Required = true }; var obsGetVulnIdOption = new Option("--vuln-id") { Description = "Filter by vulnerability IDs (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var obsGetProductKeyOption = new Option("--product-key") { Description = "Filter by product keys (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var obsGetPurlOption = new Option("--purl") { Description = "Filter by Package URLs (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var obsGetCpeOption = new Option("--cpe") { Description = "Filter by CPEs (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var obsGetStatusOption = new Option("--status") { Description = "Filter by status (affected, not_affected, fixed, under_investigation). Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var obsGetProviderOption = new Option("--provider") { Description = "Filter by provider IDs (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var obsGetLimitOption = new Option("--limit", "-l") { Description = "Maximum number of results (default 50)." }; var obsGetCursorOption = new Option("--cursor") { Description = "Pagination cursor from previous response." }; var obsGetJsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; obsGet.Add(obsGetTenantOption); obsGet.Add(obsGetVulnIdOption); obsGet.Add(obsGetProductKeyOption); obsGet.Add(obsGetPurlOption); obsGet.Add(obsGetCpeOption); obsGet.Add(obsGetStatusOption); obsGet.Add(obsGetProviderOption); obsGet.Add(obsGetLimitOption); obsGet.Add(obsGetCursorOption); obsGet.Add(obsGetJsonOption); obsGet.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(obsGetTenantOption) ?? string.Empty; var vulnIds = parseResult.GetValue(obsGetVulnIdOption) ?? Array.Empty(); var productKeys = parseResult.GetValue(obsGetProductKeyOption) ?? Array.Empty(); var purls = parseResult.GetValue(obsGetPurlOption) ?? Array.Empty(); var cpes = parseResult.GetValue(obsGetCpeOption) ?? Array.Empty(); var statuses = parseResult.GetValue(obsGetStatusOption) ?? Array.Empty(); var providers = parseResult.GetValue(obsGetProviderOption) ?? Array.Empty(); var limit = parseResult.GetValue(obsGetLimitOption); var cursor = parseResult.GetValue(obsGetCursorOption); var emitJson = parseResult.GetValue(obsGetJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVexObsGetAsync( services, tenant, vulnIds, productKeys, purls, cpes, statuses, providers, limit, cursor, emitJson, verbose, cancellationToken); }); obs.Add(obsGet); // vex linkset show var linkset = new Command("linkset", "Explore VEX observation linksets."); var linksetShow = new Command("show", "Show linked observations for a vulnerability."); var linksetShowVulnIdArg = new Argument("vulnerability-id") { Description = "Vulnerability identifier (e.g., CVE-2024-1234)." }; var linksetShowTenantOption = new Option("--tenant", new[] { "-t" }) { Description = "Tenant identifier.", Required = true }; var linksetShowProductKeyOption = new Option("--product-key") { Description = "Filter by product keys (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var linksetShowPurlOption = new Option("--purl") { Description = "Filter by Package URLs (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var linksetShowStatusOption = new Option("--status") { Description = "Filter by status (repeatable).", Arity = ArgumentArity.ZeroOrMore }; var linksetShowJsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; linksetShow.Add(linksetShowVulnIdArg); linksetShow.Add(linksetShowTenantOption); linksetShow.Add(linksetShowProductKeyOption); linksetShow.Add(linksetShowPurlOption); linksetShow.Add(linksetShowStatusOption); linksetShow.Add(linksetShowJsonOption); linksetShow.SetAction((parseResult, _) => { var vulnId = parseResult.GetValue(linksetShowVulnIdArg) ?? string.Empty; var tenant = parseResult.GetValue(linksetShowTenantOption) ?? string.Empty; var productKeys = parseResult.GetValue(linksetShowProductKeyOption) ?? Array.Empty(); var purls = parseResult.GetValue(linksetShowPurlOption) ?? Array.Empty(); var statuses = parseResult.GetValue(linksetShowStatusOption) ?? Array.Empty(); var emitJson = parseResult.GetValue(linksetShowJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVexLinksetShowAsync( services, tenant, vulnId, productKeys, purls, statuses, emitJson, verbose, cancellationToken); }); linkset.Add(linksetShow); obs.Add(linkset); vex.Add(obs); return vex; } private static Command BuildConfigCommand(StellaOpsCliOptions options) { var config = new Command("config", "Inspect CLI configuration state."); var show = new Command("show", "Display resolved configuration values."); show.SetAction((_, _) => { var authority = options.Authority ?? new StellaOpsCliAuthorityOptions(); var lines = new[] { $"Backend URL: {MaskIfEmpty(options.BackendUrl)}", $"Concelier URL: {MaskIfEmpty(options.ConcelierUrl)}", $"API Key: {DescribeSecret(options.ApiKey)}", $"Scanner Cache: {options.ScannerCacheDirectory}", $"Results Directory: {options.ResultsDirectory}", $"Default Runner: {options.DefaultRunner}", $"Authority URL: {MaskIfEmpty(authority.Url)}", $"Authority Client ID: {MaskIfEmpty(authority.ClientId)}", $"Authority Client Secret: {DescribeSecret(authority.ClientSecret ?? string.Empty)}", $"Authority Username: {MaskIfEmpty(authority.Username)}", $"Authority Password: {DescribeSecret(authority.Password ?? string.Empty)}", $"Authority Scope: {MaskIfEmpty(authority.Scope)}", $"Authority Token Cache: {MaskIfEmpty(authority.TokenCacheDirectory ?? string.Empty)}" }; foreach (var line in lines) { Console.WriteLine(line); } return Task.CompletedTask; }); config.Add(show); return config; } private static string MaskIfEmpty(string value) => string.IsNullOrWhiteSpace(value) ? "" : value; private static string DescribeSecret(string value) { if (string.IsNullOrWhiteSpace(value)) { return ""; } return value.Length switch { <= 4 => "****", _ => $"{value[..2]}***{value[^2..]}" }; } private static Command BuildAttestCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var attest = new Command("attest", "Verify and inspect DSSE attestations."); // attest verify var verify = new Command("verify", "Verify a DSSE envelope offline against policy and trust roots."); var envelopeOption = new Option("--envelope", new[] { "-e" }) { Description = "Path to the DSSE envelope file (JSON or sigstore bundle).", Required = true }; var policyOption = new Option("--policy") { Description = "Path to policy JSON file for verification rules." }; var rootOption = new Option("--root") { Description = "Path to trusted root certificate (PEM format)." }; var checkpointOption = new Option("--transparency-checkpoint") { Description = "Path to Rekor transparency checkpoint file." }; var verifyOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output path for verification report." }; var verifyFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; var verifyExplainOption = new Option("--explain") { Description = "Include detailed explanations for each verification check." }; verify.Add(envelopeOption); verify.Add(policyOption); verify.Add(rootOption); verify.Add(checkpointOption); verify.Add(verifyOutputOption); verify.Add(verifyFormatOption); verify.Add(verifyExplainOption); verify.SetAction((parseResult, _) => { var envelope = parseResult.GetValue(envelopeOption)!; var policy = parseResult.GetValue(policyOption); var root = parseResult.GetValue(rootOption); var checkpoint = parseResult.GetValue(checkpointOption); var output = parseResult.GetValue(verifyOutputOption); var format = parseResult.GetValue(verifyFormatOption) ?? "table"; var explain = parseResult.GetValue(verifyExplainOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestVerifyAsync(services, envelope, policy, root, checkpoint, output, format, explain, verbose, cancellationToken); }); // attest list (CLI-ATTEST-74-001) var list = new Command("list", "List attestations from local storage or backend."); var listTenantOption = new Option("--tenant") { Description = "Filter by tenant identifier." }; var listIssuerOption = new Option("--issuer") { Description = "Filter by issuer identifier." }; var listSubjectOption = new Option("--subject", new[] { "-s" }) { Description = "Filter by subject (e.g., image digest, package PURL)." }; var listTypeOption = new Option("--type", new[] { "-t" }) { Description = "Filter by predicate type URI." }; var listScopeOption = new Option("--scope") { Description = "Filter by scope (local, remote, all). Default: all." }; var listFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format (table, json). Default: table." }; var listLimitOption = new Option("--limit", new[] { "-n" }) { Description = "Maximum number of results to return. Default: 50." }; var listOffsetOption = new Option("--offset") { Description = "Number of results to skip (for pagination). Default: 0." }; list.Add(listTenantOption); list.Add(listIssuerOption); list.Add(listSubjectOption); list.Add(listTypeOption); list.Add(listScopeOption); list.Add(listFormatOption); list.Add(listLimitOption); list.Add(listOffsetOption); list.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(listTenantOption); var issuer = parseResult.GetValue(listIssuerOption); var subject = parseResult.GetValue(listSubjectOption); var type = parseResult.GetValue(listTypeOption); var scope = parseResult.GetValue(listScopeOption) ?? "all"; var format = parseResult.GetValue(listFormatOption) ?? "table"; var limit = parseResult.GetValue(listLimitOption); var offset = parseResult.GetValue(listOffsetOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestListAsync(services, tenant, issuer, subject, type, scope, format, limit, offset, verbose, cancellationToken); }); // attest show var show = new Command("show", "Display details for a specific attestation."); var idOption = new Option("--id") { Description = "Attestation identifier.", Required = true }; var showOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output format (json, table)." }; var includeProofOption = new Option("--include-proof") { Description = "Include Rekor inclusion proof in output." }; show.Add(idOption); show.Add(showOutputOption); show.Add(includeProofOption); show.SetAction((parseResult, _) => { var id = parseResult.GetValue(idOption)!; var output = parseResult.GetValue(showOutputOption) ?? "json"; var includeProof = parseResult.GetValue(includeProofOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestShowAsync(services, id, output, includeProof, verbose, cancellationToken); }); // attest sign (CLI-ATTEST-73-001) var sign = new Command("sign", "Create and sign a DSSE attestation envelope."); var predicateFileOption = new Option("--predicate", new[] { "-p" }) { Description = "Path to the predicate JSON file.", Required = true }; var predicateTypeOption = new Option("--predicate-type") { Description = "Predicate type URI (e.g., https://slsa.dev/provenance/v1).", Required = true }; var subjectNameOption = new Option("--subject") { Description = "Subject name or URI to attest.", Required = true }; var subjectDigestOption = new Option("--digest") { Description = "Subject digest in format algorithm:hex (e.g., sha256:abc123...).", Required = true }; var signKeyOption = new Option("--key", new[] { "-k" }) { Description = "Key identifier or path for signing." }; var keylessOption = new Option("--keyless") { Description = "Use keyless (OIDC) signing via Sigstore Fulcio." }; var transparencyLogOption = new Option("--rekor") { Description = "Submit attestation to Rekor transparency log (default: false)." }; var noRekorOption = new Option("--no-rekor") { Description = "Explicitly skip Rekor submission." }; var signOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output path for the signed DSSE envelope JSON." }; var signFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: dsse (default), sigstore-bundle." }; sign.Add(predicateFileOption); sign.Add(predicateTypeOption); sign.Add(subjectNameOption); sign.Add(subjectDigestOption); sign.Add(signKeyOption); sign.Add(keylessOption); sign.Add(transparencyLogOption); sign.Add(noRekorOption); sign.Add(signOutputOption); sign.Add(signFormatOption); sign.SetAction((parseResult, _) => { var predicatePath = parseResult.GetValue(predicateFileOption)!; var predicateType = parseResult.GetValue(predicateTypeOption)!; var subjectName = parseResult.GetValue(subjectNameOption)!; var digest = parseResult.GetValue(subjectDigestOption)!; var keyId = parseResult.GetValue(signKeyOption); var keyless = parseResult.GetValue(keylessOption); var useRekor = parseResult.GetValue(transparencyLogOption); var noRekor = parseResult.GetValue(noRekorOption); var output = parseResult.GetValue(signOutputOption); var format = parseResult.GetValue(signFormatOption) ?? "dsse"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestSignAsync( services, predicatePath, predicateType, subjectName, digest, keyId, keyless, useRekor && !noRekor, output, format, verbose, cancellationToken); }); // attest fetch (CLI-ATTEST-74-002) var fetch = new Command("fetch", "Download attestation envelopes and payloads to disk."); var fetchIdOption = new Option("--id") { Description = "Attestation ID to fetch." }; var fetchSubjectOption = new Option("--subject", new[] { "-s" }) { Description = "Subject filter (e.g., image digest, package PURL)." }; var fetchTypeOption = new Option("--type", new[] { "-t" }) { Description = "Predicate type filter." }; var fetchOutputDirOption = new Option("--output-dir", new[] { "-o" }) { Description = "Output directory for downloaded files.", Required = true }; var fetchIncludeOption = new Option("--include") { Description = "What to download: envelope, payload, both (default: both)." }; var fetchScopeOption = new Option("--scope") { Description = "Source scope: local, remote, all (default: all)." }; var fetchFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format for payloads: json (default), raw." }; var fetchOverwriteOption = new Option("--overwrite") { Description = "Overwrite existing files." }; fetch.Add(fetchIdOption); fetch.Add(fetchSubjectOption); fetch.Add(fetchTypeOption); fetch.Add(fetchOutputDirOption); fetch.Add(fetchIncludeOption); fetch.Add(fetchScopeOption); fetch.Add(fetchFormatOption); fetch.Add(fetchOverwriteOption); fetch.SetAction((parseResult, _) => { var id = parseResult.GetValue(fetchIdOption); var subject = parseResult.GetValue(fetchSubjectOption); var type = parseResult.GetValue(fetchTypeOption); var outputDir = parseResult.GetValue(fetchOutputDirOption)!; var include = parseResult.GetValue(fetchIncludeOption) ?? "both"; var scope = parseResult.GetValue(fetchScopeOption) ?? "all"; var format = parseResult.GetValue(fetchFormatOption) ?? "json"; var overwrite = parseResult.GetValue(fetchOverwriteOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestFetchAsync( services, id, subject, type, outputDir, include, scope, format, overwrite, verbose, cancellationToken); }); // attest key (CLI-ATTEST-75-001) var key = new Command("key", "Manage attestation signing keys."); // attest key create var keyCreate = new Command("create", "Create a new signing key for attestations."); var keyNameOption = new Option("--name", new[] { "-n" }) { Description = "Key identifier/name.", Required = true }; var keyAlgorithmOption = new Option("--algorithm", new[] { "-a" }) { Description = "Key algorithm: ECDSA-P256 (default), ECDSA-P384." }; var keyPasswordOption = new Option("--password", new[] { "-p" }) { Description = "Password to protect the key (required for file-based keys)." }; var keyOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output path for the key directory (default: ~/.stellaops/keys)." }; var keyFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; var keyExportPublicOption = new Option("--export-public") { Description = "Export public key to file alongside key creation." }; keyCreate.Add(keyNameOption); keyCreate.Add(keyAlgorithmOption); keyCreate.Add(keyPasswordOption); keyCreate.Add(keyOutputOption); keyCreate.Add(keyFormatOption); keyCreate.Add(keyExportPublicOption); keyCreate.SetAction((parseResult, _) => { var name = parseResult.GetValue(keyNameOption)!; var algorithm = parseResult.GetValue(keyAlgorithmOption) ?? "ECDSA-P256"; var password = parseResult.GetValue(keyPasswordOption); var output = parseResult.GetValue(keyOutputOption); var format = parseResult.GetValue(keyFormatOption) ?? "table"; var exportPublic = parseResult.GetValue(keyExportPublicOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestKeyCreateAsync( services, name, algorithm, password, output, format, exportPublic, verbose, cancellationToken); }); key.Add(keyCreate); // attest bundle (CLI-ATTEST-75-002) var bundle = new Command("bundle", "Build and verify attestation bundles."); // attest bundle build var bundleBuild = new Command("build", "Build an audit bundle from artifacts (attestations, SBOMs, VEX, scans)."); var bundleSubjectNameOption = new Option("--subject-name", new[] { "-s" }) { Description = "Primary subject name (e.g., image reference).", Required = true }; var bundleSubjectDigestOption = new Option("--subject-digest", new[] { "-d" }) { Description = "Subject digest in algorithm:hex format (e.g., sha256:abc123...).", Required = true }; var bundleSubjectTypeOption = new Option("--subject-type") { Description = "Subject type: IMAGE (default), REPO, SBOM, OTHER." }; var bundleInputDirOption = new Option("--input", new[] { "-i" }) { Description = "Input directory containing artifacts to bundle.", Required = true }; var bundleOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output path for the bundle (directory or .tar.gz file).", Required = true }; var bundleFromOption = new Option("--from") { Description = "Start of time window for artifacts (ISO-8601)." }; var bundleToOption = new Option("--to") { Description = "End of time window for artifacts (ISO-8601)." }; var bundleIncludeOption = new Option("--include") { Description = "Artifact types to include: attestations,sboms,vex,scans,policy,all (default: all)." }; var bundleCompressOption = new Option("--compress") { Description = "Compress output as tar.gz." }; var bundleCreatorIdOption = new Option("--creator-id") { Description = "Creator user ID (default: current user)." }; var bundleCreatorNameOption = new Option("--creator-name") { Description = "Creator display name (default: current user)." }; var bundleFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; bundleBuild.Add(bundleSubjectNameOption); bundleBuild.Add(bundleSubjectDigestOption); bundleBuild.Add(bundleSubjectTypeOption); bundleBuild.Add(bundleInputDirOption); bundleBuild.Add(bundleOutputOption); bundleBuild.Add(bundleFromOption); bundleBuild.Add(bundleToOption); bundleBuild.Add(bundleIncludeOption); bundleBuild.Add(bundleCompressOption); bundleBuild.Add(bundleCreatorIdOption); bundleBuild.Add(bundleCreatorNameOption); bundleBuild.Add(bundleFormatOption); bundleBuild.SetAction((parseResult, _) => { var subjectName = parseResult.GetValue(bundleSubjectNameOption)!; var subjectDigest = parseResult.GetValue(bundleSubjectDigestOption)!; var subjectType = parseResult.GetValue(bundleSubjectTypeOption) ?? "IMAGE"; var inputDir = parseResult.GetValue(bundleInputDirOption)!; var output = parseResult.GetValue(bundleOutputOption)!; var from = parseResult.GetValue(bundleFromOption); var to = parseResult.GetValue(bundleToOption); var include = parseResult.GetValue(bundleIncludeOption) ?? "all"; var compress = parseResult.GetValue(bundleCompressOption); var creatorId = parseResult.GetValue(bundleCreatorIdOption); var creatorName = parseResult.GetValue(bundleCreatorNameOption); var format = parseResult.GetValue(bundleFormatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestBundleBuildAsync( services, subjectName, subjectDigest, subjectType, inputDir, output, from, to, include, compress, creatorId, creatorName, format, verbose, cancellationToken); }); // attest bundle verify var bundleVerify = new Command("verify", "Verify an attestation bundle's integrity and signatures."); var bundleVerifyInputOption = new Option("--input", new[] { "-i" }) { Description = "Input bundle path (directory or .tar.gz file).", Required = true }; var bundleVerifyPolicyOption = new Option("--policy") { Description = "Policy file for attestation verification (JSON with requiredPredicateTypes, minimumSignatures, etc.)." }; var bundleVerifyRootOption = new Option("--root") { Description = "Trust root file (PEM certificate or public key) for signature verification." }; var bundleVerifyOutputOption = new Option("--output", new[] { "-o" }) { Description = "Write verification report to file (JSON format)." }; var bundleVerifyFormatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: table (default), json." }; var bundleVerifyStrictOption = new Option("--strict") { Description = "Treat warnings as errors (exit code 1 on any issue)." }; bundleVerify.Add(bundleVerifyInputOption); bundleVerify.Add(bundleVerifyPolicyOption); bundleVerify.Add(bundleVerifyRootOption); bundleVerify.Add(bundleVerifyOutputOption); bundleVerify.Add(bundleVerifyFormatOption); bundleVerify.Add(bundleVerifyStrictOption); bundleVerify.SetAction((parseResult, _) => { var input = parseResult.GetValue(bundleVerifyInputOption)!; var policy = parseResult.GetValue(bundleVerifyPolicyOption); var root = parseResult.GetValue(bundleVerifyRootOption); var output = parseResult.GetValue(bundleVerifyOutputOption); var format = parseResult.GetValue(bundleVerifyFormatOption) ?? "table"; var strict = parseResult.GetValue(bundleVerifyStrictOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAttestBundleVerifyAsync( services, input, policy, root, output, format, strict, verbose, cancellationToken); }); bundle.Add(bundleBuild); bundle.Add(bundleVerify); attest.Add(sign); attest.Add(verify); attest.Add(list); attest.Add(show); attest.Add(fetch); attest.Add(key); attest.Add(bundle); return attest; } private static Command BuildRiskProfileCommand(Option verboseOption, CancellationToken cancellationToken) { _ = cancellationToken; var riskProfile = new Command("risk-profile", "Manage risk profile schemas and validation."); var validate = new Command("validate", "Validate a risk profile JSON file against the schema."); var inputOption = new Option("--input", new[] { "-i" }) { Description = "Path to the risk profile JSON file to validate.", Required = true }; var formatOption = new Option("--format") { Description = "Output format: table (default) or json." }; var outputOption = new Option("--output") { Description = "Write validation report to the specified file path." }; var strictOption = new Option("--strict") { Description = "Treat warnings as errors (exit code 1 on any issue)." }; validate.Add(inputOption); validate.Add(formatOption); validate.Add(outputOption); validate.Add(strictOption); validate.SetAction((parseResult, _) => { var input = parseResult.GetValue(inputOption) ?? string.Empty; var format = parseResult.GetValue(formatOption) ?? "table"; var output = parseResult.GetValue(outputOption); var strict = parseResult.GetValue(strictOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRiskProfileValidateAsync(input, format, output, strict, verbose); }); var schema = new Command("schema", "Display or export the risk profile JSON schema."); var schemaOutputOption = new Option("--output") { Description = "Write the schema to the specified file path." }; schema.Add(schemaOutputOption); schema.SetAction((parseResult, _) => { var output = parseResult.GetValue(schemaOutputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRiskProfileSchemaAsync(output, verbose); }); riskProfile.Add(validate); riskProfile.Add(schema); return riskProfile; } // CLI-LNM-22-001: Advisory command group private static Command BuildAdvisoryCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var advisory = new Command("advisory", "Explore advisory observations, linksets, and exports (Link-Not-Merge)."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier.", Required = true }; var aliasOption = new Option("--alias", "-a") { Description = "Filter by vulnerability alias (CVE, GHSA, etc.). Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var purlOption = new Option("--purl") { Description = "Filter by Package URL. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var cpeOption = new Option("--cpe") { Description = "Filter by CPE value. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var sourceOption = new Option("--source", "-s") { Description = "Filter by source vendor (e.g., nvd, redhat, ubuntu). Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var severityOption = new Option("--severity") { Description = "Filter by severity (critical, high, medium, low)." }; var kevOption = new Option("--kev-only") { Description = "Only show advisories listed in KEV (Known Exploited Vulnerabilities)." }; var hasFixOption = new Option("--has-fix") { Description = "Filter by fix availability (true/false)." }; var limitOption = new Option("--limit", "-l") { Description = "Maximum number of results (default 200, max 500)." }; var cursorOption = new Option("--cursor") { Description = "Pagination cursor from previous response." }; // stella advisory obs get var obsGet = new Command("obs", "Get raw advisory observations."); var obsIdOption = new Option("--observation-id", "-i") { Description = "Filter by observation identifier. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var obsJsonOption = new Option("--json") { Description = "Output as JSON." }; var obsOsvOption = new Option("--osv") { Description = "Output in OSV (Open Source Vulnerability) format." }; var obsShowConflictsOption = new Option("--show-conflicts") { Description = "Include conflict information in output." }; obsGet.Add(tenantOption); obsGet.Add(obsIdOption); obsGet.Add(aliasOption); obsGet.Add(purlOption); obsGet.Add(cpeOption); obsGet.Add(sourceOption); obsGet.Add(severityOption); obsGet.Add(kevOption); obsGet.Add(hasFixOption); obsGet.Add(limitOption); obsGet.Add(cursorOption); obsGet.Add(obsJsonOption); obsGet.Add(obsOsvOption); obsGet.Add(obsShowConflictsOption); obsGet.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption) ?? string.Empty; var observationIds = parseResult.GetValue(obsIdOption) ?? Array.Empty(); var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty(); var purls = parseResult.GetValue(purlOption) ?? Array.Empty(); var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty(); var sources = parseResult.GetValue(sourceOption) ?? Array.Empty(); var severity = parseResult.GetValue(severityOption); var kevOnly = parseResult.GetValue(kevOption); var hasFix = parseResult.GetValue(hasFixOption); var limit = parseResult.GetValue(limitOption); var cursor = parseResult.GetValue(cursorOption); var emitJson = parseResult.GetValue(obsJsonOption); var emitOsv = parseResult.GetValue(obsOsvOption); var showConflicts = parseResult.GetValue(obsShowConflictsOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAdvisoryObsGetAsync( services, tenant, observationIds, aliases, purls, cpes, sources, severity, kevOnly, hasFix, limit, cursor, emitJson, emitOsv, showConflicts, verbose, cancellationToken); }); advisory.Add(obsGet); // stella advisory linkset show var linksetShow = new Command("linkset", "Show aggregated linkset with conflict summary."); var linksetJsonOption = new Option("--json") { Description = "Output as JSON." }; linksetShow.Add(tenantOption); linksetShow.Add(aliasOption); linksetShow.Add(purlOption); linksetShow.Add(cpeOption); linksetShow.Add(sourceOption); linksetShow.Add(linksetJsonOption); linksetShow.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption) ?? string.Empty; var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty(); var purls = parseResult.GetValue(purlOption) ?? Array.Empty(); var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty(); var sources = parseResult.GetValue(sourceOption) ?? Array.Empty(); var emitJson = parseResult.GetValue(linksetJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAdvisoryLinksetShowAsync( services, tenant, aliases, purls, cpes, sources, emitJson, verbose, cancellationToken); }); advisory.Add(linksetShow); // stella advisory export var export = new Command("export", "Export advisory observations to various formats."); var exportFormatOption = new Option("--format", "-f") { Description = "Export format (json, osv, ndjson, csv). Default: json." }; var exportOutputOption = new Option("--output", "-o") { Description = "Output file path. If not specified, writes to stdout." }; var exportSignedOption = new Option("--signed") { Description = "Request signed export (if supported by backend)." }; export.Add(tenantOption); export.Add(aliasOption); export.Add(purlOption); export.Add(cpeOption); export.Add(sourceOption); export.Add(severityOption); export.Add(kevOption); export.Add(hasFixOption); export.Add(limitOption); export.Add(exportFormatOption); export.Add(exportOutputOption); export.Add(exportSignedOption); export.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption) ?? string.Empty; var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty(); var purls = parseResult.GetValue(purlOption) ?? Array.Empty(); var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty(); var sources = parseResult.GetValue(sourceOption) ?? Array.Empty(); var severity = parseResult.GetValue(severityOption); var kevOnly = parseResult.GetValue(kevOption); var hasFix = parseResult.GetValue(hasFixOption); var limit = parseResult.GetValue(limitOption); var format = parseResult.GetValue(exportFormatOption) ?? "json"; var output = parseResult.GetValue(exportOutputOption); var signed = parseResult.GetValue(exportSignedOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAdvisoryExportAsync( services, tenant, aliases, purls, cpes, sources, severity, kevOnly, hasFix, limit, format, output, signed, verbose, cancellationToken); }); advisory.Add(export); return advisory; } // CLI-FORENSICS-53-001: Forensic snapshot command group private static Command BuildForensicCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var forensic = new Command("forensic", "Manage forensic snapshots and evidence locker operations."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier.", Required = true }; // stella forensic snapshot create --case var snapshotCreate = new Command("snapshot", "Create a forensic snapshot for evidence preservation."); var createCaseOption = new Option("--case", "-c") { Description = "Case identifier to associate with the snapshot.", Required = true }; var createDescOption = new Option("--description", "-d") { Description = "Description of the snapshot purpose." }; var createTagsOption = new Option("--tag") { Description = "Tags to attach to the snapshot. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var createSbomOption = new Option("--sbom-id") { Description = "SBOM IDs to include in the snapshot scope. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var createScanOption = new Option("--scan-id") { Description = "Scan IDs to include in the snapshot scope. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var createPolicyOption = new Option("--policy-id") { Description = "Policy IDs to include in the snapshot scope. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var createVulnOption = new Option("--vuln-id") { Description = "Vulnerability IDs to include in the snapshot scope. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var createRetentionOption = new Option("--retention-days") { Description = "Retention period in days (default: per tenant policy)." }; var createJsonOption = new Option("--json") { Description = "Output as JSON." }; snapshotCreate.Add(tenantOption); snapshotCreate.Add(createCaseOption); snapshotCreate.Add(createDescOption); snapshotCreate.Add(createTagsOption); snapshotCreate.Add(createSbomOption); snapshotCreate.Add(createScanOption); snapshotCreate.Add(createPolicyOption); snapshotCreate.Add(createVulnOption); snapshotCreate.Add(createRetentionOption); snapshotCreate.Add(createJsonOption); snapshotCreate.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption) ?? string.Empty; var caseId = parseResult.GetValue(createCaseOption) ?? string.Empty; var description = parseResult.GetValue(createDescOption); var tags = parseResult.GetValue(createTagsOption) ?? Array.Empty(); var sbomIds = parseResult.GetValue(createSbomOption) ?? Array.Empty(); var scanIds = parseResult.GetValue(createScanOption) ?? Array.Empty(); var policyIds = parseResult.GetValue(createPolicyOption) ?? Array.Empty(); var vulnIds = parseResult.GetValue(createVulnOption) ?? Array.Empty(); var retentionDays = parseResult.GetValue(createRetentionOption); var emitJson = parseResult.GetValue(createJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleForensicSnapshotCreateAsync( services, tenant, caseId, description, tags, sbomIds, scanIds, policyIds, vulnIds, retentionDays, emitJson, verbose, cancellationToken); }); forensic.Add(snapshotCreate); // stella forensic list var snapshotList = new Command("list", "List forensic snapshots."); var listCaseOption = new Option("--case", "-c") { Description = "Filter by case identifier." }; var listStatusOption = new Option("--status") { Description = "Filter by status (pending, creating, ready, failed, expired, archived)." }; var listTagsOption = new Option("--tag") { Description = "Filter by tags. Repeatable.", Arity = ArgumentArity.ZeroOrMore }; var listLimitOption = new Option("--limit", "-l") { Description = "Maximum number of results (default 50)." }; var listOffsetOption = new Option("--offset") { Description = "Number of results to skip for pagination." }; var listJsonOption = new Option("--json") { Description = "Output as JSON." }; snapshotList.Add(tenantOption); snapshotList.Add(listCaseOption); snapshotList.Add(listStatusOption); snapshotList.Add(listTagsOption); snapshotList.Add(listLimitOption); snapshotList.Add(listOffsetOption); snapshotList.Add(listJsonOption); snapshotList.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption) ?? string.Empty; var caseId = parseResult.GetValue(listCaseOption); var status = parseResult.GetValue(listStatusOption); var tags = parseResult.GetValue(listTagsOption) ?? Array.Empty(); var limit = parseResult.GetValue(listLimitOption); var offset = parseResult.GetValue(listOffsetOption); var emitJson = parseResult.GetValue(listJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleForensicSnapshotListAsync( services, tenant, caseId, status, tags, limit, offset, emitJson, verbose, cancellationToken); }); forensic.Add(snapshotList); // stella forensic show var snapshotShow = new Command("show", "Show forensic snapshot details including manifest digests."); var showSnapshotIdArg = new Argument("snapshot-id") { Description = "Snapshot identifier to show." }; var showJsonOption = new Option("--json") { Description = "Output as JSON." }; var showManifestOption = new Option("--manifest") { Description = "Include full manifest with artifact digests." }; snapshotShow.Add(showSnapshotIdArg); snapshotShow.Add(tenantOption); snapshotShow.Add(showJsonOption); snapshotShow.Add(showManifestOption); snapshotShow.SetAction((parseResult, _) => { var snapshotId = parseResult.GetValue(showSnapshotIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption) ?? string.Empty; var emitJson = parseResult.GetValue(showJsonOption); var includeManifest = parseResult.GetValue(showManifestOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleForensicSnapshotShowAsync( services, tenant, snapshotId, emitJson, includeManifest, verbose, cancellationToken); }); forensic.Add(snapshotShow); // CLI-FORENSICS-54-001: stella forensic verify var verifyCommand = new Command("verify", "Verify forensic bundle integrity, signatures, and chain-of-custody."); var verifyBundleArg = new Argument("bundle") { Description = "Path to forensic bundle directory or manifest file." }; var verifyJsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; var verifyTrustRootOption = new Option("--trust-root", "-r") { Description = "Path to trust root JSON file containing public keys." }; var verifySkipChecksumsOption = new Option("--skip-checksums") { Description = "Skip artifact checksum verification." }; var verifySkipSignaturesOption = new Option("--skip-signatures") { Description = "Skip DSSE signature verification." }; var verifySkipChainOption = new Option("--skip-chain") { Description = "Skip chain-of-custody verification." }; var verifyStrictTimelineOption = new Option("--strict-timeline") { Description = "Enforce strict timeline continuity (fail on gaps > 24h)." }; verifyCommand.Add(verifyBundleArg); verifyCommand.Add(verifyJsonOption); verifyCommand.Add(verifyTrustRootOption); verifyCommand.Add(verifySkipChecksumsOption); verifyCommand.Add(verifySkipSignaturesOption); verifyCommand.Add(verifySkipChainOption); verifyCommand.Add(verifyStrictTimelineOption); verifyCommand.SetAction((parseResult, _) => { var bundlePath = parseResult.GetValue(verifyBundleArg) ?? string.Empty; var emitJson = parseResult.GetValue(verifyJsonOption); var trustRootPath = parseResult.GetValue(verifyTrustRootOption); var skipChecksums = parseResult.GetValue(verifySkipChecksumsOption); var skipSignatures = parseResult.GetValue(verifySkipSignaturesOption); var skipChain = parseResult.GetValue(verifySkipChainOption); var strictTimeline = parseResult.GetValue(verifyStrictTimelineOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleForensicVerifyAsync( services, bundlePath, emitJson, trustRootPath, !skipChecksums, !skipSignatures, !skipChain, strictTimeline, verbose, cancellationToken); }); forensic.Add(verifyCommand); // CLI-FORENSICS-54-002: stella forensic attest show var attestCommand = new Command("attest", "Attestation operations for forensic artifacts."); var attestShowCommand = new Command("show", "Show attestation details including signer, timestamp, and subjects."); var attestArtifactArg = new Argument("artifact") { Description = "Path to attestation file (DSSE envelope)." }; var attestJsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; var attestTrustRootOption = new Option("--trust-root", "-r") { Description = "Path to trust root JSON file for signature verification." }; var attestVerifyOption = new Option("--verify") { Description = "Verify signatures against trust roots." }; attestShowCommand.Add(attestArtifactArg); attestShowCommand.Add(attestJsonOption); attestShowCommand.Add(attestTrustRootOption); attestShowCommand.Add(attestVerifyOption); attestShowCommand.SetAction((parseResult, _) => { var artifactPath = parseResult.GetValue(attestArtifactArg) ?? string.Empty; var emitJson = parseResult.GetValue(attestJsonOption); var trustRootPath = parseResult.GetValue(attestTrustRootOption); var verify = parseResult.GetValue(attestVerifyOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleForensicAttestShowAsync( services, artifactPath, emitJson, trustRootPath, verify, verbose, cancellationToken); }); attestCommand.Add(attestShowCommand); forensic.Add(attestCommand); return forensic; } // CLI-PROMO-70-001: Promotion commands private static Command BuildPromotionCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var promotion = new Command("promotion", "Build and manage promotion attestations."); // promotion assemble var assemble = new Command("assemble", "Assemble promotion attestation resolving image digests, hashing SBOM/VEX, and emitting stella.ops/promotion@v1 JSON."); var imageArg = new Argument("image") { Description = "Container image reference (e.g., registry.example.com/app:v1.0)." }; var sbomOption = new Option("--sbom", "-s") { Description = "Path to SBOM file (CycloneDX or SPDX)." }; var vexOption = new Option("--vex", "-v") { Description = "Path to VEX file (OpenVEX or CSAF)." }; var fromOption = new Option("--from") { Description = "Source environment (default: staging)." }; fromOption.SetDefaultValue("staging"); var toOption = new Option("--to") { Description = "Target environment (default: prod)." }; toOption.SetDefaultValue("prod"); var actorOption = new Option("--actor") { Description = "Actor performing the promotion (default: current user)." }; var pipelineOption = new Option("--pipeline") { Description = "CI/CD pipeline URL." }; var ticketOption = new Option("--ticket") { Description = "Issue tracker ticket reference (e.g., JIRA-1234)." }; var notesOption = new Option("--notes") { Description = "Additional notes about the promotion." }; var skipRekorOption = new Option("--skip-rekor") { Description = "Skip Rekor transparency log integration." }; var outputOption = new Option("--output", "-o") { Description = "Output path for the attestation JSON file." }; var jsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier." }; assemble.Add(imageArg); assemble.Add(sbomOption); assemble.Add(vexOption); assemble.Add(fromOption); assemble.Add(toOption); assemble.Add(actorOption); assemble.Add(pipelineOption); assemble.Add(ticketOption); assemble.Add(notesOption); assemble.Add(skipRekorOption); assemble.Add(outputOption); assemble.Add(jsonOption); assemble.Add(tenantOption); assemble.SetAction((parseResult, _) => { var image = parseResult.GetValue(imageArg) ?? string.Empty; var sbom = parseResult.GetValue(sbomOption); var vex = parseResult.GetValue(vexOption); var from = parseResult.GetValue(fromOption) ?? "staging"; var to = parseResult.GetValue(toOption) ?? "prod"; var actor = parseResult.GetValue(actorOption); var pipeline = parseResult.GetValue(pipelineOption); var ticket = parseResult.GetValue(ticketOption); var notes = parseResult.GetValue(notesOption); var skipRekor = parseResult.GetValue(skipRekorOption); var output = parseResult.GetValue(outputOption); var emitJson = parseResult.GetValue(jsonOption); var tenant = parseResult.GetValue(tenantOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePromotionAssembleAsync( services, image, sbom, vex, from, to, actor, pipeline, ticket, notes, skipRekor, output, emitJson, tenant, verbose, cancellationToken); }); promotion.Add(assemble); // CLI-PROMO-70-002: promotion attest var attest = new Command("attest", "Sign a promotion predicate and produce a DSSE bundle via Signer or cosign."); var attestPredicateArg = new Argument("predicate") { Description = "Path to the promotion predicate JSON file (output of 'promotion assemble')." }; var attestKeyOption = new Option("--key", "-k") { Description = "Signing key path or KMS key ID." }; var attestKeylessOption = new Option("--keyless") { Description = "Use keyless signing (Fulcio-based)." }; var attestNoRekorOption = new Option("--no-rekor") { Description = "Skip uploading to Rekor transparency log." }; var attestOutputOption = new Option("--output", "-o") { Description = "Output path for the DSSE bundle." }; var attestJsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; var attestTenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier for Signer API." }; attest.Add(attestPredicateArg); attest.Add(attestKeyOption); attest.Add(attestKeylessOption); attest.Add(attestNoRekorOption); attest.Add(attestOutputOption); attest.Add(attestJsonOption); attest.Add(attestTenantOption); attest.SetAction((parseResult, _) => { var predicatePath = parseResult.GetValue(attestPredicateArg) ?? string.Empty; var keyId = parseResult.GetValue(attestKeyOption); var useKeyless = parseResult.GetValue(attestKeylessOption); var noRekor = parseResult.GetValue(attestNoRekorOption); var output = parseResult.GetValue(attestOutputOption); var emitJson = parseResult.GetValue(attestJsonOption); var tenant = parseResult.GetValue(attestTenantOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePromotionAttestAsync( services, predicatePath, keyId, useKeyless, !noRekor, output, emitJson, tenant, verbose, cancellationToken); }); promotion.Add(attest); // CLI-PROMO-70-002: promotion verify var verify = new Command("verify", "Verify a promotion attestation bundle offline against trusted checkpoints."); var verifyBundleArg = new Argument("bundle") { Description = "Path to the DSSE bundle file." }; var verifySbomOption = new Option("--sbom") { Description = "Path to SBOM file for material verification." }; var verifyVexOption = new Option("--vex") { Description = "Path to VEX file for material verification." }; var verifyTrustRootOption = new Option("--trust-root") { Description = "Path to trusted certificate chain." }; var verifyCheckpointOption = new Option("--checkpoint") { Description = "Path to Rekor checkpoint for verification." }; var verifySkipSigOption = new Option("--skip-signature") { Description = "Skip signature verification." }; var verifySkipRekorOption = new Option("--skip-rekor") { Description = "Skip Rekor inclusion proof verification." }; var verifyJsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; var verifyTenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier." }; verify.Add(verifyBundleArg); verify.Add(verifySbomOption); verify.Add(verifyVexOption); verify.Add(verifyTrustRootOption); verify.Add(verifyCheckpointOption); verify.Add(verifySkipSigOption); verify.Add(verifySkipRekorOption); verify.Add(verifyJsonOption); verify.Add(verifyTenantOption); verify.SetAction((parseResult, _) => { var bundlePath = parseResult.GetValue(verifyBundleArg) ?? string.Empty; var sbom = parseResult.GetValue(verifySbomOption); var vex = parseResult.GetValue(verifyVexOption); var trustRoot = parseResult.GetValue(verifyTrustRootOption); var checkpoint = parseResult.GetValue(verifyCheckpointOption); var skipSig = parseResult.GetValue(verifySkipSigOption); var skipRekor = parseResult.GetValue(verifySkipRekorOption); var emitJson = parseResult.GetValue(verifyJsonOption); var tenant = parseResult.GetValue(verifyTenantOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePromotionVerifyAsync( services, bundlePath, sbom, vex, trustRoot, checkpoint, skipSig, skipRekor, emitJson, tenant, verbose, cancellationToken); }); promotion.Add(verify); return promotion; } // CLI-DETER-70-003: Determinism score commands private static Command BuildDetscoreCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var detscore = new Command("detscore", "Scanner determinism scoring harness for reproducibility testing."); // detscore run var run = new Command("run", "Run determinism harness with frozen clock, seeded RNG, and canonical hashes. Exits non-zero if score falls below threshold."); var imagesOption = new Option("--image", "-i") { Description = "Image digests to test (can be specified multiple times).", AllowMultipleArgumentsPerToken = true }; imagesOption.Required = true; var scannerOption = new Option("--scanner", "-s") { Description = "Scanner container image reference." }; scannerOption.Required = true; var policyBundleOption = new Option("--policy-bundle") { Description = "Path to policy bundle tarball." }; var feedsBundleOption = new Option("--feeds-bundle") { Description = "Path to feeds bundle tarball." }; var runsOption = new Option("--runs", "-n") { Description = "Number of runs per image (default: 10)." }; runsOption.SetDefaultValue(10); var fixedClockOption = new Option("--fixed-clock") { Description = "Fixed clock timestamp for deterministic execution (default: current UTC)." }; var rngSeedOption = new Option("--rng-seed") { Description = "RNG seed for deterministic execution (default: 1337)." }; rngSeedOption.SetDefaultValue(1337); var maxConcurrencyOption = new Option("--max-concurrency") { Description = "Maximum concurrency for scanner (default: 1 for determinism)." }; maxConcurrencyOption.SetDefaultValue(1); var memoryLimitOption = new Option("--memory") { Description = "Memory limit for container (default: 2G)." }; memoryLimitOption.SetDefaultValue("2G"); var cpuSetOption = new Option("--cpuset") { Description = "CPU set for container (default: 0)." }; cpuSetOption.SetDefaultValue("0"); var platformOption = new Option("--platform") { Description = "Platform (default: linux/amd64)." }; platformOption.SetDefaultValue("linux/amd64"); var imageThresholdOption = new Option("--image-threshold") { Description = "Minimum threshold for individual image scores (default: 0.90)." }; imageThresholdOption.SetDefaultValue(0.90); var overallThresholdOption = new Option("--overall-threshold") { Description = "Minimum threshold for overall score (default: 0.95)." }; overallThresholdOption.SetDefaultValue(0.95); var outputDirOption = new Option("--output-dir", "-o") { Description = "Output directory for determinism.json and run artifacts." }; var releaseOption = new Option("--release") { Description = "Release version string for the manifest." }; var jsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; run.Add(imagesOption); run.Add(scannerOption); run.Add(policyBundleOption); run.Add(feedsBundleOption); run.Add(runsOption); run.Add(fixedClockOption); run.Add(rngSeedOption); run.Add(maxConcurrencyOption); run.Add(memoryLimitOption); run.Add(cpuSetOption); run.Add(platformOption); run.Add(imageThresholdOption); run.Add(overallThresholdOption); run.Add(outputDirOption); run.Add(releaseOption); run.Add(jsonOption); run.Add(verboseOption); run.SetAction((parseResult, _) => { var images = parseResult.GetValue(imagesOption) ?? Array.Empty(); var scanner = parseResult.GetValue(scannerOption) ?? string.Empty; var policyBundle = parseResult.GetValue(policyBundleOption); var feedsBundle = parseResult.GetValue(feedsBundleOption); var runs = parseResult.GetValue(runsOption); var fixedClock = parseResult.GetValue(fixedClockOption); var rngSeed = parseResult.GetValue(rngSeedOption); var maxConcurrency = parseResult.GetValue(maxConcurrencyOption); var memoryLimit = parseResult.GetValue(memoryLimitOption) ?? "2G"; var cpuSet = parseResult.GetValue(cpuSetOption) ?? "0"; var platform = parseResult.GetValue(platformOption) ?? "linux/amd64"; var imageThreshold = parseResult.GetValue(imageThresholdOption); var overallThreshold = parseResult.GetValue(overallThresholdOption); var outputDir = parseResult.GetValue(outputDirOption); var release = parseResult.GetValue(releaseOption); var emitJson = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleDetscoreRunAsync( services, images, scanner, policyBundle, feedsBundle, runs, fixedClock, rngSeed, maxConcurrency, memoryLimit, cpuSet, platform, imageThreshold, overallThreshold, outputDir, release, emitJson, verbose, cancellationToken); }); detscore.Add(run); // CLI-DETER-70-004: detscore report var report = new Command("report", "Generate determinism score report from published determinism.json manifests for release notes and air-gap kits."); var manifestsArg = new Argument("manifests") { Description = "Paths to determinism.json manifest files.", Arity = ArgumentArity.OneOrMore }; var formatOption = new Option("--format", "-f") { Description = "Output format: markdown, json, csv (default: markdown)." }; formatOption.SetDefaultValue("markdown"); formatOption.FromAmong("markdown", "json", "csv"); var outputOption = new Option("--output", "-o") { Description = "Output file path. If omitted, writes to stdout." }; var detailsOption = new Option("--details") { Description = "Include per-image matrix and run details in output." }; var titleOption = new Option("--title") { Description = "Title for the report." }; var reportJsonOption = new Option("--json") { Description = "Equivalent to --format json for CI integration." }; report.Add(manifestsArg); report.Add(formatOption); report.Add(outputOption); report.Add(detailsOption); report.Add(titleOption); report.Add(reportJsonOption); report.Add(verboseOption); report.SetAction((parseResult, _) => { var manifests = parseResult.GetValue(manifestsArg) ?? Array.Empty(); var format = parseResult.GetValue(formatOption) ?? "markdown"; var output = parseResult.GetValue(outputOption); var details = parseResult.GetValue(detailsOption); var title = parseResult.GetValue(titleOption); var json = parseResult.GetValue(reportJsonOption); var verbose = parseResult.GetValue(verboseOption); // --json is shorthand for --format json if (json) { format = "json"; } return CommandHandlers.HandleDetscoreReportAsync( services, manifests, format, output, details, title, verbose, cancellationToken); }); detscore.Add(report); return detscore; } // CLI-OBS-51-001: Observability commands private static Command BuildObsCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var obs = new Command("obs", "Platform observability: service health, SLOs, burn-rate alerts, and metrics."); // obs top var top = new Command("top", "Stream service health metrics, SLO status, and burn-rate alerts (like 'top' for your platform)."); var servicesOption = new Option("--service", "-s") { Description = "Filter by service name (repeatable).", AllowMultipleArgumentsPerToken = true }; var tenantOption = new Option("--tenant", "-t") { Description = "Filter by tenant." }; var refreshOption = new Option("--refresh", "-r") { Description = "Refresh interval in seconds (0 = single fetch, default: 0)." }; refreshOption.SetDefaultValue(0); var includeQueuesOption = new Option("--queues") { Description = "Include queue health details (default: true)." }; includeQueuesOption.SetDefaultValue(true); var maxAlertsOption = new Option("--max-alerts") { Description = "Maximum number of alerts to display (default: 20)." }; maxAlertsOption.SetDefaultValue(20); var outputOption = new Option("--output", "-o") { Description = "Output format: table, json, ndjson (default: table)." }; outputOption.SetDefaultValue("table"); outputOption.FromAmong("table", "json", "ndjson"); var jsonOption = new Option("--json") { Description = "Equivalent to --output json for CI integration." }; var offlineOption = new Option("--offline") { Description = "Operate on cached data only; exit with code 5 if network access required." }; top.Add(servicesOption); top.Add(tenantOption); top.Add(refreshOption); top.Add(includeQueuesOption); top.Add(maxAlertsOption); top.Add(outputOption); top.Add(jsonOption); top.Add(offlineOption); top.Add(verboseOption); top.SetAction((parseResult, _) => { var serviceNames = parseResult.GetValue(servicesOption) ?? Array.Empty(); var tenant = parseResult.GetValue(tenantOption); var refresh = parseResult.GetValue(refreshOption); var includeQueues = parseResult.GetValue(includeQueuesOption); var maxAlerts = parseResult.GetValue(maxAlertsOption); var output = parseResult.GetValue(outputOption) ?? "table"; var json = parseResult.GetValue(jsonOption); var offline = parseResult.GetValue(offlineOption); var verbose = parseResult.GetValue(verboseOption); // --json is shorthand for --output json if (json) { output = "json"; } return CommandHandlers.HandleObsTopAsync( services, serviceNames, tenant, refresh, includeQueues, maxAlerts, output, offline, verbose, cancellationToken); }); obs.Add(top); // CLI-OBS-52-001: obs trace var trace = new Command("trace", "Fetch a distributed trace by ID with correlated spans and evidence links."); var traceIdArg = new Argument("trace_id") { Description = "The trace ID to fetch." }; var traceTenantOption = new Option("--tenant", "-t") { Description = "Filter by tenant." }; var includeEvidenceOption = new Option("--evidence") { Description = "Include evidence links (SBOM, VEX, attestations). Default: true." }; includeEvidenceOption.SetDefaultValue(true); var traceOutputOption = new Option("--output", "-o") { Description = "Output format: table, json (default: table)." }; traceOutputOption.SetDefaultValue("table"); traceOutputOption.FromAmong("table", "json"); var traceJsonOption = new Option("--json") { Description = "Equivalent to --output json." }; var traceOfflineOption = new Option("--offline") { Description = "Operate on cached data only; exit with code 5 if network access required." }; trace.Add(traceIdArg); trace.Add(traceTenantOption); trace.Add(includeEvidenceOption); trace.Add(traceOutputOption); trace.Add(traceJsonOption); trace.Add(traceOfflineOption); trace.Add(verboseOption); trace.SetAction((parseResult, _) => { var traceId = parseResult.GetValue(traceIdArg) ?? string.Empty; var tenant = parseResult.GetValue(traceTenantOption); var includeEvidence = parseResult.GetValue(includeEvidenceOption); var output = parseResult.GetValue(traceOutputOption) ?? "table"; var json = parseResult.GetValue(traceJsonOption); var offline = parseResult.GetValue(traceOfflineOption); var verbose = parseResult.GetValue(verboseOption); if (json) { output = "json"; } return CommandHandlers.HandleObsTraceAsync( services, traceId, tenant, includeEvidence, output, offline, verbose, cancellationToken); }); obs.Add(trace); // CLI-OBS-52-001: obs logs var logs = new Command("logs", "Fetch platform logs for a time window with pagination and filters."); var fromOption = new Option("--from") { Description = "Start timestamp (ISO-8601). Required." }; fromOption.Required = true; var toOption = new Option("--to") { Description = "End timestamp (ISO-8601). Required." }; toOption.Required = true; var logsTenantOption = new Option("--tenant", "-t") { Description = "Filter by tenant." }; var logsServicesOption = new Option("--service", "-s") { Description = "Filter by service name (repeatable).", AllowMultipleArgumentsPerToken = true }; var logsLevelsOption = new Option("--level", "-l") { Description = "Filter by log level: debug, info, warn, error (repeatable).", AllowMultipleArgumentsPerToken = true }; var logsQueryOption = new Option("--query", "-q") { Description = "Full-text search query." }; var logsPageSizeOption = new Option("--page-size") { Description = "Number of logs per page (default: 100, max: 500)." }; logsPageSizeOption.SetDefaultValue(100); var logsPageTokenOption = new Option("--page-token") { Description = "Pagination token for fetching next page." }; var logsOutputOption = new Option("--output", "-o") { Description = "Output format: table, json, ndjson (default: table)." }; logsOutputOption.SetDefaultValue("table"); logsOutputOption.FromAmong("table", "json", "ndjson"); var logsJsonOption = new Option("--json") { Description = "Equivalent to --output json." }; var logsOfflineOption = new Option("--offline") { Description = "Operate on cached data only; exit with code 5 if network access required." }; logs.Add(fromOption); logs.Add(toOption); logs.Add(logsTenantOption); logs.Add(logsServicesOption); logs.Add(logsLevelsOption); logs.Add(logsQueryOption); logs.Add(logsPageSizeOption); logs.Add(logsPageTokenOption); logs.Add(logsOutputOption); logs.Add(logsJsonOption); logs.Add(logsOfflineOption); logs.Add(verboseOption); logs.SetAction((parseResult, _) => { var from = parseResult.GetValue(fromOption); var to = parseResult.GetValue(toOption); var tenant = parseResult.GetValue(logsTenantOption); var serviceNames = parseResult.GetValue(logsServicesOption) ?? Array.Empty(); var levels = parseResult.GetValue(logsLevelsOption) ?? Array.Empty(); var query = parseResult.GetValue(logsQueryOption); var pageSize = parseResult.GetValue(logsPageSizeOption); var pageToken = parseResult.GetValue(logsPageTokenOption); var output = parseResult.GetValue(logsOutputOption) ?? "table"; var json = parseResult.GetValue(logsJsonOption); var offline = parseResult.GetValue(logsOfflineOption); var verbose = parseResult.GetValue(verboseOption); if (json) { output = "json"; } return CommandHandlers.HandleObsLogsAsync( services, from, to, tenant, serviceNames, levels, query, pageSize, pageToken, output, offline, verbose, cancellationToken); }); obs.Add(logs); // CLI-OBS-55-001: obs incident-mode var incidentMode = new Command("incident-mode", "Manage incident mode for enhanced forensic fidelity and retention."); // incident-mode enable var incidentEnable = new Command("enable", "Enable incident mode with extended retention and debug artefacts."); var enableTenantOption = new Option("--tenant", "-t") { Description = "Tenant scope for incident mode." }; var enableTtlOption = new Option("--ttl") { Description = "Time-to-live in minutes (default: 30). Mode auto-expires after TTL." }; enableTtlOption.SetDefaultValue(30); var enableRetentionOption = new Option("--retention-days") { Description = "Extended retention period in days (default: 60)." }; enableRetentionOption.SetDefaultValue(60); var enableReasonOption = new Option("--reason") { Description = "Reason for enabling incident mode (appears in audit log)." }; var enableJsonOption = new Option("--json") { Description = "Output as JSON." }; incidentEnable.Add(enableTenantOption); incidentEnable.Add(enableTtlOption); incidentEnable.Add(enableRetentionOption); incidentEnable.Add(enableReasonOption); incidentEnable.Add(enableJsonOption); incidentEnable.Add(verboseOption); incidentEnable.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(enableTenantOption); var ttl = parseResult.GetValue(enableTtlOption); var retention = parseResult.GetValue(enableRetentionOption); var reason = parseResult.GetValue(enableReasonOption); var json = parseResult.GetValue(enableJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleObsIncidentModeEnableAsync( services, tenant, ttl, retention, reason, json, verbose, cancellationToken); }); incidentMode.Add(incidentEnable); // incident-mode disable var incidentDisable = new Command("disable", "Disable incident mode and return to normal operation."); var disableTenantOption = new Option("--tenant", "-t") { Description = "Tenant scope for incident mode." }; var disableReasonOption = new Option("--reason") { Description = "Reason for disabling incident mode (appears in audit log)." }; var disableJsonOption = new Option("--json") { Description = "Output as JSON." }; incidentDisable.Add(disableTenantOption); incidentDisable.Add(disableReasonOption); incidentDisable.Add(disableJsonOption); incidentDisable.Add(verboseOption); incidentDisable.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(disableTenantOption); var reason = parseResult.GetValue(disableReasonOption); var json = parseResult.GetValue(disableJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleObsIncidentModeDisableAsync( services, tenant, reason, json, verbose, cancellationToken); }); incidentMode.Add(incidentDisable); // incident-mode status var incidentStatus = new Command("status", "Show current incident mode status."); var statusTenantOption = new Option("--tenant", "-t") { Description = "Tenant scope for incident mode." }; var statusJsonOption = new Option("--json") { Description = "Output as JSON." }; incidentStatus.Add(statusTenantOption); incidentStatus.Add(statusJsonOption); incidentStatus.Add(verboseOption); incidentStatus.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(statusTenantOption); var json = parseResult.GetValue(statusJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleObsIncidentModeStatusAsync( services, tenant, json, verbose, cancellationToken); }); incidentMode.Add(incidentStatus); obs.Add(incidentMode); return obs; } // CLI-PACKS-42-001: Task Pack commands private static Command BuildPackCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var pack = new Command("pack", "Task Pack operations: plan, run, push, pull, verify."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant scope for the operation." }; var jsonOption = new Option("--json") { Description = "Output as JSON." }; var offlineOption = new Option("--offline") { Description = "Offline mode - only use local cache (fails if not available)." }; // pack plan var plan = new Command("plan", "Plan a pack execution and validate inputs."); var planPackIdArg = new Argument("pack-id") { Description = "Pack identifier (e.g., stellaops/scanner-audit)." }; var planVersionOption = new Option("--version", "-v") { Description = "Pack version (defaults to latest)." }; var planInputsOption = new Option("--inputs", "-i") { Description = "Path to JSON file containing input values." }; var planDryRunOption = new Option("--dry-run") { Description = "Validate only, do not prepare for execution." }; var planOutputOption = new Option("--output", "-o") { Description = "Write plan to file." }; plan.Add(planPackIdArg); plan.Add(planVersionOption); plan.Add(planInputsOption); plan.Add(planDryRunOption); plan.Add(planOutputOption); plan.Add(tenantOption); plan.Add(jsonOption); plan.Add(offlineOption); plan.Add(verboseOption); plan.SetAction((parseResult, _) => { var packId = parseResult.GetValue(planPackIdArg) ?? string.Empty; var version = parseResult.GetValue(planVersionOption); var inputsPath = parseResult.GetValue(planInputsOption); var dryRun = parseResult.GetValue(planDryRunOption); var output = parseResult.GetValue(planOutputOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var offline = parseResult.GetValue(offlineOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackPlanAsync( services, packId, version, inputsPath, dryRun, output, tenant, json, offline, verbose, cancellationToken); }); pack.Add(plan); // pack run var run = new Command("run", "Execute a pack with the specified inputs."); var runPackIdArg = new Argument("pack-id") { Description = "Pack identifier (e.g., stellaops/scanner-audit)." }; var runVersionOption = new Option("--version", "-v") { Description = "Pack version (defaults to latest)." }; var runInputsOption = new Option("--inputs", "-i") { Description = "Path to JSON file containing input values." }; var runPlanIdOption = new Option("--plan-id") { Description = "Use a previously created plan instead of inputs." }; var runWaitOption = new Option("--wait", "-w") { Description = "Wait for pack execution to complete." }; var runTimeoutOption = new Option("--timeout") { Description = "Timeout in minutes when waiting for completion (default: 60)." }; runTimeoutOption.SetDefaultValue(60); var runLabelsOption = new Option("--label", "-l") { Description = "Labels to attach to the run (key=value format, repeatable).", Arity = ArgumentArity.ZeroOrMore }; runLabelsOption.AllowMultipleArgumentsPerToken = true; var runOutputOption = new Option("--output", "-o") { Description = "Write run result to file." }; run.Add(runPackIdArg); run.Add(runVersionOption); run.Add(runInputsOption); run.Add(runPlanIdOption); run.Add(runWaitOption); run.Add(runTimeoutOption); run.Add(runLabelsOption); run.Add(runOutputOption); run.Add(tenantOption); run.Add(jsonOption); run.Add(offlineOption); run.Add(verboseOption); run.SetAction((parseResult, _) => { var packId = parseResult.GetValue(runPackIdArg) ?? string.Empty; var version = parseResult.GetValue(runVersionOption); var inputsPath = parseResult.GetValue(runInputsOption); var planId = parseResult.GetValue(runPlanIdOption); var wait = parseResult.GetValue(runWaitOption); var timeout = parseResult.GetValue(runTimeoutOption); var labels = parseResult.GetValue(runLabelsOption) ?? Array.Empty(); var output = parseResult.GetValue(runOutputOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var offline = parseResult.GetValue(offlineOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackRunAsync( services, packId, version, inputsPath, planId, wait, timeout, labels, output, tenant, json, offline, verbose, cancellationToken); }); pack.Add(run); // pack push var push = new Command("push", "Push a pack to the registry."); var pushPathArg = new Argument("path") { Description = "Path to pack file (.tar.gz) or directory." }; var pushNameOption = new Option("--name", "-n") { Description = "Pack name (overrides manifest)." }; var pushVersionOption = new Option("--version", "-v") { Description = "Pack version (overrides manifest)." }; var pushSignOption = new Option("--sign") { Description = "Sign the pack before pushing." }; var pushKeyIdOption = new Option("--key-id") { Description = "Key ID to use for signing." }; var pushForceOption = new Option("--force", "-f") { Description = "Overwrite existing version." }; push.Add(pushPathArg); push.Add(pushNameOption); push.Add(pushVersionOption); push.Add(pushSignOption); push.Add(pushKeyIdOption); push.Add(pushForceOption); push.Add(tenantOption); push.Add(jsonOption); push.Add(verboseOption); push.SetAction((parseResult, _) => { var path = parseResult.GetValue(pushPathArg) ?? string.Empty; var name = parseResult.GetValue(pushNameOption); var version = parseResult.GetValue(pushVersionOption); var sign = parseResult.GetValue(pushSignOption); var keyId = parseResult.GetValue(pushKeyIdOption); var force = parseResult.GetValue(pushForceOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackPushAsync( services, path, name, version, sign, keyId, force, tenant, json, verbose, cancellationToken); }); pack.Add(push); // pack pull var pull = new Command("pull", "Pull a pack from the registry."); var pullPackIdArg = new Argument("pack-id") { Description = "Pack identifier (e.g., stellaops/scanner-audit)." }; var pullVersionOption = new Option("--version", "-v") { Description = "Pack version (defaults to latest)." }; var pullOutputOption = new Option("--output", "-o") { Description = "Output path for downloaded pack." }; var pullNoVerifyOption = new Option("--no-verify") { Description = "Skip signature verification." }; pull.Add(pullPackIdArg); pull.Add(pullVersionOption); pull.Add(pullOutputOption); pull.Add(pullNoVerifyOption); pull.Add(tenantOption); pull.Add(jsonOption); pull.Add(verboseOption); pull.SetAction((parseResult, _) => { var packId = parseResult.GetValue(pullPackIdArg) ?? string.Empty; var version = parseResult.GetValue(pullVersionOption); var output = parseResult.GetValue(pullOutputOption); var noVerify = parseResult.GetValue(pullNoVerifyOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackPullAsync( services, packId, version, output, noVerify, tenant, json, verbose, cancellationToken); }); pack.Add(pull); // pack verify var verify = new Command("verify", "Verify a pack's signature, digest, and schema."); var verifyPathOption = new Option("--path", "-p") { Description = "Path to local pack file to verify." }; var verifyPackIdOption = new Option("--pack-id") { Description = "Pack ID to verify from registry." }; var verifyVersionOption = new Option("--version", "-v") { Description = "Pack version to verify." }; var verifyDigestOption = new Option("--digest") { Description = "Expected digest to verify against." }; var verifyNoRekorOption = new Option("--no-rekor") { Description = "Skip Rekor transparency log verification." }; var verifyNoExpiryOption = new Option("--no-expiry") { Description = "Skip certificate expiry check." }; verify.Add(verifyPathOption); verify.Add(verifyPackIdOption); verify.Add(verifyVersionOption); verify.Add(verifyDigestOption); verify.Add(verifyNoRekorOption); verify.Add(verifyNoExpiryOption); verify.Add(tenantOption); verify.Add(jsonOption); verify.Add(verboseOption); verify.SetAction((parseResult, _) => { var path = parseResult.GetValue(verifyPathOption); var packId = parseResult.GetValue(verifyPackIdOption); var version = parseResult.GetValue(verifyVersionOption); var digest = parseResult.GetValue(verifyDigestOption); var noRekor = parseResult.GetValue(verifyNoRekorOption); var noExpiry = parseResult.GetValue(verifyNoExpiryOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackVerifyAsync( services, path, packId, version, digest, noRekor, noExpiry, tenant, json, verbose, cancellationToken); }); pack.Add(verify); // CLI-PACKS-43-001: Advanced pack features // pack runs list var runs = new Command("runs", "Manage pack runs."); var runsList = new Command("list", "List pack runs."); var runsListPackOption = new Option("--pack") { Description = "Filter by pack ID." }; var runsListStatusOption = new Option("--status", "-s") { Description = "Filter by status: pending, running, succeeded, failed, cancelled, waiting_approval." }; var runsListActorOption = new Option("--actor") { Description = "Filter by actor (who started the run)." }; var runsListSinceOption = new Option("--since") { Description = "Filter by start time (ISO-8601)." }; var runsListUntilOption = new Option("--until") { Description = "Filter by end time (ISO-8601)." }; var runsListPageSizeOption = new Option("--page-size") { Description = "Page size (default: 20)." }; runsListPageSizeOption.SetDefaultValue(20); var runsListPageTokenOption = new Option("--page-token") { Description = "Page token for pagination." }; runsList.Add(runsListPackOption); runsList.Add(runsListStatusOption); runsList.Add(runsListActorOption); runsList.Add(runsListSinceOption); runsList.Add(runsListUntilOption); runsList.Add(runsListPageSizeOption); runsList.Add(runsListPageTokenOption); runsList.Add(tenantOption); runsList.Add(jsonOption); runsList.Add(verboseOption); runsList.SetAction((parseResult, _) => { var packId = parseResult.GetValue(runsListPackOption); var status = parseResult.GetValue(runsListStatusOption); var actor = parseResult.GetValue(runsListActorOption); var since = parseResult.GetValue(runsListSinceOption); var until = parseResult.GetValue(runsListUntilOption); var pageSize = parseResult.GetValue(runsListPageSizeOption); var pageToken = parseResult.GetValue(runsListPageTokenOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackRunsListAsync( services, packId, status, actor, since, until, pageSize, pageToken, tenant, json, verbose, cancellationToken); }); runs.Add(runsList); // pack runs show var runsShow = new Command("show", "Show details of a pack run."); var runsShowIdArg = new Argument("run-id") { Description = "Run ID to show." }; runsShow.Add(runsShowIdArg); runsShow.Add(tenantOption); runsShow.Add(jsonOption); runsShow.Add(verboseOption); runsShow.SetAction((parseResult, _) => { var runId = parseResult.GetValue(runsShowIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackRunsShowAsync( services, runId, tenant, json, verbose, cancellationToken); }); runs.Add(runsShow); // pack runs cancel var runsCancel = new Command("cancel", "Cancel a running pack."); var runsCancelIdArg = new Argument("run-id") { Description = "Run ID to cancel." }; var runsCancelReasonOption = new Option("--reason") { Description = "Reason for cancellation (appears in audit log)." }; var runsCancelForceOption = new Option("--force") { Description = "Force cancel even if steps are running." }; runsCancel.Add(runsCancelIdArg); runsCancel.Add(runsCancelReasonOption); runsCancel.Add(runsCancelForceOption); runsCancel.Add(tenantOption); runsCancel.Add(jsonOption); runsCancel.Add(verboseOption); runsCancel.SetAction((parseResult, _) => { var runId = parseResult.GetValue(runsCancelIdArg) ?? string.Empty; var reason = parseResult.GetValue(runsCancelReasonOption); var force = parseResult.GetValue(runsCancelForceOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackRunsCancelAsync( services, runId, reason, force, tenant, json, verbose, cancellationToken); }); runs.Add(runsCancel); // pack runs pause var runsPause = new Command("pause", "Pause a pack run for approval."); var runsPauseIdArg = new Argument("run-id") { Description = "Run ID to pause." }; var runsPauseReasonOption = new Option("--reason") { Description = "Reason for pause (appears in audit log)." }; var runsPauseStepOption = new Option("--step") { Description = "Specific step to pause at (next step if not specified)." }; runsPause.Add(runsPauseIdArg); runsPause.Add(runsPauseReasonOption); runsPause.Add(runsPauseStepOption); runsPause.Add(tenantOption); runsPause.Add(jsonOption); runsPause.Add(verboseOption); runsPause.SetAction((parseResult, _) => { var runId = parseResult.GetValue(runsPauseIdArg) ?? string.Empty; var reason = parseResult.GetValue(runsPauseReasonOption); var stepId = parseResult.GetValue(runsPauseStepOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackRunsPauseAsync( services, runId, reason, stepId, tenant, json, verbose, cancellationToken); }); runs.Add(runsPause); // pack runs resume var runsResume = new Command("resume", "Resume a paused pack run."); var runsResumeIdArg = new Argument("run-id") { Description = "Run ID to resume." }; var runsResumeApproveOption = new Option("--approve") { Description = "Approve the pending step (default: true)." }; runsResumeApproveOption.SetDefaultValue(true); var runsResumeReasonOption = new Option("--reason") { Description = "Reason for approval decision (appears in audit log)." }; var runsResumeStepOption = new Option("--step") { Description = "Specific step to approve (current pending step if not specified)." }; runsResume.Add(runsResumeIdArg); runsResume.Add(runsResumeApproveOption); runsResume.Add(runsResumeReasonOption); runsResume.Add(runsResumeStepOption); runsResume.Add(tenantOption); runsResume.Add(jsonOption); runsResume.Add(verboseOption); runsResume.SetAction((parseResult, _) => { var runId = parseResult.GetValue(runsResumeIdArg) ?? string.Empty; var approve = parseResult.GetValue(runsResumeApproveOption); var reason = parseResult.GetValue(runsResumeReasonOption); var stepId = parseResult.GetValue(runsResumeStepOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackRunsResumeAsync( services, runId, approve, reason, stepId, tenant, json, verbose, cancellationToken); }); runs.Add(runsResume); // pack runs logs var runsLogs = new Command("logs", "Get logs for a pack run."); var runsLogsIdArg = new Argument("run-id") { Description = "Run ID to get logs for." }; var runsLogsStepOption = new Option("--step") { Description = "Filter logs by step ID." }; var runsLogsTailOption = new Option("--tail") { Description = "Show only the last N lines." }; var runsLogsSinceOption = new Option("--since") { Description = "Show logs since timestamp (ISO-8601)." }; runsLogs.Add(runsLogsIdArg); runsLogs.Add(runsLogsStepOption); runsLogs.Add(runsLogsTailOption); runsLogs.Add(runsLogsSinceOption); runsLogs.Add(tenantOption); runsLogs.Add(jsonOption); runsLogs.Add(verboseOption); runsLogs.SetAction((parseResult, _) => { var runId = parseResult.GetValue(runsLogsIdArg) ?? string.Empty; var stepId = parseResult.GetValue(runsLogsStepOption); var tail = parseResult.GetValue(runsLogsTailOption); var since = parseResult.GetValue(runsLogsSinceOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackRunsLogsAsync( services, runId, stepId, tail, since, tenant, json, verbose, cancellationToken); }); runs.Add(runsLogs); pack.Add(runs); // pack secrets inject var secrets = new Command("secrets", "Secret injection for pack runs."); var secretsInject = new Command("inject", "Inject a secret into a pack run."); var secretsInjectRunIdArg = new Argument("run-id") { Description = "Run ID to inject secret into." }; var secretsInjectRefOption = new Option("--secret-ref") { Description = "Secret reference (provider-specific path).", Required = true }; var secretsInjectProviderOption = new Option("--provider") { Description = "Secret provider: vault, aws-ssm, azure-keyvault, k8s-secret." }; secretsInjectProviderOption.SetDefaultValue("vault"); var secretsInjectEnvVarOption = new Option("--env-var") { Description = "Target environment variable name." }; var secretsInjectPathOption = new Option("--path") { Description = "Target file path within the run container." }; var secretsInjectStepOption = new Option("--step") { Description = "Inject for specific step only." }; secretsInject.Add(secretsInjectRunIdArg); secretsInject.Add(secretsInjectRefOption); secretsInject.Add(secretsInjectProviderOption); secretsInject.Add(secretsInjectEnvVarOption); secretsInject.Add(secretsInjectPathOption); secretsInject.Add(secretsInjectStepOption); secretsInject.Add(tenantOption); secretsInject.Add(jsonOption); secretsInject.Add(verboseOption); secretsInject.SetAction((parseResult, _) => { var runId = parseResult.GetValue(secretsInjectRunIdArg) ?? string.Empty; var secretRef = parseResult.GetValue(secretsInjectRefOption) ?? string.Empty; var provider = parseResult.GetValue(secretsInjectProviderOption) ?? "vault"; var envVar = parseResult.GetValue(secretsInjectEnvVarOption); var path = parseResult.GetValue(secretsInjectPathOption); var stepId = parseResult.GetValue(secretsInjectStepOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackSecretsInjectAsync( services, runId, secretRef, provider, envVar, path, stepId, tenant, json, verbose, cancellationToken); }); secrets.Add(secretsInject); pack.Add(secrets); // pack cache var cache = new Command("cache", "Manage offline pack cache."); // pack cache list var cacheList = new Command("list", "List cached packs."); var cacheDirOption = new Option("--cache-dir") { Description = "Cache directory path (uses default if not specified)." }; cacheList.Add(cacheDirOption); cacheList.Add(jsonOption); cacheList.Add(verboseOption); cacheList.SetAction((parseResult, _) => { var cacheDir = parseResult.GetValue(cacheDirOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackCacheListAsync( services, cacheDir, json, verbose, cancellationToken); }); cache.Add(cacheList); // pack cache add var cacheAdd = new Command("add", "Add a pack to the cache."); var cacheAddPackIdArg = new Argument("pack-id") { Description = "Pack ID to cache." }; var cacheAddVersionOption = new Option("--version", "-v") { Description = "Pack version (defaults to latest)." }; cacheAdd.Add(cacheAddPackIdArg); cacheAdd.Add(cacheAddVersionOption); cacheAdd.Add(cacheDirOption); cacheAdd.Add(tenantOption); cacheAdd.Add(jsonOption); cacheAdd.Add(verboseOption); cacheAdd.SetAction((parseResult, _) => { var packId = parseResult.GetValue(cacheAddPackIdArg) ?? string.Empty; var version = parseResult.GetValue(cacheAddVersionOption); var cacheDir = parseResult.GetValue(cacheDirOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackCacheAddAsync( services, packId, version, cacheDir, tenant, json, verbose, cancellationToken); }); cache.Add(cacheAdd); // pack cache prune var cachePrune = new Command("prune", "Remove old or unused packs from cache."); var cachePruneMaxAgeOption = new Option("--max-age-days") { Description = "Remove packs older than N days." }; var cachePruneMaxSizeOption = new Option("--max-size-mb") { Description = "Prune to keep cache under N megabytes." }; var cachePruneDryRunOption = new Option("--dry-run") { Description = "Preview what would be removed without actually pruning." }; cachePrune.Add(cachePruneMaxAgeOption); cachePrune.Add(cachePruneMaxSizeOption); cachePrune.Add(cachePruneDryRunOption); cachePrune.Add(cacheDirOption); cachePrune.Add(jsonOption); cachePrune.Add(verboseOption); cachePrune.SetAction((parseResult, _) => { var maxAgeDays = parseResult.GetValue(cachePruneMaxAgeOption); var maxSizeMb = parseResult.GetValue(cachePruneMaxSizeOption); var dryRun = parseResult.GetValue(cachePruneDryRunOption); var cacheDir = parseResult.GetValue(cacheDirOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePackCachePruneAsync( services, maxAgeDays, maxSizeMb, dryRun, cacheDir, json, verbose, cancellationToken); }); cache.Add(cachePrune); pack.Add(cache); return pack; } // CLI-EXC-25-001: Exception governance commands private static Command BuildExceptionsCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var exceptions = new Command("exceptions", "Exception governance: list, show, create, promote, revoke, import, export."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant scope for the operation." }; var jsonOption = new Option("--json") { Description = "Output as JSON." }; // exceptions list var list = new Command("list", "List exceptions with filters."); var listVulnOption = new Option("--vuln") { Description = "Filter by vulnerability ID (CVE or alias)." }; var listScopeTypeOption = new Option("--scope-type") { Description = "Filter by scope type: purl, image, component, tenant." }; var listScopeValueOption = new Option("--scope-value") { Description = "Filter by scope value (e.g., purl string, image ref)." }; var listStatusOption = new Option("--status", "-s") { Description = "Filter by status (repeatable): draft, staged, active, expired, revoked.", Arity = ArgumentArity.ZeroOrMore }; listStatusOption.AllowMultipleArgumentsPerToken = true; var listOwnerOption = new Option("--owner") { Description = "Filter by owner." }; var listEffectOption = new Option("--effect") { Description = "Filter by effect type: suppress, defer, downgrade, requireControl." }; var listExpiringOption = new Option("--expiring-within-days") { Description = "Show exceptions expiring within N days." }; var listIncludeExpiredOption = new Option("--include-expired") { Description = "Include expired exceptions in results." }; var listPageSizeOption = new Option("--page-size") { Description = "Results per page (default: 50)." }; listPageSizeOption.SetDefaultValue(50); var listPageTokenOption = new Option("--page-token") { Description = "Pagination token for next page." }; var listCsvOption = new Option("--csv") { Description = "Output as CSV." }; list.Add(listVulnOption); list.Add(listScopeTypeOption); list.Add(listScopeValueOption); list.Add(listStatusOption); list.Add(listOwnerOption); list.Add(listEffectOption); list.Add(listExpiringOption); list.Add(listIncludeExpiredOption); list.Add(listPageSizeOption); list.Add(listPageTokenOption); list.Add(tenantOption); list.Add(jsonOption); list.Add(listCsvOption); list.Add(verboseOption); list.SetAction((parseResult, _) => { var vuln = parseResult.GetValue(listVulnOption); var scopeType = parseResult.GetValue(listScopeTypeOption); var scopeValue = parseResult.GetValue(listScopeValueOption); var statuses = parseResult.GetValue(listStatusOption) ?? Array.Empty(); var owner = parseResult.GetValue(listOwnerOption); var effect = parseResult.GetValue(listEffectOption); var expiringDays = parseResult.GetValue(listExpiringOption); var includeExpired = parseResult.GetValue(listIncludeExpiredOption); var pageSize = parseResult.GetValue(listPageSizeOption); var pageToken = parseResult.GetValue(listPageTokenOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var csv = parseResult.GetValue(listCsvOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExceptionsListAsync( services, tenant, vuln, scopeType, scopeValue, statuses, owner, effect, expiringDays.HasValue ? DateTimeOffset.UtcNow.AddDays(expiringDays.Value) : null, includeExpired, pageSize, pageToken, json || csv, verbose, cancellationToken); }); exceptions.Add(list); // exceptions show var show = new Command("show", "Show exception details."); var showIdArg = new Argument("exception-id") { Description = "Exception ID to show." }; show.Add(showIdArg); show.Add(tenantOption); show.Add(jsonOption); show.Add(verboseOption); show.SetAction((parseResult, _) => { var exceptionId = parseResult.GetValue(showIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExceptionsShowAsync( services, exceptionId, tenant, json, verbose, cancellationToken); }); exceptions.Add(show); // exceptions create var create = new Command("create", "Create a new exception."); var createVulnOption = new Option("--vuln") { Description = "Vulnerability ID (CVE or alias).", Required = true }; var createScopeTypeOption = new Option("--scope-type") { Description = "Scope type: purl, image, component, tenant.", Required = true }; var createScopeValueOption = new Option("--scope-value") { Description = "Scope value (e.g., purl string, image ref).", Required = true }; var createEffectOption = new Option("--effect") { Description = "Effect ID to apply.", Required = true }; var createJustificationOption = new Option("--justification") { Description = "Justification for the exception.", Required = true }; var createOwnerOption = new Option("--owner") { Description = "Owner of the exception.", Required = true }; var createExpirationOption = new Option("--expiration") { Description = "Expiration date (ISO-8601) or relative (e.g., +30d, +90d)." }; var createEvidenceOption = new Option("--evidence") { Description = "Evidence reference (type:uri format, repeatable).", Arity = ArgumentArity.ZeroOrMore }; createEvidenceOption.AllowMultipleArgumentsPerToken = true; var createPolicyOption = new Option("--policy") { Description = "Policy binding (policy ID or version)." }; var createStageOption = new Option("--stage") { Description = "Create as staged (skip draft status)." }; create.Add(createVulnOption); create.Add(createScopeTypeOption); create.Add(createScopeValueOption); create.Add(createEffectOption); create.Add(createJustificationOption); create.Add(createOwnerOption); create.Add(createExpirationOption); create.Add(createEvidenceOption); create.Add(createPolicyOption); create.Add(createStageOption); create.Add(tenantOption); create.Add(jsonOption); create.Add(verboseOption); create.SetAction((parseResult, _) => { var vuln = parseResult.GetValue(createVulnOption) ?? string.Empty; var scopeType = parseResult.GetValue(createScopeTypeOption) ?? string.Empty; var scopeValue = parseResult.GetValue(createScopeValueOption) ?? string.Empty; var effect = parseResult.GetValue(createEffectOption) ?? string.Empty; var justification = parseResult.GetValue(createJustificationOption) ?? string.Empty; var owner = parseResult.GetValue(createOwnerOption) ?? string.Empty; var expirationStr = parseResult.GetValue(createExpirationOption); var expiration = !string.IsNullOrWhiteSpace(expirationStr) && DateTimeOffset.TryParse(expirationStr, out var exp) ? exp : (DateTimeOffset?)null; var evidence = parseResult.GetValue(createEvidenceOption) ?? Array.Empty(); var policy = parseResult.GetValue(createPolicyOption); var stage = parseResult.GetValue(createStageOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExceptionsCreateAsync( services, tenant ?? string.Empty, vuln, scopeType, scopeValue, effect, justification, owner ?? string.Empty, expiration, evidence, policy, stage, json, verbose, cancellationToken); }); exceptions.Add(create); // exceptions promote var promote = new Command("promote", "Promote exception to next lifecycle stage."); var promoteIdArg = new Argument("exception-id") { Description = "Exception ID to promote." }; var promoteTargetOption = new Option("--target") { Description = "Target status: staged or active (defaults to next stage)." }; var promoteCommentOption = new Option("--comment") { Description = "Comment for the promotion (appears in audit log)." }; promote.Add(promoteIdArg); promote.Add(promoteTargetOption); promote.Add(promoteCommentOption); promote.Add(tenantOption); promote.Add(jsonOption); promote.Add(verboseOption); promote.SetAction((parseResult, _) => { var exceptionId = parseResult.GetValue(promoteIdArg) ?? string.Empty; var target = parseResult.GetValue(promoteTargetOption); var comment = parseResult.GetValue(promoteCommentOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExceptionsPromoteAsync( services, exceptionId, tenant, target ?? "active", comment, json, verbose, cancellationToken); }); exceptions.Add(promote); // exceptions revoke var revoke = new Command("revoke", "Revoke an active exception."); var revokeIdArg = new Argument("exception-id") { Description = "Exception ID to revoke." }; var revokeReasonOption = new Option("--reason") { Description = "Reason for revocation (appears in audit log)." }; revoke.Add(revokeIdArg); revoke.Add(revokeReasonOption); revoke.Add(tenantOption); revoke.Add(jsonOption); revoke.Add(verboseOption); revoke.SetAction((parseResult, _) => { var exceptionId = parseResult.GetValue(revokeIdArg) ?? string.Empty; var reason = parseResult.GetValue(revokeReasonOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExceptionsRevokeAsync( services, exceptionId, reason, tenant, json, verbose, cancellationToken); }); exceptions.Add(revoke); // exceptions import var import = new Command("import", "Import exceptions from NDJSON file."); var importFileArg = new Argument("file") { Description = "Path to NDJSON file containing exceptions." }; var importStageOption = new Option("--stage") { Description = "Import as staged (default: true)." }; importStageOption.SetDefaultValue(true); var importSourceOption = new Option("--source") { Description = "Source label for imported exceptions." }; import.Add(importFileArg); import.Add(importStageOption); import.Add(importSourceOption); import.Add(tenantOption); import.Add(jsonOption); import.Add(verboseOption); import.SetAction((parseResult, _) => { var file = parseResult.GetValue(importFileArg) ?? string.Empty; var stage = parseResult.GetValue(importStageOption); var source = parseResult.GetValue(importSourceOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExceptionsImportAsync( services, tenant ?? string.Empty, file, stage, source, json, verbose, cancellationToken); }); exceptions.Add(import); // exceptions export var export = new Command("export", "Export exceptions to file."); var exportOutputOption = new Option("--output", "-o") { Description = "Output file path.", Required = true }; var exportStatusOption = new Option("--status", "-s") { Description = "Filter by status (repeatable).", Arity = ArgumentArity.ZeroOrMore }; exportStatusOption.AllowMultipleArgumentsPerToken = true; var exportFormatOption = new Option("--format") { Description = "Output format: ndjson or json (default: ndjson)." }; exportFormatOption.SetDefaultValue("ndjson"); var exportSignedOption = new Option("--signed") { Description = "Request signed export with attestation." }; export.Add(exportOutputOption); export.Add(exportStatusOption); export.Add(exportFormatOption); export.Add(exportSignedOption); export.Add(tenantOption); export.Add(verboseOption); export.SetAction((parseResult, _) => { var output = parseResult.GetValue(exportOutputOption) ?? string.Empty; var statuses = parseResult.GetValue(exportStatusOption) ?? Array.Empty(); var format = parseResult.GetValue(exportFormatOption) ?? "ndjson"; var signed = parseResult.GetValue(exportSignedOption); var tenant = parseResult.GetValue(tenantOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleExceptionsExportAsync( services, tenant, statuses, format, output, false, // includeManifest signed, false, // json output verbose, cancellationToken); }); exceptions.Add(export); return exceptions; } // CLI-ORCH-32-001: Orchestrator commands private static Command BuildOrchCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var orch = new Command("orch", "Interact with Source & Job Orchestrator."); // Common options var tenantOption = new Option("--tenant") { Description = "Tenant ID to scope the operation." }; var jsonOption = new Option("--json") { Description = "Output results as JSON." }; // sources subcommand group var sources = new Command("sources", "Manage orchestrator data sources."); // sources list var sourcesList = new Command("list", "List orchestrator sources."); var typeOption = new Option("--type") { Description = "Filter by source type (advisory, vex, sbom, package, registry, custom)." }; var statusOption = new Option("--status") { Description = "Filter by status (active, paused, disabled, throttled, error)." }; var enabledOption = new Option("--enabled") { Description = "Filter by enabled state." }; var hostOption = new Option("--host") { Description = "Filter by host name." }; var tagOption = new Option("--tag") { Description = "Filter by tag." }; var pageSizeOption = new Option("--page-size") { Description = "Number of results per page (default 50)." }; pageSizeOption.SetDefaultValue(50); var pageTokenOption = new Option("--page-token") { Description = "Page token for pagination." }; sourcesList.Add(tenantOption); sourcesList.Add(typeOption); sourcesList.Add(statusOption); sourcesList.Add(enabledOption); sourcesList.Add(hostOption); sourcesList.Add(tagOption); sourcesList.Add(pageSizeOption); sourcesList.Add(pageTokenOption); sourcesList.Add(jsonOption); sourcesList.Add(verboseOption); sourcesList.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var type = parseResult.GetValue(typeOption); var status = parseResult.GetValue(statusOption); var enabled = parseResult.GetValue(enabledOption); var host = parseResult.GetValue(hostOption); var tag = parseResult.GetValue(tagOption); var pageSize = parseResult.GetValue(pageSizeOption); var pageToken = parseResult.GetValue(pageTokenOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchSourcesListAsync( services, tenant, type, status, enabled, host, tag, pageSize, pageToken, json, verbose, cancellationToken); }); sources.Add(sourcesList); // sources show var sourcesShow = new Command("show", "Show details for a specific source."); var sourceIdArg = new Argument("source-id") { Description = "Source ID to show." }; sourcesShow.Add(sourceIdArg); sourcesShow.Add(tenantOption); sourcesShow.Add(jsonOption); sourcesShow.Add(verboseOption); sourcesShow.SetAction((parseResult, _) => { var sourceId = parseResult.GetValue(sourceIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchSourcesShowAsync( services, sourceId, tenant, json, verbose, cancellationToken); }); sources.Add(sourcesShow); // CLI-ORCH-33-001: sources test var sourcesTest = new Command("test", "Test connectivity to a source."); var testSourceIdArg = new Argument("source-id") { Description = "Source ID to test." }; var testTimeoutOption = new Option("--timeout") { Description = "Timeout in seconds (default 30)." }; testTimeoutOption.SetDefaultValue(30); sourcesTest.Add(testSourceIdArg); sourcesTest.Add(tenantOption); sourcesTest.Add(testTimeoutOption); sourcesTest.Add(jsonOption); sourcesTest.Add(verboseOption); sourcesTest.SetAction((parseResult, _) => { var sourceId = parseResult.GetValue(testSourceIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var timeout = parseResult.GetValue(testTimeoutOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchSourcesTestAsync( services, sourceId, tenant, timeout, json, verbose, cancellationToken); }); sources.Add(sourcesTest); // CLI-ORCH-33-001: sources pause var sourcesPause = new Command("pause", "Pause a source (stops scheduled runs)."); var pauseSourceIdArg = new Argument("source-id") { Description = "Source ID to pause." }; var pauseReasonOption = new Option("--reason") { Description = "Reason for pausing (appears in audit log)." }; var pauseDurationOption = new Option("--duration") { Description = "Duration in minutes before auto-resume (optional)." }; sourcesPause.Add(pauseSourceIdArg); sourcesPause.Add(tenantOption); sourcesPause.Add(pauseReasonOption); sourcesPause.Add(pauseDurationOption); sourcesPause.Add(jsonOption); sourcesPause.Add(verboseOption); sourcesPause.SetAction((parseResult, _) => { var sourceId = parseResult.GetValue(pauseSourceIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var reason = parseResult.GetValue(pauseReasonOption); var duration = parseResult.GetValue(pauseDurationOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchSourcesPauseAsync( services, sourceId, tenant, reason, duration, json, verbose, cancellationToken); }); sources.Add(sourcesPause); // CLI-ORCH-33-001: sources resume var sourcesResume = new Command("resume", "Resume a paused source."); var resumeSourceIdArg = new Argument("source-id") { Description = "Source ID to resume." }; var resumeReasonOption = new Option("--reason") { Description = "Reason for resuming (appears in audit log)." }; sourcesResume.Add(resumeSourceIdArg); sourcesResume.Add(tenantOption); sourcesResume.Add(resumeReasonOption); sourcesResume.Add(jsonOption); sourcesResume.Add(verboseOption); sourcesResume.SetAction((parseResult, _) => { var sourceId = parseResult.GetValue(resumeSourceIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var reason = parseResult.GetValue(resumeReasonOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchSourcesResumeAsync( services, sourceId, tenant, reason, json, verbose, cancellationToken); }); sources.Add(sourcesResume); orch.Add(sources); // CLI-ORCH-34-001: backfill command group var backfill = new Command("backfill", "Manage backfill operations for data sources."); // backfill start (wizard) var backfillStart = new Command("start", "Start a backfill operation for a source."); var backfillSourceIdArg = new Argument("source-id") { Description = "Source ID to backfill." }; var backfillFromOption = new Option("--from") { Description = "Start date/time for backfill (ISO 8601 format).", Required = true }; var backfillToOption = new Option("--to") { Description = "End date/time for backfill (ISO 8601 format).", Required = true }; var backfillDryRunOption = new Option("--dry-run") { Description = "Preview what would be backfilled without executing." }; var backfillPriorityOption = new Option("--priority") { Description = "Priority level 1-10 (default 5, higher = more resources)." }; backfillPriorityOption.SetDefaultValue(5); var backfillConcurrencyOption = new Option("--concurrency") { Description = "Number of concurrent workers (default 1)." }; backfillConcurrencyOption.SetDefaultValue(1); var backfillBatchSizeOption = new Option("--batch-size") { Description = "Items per batch (default 100)." }; backfillBatchSizeOption.SetDefaultValue(100); var backfillResumeOption = new Option("--resume") { Description = "Resume from last checkpoint if a previous backfill was interrupted." }; var backfillFilterOption = new Option("--filter") { Description = "Filter expression to limit items (source-specific syntax)." }; var backfillForceOption = new Option("--force") { Description = "Force backfill even if data already exists (overwrites)." }; backfillStart.Add(backfillSourceIdArg); backfillStart.Add(tenantOption); backfillStart.Add(backfillFromOption); backfillStart.Add(backfillToOption); backfillStart.Add(backfillDryRunOption); backfillStart.Add(backfillPriorityOption); backfillStart.Add(backfillConcurrencyOption); backfillStart.Add(backfillBatchSizeOption); backfillStart.Add(backfillResumeOption); backfillStart.Add(backfillFilterOption); backfillStart.Add(backfillForceOption); backfillStart.Add(jsonOption); backfillStart.Add(verboseOption); backfillStart.SetAction((parseResult, _) => { var sourceId = parseResult.GetValue(backfillSourceIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var from = parseResult.GetValue(backfillFromOption); var to = parseResult.GetValue(backfillToOption); var dryRun = parseResult.GetValue(backfillDryRunOption); var priority = parseResult.GetValue(backfillPriorityOption); var concurrency = parseResult.GetValue(backfillConcurrencyOption); var batchSize = parseResult.GetValue(backfillBatchSizeOption); var resume = parseResult.GetValue(backfillResumeOption); var filter = parseResult.GetValue(backfillFilterOption); var force = parseResult.GetValue(backfillForceOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchBackfillStartAsync( services, sourceId, tenant, from, to, dryRun, priority, concurrency, batchSize, resume, filter, force, json, verbose, cancellationToken); }); backfill.Add(backfillStart); // backfill status var backfillStatus = new Command("status", "Show status of a backfill operation."); var backfillIdArg = new Argument("backfill-id") { Description = "Backfill operation ID." }; backfillStatus.Add(backfillIdArg); backfillStatus.Add(tenantOption); backfillStatus.Add(jsonOption); backfillStatus.Add(verboseOption); backfillStatus.SetAction((parseResult, _) => { var backfillId = parseResult.GetValue(backfillIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchBackfillStatusAsync( services, backfillId, tenant, json, verbose, cancellationToken); }); backfill.Add(backfillStatus); // backfill list var backfillList = new Command("list", "List backfill operations."); var backfillSourceFilterOption = new Option("--source") { Description = "Filter by source ID." }; var backfillStatusFilterOption = new Option("--status") { Description = "Filter by status (pending, running, completed, failed, cancelled)." }; backfillList.Add(backfillSourceFilterOption); backfillList.Add(backfillStatusFilterOption); backfillList.Add(tenantOption); backfillList.Add(pageSizeOption); backfillList.Add(pageTokenOption); backfillList.Add(jsonOption); backfillList.Add(verboseOption); backfillList.SetAction((parseResult, _) => { var sourceId = parseResult.GetValue(backfillSourceFilterOption); var status = parseResult.GetValue(backfillStatusFilterOption); var tenant = parseResult.GetValue(tenantOption); var pageSize = parseResult.GetValue(pageSizeOption); var pageToken = parseResult.GetValue(pageTokenOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchBackfillListAsync( services, sourceId, status, tenant, pageSize, pageToken, json, verbose, cancellationToken); }); backfill.Add(backfillList); // backfill cancel var backfillCancel = new Command("cancel", "Cancel a running backfill operation."); var cancelBackfillIdArg = new Argument("backfill-id") { Description = "Backfill operation ID to cancel." }; var cancelReasonOption = new Option("--reason") { Description = "Reason for cancellation (appears in audit log)." }; backfillCancel.Add(cancelBackfillIdArg); backfillCancel.Add(tenantOption); backfillCancel.Add(cancelReasonOption); backfillCancel.Add(jsonOption); backfillCancel.Add(verboseOption); backfillCancel.SetAction((parseResult, _) => { var backfillId = parseResult.GetValue(cancelBackfillIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var reason = parseResult.GetValue(cancelReasonOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchBackfillCancelAsync( services, backfillId, tenant, reason, json, verbose, cancellationToken); }); backfill.Add(backfillCancel); orch.Add(backfill); // CLI-ORCH-34-001: quotas command group var quotas = new Command("quotas", "Manage resource quotas."); // quotas get var quotasGet = new Command("get", "Get current quota usage."); var quotaSourceOption = new Option("--source") { Description = "Filter by source ID." }; var quotaResourceTypeOption = new Option("--resource-type") { Description = "Filter by resource type (api_calls, data_ingested_bytes, items_processed, backfills, concurrent_jobs, storage_bytes)." }; quotasGet.Add(tenantOption); quotasGet.Add(quotaSourceOption); quotasGet.Add(quotaResourceTypeOption); quotasGet.Add(jsonOption); quotasGet.Add(verboseOption); quotasGet.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var sourceId = parseResult.GetValue(quotaSourceOption); var resourceType = parseResult.GetValue(quotaResourceTypeOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchQuotasGetAsync( services, tenant, sourceId, resourceType, json, verbose, cancellationToken); }); quotas.Add(quotasGet); // quotas set var quotasSet = new Command("set", "Set a quota limit."); var quotaSetTenantOption = new Option("--tenant") { Description = "Tenant ID.", Required = true }; var quotaSetResourceTypeOption = new Option("--resource-type") { Description = "Resource type (api_calls, data_ingested_bytes, items_processed, backfills, concurrent_jobs, storage_bytes).", Required = true }; var quotaSetLimitOption = new Option("--limit") { Description = "Quota limit value.", Required = true }; var quotaSetPeriodOption = new Option("--period") { Description = "Quota period (hourly, daily, weekly, monthly). Default: monthly." }; quotaSetPeriodOption.SetDefaultValue("monthly"); var quotaSetWarningThresholdOption = new Option("--warning-threshold") { Description = "Warning threshold as percentage (0.0-1.0). Default: 0.8." }; quotaSetWarningThresholdOption.SetDefaultValue(0.8); quotasSet.Add(quotaSetTenantOption); quotasSet.Add(quotaSourceOption); quotasSet.Add(quotaSetResourceTypeOption); quotasSet.Add(quotaSetLimitOption); quotasSet.Add(quotaSetPeriodOption); quotasSet.Add(quotaSetWarningThresholdOption); quotasSet.Add(jsonOption); quotasSet.Add(verboseOption); quotasSet.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(quotaSetTenantOption) ?? string.Empty; var sourceId = parseResult.GetValue(quotaSourceOption); var resourceType = parseResult.GetValue(quotaSetResourceTypeOption) ?? string.Empty; var limit = parseResult.GetValue(quotaSetLimitOption); var period = parseResult.GetValue(quotaSetPeriodOption) ?? "monthly"; var warningThreshold = parseResult.GetValue(quotaSetWarningThresholdOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchQuotasSetAsync( services, tenant, sourceId, resourceType, limit, period, warningThreshold, json, verbose, cancellationToken); }); quotas.Add(quotasSet); // quotas reset var quotasReset = new Command("reset", "Reset a quota's usage counter."); var quotaResetTenantOption = new Option("--tenant") { Description = "Tenant ID.", Required = true }; var quotaResetResourceTypeOption = new Option("--resource-type") { Description = "Resource type to reset.", Required = true }; var quotaResetReasonOption = new Option("--reason") { Description = "Reason for reset (appears in audit log)." }; quotasReset.Add(quotaResetTenantOption); quotasReset.Add(quotaSourceOption); quotasReset.Add(quotaResetResourceTypeOption); quotasReset.Add(quotaResetReasonOption); quotasReset.Add(jsonOption); quotasReset.Add(verboseOption); quotasReset.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(quotaResetTenantOption) ?? string.Empty; var sourceId = parseResult.GetValue(quotaSourceOption); var resourceType = parseResult.GetValue(quotaResetResourceTypeOption) ?? string.Empty; var reason = parseResult.GetValue(quotaResetReasonOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOrchQuotasResetAsync( services, tenant, sourceId, resourceType, reason, json, verbose, cancellationToken); }); quotas.Add(quotasReset); orch.Add(quotas); return orch; } // CLI-PARITY-41-001: SBOM command group private static Command BuildSbomCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var sbom = new Command("sbom", "Explore and manage Software Bill of Materials (SBOM) documents."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier (overrides profile/environment)." }; var jsonOption = new Option("--json") { Description = "Output as JSON for CI integration." }; // sbom list var list = new Command("list", "List SBOMs with filters and pagination."); var listImageRefOption = new Option("--image") { Description = "Filter by image reference (e.g., myregistry.io/app:v1)." }; var listDigestOption = new Option("--digest") { Description = "Filter by image digest (sha256:...)." }; var listFormatOption = new Option("--format") { Description = "Filter by SBOM format (spdx, cyclonedx)." }; var listCreatedAfterOption = new Option("--created-after") { Description = "Filter by creation date (ISO 8601)." }; var listCreatedBeforeOption = new Option("--created-before") { Description = "Filter by creation date (ISO 8601)." }; var listHasVulnsOption = new Option("--has-vulnerabilities") { Description = "Filter by vulnerability presence." }; var listLimitOption = new Option("--limit") { Description = "Maximum results (default 50)." }; listLimitOption.SetDefaultValue(50); var listOffsetOption = new Option("--offset") { Description = "Skip N results for pagination." }; var listCursorOption = new Option("--cursor") { Description = "Pagination cursor from previous response." }; list.Add(tenantOption); list.Add(listImageRefOption); list.Add(listDigestOption); list.Add(listFormatOption); list.Add(listCreatedAfterOption); list.Add(listCreatedBeforeOption); list.Add(listHasVulnsOption); list.Add(listLimitOption); list.Add(listOffsetOption); list.Add(listCursorOption); list.Add(jsonOption); list.Add(verboseOption); list.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var imageRef = parseResult.GetValue(listImageRefOption); var digest = parseResult.GetValue(listDigestOption); var format = parseResult.GetValue(listFormatOption); var createdAfter = parseResult.GetValue(listCreatedAfterOption); var createdBefore = parseResult.GetValue(listCreatedBeforeOption); var hasVulns = parseResult.GetValue(listHasVulnsOption); var limit = parseResult.GetValue(listLimitOption); var offset = parseResult.GetValue(listOffsetOption); var cursor = parseResult.GetValue(listCursorOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomListAsync( services, tenant, imageRef, digest, format, createdAfter, createdBefore, hasVulns, limit, offset, cursor, json, verbose, cancellationToken); }); sbom.Add(list); // sbom show var show = new Command("show", "Display detailed SBOM information including components, vulnerabilities, and licenses."); var showSbomIdArg = new Argument("sbom-id") { Description = "SBOM identifier." }; var showComponentsOption = new Option("--components") { Description = "Include component list." }; var showVulnsOption = new Option("--vulnerabilities") { Description = "Include vulnerability list." }; var showLicensesOption = new Option("--licenses") { Description = "Include license breakdown." }; var showExplainOption = new Option("--explain") { Description = "Include determinism factors and composition path." }; show.Add(showSbomIdArg); show.Add(tenantOption); show.Add(showComponentsOption); show.Add(showVulnsOption); show.Add(showLicensesOption); show.Add(showExplainOption); show.Add(jsonOption); show.Add(verboseOption); show.SetAction((parseResult, _) => { var sbomId = parseResult.GetValue(showSbomIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var includeComponents = parseResult.GetValue(showComponentsOption); var includeVulns = parseResult.GetValue(showVulnsOption); var includeLicenses = parseResult.GetValue(showLicensesOption); var explain = parseResult.GetValue(showExplainOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomShowAsync( services, sbomId, tenant, includeComponents, includeVulns, includeLicenses, explain, json, verbose, cancellationToken); }); sbom.Add(show); // sbom compare var compare = new Command("compare", "Compare two SBOMs to show component, vulnerability, and license differences."); var compareBaseArg = new Argument("base-sbom-id") { Description = "Base SBOM identifier (before)." }; var compareTargetArg = new Argument("target-sbom-id") { Description = "Target SBOM identifier (after)." }; var compareUnchangedOption = new Option("--include-unchanged") { Description = "Include unchanged items in output." }; compare.Add(compareBaseArg); compare.Add(compareTargetArg); compare.Add(tenantOption); compare.Add(compareUnchangedOption); compare.Add(jsonOption); compare.Add(verboseOption); compare.SetAction((parseResult, _) => { var baseSbomId = parseResult.GetValue(compareBaseArg) ?? string.Empty; var targetSbomId = parseResult.GetValue(compareTargetArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var includeUnchanged = parseResult.GetValue(compareUnchangedOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomCompareAsync( services, baseSbomId, targetSbomId, tenant, includeUnchanged, json, verbose, cancellationToken); }); sbom.Add(compare); // sbom export var export = new Command("export", "Export an SBOM in SPDX or CycloneDX format."); var exportSbomIdArg = new Argument("sbom-id") { Description = "SBOM identifier to export." }; var exportFormatOption = new Option("--format") { Description = "Export format (spdx, cyclonedx). Default: spdx." }; exportFormatOption.SetDefaultValue("spdx"); var exportVersionOption = new Option("--format-version") { Description = "Format version (e.g., 3.0.1 for SPDX, 1.6 for CycloneDX)." }; var exportOutputOption = new Option("--output", "-o") { Description = "Output file path. If not specified, writes to stdout." }; var exportSignedOption = new Option("--signed") { Description = "Request signed export with attestation." }; var exportVexOption = new Option("--include-vex") { Description = "Embed VEX information in the export." }; export.Add(exportSbomIdArg); export.Add(tenantOption); export.Add(exportFormatOption); export.Add(exportVersionOption); export.Add(exportOutputOption); export.Add(exportSignedOption); export.Add(exportVexOption); export.Add(verboseOption); export.SetAction((parseResult, _) => { var sbomId = parseResult.GetValue(exportSbomIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var format = parseResult.GetValue(exportFormatOption) ?? "spdx"; var formatVersion = parseResult.GetValue(exportVersionOption); var output = parseResult.GetValue(exportOutputOption); var signed = parseResult.GetValue(exportSignedOption); var includeVex = parseResult.GetValue(exportVexOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomExportAsync( services, sbomId, tenant, format, formatVersion, output, signed, includeVex, verbose, cancellationToken); }); sbom.Add(export); // sbom parity-matrix var parityMatrix = new Command("parity-matrix", "Show CLI command coverage and parity matrix."); parityMatrix.Add(tenantOption); parityMatrix.Add(jsonOption); parityMatrix.Add(verboseOption); parityMatrix.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomParityMatrixAsync( services, tenant, json, verbose, cancellationToken); }); sbom.Add(parityMatrix); return sbom; } // CLI-PARITY-41-002: Notify command group private static Command BuildNotifyCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var notify = new Command("notify", "Manage notification channels, rules, and deliveries."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier." }; var jsonOption = new Option("--json") { Description = "Output in JSON format." }; var limitOption = new Option("--limit", "-l") { Description = "Maximum number of items to return." }; var cursorOption = new Option("--cursor") { Description = "Pagination cursor for next page." }; // notify channels var channels = new Command("channels", "Manage notification channels."); // notify channels list var channelsList = new Command("list", "List notification channels."); var channelTypeOption = new Option("--type") { Description = "Filter by channel type (Slack, Teams, Email, Webhook, PagerDuty, OpsGenie)." }; var channelEnabledOption = new Option("--enabled") { Description = "Filter by enabled status." }; channelsList.Add(tenantOption); channelsList.Add(channelTypeOption); channelsList.Add(channelEnabledOption); channelsList.Add(limitOption); channelsList.Add(cursorOption); channelsList.Add(jsonOption); channelsList.Add(verboseOption); channelsList.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var channelType = parseResult.GetValue(channelTypeOption); var enabled = parseResult.GetValue(channelEnabledOption); var limit = parseResult.GetValue(limitOption); var cursor = parseResult.GetValue(cursorOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifyChannelsListAsync( services, tenant, channelType, enabled, limit, cursor, json, verbose, cancellationToken); }); channels.Add(channelsList); // notify channels show var channelsShow = new Command("show", "Show notification channel details."); var channelIdArg = new Argument("channel-id") { Description = "Channel identifier." }; channelsShow.Add(channelIdArg); channelsShow.Add(tenantOption); channelsShow.Add(jsonOption); channelsShow.Add(verboseOption); channelsShow.SetAction((parseResult, _) => { var channelId = parseResult.GetValue(channelIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifyChannelsShowAsync( services, channelId, tenant, json, verbose, cancellationToken); }); channels.Add(channelsShow); // notify channels test var channelsTest = new Command("test", "Test a notification channel by sending a test message."); var testMessageOption = new Option("--message", "-m") { Description = "Custom test message." }; channelsTest.Add(channelIdArg); channelsTest.Add(tenantOption); channelsTest.Add(testMessageOption); channelsTest.Add(jsonOption); channelsTest.Add(verboseOption); channelsTest.SetAction((parseResult, _) => { var channelId = parseResult.GetValue(channelIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var message = parseResult.GetValue(testMessageOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifyChannelsTestAsync( services, channelId, tenant, message, json, verbose, cancellationToken); }); channels.Add(channelsTest); notify.Add(channels); // notify rules var rules = new Command("rules", "Manage notification routing rules."); // notify rules list var rulesList = new Command("list", "List notification rules."); var ruleEnabledOption = new Option("--enabled") { Description = "Filter by enabled status." }; var ruleEventTypeOption = new Option("--event-type") { Description = "Filter by event type." }; var ruleChannelIdOption = new Option("--channel-id") { Description = "Filter by channel ID." }; rulesList.Add(tenantOption); rulesList.Add(ruleEnabledOption); rulesList.Add(ruleEventTypeOption); rulesList.Add(ruleChannelIdOption); rulesList.Add(limitOption); rulesList.Add(jsonOption); rulesList.Add(verboseOption); rulesList.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var enabled = parseResult.GetValue(ruleEnabledOption); var eventType = parseResult.GetValue(ruleEventTypeOption); var channelId = parseResult.GetValue(ruleChannelIdOption); var limit = parseResult.GetValue(limitOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifyRulesListAsync( services, tenant, enabled, eventType, channelId, limit, json, verbose, cancellationToken); }); rules.Add(rulesList); notify.Add(rules); // notify deliveries var deliveries = new Command("deliveries", "View and manage notification deliveries."); // notify deliveries list var deliveriesList = new Command("list", "List notification deliveries."); var deliveryStatusOption = new Option("--status") { Description = "Filter by status (Pending, Sent, Failed, Throttled, Digested, Dropped)." }; var deliveryEventTypeOption = new Option("--event-type") { Description = "Filter by event type." }; var deliveryChannelIdOption = new Option("--channel-id") { Description = "Filter by channel ID." }; var deliverySinceOption = new Option("--since") { Description = "Filter deliveries since this time (ISO 8601 format)." }; var deliveryUntilOption = new Option("--until") { Description = "Filter deliveries until this time (ISO 8601 format)." }; deliveriesList.Add(tenantOption); deliveriesList.Add(deliveryStatusOption); deliveriesList.Add(deliveryEventTypeOption); deliveriesList.Add(deliveryChannelIdOption); deliveriesList.Add(deliverySinceOption); deliveriesList.Add(deliveryUntilOption); deliveriesList.Add(limitOption); deliveriesList.Add(cursorOption); deliveriesList.Add(jsonOption); deliveriesList.Add(verboseOption); deliveriesList.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var status = parseResult.GetValue(deliveryStatusOption); var eventType = parseResult.GetValue(deliveryEventTypeOption); var channelId = parseResult.GetValue(deliveryChannelIdOption); var since = parseResult.GetValue(deliverySinceOption); var until = parseResult.GetValue(deliveryUntilOption); var limit = parseResult.GetValue(limitOption); var cursor = parseResult.GetValue(cursorOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifyDeliveriesListAsync( services, tenant, status, eventType, channelId, since, until, limit, cursor, json, verbose, cancellationToken); }); deliveries.Add(deliveriesList); // notify deliveries show var deliveriesShow = new Command("show", "Show notification delivery details."); var deliveryIdArg = new Argument("delivery-id") { Description = "Delivery identifier." }; deliveriesShow.Add(deliveryIdArg); deliveriesShow.Add(tenantOption); deliveriesShow.Add(jsonOption); deliveriesShow.Add(verboseOption); deliveriesShow.SetAction((parseResult, _) => { var deliveryId = parseResult.GetValue(deliveryIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifyDeliveriesShowAsync( services, deliveryId, tenant, json, verbose, cancellationToken); }); deliveries.Add(deliveriesShow); // notify deliveries retry var deliveriesRetry = new Command("retry", "Retry a failed notification delivery."); var idempotencyKeyOption = new Option("--idempotency-key") { Description = "Idempotency key to ensure retry is processed exactly once." }; deliveriesRetry.Add(deliveryIdArg); deliveriesRetry.Add(tenantOption); deliveriesRetry.Add(idempotencyKeyOption); deliveriesRetry.Add(jsonOption); deliveriesRetry.Add(verboseOption); deliveriesRetry.SetAction((parseResult, _) => { var deliveryId = parseResult.GetValue(deliveryIdArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var idempotencyKey = parseResult.GetValue(idempotencyKeyOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifyDeliveriesRetryAsync( services, deliveryId, tenant, idempotencyKey, json, verbose, cancellationToken); }); deliveries.Add(deliveriesRetry); notify.Add(deliveries); // notify send var send = new Command("send", "Send a notification."); var eventTypeArg = new Argument("event-type") { Description = "Event type for the notification." }; var bodyArg = new Argument("body") { Description = "Notification body/message." }; var sendChannelIdOption = new Option("--channel-id") { Description = "Target channel ID (if not using routing rules)." }; var sendSubjectOption = new Option("--subject", "-s") { Description = "Notification subject." }; var sendSeverityOption = new Option("--severity") { Description = "Severity level (info, warning, error, critical)." }; var sendMetadataOption = new Option("--metadata", "-m") { Description = "Additional metadata as key=value pairs.", AllowMultipleArgumentsPerToken = true }; var sendIdempotencyKeyOption = new Option("--idempotency-key") { Description = "Idempotency key to ensure notification is sent exactly once." }; send.Add(eventTypeArg); send.Add(bodyArg); send.Add(tenantOption); send.Add(sendChannelIdOption); send.Add(sendSubjectOption); send.Add(sendSeverityOption); send.Add(sendMetadataOption); send.Add(sendIdempotencyKeyOption); send.Add(jsonOption); send.Add(verboseOption); send.SetAction((parseResult, _) => { var eventType = parseResult.GetValue(eventTypeArg) ?? string.Empty; var body = parseResult.GetValue(bodyArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var channelId = parseResult.GetValue(sendChannelIdOption); var subject = parseResult.GetValue(sendSubjectOption); var severity = parseResult.GetValue(sendSeverityOption); var metadata = parseResult.GetValue(sendMetadataOption); var idempotencyKey = parseResult.GetValue(sendIdempotencyKeyOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleNotifySendAsync( services, eventType, body, tenant, channelId, subject, severity, metadata, idempotencyKey, json, verbose, cancellationToken); }); notify.Add(send); return notify; } // CLI-SBOM-60-001: Sbomer command group for layer/compose operations private static Command BuildSbomerCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var sbomer = new Command("sbomer", "SBOM composition: layer fragments, canonical merge, and Merkle verification."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier." }; var jsonOption = new Option("--json") { Description = "Output in JSON format." }; var scanIdOption = new Option("--scan-id") { Description = "Scan identifier." }; var imageRefOption = new Option("--image-ref") { Description = "Container image reference." }; var digestOption = new Option("--digest") { Description = "Container image digest." }; var offlineOption = new Option("--offline") { Description = "Run in offline mode using local files only." }; var verifiersPathOption = new Option("--verifiers-path") { Description = "Path to verifiers.json for DSSE signature verification." }; // sbomer layer var layer = new Command("layer", "Manage SBOM layer fragments."); // sbomer layer list var layerList = new Command("list", "List layer fragments for a scan."); var limitOption = new Option("--limit", "-l") { Description = "Maximum number of items to return." }; var cursorOption = new Option("--cursor") { Description = "Pagination cursor for next page." }; layerList.Add(tenantOption); layerList.Add(scanIdOption); layerList.Add(imageRefOption); layerList.Add(digestOption); layerList.Add(limitOption); layerList.Add(cursorOption); layerList.Add(jsonOption); layerList.Add(verboseOption); layerList.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var imageRef = parseResult.GetValue(imageRefOption); var digest = parseResult.GetValue(digestOption); var limit = parseResult.GetValue(limitOption); var cursor = parseResult.GetValue(cursorOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerLayerListAsync( services, tenant, scanId, imageRef, digest, limit, cursor, json, verbose, cancellationToken); }); layer.Add(layerList); // sbomer layer show var layerShow = new Command("show", "Show layer fragment details."); var layerDigestArg = new Argument("layer-digest") { Description = "Layer digest (sha256:...)." }; var includeComponentsOption = new Option("--components") { Description = "Include component list." }; var includeDsseOption = new Option("--dsse") { Description = "Include DSSE envelope details." }; layerShow.Add(layerDigestArg); layerShow.Add(tenantOption); layerShow.Add(scanIdOption); layerShow.Add(includeComponentsOption); layerShow.Add(includeDsseOption); layerShow.Add(jsonOption); layerShow.Add(verboseOption); layerShow.SetAction((parseResult, _) => { var layerDigest = parseResult.GetValue(layerDigestArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var includeComponents = parseResult.GetValue(includeComponentsOption); var includeDsse = parseResult.GetValue(includeDsseOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerLayerShowAsync( services, layerDigest, tenant, scanId, includeComponents, includeDsse, json, verbose, cancellationToken); }); layer.Add(layerShow); // sbomer layer verify var layerVerify = new Command("verify", "Verify layer fragment DSSE signature and content hash."); layerVerify.Add(layerDigestArg); layerVerify.Add(tenantOption); layerVerify.Add(scanIdOption); layerVerify.Add(verifiersPathOption); layerVerify.Add(offlineOption); layerVerify.Add(jsonOption); layerVerify.Add(verboseOption); layerVerify.SetAction((parseResult, _) => { var layerDigest = parseResult.GetValue(layerDigestArg) ?? string.Empty; var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var verifiersPath = parseResult.GetValue(verifiersPathOption); var offline = parseResult.GetValue(offlineOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerLayerVerifyAsync( services, layerDigest, tenant, scanId, verifiersPath, offline, json, verbose, cancellationToken); }); layer.Add(layerVerify); sbomer.Add(layer); // sbomer compose var compose = new Command("compose", "Compose SBOM from layer fragments with canonical ordering."); var outputPathOption = new Option("--output", "-o") { Description = "Output file path for composed SBOM." }; var formatOption = new Option("--format") { Description = "Output format (cyclonedx, spdx). Default: cyclonedx." }; var verifyFragmentsOption = new Option("--verify") { Description = "Verify all fragment DSSE signatures before composing." }; var emitManifestOption = new Option("--emit-manifest") { Description = "Emit _composition.json manifest. Default: true." }; emitManifestOption.SetDefaultValue(true); var emitMerkleOption = new Option("--emit-merkle") { Description = "Emit Merkle diagnostics file." }; compose.Add(tenantOption); compose.Add(scanIdOption); compose.Add(imageRefOption); compose.Add(digestOption); compose.Add(outputPathOption); compose.Add(formatOption); compose.Add(verifyFragmentsOption); compose.Add(verifiersPathOption); compose.Add(offlineOption); compose.Add(emitManifestOption); compose.Add(emitMerkleOption); compose.Add(jsonOption); compose.Add(verboseOption); compose.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var imageRef = parseResult.GetValue(imageRefOption); var digest = parseResult.GetValue(digestOption); var outputPath = parseResult.GetValue(outputPathOption); var format = parseResult.GetValue(formatOption); var verifyFragments = parseResult.GetValue(verifyFragmentsOption); var verifiersPath = parseResult.GetValue(verifiersPathOption); var offline = parseResult.GetValue(offlineOption); var emitManifest = parseResult.GetValue(emitManifestOption); var emitMerkle = parseResult.GetValue(emitMerkleOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerComposeAsync( services, tenant, scanId, imageRef, digest, outputPath, format, verifyFragments, verifiersPath, offline, emitManifest, emitMerkle, json, verbose, cancellationToken); }); sbomer.Add(compose); // sbomer composition var composition = new Command("composition", "View and verify composition manifests."); // sbomer composition show var compositionShow = new Command("show", "Show composition manifest details."); var compositionPathOption = new Option("--path") { Description = "Path to local _composition.json file." }; compositionShow.Add(tenantOption); compositionShow.Add(scanIdOption); compositionShow.Add(compositionPathOption); compositionShow.Add(jsonOption); compositionShow.Add(verboseOption); compositionShow.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var compositionPath = parseResult.GetValue(compositionPathOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerCompositionShowAsync( services, tenant, scanId, compositionPath, json, verbose, cancellationToken); }); composition.Add(compositionShow); // sbomer composition verify var compositionVerify = new Command("verify", "Verify composition against manifest and recompute Merkle root."); var sbomPathOption = new Option("--sbom-path") { Description = "Path to composed SBOM file to verify." }; var recomposeOption = new Option("--recompose") { Description = "Re-run composition locally and compare hashes." }; compositionVerify.Add(tenantOption); compositionVerify.Add(scanIdOption); compositionVerify.Add(compositionPathOption); compositionVerify.Add(sbomPathOption); compositionVerify.Add(verifiersPathOption); compositionVerify.Add(offlineOption); compositionVerify.Add(recomposeOption); compositionVerify.Add(jsonOption); compositionVerify.Add(verboseOption); compositionVerify.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var compositionPath = parseResult.GetValue(compositionPathOption); var sbomPath = parseResult.GetValue(sbomPathOption); var verifiersPath = parseResult.GetValue(verifiersPathOption); var offline = parseResult.GetValue(offlineOption); var recompose = parseResult.GetValue(recomposeOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerCompositionVerifyAsync( services, tenant, scanId, compositionPath, sbomPath, verifiersPath, offline, recompose, json, verbose, cancellationToken); }); composition.Add(compositionVerify); // sbomer composition merkle var compositionMerkle = new Command("merkle", "Show Merkle tree diagnostics for a composition."); compositionMerkle.Add(scanIdOption); compositionMerkle.Add(tenantOption); compositionMerkle.Add(jsonOption); compositionMerkle.Add(verboseOption); compositionMerkle.SetAction((parseResult, _) => { var scanId = parseResult.GetValue(scanIdOption); var tenant = parseResult.GetValue(tenantOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerCompositionMerkleAsync( services, scanId ?? string.Empty, tenant, json, verbose, cancellationToken); }); composition.Add(compositionMerkle); sbomer.Add(composition); // CLI-SBOM-60-002: sbomer drift var drift = new Command("drift", "Detect and explain determinism drift in SBOM composition."); // sbomer drift (analyze) var driftAnalyze = new Command("analyze", "Analyze drift between current SBOM and baseline, highlighting determinism breaks.") { Aliases = { "diff" } }; var baselineScanIdOption = new Option("--baseline-scan-id") { Description = "Baseline scan ID to compare against." }; var baselinePathOption = new Option("--baseline-path") { Description = "Path to baseline SBOM file." }; var sbomPathOptionDrift = new Option("--sbom-path") { Description = "Path to current SBOM file." }; var explainOption = new Option("--explain") { Description = "Provide detailed explanations for each drift, including root cause and remediation." }; var offlineKitPathOption = new Option("--offline-kit") { Description = "Path to offline kit bundle for air-gapped verification." }; driftAnalyze.Add(tenantOption); driftAnalyze.Add(scanIdOption); driftAnalyze.Add(baselineScanIdOption); driftAnalyze.Add(sbomPathOptionDrift); driftAnalyze.Add(baselinePathOption); driftAnalyze.Add(compositionPathOption); driftAnalyze.Add(explainOption); driftAnalyze.Add(offlineOption); driftAnalyze.Add(offlineKitPathOption); driftAnalyze.Add(jsonOption); driftAnalyze.Add(verboseOption); driftAnalyze.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var baselineScanId = parseResult.GetValue(baselineScanIdOption); var sbomPath = parseResult.GetValue(sbomPathOptionDrift); var baselinePath = parseResult.GetValue(baselinePathOption); var compositionPath = parseResult.GetValue(compositionPathOption); var explain = parseResult.GetValue(explainOption); var offline = parseResult.GetValue(offlineOption); var offlineKitPath = parseResult.GetValue(offlineKitPathOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerDriftAnalyzeAsync( services, tenant, scanId, baselineScanId, sbomPath, baselinePath, compositionPath, explain, offline, offlineKitPath, json, verbose, cancellationToken); }); drift.Add(driftAnalyze); // sbomer drift verify var driftVerify = new Command("verify", "Verify SBOM with local recomposition and drift detection from offline kit."); var recomposeLocallyOption = new Option("--recompose") { Description = "Re-run composition locally and compare hashes." }; var validateFragmentsOption = new Option("--validate-fragments") { Description = "Validate all fragment DSSE signatures." }; validateFragmentsOption.SetDefaultValue(true); var checkMerkleOption = new Option("--check-merkle") { Description = "Verify Merkle proofs for all fragments." }; checkMerkleOption.SetDefaultValue(true); driftVerify.Add(tenantOption); driftVerify.Add(scanIdOption); driftVerify.Add(sbomPathOptionDrift); driftVerify.Add(compositionPathOption); driftVerify.Add(verifiersPathOption); driftVerify.Add(offlineKitPathOption); driftVerify.Add(recomposeLocallyOption); driftVerify.Add(validateFragmentsOption); driftVerify.Add(checkMerkleOption); driftVerify.Add(jsonOption); driftVerify.Add(verboseOption); driftVerify.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(scanIdOption); var sbomPath = parseResult.GetValue(sbomPathOptionDrift); var compositionPath = parseResult.GetValue(compositionPathOption); var verifiersPath = parseResult.GetValue(verifiersPathOption); var offlineKitPath = parseResult.GetValue(offlineKitPathOption); var recomposeLocally = parseResult.GetValue(recomposeLocallyOption); var validateFragments = parseResult.GetValue(validateFragmentsOption); var checkMerkle = parseResult.GetValue(checkMerkleOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleSbomerDriftVerifyAsync( services, tenant, scanId, sbomPath, compositionPath, verifiersPath, offlineKitPath, recomposeLocally, validateFragments, checkMerkle, json, verbose, cancellationToken); }); drift.Add(driftVerify); sbomer.Add(drift); return sbomer; } // CLI-RISK-66-001 through CLI-RISK-68-001: Risk command group private static Command BuildRiskCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var risk = new Command("risk", "Manage risk profiles, scoring, and bundle verification."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier." }; var jsonOption = new Option("--json") { Description = "Output as JSON." }; // CLI-RISK-66-001: stella risk profile list var profile = new Command("profile", "Manage risk profiles."); var profileList = new Command("list", "List available risk profiles."); var includeDisabledOption = new Option("--include-disabled") { Description = "Include disabled profiles in the listing." }; var categoryOption = new Option("--category", "-c") { Description = "Filter by profile category." }; var limitOption = new Option("--limit", "-l") { Description = "Maximum number of results (default 100)." }; var offsetOption = new Option("--offset", "-o") { Description = "Pagination offset." }; profileList.Add(tenantOption); profileList.Add(includeDisabledOption); profileList.Add(categoryOption); profileList.Add(limitOption); profileList.Add(offsetOption); profileList.Add(jsonOption); profileList.Add(verboseOption); profileList.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var includeDisabled = parseResult.GetValue(includeDisabledOption); var category = parseResult.GetValue(categoryOption); var limit = parseResult.GetValue(limitOption); var offset = parseResult.GetValue(offsetOption); var emitJson = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRiskProfileListAsync( services, tenant, includeDisabled, category, limit, offset, emitJson, verbose, cancellationToken); }); profile.Add(profileList); risk.Add(profile); // CLI-RISK-66-002: stella risk simulate var simulate = new Command("simulate", "Simulate risk scoring against an SBOM or asset."); var profileIdOption = new Option("--profile-id", "-p") { Description = "Risk profile identifier to use for simulation." }; var sbomIdOption = new Option("--sbom-id") { Description = "SBOM identifier for risk evaluation." }; var sbomPathOption = new Option("--sbom-path") { Description = "Local path to SBOM file for risk evaluation." }; var assetIdOption = new Option("--asset-id", "-a") { Description = "Asset identifier for risk evaluation." }; var diffModeOption = new Option("--diff") { Description = "Enable diff mode to compare with baseline." }; var baselineProfileIdOption = new Option("--baseline-profile-id") { Description = "Baseline profile identifier for diff comparison." }; var csvOption = new Option("--csv") { Description = "Output as CSV." }; var outputOption = new Option("--output") { Description = "Write output to specified file path." }; simulate.Add(tenantOption); simulate.Add(profileIdOption); simulate.Add(sbomIdOption); simulate.Add(sbomPathOption); simulate.Add(assetIdOption); simulate.Add(diffModeOption); simulate.Add(baselineProfileIdOption); simulate.Add(jsonOption); simulate.Add(csvOption); simulate.Add(outputOption); simulate.Add(verboseOption); simulate.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var profileId = parseResult.GetValue(profileIdOption); var sbomId = parseResult.GetValue(sbomIdOption); var sbomPath = parseResult.GetValue(sbomPathOption); var assetId = parseResult.GetValue(assetIdOption); var diffMode = parseResult.GetValue(diffModeOption); var baselineProfileId = parseResult.GetValue(baselineProfileIdOption); var emitJson = parseResult.GetValue(jsonOption); var emitCsv = parseResult.GetValue(csvOption); var output = parseResult.GetValue(outputOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRiskSimulateAsync( services, tenant, profileId, sbomId, sbomPath, assetId, diffMode, baselineProfileId, emitJson, emitCsv, output, verbose, cancellationToken); }); risk.Add(simulate); // CLI-RISK-67-001: stella risk results var results = new Command("results", "Get risk evaluation results."); var resultsAssetIdOption = new Option("--asset-id", "-a") { Description = "Filter by asset identifier." }; var resultsSbomIdOption = new Option("--sbom-id") { Description = "Filter by SBOM identifier." }; var resultsProfileIdOption = new Option("--profile-id", "-p") { Description = "Filter by risk profile identifier." }; var minSeverityOption = new Option("--min-severity") { Description = "Minimum severity threshold (critical, high, medium, low, info)." }; var maxScoreOption = new Option("--max-score") { Description = "Maximum score threshold (0-100)." }; var includeExplainOption = new Option("--explain") { Description = "Include explainability information in results." }; results.Add(tenantOption); results.Add(resultsAssetIdOption); results.Add(resultsSbomIdOption); results.Add(resultsProfileIdOption); results.Add(minSeverityOption); results.Add(maxScoreOption); results.Add(includeExplainOption); results.Add(limitOption); results.Add(offsetOption); results.Add(jsonOption); results.Add(csvOption); results.Add(verboseOption); results.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var assetId = parseResult.GetValue(resultsAssetIdOption); var sbomId = parseResult.GetValue(resultsSbomIdOption); var profileId = parseResult.GetValue(resultsProfileIdOption); var minSeverity = parseResult.GetValue(minSeverityOption); var maxScore = parseResult.GetValue(maxScoreOption); var includeExplain = parseResult.GetValue(includeExplainOption); var limit = parseResult.GetValue(limitOption); var offset = parseResult.GetValue(offsetOption); var emitJson = parseResult.GetValue(jsonOption); var emitCsv = parseResult.GetValue(csvOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRiskResultsAsync( services, tenant, assetId, sbomId, profileId, minSeverity, maxScore, includeExplain, limit, offset, emitJson, emitCsv, verbose, cancellationToken); }); risk.Add(results); // CLI-RISK-68-001: stella risk bundle verify var bundle = new Command("bundle", "Risk bundle operations."); var bundleVerify = new Command("verify", "Verify a risk bundle for integrity and signatures."); var bundlePathOption = new Option("--bundle-path", "-b") { Description = "Path to the risk bundle file.", Required = true }; var signaturePathOption = new Option("--signature-path", "-s") { Description = "Path to detached signature file." }; var checkRekorOption = new Option("--check-rekor") { Description = "Verify transparency log entry in Sigstore Rekor." }; bundleVerify.Add(tenantOption); bundleVerify.Add(bundlePathOption); bundleVerify.Add(signaturePathOption); bundleVerify.Add(checkRekorOption); bundleVerify.Add(jsonOption); bundleVerify.Add(verboseOption); bundleVerify.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var bundlePath = parseResult.GetValue(bundlePathOption) ?? string.Empty; var signaturePath = parseResult.GetValue(signaturePathOption); var checkRekor = parseResult.GetValue(checkRekorOption); var emitJson = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleRiskBundleVerifyAsync( services, tenant, bundlePath, signaturePath, checkRekor, emitJson, verbose, cancellationToken); }); bundle.Add(bundleVerify); risk.Add(bundle); return risk; } // CLI-SIG-26-001: Reachability command group private static Command BuildReachabilityCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var reachability = new Command("reachability", "Reachability analysis for vulnerability exploitability."); // Common options var tenantOption = new Option("--tenant", "-t") { Description = "Tenant identifier." }; var jsonOption = new Option("--json") { Description = "Output as JSON." }; // stella reachability upload-callgraph var uploadCallGraph = new Command("upload-callgraph", "Upload a call graph for reachability analysis."); var callGraphPathOption = new Option("--path", "-p") { Description = "Path to the call graph file.", Required = true }; var scanIdOption = new Option("--scan-id") { Description = "Scan identifier to associate with the call graph." }; var assetIdOption = new Option("--asset-id", "-a") { Description = "Asset identifier to associate with the call graph." }; var formatOption = new Option("--format", "-f") { Description = "Call graph format (auto, json, proto, dot). Default: auto-detect." }; uploadCallGraph.Add(tenantOption); uploadCallGraph.Add(callGraphPathOption); uploadCallGraph.Add(scanIdOption); uploadCallGraph.Add(assetIdOption); uploadCallGraph.Add(formatOption); uploadCallGraph.Add(jsonOption); uploadCallGraph.Add(verboseOption); uploadCallGraph.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var callGraphPath = parseResult.GetValue(callGraphPathOption) ?? string.Empty; var scanId = parseResult.GetValue(scanIdOption); var assetId = parseResult.GetValue(assetIdOption); var format = parseResult.GetValue(formatOption); var emitJson = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleReachabilityUploadCallGraphAsync( services, tenant, callGraphPath, scanId, assetId, format, emitJson, verbose, cancellationToken); }); reachability.Add(uploadCallGraph); // stella reachability list var list = new Command("list", "List reachability analyses."); var listScanIdOption = new Option("--scan-id") { Description = "Filter by scan identifier." }; var listAssetIdOption = new Option("--asset-id", "-a") { Description = "Filter by asset identifier." }; var statusOption = new Option("--status") { Description = "Filter by status (pending, processing, completed, failed)." }; var limitOption = new Option("--limit", "-l") { Description = "Maximum number of results (default 100)." }; var offsetOption = new Option("--offset", "-o") { Description = "Pagination offset." }; list.Add(tenantOption); list.Add(listScanIdOption); list.Add(listAssetIdOption); list.Add(statusOption); list.Add(limitOption); list.Add(offsetOption); list.Add(jsonOption); list.Add(verboseOption); list.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var scanId = parseResult.GetValue(listScanIdOption); var assetId = parseResult.GetValue(listAssetIdOption); var status = parseResult.GetValue(statusOption); var limit = parseResult.GetValue(limitOption); var offset = parseResult.GetValue(offsetOption); var emitJson = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleReachabilityListAsync( services, tenant, scanId, assetId, status, limit, offset, emitJson, verbose, cancellationToken); }); reachability.Add(list); // stella reachability explain var explain = new Command("explain", "Explain reachability for a vulnerability or package."); var analysisIdOption = new Option("--analysis-id", "-i") { Description = "Analysis identifier.", Required = true }; var vulnerabilityIdOption = new Option("--vuln-id", "-v") { Description = "Vulnerability identifier to explain." }; var packagePurlOption = new Option("--purl") { Description = "Package URL to explain." }; var includeCallPathsOption = new Option("--call-paths") { Description = "Include detailed call paths in the explanation." }; explain.Add(tenantOption); explain.Add(analysisIdOption); explain.Add(vulnerabilityIdOption); explain.Add(packagePurlOption); explain.Add(includeCallPathsOption); explain.Add(jsonOption); explain.Add(verboseOption); explain.SetAction((parseResult, _) => { var tenant = parseResult.GetValue(tenantOption); var analysisId = parseResult.GetValue(analysisIdOption) ?? string.Empty; var vulnerabilityId = parseResult.GetValue(vulnerabilityIdOption); var packagePurl = parseResult.GetValue(packagePurlOption); var includeCallPaths = parseResult.GetValue(includeCallPathsOption); var emitJson = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleReachabilityExplainAsync( services, tenant, analysisId, vulnerabilityId, packagePurl, includeCallPaths, emitJson, verbose, cancellationToken); }); reachability.Add(explain); return reachability; } // CLI-SDK-63-001: stella api command private static Command BuildApiCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var api = new Command("api", "API management commands."); // stella api spec var spec = new Command("spec", "API specification operations."); // stella api spec list var list = new Command("list", "List available API specifications."); var tenantOption = new Option("--tenant", "-t") { Description = "Tenant context for the operation." }; var emitJsonOption = new Option("--json") { Description = "Output in JSON format." }; list.Add(tenantOption); list.Add(emitJsonOption); list.Add(verboseOption); list.SetAction(async (parseResult, ct) => { var tenant = parseResult.GetValue(tenantOption); var emitJson = parseResult.GetValue(emitJsonOption); var verbose = parseResult.GetValue(verboseOption); await CommandHandlers.HandleApiSpecListAsync( services, tenant, emitJson, verbose, cancellationToken); }); spec.Add(list); // stella api spec download var download = new Command("download", "Download API specification."); var outputOption = new Option("--output", "-o") { Description = "Output path for the downloaded spec (file or directory).", Required = true }; var serviceOption = new Option("--service", "-s") { Description = "Service to download spec for (e.g., concelier, scanner, policy). Omit for aggregate spec." }; var formatOption = new Option("--format", "-f") { Description = "Output format: openapi-json (default) or openapi-yaml." }; formatOption.SetDefaultValue("openapi-json"); var overwriteOption = new Option("--overwrite") { Description = "Overwrite existing file if present." }; var etagOption = new Option("--etag") { Description = "Expected ETag for conditional download (If-None-Match)." }; var checksumOption = new Option("--checksum") { Description = "Expected checksum for verification after download." }; var checksumAlgoOption = new Option("--checksum-algorithm") { Description = "Checksum algorithm: sha256 (default), sha384, sha512." }; checksumAlgoOption.SetDefaultValue("sha256"); download.Add(tenantOption); download.Add(outputOption); download.Add(serviceOption); download.Add(formatOption); download.Add(overwriteOption); download.Add(etagOption); download.Add(checksumOption); download.Add(checksumAlgoOption); download.Add(emitJsonOption); download.Add(verboseOption); download.SetAction(async (parseResult, ct) => { var tenant = parseResult.GetValue(tenantOption); var output = parseResult.GetValue(outputOption)!; var service = parseResult.GetValue(serviceOption); var format = parseResult.GetValue(formatOption)!; var overwrite = parseResult.GetValue(overwriteOption); var etag = parseResult.GetValue(etagOption); var checksum = parseResult.GetValue(checksumOption); var checksumAlgo = parseResult.GetValue(checksumAlgoOption)!; var emitJson = parseResult.GetValue(emitJsonOption); var verbose = parseResult.GetValue(verboseOption); await CommandHandlers.HandleApiSpecDownloadAsync( services, tenant, output, service, format, overwrite, etag, checksum, checksumAlgo, emitJson, verbose, cancellationToken); }); spec.Add(download); api.Add(spec); return api; } // CLI-SDK-64-001: stella sdk command private static Command BuildSdkCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var sdk = new Command("sdk", "SDK management commands."); // stella sdk update var update = new Command("update", "Check for SDK updates and fetch latest manifests/changelogs."); var tenantOption = new Option("--tenant", "-t") { Description = "Tenant context for the operation." }; var languageOption = new Option("--language", "-l") { Description = "SDK language filter (typescript, go, csharp, python, java). Omit for all." }; var checkOnlyOption = new Option("--check-only") { Description = "Only check for updates, don't download." }; var showChangelogOption = new Option("--changelog") { Description = "Show changelog for available updates." }; var showDeprecationsOption = new Option("--deprecations") { Description = "Show deprecation notices." }; var emitJsonOption = new Option("--json") { Description = "Output in JSON format." }; update.Add(tenantOption); update.Add(languageOption); update.Add(checkOnlyOption); update.Add(showChangelogOption); update.Add(showDeprecationsOption); update.Add(emitJsonOption); update.Add(verboseOption); update.SetAction(async (parseResult, ct) => { var tenant = parseResult.GetValue(tenantOption); var language = parseResult.GetValue(languageOption); var checkOnly = parseResult.GetValue(checkOnlyOption); var showChangelog = parseResult.GetValue(showChangelogOption); var showDeprecations = parseResult.GetValue(showDeprecationsOption); var emitJson = parseResult.GetValue(emitJsonOption); var verbose = parseResult.GetValue(verboseOption); await CommandHandlers.HandleSdkUpdateAsync( services, tenant, language, checkOnly, showChangelog, showDeprecations, emitJson, verbose, cancellationToken); }); sdk.Add(update); // stella sdk list var list = new Command("list", "List installed SDK versions."); list.Add(tenantOption); list.Add(languageOption); list.Add(emitJsonOption); list.Add(verboseOption); list.SetAction(async (parseResult, ct) => { var tenant = parseResult.GetValue(tenantOption); var language = parseResult.GetValue(languageOption); var emitJson = parseResult.GetValue(emitJsonOption); var verbose = parseResult.GetValue(verboseOption); await CommandHandlers.HandleSdkListAsync( services, tenant, language, emitJson, verbose, cancellationToken); }); sdk.Add(list); return sdk; } private static Command BuildMirrorCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var mirror = new Command("mirror", "Manage air-gap mirror bundles for offline distribution."); // mirror create var create = new Command("create", "Create an air-gap mirror bundle."); var domainOption = new Option("--domain", new[] { "-d" }) { Description = "Domain identifier (e.g., vex-advisories, vulnerability-feeds, policy-packs).", Required = true }; var outputOption = new Option("--output", new[] { "-o" }) { Description = "Output directory for the bundle files.", Required = true }; var formatOption = new Option("--format", new[] { "-f" }) { Description = "Export format filter (openvex, csaf, cyclonedx, spdx, ndjson, json)." }; var tenantOption = new Option("--tenant") { Description = "Tenant scope for the exports." }; var displayNameOption = new Option("--display-name") { Description = "Human-readable display name for the bundle." }; var targetRepoOption = new Option("--target-repository") { Description = "Target OCI repository URI for this bundle." }; var providersOption = new Option("--provider", new[] { "-p" }) { Description = "Provider filter for VEX exports (can be specified multiple times).", AllowMultipleArgumentsPerToken = true }; var signOption = new Option("--sign") { Description = "Include DSSE signatures in the bundle." }; var attestOption = new Option("--attest") { Description = "Include attestation metadata in the bundle." }; var jsonOption = new Option("--json") { Description = "Output result in JSON format." }; create.Add(domainOption); create.Add(outputOption); create.Add(formatOption); create.Add(tenantOption); create.Add(displayNameOption); create.Add(targetRepoOption); create.Add(providersOption); create.Add(signOption); create.Add(attestOption); create.Add(jsonOption); create.SetAction((parseResult, _) => { var domain = parseResult.GetValue(domainOption) ?? string.Empty; var output = parseResult.GetValue(outputOption) ?? string.Empty; var format = parseResult.GetValue(formatOption); var tenant = parseResult.GetValue(tenantOption); var displayName = parseResult.GetValue(displayNameOption); var targetRepo = parseResult.GetValue(targetRepoOption); var providers = parseResult.GetValue(providersOption); var sign = parseResult.GetValue(signOption); var attest = parseResult.GetValue(attestOption); var json = parseResult.GetValue(jsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleMirrorCreateAsync( services, domain, output, format, tenant, displayName, targetRepo, providers?.ToList(), sign, attest, json, verbose, cancellationToken); }); mirror.Add(create); return mirror; } private static Command BuildAirgapCommand(IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var airgap = new Command("airgap", "Manage air-gapped environment operations."); // airgap import (CLI-AIRGAP-57-001) var import = new Command("import", "Import an air-gap mirror bundle into the local data store."); var bundlePathOption = new Option("--bundle", new[] { "-b" }) { Description = "Path to the bundle directory (contains manifest.json and artifacts).", Required = true }; var importTenantOption = new Option("--tenant") { Description = "Import data under a specific tenant scope." }; var globalOption = new Option("--global") { Description = "Import data to the global scope (requires elevated permissions)." }; var dryRunOption = new Option("--dry-run") { Description = "Preview the import without making changes." }; var forceOption = new Option("--force") { Description = "Force import even if checksums have been verified before." }; var verifyOnlyOption = new Option("--verify-only") { Description = "Verify bundle integrity without importing." }; var importJsonOption = new Option("--json") { Description = "Output results in JSON format." }; import.Add(bundlePathOption); import.Add(importTenantOption); import.Add(globalOption); import.Add(dryRunOption); import.Add(forceOption); import.Add(verifyOnlyOption); import.Add(importJsonOption); import.SetAction((parseResult, _) => { var bundlePath = parseResult.GetValue(bundlePathOption)!; var tenant = parseResult.GetValue(importTenantOption); var global = parseResult.GetValue(globalOption); var dryRun = parseResult.GetValue(dryRunOption); var force = parseResult.GetValue(forceOption); var verifyOnly = parseResult.GetValue(verifyOnlyOption); var json = parseResult.GetValue(importJsonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAirgapImportAsync( services, bundlePath, tenant, global, dryRun, force, verifyOnly, json, verbose, cancellationToken); }); airgap.Add(import); // airgap seal (CLI-AIRGAP-57-002) var seal = new Command("seal", "Seal the environment for air-gapped operation."); var sealConfigDirOption = new Option("--config-dir", new[] { "-c" }) { Description = "Path to the configuration directory (defaults to ~/.stellaops)." }; var sealVerifyOption = new Option("--verify") { Description = "Verify imported bundles before sealing." }; var sealForceOption = new Option("--force") { Description = "Force seal even if verification warnings exist." }; var sealDryRunOption = new Option("--dry-run") { Description = "Preview the seal operation without making changes." }; var sealJsonOption = new Option("--json") { Description = "Output results in JSON format." }; var sealReasonOption = new Option("--reason") { Description = "Reason for sealing (recorded in audit log)." }; seal.Add(sealConfigDirOption); seal.Add(sealVerifyOption); seal.Add(sealForceOption); seal.Add(sealDryRunOption); seal.Add(sealJsonOption); seal.Add(sealReasonOption); seal.SetAction((parseResult, _) => { var configDir = parseResult.GetValue(sealConfigDirOption); var verify = parseResult.GetValue(sealVerifyOption); var force = parseResult.GetValue(sealForceOption); var dryRun = parseResult.GetValue(sealDryRunOption); var json = parseResult.GetValue(sealJsonOption); var reason = parseResult.GetValue(sealReasonOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAirgapSealAsync( services, configDir, verify, force, dryRun, json, reason, verbose, cancellationToken); }); airgap.Add(seal); // airgap export-evidence (CLI-AIRGAP-58-001) var exportEvidence = new Command("export-evidence", "Export portable evidence packages for audit and compliance."); var evidenceOutputOption = new Option("--output", new[] { "-o" }) { Description = "Output directory for the evidence package.", Required = true }; var evidenceIncludeOption = new Option("--include", new[] { "-i" }) { Description = "Evidence types to include: attestations, sboms, scans, vex, all (default: all).", AllowMultipleArgumentsPerToken = true }; var evidenceFromOption = new Option("--from") { Description = "Include evidence from this date (UTC, ISO-8601)." }; var evidenceToOption = new Option("--to") { Description = "Include evidence up to this date (UTC, ISO-8601)." }; var evidenceTenantOption = new Option("--tenant") { Description = "Export evidence for a specific tenant." }; var evidenceSubjectOption = new Option("--subject") { Description = "Filter evidence by subject (e.g., image digest, package PURL)." }; var evidenceCompressOption = new Option("--compress") { Description = "Compress the output package as a .tar.gz archive." }; var evidenceJsonOption = new Option("--json") { Description = "Output results in JSON format." }; var evidenceVerifyOption = new Option("--verify") { Description = "Verify evidence signatures before export." }; exportEvidence.Add(evidenceOutputOption); exportEvidence.Add(evidenceIncludeOption); exportEvidence.Add(evidenceFromOption); exportEvidence.Add(evidenceToOption); exportEvidence.Add(evidenceTenantOption); exportEvidence.Add(evidenceSubjectOption); exportEvidence.Add(evidenceCompressOption); exportEvidence.Add(evidenceJsonOption); exportEvidence.Add(evidenceVerifyOption); exportEvidence.SetAction((parseResult, _) => { var output = parseResult.GetValue(evidenceOutputOption)!; var include = parseResult.GetValue(evidenceIncludeOption) ?? Array.Empty(); var from = parseResult.GetValue(evidenceFromOption); var to = parseResult.GetValue(evidenceToOption); var tenant = parseResult.GetValue(evidenceTenantOption); var subject = parseResult.GetValue(evidenceSubjectOption); var compress = parseResult.GetValue(evidenceCompressOption); var json = parseResult.GetValue(evidenceJsonOption); var verify = parseResult.GetValue(evidenceVerifyOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleAirgapExportEvidenceAsync( services, output, include, from, to, tenant, subject, compress, json, verify, verbose, cancellationToken); }); airgap.Add(exportEvidence); return airgap; } }