// ----------------------------------------------------------------------------- // DeltaCommandGroup.cs // Sprint: SPRINT_5100_0002_0003_delta_verdict_generator // Description: CLI commands for delta verdict operations // ----------------------------------------------------------------------------- using System.CommandLine; using System.Text.Json; using System.Text.Json.Serialization; using StellaOps.DeltaVerdict.Engine; using StellaOps.DeltaVerdict.Models; using StellaOps.DeltaVerdict.Oci; using StellaOps.DeltaVerdict.Policy; using StellaOps.DeltaVerdict.Serialization; using StellaOps.DeltaVerdict.Signing; namespace StellaOps.Cli.Commands; public static class DeltaCommandGroup { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public static Command BuildDeltaCommand(Option verboseOption, CancellationToken cancellationToken) { var delta = new Command("delta", "Delta verdict operations"); delta.Add(BuildComputeCommand(verboseOption, cancellationToken)); delta.Add(BuildCheckCommand(verboseOption, cancellationToken)); delta.Add(BuildAttachCommand(verboseOption, cancellationToken)); delta.Add(BuildVerifyCommand(verboseOption, cancellationToken)); delta.Add(BuildPushCommand(verboseOption, cancellationToken)); return delta; } private static Command BuildComputeCommand(Option verboseOption, CancellationToken cancellationToken) { var baseOption = new Option("--base") { Description = "Base verdict JSON file", Required = true }; var headOption = new Option("--head") { Description = "Head verdict JSON file", Required = true }; var outputOption = new Option("--output") { Description = "Output delta JSON path" }; var signOption = new Option("--sign") { Description = "Sign delta verdict" }; var keyIdOption = new Option("--key-id") { Description = "Signing key identifier" }; var secretOption = new Option("--secret") { Description = "Base64 secret for HMAC signing" }; var compute = new Command("compute", "Compute delta between two verdicts"); compute.Add(baseOption); compute.Add(headOption); compute.Add(outputOption); compute.Add(signOption); compute.Add(keyIdOption); compute.Add(secretOption); compute.Add(verboseOption); compute.SetAction(async (parseResult, _) => { var basePath = parseResult.GetValue(baseOption) ?? string.Empty; var headPath = parseResult.GetValue(headOption) ?? string.Empty; var outputPath = parseResult.GetValue(outputOption); var sign = parseResult.GetValue(signOption); var keyId = parseResult.GetValue(keyIdOption) ?? "delta-dev"; var secret = parseResult.GetValue(secretOption); var baseVerdict = VerdictSerializer.Deserialize(await File.ReadAllTextAsync(basePath, cancellationToken)); var headVerdict = VerdictSerializer.Deserialize(await File.ReadAllTextAsync(headPath, cancellationToken)); var engine = new DeltaComputationEngine(); var deltaVerdict = engine.ComputeDelta(baseVerdict, headVerdict); deltaVerdict = DeltaVerdictSerializer.WithDigest(deltaVerdict); if (sign) { var signer = new DeltaSigningService(); deltaVerdict = await signer.SignAsync(deltaVerdict, new SigningOptions { KeyId = keyId, SecretBase64 = secret ?? Convert.ToBase64String("delta-dev-secret"u8.ToArray()) }, cancellationToken); } var json = DeltaVerdictSerializer.Serialize(deltaVerdict); if (!string.IsNullOrWhiteSpace(outputPath)) { await File.WriteAllTextAsync(outputPath, json, cancellationToken); return 0; } Console.WriteLine(json); return 0; }); return compute; } private static Command BuildCheckCommand(Option verboseOption, CancellationToken cancellationToken) { var deltaOption = new Option("--delta") { Description = "Delta verdict JSON file", Required = true }; var budgetOption = new Option("--budget") { Description = "Budget profile (prod|stage|dev) or JSON path", Arity = ArgumentArity.ZeroOrOne }; var outputOption = new Option("--output") { Description = "Output format (text|json)", Arity = ArgumentArity.ZeroOrOne }; var check = new Command("check", "Check delta against risk budget"); check.Add(deltaOption); check.Add(budgetOption); check.Add(outputOption); check.Add(verboseOption); check.SetAction(async (parseResult, _) => { var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty; var budgetValue = parseResult.GetValue(budgetOption); var outputFormat = parseResult.GetValue(outputOption) ?? "text"; var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken)); var budget = await ResolveBudgetAsync(budgetValue, cancellationToken); var evaluator = new RiskBudgetEvaluator(); var result = evaluator.Evaluate(delta, budget); if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); } else { var status = result.IsWithinBudget ? "[PASS]" : "[FAIL]"; Console.WriteLine($"{status} Delta Budget Check"); Console.WriteLine($" Total Changes: {result.Delta.Summary.TotalChanges}"); Console.WriteLine($" Magnitude: {result.Delta.Summary.Magnitude}"); if (result.Violations.Count > 0) { Console.WriteLine(" Violations:"); foreach (var violation in result.Violations) { Console.WriteLine($" - {violation.Category}: {violation.Message}"); } } } return result.IsWithinBudget ? 0 : 2; }); return check; } private static Command BuildAttachCommand(Option verboseOption, CancellationToken cancellationToken) { var deltaOption = new Option("--delta") { Description = "Delta verdict JSON file", Required = true }; var artifactOption = new Option("--artifact") { Description = "OCI artifact reference", Required = true }; var outputOption = new Option("--output") { Description = "Output format (text|json)" }; var attach = new Command("attach", "Prepare OCI attachment metadata for delta verdict"); attach.Add(deltaOption); attach.Add(artifactOption); attach.Add(outputOption); attach.Add(verboseOption); attach.SetAction(async (parseResult, _) => { var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty; var artifactRef = parseResult.GetValue(artifactOption) ?? string.Empty; var outputFormat = parseResult.GetValue(outputOption) ?? "json"; var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken)); var attacher = new DeltaOciAttacher(); var attachment = attacher.CreateAttachment(delta, artifactRef); if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(attachment, JsonOptions)); } else { Console.WriteLine("Delta OCI Attachment"); Console.WriteLine($" Artifact: {attachment.ArtifactReference}"); Console.WriteLine($" MediaType: {attachment.MediaType}"); Console.WriteLine($" PayloadBytes: {attachment.Payload.Length}"); } return 0; }); return attach; } private static async Task ResolveBudgetAsync(string? budgetValue, CancellationToken cancellationToken) { if (!string.IsNullOrWhiteSpace(budgetValue) && File.Exists(budgetValue)) { var json = await File.ReadAllTextAsync(budgetValue, cancellationToken); return JsonSerializer.Deserialize(json, JsonOptions) ?? new RiskBudget(); } return (budgetValue ?? "prod").ToLowerInvariant() switch { "dev" => new RiskBudget { MaxNewCriticalVulnerabilities = 2, MaxNewHighVulnerabilities = 5, MaxRiskScoreIncrease = 25, MaxMagnitude = DeltaMagnitude.Large }, "stage" => new RiskBudget { MaxNewCriticalVulnerabilities = 1, MaxNewHighVulnerabilities = 3, MaxRiskScoreIncrease = 15, MaxMagnitude = DeltaMagnitude.Medium }, _ => new RiskBudget { MaxNewCriticalVulnerabilities = 0, MaxNewHighVulnerabilities = 1, MaxRiskScoreIncrease = 5, MaxMagnitude = DeltaMagnitude.Small } }; } private static Command BuildVerifyCommand(Option verboseOption, CancellationToken cancellationToken) { var deltaOption = new Option("--delta") { Description = "Delta verdict JSON file", Required = true }; var keyIdOption = new Option("--key-id") { Description = "Signing key identifier" }; var secretOption = new Option("--secret") { Description = "Base64 secret for HMAC verification" }; var outputOption = new Option("--output") { Description = "Output format (text|json)", Arity = ArgumentArity.ZeroOrOne }; var verify = new Command("verify", "Verify delta verdict signature"); verify.Add(deltaOption); verify.Add(keyIdOption); verify.Add(secretOption); verify.Add(outputOption); verify.Add(verboseOption); verify.SetAction(async (parseResult, _) => { var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty; var keyId = parseResult.GetValue(keyIdOption) ?? "delta-dev"; var secret = parseResult.GetValue(secretOption); var outputFormat = parseResult.GetValue(outputOption) ?? "text"; var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken)); var signer = new DeltaSigningService(); var result = await signer.VerifyAsync(delta, new VerificationOptions { KeyId = keyId, SecretBase64 = secret ?? Convert.ToBase64String("delta-dev-secret"u8.ToArray()) }, cancellationToken); if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(new { isValid = result.IsValid, error = result.Error, deltaDigest = delta.DeltaDigest }, JsonOptions)); } else { var status = result.IsValid ? "[PASS]" : "[FAIL]"; Console.WriteLine($"{status} Delta Signature Verification"); Console.WriteLine($" Delta Digest: {delta.DeltaDigest ?? "N/A"}"); Console.WriteLine($" Valid: {result.IsValid}"); if (!string.IsNullOrEmpty(result.Error)) { Console.WriteLine($" Error: {result.Error}"); } } return result.IsValid ? 0 : 1; }); return verify; } private static Command BuildPushCommand(Option verboseOption, CancellationToken cancellationToken) { var deltaOption = new Option("--delta") { Description = "Delta verdict JSON file", Required = true }; var targetOption = new Option("--target") { Description = "Target OCI artifact reference (e.g., registry.example.com/repo:tag)", Required = true }; var dryRunOption = new Option("--dry-run") { Description = "Preview push without executing" }; var outputOption = new Option("--output") { Description = "Output format (text|json)" }; var push = new Command("push", "Push delta verdict to OCI registry as referrer"); push.Add(deltaOption); push.Add(targetOption); push.Add(dryRunOption); push.Add(outputOption); push.Add(verboseOption); push.SetAction(async (parseResult, _) => { var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty; var targetRef = parseResult.GetValue(targetOption) ?? string.Empty; var dryRun = parseResult.GetValue(dryRunOption); var outputFormat = parseResult.GetValue(outputOption) ?? "text"; var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken)); var attacher = new DeltaOciAttacher(); var attachment = attacher.CreateAttachment(delta, targetRef); if (dryRun) { if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(new { dryRun = true, artifact = attachment.ArtifactReference, mediaType = attachment.MediaType, payloadSize = attachment.Payload.Length, annotations = attachment.Annotations }, JsonOptions)); } else { Console.WriteLine("[DRY-RUN] Delta OCI Push"); Console.WriteLine($" Target: {attachment.ArtifactReference}"); Console.WriteLine($" MediaType: {attachment.MediaType}"); Console.WriteLine($" PayloadSize: {attachment.Payload.Length} bytes"); Console.WriteLine($" Annotations:"); foreach (var (key, value) in attachment.Annotations) { Console.WriteLine($" {key}: {value}"); } } return 0; } // For actual push, we need to use the OCI pusher infrastructure // This would require DI container setup; for CLI direct usage, output the attachment info if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(attachment, JsonOptions)); } else { Console.WriteLine("Delta OCI Push Prepared"); Console.WriteLine($" Target: {attachment.ArtifactReference}"); Console.WriteLine($" MediaType: {attachment.MediaType}"); Console.WriteLine($" PayloadSize: {attachment.Payload.Length} bytes"); Console.WriteLine(" Use 'oras push' or OCI-compliant tooling to complete the push."); } return 0; }); return push; } }