using System; using System.CommandLine; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using StellaOps.Cli.Configuration; using StellaOps.Cli.Plugins; 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 root = new RootCommand("StellaOps command-line interface") { TreatUnmatchedTokensAsErrors = true }; root.Add(verboseOption); root.Add(BuildScannerCommand(services, verboseOption, cancellationToken)); root.Add(BuildScanCommand(services, options, 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(BuildPolicyCommand(services, options, verboseOption, cancellationToken)); root.Add(BuildFindingsCommand(services, verboseOption, cancellationToken)); root.Add(BuildConfigCommand(options)); root.Add(BuildVulnCommand(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 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); }); scan.Add(run); scan.Add(upload); return scan; } 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 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); auth.Add(login); auth.Add(logout); auth.Add(status); auth.Add(whoami); auth.Add(revoke); return auth; } 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 or json." }; var outputOption = new Option("--output") { Description = "Write JSON 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." }; 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.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 verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandlePolicySimulateAsync( services, policyId, baseVersion, candidateVersion, sbomSet, environment, format, output, explain, failOnDiff, 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." }; 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); return policy; } 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 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); return vuln; } 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..]}" }; } }