// ----------------------------------------------------------------------------- // EvidenceCliCommands.cs // Sprint: SPRINT_20260119_010 Attestor TST Integration // Task: ATT-005 - CLI Commands // Description: CLI commands for evidence storage operations. // ----------------------------------------------------------------------------- using System.CommandLine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Cli.Configuration; namespace StellaOps.Cli.Plugins.Timestamp; /// /// CLI commands for evidence storage operations. /// public static class EvidenceCliCommands { /// /// stella evidence store --artifact --tst --rekor-bundle /// --tsa-chain --ocsp /// public static Command BuildStoreCommand( IServiceProvider services, StellaOpsCliOptions options, Option verboseOption) { var artifactOption = new Option("--artifact", "DSSE envelope or artifact file") { IsRequired = true }; artifactOption.AddAlias("-a"); var tstOption = new Option("--tst", "Timestamp token file"); tstOption.AddAlias("-t"); var rekorOption = new Option("--rekor-bundle", "Rekor bundle JSON file"); rekorOption.AddAlias("-r"); var chainOption = new Option("--tsa-chain", "TSA certificate chain PEM file"); chainOption.AddAlias("-c"); var ocspOption = new Option("--ocsp", "Stapled OCSP response file"); var crlOption = new Option("--crl", "CRL snapshot file"); var cmd = new Command("store", "Store timestamp and attestation evidence.") { artifactOption, tstOption, rekorOption, chainOption, ocspOption, crlOption, verboseOption }; cmd.SetHandler(async (context) => { var artifact = context.ParseResult.GetValueForOption(artifactOption)!; var tst = context.ParseResult.GetValueForOption(tstOption); var rekor = context.ParseResult.GetValueForOption(rekorOption); var chain = context.ParseResult.GetValueForOption(chainOption); var ocsp = context.ParseResult.GetValueForOption(ocspOption); var crl = context.ParseResult.GetValueForOption(crlOption); var verbose = context.ParseResult.GetValueForOption(verboseOption); var logger = services.GetRequiredService>(); try { if (!artifact.Exists) { Console.Error.WriteLine($"Error: Artifact file not found: {artifact.FullName}"); context.ExitCode = 1; return; } // Compute artifact digest await using var stream = artifact.OpenRead(); var hash = await System.Security.Cryptography.SHA256.HashDataAsync(stream, context.GetCancellationToken()); var artifactDigest = $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; Console.WriteLine($"Artifact: {artifact.Name}"); Console.WriteLine($"Digest: {artifactDigest}"); var evidenceStore = services.GetService(); if (evidenceStore is null) { Console.Error.WriteLine("Error: Evidence store not available."); context.ExitCode = 1; return; } var storeRequest = new EvidenceStoreRequest { ArtifactDigest = artifactDigest }; // Load optional files if (tst is not null && tst.Exists) { storeRequest.TimestampToken = await File.ReadAllBytesAsync(tst.FullName, context.GetCancellationToken()); Console.WriteLine($"TST: {tst.Name} ({storeRequest.TimestampToken.Length} bytes)"); } if (rekor is not null && rekor.Exists) { storeRequest.RekorBundle = await File.ReadAllTextAsync(rekor.FullName, context.GetCancellationToken()); Console.WriteLine($"Rekor bundle: {rekor.Name}"); } if (chain is not null && chain.Exists) { storeRequest.TsaChainPem = await File.ReadAllTextAsync(chain.FullName, context.GetCancellationToken()); Console.WriteLine($"TSA chain: {chain.Name}"); } if (ocsp is not null && ocsp.Exists) { storeRequest.OcspResponse = await File.ReadAllBytesAsync(ocsp.FullName, context.GetCancellationToken()); Console.WriteLine($"OCSP response: {ocsp.Name} ({storeRequest.OcspResponse.Length} bytes)"); } if (crl is not null && crl.Exists) { storeRequest.CrlSnapshot = await File.ReadAllBytesAsync(crl.FullName, context.GetCancellationToken()); Console.WriteLine($"CRL snapshot: {crl.Name} ({storeRequest.CrlSnapshot.Length} bytes)"); } var result = await evidenceStore.StoreAsync(storeRequest, context.GetCancellationToken()); Console.WriteLine(); Console.WriteLine($"Evidence stored successfully."); Console.WriteLine($"Evidence ID: {result.EvidenceId}"); if (verbose) { Console.WriteLine($"Stored at: {result.StoredAt:O}"); if (result.TimestampId.HasValue) { Console.WriteLine($"Timestamp evidence ID: {result.TimestampId}"); } } context.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to store evidence"); Console.Error.WriteLine($"Error: {ex.Message}"); context.ExitCode = 1; } }); return cmd; } /// /// stella evidence export --artifact --out /// public static Command BuildExportCommand( IServiceProvider services, Option verboseOption) { var artifactOption = new Option("--artifact", "Artifact digest to export evidence for") { IsRequired = true }; artifactOption.AddAlias("-a"); var outOption = new Option("--out", "Output directory for evidence bundle") { IsRequired = true }; outOption.AddAlias("-o"); var formatOption = new Option("--format", () => "bundle", "Export format: bundle, json, or individual"); var cmd = new Command("export", "Export evidence for an artifact.") { artifactOption, outOption, formatOption, verboseOption }; cmd.SetHandler(async (context) => { var artifact = context.ParseResult.GetValueForOption(artifactOption)!; var outDir = context.ParseResult.GetValueForOption(outOption)!; var format = context.ParseResult.GetValueForOption(formatOption); var verbose = context.ParseResult.GetValueForOption(verboseOption); var logger = services.GetRequiredService>(); try { var evidenceStore = services.GetService(); if (evidenceStore is null) { Console.Error.WriteLine("Error: Evidence store not available."); context.ExitCode = 1; return; } if (!outDir.Exists) { outDir.Create(); } var result = await evidenceStore.ExportAsync( artifact, outDir.FullName, format, context.GetCancellationToken()); Console.WriteLine($"Evidence exported to: {outDir.FullName}"); Console.WriteLine($"Files exported: {result.FileCount}"); if (verbose) { foreach (var file in result.Files) { Console.WriteLine($" {file}"); } } context.ExitCode = 0; } catch (Exception ex) { logger.LogError(ex, "Failed to export evidence"); Console.Error.WriteLine($"Error: {ex.Message}"); context.ExitCode = 1; } }); return cmd; } } #region Service Interfaces /// /// Evidence storage service. /// public interface IEvidenceStore { Task StoreAsync(EvidenceStoreRequest request, CancellationToken ct); Task ExportAsync(string artifactDigest, string outputPath, string format, CancellationToken ct); } /// /// Evidence store request. /// public sealed record EvidenceStoreRequest { public required string ArtifactDigest { get; init; } public byte[]? TimestampToken { get; set; } public string? RekorBundle { get; set; } public string? TsaChainPem { get; set; } public byte[]? OcspResponse { get; set; } public byte[]? CrlSnapshot { get; set; } } /// /// Evidence store result. /// public sealed record EvidenceStoreResult { public required Guid EvidenceId { get; init; } public Guid? TimestampId { get; init; } public required DateTimeOffset StoredAt { get; init; } } /// /// Evidence export result. /// public sealed record EvidenceExportResult { public required int FileCount { get; init; } public required IReadOnlyList Files { get; init; } } #endregion