Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
		
			
				
	
	
		
			754 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			754 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.CommandLine;
 | |
| using System.Threading;
 | |
| using System.Threading.Tasks;
 | |
| using StellaOps.Cli.Configuration;
 | |
| 
 | |
| namespace StellaOps.Cli.Commands;
 | |
| 
 | |
| internal static class CommandFactory
 | |
| {
 | |
|     public static RootCommand Create(IServiceProvider services, StellaOpsCliOptions options, CancellationToken cancellationToken)
 | |
|     {
 | |
|         var verboseOption = new Option<bool>("--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(BuildExcititorCommand(services, verboseOption, cancellationToken));
 | |
|         root.Add(BuildRuntimeCommand(services, verboseOption, cancellationToken));
 | |
|         root.Add(BuildAuthCommand(services, options, verboseOption, cancellationToken));
 | |
|         root.Add(BuildOfflineCommand(services, verboseOption, cancellationToken));
 | |
|         root.Add(BuildConfigCommand(options));
 | |
| 
 | |
|         return root;
 | |
|     }
 | |
| 
 | |
|     private static Command BuildScannerCommand(IServiceProvider services, Option<bool> 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<string>("--channel", new[] { "-c" })
 | |
|         {
 | |
|             Description = "Scanner channel (stable, beta, nightly)."
 | |
|         };
 | |
| 
 | |
|         var outputOption = new Option<string?>("--output")
 | |
|         {
 | |
|             Description = "Optional output path for the downloaded bundle."
 | |
|         };
 | |
| 
 | |
|         var overwriteOption = new Option<bool>("--overwrite")
 | |
|         {
 | |
|             Description = "Overwrite existing bundle if present."
 | |
|         };
 | |
| 
 | |
|         var noInstallOption = new Option<bool>("--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<bool> 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<string>("--runner")
 | |
|         {
 | |
|             Description = "Execution runtime (dotnet, self, docker)."
 | |
|         };
 | |
|         var entryOption = new Option<string>("--entry")
 | |
|         {
 | |
|             Description = "Path to the scanner entrypoint or Docker image.",
 | |
|             Required = true
 | |
|         };
 | |
|         var targetOption = new Option<string>("--target")
 | |
|         {
 | |
|             Description = "Directory to scan.",
 | |
|             Required = true
 | |
|         };
 | |
| 
 | |
|         var argsArgument = new Argument<string[]>("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<string>();
 | |
|             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<string>("--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<bool> 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<string>("--source")
 | |
|         {
 | |
|             Description = "Connector source identifier (e.g. redhat, osv, vmware).",
 | |
|             Required = true
 | |
|         };
 | |
|         var stageOption = new Option<string>("--stage")
 | |
|         {
 | |
|             Description = "Stage to trigger: fetch, parse, or map."
 | |
|         };
 | |
|         var modeOption = new Option<string?>("--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<string>("--format")
 | |
|         {
 | |
|             Description = "Export format: json or trivy-db."
 | |
|         };
 | |
|         var deltaOption = new Option<bool>("--delta")
 | |
|         {
 | |
|             Description = "Request a delta export when supported."
 | |
|         };
 | |
|         var publishFullOption = new Option<bool?>("--publish-full")
 | |
|         {
 | |
|             Description = "Override whether full exports push to ORAS (true/false)."
 | |
|         };
 | |
|         var publishDeltaOption = new Option<bool?>("--publish-delta")
 | |
|         {
 | |
|             Description = "Override whether delta exports push to ORAS (true/false)."
 | |
|         };
 | |
|         var includeFullOption = new Option<bool?>("--bundle-full")
 | |
|         {
 | |
|             Description = "Override whether offline bundles include full exports (true/false)."
 | |
|         };
 | |
|         var includeDeltaOption = new Option<bool?>("--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 BuildExcititorCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
 | |
|     {
 | |
|         var excititor = new Command("excititor", "Manage Excititor ingest, exports, and reconciliation workflows.");
 | |
| 
 | |
|         var init = new Command("init", "Initialize Excititor ingest state.");
 | |
|         var initProviders = new Option<string[]>("--provider", new[] { "-p" })
 | |
|         {
 | |
|             Description = "Optional provider identifier(s) to initialize.",
 | |
|             Arity = ArgumentArity.ZeroOrMore
 | |
|         };
 | |
|         var resumeOption = new Option<bool>("--resume")
 | |
|         {
 | |
|             Description = "Resume ingest from the last persisted checkpoint instead of starting fresh."
 | |
|         };
 | |
|         init.Add(initProviders);
 | |
|         init.Add(resumeOption);
 | |
|         init.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var providers = parseResult.GetValue(initProviders) ?? Array.Empty<string>();
 | |
|             var resume = parseResult.GetValue(resumeOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorInitAsync(services, providers, resume, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var pull = new Command("pull", "Trigger Excititor ingest for configured providers.");
 | |
|         var pullProviders = new Option<string[]>("--provider", new[] { "-p" })
 | |
|         {
 | |
|             Description = "Optional provider identifier(s) to ingest.",
 | |
|             Arity = ArgumentArity.ZeroOrMore
 | |
|         };
 | |
|         var sinceOption = new Option<DateTimeOffset?>("--since")
 | |
|         {
 | |
|             Description = "Optional ISO-8601 timestamp to begin the ingest window."
 | |
|         };
 | |
|         var windowOption = new Option<TimeSpan?>("--window")
 | |
|         {
 | |
|             Description = "Optional window duration (e.g. 24:00:00)."
 | |
|         };
 | |
|         var forceOption = new Option<bool>("--force")
 | |
|         {
 | |
|             Description = "Force ingestion even if the backend reports no pending work."
 | |
|         };
 | |
|         pull.Add(pullProviders);
 | |
|         pull.Add(sinceOption);
 | |
|         pull.Add(windowOption);
 | |
|         pull.Add(forceOption);
 | |
|         pull.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var providers = parseResult.GetValue(pullProviders) ?? Array.Empty<string>();
 | |
|             var since = parseResult.GetValue(sinceOption);
 | |
|             var window = parseResult.GetValue(windowOption);
 | |
|             var force = parseResult.GetValue(forceOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorPullAsync(services, providers, since, window, force, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var resume = new Command("resume", "Resume Excititor ingest using a checkpoint token.");
 | |
|         var resumeProviders = new Option<string[]>("--provider", new[] { "-p" })
 | |
|         {
 | |
|             Description = "Optional provider identifier(s) to resume.",
 | |
|             Arity = ArgumentArity.ZeroOrMore
 | |
|         };
 | |
|         var checkpointOption = new Option<string?>("--checkpoint")
 | |
|         {
 | |
|             Description = "Optional checkpoint identifier to resume from."
 | |
|         };
 | |
|         resume.Add(resumeProviders);
 | |
|         resume.Add(checkpointOption);
 | |
|         resume.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var providers = parseResult.GetValue(resumeProviders) ?? Array.Empty<string>();
 | |
|             var checkpoint = parseResult.GetValue(checkpointOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorResumeAsync(services, providers, checkpoint, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var list = new Command("list-providers", "List Excititor providers and their ingest status.");
 | |
|         var includeDisabledOption = new Option<bool>("--include-disabled")
 | |
|         {
 | |
|             Description = "Include disabled providers in the listing."
 | |
|         };
 | |
|         list.Add(includeDisabledOption);
 | |
|         list.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var includeDisabled = parseResult.GetValue(includeDisabledOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorListProvidersAsync(services, includeDisabled, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var export = new Command("export", "Trigger Excititor export generation.");
 | |
|         var formatOption = new Option<string>("--format")
 | |
|         {
 | |
|             Description = "Export format (e.g. openvex, json)."
 | |
|         };
 | |
|         var exportDeltaOption = new Option<bool>("--delta")
 | |
|         {
 | |
|             Description = "Request a delta export when supported."
 | |
|         };
 | |
|         var exportScopeOption = new Option<string?>("--scope")
 | |
|         {
 | |
|             Description = "Optional policy scope or tenant identifier."
 | |
|         };
 | |
|         var exportSinceOption = new Option<DateTimeOffset?>("--since")
 | |
|         {
 | |
|             Description = "Optional ISO-8601 timestamp to restrict export contents."
 | |
|         };
 | |
|         var exportProviderOption = new Option<string?>("--provider")
 | |
|         {
 | |
|             Description = "Optional provider identifier when requesting targeted exports."
 | |
|         };
 | |
|         var exportOutputOption = new Option<string?>("--output")
 | |
|         {
 | |
|             Description = "Optional path to download the export artifact."
 | |
|         };
 | |
|         export.Add(formatOption);
 | |
|         export.Add(exportDeltaOption);
 | |
|         export.Add(exportScopeOption);
 | |
|         export.Add(exportSinceOption);
 | |
|         export.Add(exportProviderOption);
 | |
|         export.Add(exportOutputOption);
 | |
|         export.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var format = parseResult.GetValue(formatOption) ?? "openvex";
 | |
|             var delta = parseResult.GetValue(exportDeltaOption);
 | |
|             var scope = parseResult.GetValue(exportScopeOption);
 | |
|             var since = parseResult.GetValue(exportSinceOption);
 | |
|             var provider = parseResult.GetValue(exportProviderOption);
 | |
|             var output = parseResult.GetValue(exportOutputOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorExportAsync(services, format, delta, scope, since, provider, output, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var backfill = new Command("backfill-statements", "Replay historical raw documents into Excititor statements.");
 | |
|         var backfillRetrievedSinceOption = new Option<DateTimeOffset?>("--retrieved-since")
 | |
|         {
 | |
|             Description = "Only process raw documents retrieved on or after the provided ISO-8601 timestamp."
 | |
|         };
 | |
|         var backfillForceOption = new Option<bool>("--force")
 | |
|         {
 | |
|             Description = "Reprocess documents even if statements already exist."
 | |
|         };
 | |
|         var backfillBatchSizeOption = new Option<int>("--batch-size")
 | |
|         {
 | |
|             Description = "Number of raw documents to fetch per batch (default 100)."
 | |
|         };
 | |
|         var backfillMaxDocumentsOption = new Option<int?>("--max-documents")
 | |
|         {
 | |
|             Description = "Optional maximum number of raw documents to process."
 | |
|         };
 | |
|         backfill.Add(backfillRetrievedSinceOption);
 | |
|         backfill.Add(backfillForceOption);
 | |
|         backfill.Add(backfillBatchSizeOption);
 | |
|         backfill.Add(backfillMaxDocumentsOption);
 | |
|         backfill.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var retrievedSince = parseResult.GetValue(backfillRetrievedSinceOption);
 | |
|             var force = parseResult.GetValue(backfillForceOption);
 | |
|             var batchSize = parseResult.GetValue(backfillBatchSizeOption);
 | |
|             if (batchSize <= 0)
 | |
|             {
 | |
|                 batchSize = 100;
 | |
|             }
 | |
|             var maxDocuments = parseResult.GetValue(backfillMaxDocumentsOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorBackfillStatementsAsync(
 | |
|                 services,
 | |
|                 retrievedSince,
 | |
|                 force,
 | |
|                 batchSize,
 | |
|                 maxDocuments,
 | |
|                 verbose,
 | |
|                 cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var verify = new Command("verify", "Verify Excititor exports or attestations.");
 | |
|         var exportIdOption = new Option<string?>("--export-id")
 | |
|         {
 | |
|             Description = "Export identifier to verify."
 | |
|         };
 | |
|         var digestOption = new Option<string?>("--digest")
 | |
|         {
 | |
|             Description = "Expected digest for the export or attestation."
 | |
|         };
 | |
|         var attestationOption = new Option<string?>("--attestation")
 | |
|         {
 | |
|             Description = "Path to a local attestation file to verify (base64 content will be uploaded)."
 | |
|         };
 | |
|         verify.Add(exportIdOption);
 | |
|         verify.Add(digestOption);
 | |
|         verify.Add(attestationOption);
 | |
|         verify.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var exportId = parseResult.GetValue(exportIdOption);
 | |
|             var digest = parseResult.GetValue(digestOption);
 | |
|             var attestation = parseResult.GetValue(attestationOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorVerifyAsync(services, exportId, digest, attestation, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var reconcile = new Command("reconcile", "Trigger Excititor reconciliation against canonical advisories.");
 | |
|         var reconcileProviders = new Option<string[]>("--provider", new[] { "-p" })
 | |
|         {
 | |
|             Description = "Optional provider identifier(s) to reconcile.",
 | |
|             Arity = ArgumentArity.ZeroOrMore
 | |
|         };
 | |
|         var maxAgeOption = new Option<TimeSpan?>("--max-age")
 | |
|         {
 | |
|             Description = "Optional maximum age window (e.g. 7.00:00:00)."
 | |
|         };
 | |
|         reconcile.Add(reconcileProviders);
 | |
|         reconcile.Add(maxAgeOption);
 | |
|         reconcile.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var providers = parseResult.GetValue(reconcileProviders) ?? Array.Empty<string>();
 | |
|             var maxAge = parseResult.GetValue(maxAgeOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleExcititorReconcileAsync(services, providers, maxAge, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         excititor.Add(init);
 | |
|         excititor.Add(pull);
 | |
|         excititor.Add(resume);
 | |
|         excititor.Add(list);
 | |
|         excititor.Add(export);
 | |
|         excititor.Add(backfill);
 | |
|         excititor.Add(verify);
 | |
|         excititor.Add(reconcile);
 | |
|         return excititor;
 | |
|     }
 | |
| 
 | |
|     private static Command BuildRuntimeCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
 | |
|     {
 | |
|         var runtime = new Command("runtime", "Interact with runtime admission policy APIs.");
 | |
|         var policy = new Command("policy", "Runtime policy operations.");
 | |
| 
 | |
|         var test = new Command("test", "Evaluate runtime policy decisions for image digests.");
 | |
|         var namespaceOption = new Option<string?>("--namespace", new[] { "--ns" })
 | |
|         {
 | |
|             Description = "Namespace or logical scope for the evaluation."
 | |
|         };
 | |
| 
 | |
|         var imageOption = new Option<string[]>("--image", new[] { "-i", "--images" })
 | |
|         {
 | |
|             Description = "Image digests to evaluate (repeatable).",
 | |
|             Arity = ArgumentArity.ZeroOrMore
 | |
|         };
 | |
| 
 | |
|         var fileOption = new Option<string?>("--file", new[] { "-f" })
 | |
|         {
 | |
|             Description = "Path to a file containing image digests (one per line)."
 | |
|         };
 | |
| 
 | |
|         var labelOption = new Option<string[]>("--label", new[] { "-l", "--labels" })
 | |
|         {
 | |
|             Description = "Pod labels in key=value format (repeatable).",
 | |
|             Arity = ArgumentArity.ZeroOrMore
 | |
|         };
 | |
| 
 | |
|         var jsonOption = new Option<bool>("--json")
 | |
|         {
 | |
|             Description = "Emit the raw JSON response."
 | |
|         };
 | |
| 
 | |
|         test.Add(namespaceOption);
 | |
|         test.Add(imageOption);
 | |
|         test.Add(fileOption);
 | |
|         test.Add(labelOption);
 | |
|         test.Add(jsonOption);
 | |
| 
 | |
|         test.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var nsValue = parseResult.GetValue(namespaceOption);
 | |
|             var images = parseResult.GetValue(imageOption) ?? Array.Empty<string>();
 | |
|             var file = parseResult.GetValue(fileOption);
 | |
|             var labels = parseResult.GetValue(labelOption) ?? Array.Empty<string>();
 | |
|             var outputJson = parseResult.GetValue(jsonOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
| 
 | |
|             return CommandHandlers.HandleRuntimePolicyTestAsync(
 | |
|                 services,
 | |
|                 nsValue,
 | |
|                 images,
 | |
|                 file,
 | |
|                 labels,
 | |
|                 outputJson,
 | |
|                 verbose,
 | |
|                 cancellationToken);
 | |
|         });
 | |
| 
 | |
|         policy.Add(test);
 | |
|         runtime.Add(policy);
 | |
|         return runtime;
 | |
|     }
 | |
| 
 | |
|     private static Command BuildAuthCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> 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<bool>("--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<string?>("--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<string>("--bundle")
 | |
|         {
 | |
|             Description = "Path to the revocation-bundle.json file."
 | |
|         };
 | |
|         var signatureOption = new Option<string>("--signature")
 | |
|         {
 | |
|             Description = "Path to the revocation-bundle.json.jws file."
 | |
|         };
 | |
|         var keyOption = new Option<string>("--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 BuildOfflineCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
 | |
|     {
 | |
|         var offline = new Command("offline", "Offline kit workflows and utilities.");
 | |
| 
 | |
|         var kit = new Command("kit", "Manage offline kit bundles.");
 | |
| 
 | |
|         var pull = new Command("pull", "Download the latest offline kit bundle.");
 | |
|         var bundleIdOption = new Option<string?>("--bundle-id")
 | |
|         {
 | |
|             Description = "Optional bundle identifier. Defaults to the latest available."
 | |
|         };
 | |
|         var destinationOption = new Option<string?>("--destination")
 | |
|         {
 | |
|             Description = "Directory to store downloaded bundles (defaults to the configured offline kits directory)."
 | |
|         };
 | |
|         var overwriteOption = new Option<bool>("--overwrite")
 | |
|         {
 | |
|             Description = "Overwrite existing files even if checksums match."
 | |
|         };
 | |
|         var noResumeOption = new Option<bool>("--no-resume")
 | |
|         {
 | |
|             Description = "Disable resuming partial downloads."
 | |
|         };
 | |
| 
 | |
|         pull.Add(bundleIdOption);
 | |
|         pull.Add(destinationOption);
 | |
|         pull.Add(overwriteOption);
 | |
|         pull.Add(noResumeOption);
 | |
|         pull.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var bundleId = parseResult.GetValue(bundleIdOption);
 | |
|             var destination = parseResult.GetValue(destinationOption);
 | |
|             var overwrite = parseResult.GetValue(overwriteOption);
 | |
|             var resume = !parseResult.GetValue(noResumeOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleOfflineKitPullAsync(services, bundleId, destination, overwrite, resume, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var import = new Command("import", "Upload an offline kit bundle to the backend.");
 | |
|         var bundleArgument = new Argument<string>("bundle")
 | |
|         {
 | |
|             Description = "Path to the offline kit tarball (.tgz)."
 | |
|         };
 | |
|         var manifestOption = new Option<string?>("--manifest")
 | |
|         {
 | |
|             Description = "Offline manifest JSON path (defaults to metadata or sibling file)."
 | |
|         };
 | |
|         var bundleSignatureOption = new Option<string?>("--bundle-signature")
 | |
|         {
 | |
|             Description = "Detached signature for the offline bundle (e.g. .sig)."
 | |
|         };
 | |
|         var manifestSignatureOption = new Option<string?>("--manifest-signature")
 | |
|         {
 | |
|             Description = "Detached signature for the offline manifest (e.g. .jws)."
 | |
|         };
 | |
| 
 | |
|         import.Add(bundleArgument);
 | |
|         import.Add(manifestOption);
 | |
|         import.Add(bundleSignatureOption);
 | |
|         import.Add(manifestSignatureOption);
 | |
|         import.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var bundlePath = parseResult.GetValue(bundleArgument) ?? string.Empty;
 | |
|             var manifest = parseResult.GetValue(manifestOption);
 | |
|             var bundleSignature = parseResult.GetValue(bundleSignatureOption);
 | |
|             var manifestSignature = parseResult.GetValue(manifestSignatureOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleOfflineKitImportAsync(services, bundlePath, manifest, bundleSignature, manifestSignature, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         var status = new Command("status", "Display offline kit installation status.");
 | |
|         var jsonOption = new Option<bool>("--json")
 | |
|         {
 | |
|             Description = "Emit status as JSON."
 | |
|         };
 | |
|         status.Add(jsonOption);
 | |
|         status.SetAction((parseResult, _) =>
 | |
|         {
 | |
|             var asJson = parseResult.GetValue(jsonOption);
 | |
|             var verbose = parseResult.GetValue(verboseOption);
 | |
|             return CommandHandlers.HandleOfflineKitStatusAsync(services, asJson, verbose, cancellationToken);
 | |
|         });
 | |
| 
 | |
|         kit.Add(pull);
 | |
|         kit.Add(import);
 | |
|         kit.Add(status);
 | |
| 
 | |
|         offline.Add(kit);
 | |
|         return offline;
 | |
|     }
 | |
| 
 | |
|     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)}",
 | |
|                 $"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) ? "<not configured>" : value;
 | |
| 
 | |
|     private static string DescribeSecret(string value)
 | |
|     {
 | |
|         if (string.IsNullOrWhiteSpace(value))
 | |
|         {
 | |
|             return "<not configured>";
 | |
|         }
 | |
| 
 | |
|         return value.Length switch
 | |
|         {
 | |
|             <= 4 => "****",
 | |
|             _ => $"{value[..2]}***{value[^2..]}"
 | |
|         };
 | |
|     }
 | |
| }
 |