using System; using System.Buffers; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Spectre.Console; using Spectre.Console.Rendering; using StellaOps.Auth.Client; using StellaOps.Cli.Configuration; using StellaOps.Cli.Prompts; using StellaOps.Cli.Services; using StellaOps.Cli.Services.Models; using StellaOps.Cli.Services.Models.AdvisoryAi; using StellaOps.Cli.Services.Models.Ruby; using StellaOps.Cli.Telemetry; using StellaOps.Cryptography; using StellaOps.Cryptography.DependencyInjection; using StellaOps.Cryptography.Kms; using StellaOps.Scanner.Analyzers.Lang; using StellaOps.Scanner.Analyzers.Lang.Java; using StellaOps.Scanner.Analyzers.Lang.Node; using StellaOps.Scanner.Analyzers.Lang.Python; using StellaOps.Scanner.Analyzers.Lang.Ruby; namespace StellaOps.Cli.Commands; internal static class CommandHandlers { private const string KmsPassphraseEnvironmentVariable = "STELLAOPS_KMS_PASSPHRASE"; private static readonly JsonSerializerOptions KmsJsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true }; public static async Task HandleScannerDownloadAsync( IServiceProvider services, string channel, string? output, bool overwrite, bool install, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("scanner-download"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.scanner.download", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "scanner download"); activity?.SetTag("stellaops.cli.channel", channel); using var duration = CliMetrics.MeasureCommandDuration("scanner download"); try { var result = await client.DownloadScannerAsync(channel, output ?? string.Empty, overwrite, verbose, cancellationToken).ConfigureAwait(false); if (result.FromCache) { logger.LogInformation("Using cached scanner at {Path}.", result.Path); } else { logger.LogInformation("Scanner downloaded to {Path} ({Size} bytes).", result.Path, result.SizeBytes); } CliMetrics.RecordScannerDownload(channel, result.FromCache); if (install) { var installer = scope.ServiceProvider.GetRequiredService(); await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false); CliMetrics.RecordScannerInstall(channel); } Environment.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to download scanner bundle."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleTaskRunnerSimulateAsync( IServiceProvider services, string manifestPath, string? inputsPath, string? format, string? outputPath, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("task-runner-simulate"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.taskrunner.simulate", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "task-runner simulate"); using var duration = CliMetrics.MeasureCommandDuration("task-runner simulate"); try { if (string.IsNullOrWhiteSpace(manifestPath)) { throw new ArgumentException("Manifest path must be provided.", nameof(manifestPath)); } var manifestFullPath = Path.GetFullPath(manifestPath); if (!File.Exists(manifestFullPath)) { throw new FileNotFoundException("Manifest file not found.", manifestFullPath); } activity?.SetTag("stellaops.cli.manifest_path", manifestFullPath); var manifest = await File.ReadAllTextAsync(manifestFullPath, cancellationToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(manifest)) { throw new InvalidOperationException("Manifest file was empty."); } JsonObject? inputsObject = null; if (!string.IsNullOrWhiteSpace(inputsPath)) { var inputsFullPath = Path.GetFullPath(inputsPath!); if (!File.Exists(inputsFullPath)) { throw new FileNotFoundException("Inputs file not found.", inputsFullPath); } await using var stream = File.OpenRead(inputsFullPath); var parsed = await JsonNode.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); if (parsed is JsonObject obj) { inputsObject = obj; } else { throw new InvalidOperationException("Simulation inputs must be a JSON object."); } activity?.SetTag("stellaops.cli.inputs_path", inputsFullPath); } var request = new TaskRunnerSimulationRequest(manifest, inputsObject); var result = await client.SimulateTaskRunnerAsync(request, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.plan_hash", result.PlanHash); activity?.SetTag("stellaops.cli.pending_approvals", result.HasPendingApprovals); activity?.SetTag("stellaops.cli.step_count", result.Steps.Count); var outputFormat = DetermineTaskRunnerSimulationFormat(format, outputPath); var payload = BuildTaskRunnerSimulationPayload(result); if (!string.IsNullOrWhiteSpace(outputPath)) { await WriteSimulationOutputAsync(outputPath!, payload, cancellationToken).ConfigureAwait(false); logger.LogInformation("Simulation payload written to {Path}.", Path.GetFullPath(outputPath!)); } if (outputFormat == TaskRunnerSimulationOutputFormat.Json) { Console.WriteLine(JsonSerializer.Serialize(payload, SimulationJsonOptions)); } else { RenderTaskRunnerSimulationResult(result); } var outcome = result.HasPendingApprovals ? "pending-approvals" : "ok"; CliMetrics.RecordTaskRunnerSimulation(outcome); Environment.ExitCode = 0; } catch (FileNotFoundException ex) { logger.LogError(ex.Message); CliMetrics.RecordTaskRunnerSimulation("error"); Environment.ExitCode = 66; } catch (ArgumentException ex) { logger.LogError(ex.Message); CliMetrics.RecordTaskRunnerSimulation("error"); Environment.ExitCode = 64; } catch (InvalidOperationException ex) { logger.LogError(ex, "Task Runner simulation failed."); CliMetrics.RecordTaskRunnerSimulation("error"); Environment.ExitCode = 1; } catch (Exception ex) { logger.LogError(ex, "Task Runner simulation failed."); CliMetrics.RecordTaskRunnerSimulation("error"); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } private static void RenderEntryTrace(EntryTraceResponseModel result, bool includeNdjson) { var console = AnsiConsole.Console; console.MarkupLine($"[bold]Scan[/]: {result.ScanId}"); console.MarkupLine($"Image: {result.ImageDigest}"); console.MarkupLine($"Generated: {result.GeneratedAt:O}"); console.MarkupLine($"Outcome: {result.Graph.Outcome}"); var planTable = new Table() .AddColumn("Terminal") .AddColumn("Runtime") .AddColumn("Type") .AddColumn("Confidence") .AddColumn("User") .AddColumn("Workdir"); foreach (var plan in result.Graph.Plans.OrderByDescending(p => p.Confidence)) { planTable.AddRow( plan.TerminalPath, plan.Runtime ?? "-", plan.Type.ToString(), plan.Confidence.ToString("F1", CultureInfo.InvariantCulture), plan.User, plan.WorkingDirectory); } if (planTable.Rows.Count > 0) { console.Write(planTable); } else { console.MarkupLine("[italic]No entry trace plans recorded.[/]"); } if (result.Graph.Diagnostics.Length > 0) { var diagTable = new Table() .AddColumn("Severity") .AddColumn("Reason") .AddColumn("Message"); foreach (var diagnostic in result.Graph.Diagnostics) { diagTable.AddRow( diagnostic.Severity.ToString(), diagnostic.Reason.ToString(), diagnostic.Message); } console.Write(diagTable); } if (includeNdjson && result.Ndjson.Count > 0) { console.MarkupLine("[bold]NDJSON Output[/]"); foreach (var line in result.Ndjson) { console.WriteLine(line); } } } public static async Task HandleScannerRunAsync( IServiceProvider services, string runner, string entry, string targetDirectory, IReadOnlyList arguments, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var executor = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("scanner-run"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.scan.run", ActivityKind.Internal); activity?.SetTag("stellaops.cli.command", "scan run"); activity?.SetTag("stellaops.cli.runner", runner); activity?.SetTag("stellaops.cli.entry", entry); activity?.SetTag("stellaops.cli.target", targetDirectory); using var duration = CliMetrics.MeasureCommandDuration("scan run"); try { var options = scope.ServiceProvider.GetRequiredService(); var resultsDirectory = options.ResultsDirectory; var executionResult = await executor.RunAsync( runner, entry, targetDirectory, resultsDirectory, arguments, verbose, cancellationToken).ConfigureAwait(false); Environment.ExitCode = executionResult.ExitCode; CliMetrics.RecordScanRun(runner, executionResult.ExitCode); if (executionResult.ExitCode == 0) { var backend = scope.ServiceProvider.GetRequiredService(); logger.LogInformation("Uploading scan artefact {Path}...", executionResult.ResultsPath); await backend.UploadScanResultsAsync(executionResult.ResultsPath, cancellationToken).ConfigureAwait(false); logger.LogInformation("Scan artefact uploaded."); activity?.SetTag("stellaops.cli.results", executionResult.ResultsPath); } else { logger.LogWarning("Skipping automatic upload because scan exited with code {Code}.", executionResult.ExitCode); } logger.LogInformation("Run metadata written to {Path}.", executionResult.RunMetadataPath); activity?.SetTag("stellaops.cli.run_metadata", executionResult.RunMetadataPath); } catch (Exception ex) { logger.LogError(ex, "Scanner execution failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleScanUploadAsync( IServiceProvider services, string file, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("scanner-upload"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.scan.upload", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "scan upload"); activity?.SetTag("stellaops.cli.file", file); using var duration = CliMetrics.MeasureCommandDuration("scan upload"); try { var pathFull = Path.GetFullPath(file); await client.UploadScanResultsAsync(pathFull, cancellationToken).ConfigureAwait(false); logger.LogInformation("Scan results uploaded successfully."); Environment.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to upload scan results."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleScanEntryTraceAsync( IServiceProvider services, string scanId, bool includeNdjson, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("scan-entrytrace"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.scan.entrytrace", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "scan entrytrace"); activity?.SetTag("stellaops.cli.scan_id", scanId); using var duration = CliMetrics.MeasureCommandDuration("scan entrytrace"); try { var result = await client.GetEntryTraceAsync(scanId, cancellationToken).ConfigureAwait(false); if (result is null) { logger.LogWarning("No EntryTrace data available for scan {ScanId}.", scanId); var console = AnsiConsole.Console; console.MarkupLine("[yellow]No EntryTrace data available for scan {0}.[/]", Markup.Escape(scanId)); console.Write(new Text($"No EntryTrace data available for scan {scanId}.{Environment.NewLine}")); Console.WriteLine($"No EntryTrace data available for scan {scanId}."); Environment.ExitCode = 1; return; } RenderEntryTrace(result, includeNdjson); Environment.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to fetch EntryTrace for scan {ScanId}.", scanId); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleAdviseRunAsync( IServiceProvider services, AdvisoryAiTaskType taskType, string advisoryKey, string? artifactId, string? artifactPurl, string? policyVersion, string profile, IReadOnlyList preferredSections, bool forceRefresh, int timeoutSeconds, AdvisoryOutputFormat outputFormat, string? outputPath, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("advise-run"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.advisory.run", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "advise run"); activity?.SetTag("stellaops.cli.task", taskType.ToString()); using var duration = CliMetrics.MeasureCommandDuration("advisory run"); activity?.SetTag("stellaops.cli.force_refresh", forceRefresh); var outcome = "error"; try { var normalizedKey = advisoryKey?.Trim(); if (string.IsNullOrWhiteSpace(normalizedKey)) { throw new ArgumentException("Advisory key is required.", nameof(advisoryKey)); } activity?.SetTag("stellaops.cli.advisory.key", normalizedKey); var normalizedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile.Trim(); activity?.SetTag("stellaops.cli.profile", normalizedProfile); var normalizedSections = NormalizeSections(preferredSections); var request = new AdvisoryPipelinePlanRequestModel { TaskType = taskType, AdvisoryKey = normalizedKey, ArtifactId = string.IsNullOrWhiteSpace(artifactId) ? null : artifactId!.Trim(), ArtifactPurl = string.IsNullOrWhiteSpace(artifactPurl) ? null : artifactPurl!.Trim(), PolicyVersion = string.IsNullOrWhiteSpace(policyVersion) ? null : policyVersion!.Trim(), Profile = normalizedProfile, PreferredSections = normalizedSections.Length > 0 ? normalizedSections : null, ForceRefresh = forceRefresh }; logger.LogInformation("Requesting advisory plan for {TaskType} (advisory={AdvisoryKey}).", taskType, normalizedKey); var plan = await client.CreateAdvisoryPipelinePlanAsync(taskType, request, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.advisory.cache_key", plan.CacheKey); RenderAdvisoryPlan(plan); logger.LogInformation("Plan {CacheKey} queued with {Chunks} chunks and {Vectors} vectors.", plan.CacheKey, plan.Chunks.Count, plan.Vectors.Count); var pollDelay = TimeSpan.FromSeconds(1); var shouldWait = timeoutSeconds > 0; var deadline = shouldWait ? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(timeoutSeconds) : DateTimeOffset.UtcNow; AdvisoryPipelineOutputModel? output = null; while (true) { cancellationToken.ThrowIfCancellationRequested(); output = await client .TryGetAdvisoryPipelineOutputAsync(plan.CacheKey, taskType, normalizedProfile, cancellationToken) .ConfigureAwait(false); if (output is not null) { break; } if (!shouldWait || DateTimeOffset.UtcNow >= deadline) { break; } logger.LogDebug("Advisory output pending for {CacheKey}; retrying in {DelaySeconds}s.", plan.CacheKey, pollDelay.TotalSeconds); await Task.Delay(pollDelay, cancellationToken).ConfigureAwait(false); } if (output is null) { logger.LogError("Timed out after {Timeout}s waiting for advisory output (cache key {CacheKey}).", Math.Max(timeoutSeconds, 0), plan.CacheKey); activity?.SetStatus(ActivityStatusCode.Error, "timeout"); outcome = "timeout"; Environment.ExitCode = Environment.ExitCode == 0 ? 70 : Environment.ExitCode; return; } activity?.SetTag("stellaops.cli.advisory.generated_at", output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture)); activity?.SetTag("stellaops.cli.advisory.cache_hit", output.PlanFromCache); logger.LogInformation("Advisory output ready (cache key {CacheKey}).", output.CacheKey); var rendered = RenderAdvisoryOutput(output, outputFormat); if (!string.IsNullOrWhiteSpace(outputPath) && rendered is not null) { var fullPath = Path.GetFullPath(outputPath!); await File.WriteAllTextAsync(fullPath, rendered, cancellationToken).ConfigureAwait(false); logger.LogInformation("Advisory output written to {Path}.", fullPath); } if (rendered is not null) { // Surface the rendered advisory to the active console so users (and tests) can see it even when also writing to disk. AnsiConsole.Console.WriteLine(rendered); } if (output.Guardrail.Blocked) { logger.LogError("Guardrail blocked advisory output (cache key {CacheKey}).", output.CacheKey); activity?.SetStatus(ActivityStatusCode.Error, "guardrail_blocked"); outcome = "blocked"; Environment.ExitCode = Environment.ExitCode == 0 ? 65 : Environment.ExitCode; return; } activity?.SetStatus(ActivityStatusCode.Ok); outcome = output.PlanFromCache ? "cache-hit" : "ok"; Environment.ExitCode = 0; } catch (OperationCanceledException) { outcome = "cancelled"; activity?.SetStatus(ActivityStatusCode.Error, "cancelled"); Environment.ExitCode = Environment.ExitCode == 0 ? 130 : Environment.ExitCode; } catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error, ex.Message); logger.LogError(ex, "Failed to run advisory task."); outcome = "error"; Environment.ExitCode = Environment.ExitCode == 0 ? 1 : Environment.ExitCode; } finally { activity?.SetTag("stellaops.cli.advisory.outcome", outcome); CliMetrics.RecordAdvisoryRun(taskType.ToString(), outcome); verbosity.MinimumLevel = previousLevel; } } public static async Task HandleAdviseBatchAsync( IServiceProvider services, AdvisoryAiTaskType taskType, IReadOnlyList advisoryKeys, string? artifactId, string? artifactPurl, string? policyVersion, string profile, IReadOnlyList preferredSections, bool forceRefresh, int timeoutSeconds, AdvisoryOutputFormat outputFormat, string? outputDirectory, bool verbose, CancellationToken cancellationToken) { if (advisoryKeys.Count == 0) { throw new ArgumentException("At least one advisory key is required.", nameof(advisoryKeys)); } var outputDir = string.IsNullOrWhiteSpace(outputDirectory) ? null : Path.GetFullPath(outputDirectory!); if (outputDir is not null) { Directory.CreateDirectory(outputDir); } var results = new List<(string Advisory, int ExitCode)>(); var overallExit = 0; foreach (var key in advisoryKeys) { var sanitized = string.IsNullOrWhiteSpace(key) ? "unknown" : key.Trim(); var ext = outputFormat switch { AdvisoryOutputFormat.Json => ".json", AdvisoryOutputFormat.Markdown => ".md", _ => ".txt" }; var outputPath = outputDir is null ? null : Path.Combine(outputDir, $"{SanitizeFileName(sanitized)}-{taskType.ToString().ToLowerInvariant()}{ext}"); Environment.ExitCode = 0; // reset per advisory to capture individual result await HandleAdviseRunAsync( services, taskType, sanitized, artifactId, artifactPurl, policyVersion, profile, preferredSections, forceRefresh, timeoutSeconds, outputFormat, outputPath, verbose, cancellationToken); var code = Environment.ExitCode; results.Add((sanitized, code)); overallExit = overallExit == 0 ? code : overallExit; // retain first non-zero if any } if (results.Count > 1) { var table = new Table() .Border(TableBorder.Rounded) .Title("[bold]Advisory Batch[/]"); table.AddColumn("Advisory"); table.AddColumn("Task"); table.AddColumn("Exit Code"); foreach (var result in results) { var exitText = result.ExitCode == 0 ? "[green]0[/]" : $"[red]{result.ExitCode}[/]"; table.AddRow(Markup.Escape(result.Advisory), taskType.ToString(), exitText); } AnsiConsole.Console.Write(table); } Environment.ExitCode = overallExit; } public static async Task HandleSourcesIngestAsync( IServiceProvider services, bool dryRun, string source, string input, string? tenantOverride, string format, bool disableColor, string? output, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("sources-ingest"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.sources.ingest.dry_run", ActivityKind.Client); var statusMetric = "unknown"; using var duration = CliMetrics.MeasureCommandDuration("sources ingest dry-run"); try { if (!dryRun) { statusMetric = "unsupported"; logger.LogError("Only --dry-run mode is supported for 'stella sources ingest' at this time."); Environment.ExitCode = 1; return; } source = source?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(source)) { throw new InvalidOperationException("Source identifier must be provided."); } var formatNormalized = string.IsNullOrWhiteSpace(format) ? "table" : format.Trim().ToLowerInvariant(); if (formatNormalized is not ("table" or "json")) { throw new InvalidOperationException("Format must be either 'table' or 'json'."); } var tenant = ResolveTenant(tenantOverride); if (string.IsNullOrWhiteSpace(tenant)) { throw new InvalidOperationException("Tenant must be provided via --tenant or STELLA_TENANT."); } var payload = await LoadIngestInputAsync(services, input, cancellationToken).ConfigureAwait(false); logger.LogInformation("Executing ingestion dry-run for source {Source} using input {Input}.", source, payload.Name); activity?.SetTag("stellaops.cli.command", "sources ingest dry-run"); activity?.SetTag("stellaops.cli.source", source); activity?.SetTag("stellaops.cli.tenant", tenant); activity?.SetTag("stellaops.cli.format", formatNormalized); activity?.SetTag("stellaops.cli.input_kind", payload.Kind); var request = new AocIngestDryRunRequest { Tenant = tenant, Source = source, Document = new AocIngestDryRunDocument { Name = payload.Name, Content = payload.Content, ContentType = payload.ContentType, ContentEncoding = payload.ContentEncoding } }; var response = await client.ExecuteAocIngestDryRunAsync(request, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.status", response.Status ?? "unknown"); if (!string.IsNullOrWhiteSpace(output)) { var reportPath = await WriteJsonReportAsync(response, output, cancellationToken).ConfigureAwait(false); logger.LogInformation("Dry-run report written to {Path}.", reportPath); } if (formatNormalized == "json") { var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); Console.WriteLine(json); } else { RenderDryRunTable(response, !disableColor); } var exitCode = DetermineDryRunExitCode(response); Environment.ExitCode = exitCode; statusMetric = exitCode == 0 ? "ok" : "violation"; activity?.SetTag("stellaops.cli.exit_code", exitCode); } catch (Exception ex) { statusMetric = "transport_error"; logger.LogError(ex, "Dry-run ingestion failed."); Environment.ExitCode = 70; } finally { verbosity.MinimumLevel = previousLevel; CliMetrics.RecordSourcesDryRun(statusMetric); } } public static async Task HandleAocVerifyAsync( IServiceProvider services, string? sinceOption, int? limitOption, string? sourcesOption, string? codesOption, string format, string? exportPath, string? tenantOverride, bool disableColor, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("aoc-verify"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.aoc.verify", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("aoc verify"); var outcome = "unknown"; try { var tenant = ResolveTenant(tenantOverride); if (string.IsNullOrWhiteSpace(tenant)) { throw new InvalidOperationException("Tenant must be provided via --tenant or STELLA_TENANT."); } var normalizedFormat = string.IsNullOrWhiteSpace(format) ? "table" : format.Trim().ToLowerInvariant(); if (normalizedFormat is not ("table" or "json")) { throw new InvalidOperationException("Format must be either 'table' or 'json'."); } var since = DetermineVerificationSince(sinceOption); var sinceIso = since.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture); var limit = NormalizeLimit(limitOption); var sources = ParseCommaSeparatedList(sourcesOption); var codes = ParseCommaSeparatedList(codesOption); var normalizedSources = sources.Count == 0 ? Array.Empty() : sources.Select(item => item.ToLowerInvariant()).ToArray(); var normalizedCodes = codes.Count == 0 ? Array.Empty() : codes.Select(item => item.ToUpperInvariant()).ToArray(); activity?.SetTag("stellaops.cli.command", "aoc verify"); activity?.SetTag("stellaops.cli.tenant", tenant); activity?.SetTag("stellaops.cli.since", sinceIso); activity?.SetTag("stellaops.cli.limit", limit); activity?.SetTag("stellaops.cli.format", normalizedFormat); if (normalizedSources.Length > 0) { activity?.SetTag("stellaops.cli.sources", string.Join(",", normalizedSources)); } if (normalizedCodes.Length > 0) { activity?.SetTag("stellaops.cli.codes", string.Join(",", normalizedCodes)); } var request = new AocVerifyRequest { Tenant = tenant, Since = sinceIso, Limit = limit, Sources = normalizedSources.Length == 0 ? null : normalizedSources, Codes = normalizedCodes.Length == 0 ? null : normalizedCodes }; var response = await client.ExecuteAocVerifyAsync(request, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(exportPath)) { var reportPath = await WriteJsonReportAsync(response, exportPath, cancellationToken).ConfigureAwait(false); logger.LogInformation("Verification report written to {Path}.", reportPath); } if (normalizedFormat == "json") { var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); Console.WriteLine(json); } else { RenderAocVerifyTable(response, !disableColor, limit); } var exitCode = DetermineVerifyExitCode(response); Environment.ExitCode = exitCode; activity?.SetTag("stellaops.cli.exit_code", exitCode); outcome = exitCode switch { 0 => "ok", >= 11 and <= 17 => "violations", 18 => "truncated", _ => "unknown" }; } catch (InvalidOperationException ex) { outcome = "usage_error"; logger.LogError(ex, "Verification failed: {Message}", ex.Message); Console.Error.WriteLine(ex.Message); Environment.ExitCode = 71; activity?.SetStatus(ActivityStatusCode.Error, ex.Message); } catch (Exception ex) { outcome = "transport_error"; logger.LogError(ex, "Verification request failed."); Console.Error.WriteLine(ex.Message); Environment.ExitCode = 70; activity?.SetStatus(ActivityStatusCode.Error, ex.Message); } finally { verbosity.MinimumLevel = previousLevel; CliMetrics.RecordAocVerify(outcome); } } public static async Task HandleConnectorJobAsync( IServiceProvider services, string source, string stage, string? mode, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("db-connector"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.db.fetch", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "db fetch"); activity?.SetTag("stellaops.cli.source", source); activity?.SetTag("stellaops.cli.stage", stage); if (!string.IsNullOrWhiteSpace(mode)) { activity?.SetTag("stellaops.cli.mode", mode); } using var duration = CliMetrics.MeasureCommandDuration("db fetch"); try { var jobKind = $"source:{source}:{stage}"; var parameters = new Dictionary(StringComparer.Ordinal); if (!string.IsNullOrWhiteSpace(mode)) { parameters["mode"] = mode; } await TriggerJobAsync(client, logger, jobKind, parameters, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { logger.LogError(ex, "Connector job failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleMergeJobAsync( IServiceProvider services, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("db-merge"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.db.merge", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "db merge"); using var duration = CliMetrics.MeasureCommandDuration("db merge"); try { await TriggerJobAsync(client, logger, "merge:reconcile", new Dictionary(StringComparer.Ordinal), cancellationToken).ConfigureAwait(false); } catch (Exception ex) { logger.LogError(ex, "Merge job failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleExportJobAsync( IServiceProvider services, string format, bool delta, bool? publishFull, bool? publishDelta, bool? includeFull, bool? includeDelta, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("db-export"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.db.export", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "db export"); activity?.SetTag("stellaops.cli.format", format); activity?.SetTag("stellaops.cli.delta", delta); using var duration = CliMetrics.MeasureCommandDuration("db export"); activity?.SetTag("stellaops.cli.publish_full", publishFull); activity?.SetTag("stellaops.cli.publish_delta", publishDelta); activity?.SetTag("stellaops.cli.include_full", includeFull); activity?.SetTag("stellaops.cli.include_delta", includeDelta); try { var jobKind = format switch { "trivy-db" or "trivy" => "export:trivy-db", _ => "export:json" }; var isTrivy = jobKind == "export:trivy-db"; if (isTrivy && !publishFull.HasValue && !publishDelta.HasValue && !includeFull.HasValue && !includeDelta.HasValue && AnsiConsole.Profile.Capabilities.Interactive) { var overrides = TrivyDbExportPrompt.PromptOverrides(); publishFull = overrides.publishFull; publishDelta = overrides.publishDelta; includeFull = overrides.includeFull; includeDelta = overrides.includeDelta; } var parameters = new Dictionary(StringComparer.Ordinal) { ["delta"] = delta }; if (publishFull.HasValue) { parameters["publishFull"] = publishFull.Value; } if (publishDelta.HasValue) { parameters["publishDelta"] = publishDelta.Value; } if (includeFull.HasValue) { parameters["includeFull"] = includeFull.Value; } if (includeDelta.HasValue) { parameters["includeDelta"] = includeDelta.Value; } await TriggerJobAsync(client, logger, jobKind, parameters, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { logger.LogError(ex, "Export job failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static Task HandleExcititorInitAsync( IServiceProvider services, IReadOnlyList providers, bool resume, bool verbose, CancellationToken cancellationToken) { var normalizedProviders = NormalizeProviders(providers); var payload = new Dictionary(StringComparer.Ordinal); if (normalizedProviders.Count > 0) { payload["providers"] = normalizedProviders; } if (resume) { payload["resume"] = true; } return ExecuteExcititorCommandAsync( services, commandName: "excititor init", verbose, new Dictionary { ["providers"] = normalizedProviders.Count, ["resume"] = resume }, client => client.ExecuteExcititorOperationAsync("init", HttpMethod.Post, RemoveNullValues(payload), cancellationToken), cancellationToken); } public static Task HandleExcititorPullAsync( IServiceProvider services, IReadOnlyList providers, DateTimeOffset? since, TimeSpan? window, bool force, bool verbose, CancellationToken cancellationToken) { var normalizedProviders = NormalizeProviders(providers); var payload = new Dictionary(StringComparer.Ordinal); if (normalizedProviders.Count > 0) { payload["providers"] = normalizedProviders; } if (since.HasValue) { payload["since"] = since.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture); } if (window.HasValue) { payload["window"] = window.Value.ToString("c", CultureInfo.InvariantCulture); } if (force) { payload["force"] = true; } return ExecuteExcititorCommandAsync( services, commandName: "excititor pull", verbose, new Dictionary { ["providers"] = normalizedProviders.Count, ["force"] = force, ["since"] = since?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture), ["window"] = window?.ToString("c", CultureInfo.InvariantCulture) }, client => client.ExecuteExcititorOperationAsync("ingest/run", HttpMethod.Post, RemoveNullValues(payload), cancellationToken), cancellationToken); } public static Task HandleExcititorResumeAsync( IServiceProvider services, IReadOnlyList providers, string? checkpoint, bool verbose, CancellationToken cancellationToken) { var normalizedProviders = NormalizeProviders(providers); var payload = new Dictionary(StringComparer.Ordinal); if (normalizedProviders.Count > 0) { payload["providers"] = normalizedProviders; } if (!string.IsNullOrWhiteSpace(checkpoint)) { payload["checkpoint"] = checkpoint.Trim(); } return ExecuteExcititorCommandAsync( services, commandName: "excititor resume", verbose, new Dictionary { ["providers"] = normalizedProviders.Count, ["checkpoint"] = checkpoint }, client => client.ExecuteExcititorOperationAsync("ingest/resume", HttpMethod.Post, RemoveNullValues(payload), cancellationToken), cancellationToken); } public static async Task HandleExcititorListProvidersAsync( IServiceProvider services, bool includeDisabled, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("excititor-list-providers"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.excititor.list-providers", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "excititor list-providers"); activity?.SetTag("stellaops.cli.include_disabled", includeDisabled); using var duration = CliMetrics.MeasureCommandDuration("excititor list-providers"); try { var providers = await client.GetExcititorProvidersAsync(includeDisabled, cancellationToken).ConfigureAwait(false); Environment.ExitCode = 0; logger.LogInformation("Providers returned: {Count}", providers.Count); if (providers.Count > 0) { if (AnsiConsole.Profile.Capabilities.Interactive) { var table = new Table().Border(TableBorder.Rounded).AddColumns("Provider", "Kind", "Trust", "Enabled", "Last Ingested"); foreach (var provider in providers) { table.AddRow( provider.Id, provider.Kind, string.IsNullOrWhiteSpace(provider.TrustTier) ? "-" : provider.TrustTier, provider.Enabled ? "yes" : "no", provider.LastIngestedAt?.ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture) ?? "unknown"); } AnsiConsole.Write(table); } else { foreach (var provider in providers) { logger.LogInformation("{ProviderId} [{Kind}] Enabled={Enabled} Trust={Trust} LastIngested={LastIngested}", provider.Id, provider.Kind, provider.Enabled ? "yes" : "no", string.IsNullOrWhiteSpace(provider.TrustTier) ? "-" : provider.TrustTier, provider.LastIngestedAt?.ToString("O", CultureInfo.InvariantCulture) ?? "unknown"); } } } } catch (Exception ex) { logger.LogError(ex, "Failed to list Excititor providers."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleExcititorExportAsync( IServiceProvider services, string format, bool delta, string? scope, DateTimeOffset? since, string? provider, string? outputPath, bool verbose, CancellationToken cancellationToken) { await using var scopeHandle = services.CreateAsyncScope(); var client = scopeHandle.ServiceProvider.GetRequiredService(); var logger = scopeHandle.ServiceProvider.GetRequiredService().CreateLogger("excititor-export"); var options = scopeHandle.ServiceProvider.GetRequiredService(); var verbosity = scopeHandle.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.excititor.export", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "excititor export"); activity?.SetTag("stellaops.cli.format", format); activity?.SetTag("stellaops.cli.delta", delta); if (!string.IsNullOrWhiteSpace(scope)) { activity?.SetTag("stellaops.cli.scope", scope); } if (since.HasValue) { activity?.SetTag("stellaops.cli.since", since.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture)); } if (!string.IsNullOrWhiteSpace(provider)) { activity?.SetTag("stellaops.cli.provider", provider); } if (!string.IsNullOrWhiteSpace(outputPath)) { activity?.SetTag("stellaops.cli.output", outputPath); } using var duration = CliMetrics.MeasureCommandDuration("excititor export"); try { var payload = new Dictionary(StringComparer.Ordinal) { ["format"] = string.IsNullOrWhiteSpace(format) ? "openvex" : format.Trim(), ["delta"] = delta }; if (!string.IsNullOrWhiteSpace(scope)) { payload["scope"] = scope.Trim(); } if (since.HasValue) { payload["since"] = since.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture); } if (!string.IsNullOrWhiteSpace(provider)) { payload["provider"] = provider.Trim(); } var result = await client.ExecuteExcititorOperationAsync( "export", HttpMethod.Post, RemoveNullValues(payload), cancellationToken).ConfigureAwait(false); if (!result.Success) { logger.LogError(string.IsNullOrWhiteSpace(result.Message) ? "Excititor export failed." : result.Message); Environment.ExitCode = 1; return; } Environment.ExitCode = 0; var manifest = TryParseExportManifest(result.Payload); if (!string.IsNullOrWhiteSpace(result.Message) && (manifest is null || !string.Equals(result.Message, "ok", StringComparison.OrdinalIgnoreCase))) { logger.LogInformation(result.Message); } if (manifest is not null) { activity?.SetTag("stellaops.cli.export_id", manifest.ExportId); if (!string.IsNullOrWhiteSpace(manifest.Format)) { activity?.SetTag("stellaops.cli.export_format", manifest.Format); } if (manifest.FromCache.HasValue) { activity?.SetTag("stellaops.cli.export_cached", manifest.FromCache.Value); } if (manifest.SizeBytes.HasValue) { activity?.SetTag("stellaops.cli.export_size", manifest.SizeBytes.Value); } if (manifest.FromCache == true) { logger.LogInformation("Reusing cached export {ExportId} ({Format}).", manifest.ExportId, manifest.Format ?? "unknown"); } else { logger.LogInformation("Export ready: {ExportId} ({Format}).", manifest.ExportId, manifest.Format ?? "unknown"); } if (manifest.CreatedAt.HasValue) { logger.LogInformation("Created at {CreatedAt}.", manifest.CreatedAt.Value.ToString("u", CultureInfo.InvariantCulture)); } if (!string.IsNullOrWhiteSpace(manifest.Digest)) { var digestDisplay = BuildDigestDisplay(manifest.Algorithm, manifest.Digest); if (manifest.SizeBytes.HasValue) { logger.LogInformation("Digest {Digest} ({Size}).", digestDisplay, FormatSize(manifest.SizeBytes.Value)); } else { logger.LogInformation("Digest {Digest}.", digestDisplay); } } if (!string.IsNullOrWhiteSpace(manifest.RekorLocation)) { if (!string.IsNullOrWhiteSpace(manifest.RekorIndex)) { logger.LogInformation("Rekor entry: {Location} (index {Index}).", manifest.RekorLocation, manifest.RekorIndex); } else { logger.LogInformation("Rekor entry: {Location}.", manifest.RekorLocation); } } if (!string.IsNullOrWhiteSpace(manifest.RekorInclusionUrl) && !string.Equals(manifest.RekorInclusionUrl, manifest.RekorLocation, StringComparison.OrdinalIgnoreCase)) { logger.LogInformation("Rekor inclusion proof: {Url}.", manifest.RekorInclusionUrl); } if (!string.IsNullOrWhiteSpace(outputPath)) { var resolvedPath = ResolveExportOutputPath(outputPath!, manifest); var download = await client.DownloadExcititorExportAsync( manifest.ExportId, resolvedPath, manifest.Algorithm, manifest.Digest, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.export_path", download.Path); if (download.FromCache) { logger.LogInformation("Export already cached at {Path} ({Size}).", download.Path, FormatSize(download.SizeBytes)); } else { logger.LogInformation("Export saved to {Path} ({Size}).", download.Path, FormatSize(download.SizeBytes)); } } else if (!string.IsNullOrWhiteSpace(result.Location)) { var downloadUrl = ResolveLocationUrl(options, result.Location); if (!string.IsNullOrWhiteSpace(downloadUrl)) { logger.LogInformation("Download URL: {Url}", downloadUrl); } else { logger.LogInformation("Download location: {Location}", result.Location); } } } else { if (!string.IsNullOrWhiteSpace(result.Location)) { var downloadUrl = ResolveLocationUrl(options, result.Location); if (!string.IsNullOrWhiteSpace(downloadUrl)) { logger.LogInformation("Download URL: {Url}", downloadUrl); } else { logger.LogInformation("Location: {Location}", result.Location); } } else if (string.IsNullOrWhiteSpace(result.Message)) { logger.LogInformation("Export request accepted."); } } } catch (Exception ex) { logger.LogError(ex, "Excititor export failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static Task HandleExcititorBackfillStatementsAsync( IServiceProvider services, DateTimeOffset? retrievedSince, bool force, int batchSize, int? maxDocuments, bool verbose, CancellationToken cancellationToken) { if (batchSize <= 0) { throw new ArgumentOutOfRangeException(nameof(batchSize), "Batch size must be greater than zero."); } if (maxDocuments.HasValue && maxDocuments.Value <= 0) { throw new ArgumentOutOfRangeException(nameof(maxDocuments), "Max documents must be greater than zero when specified."); } var payload = new Dictionary(StringComparer.Ordinal) { ["force"] = force, ["batchSize"] = batchSize, ["maxDocuments"] = maxDocuments }; if (retrievedSince.HasValue) { payload["retrievedSince"] = retrievedSince.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture); } var activityTags = new Dictionary(StringComparer.Ordinal) { ["stellaops.cli.force"] = force, ["stellaops.cli.batch_size"] = batchSize, ["stellaops.cli.max_documents"] = maxDocuments }; if (retrievedSince.HasValue) { activityTags["stellaops.cli.retrieved_since"] = retrievedSince.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture); } return ExecuteExcititorCommandAsync( services, commandName: "excititor backfill-statements", verbose, activityTags, client => client.ExecuteExcititorOperationAsync( "admin/backfill-statements", HttpMethod.Post, RemoveNullValues(payload), cancellationToken), cancellationToken); } public static Task HandleExcititorVerifyAsync( IServiceProvider services, string? exportId, string? digest, string? attestationPath, bool verbose, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(exportId) && string.IsNullOrWhiteSpace(digest) && string.IsNullOrWhiteSpace(attestationPath)) { var logger = services.GetRequiredService().CreateLogger("excititor-verify"); logger.LogError("At least one of --export-id, --digest, or --attestation must be provided."); Environment.ExitCode = 1; return Task.CompletedTask; } var payload = new Dictionary(StringComparer.Ordinal); if (!string.IsNullOrWhiteSpace(exportId)) { payload["exportId"] = exportId.Trim(); } if (!string.IsNullOrWhiteSpace(digest)) { payload["digest"] = digest.Trim(); } if (!string.IsNullOrWhiteSpace(attestationPath)) { var fullPath = Path.GetFullPath(attestationPath); if (!File.Exists(fullPath)) { var logger = services.GetRequiredService().CreateLogger("excititor-verify"); logger.LogError("Attestation file not found at {Path}.", fullPath); Environment.ExitCode = 1; return Task.CompletedTask; } var bytes = File.ReadAllBytes(fullPath); payload["attestation"] = new Dictionary(StringComparer.Ordinal) { ["fileName"] = Path.GetFileName(fullPath), ["base64"] = Convert.ToBase64String(bytes) }; } return ExecuteExcititorCommandAsync( services, commandName: "excititor verify", verbose, new Dictionary { ["export_id"] = exportId, ["digest"] = digest, ["attestation_path"] = attestationPath }, client => client.ExecuteExcititorOperationAsync("verify", HttpMethod.Post, RemoveNullValues(payload), cancellationToken), cancellationToken); } public static Task HandleExcititorReconcileAsync( IServiceProvider services, IReadOnlyList providers, TimeSpan? maxAge, bool verbose, CancellationToken cancellationToken) { var normalizedProviders = NormalizeProviders(providers); var payload = new Dictionary(StringComparer.Ordinal); if (normalizedProviders.Count > 0) { payload["providers"] = normalizedProviders; } if (maxAge.HasValue) { payload["maxAge"] = maxAge.Value.ToString("c", CultureInfo.InvariantCulture); } return ExecuteExcititorCommandAsync( services, commandName: "excititor reconcile", verbose, new Dictionary { ["providers"] = normalizedProviders.Count, ["max_age"] = maxAge?.ToString("c", CultureInfo.InvariantCulture) }, client => client.ExecuteExcititorOperationAsync("reconcile", HttpMethod.Post, RemoveNullValues(payload), cancellationToken), cancellationToken); } public static async Task HandleRuntimePolicyTestAsync( IServiceProvider services, string? namespaceValue, IReadOnlyList imageArguments, string? filePath, IReadOnlyList labelArguments, bool outputJson, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("runtime-policy-test"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.runtime.policy.test", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "runtime policy test"); if (!string.IsNullOrWhiteSpace(namespaceValue)) { activity?.SetTag("stellaops.cli.namespace", namespaceValue); } using var duration = CliMetrics.MeasureCommandDuration("runtime policy test"); try { IReadOnlyList images; try { images = await GatherImageDigestsAsync(imageArguments, filePath, cancellationToken).ConfigureAwait(false); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or FileNotFoundException) { logger.LogError(ex, "Failed to gather image digests: {Message}", ex.Message); Environment.ExitCode = 9; return; } if (images.Count == 0) { logger.LogError("No image digests provided. Use --image, --file, or pipe digests via stdin."); Environment.ExitCode = 9; return; } IReadOnlyDictionary labels; try { labels = ParseLabelSelectors(labelArguments); } catch (ArgumentException ex) { logger.LogError(ex.Message); Environment.ExitCode = 9; return; } activity?.SetTag("stellaops.cli.images", images.Count); activity?.SetTag("stellaops.cli.labels", labels.Count); var request = new RuntimePolicyEvaluationRequest(namespaceValue, labels, images); var result = await client.EvaluateRuntimePolicyAsync(request, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.ttl_seconds", result.TtlSeconds); Environment.ExitCode = 0; if (outputJson) { var json = BuildRuntimePolicyJson(result, images); Console.WriteLine(json); return; } if (result.ExpiresAtUtc.HasValue) { logger.LogInformation("Decision TTL: {TtlSeconds}s (expires {ExpiresAt})", result.TtlSeconds, result.ExpiresAtUtc.Value.ToString("u", CultureInfo.InvariantCulture)); } else { logger.LogInformation("Decision TTL: {TtlSeconds}s", result.TtlSeconds); } if (!string.IsNullOrWhiteSpace(result.PolicyRevision)) { logger.LogInformation("Policy revision: {Revision}", result.PolicyRevision); } DisplayRuntimePolicyResults(logger, result, images); } catch (Exception ex) { logger.LogError(ex, "Runtime policy evaluation failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleAuthLoginAsync( IServiceProvider services, StellaOpsCliOptions options, bool verbose, bool force, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("auth-login"); Environment.ExitCode = 0; if (string.IsNullOrWhiteSpace(options.Authority?.Url)) { logger.LogError("Authority URL is not configured. Set STELLAOPS_AUTHORITY_URL or update your configuration."); Environment.ExitCode = 1; return; } var tokenClient = scope.ServiceProvider.GetService(); if (tokenClient is null) { logger.LogError("Authority client is not available. Ensure AddStellaOpsAuthClient is registered in Program.cs."); Environment.ExitCode = 1; return; } var cacheKey = AuthorityTokenUtilities.BuildCacheKey(options); if (string.IsNullOrWhiteSpace(cacheKey)) { logger.LogError("Authority configuration is incomplete; unable to determine cache key."); Environment.ExitCode = 1; return; } try { if (force) { await tokenClient.ClearCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false); } var scopeName = AuthorityTokenUtilities.ResolveScope(options); StellaOpsTokenResult token; if (!string.IsNullOrWhiteSpace(options.Authority.Username)) { if (string.IsNullOrWhiteSpace(options.Authority.Password)) { logger.LogError("Authority password must be provided when username is configured."); Environment.ExitCode = 1; return; } token = await tokenClient.RequestPasswordTokenAsync( options.Authority.Username, options.Authority.Password!, scopeName, null, cancellationToken).ConfigureAwait(false); } else { token = await tokenClient.RequestClientCredentialsTokenAsync(scopeName, null, cancellationToken).ConfigureAwait(false); } await tokenClient.CacheTokenAsync(cacheKey, token.ToCacheEntry(), cancellationToken).ConfigureAwait(false); if (verbose) { logger.LogInformation("Authenticated with {Authority} (scopes: {Scopes}).", options.Authority.Url, string.Join(", ", token.Scopes)); } logger.LogInformation("Login successful. Access token expires at {Expires}.", token.ExpiresAtUtc.ToString("u")); } catch (Exception ex) { logger.LogError(ex, "Authentication failed: {Message}", ex.Message); Environment.ExitCode = 1; } } public static async Task HandleAuthLogoutAsync( IServiceProvider services, StellaOpsCliOptions options, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("auth-logout"); Environment.ExitCode = 0; var tokenClient = scope.ServiceProvider.GetService(); if (tokenClient is null) { logger.LogInformation("No authority client registered; nothing to remove."); return; } var cacheKey = AuthorityTokenUtilities.BuildCacheKey(options); if (string.IsNullOrWhiteSpace(cacheKey)) { logger.LogInformation("Authority configuration missing; no cached tokens to remove."); return; } await tokenClient.ClearCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (verbose) { logger.LogInformation("Cleared cached token for {Authority}.", options.Authority?.Url ?? "authority"); } } public static async Task HandleAuthStatusAsync( IServiceProvider services, StellaOpsCliOptions options, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("auth-status"); Environment.ExitCode = 0; if (string.IsNullOrWhiteSpace(options.Authority?.Url)) { logger.LogInformation("Authority URL not configured. Set STELLAOPS_AUTHORITY_URL and run 'auth login'."); Environment.ExitCode = 1; return; } var tokenClient = scope.ServiceProvider.GetService(); if (tokenClient is null) { logger.LogInformation("Authority client not registered; no cached tokens available."); Environment.ExitCode = 1; return; } var cacheKey = AuthorityTokenUtilities.BuildCacheKey(options); if (string.IsNullOrWhiteSpace(cacheKey)) { logger.LogInformation("Authority configuration incomplete; no cached tokens available."); Environment.ExitCode = 1; return; } var entry = await tokenClient.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (entry is null) { logger.LogInformation("No cached token for {Authority}. Run 'auth login' to authenticate.", options.Authority.Url); Environment.ExitCode = 1; return; } logger.LogInformation("Cached token for {Authority} expires at {Expires}.", options.Authority.Url, entry.ExpiresAtUtc.ToString("u")); if (verbose) { logger.LogInformation("Scopes: {Scopes}", string.Join(", ", entry.Scopes)); } } public static async Task HandleAuthWhoAmIAsync( IServiceProvider services, StellaOpsCliOptions options, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("auth-whoami"); Environment.ExitCode = 0; if (string.IsNullOrWhiteSpace(options.Authority?.Url)) { logger.LogInformation("Authority URL not configured. Set STELLAOPS_AUTHORITY_URL and run 'auth login'."); Environment.ExitCode = 1; return; } var tokenClient = scope.ServiceProvider.GetService(); if (tokenClient is null) { logger.LogInformation("Authority client not registered; no cached tokens available."); Environment.ExitCode = 1; return; } var cacheKey = AuthorityTokenUtilities.BuildCacheKey(options); if (string.IsNullOrWhiteSpace(cacheKey)) { logger.LogInformation("Authority configuration incomplete; no cached tokens available."); Environment.ExitCode = 1; return; } var entry = await tokenClient.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (entry is null) { logger.LogInformation("No cached token for {Authority}. Run 'auth login' to authenticate.", options.Authority.Url); Environment.ExitCode = 1; return; } var grantType = string.IsNullOrWhiteSpace(options.Authority.Username) ? "client_credentials" : "password"; var now = DateTimeOffset.UtcNow; var remaining = entry.ExpiresAtUtc - now; if (remaining < TimeSpan.Zero) { remaining = TimeSpan.Zero; } logger.LogInformation("Authority: {Authority}", options.Authority.Url); logger.LogInformation("Grant type: {GrantType}", grantType); logger.LogInformation("Token type: {TokenType}", entry.TokenType); logger.LogInformation("Expires: {Expires} ({Remaining})", entry.ExpiresAtUtc.ToString("u"), FormatDuration(remaining)); if (entry.Scopes.Count > 0) { logger.LogInformation("Scopes: {Scopes}", string.Join(", ", entry.Scopes)); } if (TryExtractJwtClaims(entry.AccessToken, out var claims, out var issuedAt, out var notBefore)) { if (claims.TryGetValue("sub", out var subject) && !string.IsNullOrWhiteSpace(subject)) { logger.LogInformation("Subject: {Subject}", subject); } if (claims.TryGetValue("client_id", out var clientId) && !string.IsNullOrWhiteSpace(clientId)) { logger.LogInformation("Client ID (token): {ClientId}", clientId); } if (claims.TryGetValue("aud", out var audience) && !string.IsNullOrWhiteSpace(audience)) { logger.LogInformation("Audience: {Audience}", audience); } if (claims.TryGetValue("iss", out var issuer) && !string.IsNullOrWhiteSpace(issuer)) { logger.LogInformation("Issuer: {Issuer}", issuer); } if (issuedAt is not null) { logger.LogInformation("Issued at: {IssuedAt}", issuedAt.Value.ToString("u")); } if (notBefore is not null) { logger.LogInformation("Not before: {NotBefore}", notBefore.Value.ToString("u")); } var extraClaims = CollectAdditionalClaims(claims); if (extraClaims.Count > 0 && verbose) { logger.LogInformation("Additional claims: {Claims}", string.Join(", ", extraClaims)); } } else { logger.LogInformation("Access token appears opaque; claims are unavailable."); } } public static async Task HandleAuthRevokeExportAsync( IServiceProvider services, StellaOpsCliOptions options, string? outputDirectory, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("auth-revoke-export"); Environment.ExitCode = 0; try { var client = scope.ServiceProvider.GetRequiredService(); var result = await client.ExportAsync(verbose, cancellationToken).ConfigureAwait(false); var directory = string.IsNullOrWhiteSpace(outputDirectory) ? Directory.GetCurrentDirectory() : Path.GetFullPath(outputDirectory); Directory.CreateDirectory(directory); var bundlePath = Path.Combine(directory, "revocation-bundle.json"); var signaturePath = Path.Combine(directory, "revocation-bundle.json.jws"); var digestPath = Path.Combine(directory, "revocation-bundle.json.sha256"); await File.WriteAllBytesAsync(bundlePath, result.BundleBytes, cancellationToken).ConfigureAwait(false); await File.WriteAllTextAsync(signaturePath, result.Signature, cancellationToken).ConfigureAwait(false); await File.WriteAllTextAsync(digestPath, $"sha256:{result.Digest}", cancellationToken).ConfigureAwait(false); var computedDigest = Convert.ToHexString(SHA256.HashData(result.BundleBytes)).ToLowerInvariant(); if (!string.Equals(computedDigest, result.Digest, StringComparison.OrdinalIgnoreCase)) { logger.LogError("Digest mismatch. Expected {Expected} but computed {Actual}.", result.Digest, computedDigest); Environment.ExitCode = 1; return; } logger.LogInformation( "Revocation bundle exported to {Directory} (sequence {Sequence}, issued {Issued:u}, signing key {KeyId}, provider {Provider}).", directory, result.Sequence, result.IssuedAt, string.IsNullOrWhiteSpace(result.SigningKeyId) ? "" : result.SigningKeyId, string.IsNullOrWhiteSpace(result.SigningProvider) ? "default" : result.SigningProvider); } catch (Exception ex) { logger.LogError(ex, "Failed to export revocation bundle."); Environment.ExitCode = 1; } } public static async Task HandleAuthRevokeVerifyAsync( string bundlePath, string signaturePath, string keyPath, bool verbose, CancellationToken cancellationToken) { var loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(options => { options.SingleLine = true; options.TimestampFormat = "HH:mm:ss "; })); var logger = loggerFactory.CreateLogger("auth-revoke-verify"); Environment.ExitCode = 0; try { if (string.IsNullOrWhiteSpace(bundlePath) || string.IsNullOrWhiteSpace(signaturePath) || string.IsNullOrWhiteSpace(keyPath)) { logger.LogError("Arguments --bundle, --signature, and --key are required."); Environment.ExitCode = 1; return; } var bundleBytes = await File.ReadAllBytesAsync(bundlePath, cancellationToken).ConfigureAwait(false); var signatureContent = (await File.ReadAllTextAsync(signaturePath, cancellationToken).ConfigureAwait(false)).Trim(); var keyPem = await File.ReadAllTextAsync(keyPath, cancellationToken).ConfigureAwait(false); var digest = Convert.ToHexString(SHA256.HashData(bundleBytes)).ToLowerInvariant(); logger.LogInformation("Bundle digest sha256:{Digest}", digest); if (!TryParseDetachedJws(signatureContent, out var encodedHeader, out var encodedSignature)) { logger.LogError("Signature is not in detached JWS format."); Environment.ExitCode = 1; return; } var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(encodedHeader)); using var headerDocument = JsonDocument.Parse(headerJson); var header = headerDocument.RootElement; if (!header.TryGetProperty("b64", out var b64Element) || b64Element.GetBoolean()) { logger.LogError("Detached JWS header must include '\"b64\": false'."); Environment.ExitCode = 1; return; } var algorithm = header.TryGetProperty("alg", out var algElement) ? algElement.GetString() : SignatureAlgorithms.Es256; if (string.IsNullOrWhiteSpace(algorithm)) { algorithm = SignatureAlgorithms.Es256; } var providerHint = header.TryGetProperty("provider", out var providerElement) ? providerElement.GetString() : null; var keyId = header.TryGetProperty("kid", out var kidElement) ? kidElement.GetString() : null; if (string.IsNullOrWhiteSpace(keyId)) { keyId = Path.GetFileNameWithoutExtension(keyPath); logger.LogWarning("JWS header missing 'kid'; using fallback key id {KeyId}.", keyId); } CryptoSigningKey signingKey; try { signingKey = CreateVerificationSigningKey(keyId!, algorithm!, providerHint, keyPem, keyPath); } catch (Exception ex) when (ex is InvalidOperationException or CryptographicException) { logger.LogError(ex, "Failed to load verification key material."); Environment.ExitCode = 1; return; } var providers = new List { new DefaultCryptoProvider() }; #if STELLAOPS_CRYPTO_SODIUM providers.Add(new LibsodiumCryptoProvider()); #endif foreach (var provider in providers) { if (provider.Supports(CryptoCapability.Verification, algorithm!)) { provider.UpsertSigningKey(signingKey); } } var preferredOrder = !string.IsNullOrWhiteSpace(providerHint) ? new[] { providerHint! } : Array.Empty(); var registry = new CryptoProviderRegistry(providers, preferredOrder); CryptoSignerResolution resolution; try { resolution = registry.ResolveSigner( CryptoCapability.Verification, algorithm!, signingKey.Reference, providerHint); } catch (Exception ex) { logger.LogError(ex, "No crypto provider available for verification (algorithm {Algorithm}).", algorithm); Environment.ExitCode = 1; return; } var signingInputLength = encodedHeader.Length + 1 + bundleBytes.Length; var buffer = ArrayPool.Shared.Rent(signingInputLength); try { var headerBytes = Encoding.ASCII.GetBytes(encodedHeader); Buffer.BlockCopy(headerBytes, 0, buffer, 0, headerBytes.Length); buffer[headerBytes.Length] = (byte)'.'; Buffer.BlockCopy(bundleBytes, 0, buffer, headerBytes.Length + 1, bundleBytes.Length); var signatureBytes = Base64UrlDecode(encodedSignature); var verified = await resolution.Signer.VerifyAsync( new ReadOnlyMemory(buffer, 0, signingInputLength), signatureBytes, cancellationToken).ConfigureAwait(false); if (!verified) { logger.LogError("Signature verification failed."); Environment.ExitCode = 1; return; } } finally { ArrayPool.Shared.Return(buffer); } if (!string.IsNullOrWhiteSpace(providerHint) && !string.Equals(providerHint, resolution.ProviderName, StringComparison.OrdinalIgnoreCase)) { logger.LogWarning( "Preferred provider '{Preferred}' unavailable; verification used '{Provider}'.", providerHint, resolution.ProviderName); } logger.LogInformation( "Signature verified using algorithm {Algorithm} via provider {Provider} (kid {KeyId}).", algorithm, resolution.ProviderName, signingKey.Reference.KeyId); if (verbose) { logger.LogInformation("JWS header: {Header}", headerJson); } } catch (Exception ex) { logger.LogError(ex, "Failed to verify revocation bundle."); Environment.ExitCode = 1; } finally { loggerFactory.Dispose(); } } public static async Task HandleVulnObservationsAsync( IServiceProvider services, string tenant, IReadOnlyList observationIds, IReadOnlyList aliases, IReadOnlyList purls, IReadOnlyList cpes, int? limit, string? cursor, bool emitJson, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("vuln-observations"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.vuln.observations", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "vuln observations"); activity?.SetTag("stellaops.cli.tenant", tenant); using var duration = CliMetrics.MeasureCommandDuration("vuln observations"); try { tenant = tenant?.Trim().ToLowerInvariant() ?? string.Empty; if (string.IsNullOrWhiteSpace(tenant)) { throw new InvalidOperationException("Tenant must be provided."); } var query = new AdvisoryObservationsQuery( tenant, NormalizeSet(observationIds, toLower: false), NormalizeSet(aliases, toLower: true), NormalizeSet(purls, toLower: false), NormalizeSet(cpes, toLower: false), limit, cursor); var response = await client.GetObservationsAsync(query, cancellationToken).ConfigureAwait(false); if (emitJson) { var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); Console.WriteLine(json); Environment.ExitCode = 0; return; } RenderObservationTable(response); if (!emitJson && response.HasMore && !string.IsNullOrWhiteSpace(response.NextCursor)) { var escapedCursor = Markup.Escape(response.NextCursor); AnsiConsole.MarkupLine($"[yellow]More observations available. Continue with[/] [cyan]--cursor[/] [grey]{escapedCursor}[/]"); } Environment.ExitCode = 0; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { logger.LogWarning("Operation cancelled by user."); Environment.ExitCode = 130; } catch (Exception ex) { logger.LogError(ex, "Failed to fetch observations from Concelier."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } static IReadOnlyList NormalizeSet(IReadOnlyList values, bool toLower) { if (values is null || values.Count == 0) { return Array.Empty(); } var set = new HashSet(StringComparer.Ordinal); foreach (var raw in values) { if (string.IsNullOrWhiteSpace(raw)) { continue; } var normalized = raw.Trim(); if (toLower) { normalized = normalized.ToLowerInvariant(); } set.Add(normalized); } return set.Count == 0 ? Array.Empty() : set.ToArray(); } static void RenderObservationTable(AdvisoryObservationsResponse response) { var observations = response.Observations ?? Array.Empty(); if (observations.Count == 0) { AnsiConsole.MarkupLine("[yellow]No observations matched the provided filters.[/]"); return; } var table = new Table() .Centered() .Border(TableBorder.Rounded); table.AddColumn("Observation"); table.AddColumn("Source"); table.AddColumn("Upstream Id"); table.AddColumn("Aliases"); table.AddColumn("PURLs"); table.AddColumn("CPEs"); table.AddColumn("Created (UTC)"); foreach (var observation in observations) { var sourceVendor = observation.Source?.Vendor ?? "(unknown)"; var upstreamId = observation.Upstream?.UpstreamId ?? "(unknown)"; var aliasesText = FormatList(observation.Linkset?.Aliases); var purlsText = FormatList(observation.Linkset?.Purls); var cpesText = FormatList(observation.Linkset?.Cpes); table.AddRow( Markup.Escape(observation.ObservationId), Markup.Escape(sourceVendor), Markup.Escape(upstreamId), Markup.Escape(aliasesText), Markup.Escape(purlsText), Markup.Escape(cpesText), observation.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture)); } AnsiConsole.Write(table); AnsiConsole.MarkupLine( "[green]{0}[/] observation(s). Aliases: [green]{1}[/], PURLs: [green]{2}[/], CPEs: [green]{3}[/].", observations.Count, response.Linkset?.Aliases?.Count ?? 0, response.Linkset?.Purls?.Count ?? 0, response.Linkset?.Cpes?.Count ?? 0); } static string FormatList(IReadOnlyList? values) { if (values is null || values.Count == 0) { return "(none)"; } const int MaxItems = 3; if (values.Count <= MaxItems) { return string.Join(", ", values); } var preview = values.Take(MaxItems); return $"{string.Join(", ", preview)} (+{values.Count - MaxItems})"; } } public static async Task HandleOfflineKitPullAsync( IServiceProvider services, string? bundleId, string? destinationDirectory, bool overwrite, bool resume, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("offline-kit-pull"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.offline.kit.pull", ActivityKind.Client); activity?.SetTag("stellaops.cli.bundle_id", string.IsNullOrWhiteSpace(bundleId) ? "latest" : bundleId); using var duration = CliMetrics.MeasureCommandDuration("offline kit pull"); try { var targetDirectory = string.IsNullOrWhiteSpace(destinationDirectory) ? options.Offline?.KitsDirectory ?? Path.Combine(Environment.CurrentDirectory, "offline-kits") : destinationDirectory; targetDirectory = Path.GetFullPath(targetDirectory); Directory.CreateDirectory(targetDirectory); var result = await client.DownloadOfflineKitAsync(bundleId, targetDirectory, overwrite, resume, cancellationToken).ConfigureAwait(false); logger.LogInformation( "Bundle {BundleId} stored at {Path} (captured {Captured:u}, sha256:{Digest}).", result.Descriptor.BundleId, result.BundlePath, result.Descriptor.CapturedAt, result.Descriptor.BundleSha256); logger.LogInformation("Manifest saved to {Manifest}.", result.ManifestPath); if (!string.IsNullOrWhiteSpace(result.MetadataPath)) { logger.LogDebug("Metadata recorded at {Metadata}.", result.MetadataPath); } if (result.BundleSignaturePath is not null) { logger.LogInformation("Bundle signature saved to {Signature}.", result.BundleSignaturePath); } if (result.ManifestSignaturePath is not null) { logger.LogInformation("Manifest signature saved to {Signature}.", result.ManifestSignaturePath); } CliMetrics.RecordOfflineKitDownload(result.Descriptor.Kind ?? "unknown", result.FromCache); activity?.SetTag("stellaops.cli.bundle_cache", result.FromCache); Environment.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to download offline kit bundle."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandlePolicyFindingsListAsync( IServiceProvider services, string policyId, string[] sbomFilters, string[] statusFilters, string[] severityFilters, string? since, string? cursor, int? page, int? pageSize, string? format, string? outputPath, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("policy-findings-ls"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.policy.findings.list", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("policy findings list"); try { if (string.IsNullOrWhiteSpace(policyId)) { throw new ArgumentException("Policy identifier must be provided.", nameof(policyId)); } if (page.HasValue && page.Value < 1) { throw new ArgumentException("--page must be greater than or equal to 1.", nameof(page)); } if (pageSize.HasValue && (pageSize.Value < 1 || pageSize.Value > 500)) { throw new ArgumentException("--page-size must be between 1 and 500.", nameof(pageSize)); } var normalizedPolicyId = policyId.Trim(); var sboms = NormalizePolicyFilterValues(sbomFilters); var statuses = NormalizePolicyFilterValues(statusFilters, toLower: true); var severities = NormalizePolicyFilterValues(severityFilters); var sinceValue = ParsePolicySince(since); var cursorValue = string.IsNullOrWhiteSpace(cursor) ? null : cursor.Trim(); var query = new PolicyFindingsQuery( normalizedPolicyId, sboms, statuses, severities, cursorValue, page, pageSize, sinceValue); activity?.SetTag("stellaops.cli.policy_id", normalizedPolicyId); if (sboms.Count > 0) { activity?.SetTag("stellaops.cli.findings.sbom_filters", string.Join(",", sboms)); } if (statuses.Count > 0) { activity?.SetTag("stellaops.cli.findings.status_filters", string.Join(",", statuses)); } if (severities.Count > 0) { activity?.SetTag("stellaops.cli.findings.severity_filters", string.Join(",", severities)); } if (!string.IsNullOrWhiteSpace(cursorValue)) { activity?.SetTag("stellaops.cli.findings.cursor", cursorValue); } if (page.HasValue) { activity?.SetTag("stellaops.cli.findings.page", page.Value); } if (pageSize.HasValue) { activity?.SetTag("stellaops.cli.findings.page_size", pageSize.Value); } if (sinceValue.HasValue) { activity?.SetTag("stellaops.cli.findings.since", sinceValue.Value.ToString("o", CultureInfo.InvariantCulture)); } var result = await client.GetPolicyFindingsAsync(query, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.findings.count", result.Items.Count); if (!string.IsNullOrWhiteSpace(result.NextCursor)) { activity?.SetTag("stellaops.cli.findings.next_cursor", result.NextCursor); } var payload = BuildPolicyFindingsPayload(normalizedPolicyId, query, result); if (!string.IsNullOrWhiteSpace(outputPath)) { await WriteJsonPayloadAsync(outputPath!, payload, cancellationToken).ConfigureAwait(false); logger.LogInformation("Results written to {Path}.", Path.GetFullPath(outputPath!)); } var outputFormat = DeterminePolicyFindingsFormat(format, outputPath); if (outputFormat == PolicyFindingsOutputFormat.Json) { var json = JsonSerializer.Serialize(payload, SimulationJsonOptions); Console.WriteLine(json); } else { RenderPolicyFindingsTable(logger, result); } CliMetrics.RecordPolicyFindingsList(result.Items.Count == 0 ? "empty" : "ok"); Environment.ExitCode = 0; } catch (ArgumentException ex) { logger.LogError(ex.Message); CliMetrics.RecordPolicyFindingsList("error"); Environment.ExitCode = 64; } catch (PolicyApiException ex) { HandlePolicyFindingsFailure(ex, logger, CliMetrics.RecordPolicyFindingsList); } catch (Exception ex) { logger.LogError(ex, "Failed to list policy findings."); CliMetrics.RecordPolicyFindingsList("error"); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandlePolicyFindingsGetAsync( IServiceProvider services, string policyId, string findingId, string? format, string? outputPath, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("policy-findings-get"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.policy.findings.get", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("policy findings get"); try { if (string.IsNullOrWhiteSpace(policyId)) { throw new ArgumentException("Policy identifier must be provided.", nameof(policyId)); } if (string.IsNullOrWhiteSpace(findingId)) { throw new ArgumentException("Finding identifier must be provided.", nameof(findingId)); } var normalizedPolicyId = policyId.Trim(); var normalizedFindingId = findingId.Trim(); activity?.SetTag("stellaops.cli.policy_id", normalizedPolicyId); activity?.SetTag("stellaops.cli.finding_id", normalizedFindingId); var result = await client.GetPolicyFindingAsync(normalizedPolicyId, normalizedFindingId, cancellationToken).ConfigureAwait(false); var payload = BuildPolicyFindingPayload(normalizedPolicyId, result); if (!string.IsNullOrWhiteSpace(outputPath)) { await WriteJsonPayloadAsync(outputPath!, payload, cancellationToken).ConfigureAwait(false); logger.LogInformation("Finding written to {Path}.", Path.GetFullPath(outputPath!)); } var outputFormat = DeterminePolicyFindingsFormat(format, outputPath); if (outputFormat == PolicyFindingsOutputFormat.Json) { Console.WriteLine(JsonSerializer.Serialize(payload, SimulationJsonOptions)); } else { RenderPolicyFindingDetails(logger, result); } var outcome = string.IsNullOrWhiteSpace(result.Status) ? "unknown" : result.Status.ToLowerInvariant(); CliMetrics.RecordPolicyFindingsGet(outcome); Environment.ExitCode = 0; } catch (ArgumentException ex) { logger.LogError(ex.Message); CliMetrics.RecordPolicyFindingsGet("error"); Environment.ExitCode = 64; } catch (PolicyApiException ex) { HandlePolicyFindingsFailure(ex, logger, CliMetrics.RecordPolicyFindingsGet); } catch (Exception ex) { logger.LogError(ex, "Failed to retrieve policy finding."); CliMetrics.RecordPolicyFindingsGet("error"); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandlePolicyFindingsExplainAsync( IServiceProvider services, string policyId, string findingId, string? mode, string? format, string? outputPath, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("policy-findings-explain"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.policy.findings.explain", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("policy findings explain"); try { if (string.IsNullOrWhiteSpace(policyId)) { throw new ArgumentException("Policy identifier must be provided.", nameof(policyId)); } if (string.IsNullOrWhiteSpace(findingId)) { throw new ArgumentException("Finding identifier must be provided.", nameof(findingId)); } var normalizedPolicyId = policyId.Trim(); var normalizedFindingId = findingId.Trim(); var normalizedMode = NormalizeExplainMode(mode); activity?.SetTag("stellaops.cli.policy_id", normalizedPolicyId); activity?.SetTag("stellaops.cli.finding_id", normalizedFindingId); if (!string.IsNullOrWhiteSpace(normalizedMode)) { activity?.SetTag("stellaops.cli.findings.mode", normalizedMode); } var result = await client.GetPolicyFindingExplainAsync(normalizedPolicyId, normalizedFindingId, normalizedMode, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.findings.step_count", result.Steps.Count); var payload = BuildPolicyFindingExplainPayload(normalizedPolicyId, normalizedFindingId, normalizedMode, result); if (!string.IsNullOrWhiteSpace(outputPath)) { await WriteJsonPayloadAsync(outputPath!, payload, cancellationToken).ConfigureAwait(false); logger.LogInformation("Explain trace written to {Path}.", Path.GetFullPath(outputPath!)); } var outputFormat = DeterminePolicyFindingsFormat(format, outputPath); if (outputFormat == PolicyFindingsOutputFormat.Json) { Console.WriteLine(JsonSerializer.Serialize(payload, SimulationJsonOptions)); } else { RenderPolicyFindingExplain(logger, result); } CliMetrics.RecordPolicyFindingsExplain(result.Steps.Count == 0 ? "empty" : "ok"); Environment.ExitCode = 0; } catch (ArgumentException ex) { logger.LogError(ex.Message); CliMetrics.RecordPolicyFindingsExplain("error"); Environment.ExitCode = 64; } catch (PolicyApiException ex) { HandlePolicyFindingsFailure(ex, logger, CliMetrics.RecordPolicyFindingsExplain); } catch (Exception ex) { logger.LogError(ex, "Failed to fetch policy explain trace."); CliMetrics.RecordPolicyFindingsExplain("error"); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandlePolicyActivateAsync( IServiceProvider services, string policyId, int version, string? note, bool runNow, string? scheduledAt, string? priority, bool rollback, string? incidentId, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("policy-activate"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.policy.activate", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "policy activate"); using var duration = CliMetrics.MeasureCommandDuration("policy activate"); try { if (string.IsNullOrWhiteSpace(policyId)) { throw new ArgumentException("Policy identifier must be provided.", nameof(policyId)); } if (version <= 0) { throw new ArgumentOutOfRangeException(nameof(version), "Version must be greater than zero."); } var normalizedPolicyId = policyId.Trim(); DateTimeOffset? scheduled = null; if (!string.IsNullOrWhiteSpace(scheduledAt)) { if (!DateTimeOffset.TryParse(scheduledAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed)) { throw new ArgumentException("Scheduled timestamp must be a valid ISO-8601 value.", nameof(scheduledAt)); } scheduled = parsed; } var request = new PolicyActivationRequest( runNow, scheduled, NormalizePolicyPriority(priority), rollback, string.IsNullOrWhiteSpace(incidentId) ? null : incidentId.Trim(), string.IsNullOrWhiteSpace(note) ? null : note.Trim()); activity?.SetTag("stellaops.cli.policy_id", normalizedPolicyId); activity?.SetTag("stellaops.cli.policy_version", version); if (request.RunNow) { activity?.SetTag("stellaops.cli.policy_run_now", true); } if (request.ScheduledAt.HasValue) { activity?.SetTag("stellaops.cli.policy_scheduled_at", request.ScheduledAt.Value.ToString("o", CultureInfo.InvariantCulture)); } if (!string.IsNullOrWhiteSpace(request.Priority)) { activity?.SetTag("stellaops.cli.policy_priority", request.Priority); } if (request.Rollback) { activity?.SetTag("stellaops.cli.policy_rollback", true); } var result = await client.ActivatePolicyRevisionAsync(normalizedPolicyId, version, request, cancellationToken).ConfigureAwait(false); var outcome = NormalizePolicyActivationOutcome(result.Status); CliMetrics.RecordPolicyActivation(outcome); RenderPolicyActivationResult(result, request); var exitCode = DeterminePolicyActivationExitCode(outcome); Environment.ExitCode = exitCode; if (exitCode == 0) { logger.LogInformation("Policy {PolicyId} v{Version} activation status: {Status}.", result.Revision.PolicyId, result.Revision.Version, outcome); } else { logger.LogWarning("Policy {PolicyId} v{Version} requires additional approval (status: {Status}).", result.Revision.PolicyId, result.Revision.Version, outcome); } } catch (ArgumentException ex) { logger.LogError(ex.Message); CliMetrics.RecordPolicyActivation("error"); Environment.ExitCode = 64; } catch (PolicyApiException ex) { HandlePolicyActivationFailure(ex, logger); } catch (Exception ex) { logger.LogError(ex, "Policy activation failed."); CliMetrics.RecordPolicyActivation("error"); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandlePolicySimulateAsync( IServiceProvider services, string policyId, int? baseVersion, int? candidateVersion, IReadOnlyList sbomArguments, IReadOnlyList environmentArguments, string? format, string? outputPath, bool explain, bool failOnDiff, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("policy-simulate"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.policy.simulate", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "policy simulate"); activity?.SetTag("stellaops.cli.policy_id", policyId); if (baseVersion.HasValue) { activity?.SetTag("stellaops.cli.base_version", baseVersion.Value); } if (candidateVersion.HasValue) { activity?.SetTag("stellaops.cli.candidate_version", candidateVersion.Value); } using var duration = CliMetrics.MeasureCommandDuration("policy simulate"); try { if (string.IsNullOrWhiteSpace(policyId)) { throw new ArgumentException("Policy identifier must be provided.", nameof(policyId)); } var normalizedPolicyId = policyId.Trim(); var sbomSet = NormalizePolicySbomSet(sbomArguments); var environment = ParsePolicyEnvironment(environmentArguments); var input = new PolicySimulationInput( baseVersion, candidateVersion, sbomSet, environment, explain); var result = await client.SimulatePolicyAsync(normalizedPolicyId, input, cancellationToken).ConfigureAwait(false); activity?.SetTag("stellaops.cli.diff_added", result.Diff.Added); activity?.SetTag("stellaops.cli.diff_removed", result.Diff.Removed); if (result.Diff.BySeverity.Count > 0) { activity?.SetTag("stellaops.cli.severity_buckets", result.Diff.BySeverity.Count); } var outputFormat = DeterminePolicySimulationFormat(format, outputPath); var payload = BuildPolicySimulationPayload(normalizedPolicyId, baseVersion, candidateVersion, sbomSet, environment, result); if (!string.IsNullOrWhiteSpace(outputPath)) { await WriteSimulationOutputAsync(outputPath!, payload, cancellationToken).ConfigureAwait(false); logger.LogInformation("Simulation results written to {Path}.", Path.GetFullPath(outputPath!)); } RenderPolicySimulationResult(logger, payload, result, outputFormat); var exitCode = DetermineSimulationExitCode(result, failOnDiff); Environment.ExitCode = exitCode; var outcome = exitCode == 20 ? "diff_blocked" : (result.Diff.Added + result.Diff.Removed) > 0 ? "diff" : "clean"; CliMetrics.RecordPolicySimulation(outcome); if (exitCode == 20) { logger.LogWarning("Differences detected; exiting with code 20 due to --fail-on-diff."); } if (!string.IsNullOrWhiteSpace(result.ExplainUri)) { activity?.SetTag("stellaops.cli.explain_uri", result.ExplainUri); } } catch (ArgumentException ex) { logger.LogError(ex.Message); CliMetrics.RecordPolicySimulation("error"); Environment.ExitCode = 64; } catch (PolicyApiException ex) { HandlePolicySimulationFailure(ex, logger); } catch (Exception ex) { logger.LogError(ex, "Policy simulation failed."); CliMetrics.RecordPolicySimulation("error"); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleOfflineKitImportAsync( IServiceProvider services, string bundlePath, string? manifestPath, string? bundleSignaturePath, string? manifestSignaturePath, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("offline-kit-import"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.offline.kit.import", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("offline kit import"); try { if (string.IsNullOrWhiteSpace(bundlePath)) { logger.LogError("Bundle path is required."); Environment.ExitCode = 1; return; } bundlePath = Path.GetFullPath(bundlePath); if (!File.Exists(bundlePath)) { logger.LogError("Bundle file {Path} not found.", bundlePath); Environment.ExitCode = 1; return; } var metadata = await LoadOfflineKitMetadataAsync(bundlePath, cancellationToken).ConfigureAwait(false); if (metadata is not null) { manifestPath ??= metadata.ManifestPath; bundleSignaturePath ??= metadata.BundleSignaturePath; manifestSignaturePath ??= metadata.ManifestSignaturePath; } manifestPath = NormalizeFilePath(manifestPath); bundleSignaturePath = NormalizeFilePath(bundleSignaturePath); manifestSignaturePath = NormalizeFilePath(manifestSignaturePath); if (manifestPath is null) { manifestPath = TryInferManifestPath(bundlePath); if (manifestPath is not null) { logger.LogDebug("Using inferred manifest path {Path}.", manifestPath); } } if (manifestPath is not null && !File.Exists(manifestPath)) { logger.LogError("Manifest file {Path} not found.", manifestPath); Environment.ExitCode = 1; return; } if (bundleSignaturePath is not null && !File.Exists(bundleSignaturePath)) { logger.LogWarning("Bundle signature {Path} not found; skipping.", bundleSignaturePath); bundleSignaturePath = null; } if (manifestSignaturePath is not null && !File.Exists(manifestSignaturePath)) { logger.LogWarning("Manifest signature {Path} not found; skipping.", manifestSignaturePath); manifestSignaturePath = null; } if (metadata is not null) { var computedBundleDigest = await ComputeSha256Async(bundlePath, cancellationToken).ConfigureAwait(false); if (!DigestsEqual(computedBundleDigest, metadata.BundleSha256)) { logger.LogError("Bundle digest mismatch. Expected sha256:{Expected} but computed sha256:{Actual}.", metadata.BundleSha256, computedBundleDigest); Environment.ExitCode = 1; return; } if (manifestPath is not null) { var computedManifestDigest = await ComputeSha256Async(manifestPath, cancellationToken).ConfigureAwait(false); if (!DigestsEqual(computedManifestDigest, metadata.ManifestSha256)) { logger.LogError("Manifest digest mismatch. Expected sha256:{Expected} but computed sha256:{Actual}.", metadata.ManifestSha256, computedManifestDigest); Environment.ExitCode = 1; return; } } } var request = new OfflineKitImportRequest( bundlePath, manifestPath, bundleSignaturePath, manifestSignaturePath, metadata?.BundleId, metadata?.BundleSha256, metadata?.BundleSize, metadata?.CapturedAt, metadata?.Channel, metadata?.Kind, metadata?.IsDelta, metadata?.BaseBundleId, metadata?.ManifestSha256, metadata?.ManifestSize); var result = await client.ImportOfflineKitAsync(request, cancellationToken).ConfigureAwait(false); CliMetrics.RecordOfflineKitImport(result.Status); logger.LogInformation( "Import {ImportId} submitted at {Submitted:u} with status {Status}.", string.IsNullOrWhiteSpace(result.ImportId) ? "" : result.ImportId, result.SubmittedAt, string.IsNullOrWhiteSpace(result.Status) ? "queued" : result.Status); if (!string.IsNullOrWhiteSpace(result.Message)) { logger.LogInformation(result.Message); } Environment.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Offline kit import failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleOfflineKitStatusAsync( IServiceProvider services, bool asJson, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("offline-kit-status"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.offline.kit.status", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("offline kit status"); try { var status = await client.GetOfflineKitStatusAsync(cancellationToken).ConfigureAwait(false); if (asJson) { var payload = new { bundleId = status.BundleId, channel = status.Channel, kind = status.Kind, isDelta = status.IsDelta, baseBundleId = status.BaseBundleId, capturedAt = status.CapturedAt, importedAt = status.ImportedAt, sha256 = status.BundleSha256, sizeBytes = status.BundleSize, components = status.Components.Select(component => new { component.Name, component.Version, component.Digest, component.CapturedAt, component.SizeBytes }) }; var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }); AnsiConsole.Console.WriteLine(json); } else { if (string.IsNullOrWhiteSpace(status.BundleId)) { logger.LogInformation("No offline kit bundle has been imported yet."); } else { logger.LogInformation( "Current bundle {BundleId} ({Kind}) captured {Captured:u}, imported {Imported:u}, sha256:{Digest}, size {Size}.", status.BundleId, status.Kind ?? "unknown", status.CapturedAt ?? default, status.ImportedAt ?? default, status.BundleSha256 ?? "", status.BundleSize.HasValue ? status.BundleSize.Value.ToString("N0", CultureInfo.InvariantCulture) : ""); } if (status.Components.Count > 0) { var table = new Table().AddColumns("Component", "Version", "Digest", "Captured", "Size (bytes)"); foreach (var component in status.Components) { table.AddRow( component.Name, string.IsNullOrWhiteSpace(component.Version) ? "-" : component.Version!, string.IsNullOrWhiteSpace(component.Digest) ? "-" : $"sha256:{component.Digest}", component.CapturedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "-", component.SizeBytes.HasValue ? component.SizeBytes.Value.ToString("N0", CultureInfo.InvariantCulture) : "-"); } AnsiConsole.Write(table); } } Environment.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to read offline kit status."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } private static async Task LoadOfflineKitMetadataAsync(string bundlePath, CancellationToken cancellationToken) { var metadataPath = bundlePath + ".metadata.json"; if (!File.Exists(metadataPath)) { return null; } try { await using var stream = File.OpenRead(metadataPath); return await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); } catch { return null; } } private static string? NormalizeFilePath(string? path) { if (string.IsNullOrWhiteSpace(path)) { return null; } return Path.GetFullPath(path); } private static string? TryInferManifestPath(string bundlePath) { var directory = Path.GetDirectoryName(bundlePath); if (string.IsNullOrWhiteSpace(directory)) { return null; } var baseName = Path.GetFileName(bundlePath); if (string.IsNullOrWhiteSpace(baseName)) { return null; } baseName = Path.GetFileNameWithoutExtension(baseName); if (baseName.EndsWith(".tar", StringComparison.OrdinalIgnoreCase)) { baseName = Path.GetFileNameWithoutExtension(baseName); } var candidates = new[] { Path.Combine(directory, $"offline-manifest-{baseName}.json"), Path.Combine(directory, "offline-manifest.json") }; foreach (var candidate in candidates) { if (File.Exists(candidate)) { return Path.GetFullPath(candidate); } } return Directory.EnumerateFiles(directory, "offline-manifest*.json").FirstOrDefault(); } private static bool DigestsEqual(string computed, string? expected) { if (string.IsNullOrWhiteSpace(expected)) { return true; } return string.Equals(NormalizeDigest(computed), NormalizeDigest(expected), StringComparison.OrdinalIgnoreCase); } private static string NormalizeDigest(string digest) { var value = digest.Trim(); if (value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)) { value = value.Substring("sha256:".Length); } return value.ToLowerInvariant(); } private static async Task ComputeSha256Async(string path, CancellationToken cancellationToken) { await using var stream = File.OpenRead(path); var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false); return Convert.ToHexString(hash).ToLowerInvariant(); } private static bool TryParseDetachedJws(string value, out string encodedHeader, out string encodedSignature) { encodedHeader = string.Empty; encodedSignature = string.Empty; if (string.IsNullOrWhiteSpace(value)) { return false; } var parts = value.Split('.'); if (parts.Length != 3) { return false; } encodedHeader = parts[0]; encodedSignature = parts[2]; return parts[1].Length == 0; } private static byte[] Base64UrlDecode(string value) { var normalized = value.Replace('-', '+').Replace('_', '/'); var padding = normalized.Length % 4; if (padding == 2) { normalized += "=="; } else if (padding == 3) { normalized += "="; } else if (padding == 1) { throw new FormatException("Invalid Base64Url value."); } return Convert.FromBase64String(normalized); } private static CryptoSigningKey CreateVerificationSigningKey( string keyId, string algorithm, string? providerHint, string keyPem, string keyPath) { if (string.IsNullOrWhiteSpace(keyPem)) { throw new InvalidOperationException("Verification key PEM content is empty."); } using var ecdsa = ECDsa.Create(); ecdsa.ImportFromPem(keyPem); var parameters = ecdsa.ExportParameters(includePrivateParameters: false); if (parameters.D is null || parameters.D.Length == 0) { parameters.D = new byte[] { 0x01 }; } var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["source"] = Path.GetFullPath(keyPath), ["verificationOnly"] = "true" }; return new CryptoSigningKey( new CryptoKeyReference(keyId, providerHint), algorithm, in parameters, DateTimeOffset.UtcNow, metadata: metadata); } private static string FormatDuration(TimeSpan duration) { if (duration <= TimeSpan.Zero) { return "expired"; } if (duration.TotalDays >= 1) { var days = (int)duration.TotalDays; var hours = duration.Hours; return hours > 0 ? FormattableString.Invariant($"{days}d {hours}h") : FormattableString.Invariant($"{days}d"); } if (duration.TotalHours >= 1) { return FormattableString.Invariant($"{(int)duration.TotalHours}h {duration.Minutes}m"); } if (duration.TotalMinutes >= 1) { return FormattableString.Invariant($"{(int)duration.TotalMinutes}m {duration.Seconds}s"); } return FormattableString.Invariant($"{duration.Seconds}s"); } private static bool TryExtractJwtClaims( string accessToken, out Dictionary claims, out DateTimeOffset? issuedAt, out DateTimeOffset? notBefore) { claims = new Dictionary(StringComparer.OrdinalIgnoreCase); issuedAt = null; notBefore = null; if (string.IsNullOrWhiteSpace(accessToken)) { return false; } var parts = accessToken.Split('.'); if (parts.Length < 2) { return false; } if (!TryDecodeBase64Url(parts[1], out var payloadBytes)) { return false; } try { using var document = JsonDocument.Parse(payloadBytes); foreach (var property in document.RootElement.EnumerateObject()) { var value = FormatJsonValue(property.Value); claims[property.Name] = value; if (issuedAt is null && property.NameEquals("iat") && TryParseUnixSeconds(property.Value, out var parsedIat)) { issuedAt = parsedIat; } if (notBefore is null && property.NameEquals("nbf") && TryParseUnixSeconds(property.Value, out var parsedNbf)) { notBefore = parsedNbf; } } return true; } catch (JsonException) { claims.Clear(); issuedAt = null; notBefore = null; return false; } } private static bool TryDecodeBase64Url(string value, out byte[] bytes) { bytes = Array.Empty(); if (string.IsNullOrWhiteSpace(value)) { return false; } var normalized = value.Replace('-', '+').Replace('_', '/'); var padding = normalized.Length % 4; if (padding is 2 or 3) { normalized = normalized.PadRight(normalized.Length + (4 - padding), '='); } else if (padding == 1) { return false; } try { bytes = Convert.FromBase64String(normalized); return true; } catch (FormatException) { return false; } } private static string FormatJsonValue(JsonElement element) { return element.ValueKind switch { JsonValueKind.String => element.GetString() ?? string.Empty, JsonValueKind.Number => element.TryGetInt64(out var longValue) ? longValue.ToString(CultureInfo.InvariantCulture) : element.GetDouble().ToString(CultureInfo.InvariantCulture), JsonValueKind.True => "true", JsonValueKind.False => "false", JsonValueKind.Null => "null", JsonValueKind.Array => FormatArray(element), JsonValueKind.Object => element.GetRawText(), _ => element.GetRawText() }; } private static string FormatArray(JsonElement array) { var values = new List(); foreach (var item in array.EnumerateArray()) { values.Add(FormatJsonValue(item)); } return string.Join(", ", values); } private static bool TryParseUnixSeconds(JsonElement element, out DateTimeOffset value) { value = default; if (element.ValueKind == JsonValueKind.Number) { if (element.TryGetInt64(out var seconds)) { value = DateTimeOffset.FromUnixTimeSeconds(seconds); return true; } if (element.TryGetDouble(out var doubleValue)) { value = DateTimeOffset.FromUnixTimeSeconds((long)doubleValue); return true; } } if (element.ValueKind == JsonValueKind.String) { var text = element.GetString(); if (!string.IsNullOrWhiteSpace(text) && long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) { value = DateTimeOffset.FromUnixTimeSeconds(seconds); return true; } } return false; } private static List CollectAdditionalClaims(Dictionary claims) { var result = new List(); foreach (var pair in claims) { if (CommonClaimNames.Contains(pair.Key)) { continue; } result.Add(FormattableString.Invariant($"{pair.Key}={pair.Value}")); } result.Sort(StringComparer.OrdinalIgnoreCase); return result; } private static readonly HashSet CommonClaimNames = new(StringComparer.OrdinalIgnoreCase) { "aud", "client_id", "exp", "iat", "iss", "nbf", "scope", "scopes", "sub", "token_type", "jti" }; private static async Task ExecuteExcititorCommandAsync( IServiceProvider services, string commandName, bool verbose, IDictionary? activityTags, Func> operation, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger(commandName.Replace(' ', '-')); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity($"cli.{commandName.Replace(' ', '.')}" , ActivityKind.Client); activity?.SetTag("stellaops.cli.command", commandName); if (activityTags is not null) { foreach (var tag in activityTags) { activity?.SetTag(tag.Key, tag.Value); } } using var duration = CliMetrics.MeasureCommandDuration(commandName); try { var result = await operation(client).ConfigureAwait(false); if (result.Success) { if (!string.IsNullOrWhiteSpace(result.Message)) { logger.LogInformation(result.Message); } else { logger.LogInformation("Operation completed successfully."); } if (!string.IsNullOrWhiteSpace(result.Location)) { logger.LogInformation("Location: {Location}", result.Location); } if (result.Payload is JsonElement payload && payload.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null) { logger.LogDebug("Response payload: {Payload}", payload.ToString()); } Environment.ExitCode = 0; } else { logger.LogError(string.IsNullOrWhiteSpace(result.Message) ? "Operation failed." : result.Message); Environment.ExitCode = 1; } } catch (Exception ex) { logger.LogError(ex, "Excititor operation failed."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } private static async Task> GatherImageDigestsAsync( IReadOnlyList inline, string? filePath, CancellationToken cancellationToken) { var results = new List(); var seen = new HashSet(StringComparer.Ordinal); void AddCandidates(string? candidate) { foreach (var image in SplitImageCandidates(candidate)) { if (seen.Add(image)) { results.Add(image); } } } if (inline is not null) { foreach (var entry in inline) { AddCandidates(entry); } } if (!string.IsNullOrWhiteSpace(filePath)) { var path = Path.GetFullPath(filePath); if (!File.Exists(path)) { throw new FileNotFoundException("Input file not found.", path); } foreach (var line in File.ReadLines(path)) { cancellationToken.ThrowIfCancellationRequested(); AddCandidates(line); } } if (Console.IsInputRedirected) { while (!cancellationToken.IsCancellationRequested) { var line = await Console.In.ReadLineAsync().ConfigureAwait(false); if (line is null) { break; } AddCandidates(line); } } return new ReadOnlyCollection(results); } private static IEnumerable SplitImageCandidates(string? raw) { if (string.IsNullOrWhiteSpace(raw)) { yield break; } var candidate = raw.Trim(); var commentIndex = candidate.IndexOf('#'); if (commentIndex >= 0) { candidate = candidate[..commentIndex].Trim(); } if (candidate.Length == 0) { yield break; } var tokens = candidate.Split(new[] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); foreach (var token in tokens) { var trimmed = token.Trim(); if (trimmed.Length > 0) { yield return trimmed; } } } private static IReadOnlyDictionary ParseLabelSelectors(IReadOnlyList labelArguments) { if (labelArguments is null || labelArguments.Count == 0) { return EmptyLabelSelectors; } var labels = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var raw in labelArguments) { if (string.IsNullOrWhiteSpace(raw)) { continue; } var trimmed = raw.Trim(); var delimiter = trimmed.IndexOf('='); if (delimiter <= 0 || delimiter == trimmed.Length - 1) { throw new ArgumentException($"Invalid label '{raw}'. Expected key=value format."); } var key = trimmed[..delimiter].Trim(); var value = trimmed[(delimiter + 1)..].Trim(); if (key.Length == 0) { throw new ArgumentException($"Invalid label '{raw}'. Label key cannot be empty."); } labels[key] = value; } return labels.Count == 0 ? EmptyLabelSelectors : new ReadOnlyDictionary(labels); } private sealed record ExcititorExportManifestSummary( string ExportId, string? Format, string? Algorithm, string? Digest, long? SizeBytes, bool? FromCache, DateTimeOffset? CreatedAt, string? RekorLocation, string? RekorIndex, string? RekorInclusionUrl); private static ExcititorExportManifestSummary? TryParseExportManifest(JsonElement? payload) { if (payload is null || payload.Value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) { return null; } var element = payload.Value; var exportId = GetStringProperty(element, "exportId"); if (string.IsNullOrWhiteSpace(exportId)) { return null; } var format = GetStringProperty(element, "format"); var algorithm = default(string?); var digest = default(string?); if (TryGetPropertyCaseInsensitive(element, "artifact", out var artifact) && artifact.ValueKind == JsonValueKind.Object) { algorithm = GetStringProperty(artifact, "algorithm"); digest = GetStringProperty(artifact, "digest"); } var sizeBytes = GetInt64Property(element, "sizeBytes"); var fromCache = GetBooleanProperty(element, "fromCache"); var createdAt = GetDateTimeOffsetProperty(element, "createdAt"); string? rekorLocation = null; string? rekorIndex = null; string? rekorInclusion = null; if (TryGetPropertyCaseInsensitive(element, "attestation", out var attestation) && attestation.ValueKind == JsonValueKind.Object) { if (TryGetPropertyCaseInsensitive(attestation, "rekor", out var rekor) && rekor.ValueKind == JsonValueKind.Object) { rekorLocation = GetStringProperty(rekor, "location"); rekorIndex = GetStringProperty(rekor, "logIndex"); var inclusion = GetStringProperty(rekor, "inclusionProofUri"); if (!string.IsNullOrWhiteSpace(inclusion)) { rekorInclusion = inclusion; } } } return new ExcititorExportManifestSummary( exportId.Trim(), format, algorithm, digest, sizeBytes, fromCache, createdAt, rekorLocation, rekorIndex, rekorInclusion); } private static bool TryGetPropertyCaseInsensitive(JsonElement element, string propertyName, out JsonElement property) { if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(propertyName, out property)) { return true; } if (element.ValueKind == JsonValueKind.Object) { foreach (var candidate in element.EnumerateObject()) { if (string.Equals(candidate.Name, propertyName, StringComparison.OrdinalIgnoreCase)) { property = candidate.Value; return true; } } } property = default; return false; } private static string? GetStringProperty(JsonElement element, string propertyName) { if (TryGetPropertyCaseInsensitive(element, propertyName, out var property)) { return property.ValueKind switch { JsonValueKind.String => property.GetString(), JsonValueKind.Number => property.ToString(), _ => null }; } return null; } private static bool? GetBooleanProperty(JsonElement element, string propertyName) { if (TryGetPropertyCaseInsensitive(element, propertyName, out var property)) { return property.ValueKind switch { JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.String when bool.TryParse(property.GetString(), out var parsed) => parsed, _ => null }; } return null; } private static long? GetInt64Property(JsonElement element, string propertyName) { if (TryGetPropertyCaseInsensitive(element, propertyName, out var property)) { if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out var value)) { return value; } if (property.ValueKind == JsonValueKind.String && long.TryParse(property.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) { return parsed; } } return null; } private static DateTimeOffset? GetDateTimeOffsetProperty(JsonElement element, string propertyName) { if (TryGetPropertyCaseInsensitive(element, propertyName, out var property) && property.ValueKind == JsonValueKind.String && DateTimeOffset.TryParse(property.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var value)) { return value.ToUniversalTime(); } return null; } private static string BuildDigestDisplay(string? algorithm, string digest) { if (string.IsNullOrWhiteSpace(digest)) { return string.Empty; } if (digest.Contains(':', StringComparison.Ordinal)) { return digest; } if (string.IsNullOrWhiteSpace(algorithm) || algorithm.Equals("sha256", StringComparison.OrdinalIgnoreCase)) { return $"sha256:{digest}"; } return $"{algorithm}:{digest}"; } private static string FormatSize(long sizeBytes) { if (sizeBytes < 0) { return $"{sizeBytes} bytes"; } string[] units = { "bytes", "KB", "MB", "GB", "TB" }; double size = sizeBytes; var unit = 0; while (size >= 1024 && unit < units.Length - 1) { size /= 1024; unit++; } return unit == 0 ? $"{sizeBytes} bytes" : $"{size:0.##} {units[unit]}"; } private static string ResolveExportOutputPath(string outputPath, ExcititorExportManifestSummary manifest) { if (string.IsNullOrWhiteSpace(outputPath)) { throw new ArgumentException("Output path must be provided.", nameof(outputPath)); } var fullPath = Path.GetFullPath(outputPath); if (Directory.Exists(fullPath) || outputPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) || outputPath.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal)) { return Path.Combine(fullPath, BuildExportFileName(manifest)); } var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } return fullPath; } private static string BuildExportFileName(ExcititorExportManifestSummary manifest) { var token = !string.IsNullOrWhiteSpace(manifest.Digest) ? manifest.Digest! : manifest.ExportId; token = SanitizeToken(token); if (token.Length > 40) { token = token[..40]; } var extension = DetermineExportExtension(manifest.Format); return $"stellaops-excititor-{token}{extension}"; } private static string DetermineExportExtension(string? format) { if (string.IsNullOrWhiteSpace(format)) { return ".bin"; } return format switch { not null when format.Equals("jsonl", StringComparison.OrdinalIgnoreCase) => ".jsonl", not null when format.Equals("json", StringComparison.OrdinalIgnoreCase) => ".json", not null when format.Equals("openvex", StringComparison.OrdinalIgnoreCase) => ".json", not null when format.Equals("csaf", StringComparison.OrdinalIgnoreCase) => ".json", _ => ".bin" }; } private static string SanitizeToken(string token) { var builder = new StringBuilder(token.Length); foreach (var ch in token) { if (char.IsLetterOrDigit(ch)) { builder.Append(char.ToLowerInvariant(ch)); } } if (builder.Length == 0) { builder.Append("export"); } return builder.ToString(); } private static string? ResolveLocationUrl(StellaOpsCliOptions options, string location) { if (string.IsNullOrWhiteSpace(location)) { return null; } if (Uri.TryCreate(location, UriKind.Absolute, out var absolute)) { return absolute.ToString(); } if (!string.IsNullOrWhiteSpace(options?.BackendUrl) && Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var baseUri)) { if (!location.StartsWith("/", StringComparison.Ordinal)) { location = "/" + location; } return new Uri(baseUri, location).ToString(); } return location; } private static string BuildRuntimePolicyJson(RuntimePolicyEvaluationResult result, IReadOnlyList requestedImages) { var orderedImages = BuildImageOrder(requestedImages, result.Decisions.Keys); var results = new Dictionary(StringComparer.Ordinal); foreach (var image in orderedImages) { if (result.Decisions.TryGetValue(image, out var decision)) { results[image] = BuildDecisionMap(decision); } } var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; var payload = new Dictionary(StringComparer.Ordinal) { ["ttlSeconds"] = result.TtlSeconds, ["expiresAtUtc"] = result.ExpiresAtUtc?.ToString("O", CultureInfo.InvariantCulture), ["policyRevision"] = result.PolicyRevision, ["results"] = results }; return JsonSerializer.Serialize(payload, options); } private static IDictionary BuildDecisionMap(RuntimePolicyImageDecision decision) { var map = new Dictionary(StringComparer.Ordinal) { ["policyVerdict"] = decision.PolicyVerdict, ["signed"] = decision.Signed, ["hasSbomReferrers"] = decision.HasSbomReferrers }; if (decision.Reasons.Count > 0) { map["reasons"] = decision.Reasons; } if (decision.Rekor is not null) { var rekorMap = new Dictionary(StringComparer.Ordinal); if (!string.IsNullOrWhiteSpace(decision.Rekor.Uuid)) { rekorMap["uuid"] = decision.Rekor.Uuid; } if (!string.IsNullOrWhiteSpace(decision.Rekor.Url)) { rekorMap["url"] = decision.Rekor.Url; } if (decision.Rekor.Verified.HasValue) { rekorMap["verified"] = decision.Rekor.Verified; } if (rekorMap.Count > 0) { map["rekor"] = rekorMap; } } foreach (var kvp in decision.AdditionalProperties) { map[kvp.Key] = kvp.Value; } return map; } private static void DisplayRuntimePolicyResults(ILogger logger, RuntimePolicyEvaluationResult result, IReadOnlyList requestedImages) { var orderedImages = BuildImageOrder(requestedImages, result.Decisions.Keys); var summary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (AnsiConsole.Profile.Capabilities.Interactive) { var table = new Table().Border(TableBorder.Rounded) .AddColumns("Image", "Verdict", "Signed", "SBOM Ref", "Quieted", "Confidence", "Reasons", "Attestation"); foreach (var image in orderedImages) { if (result.Decisions.TryGetValue(image, out var decision)) { table.AddRow( image, decision.PolicyVerdict, FormatBoolean(decision.Signed), FormatBoolean(decision.HasSbomReferrers), FormatQuietedDisplay(decision.AdditionalProperties), FormatConfidenceDisplay(decision.AdditionalProperties), decision.Reasons.Count > 0 ? string.Join(Environment.NewLine, decision.Reasons) : "-", FormatAttestation(decision.Rekor)); summary[decision.PolicyVerdict] = summary.TryGetValue(decision.PolicyVerdict, out var count) ? count + 1 : 1; if (decision.AdditionalProperties.Count > 0) { var metadata = string.Join(", ", decision.AdditionalProperties.Select(kvp => $"{kvp.Key}={FormatAdditionalValue(kvp.Value)}")); logger.LogDebug("Metadata for {Image}: {Metadata}", image, metadata); } } else { table.AddRow(image, "", "-", "-", "-", "-", "-", "-"); } } AnsiConsole.Write(table); } else { foreach (var image in orderedImages) { if (result.Decisions.TryGetValue(image, out var decision)) { var reasons = decision.Reasons.Count > 0 ? string.Join(", ", decision.Reasons) : "none"; logger.LogInformation( "{Image} -> verdict={Verdict} signed={Signed} sbomRef={Sbom} quieted={Quieted} confidence={Confidence} attestation={Attestation} reasons={Reasons}", image, decision.PolicyVerdict, FormatBoolean(decision.Signed), FormatBoolean(decision.HasSbomReferrers), FormatQuietedDisplay(decision.AdditionalProperties), FormatConfidenceDisplay(decision.AdditionalProperties), FormatAttestation(decision.Rekor), reasons); summary[decision.PolicyVerdict] = summary.TryGetValue(decision.PolicyVerdict, out var count) ? count + 1 : 1; if (decision.AdditionalProperties.Count > 0) { var metadata = string.Join(", ", decision.AdditionalProperties.Select(kvp => $"{kvp.Key}={FormatAdditionalValue(kvp.Value)}")); logger.LogDebug("Metadata for {Image}: {Metadata}", image, metadata); } } else { logger.LogWarning("{Image} -> no decision returned by backend.", image); } } } if (summary.Count > 0) { var summaryText = string.Join(", ", summary.Select(kvp => $"{kvp.Key}:{kvp.Value}")); logger.LogInformation("Verdict summary: {Summary}", summaryText); } } private static IReadOnlyList BuildImageOrder(IReadOnlyList requestedImages, IEnumerable actual) { var order = new List(); var seen = new HashSet(StringComparer.Ordinal); if (requestedImages is not null) { foreach (var image in requestedImages) { if (!string.IsNullOrWhiteSpace(image)) { var trimmed = image.Trim(); if (seen.Add(trimmed)) { order.Add(trimmed); } } } } foreach (var image in actual) { if (!string.IsNullOrWhiteSpace(image)) { var trimmed = image.Trim(); if (seen.Add(trimmed)) { order.Add(trimmed); } } } return new ReadOnlyCollection(order); } private static string FormatBoolean(bool? value) => value is null ? "unknown" : value.Value ? "yes" : "no"; private static string FormatQuietedDisplay(IReadOnlyDictionary metadata) { var quieted = GetMetadataBoolean(metadata, "quieted", "quiet"); var quietedBy = GetMetadataString(metadata, "quietedBy", "quietedReason"); if (quieted is true) { return string.IsNullOrWhiteSpace(quietedBy) ? "yes" : $"yes ({quietedBy})"; } if (quieted is false) { return "no"; } return string.IsNullOrWhiteSpace(quietedBy) ? "-" : $"? ({quietedBy})"; } private static string FormatConfidenceDisplay(IReadOnlyDictionary metadata) { var confidence = GetMetadataDouble(metadata, "confidence"); var confidenceBand = GetMetadataString(metadata, "confidenceBand", "confidenceTier"); if (confidence.HasValue && !string.IsNullOrWhiteSpace(confidenceBand)) { return string.Format(CultureInfo.InvariantCulture, "{0:0.###} ({1})", confidence.Value, confidenceBand); } if (confidence.HasValue) { return confidence.Value.ToString("0.###", CultureInfo.InvariantCulture); } if (!string.IsNullOrWhiteSpace(confidenceBand)) { return confidenceBand!; } return "-"; } private static string FormatAttestation(RuntimePolicyRekorReference? rekor) { if (rekor is null) { return "-"; } var uuid = string.IsNullOrWhiteSpace(rekor.Uuid) ? null : rekor.Uuid; var url = string.IsNullOrWhiteSpace(rekor.Url) ? null : rekor.Url; var verified = rekor.Verified; var core = uuid ?? url; if (!string.IsNullOrEmpty(core)) { if (verified.HasValue) { var suffix = verified.Value ? " (verified)" : " (unverified)"; return core + suffix; } return core!; } if (verified.HasValue) { return verified.Value ? "verified" : "unverified"; } return "-"; } private static bool? GetMetadataBoolean(IReadOnlyDictionary metadata, params string[] keys) { foreach (var key in keys) { if (metadata.TryGetValue(key, out var value) && value is not null) { switch (value) { case bool b: return b; case string s when bool.TryParse(s, out var parsed): return parsed; } } } return null; } private static string? GetMetadataString(IReadOnlyDictionary metadata, params string[] keys) { foreach (var key in keys) { if (metadata.TryGetValue(key, out var value) && value is not null) { if (value is string s) { return string.IsNullOrWhiteSpace(s) ? null : s; } } } return null; } private static double? GetMetadataDouble(IReadOnlyDictionary metadata, params string[] keys) { foreach (var key in keys) { if (metadata.TryGetValue(key, out var value) && value is not null) { switch (value) { case double d: return d; case float f: return f; case decimal m: return (double)m; case long l: return l; case int i: return i; case string s when double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var parsed): return parsed; } } } return null; } private static TaskRunnerSimulationOutputFormat DetermineTaskRunnerSimulationFormat(string? value, string? outputPath) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim().ToLowerInvariant() switch { "table" => TaskRunnerSimulationOutputFormat.Table, "json" => TaskRunnerSimulationOutputFormat.Json, _ => throw new ArgumentException("Invalid format. Use 'table' or 'json'.") }; } if (!string.IsNullOrWhiteSpace(outputPath)) { return TaskRunnerSimulationOutputFormat.Json; } return TaskRunnerSimulationOutputFormat.Table; } private static object BuildTaskRunnerSimulationPayload(TaskRunnerSimulationResult result) => new { planHash = result.PlanHash, failurePolicy = new { result.FailurePolicy.MaxAttempts, result.FailurePolicy.BackoffSeconds, result.FailurePolicy.ContinueOnError }, hasPendingApprovals = result.HasPendingApprovals, steps = result.Steps, outputs = result.Outputs }; private static void RenderTaskRunnerSimulationResult(TaskRunnerSimulationResult result) { var console = AnsiConsole.Console; var table = new Table { Border = TableBorder.Rounded }; table.AddColumn("Step"); table.AddColumn("Kind"); table.AddColumn("Status"); table.AddColumn("Reason"); table.AddColumn("MaxParallel"); table.AddColumn("ContinueOnError"); table.AddColumn("Approval"); foreach (var (step, depth) in FlattenTaskRunnerSimulationSteps(result.Steps)) { var indent = new string(' ', depth * 2); table.AddRow( Markup.Escape($"{indent}{step.Id}"), Markup.Escape(step.Kind), Markup.Escape(step.Status), Markup.Escape(string.IsNullOrWhiteSpace(step.StatusReason) ? "-" : step.StatusReason!), step.MaxParallel?.ToString(CultureInfo.InvariantCulture) ?? "-", step.ContinueOnError ? "yes" : "no", Markup.Escape(string.IsNullOrWhiteSpace(step.ApprovalId) ? "-" : step.ApprovalId!)); } console.Write(table); if (result.Outputs.Count > 0) { var outputsTable = new Table { Border = TableBorder.Rounded }; outputsTable.AddColumn("Name"); outputsTable.AddColumn("Type"); outputsTable.AddColumn("Requires Runtime"); outputsTable.AddColumn("Path"); outputsTable.AddColumn("Expression"); foreach (var output in result.Outputs) { outputsTable.AddRow( Markup.Escape(output.Name), Markup.Escape(output.Type), output.RequiresRuntimeValue ? "yes" : "no", Markup.Escape(string.IsNullOrWhiteSpace(output.PathExpression) ? "-" : output.PathExpression!), Markup.Escape(string.IsNullOrWhiteSpace(output.ValueExpression) ? "-" : output.ValueExpression!)); } console.WriteLine(); console.Write(outputsTable); } console.WriteLine(); console.MarkupLine($"[grey]Plan Hash:[/] {Markup.Escape(result.PlanHash)}"); console.MarkupLine($"[grey]Pending Approvals:[/] {(result.HasPendingApprovals ? "yes" : "no")}"); console.Write(new Text($"Plan Hash: {result.PlanHash}{Environment.NewLine}")); console.Write(new Text($"Pending Approvals: {(result.HasPendingApprovals ? "yes" : "no")}{Environment.NewLine}")); } private static IEnumerable<(TaskRunnerSimulationStep Step, int Depth)> FlattenTaskRunnerSimulationSteps( IReadOnlyList steps, int depth = 0) { for (var i = 0; i < steps.Count; i++) { var step = steps[i]; yield return (step, depth); foreach (var child in FlattenTaskRunnerSimulationSteps(step.Children, depth + 1)) { yield return child; } } } private static PolicySimulationOutputFormat DeterminePolicySimulationFormat(string? value, string? outputPath) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim().ToLowerInvariant() switch { "table" => PolicySimulationOutputFormat.Table, "json" => PolicySimulationOutputFormat.Json, _ => throw new ArgumentException("Invalid format. Use 'table' or 'json'.") }; } if (!string.IsNullOrWhiteSpace(outputPath) || Console.IsOutputRedirected) { return PolicySimulationOutputFormat.Json; } return PolicySimulationOutputFormat.Table; } private static object BuildPolicySimulationPayload( string policyId, int? baseVersion, int? candidateVersion, IReadOnlyList sbomSet, IReadOnlyDictionary environment, PolicySimulationResult result) => new { policyId, baseVersion, candidateVersion, sbomSet = sbomSet.Count == 0 ? Array.Empty() : sbomSet, environment = environment.Count == 0 ? null : environment, diff = result.Diff, explainUri = result.ExplainUri }; private static void RenderPolicySimulationResult( ILogger logger, object payload, PolicySimulationResult result, PolicySimulationOutputFormat format) { if (format == PolicySimulationOutputFormat.Json) { var json = JsonSerializer.Serialize(payload, SimulationJsonOptions); Console.WriteLine(json); return; } logger.LogInformation( "Policy diff summary — Added: {Added}, Removed: {Removed}, Unchanged: {Unchanged}.", result.Diff.Added, result.Diff.Removed, result.Diff.Unchanged); if (result.Diff.BySeverity.Count > 0) { if (AnsiConsole.Profile.Capabilities.Interactive) { var table = new Table().AddColumns("Severity", "Up", "Down"); foreach (var entry in result.Diff.BySeverity.OrderBy(kvp => kvp.Key, StringComparer.Ordinal)) { table.AddRow( entry.Key, FormatDelta(entry.Value.Up), FormatDelta(entry.Value.Down)); } AnsiConsole.Write(table); } else { foreach (var entry in result.Diff.BySeverity.OrderBy(kvp => kvp.Key, StringComparer.Ordinal)) { logger.LogInformation("Severity {Severity}: up={Up}, down={Down}", entry.Key, entry.Value.Up ?? 0, entry.Value.Down ?? 0); } } } if (result.Diff.RuleHits.Count > 0) { if (AnsiConsole.Profile.Capabilities.Interactive) { var table = new Table().AddColumns("Rule", "Up", "Down"); foreach (var hit in result.Diff.RuleHits) { table.AddRow( string.IsNullOrWhiteSpace(hit.RuleName) ? hit.RuleId : $"{hit.RuleName} ({hit.RuleId})", FormatDelta(hit.Up), FormatDelta(hit.Down)); } AnsiConsole.Write(table); } else { foreach (var hit in result.Diff.RuleHits) { logger.LogInformation("Rule {RuleId}: up={Up}, down={Down}", hit.RuleId, hit.Up ?? 0, hit.Down ?? 0); } } } if (!string.IsNullOrWhiteSpace(result.ExplainUri)) { logger.LogInformation("Explain trace available at {ExplainUri}.", result.ExplainUri); } } private static IReadOnlyList NormalizePolicySbomSet(IReadOnlyList arguments) { if (arguments is null || arguments.Count == 0) { return EmptyPolicySbomSet; } var set = new SortedSet(StringComparer.Ordinal); foreach (var raw in arguments) { if (string.IsNullOrWhiteSpace(raw)) { continue; } var trimmed = raw.Trim(); if (trimmed.Length > 0) { set.Add(trimmed); } } if (set.Count == 0) { return EmptyPolicySbomSet; } var list = set.ToList(); return new ReadOnlyCollection(list); } private static IReadOnlyDictionary ParsePolicyEnvironment(IReadOnlyList arguments) { if (arguments is null || arguments.Count == 0) { return EmptyPolicyEnvironment; } var env = new SortedDictionary(StringComparer.Ordinal); foreach (var raw in arguments) { if (string.IsNullOrWhiteSpace(raw)) { continue; } var trimmed = raw.Trim(); var separator = trimmed.IndexOf('='); if (separator <= 0 || separator == trimmed.Length - 1) { throw new ArgumentException($"Invalid environment assignment '{raw}'. Expected key=value."); } var key = trimmed[..separator].Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentException($"Invalid environment assignment '{raw}'. Expected key=value."); } var valueToken = trimmed[(separator + 1)..].Trim(); env[key] = ParsePolicyEnvironmentValue(valueToken); } return env.Count == 0 ? EmptyPolicyEnvironment : new ReadOnlyDictionary(env); } private static object? ParsePolicyEnvironmentValue(string token) { if (string.IsNullOrWhiteSpace(token)) { return string.Empty; } var value = token; if ((value.Length >= 2 && value.StartsWith("\"", StringComparison.Ordinal) && value.EndsWith("\"", StringComparison.Ordinal)) || (value.Length >= 2 && value.StartsWith("'", StringComparison.Ordinal) && value.EndsWith("'", StringComparison.Ordinal))) { value = value[1..^1]; } if (string.Equals(value, "null", StringComparison.OrdinalIgnoreCase)) { return null; } if (bool.TryParse(value, out var boolResult)) { return boolResult; } if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longResult)) { return longResult; } if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleResult)) { return doubleResult; } return value; } private static Task WriteSimulationOutputAsync(string outputPath, object payload, CancellationToken cancellationToken) => WriteJsonPayloadAsync(outputPath, payload, cancellationToken); private static async Task WriteJsonPayloadAsync(string outputPath, object payload, CancellationToken cancellationToken) { var fullPath = Path.GetFullPath(outputPath); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } var json = JsonSerializer.Serialize(payload, SimulationJsonOptions); await File.WriteAllTextAsync(fullPath, json + Environment.NewLine, cancellationToken).ConfigureAwait(false); } private static int DetermineSimulationExitCode(PolicySimulationResult result, bool failOnDiff) { if (!failOnDiff) { return 0; } return (result.Diff.Added + result.Diff.Removed) > 0 ? 20 : 0; } private static void HandlePolicySimulationFailure(PolicyApiException exception, ILogger logger) { var exitCode = exception.ErrorCode switch { "ERR_POL_001" => 10, "ERR_POL_002" or "ERR_POL_005" => 12, "ERR_POL_003" => 21, "ERR_POL_004" => 22, "ERR_POL_006" => 23, _ when exception.StatusCode == HttpStatusCode.Forbidden || exception.StatusCode == HttpStatusCode.Unauthorized => 12, _ => 1 }; if (string.IsNullOrWhiteSpace(exception.ErrorCode)) { logger.LogError("Policy simulation failed ({StatusCode}): {Message}", (int)exception.StatusCode, exception.Message); } else { logger.LogError("Policy simulation failed ({StatusCode} {Code}): {Message}", (int)exception.StatusCode, exception.ErrorCode, exception.Message); } CliMetrics.RecordPolicySimulation("error"); Environment.ExitCode = exitCode; } private static void HandlePolicyActivationFailure(PolicyApiException exception, ILogger logger) { var exitCode = exception.ErrorCode switch { "ERR_POL_002" => 70, "ERR_POL_003" => 71, "ERR_POL_004" => 72, _ when exception.StatusCode == HttpStatusCode.Forbidden || exception.StatusCode == HttpStatusCode.Unauthorized => 12, _ => 1 }; if (string.IsNullOrWhiteSpace(exception.ErrorCode)) { logger.LogError("Policy activation failed ({StatusCode}): {Message}", (int)exception.StatusCode, exception.Message); } else { logger.LogError("Policy activation failed ({StatusCode} {Code}): {Message}", (int)exception.StatusCode, exception.ErrorCode, exception.Message); } CliMetrics.RecordPolicyActivation("error"); Environment.ExitCode = exitCode; } private static IReadOnlyList NormalizePolicyFilterValues(string[] values, bool toLower = false) { if (values is null || values.Length == 0) { return Array.Empty(); } var set = new HashSet(StringComparer.OrdinalIgnoreCase); var list = new List(); foreach (var raw in values) { var candidate = raw?.Trim(); if (string.IsNullOrWhiteSpace(candidate)) { continue; } var normalized = toLower ? candidate.ToLowerInvariant() : candidate; if (set.Add(normalized)) { list.Add(normalized); } } return list.Count == 0 ? Array.Empty() : list; } private static string? NormalizePolicyPriority(string? priority) { if (string.IsNullOrWhiteSpace(priority)) { return null; } var normalized = priority.Trim(); return string.IsNullOrWhiteSpace(normalized) ? null : normalized.ToLowerInvariant(); } private static string NormalizePolicyActivationOutcome(string status) { if (string.IsNullOrWhiteSpace(status)) { return "unknown"; } return status.Trim().ToLowerInvariant(); } private static int DeterminePolicyActivationExitCode(string outcome) => string.Equals(outcome, "pending_second_approval", StringComparison.Ordinal) ? 75 : 0; private static void RenderPolicyActivationResult(PolicyActivationResult result, PolicyActivationRequest request) { if (AnsiConsole.Profile.Capabilities.Interactive) { var summary = new Table().Expand(); summary.Border(TableBorder.Rounded); summary.AddColumn(new TableColumn("[grey]Field[/]").LeftAligned()); summary.AddColumn(new TableColumn("[grey]Value[/]").LeftAligned()); summary.AddRow("Policy", Markup.Escape($"{result.Revision.PolicyId} v{result.Revision.Version}")); summary.AddRow("Status", FormatActivationStatus(result.Status)); summary.AddRow("Requires 2 approvals", result.Revision.RequiresTwoPersonApproval ? "[yellow]yes[/]" : "[green]no[/]"); summary.AddRow("Created (UTC)", Markup.Escape(FormatUpdatedAt(result.Revision.CreatedAt))); summary.AddRow("Activated (UTC)", result.Revision.ActivatedAt.HasValue ? Markup.Escape(FormatUpdatedAt(result.Revision.ActivatedAt.Value)) : "[grey](not yet active)[/]"); if (request.RunNow) { summary.AddRow("Run", "[green]immediate[/]"); } else if (request.ScheduledAt.HasValue) { summary.AddRow("Scheduled at", Markup.Escape(FormatUpdatedAt(request.ScheduledAt.Value))); } if (!string.IsNullOrWhiteSpace(request.Priority)) { summary.AddRow("Priority", Markup.Escape(request.Priority!)); } if (request.Rollback) { summary.AddRow("Rollback", "[yellow]yes[/]"); } if (!string.IsNullOrWhiteSpace(request.IncidentId)) { summary.AddRow("Incident", Markup.Escape(request.IncidentId!)); } if (!string.IsNullOrWhiteSpace(request.Comment)) { summary.AddRow("Note", Markup.Escape(request.Comment!)); } AnsiConsole.Write(summary); if (result.Revision.Approvals.Count > 0) { var approvalTable = new Table().Title("[grey]Approvals[/]"); approvalTable.Border(TableBorder.Minimal); approvalTable.AddColumn(new TableColumn("Actor").LeftAligned()); approvalTable.AddColumn(new TableColumn("Approved (UTC)").LeftAligned()); approvalTable.AddColumn(new TableColumn("Comment").LeftAligned()); foreach (var approval in result.Revision.Approvals) { var comment = string.IsNullOrWhiteSpace(approval.Comment) ? "-" : approval.Comment!; approvalTable.AddRow( Markup.Escape(approval.ActorId), Markup.Escape(FormatUpdatedAt(approval.ApprovedAt)), Markup.Escape(comment)); } AnsiConsole.Write(approvalTable); } else { AnsiConsole.MarkupLine("[grey]No activation approvals recorded yet.[/]"); } } else { Console.WriteLine(FormattableString.Invariant($"Policy: {result.Revision.PolicyId} v{result.Revision.Version}")); Console.WriteLine(FormattableString.Invariant($"Status: {NormalizePolicyActivationOutcome(result.Status)}")); Console.WriteLine(FormattableString.Invariant($"Requires 2 approvals: {(result.Revision.RequiresTwoPersonApproval ? "yes" : "no")}")); Console.WriteLine(FormattableString.Invariant($"Created (UTC): {FormatUpdatedAt(result.Revision.CreatedAt)}")); Console.WriteLine(FormattableString.Invariant($"Activated (UTC): {(result.Revision.ActivatedAt.HasValue ? FormatUpdatedAt(result.Revision.ActivatedAt.Value) : "(not yet active)")}")); if (request.RunNow) { Console.WriteLine("Run: immediate"); } else if (request.ScheduledAt.HasValue) { Console.WriteLine(FormattableString.Invariant($"Scheduled at: {FormatUpdatedAt(request.ScheduledAt.Value)}")); } if (!string.IsNullOrWhiteSpace(request.Priority)) { Console.WriteLine(FormattableString.Invariant($"Priority: {request.Priority}")); } if (request.Rollback) { Console.WriteLine("Rollback: yes"); } if (!string.IsNullOrWhiteSpace(request.IncidentId)) { Console.WriteLine(FormattableString.Invariant($"Incident: {request.IncidentId}")); } if (!string.IsNullOrWhiteSpace(request.Comment)) { Console.WriteLine(FormattableString.Invariant($"Note: {request.Comment}")); } if (result.Revision.Approvals.Count == 0) { Console.WriteLine("Approvals: none"); } else { foreach (var approval in result.Revision.Approvals) { var comment = string.IsNullOrWhiteSpace(approval.Comment) ? "-" : approval.Comment; Console.WriteLine(FormattableString.Invariant($"Approval: {approval.ActorId} at {FormatUpdatedAt(approval.ApprovedAt)} ({comment})")); } } } } private static string FormatActivationStatus(string status) { var normalized = NormalizePolicyActivationOutcome(status); return normalized switch { "activated" => "[green]activated[/]", "already_active" => "[yellow]already_active[/]", "pending_second_approval" => "[yellow]pending_second_approval[/]", _ => "[red]" + Markup.Escape(string.IsNullOrWhiteSpace(status) ? "unknown" : status) + "[/]" }; } private static DateTimeOffset? ParsePolicySince(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } if (DateTimeOffset.TryParse( value.Trim(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed)) { return parsed.ToUniversalTime(); } throw new ArgumentException("Invalid --since value. Use an ISO-8601 timestamp."); } private static string? NormalizeExplainMode(string? mode) => string.IsNullOrWhiteSpace(mode) ? null : mode.Trim().ToLowerInvariant(); private static PolicyFindingsOutputFormat DeterminePolicyFindingsFormat(string? value, string? outputPath) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim().ToLowerInvariant() switch { "table" => PolicyFindingsOutputFormat.Table, "json" => PolicyFindingsOutputFormat.Json, _ => throw new ArgumentException("Invalid format. Use 'table' or 'json'.") }; } if (!string.IsNullOrWhiteSpace(outputPath) || Console.IsOutputRedirected) { return PolicyFindingsOutputFormat.Json; } return PolicyFindingsOutputFormat.Table; } private static object BuildPolicyFindingsPayload( string policyId, PolicyFindingsQuery query, PolicyFindingsPage page) => new { policyId, filters = new { sbom = query.SbomIds, status = query.Statuses, severity = query.Severities, cursor = query.Cursor, page = query.Page, pageSize = query.PageSize, since = query.Since?.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture) }, items = page.Items.Select(item => new { findingId = item.FindingId, status = item.Status, severity = new { normalized = item.Severity.Normalized, score = item.Severity.Score }, sbomId = item.SbomId, advisoryIds = item.AdvisoryIds, vex = item.Vex is null ? null : new { winningStatementId = item.Vex.WinningStatementId, source = item.Vex.Source, status = item.Vex.Status }, policyVersion = item.PolicyVersion, updatedAt = item.UpdatedAt == DateTimeOffset.MinValue ? null : item.UpdatedAt.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), runId = item.RunId }), nextCursor = page.NextCursor, totalCount = page.TotalCount }; private static object BuildPolicyFindingPayload(string policyId, PolicyFindingDocument finding) => new { policyId, finding = new { findingId = finding.FindingId, status = finding.Status, severity = new { normalized = finding.Severity.Normalized, score = finding.Severity.Score }, sbomId = finding.SbomId, advisoryIds = finding.AdvisoryIds, vex = finding.Vex is null ? null : new { winningStatementId = finding.Vex.WinningStatementId, source = finding.Vex.Source, status = finding.Vex.Status }, policyVersion = finding.PolicyVersion, updatedAt = finding.UpdatedAt == DateTimeOffset.MinValue ? null : finding.UpdatedAt.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), runId = finding.RunId } }; private static object BuildPolicyFindingExplainPayload( string policyId, string findingId, string? mode, PolicyFindingExplainResult explain) => new { policyId, findingId, mode, explain = new { policyVersion = explain.PolicyVersion, steps = explain.Steps.Select(step => new { rule = step.Rule, status = step.Status, action = step.Action, score = step.Score, inputs = step.Inputs, evidence = step.Evidence }), sealedHints = explain.SealedHints.Select(hint => hint.Message) } }; private static void RenderPolicyFindingsTable(ILogger logger, PolicyFindingsPage page) { var items = page.Items; if (items.Count == 0) { if (AnsiConsole.Profile.Capabilities.Interactive) { AnsiConsole.MarkupLine("[yellow]No findings matched the provided filters.[/]"); } else { logger.LogWarning("No findings matched the provided filters."); } return; } if (AnsiConsole.Profile.Capabilities.Interactive) { var table = new Table() .Border(TableBorder.Rounded) .Centered(); table.AddColumn("Finding"); table.AddColumn("Status"); table.AddColumn("Severity"); table.AddColumn("Score"); table.AddColumn("SBOM"); table.AddColumn("Advisories"); table.AddColumn("Updated (UTC)"); foreach (var item in items) { table.AddRow( Markup.Escape(item.FindingId), Markup.Escape(item.Status), Markup.Escape(item.Severity.Normalized), Markup.Escape(FormatScore(item.Severity.Score)), Markup.Escape(item.SbomId), Markup.Escape(FormatListPreview(item.AdvisoryIds)), Markup.Escape(FormatUpdatedAt(item.UpdatedAt))); } AnsiConsole.Write(table); } else { foreach (var item in items) { logger.LogInformation( "{Finding} — Status {Status}, Severity {Severity} ({Score}), SBOM {Sbom}, Updated {Updated}", item.FindingId, item.Status, item.Severity.Normalized, item.Severity.Score?.ToString("0.00", CultureInfo.InvariantCulture) ?? "n/a", item.SbomId, FormatUpdatedAt(item.UpdatedAt)); } } logger.LogInformation("{Count} finding(s).", items.Count); if (page.TotalCount.HasValue) { logger.LogInformation("Total available: {Total}", page.TotalCount.Value); } if (!string.IsNullOrWhiteSpace(page.NextCursor)) { logger.LogInformation("Next cursor: {Cursor}", page.NextCursor); } } private static void RenderPolicyFindingDetails(ILogger logger, PolicyFindingDocument finding) { if (AnsiConsole.Profile.Capabilities.Interactive) { var table = new Table() .Border(TableBorder.Rounded) .AddColumn("Field") .AddColumn("Value"); table.AddRow("Finding", Markup.Escape(finding.FindingId)); table.AddRow("Status", Markup.Escape(finding.Status)); table.AddRow("Severity", Markup.Escape(FormatSeverity(finding.Severity))); table.AddRow("SBOM", Markup.Escape(finding.SbomId)); table.AddRow("Policy Version", Markup.Escape(finding.PolicyVersion.ToString(CultureInfo.InvariantCulture))); table.AddRow("Updated (UTC)", Markup.Escape(FormatUpdatedAt(finding.UpdatedAt))); table.AddRow("Run Id", Markup.Escape(string.IsNullOrWhiteSpace(finding.RunId) ? "(none)" : finding.RunId)); table.AddRow("Advisories", Markup.Escape(FormatListPreview(finding.AdvisoryIds))); table.AddRow("VEX", Markup.Escape(FormatVexMetadata(finding.Vex))); AnsiConsole.Write(table); } else { logger.LogInformation("Finding {Finding}", finding.FindingId); logger.LogInformation(" Status: {Status}", finding.Status); logger.LogInformation(" Severity: {Severity}", FormatSeverity(finding.Severity)); logger.LogInformation(" SBOM: {Sbom}", finding.SbomId); logger.LogInformation(" Policy version: {Version}", finding.PolicyVersion); logger.LogInformation(" Updated (UTC): {Updated}", FormatUpdatedAt(finding.UpdatedAt)); if (!string.IsNullOrWhiteSpace(finding.RunId)) { logger.LogInformation(" Run Id: {Run}", finding.RunId); } if (finding.AdvisoryIds.Count > 0) { logger.LogInformation(" Advisories: {Advisories}", string.Join(", ", finding.AdvisoryIds)); } if (!string.IsNullOrWhiteSpace(FormatVexMetadata(finding.Vex))) { logger.LogInformation(" VEX: {Vex}", FormatVexMetadata(finding.Vex)); } } } private static void RenderPolicyFindingExplain(ILogger logger, PolicyFindingExplainResult explain) { if (explain.Steps.Count == 0) { if (AnsiConsole.Profile.Capabilities.Interactive) { AnsiConsole.MarkupLine("[yellow]No explain steps were returned.[/]"); } else { logger.LogWarning("No explain steps were returned."); } } else if (AnsiConsole.Profile.Capabilities.Interactive) { var table = new Table() .Border(TableBorder.Rounded) .AddColumn("Rule") .AddColumn("Status") .AddColumn("Action") .AddColumn("Score") .AddColumn("Inputs") .AddColumn("Evidence"); foreach (var step in explain.Steps) { table.AddRow( Markup.Escape(step.Rule), Markup.Escape(step.Status ?? "(n/a)"), Markup.Escape(step.Action ?? "(n/a)"), Markup.Escape(step.Score.HasValue ? step.Score.Value.ToString("0.00", CultureInfo.InvariantCulture) : "-"), Markup.Escape(FormatKeyValuePairs(step.Inputs)), Markup.Escape(FormatKeyValuePairs(step.Evidence))); } AnsiConsole.Write(table); } else { logger.LogInformation("{Count} explain step(s).", explain.Steps.Count); foreach (var step in explain.Steps) { logger.LogInformation( "Rule {Rule} — Status {Status}, Action {Action}, Score {Score}, Inputs {Inputs}", step.Rule, step.Status ?? "n/a", step.Action ?? "n/a", step.Score?.ToString("0.00", CultureInfo.InvariantCulture) ?? "n/a", FormatKeyValuePairs(step.Inputs)); if (step.Evidence is not null && step.Evidence.Count > 0) { logger.LogInformation(" Evidence: {Evidence}", FormatKeyValuePairs(step.Evidence)); } } } if (explain.SealedHints.Count > 0) { if (AnsiConsole.Profile.Capabilities.Interactive) { AnsiConsole.MarkupLine("[grey]Hints:[/]"); foreach (var hint in explain.SealedHints) { AnsiConsole.MarkupLine($" • {Markup.Escape(hint.Message)}"); } } else { foreach (var hint in explain.SealedHints) { logger.LogInformation("Hint: {Hint}", hint.Message); } } } } private static string FormatSeverity(PolicyFindingSeverity severity) { if (severity.Score.HasValue) { return FormattableString.Invariant($"{severity.Normalized} ({severity.Score.Value:0.00})"); } return severity.Normalized; } private static string FormatListPreview(IReadOnlyList values) { if (values is null || values.Count == 0) { return "(none)"; } const int MaxItems = 3; if (values.Count <= MaxItems) { return string.Join(", ", values); } var preview = string.Join(", ", values.Take(MaxItems)); return FormattableString.Invariant($"{preview} (+{values.Count - MaxItems})"); } private static string FormatUpdatedAt(DateTimeOffset timestamp) { if (timestamp == DateTimeOffset.MinValue) { return "(unknown)"; } return timestamp.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss'Z'", CultureInfo.InvariantCulture); } private static string FormatScore(double? score) => score.HasValue ? score.Value.ToString("0.00", CultureInfo.InvariantCulture) : "-"; private static string FormatKeyValuePairs(IReadOnlyDictionary? values) { if (values is null || values.Count == 0) { return "(none)"; } return string.Join(", ", values.Select(pair => $"{pair.Key}={pair.Value}")); } private static string FormatVexMetadata(PolicyFindingVexMetadata? value) { if (value is null) { return "(none)"; } var parts = new List(3); if (!string.IsNullOrWhiteSpace(value.WinningStatementId)) { parts.Add($"winning={value.WinningStatementId}"); } if (!string.IsNullOrWhiteSpace(value.Source)) { parts.Add($"source={value.Source}"); } if (!string.IsNullOrWhiteSpace(value.Status)) { parts.Add($"status={value.Status}"); } return parts.Count == 0 ? "(none)" : string.Join(", ", parts); } private static void HandlePolicyFindingsFailure(PolicyApiException exception, ILogger logger, Action recordMetric) { var exitCode = exception.StatusCode switch { HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden => 12, HttpStatusCode.NotFound => 1, _ => 1 }; if (string.IsNullOrWhiteSpace(exception.ErrorCode)) { logger.LogError("Policy API request failed ({StatusCode}): {Message}", (int)exception.StatusCode, exception.Message); } else { logger.LogError("Policy API request failed ({StatusCode} {Code}): {Message}", (int)exception.StatusCode, exception.ErrorCode, exception.Message); } recordMetric("error"); Environment.ExitCode = exitCode; } private static string FormatDelta(int? value) => value.HasValue ? value.Value.ToString("N0", CultureInfo.InvariantCulture) : "-"; private static readonly JsonSerializerOptions SimulationJsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true }; private static readonly IReadOnlyDictionary EmptyPolicyEnvironment = new ReadOnlyDictionary(new Dictionary(0, StringComparer.Ordinal)); private static readonly IReadOnlyList EmptyPolicySbomSet = new ReadOnlyCollection(Array.Empty()); private static readonly IReadOnlyDictionary EmptyLabelSelectors = new ReadOnlyDictionary(new Dictionary(0, StringComparer.OrdinalIgnoreCase)); private enum TaskRunnerSimulationOutputFormat { Table, Json } private enum PolicySimulationOutputFormat { Table, Json } private enum PolicyFindingsOutputFormat { Table, Json } private static string FormatAdditionalValue(object? value) { return value switch { null => "null", bool b => b ? "true" : "false", double d => d.ToString("G17", CultureInfo.InvariantCulture), float f => f.ToString("G9", CultureInfo.InvariantCulture), IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), _ => value.ToString() ?? string.Empty }; } private static IReadOnlyList NormalizeProviders(IReadOnlyList providers) { if (providers is null || providers.Count == 0) { return Array.Empty(); } var list = new List(); foreach (var provider in providers) { if (!string.IsNullOrWhiteSpace(provider)) { list.Add(provider.Trim()); } } return list.Count == 0 ? Array.Empty() : list; } private static string ResolveTenant(string? tenantOption) { if (!string.IsNullOrWhiteSpace(tenantOption)) { return tenantOption.Trim(); } var fromEnvironment = Environment.GetEnvironmentVariable("STELLA_TENANT"); return string.IsNullOrWhiteSpace(fromEnvironment) ? string.Empty : fromEnvironment.Trim(); } private static async Task LoadIngestInputAsync(IServiceProvider services, string input, CancellationToken cancellationToken) { if (Uri.TryCreate(input, UriKind.Absolute, out var uri) && (uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))) { return await LoadIngestInputFromHttpAsync(services, uri, cancellationToken).ConfigureAwait(false); } return await LoadIngestInputFromFileAsync(input, cancellationToken).ConfigureAwait(false); } private static async Task LoadIngestInputFromHttpAsync(IServiceProvider services, Uri uri, CancellationToken cancellationToken) { var httpClientFactory = services.GetRequiredService(); var httpClient = httpClientFactory.CreateClient("stellaops-cli.ingest-download"); using var response = await httpClient.GetAsync(uri, cancellationToken).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { throw new InvalidOperationException($"Failed to download document from {uri} (HTTP {(int)response.StatusCode})."); } var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/json"; var contentEncoding = response.Content.Headers.ContentEncoding is { Count: > 0 } ? string.Join(",", response.Content.Headers.ContentEncoding) : null; var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); var normalized = NormalizeDocument(bytes, contentType, contentEncoding); return new IngestInputPayload( "uri", uri.ToString(), normalized.Content, normalized.ContentType, normalized.ContentEncoding); } private static async Task LoadIngestInputFromFileAsync(string path, CancellationToken cancellationToken) { var fullPath = Path.GetFullPath(path); if (!File.Exists(fullPath)) { throw new FileNotFoundException("Input document not found.", fullPath); } var bytes = await File.ReadAllBytesAsync(fullPath, cancellationToken).ConfigureAwait(false); var normalized = NormalizeDocument(bytes, GuessContentTypeFromExtension(fullPath), null); return new IngestInputPayload( "file", Path.GetFileName(fullPath), normalized.Content, normalized.ContentType, normalized.ContentEncoding); } private static DocumentNormalizationResult NormalizeDocument(byte[] bytes, string? contentType, string? encodingHint) { if (bytes is null || bytes.Length == 0) { throw new InvalidOperationException("Input document is empty."); } var working = bytes; var encodings = new List(); if (!string.IsNullOrWhiteSpace(encodingHint)) { encodings.Add(encodingHint); } if (IsGzip(working)) { working = DecompressGzip(working); encodings.Add("gzip"); } var text = DecodeText(working); var trimmed = text.TrimStart(); if (!string.IsNullOrWhiteSpace(trimmed) && trimmed[0] != '{' && trimmed[0] != '[') { if (TryDecodeBase64(text, out var decodedBytes)) { working = decodedBytes; encodings.Add("base64"); if (IsGzip(working)) { working = DecompressGzip(working); encodings.Add("gzip"); } text = DecodeText(working); } } text = text.Trim(); if (string.IsNullOrWhiteSpace(text)) { throw new InvalidOperationException("Input document contained no data after decoding."); } var encodingLabel = encodings.Count == 0 ? null : string.Join("+", encodings); var finalContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; return new DocumentNormalizationResult(text, finalContentType, encodingLabel); } private static string GuessContentTypeFromExtension(string path) { var extension = Path.GetExtension(path); if (string.IsNullOrWhiteSpace(extension)) { return "application/json"; } return extension.ToLowerInvariant() switch { ".json" or ".csaf" => "application/json", ".xml" => "application/xml", _ => "application/json" }; } private static DateTimeOffset DetermineVerificationSince(string? sinceOption) { if (string.IsNullOrWhiteSpace(sinceOption)) { return DateTimeOffset.UtcNow.AddHours(-24); } var trimmed = sinceOption.Trim(); if (DateTimeOffset.TryParse( trimmed, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsedTimestamp)) { return parsedTimestamp.ToUniversalTime(); } if (TryParseRelativeDuration(trimmed, out var duration)) { return DateTimeOffset.UtcNow.Subtract(duration); } throw new InvalidOperationException("Invalid --since value. Use ISO-8601 timestamp or duration (e.g. 24h, 7d)."); } private static bool TryParseRelativeDuration(string value, out TimeSpan duration) { duration = TimeSpan.Zero; if (string.IsNullOrWhiteSpace(value)) { return false; } var normalized = value.Trim().ToLowerInvariant(); if (normalized.Length < 2) { return false; } var suffix = normalized[^1]; var magnitudeText = normalized[..^1]; double multiplier = suffix switch { 's' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, 'w' => 604800, _ => 0 }; if (multiplier == 0) { return false; } if (!double.TryParse(magnitudeText, NumberStyles.Float, CultureInfo.InvariantCulture, out var magnitude)) { return false; } if (double.IsNaN(magnitude) || double.IsInfinity(magnitude) || magnitude <= 0) { return false; } var seconds = magnitude * multiplier; if (double.IsNaN(seconds) || double.IsInfinity(seconds) || seconds <= 0) { return false; } duration = TimeSpan.FromSeconds(seconds); return true; } private static int NormalizeLimit(int? limitOption) { if (!limitOption.HasValue) { return 20; } if (limitOption.Value < 0) { throw new InvalidOperationException("Limit cannot be negative."); } return limitOption.Value; } private static IReadOnlyList ParseCommaSeparatedList(string? raw) { if (string.IsNullOrWhiteSpace(raw)) { return Array.Empty(); } var tokens = raw .Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(token => token.Trim()) .Where(token => token.Length > 0) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); return tokens.Length == 0 ? Array.Empty() : tokens; } private static string FormatWindowRange(AocVerifyWindow? window) { if (window is null) { return "(unspecified)"; } var fromText = window.From?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) ?? "(unknown)"; var toText = window.To?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) ?? "(unknown)"; return $"{fromText} -> {toText}"; } private static string FormatCheckedCounts(AocVerifyChecked? checkedCounts) { if (checkedCounts is null) { return "(unspecified)"; } return $"advisories: {checkedCounts.Advisories.ToString("N0", CultureInfo.InvariantCulture)}, vex: {checkedCounts.Vex.ToString("N0", CultureInfo.InvariantCulture)}"; } private static string DetermineVerifyStatus(AocVerifyResponse? response) { if (response is null) { return "unknown"; } if (response.Truncated == true && (response.Violations is null || response.Violations.Count == 0)) { return "truncated"; } var total = response.Violations?.Sum(violation => Math.Max(0, violation?.Count ?? 0)) ?? 0; return total > 0 ? "violations" : "ok"; } private static string FormatBoolean(bool value, bool useColor) { var text = value ? "yes" : "no"; if (!useColor) { return text; } return value ? $"[yellow]{text}[/]" : $"[green]{text}[/]"; } private static string FormatVerifyStatus(string? status, bool useColor) { var normalized = string.IsNullOrWhiteSpace(status) ? "unknown" : status.Trim(); var escaped = Markup.Escape(normalized); if (!useColor) { return escaped; } return normalized switch { "ok" => $"[green]{escaped}[/]", "violations" => $"[red]{escaped}[/]", "truncated" => $"[yellow]{escaped}[/]", _ => $"[grey]{escaped}[/]" }; } private static string FormatViolationExample(AocVerifyViolationExample? example) { if (example is null) { return "(n/a)"; } var parts = new List(); if (!string.IsNullOrWhiteSpace(example.Source)) { parts.Add(example.Source.Trim()); } if (!string.IsNullOrWhiteSpace(example.DocumentId)) { parts.Add(example.DocumentId.Trim()); } var label = parts.Count == 0 ? "(n/a)" : string.Join(" | ", parts); if (!string.IsNullOrWhiteSpace(example.ContentHash)) { label = $"{label} [{example.ContentHash.Trim()}]"; } return label; } private static void RenderAocVerifyTable(AocVerifyResponse response, bool useColor, int limit) { var summary = new Table().Border(TableBorder.Rounded); summary.AddColumn("Field"); summary.AddColumn("Value"); summary.AddRow("Tenant", Markup.Escape(string.IsNullOrWhiteSpace(response?.Tenant) ? "(unknown)" : response.Tenant!)); summary.AddRow("Window", Markup.Escape(FormatWindowRange(response?.Window))); summary.AddRow("Checked", Markup.Escape(FormatCheckedCounts(response?.Checked))); summary.AddRow("Limit", Markup.Escape(limit <= 0 ? "unbounded" : limit.ToString(CultureInfo.InvariantCulture))); summary.AddRow("Status", FormatVerifyStatus(DetermineVerifyStatus(response), useColor)); if (response?.Metrics?.IngestionWriteTotal is int writes) { summary.AddRow("Ingestion Writes", Markup.Escape(writes.ToString("N0", CultureInfo.InvariantCulture))); } if (response?.Metrics?.AocViolationTotal is int totalViolations) { summary.AddRow("Violations (total)", Markup.Escape(totalViolations.ToString("N0", CultureInfo.InvariantCulture))); } else { var computedViolations = response?.Violations?.Sum(violation => Math.Max(0, violation?.Count ?? 0)) ?? 0; summary.AddRow("Violations (total)", Markup.Escape(computedViolations.ToString("N0", CultureInfo.InvariantCulture))); } summary.AddRow("Truncated", FormatBoolean(response?.Truncated == true, useColor)); AnsiConsole.Write(summary); if (response?.Violations is null || response.Violations.Count == 0) { var message = response?.Truncated == true ? "No violations reported, but results were truncated. Increase --limit to review full output." : "No AOC violations detected in the requested window."; if (useColor) { var color = response?.Truncated == true ? "yellow" : "green"; AnsiConsole.MarkupLine($"[{color}]{Markup.Escape(message)}[/]"); } else { Console.WriteLine(message); } return; } var violationTable = new Table().Border(TableBorder.Rounded); violationTable.AddColumn("Code"); violationTable.AddColumn("Count"); violationTable.AddColumn("Sample Document"); violationTable.AddColumn("Path"); foreach (var violation in response.Violations) { var codeDisplay = FormatViolationCode(violation.Code, useColor); var countDisplay = violation.Count.ToString("N0", CultureInfo.InvariantCulture); var example = violation.Examples?.FirstOrDefault(); var documentDisplay = Markup.Escape(FormatViolationExample(example)); var pathDisplay = example is null || string.IsNullOrWhiteSpace(example.Path) ? "(none)" : example.Path!; violationTable.AddRow(codeDisplay, countDisplay, documentDisplay, Markup.Escape(pathDisplay)); } AnsiConsole.Write(violationTable); } private static int DetermineVerifyExitCode(AocVerifyResponse response) { ArgumentNullException.ThrowIfNull(response); if (response.Violations is not null && response.Violations.Count > 0) { var exitCodes = new List(); foreach (var violation in response.Violations) { if (string.IsNullOrWhiteSpace(violation.Code)) { continue; } if (AocViolationExitCodeMap.TryGetValue(violation.Code, out var mapped)) { exitCodes.Add(mapped); } } if (exitCodes.Count > 0) { return exitCodes.Min(); } return response.Truncated == true ? 18 : 17; } if (response.Truncated == true) { return 18; } return 0; } private static async Task WriteJsonReportAsync(T payload, string destination, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(payload); if (string.IsNullOrWhiteSpace(destination)) { throw new InvalidOperationException("Output path must be provided."); } var outputPath = Path.GetFullPath(destination); var directory = Path.GetDirectoryName(outputPath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(outputPath, json, cancellationToken).ConfigureAwait(false); return outputPath; } private static void RenderDryRunTable(AocIngestDryRunResponse response, bool useColor) { var summary = new Table().Border(TableBorder.Rounded); summary.AddColumn("Field"); summary.AddColumn("Value"); summary.AddRow("Source", Markup.Escape(response?.Source ?? "(unknown)")); summary.AddRow("Tenant", Markup.Escape(response?.Tenant ?? "(unknown)")); summary.AddRow("Guard Version", Markup.Escape(response?.GuardVersion ?? "(unknown)")); summary.AddRow("Status", FormatStatusMarkup(response?.Status, useColor)); var violationCount = response?.Violations?.Count ?? 0; summary.AddRow("Violations", violationCount.ToString(CultureInfo.InvariantCulture)); if (!string.IsNullOrWhiteSpace(response?.Document?.ContentHash)) { summary.AddRow("Content Hash", Markup.Escape(response.Document.ContentHash!)); } if (!string.IsNullOrWhiteSpace(response?.Document?.Supersedes)) { summary.AddRow("Supersedes", Markup.Escape(response.Document.Supersedes!)); } if (!string.IsNullOrWhiteSpace(response?.Document?.Provenance?.Signature?.Format)) { var signature = response.Document.Provenance.Signature; var summaryText = signature!.Present ? signature.Format ?? "present" : "missing"; summary.AddRow("Signature", Markup.Escape(summaryText)); } AnsiConsole.Write(summary); if (violationCount == 0) { if (useColor) { AnsiConsole.MarkupLine("[green]No AOC violations detected.[/]"); } else { Console.WriteLine("No AOC violations detected."); } return; } var violationTable = new Table().Border(TableBorder.Rounded); violationTable.AddColumn("Code"); violationTable.AddColumn("Path"); violationTable.AddColumn("Message"); foreach (var violation in response!.Violations!) { var codeDisplay = FormatViolationCode(violation.Code, useColor); var pathDisplay = string.IsNullOrWhiteSpace(violation.Path) ? "(root)" : violation.Path!; var messageDisplay = string.IsNullOrWhiteSpace(violation.Message) ? "(unspecified)" : violation.Message!; violationTable.AddRow(codeDisplay, Markup.Escape(pathDisplay), Markup.Escape(messageDisplay)); } AnsiConsole.Write(violationTable); } private static int DetermineDryRunExitCode(AocIngestDryRunResponse response) { if (response?.Violations is null || response.Violations.Count == 0) { return 0; } var exitCodes = new List(); foreach (var violation in response.Violations) { if (string.IsNullOrWhiteSpace(violation.Code)) { continue; } if (AocViolationExitCodeMap.TryGetValue(violation.Code, out var mapped)) { exitCodes.Add(mapped); } } if (exitCodes.Count == 0) { return 17; } return exitCodes.Min(); } private static string FormatStatusMarkup(string? status, bool useColor) { var normalized = string.IsNullOrWhiteSpace(status) ? "unknown" : status.Trim(); if (!useColor) { return Markup.Escape(normalized); } return normalized.Equals("ok", StringComparison.OrdinalIgnoreCase) ? $"[green]{Markup.Escape(normalized)}[/]" : $"[red]{Markup.Escape(normalized)}[/]"; } private static string FormatViolationCode(string code, bool useColor) { var sanitized = string.IsNullOrWhiteSpace(code) ? "(unknown)" : code.Trim(); if (!useColor) { return Markup.Escape(sanitized); } return $"[red]{Markup.Escape(sanitized)}[/]"; } private static bool IsGzip(ReadOnlySpan data) { return data.Length >= 2 && data[0] == 0x1F && data[1] == 0x8B; } private static byte[] DecompressGzip(byte[] payload) { using var input = new MemoryStream(payload); using var gzip = new GZipStream(input, CompressionMode.Decompress); using var output = new MemoryStream(); gzip.CopyTo(output); return output.ToArray(); } private static string DecodeText(byte[] payload) { var encoding = DetectEncoding(payload); return encoding.GetString(payload); } private static Encoding DetectEncoding(ReadOnlySpan data) { if (data.Length >= 4) { if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0xFE && data[3] == 0xFF) { return new UTF32Encoding(bigEndian: true, byteOrderMark: true); } if (data[0] == 0xFF && data[1] == 0xFE && data[2] == 0x00 && data[3] == 0x00) { return new UTF32Encoding(bigEndian: false, byteOrderMark: true); } } if (data.Length >= 2) { if (data[0] == 0xFE && data[1] == 0xFF) { return Encoding.BigEndianUnicode; } if (data[0] == 0xFF && data[1] == 0xFE) { return Encoding.Unicode; } } if (data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) { return Encoding.UTF8; } return Encoding.UTF8; } public static async Task HandleKmsExportAsync( IServiceProvider services, string? rootPath, string keyId, string? versionId, string outputPath, bool overwrite, string? passphrase, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("kms-export"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; try { var resolvedPassphrase = ResolvePassphrase(passphrase, "Enter file KMS passphrase:"); if (string.IsNullOrEmpty(resolvedPassphrase)) { logger.LogError("KMS passphrase must be supplied via --passphrase, {EnvironmentVariable}, or interactive prompt.", KmsPassphraseEnvironmentVariable); Environment.ExitCode = 1; return; } var resolvedRoot = ResolveRootDirectory(rootPath); if (!Directory.Exists(resolvedRoot)) { logger.LogError("KMS root directory '{Root}' does not exist.", resolvedRoot); Environment.ExitCode = 1; return; } var outputFullPath = Path.GetFullPath(string.IsNullOrWhiteSpace(outputPath) ? "kms-export.json" : outputPath); if (Directory.Exists(outputFullPath)) { logger.LogError("Output path '{Output}' is a directory. Provide a file path.", outputFullPath); Environment.ExitCode = 1; return; } if (!overwrite && File.Exists(outputFullPath)) { logger.LogError("Output file '{Output}' already exists. Use --force to overwrite.", outputFullPath); Environment.ExitCode = 1; return; } var outputDirectory = Path.GetDirectoryName(outputFullPath); if (!string.IsNullOrEmpty(outputDirectory)) { Directory.CreateDirectory(outputDirectory); } using var client = new FileKmsClient(new FileKmsOptions { RootPath = resolvedRoot, Password = resolvedPassphrase! }); var material = await client.ExportAsync(keyId, versionId, cancellationToken).ConfigureAwait(false); var json = JsonSerializer.Serialize(material, KmsJsonOptions); await File.WriteAllTextAsync(outputFullPath, json, cancellationToken).ConfigureAwait(false); logger.LogInformation("Exported key {KeyId} version {VersionId} to {Output}.", material.KeyId, material.VersionId, outputFullPath); Environment.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to export key material."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } public static async Task HandleKmsImportAsync( IServiceProvider services, string? rootPath, string keyId, string inputPath, string? versionOverride, string? passphrase, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("kms-import"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; try { var resolvedPassphrase = ResolvePassphrase(passphrase, "Enter file KMS passphrase:"); if (string.IsNullOrEmpty(resolvedPassphrase)) { logger.LogError("KMS passphrase must be supplied via --passphrase, {EnvironmentVariable}, or interactive prompt.", KmsPassphraseEnvironmentVariable); Environment.ExitCode = 1; return; } var resolvedRoot = ResolveRootDirectory(rootPath); Directory.CreateDirectory(resolvedRoot); var inputFullPath = Path.GetFullPath(inputPath ?? string.Empty); if (!File.Exists(inputFullPath)) { logger.LogError("Input file '{Input}' does not exist.", inputFullPath); Environment.ExitCode = 1; return; } var json = await File.ReadAllTextAsync(inputFullPath, cancellationToken).ConfigureAwait(false); var material = JsonSerializer.Deserialize(json, KmsJsonOptions) ?? throw new InvalidOperationException("Key material payload is empty."); if (!string.IsNullOrWhiteSpace(versionOverride)) { material = material with { VersionId = versionOverride }; } var sourceKeyId = material.KeyId; material = material with { KeyId = keyId }; using var client = new FileKmsClient(new FileKmsOptions { RootPath = resolvedRoot, Password = resolvedPassphrase! }); var metadata = await client.ImportAsync(keyId, material, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(sourceKeyId) && !string.Equals(sourceKeyId, keyId, StringComparison.Ordinal)) { logger.LogWarning("Imported key material originally identified as '{SourceKeyId}' into '{TargetKeyId}'.", sourceKeyId, keyId); } var activeVersion = metadata.Versions.Length > 0 ? metadata.Versions[^1].VersionId : material.VersionId; logger.LogInformation("Imported key {KeyId} version {VersionId} into {Root}.", metadata.KeyId, activeVersion, resolvedRoot); Environment.ExitCode = 0; } catch (JsonException ex) { logger.LogError(ex, "Failed to parse key material JSON from {Input}.", inputPath); Environment.ExitCode = 1; } catch (Exception ex) { logger.LogError(ex, "Failed to import key material."); Environment.ExitCode = 1; } finally { verbosity.MinimumLevel = previousLevel; } } private static string ResolveRootDirectory(string? rootPath) => Path.GetFullPath(string.IsNullOrWhiteSpace(rootPath) ? "kms" : rootPath); private static string? ResolvePassphrase(string? passphrase, string promptMessage) { if (!string.IsNullOrWhiteSpace(passphrase)) { return passphrase; } var fromEnvironment = Environment.GetEnvironmentVariable(KmsPassphraseEnvironmentVariable); if (!string.IsNullOrWhiteSpace(fromEnvironment)) { return fromEnvironment; } return KmsPassphrasePrompt.Prompt(promptMessage); } private static bool TryDecodeBase64(string text, out byte[] decoded) { decoded = Array.Empty(); if (string.IsNullOrWhiteSpace(text)) { return false; } var builder = new StringBuilder(text.Length); foreach (var ch in text) { if (!char.IsWhiteSpace(ch)) { builder.Append(ch); } } var candidate = builder.ToString(); if (candidate.Length < 8 || candidate.Length % 4 != 0) { return false; } for (var i = 0; i < candidate.Length; i++) { var c = candidate[i]; if (!(char.IsLetterOrDigit(c) || c is '+' or '/' or '=')) { return false; } } try { decoded = Convert.FromBase64String(candidate); return true; } catch (FormatException) { return false; } } private sealed record IngestInputPayload(string Kind, string Name, string Content, string ContentType, string? ContentEncoding); private sealed record DocumentNormalizationResult(string Content, string ContentType, string? ContentEncoding); private static readonly IReadOnlyDictionary AocViolationExitCodeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["ERR_AOC_001"] = 11, ["ERR_AOC_002"] = 12, ["ERR_AOC_003"] = 13, ["ERR_AOC_004"] = 14, ["ERR_AOC_005"] = 15, ["ERR_AOC_006"] = 16, ["ERR_AOC_007"] = 17 }; private static string[] NormalizeSections(IReadOnlyList sections) { if (sections is null || sections.Count == 0) { return Array.Empty(); } return sections .Where(section => !string.IsNullOrWhiteSpace(section)) .Select(section => section.Trim()) .Where(section => section.Length > 0) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); } private static void RenderAdvisoryPlan(AdvisoryPipelinePlanResponseModel plan) { var console = AnsiConsole.Console; var summary = new Table() .Border(TableBorder.Rounded) .Title("[bold]Advisory Plan[/]"); summary.AddColumn("Field"); summary.AddColumn("Value"); summary.AddRow("Task", Markup.Escape(plan.TaskType)); summary.AddRow("Cache Key", Markup.Escape(plan.CacheKey)); summary.AddRow("Prompt Template", Markup.Escape(plan.PromptTemplate)); summary.AddRow("Chunks", plan.Chunks.Count.ToString(CultureInfo.InvariantCulture)); summary.AddRow("Vectors", plan.Vectors.Count.ToString(CultureInfo.InvariantCulture)); summary.AddRow("Prompt Tokens", plan.Budget.PromptTokens.ToString(CultureInfo.InvariantCulture)); summary.AddRow("Completion Tokens", plan.Budget.CompletionTokens.ToString(CultureInfo.InvariantCulture)); console.Write(summary); if (plan.Metadata.Count > 0) { console.Write(CreateKeyValueTable("Plan Metadata", plan.Metadata)); } } private static string? RenderAdvisoryOutput(AdvisoryPipelineOutputModel output, AdvisoryOutputFormat format) { return format switch { AdvisoryOutputFormat.Json => RenderAdvisoryOutputJson(output), AdvisoryOutputFormat.Markdown => RenderAdvisoryOutputMarkdown(output), _ => RenderAdvisoryOutputTable(output) }; } private static string RenderAdvisoryOutputJson(AdvisoryPipelineOutputModel output) { return JsonSerializer.Serialize(output, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }); } private static string RenderAdvisoryOutputMarkdown(AdvisoryPipelineOutputModel output) { var builder = new StringBuilder(); builder.AppendLine($"# Advisory {output.TaskType} ({output.Profile})"); builder.AppendLine(); builder.AppendLine($"- Cache Key: `{output.CacheKey}`"); builder.AppendLine($"- Generated: {output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture)}"); builder.AppendLine($"- Plan From Cache: {(output.PlanFromCache ? "yes" : "no")}"); builder.AppendLine($"- Guardrail Blocked: {(output.Guardrail.Blocked ? "yes" : "no")}"); builder.AppendLine(); if (!string.IsNullOrWhiteSpace(output.Response)) { builder.AppendLine("## Response"); builder.AppendLine(output.Response.Trim()); builder.AppendLine(); } if (!string.IsNullOrWhiteSpace(output.Prompt)) { builder.AppendLine("## Prompt (sanitized)"); builder.AppendLine(output.Prompt.Trim()); builder.AppendLine(); } if (output.Citations.Count > 0) { builder.AppendLine("## Citations"); foreach (var citation in output.Citations.OrderBy(c => c.Index)) { builder.AppendLine($"- [{citation.Index}] {citation.DocumentId} :: {citation.ChunkId}"); } builder.AppendLine(); } if (output.Metadata.Count > 0) { builder.AppendLine("## Output Metadata"); foreach (var entry in output.Metadata.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)) { builder.AppendLine($"- **{entry.Key}**: {entry.Value}"); } builder.AppendLine(); } if (output.Guardrail.Metadata.Count > 0) { builder.AppendLine("## Guardrail Metadata"); foreach (var entry in output.Guardrail.Metadata.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)) { builder.AppendLine($"- **{entry.Key}**: {entry.Value}"); } builder.AppendLine(); } if (output.Guardrail.Violations.Count > 0) { builder.AppendLine("## Guardrail Violations"); foreach (var violation in output.Guardrail.Violations) { builder.AppendLine($"- `{violation.Code}`: {violation.Message}"); } builder.AppendLine(); } builder.AppendLine("## Provenance"); builder.AppendLine($"- Input Digest: `{output.Provenance.InputDigest}`"); builder.AppendLine($"- Output Hash: `{output.Provenance.OutputHash}`"); if (output.Provenance.Signatures.Count > 0) { foreach (var signature in output.Provenance.Signatures) { builder.AppendLine($"- Signature: `{signature}`"); } } else { builder.AppendLine("- Signature: none"); } return builder.ToString(); } private static string? RenderAdvisoryOutputTable(AdvisoryPipelineOutputModel output) { var console = AnsiConsole.Console; var summary = new Table() .Border(TableBorder.Rounded) .Title("[bold]Advisory Output[/]"); summary.AddColumn("Field"); summary.AddColumn("Value"); summary.AddRow("Cache Key", Markup.Escape(output.CacheKey)); summary.AddRow("Task", Markup.Escape(output.TaskType)); summary.AddRow("Profile", Markup.Escape(output.Profile)); summary.AddRow("Generated", output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture)); summary.AddRow("Plan From Cache", output.PlanFromCache ? "yes" : "no"); summary.AddRow("Citations", output.Citations.Count.ToString(CultureInfo.InvariantCulture)); summary.AddRow("Guardrail Blocked", output.Guardrail.Blocked ? "[red]yes[/]" : "no"); console.Write(summary); if (!string.IsNullOrWhiteSpace(output.Response)) { var responsePanel = new Panel(new Markup(Markup.Escape(output.Response))) { Header = new PanelHeader("Response"), Border = BoxBorder.Rounded, Expand = true }; console.Write(responsePanel); } if (!string.IsNullOrWhiteSpace(output.Prompt)) { var promptPanel = new Panel(new Markup(Markup.Escape(output.Prompt))) { Header = new PanelHeader("Prompt (sanitized)"), Border = BoxBorder.Rounded, Expand = true }; console.Write(promptPanel); } if (output.Citations.Count > 0) { var citations = new Table() .Border(TableBorder.Minimal) .Title("[grey]Citations[/]"); citations.AddColumn("Index"); citations.AddColumn("Document"); citations.AddColumn("Chunk"); foreach (var citation in output.Citations.OrderBy(c => c.Index)) { citations.AddRow( citation.Index.ToString(CultureInfo.InvariantCulture), Markup.Escape(citation.DocumentId), Markup.Escape(citation.ChunkId)); } console.Write(citations); } if (output.Metadata.Count > 0) { console.Write(CreateKeyValueTable("Output Metadata", output.Metadata)); } if (output.Guardrail.Metadata.Count > 0) { console.Write(CreateKeyValueTable("Guardrail Metadata", output.Guardrail.Metadata)); } if (output.Guardrail.Violations.Count > 0) { var violations = new Table() .Border(TableBorder.Minimal) .Title("[red]Guardrail Violations[/]"); violations.AddColumn("Code"); violations.AddColumn("Message"); foreach (var violation in output.Guardrail.Violations) { violations.AddRow(Markup.Escape(violation.Code), Markup.Escape(violation.Message)); } console.Write(violations); } var provenance = new Table() .Border(TableBorder.Minimal) .Title("[grey]Provenance[/]"); provenance.AddColumn("Field"); provenance.AddColumn("Value"); provenance.AddRow("Input Digest", Markup.Escape(output.Provenance.InputDigest)); provenance.AddRow("Output Hash", Markup.Escape(output.Provenance.OutputHash)); var signatures = output.Provenance.Signatures.Count == 0 ? "none" : string.Join(Environment.NewLine, output.Provenance.Signatures.Select(Markup.Escape)); provenance.AddRow("Signatures", signatures); console.Write(provenance); return null; } private static Table CreateKeyValueTable(string title, IReadOnlyDictionary entries) { var table = new Table() .Border(TableBorder.Minimal) .Title($"[grey]{Markup.Escape(title)}[/]"); table.AddColumn("Key"); table.AddColumn("Value"); foreach (var kvp in entries.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)) { table.AddRow(Markup.Escape(kvp.Key), Markup.Escape(kvp.Value)); } return table; } private static IDictionary RemoveNullValues(Dictionary source) { foreach (var key in source.Where(kvp => kvp.Value is null).Select(kvp => kvp.Key).ToList()) { source.Remove(key); } return source; } private static async Task TriggerJobAsync( IBackendOperationsClient client, ILogger logger, string jobKind, IDictionary parameters, CancellationToken cancellationToken) { JobTriggerResult result = await client.TriggerJobAsync(jobKind, parameters, cancellationToken).ConfigureAwait(false); if (result.Success) { if (!string.IsNullOrWhiteSpace(result.Location)) { logger.LogInformation("Job accepted. Track status at {Location}.", result.Location); } else if (result.Run is not null) { logger.LogInformation("Job accepted. RunId: {RunId} Status: {Status}", result.Run.RunId, result.Run.Status); } else { logger.LogInformation("Job accepted."); } Environment.ExitCode = 0; } else { logger.LogError("Job '{JobKind}' failed: {Message}", jobKind, result.Message); Environment.ExitCode = 1; } } public static Task HandleCryptoProvidersAsync( IServiceProvider services, bool verbose, bool jsonOutput, string? profileOverride, CancellationToken cancellationToken) { using var scope = services.CreateScope(); var loggerFactory = scope.ServiceProvider.GetRequiredService(); var logger = loggerFactory.CreateLogger("crypto-providers"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.crypto.providers", ActivityKind.Internal); using var duration = CliMetrics.MeasureCommandDuration("crypto providers"); try { var registry = scope.ServiceProvider.GetService(); if (registry is null) { logger.LogWarning("Crypto provider registry not available in this environment."); AnsiConsole.MarkupLine("[yellow]Crypto subsystem is not configured in this environment.[/]"); return Task.CompletedTask; } var optionsMonitor = scope.ServiceProvider.GetService>(); var registryOptions = optionsMonitor?.CurrentValue ?? new CryptoProviderRegistryOptions(); var preferredOrder = DeterminePreferredOrder(registryOptions, profileOverride); var providers = registry.Providers .Select(provider => new ProviderInfo( provider.Name, provider.GetType().FullName ?? provider.GetType().Name, DescribeProviderKeys(provider).ToList())) .ToList(); if (jsonOutput) { var payload = new { activeProfile = registryOptions.ActiveProfile, preferredOrder, providers = providers.Select(info => new { info.Name, info.Type, keys = info.Keys.Select(k => new { k.KeyId, k.AlgorithmId, Metadata = k.Metadata }) }) }; Console.WriteLine(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); Environment.ExitCode = 0; return Task.CompletedTask; } RenderCryptoProviders(preferredOrder, providers); Environment.ExitCode = 0; } finally { verbosity.MinimumLevel = previousLevel; } return Task.CompletedTask; } public static Task HandleNodeLockValidateAsync( IServiceProvider services, string? rootPath, string format, bool verbose, CancellationToken cancellationToken) => HandleLanguageLockValidateAsync( services, loggerCategory: "node-lock-validate", activityName: "cli.node.lock_validate", rootTag: "stellaops.cli.node.root", declaredTag: "stellaops.cli.node.declared_only", missingTag: "stellaops.cli.node.lock_missing", commandName: "node lock-validate", analyzer: new NodeLanguageAnalyzer(), rootPath: rootPath, format: format, verbose: verbose, cancellationToken: cancellationToken, telemetryRecorder: CliMetrics.RecordNodeLockValidate); public static Task HandlePythonLockValidateAsync( IServiceProvider services, string? rootPath, string format, bool verbose, CancellationToken cancellationToken) => HandleLanguageLockValidateAsync( services, loggerCategory: "python-lock-validate", activityName: "cli.python.lock_validate", rootTag: "stellaops.cli.python.root", declaredTag: "stellaops.cli.python.declared_only", missingTag: "stellaops.cli.python.lock_missing", commandName: "python lock-validate", analyzer: new PythonLanguageAnalyzer(), rootPath: rootPath, format: format, verbose: verbose, cancellationToken: cancellationToken, telemetryRecorder: CliMetrics.RecordPythonLockValidate); public static Task HandleJavaLockValidateAsync( IServiceProvider services, string? rootPath, string format, bool verbose, CancellationToken cancellationToken) => HandleLanguageLockValidateAsync( services, loggerCategory: "java-lock-validate", activityName: "cli.java.lock_validate", rootTag: "stellaops.cli.java.root", declaredTag: "stellaops.cli.java.declared_only", missingTag: "stellaops.cli.java.lock_missing", commandName: "java lock-validate", analyzer: new JavaLanguageAnalyzer(), rootPath: rootPath, format: format, verbose: verbose, cancellationToken: cancellationToken, telemetryRecorder: CliMetrics.RecordJavaLockValidate); private static async Task HandleLanguageLockValidateAsync( IServiceProvider services, string loggerCategory, string activityName, string rootTag, string declaredTag, string missingTag, string commandName, ILanguageAnalyzer analyzer, string? rootPath, string format, bool verbose, CancellationToken cancellationToken, Action telemetryRecorder) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger(loggerCategory); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity(activityName, ActivityKind.Internal); using var duration = CliMetrics.MeasureCommandDuration(commandName); var outcome = "unknown"; try { var normalizedFormat = string.IsNullOrWhiteSpace(format) ? "table" : format.Trim().ToLowerInvariant(); if (normalizedFormat is not ("table" or "json")) { throw new InvalidOperationException("Format must be either 'table' or 'json'."); } var targetRoot = string.IsNullOrWhiteSpace(rootPath) ? Directory.GetCurrentDirectory() : Path.GetFullPath(rootPath); if (!Directory.Exists(targetRoot)) { throw new DirectoryNotFoundException($"Directory '{targetRoot}' was not found."); } logger.LogInformation("Validating lockfiles in {Root}.", targetRoot); activity?.SetTag(rootTag, targetRoot); var engine = new LanguageAnalyzerEngine(new[] { analyzer }); var context = new LanguageAnalyzerContext(targetRoot, TimeProvider.System); var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false); var report = LockValidationReport.Create(result.ToSnapshots()); activity?.SetTag(declaredTag, report.DeclaredOnly.Count); activity?.SetTag(missingTag, report.MissingLockMetadata.Count); if (string.Equals(normalizedFormat, "json", StringComparison.Ordinal)) { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }; Console.WriteLine(JsonSerializer.Serialize(report, options)); } else { RenderLockValidationReport(report); } outcome = report.HasIssues ? "violations" : "ok"; Environment.ExitCode = report.HasIssues ? 1 : 0; } catch (DirectoryNotFoundException ex) { outcome = "not_found"; logger.LogError(ex.Message); Environment.ExitCode = 71; } catch (Exception ex) { outcome = "error"; logger.LogError(ex, "Lock validation failed."); Environment.ExitCode = 70; } finally { verbosity.MinimumLevel = previousLevel; telemetryRecorder(outcome); } } private static void RenderLockValidationReport(LockValidationReport report) { if (!report.HasIssues) { AnsiConsole.MarkupLine("[green]Lockfiles match installed packages.[/]"); AnsiConsole.MarkupLine($"[grey]Declared components: {report.TotalDeclared}, Installed: {report.TotalInstalled}[/]"); return; } var table = new Table().Border(TableBorder.Rounded); table.AddColumn("Status"); table.AddColumn("Package"); table.AddColumn("Version"); table.AddColumn("Source"); table.AddColumn("Locator"); table.AddColumn("Path"); foreach (var entry in report.DeclaredOnly) { table.AddRow( "[red]Declared Only[/]", Markup.Escape(entry.Name), Markup.Escape(entry.Version ?? "-"), Markup.Escape(entry.LockSource ?? "-"), Markup.Escape(entry.LockLocator ?? "-"), Markup.Escape(entry.Path)); } foreach (var entry in report.MissingLockMetadata) { table.AddRow( "[yellow]Missing Lock[/]", Markup.Escape(entry.Name), Markup.Escape(entry.Version ?? "-"), "-", "-", Markup.Escape(entry.Path)); } AnsiConsole.Write(table); AnsiConsole.MarkupLine($"[grey]Declared components: {report.TotalDeclared}, Installed: {report.TotalInstalled}[/]"); } public static async Task HandleRubyInspectAsync( IServiceProvider services, string? rootPath, string format, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("ruby-inspect"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.ruby.inspect", ActivityKind.Internal); activity?.SetTag("stellaops.cli.command", "ruby inspect"); using var duration = CliMetrics.MeasureCommandDuration("ruby inspect"); var outcome = "unknown"; try { var normalizedFormat = string.IsNullOrWhiteSpace(format) ? "table" : format.Trim().ToLowerInvariant(); if (normalizedFormat is not ("table" or "json")) { throw new InvalidOperationException("Format must be either 'table' or 'json'."); } var targetRoot = string.IsNullOrWhiteSpace(rootPath) ? Directory.GetCurrentDirectory() : Path.GetFullPath(rootPath); if (!Directory.Exists(targetRoot)) { throw new DirectoryNotFoundException($"Directory '{targetRoot}' was not found."); } logger.LogInformation("Inspecting Ruby workspace in {Root}.", targetRoot); activity?.SetTag("stellaops.cli.ruby.root", targetRoot); var engine = new LanguageAnalyzerEngine(new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() }); var context = new LanguageAnalyzerContext(targetRoot, TimeProvider.System); var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false); var report = RubyInspectReport.Create(result.ToSnapshots()); activity?.SetTag("stellaops.cli.ruby.package_count", report.Packages.Count); if (string.Equals(normalizedFormat, "json", StringComparison.Ordinal)) { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }; Console.WriteLine(JsonSerializer.Serialize(report, options)); } else { RenderRubyInspectReport(report); } outcome = report.Packages.Count == 0 ? "empty" : "ok"; Environment.ExitCode = 0; } catch (DirectoryNotFoundException ex) { outcome = "not_found"; logger.LogError(ex.Message); Environment.ExitCode = 71; } catch (InvalidOperationException ex) { outcome = "invalid"; logger.LogError(ex.Message); Environment.ExitCode = 64; } catch (Exception ex) { outcome = "error"; logger.LogError(ex, "Ruby inspect failed."); Environment.ExitCode = 70; } finally { verbosity.MinimumLevel = previousLevel; CliMetrics.RecordRubyInspect(outcome); } } public static async Task HandleRubyResolveAsync( IServiceProvider services, string? imageReference, string? scanId, string format, bool verbose, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("ruby-resolve"); var verbosity = scope.ServiceProvider.GetRequiredService(); var previousLevel = verbosity.MinimumLevel; verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information; using var activity = CliActivitySource.Instance.StartActivity("cli.ruby.resolve", ActivityKind.Client); activity?.SetTag("stellaops.cli.command", "ruby resolve"); using var duration = CliMetrics.MeasureCommandDuration("ruby resolve"); var outcome = "unknown"; try { var normalizedFormat = string.IsNullOrWhiteSpace(format) ? "table" : format.Trim().ToLowerInvariant(); if (normalizedFormat is not ("table" or "json")) { throw new InvalidOperationException("Format must be either 'table' or 'json'."); } var identifier = !string.IsNullOrWhiteSpace(scanId) ? scanId!.Trim() : imageReference?.Trim(); if (string.IsNullOrWhiteSpace(identifier)) { throw new InvalidOperationException("An --image or --scan-id value is required."); } logger.LogInformation("Resolving Ruby packages for scan {ScanId}.", identifier); activity?.SetTag("stellaops.cli.scan_id", identifier); var inventory = await client.GetRubyPackagesAsync(identifier, cancellationToken).ConfigureAwait(false); if (inventory is null) { outcome = "empty"; Environment.ExitCode = 0; AnsiConsole.MarkupLine("[yellow]Ruby package inventory is not available for scan {0}.[/]", Markup.Escape(identifier)); return; } var report = RubyResolveReport.Create(inventory); if (!report.HasPackages) { outcome = "empty"; Environment.ExitCode = 0; var displayScanId = string.IsNullOrWhiteSpace(report.ScanId) ? identifier : report.ScanId; AnsiConsole.MarkupLine("[yellow]No Ruby packages found for scan {0}.[/]", Markup.Escape(displayScanId)); return; } if (string.Equals(normalizedFormat, "json", StringComparison.Ordinal)) { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }; Console.WriteLine(JsonSerializer.Serialize(report, options)); } else { RenderRubyResolveReport(report); } outcome = "ok"; Environment.ExitCode = 0; } catch (InvalidOperationException ex) { outcome = "invalid"; logger.LogError(ex.Message); Environment.ExitCode = 64; } catch (Exception ex) { outcome = "error"; logger.LogError(ex, "Ruby resolve failed."); Environment.ExitCode = 70; } finally { verbosity.MinimumLevel = previousLevel; CliMetrics.RecordRubyResolve(outcome); } } private static void RenderRubyInspectReport(RubyInspectReport report) { if (!report.Packages.Any()) { AnsiConsole.MarkupLine("[yellow]No Ruby packages detected.[/]"); return; } if (report.Observation is { } observation) { var bundler = string.IsNullOrWhiteSpace(observation.BundlerVersion) ? "n/a" : observation.BundlerVersion; AnsiConsole.MarkupLine( "[grey]Observation[/] bundler={0} • packages={1} • runtimeEdges={2}", Markup.Escape(bundler), observation.PackageCount, observation.RuntimeEdgeCount); AnsiConsole.MarkupLine( "[grey]Capabilities[/] exec={0} net={1} serialization={2}", observation.UsesExec ? "[green]on[/]" : "[red]off[/]", observation.UsesNetwork ? "[green]on[/]" : "[red]off[/]", observation.UsesSerialization ? "[green]on[/]" : "[red]off[/]"); if (observation.SchedulerCount > 0) { var schedulerLabel = observation.Schedulers.Count > 0 ? string.Join(", ", observation.Schedulers) : observation.SchedulerCount.ToString(CultureInfo.InvariantCulture); AnsiConsole.MarkupLine("[grey]Schedulers[/] {0}", Markup.Escape(schedulerLabel)); } AnsiConsole.WriteLine(); } var table = new Table().Border(TableBorder.Rounded); table.AddColumn("Package"); table.AddColumn("Version"); table.AddColumn("Groups"); table.AddColumn("Platform"); table.AddColumn(new TableColumn("Source").NoWrap()); table.AddColumn(new TableColumn("Lockfile").NoWrap()); table.AddColumn(new TableColumn("Runtime").NoWrap()); foreach (var entry in report.Packages) { var groups = entry.Groups.Count == 0 ? "-" : string.Join(", ", entry.Groups); var runtime = entry.UsedByEntrypoint ? "[green]Entrypoint[/]" : entry.RuntimeEntrypoints.Count > 0 ? Markup.Escape(string.Join(", ", entry.RuntimeEntrypoints)) : "[grey]-[/]"; table.AddRow( Markup.Escape(entry.Name), Markup.Escape(entry.Version ?? "-"), Markup.Escape(groups), Markup.Escape(entry.Platform ?? "-"), Markup.Escape(entry.Source ?? "-"), Markup.Escape(entry.Lockfile ?? "-"), runtime); } AnsiConsole.Write(table); } private static void RenderRubyResolveReport(RubyResolveReport report) { var table = new Table().Border(TableBorder.Rounded); table.AddColumn("Group"); table.AddColumn("Platform"); table.AddColumn("Package"); table.AddColumn("Version"); table.AddColumn(new TableColumn("Source").NoWrap()); table.AddColumn(new TableColumn("Lockfile").NoWrap()); table.AddColumn(new TableColumn("Runtime").NoWrap()); foreach (var group in report.Groups) { foreach (var package in group.Packages) { var runtime = package.RuntimeEntrypoints.Count > 0 ? Markup.Escape(string.Join(", ", package.RuntimeEntrypoints)) : package.RuntimeUsed ? "[green]Entrypoint[/]" : "[grey]-[/]"; table.AddRow( Markup.Escape(group.Group), Markup.Escape(group.Platform ?? "-"), Markup.Escape(package.Name), Markup.Escape(package.Version ?? "-"), Markup.Escape(package.Source ?? "-"), Markup.Escape(package.Lockfile ?? "-"), runtime); } } AnsiConsole.Write(table); AnsiConsole.MarkupLine("[grey]Scan {0} • Total packages: {1}[/]", Markup.Escape(report.ScanId), report.TotalPackages); } private static void RenderCryptoProviders( IReadOnlyList preferredOrder, IReadOnlyCollection providers) { if (preferredOrder.Count > 0) { AnsiConsole.MarkupLine("[cyan]Preferred order:[/] {0}", Markup.Escape(string.Join(", ", preferredOrder))); } else { AnsiConsole.MarkupLine("[yellow]Preferred order is not configured; using registration order.[/]"); } var table = new Table().Border(TableBorder.Rounded); table.AddColumn("Provider"); table.AddColumn("Type"); table.AddColumn("Keys"); foreach (var provider in providers) { var keySummary = provider.Keys.Count == 0 ? "[grey]No signing keys exposed (managed externally).[/]" : string.Join(Environment.NewLine, provider.Keys.Select(FormatDescriptor)); table.AddRow( Markup.Escape(provider.Name), Markup.Escape(provider.Type), keySummary); } AnsiConsole.Write(table); } private static IReadOnlyList DescribeProviderKeys(ICryptoProvider provider) { if (provider is ICryptoProviderDiagnostics diagnostics) { return diagnostics.DescribeKeys().ToList(); } var signingKeys = provider.GetSigningKeys(); if (signingKeys.Count == 0) { return Array.Empty(); } var descriptors = new List(signingKeys.Count); foreach (var signingKey in signingKeys) { var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["kind"] = signingKey.Kind.ToString(), ["createdAt"] = signingKey.CreatedAt.UtcDateTime.ToString("O"), ["providerHint"] = signingKey.Reference.ProviderHint }; if (signingKey.ExpiresAt.HasValue) { metadata["expiresAt"] = signingKey.ExpiresAt.Value.UtcDateTime.ToString("O"); } foreach (var pair in signingKey.Metadata) { metadata[$"meta.{pair.Key}"] = pair.Value; } descriptors.Add(new CryptoProviderKeyDescriptor( provider.Name, signingKey.Reference.KeyId, signingKey.AlgorithmId, metadata)); } return descriptors; } private sealed class RubyInspectReport { [JsonPropertyName("packages")] public IReadOnlyList Packages { get; } [JsonPropertyName("observation")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public RubyObservationSummary? Observation { get; } private RubyInspectReport(IReadOnlyList packages, RubyObservationSummary? observation) { Packages = packages; Observation = observation; } public static RubyInspectReport Create(IEnumerable? snapshots) { var source = snapshots?.ToArray() ?? Array.Empty(); var entries = source .Where(static snapshot => string.Equals(snapshot.Type, "gem", StringComparison.OrdinalIgnoreCase)) .Select(RubyInspectEntry.FromSnapshot) .OrderBy(static entry => entry.Name, StringComparer.OrdinalIgnoreCase) .ThenBy(static entry => entry.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase) .ToArray(); var observation = RubyObservationSummary.TryCreate(source); return new RubyInspectReport(entries, observation); } } private sealed record RubyInspectEntry( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string? Version, [property: JsonPropertyName("source")] string? Source, [property: JsonPropertyName("lockfile")] string? Lockfile, [property: JsonPropertyName("groups")] IReadOnlyList Groups, [property: JsonPropertyName("platform")] string? Platform, [property: JsonPropertyName("declaredOnly")] bool DeclaredOnly, [property: JsonPropertyName("runtimeEntrypoints")] IReadOnlyList RuntimeEntrypoints, [property: JsonPropertyName("runtimeFiles")] IReadOnlyList RuntimeFiles, [property: JsonPropertyName("runtimeReasons")] IReadOnlyList RuntimeReasons, [property: JsonPropertyName("usedByEntrypoint")] bool UsedByEntrypoint) { public static RubyInspectEntry FromSnapshot(LanguageComponentSnapshot snapshot) { var metadata = RubyMetadataHelpers.Clone(snapshot.Metadata); var groups = RubyMetadataHelpers.GetList(metadata, "groups"); var platform = RubyMetadataHelpers.GetString(metadata, "platform"); var source = RubyMetadataHelpers.GetString(metadata, "source"); var lockfile = RubyMetadataHelpers.GetString(metadata, "lockfile"); var declaredOnly = RubyMetadataHelpers.GetBool(metadata, "declaredOnly") ?? false; var runtimeEntrypoints = RubyMetadataHelpers.GetList(metadata, "runtime.entrypoints"); var runtimeFiles = RubyMetadataHelpers.GetList(metadata, "runtime.files"); var runtimeReasons = RubyMetadataHelpers.GetList(metadata, "runtime.reasons"); var usedByEntrypoint = RubyMetadataHelpers.GetBool(metadata, "runtime.used") ?? snapshot.UsedByEntrypoint; return new RubyInspectEntry( snapshot.Name, snapshot.Version, source, lockfile, groups, platform, declaredOnly, runtimeEntrypoints, runtimeFiles, runtimeReasons, usedByEntrypoint); } } private sealed record RubyObservationSummary( [property: JsonPropertyName("packageCount")] int PackageCount, [property: JsonPropertyName("runtimeEdgeCount")] int RuntimeEdgeCount, [property: JsonPropertyName("bundlerVersion")] string? BundlerVersion, [property: JsonPropertyName("usesExec")] bool UsesExec, [property: JsonPropertyName("usesNetwork")] bool UsesNetwork, [property: JsonPropertyName("usesSerialization")] bool UsesSerialization, [property: JsonPropertyName("schedulerCount")] int SchedulerCount, [property: JsonPropertyName("schedulers")] IReadOnlyList Schedulers) { public static RubyObservationSummary? TryCreate(IEnumerable snapshots) { var observation = snapshots.FirstOrDefault(static snapshot => string.Equals(snapshot.Type, "ruby-observation", StringComparison.OrdinalIgnoreCase)); if (observation is null) { return null; } var metadata = RubyMetadataHelpers.Clone(observation.Metadata); var schedulers = RubyMetadataHelpers.GetList(metadata, "ruby.observation.capability.scheduler_list"); return new RubyObservationSummary( RubyMetadataHelpers.GetInt(metadata, "ruby.observation.packages") ?? 0, RubyMetadataHelpers.GetInt(metadata, "ruby.observation.runtime_edges") ?? 0, RubyMetadataHelpers.GetString(metadata, "ruby.observation.bundler_version"), RubyMetadataHelpers.GetBool(metadata, "ruby.observation.capability.exec") ?? false, RubyMetadataHelpers.GetBool(metadata, "ruby.observation.capability.net") ?? false, RubyMetadataHelpers.GetBool(metadata, "ruby.observation.capability.serialization") ?? false, RubyMetadataHelpers.GetInt(metadata, "ruby.observation.capability.schedulers") ?? schedulers.Count, schedulers); } } private sealed class RubyResolveReport { [JsonPropertyName("scanId")] public string ScanId { get; } [JsonPropertyName("imageDigest")] public string ImageDigest { get; } [JsonPropertyName("generatedAt")] public DateTimeOffset GeneratedAt { get; } [JsonPropertyName("groups")] public IReadOnlyList Groups { get; } [JsonIgnore] public bool HasPackages => TotalPackages > 0; [JsonIgnore] public int TotalPackages => Groups.Sum(static group => group.Packages.Count); private RubyResolveReport(string scanId, string imageDigest, DateTimeOffset generatedAt, IReadOnlyList groups) { ScanId = scanId; ImageDigest = imageDigest; GeneratedAt = generatedAt; Groups = groups; } public static RubyResolveReport Create(RubyPackageInventoryModel inventory) { var resolved = (inventory.Packages ?? Array.Empty()) .Select(RubyResolvePackage.FromModel) .ToArray(); var rows = new List<(string Group, string Platform, RubyResolvePackage Package)>(); foreach (var package in resolved) { var groups = package.Groups.Count == 0 ? new[] { "(default)" } : package.Groups; foreach (var group in groups) { rows.Add((group, package.Platform ?? "-", package)); } } var grouped = rows .GroupBy(static row => (row.Group, row.Platform)) .OrderBy(static g => g.Key.Group, StringComparer.OrdinalIgnoreCase) .ThenBy(static g => g.Key.Platform, StringComparer.OrdinalIgnoreCase) .Select(group => new RubyResolveGroup( group.Key.Group, group.Key.Platform, group.Select(row => row.Package) .OrderBy(static pkg => pkg.Name, StringComparer.OrdinalIgnoreCase) .ThenBy(static pkg => pkg.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase) .ToArray())) .ToArray(); var normalizedScanId = inventory.ScanId ?? string.Empty; var normalizedDigest = inventory.ImageDigest ?? string.Empty; return new RubyResolveReport(normalizedScanId, normalizedDigest, inventory.GeneratedAt, grouped); } } private sealed record RubyResolveGroup( [property: JsonPropertyName("group")] string Group, [property: JsonPropertyName("platform")] string Platform, [property: JsonPropertyName("packages")] IReadOnlyList Packages); private sealed record RubyResolvePackage( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string? Version, [property: JsonPropertyName("source")] string? Source, [property: JsonPropertyName("lockfile")] string? Lockfile, [property: JsonPropertyName("groups")] IReadOnlyList Groups, [property: JsonPropertyName("platform")] string? Platform, [property: JsonPropertyName("declaredOnly")] bool DeclaredOnly, [property: JsonPropertyName("runtimeEntrypoints")] IReadOnlyList RuntimeEntrypoints, [property: JsonPropertyName("runtimeFiles")] IReadOnlyList RuntimeFiles, [property: JsonPropertyName("runtimeReasons")] IReadOnlyList RuntimeReasons, [property: JsonPropertyName("runtimeUsed")] bool RuntimeUsed) { public static RubyResolvePackage FromModel(RubyPackageArtifactModel model) { var metadata = RubyMetadataHelpers.Clone(model.Metadata); IReadOnlyList groups = model.Groups is { Count: > 0 } ? model.Groups .Where(static group => !string.IsNullOrWhiteSpace(group)) .Select(static group => group.Trim()) .ToArray() : RubyMetadataHelpers.GetList(metadata, "groups"); IReadOnlyList? runtimeEntrypoints = model.Runtime?.Entrypoints?.Where(static e => !string.IsNullOrWhiteSpace(e)).Select(static e => e.Trim()).ToArray(); if (runtimeEntrypoints is null || runtimeEntrypoints.Count == 0) { runtimeEntrypoints = RubyMetadataHelpers.GetList(metadata, "runtime.entrypoints"); } IReadOnlyList? runtimeFiles = model.Runtime?.Files?.Where(static e => !string.IsNullOrWhiteSpace(e)).Select(static e => e.Trim()).ToArray(); if (runtimeFiles is null || runtimeFiles.Count == 0) { runtimeFiles = RubyMetadataHelpers.GetList(metadata, "runtime.files"); } IReadOnlyList? runtimeReasons = model.Runtime?.Reasons?.Where(static e => !string.IsNullOrWhiteSpace(e)).Select(static e => e.Trim()).ToArray(); if (runtimeReasons is null || runtimeReasons.Count == 0) { runtimeReasons = RubyMetadataHelpers.GetList(metadata, "runtime.reasons"); } runtimeEntrypoints ??= Array.Empty(); runtimeFiles ??= Array.Empty(); runtimeReasons ??= Array.Empty(); var source = model.Provenance?.Source ?? model.Source ?? RubyMetadataHelpers.GetString(metadata, "source"); var lockfile = model.Provenance?.Lockfile ?? RubyMetadataHelpers.GetString(metadata, "lockfile"); var platform = model.Platform ?? RubyMetadataHelpers.GetString(metadata, "platform"); var declaredOnly = model.DeclaredOnly ?? RubyMetadataHelpers.GetBool(metadata, "declaredOnly") ?? false; var runtimeUsed = model.RuntimeUsed ?? RubyMetadataHelpers.GetBool(metadata, "runtime.used") ?? false; return new RubyResolvePackage( model.Name, model.Version, source, lockfile, groups, platform, declaredOnly, runtimeEntrypoints, runtimeFiles, runtimeReasons, runtimeUsed); } } private static class RubyMetadataHelpers { public static IDictionary Clone(IDictionary? metadata) { if (metadata is null || metadata.Count == 0) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } var clone = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var pair in metadata) { clone[pair.Key] = pair.Value; } return clone; } public static string? GetString(IDictionary metadata, string key) { if (metadata.TryGetValue(key, out var value)) { return value; } foreach (var pair in metadata) { if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase)) { return pair.Value; } } return null; } public static IReadOnlyList GetList(IDictionary metadata, string key) { var value = GetString(metadata, key); if (string.IsNullOrWhiteSpace(value)) { return Array.Empty(); } return value .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToArray(); } public static bool? GetBool(IDictionary metadata, string key) { var value = GetString(metadata, key); if (string.IsNullOrWhiteSpace(value)) { return null; } if (bool.TryParse(value, out var parsed)) { return parsed; } return null; } public static int? GetInt(IDictionary metadata, string key) { var value = GetString(metadata, key); if (string.IsNullOrWhiteSpace(value)) { return null; } if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) { return parsed; } return null; } } private sealed record LockValidationEntry( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string? Version, [property: JsonPropertyName("path")] string Path, [property: JsonPropertyName("lockSource")] string? LockSource, [property: JsonPropertyName("lockLocator")] string? LockLocator, [property: JsonPropertyName("resolved")] string? Resolved, [property: JsonPropertyName("integrity")] string? Integrity); private sealed class LockValidationReport { public LockValidationReport( IReadOnlyList declaredOnly, IReadOnlyList missingLockMetadata, int totalDeclared, int totalInstalled) { DeclaredOnly = declaredOnly; MissingLockMetadata = missingLockMetadata; TotalDeclared = totalDeclared; TotalInstalled = totalInstalled; } [JsonPropertyName("declaredOnly")] public IReadOnlyList DeclaredOnly { get; } [JsonPropertyName("missingLockMetadata")] public IReadOnlyList MissingLockMetadata { get; } [JsonPropertyName("totalDeclared")] public int TotalDeclared { get; } [JsonPropertyName("totalInstalled")] public int TotalInstalled { get; } [JsonIgnore] public bool HasIssues => DeclaredOnly.Count > 0 || MissingLockMetadata.Count > 0; public static LockValidationReport Create(IEnumerable snapshots) { var declaredOnly = new List(); var missingLock = new List(); var declaredCount = 0; var installedCount = 0; foreach (var component in snapshots ?? Array.Empty()) { var metadata = component.Metadata ?? new Dictionary(StringComparer.Ordinal); var entry = CreateEntry(component, metadata); if (IsDeclaredOnly(metadata)) { declaredOnly.Add(entry); declaredCount++; continue; } installedCount++; if (!metadata.TryGetValue("lockSource", out var lockSource) || string.IsNullOrWhiteSpace(lockSource)) { missingLock.Add(entry); } } declaredOnly.Sort(CompareEntries); missingLock.Sort(CompareEntries); return new LockValidationReport(declaredOnly, missingLock, declaredCount, installedCount); } private static LockValidationEntry CreateEntry( LanguageComponentSnapshot component, IDictionary metadata) { metadata.TryGetValue("path", out var path); metadata.TryGetValue("lockSource", out var lockSource); metadata.TryGetValue("lockLocator", out var lockLocator); metadata.TryGetValue("resolved", out var resolved); metadata.TryGetValue("integrity", out var integrity); return new LockValidationEntry( component.Name, component.Version, string.IsNullOrWhiteSpace(path) ? "." : path!, lockSource, lockLocator, resolved, integrity); } private static bool IsDeclaredOnly(IDictionary metadata) { if (metadata.TryGetValue("declaredOnly", out var value)) { return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); } return false; } private static int CompareEntries(LockValidationEntry left, LockValidationEntry right) { var nameComparison = string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase); if (nameComparison != 0) { return nameComparison; } return string.Compare(left.Version, right.Version, StringComparison.OrdinalIgnoreCase); } } private static IReadOnlyList DeterminePreferredOrder( CryptoProviderRegistryOptions? options, string? overrideProfile) { if (options is null) { return Array.Empty(); } if (!string.IsNullOrWhiteSpace(overrideProfile) && options.Profiles.TryGetValue(overrideProfile, out var profile) && profile.PreferredProviders.Count > 0) { return profile.PreferredProviders .Where(static provider => !string.IsNullOrWhiteSpace(provider)) .Select(static provider => provider.Trim()) .ToArray(); } return options.ResolvePreferredProviders(); } private static string FormatDescriptor(CryptoProviderKeyDescriptor descriptor) { if (descriptor.Metadata.Count == 0) { return $"{Markup.Escape(descriptor.KeyId)} ({Markup.Escape(descriptor.AlgorithmId)})"; } var metadataText = string.Join( ", ", descriptor.Metadata.Select(pair => $"{pair.Key}={pair.Value}")); return $"{Markup.Escape(descriptor.KeyId)} ({Markup.Escape(descriptor.AlgorithmId)}){Environment.NewLine}[grey]{Markup.Escape(metadataText)}[/]"; } private sealed record ProviderInfo(string Name, string Type, IReadOnlyList Keys); #region Risk Profile Commands public static async Task HandleRiskProfileValidateAsync( string inputPath, string format, string? outputPath, bool strict, bool verbose) { _ = verbose; using var activity = CliActivitySource.Instance.StartActivity("cli.riskprofile.validate", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("risk-profile validate"); try { if (!File.Exists(inputPath)) { AnsiConsole.MarkupLine("[red]Error:[/] Input file not found: {0}", Markup.Escape(inputPath)); Environment.ExitCode = 1; return; } var profileJson = await File.ReadAllTextAsync(inputPath).ConfigureAwait(false); var schema = StellaOps.Policy.RiskProfile.Schema.RiskProfileSchemaProvider.GetSchema(); var schemaVersion = StellaOps.Policy.RiskProfile.Schema.RiskProfileSchemaProvider.GetSchemaVersion(); JsonNode? profileNode; try { profileNode = JsonNode.Parse(profileJson); if (profileNode is null) { throw new InvalidOperationException("Parsed JSON is null."); } } catch (JsonException ex) { AnsiConsole.MarkupLine("[red]Error:[/] Invalid JSON: {0}", Markup.Escape(ex.Message)); Environment.ExitCode = 1; return; } var result = schema.Evaluate(profileNode); var issues = new List(); if (!result.IsValid) { CollectValidationIssues(result, issues); } var report = new RiskProfileValidationReport( FilePath: inputPath, IsValid: result.IsValid, SchemaVersion: schemaVersion, Issues: issues); if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) { var reportJson = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); if (!string.IsNullOrEmpty(outputPath)) { await File.WriteAllTextAsync(outputPath, reportJson).ConfigureAwait(false); AnsiConsole.MarkupLine("Validation report written to [cyan]{0}[/]", Markup.Escape(outputPath)); } else { Console.WriteLine(reportJson); } } else { if (result.IsValid) { AnsiConsole.MarkupLine("[green]✓[/] Profile is valid (schema v{0})", schemaVersion); } else { AnsiConsole.MarkupLine("[red]✗[/] Profile is invalid (schema v{0})", schemaVersion); AnsiConsole.WriteLine(); var table = new Table(); table.AddColumn("Path"); table.AddColumn("Error"); table.AddColumn("Message"); foreach (var issue in issues) { table.AddRow( Markup.Escape(issue.Path), Markup.Escape(issue.Error), Markup.Escape(issue.Message)); } AnsiConsole.Write(table); } if (!string.IsNullOrEmpty(outputPath)) { var reportJson = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); await File.WriteAllTextAsync(outputPath, reportJson).ConfigureAwait(false); AnsiConsole.MarkupLine("Validation report written to [cyan]{0}[/]", Markup.Escape(outputPath)); } } Environment.ExitCode = result.IsValid ? 0 : (strict ? 1 : 0); if (!result.IsValid && !strict) { Environment.ExitCode = 1; } } catch (Exception ex) { AnsiConsole.MarkupLine("[red]Error:[/] {0}", Markup.Escape(ex.Message)); Environment.ExitCode = 1; } await Task.CompletedTask.ConfigureAwait(false); } public static async Task HandleRiskProfileSchemaAsync(string? outputPath, bool verbose) { _ = verbose; using var activity = CliActivitySource.Instance.StartActivity("cli.riskprofile.schema", ActivityKind.Client); using var duration = CliMetrics.MeasureCommandDuration("risk-profile schema"); try { var schemaText = StellaOps.Policy.RiskProfile.Schema.RiskProfileSchemaProvider.GetSchemaText(); var schemaVersion = StellaOps.Policy.RiskProfile.Schema.RiskProfileSchemaProvider.GetSchemaVersion(); if (!string.IsNullOrEmpty(outputPath)) { await File.WriteAllTextAsync(outputPath, schemaText).ConfigureAwait(false); AnsiConsole.MarkupLine("Risk profile schema v{0} written to [cyan]{1}[/]", schemaVersion, Markup.Escape(outputPath)); } else { Console.WriteLine(schemaText); } Environment.ExitCode = 0; } catch (Exception ex) { AnsiConsole.MarkupLine("[red]Error:[/] {0}", Markup.Escape(ex.Message)); Environment.ExitCode = 1; } } private static void CollectValidationIssues( Json.Schema.EvaluationResults results, List issues, string path = "") { if (results.Errors is not null) { foreach (var (key, message) in results.Errors) { var instancePath = results.InstanceLocation?.ToString() ?? path; issues.Add(new RiskProfileValidationIssue(instancePath, key, message)); } } if (results.Details is not null) { foreach (var detail in results.Details) { if (!detail.IsValid) { CollectValidationIssues(detail, issues, detail.InstanceLocation?.ToString() ?? path); } } } } private sealed record RiskProfileValidationReport( string FilePath, bool IsValid, string SchemaVersion, IReadOnlyList Issues); private sealed record RiskProfileValidationIssue(string Path, string Error, string Message); #endregion }