using System.CommandLine; using StellaOps.Cli.Extensions; namespace StellaOps.Cli.Commands; internal static class VerifyCommandGroup { internal static Command BuildVerifyCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var verify = new Command("verify", "Verification commands (offline-first)."); verify.Add(BuildVerifyOfflineCommand(services, verboseOption, cancellationToken)); verify.Add(BuildVerifyImageCommand(services, verboseOption, cancellationToken)); verify.Add(BuildVerifyBundleCommand(services, verboseOption, cancellationToken)); return verify; } private static Command BuildVerifyOfflineCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var evidenceDirOption = new Option("--evidence-dir") { Description = "Path to offline evidence directory (contains keys/, policy/, sboms/, attestations/, tlog/).", Required = true }; var artifactOption = new Option("--artifact") { Description = "Artifact digest to verify (sha256:).", Required = true }; var policyOption = new Option("--policy") { Description = "Policy file path (YAML or JSON). If relative, resolves under evidence-dir.", Required = true }; var outputDirOption = new Option("--output-dir") { Description = "Directory to write deterministic reconciliation outputs." }; var outputOption = new Option("--output", new[] { "-o" }) { Description = "Output format: table (default), json." }.SetDefaultValue("table").FromAmong("table", "json"); var command = new Command("offline", "Verify offline evidence for a specific artifact.") { evidenceDirOption, artifactOption, policyOption, outputDirOption, outputOption, verboseOption }; command.SetAction(parseResult => { var evidenceDir = parseResult.GetValue(evidenceDirOption) ?? string.Empty; var artifact = parseResult.GetValue(artifactOption) ?? string.Empty; var policy = parseResult.GetValue(policyOption) ?? string.Empty; var outputDir = parseResult.GetValue(outputDirOption); var outputFormat = parseResult.GetValue(outputOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVerifyOfflineAsync( services, evidenceDir, artifact, policy, outputDir, outputFormat, verbose, cancellationToken); }); return command; } private static Command BuildVerifyImageCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var referenceArg = new Argument("reference") { Description = "Image reference (registry/repo@sha256:digest or registry/repo:tag)" }; var requireOption = new Option("--require", "-r") { Description = "Required attestation types: sbom, vex, decision, approval", AllowMultipleArgumentsPerToken = true }; requireOption.SetDefaultValue(new[] { "sbom", "vex", "decision" }); var trustPolicyOption = new Option("--trust-policy") { Description = "Path to trust policy file (YAML or JSON)" }; var outputOption = new Option("--output", "-o") { Description = "Output format: table, json, sarif" }.SetDefaultValue("table").FromAmong("table", "json", "sarif"); var strictOption = new Option("--strict") { Description = "Fail if any required attestation is missing" }; var command = new Command("image", "Verify attestation chain for a container image") { referenceArg, requireOption, trustPolicyOption, outputOption, strictOption, verboseOption }; command.SetAction(parseResult => { var reference = parseResult.GetValue(referenceArg) ?? string.Empty; var require = parseResult.GetValue(requireOption) ?? Array.Empty(); var trustPolicy = parseResult.GetValue(trustPolicyOption); var output = parseResult.GetValue(outputOption) ?? "table"; var strict = parseResult.GetValue(strictOption); var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleVerifyImageAsync( services, reference, require, trustPolicy, output, strict, verbose, cancellationToken); }); return command; } private static Command BuildVerifyBundleCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var bundleOption = new Option("--bundle") { Description = "Path to evidence bundle (directory or .tar.gz file).", Required = true }; var skipReplayOption = new Option("--skip-replay") { Description = "Skip verdict replay (only validate input hashes)." }; var outputOption = new Option("--output", new[] { "-o" }) { Description = "Output format: table (default), json." }.SetDefaultValue("table").FromAmong("table", "json"); var command = new Command("bundle", "Verify E2E evidence bundle for reproducibility.") { bundleOption, skipReplayOption, outputOption, verboseOption }; command.SetAction(parseResult => { var bundle = parseResult.GetValue(bundleOption) ?? string.Empty; var skipReplay = parseResult.GetValue(skipReplayOption); var verbose = parseResult.GetValue(verboseOption); var outputFormat = parseResult.GetValue(outputOption) ?? "table"; return CommandHandlers.HandleVerifyBundleAsync( services, bundle, skipReplay, verbose, outputFormat, cancellationToken); }); return command; } }