using System.CommandLine; using System.CommandLine.Invocation; using Microsoft.Extensions.Logging; namespace StellaOps.Cli.Commands.Proof; /// /// Command group for proof chain operations. /// Implements advisory §15 CLI commands. /// public class ProofCommandGroup { private readonly ILogger _logger; public ProofCommandGroup(ILogger logger) { _logger = logger; } /// /// Build the proof command tree. /// public Command BuildCommand() { var proofCommand = new Command("proof", "Proof chain operations"); proofCommand.AddCommand(BuildVerifyCommand()); proofCommand.AddCommand(BuildSpineCommand()); return proofCommand; } private Command BuildVerifyCommand() { var artifactArg = new Argument( name: "artifact", description: "Artifact digest (sha256:...) or PURL"); var sbomOption = new Option( aliases: ["-s", "--sbom"], description: "Path to SBOM file"); var vexOption = new Option( aliases: ["--vex"], description: "Path to VEX file"); var anchorOption = new Option( aliases: ["-a", "--anchor"], description: "Trust anchor ID"); var offlineOption = new Option( name: "--offline", description: "Offline mode (skip Rekor verification)"); var outputOption = new Option( name: "--output", getDefaultValue: () => "text", description: "Output format: text, json"); var verboseOption = new Option( aliases: ["-v", "--verbose"], getDefaultValue: () => 0, description: "Verbose output level (use -vv for very verbose)"); var verifyCommand = new Command("verify", "Verify an artifact's proof chain") { artifactArg, sbomOption, vexOption, anchorOption, offlineOption, outputOption, verboseOption }; verifyCommand.SetHandler(async (context) => { var artifact = context.ParseResult.GetValueForArgument(artifactArg); var sbomFile = context.ParseResult.GetValueForOption(sbomOption); var vexFile = context.ParseResult.GetValueForOption(vexOption); var anchorId = context.ParseResult.GetValueForOption(anchorOption); var offline = context.ParseResult.GetValueForOption(offlineOption); var output = context.ParseResult.GetValueForOption(outputOption) ?? "text"; var verbose = context.ParseResult.GetValueForOption(verboseOption); context.ExitCode = await VerifyAsync( artifact, sbomFile, vexFile, anchorId, offline, output, verbose, context.GetCancellationToken()); }); return verifyCommand; } private Command BuildSpineCommand() { var spineCommand = new Command("spine", "Proof spine operations"); // stellaops proof spine create var createCommand = new Command("create", "Create a proof spine for an artifact"); var artifactArg = new Argument("artifact", "Artifact digest or PURL"); createCommand.AddArgument(artifactArg); createCommand.SetHandler(async (context) => { var artifact = context.ParseResult.GetValueForArgument(artifactArg); context.ExitCode = await CreateSpineAsync(artifact, context.GetCancellationToken()); }); // stellaops proof spine show var showCommand = new Command("show", "Show proof spine details"); var bundleArg = new Argument("bundleId", "Proof bundle ID"); showCommand.AddArgument(bundleArg); showCommand.SetHandler(async (context) => { var bundleId = context.ParseResult.GetValueForArgument(bundleArg); context.ExitCode = await ShowSpineAsync(bundleId, context.GetCancellationToken()); }); spineCommand.AddCommand(createCommand); spineCommand.AddCommand(showCommand); return spineCommand; } private async Task VerifyAsync( string artifact, FileInfo? sbomFile, FileInfo? vexFile, Guid? anchorId, bool offline, string output, int verbose, CancellationToken ct) { try { if (verbose > 0) { _logger.LogDebug("Starting proof verification for {Artifact}", artifact); } // Validate artifact format if (!IsValidArtifactId(artifact)) { _logger.LogError("Invalid artifact format: {Artifact}", artifact); return ProofExitCodes.SystemError; } if (verbose > 0) { _logger.LogDebug("Artifact format valid: {Artifact}", artifact); } // TODO: Implement actual verification using IVerificationPipeline // 1. Load SBOM if provided // 2. Load VEX if provided // 3. Find or use specified trust anchor // 4. Run verification pipeline // 5. Check Rekor inclusion (unless offline) // 6. Generate receipt if (verbose > 0) { _logger.LogDebug("Verification pipeline not yet implemented"); } if (output == "json") { Console.WriteLine("{"); Console.WriteLine($" \"artifact\": \"{artifact}\","); Console.WriteLine(" \"status\": \"pass\","); Console.WriteLine(" \"message\": \"Verification successful (stub)\""); Console.WriteLine("}"); } else { Console.WriteLine("StellaOps Scan Summary"); Console.WriteLine("══════════════════════"); Console.WriteLine($"Artifact: {artifact}"); Console.WriteLine("Status: PASS (stub - verification not yet implemented)"); } return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Verification failed for {Artifact}", artifact); return ProofExitCodes.SystemError; } } private async Task CreateSpineAsync(string artifact, CancellationToken ct) { try { _logger.LogInformation("Creating proof spine for {Artifact}", artifact); // TODO: Implement spine creation using IProofSpineAssembler Console.WriteLine($"Creating proof spine for: {artifact}"); Console.WriteLine("Spine creation not yet implemented"); return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to create spine for {Artifact}", artifact); return ProofExitCodes.SystemError; } } private async Task ShowSpineAsync(string bundleId, CancellationToken ct) { try { _logger.LogInformation("Showing proof spine {BundleId}", bundleId); // TODO: Implement spine retrieval Console.WriteLine($"Proof spine: {bundleId}"); Console.WriteLine("Spine display not yet implemented"); return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to show spine {BundleId}", bundleId); return ProofExitCodes.SystemError; } } private static bool IsValidArtifactId(string artifact) { if (string.IsNullOrWhiteSpace(artifact)) return false; // sha256:<64-hex> if (artifact.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)) { var hash = artifact[7..]; return hash.Length == 64 && hash.All(c => "0123456789abcdef".Contains(char.ToLowerInvariant(c))); } // pkg:type/... if (artifact.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase)) { return artifact.Length > 5; // Minimal PURL validation } return false; } }