sprints work.
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// CLI commands for evidence storage operations.
|
||||
/// </summary>
|
||||
public static class EvidenceCliCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// stella evidence store --artifact <file.dsse> --tst <file.tst> --rekor-bundle <file.json>
|
||||
/// --tsa-chain <chain.pem> --ocsp <ocsp.der>
|
||||
/// </summary>
|
||||
public static Command BuildStoreCommand(
|
||||
IServiceProvider services,
|
||||
StellaOpsCliOptions options,
|
||||
Option<bool> verboseOption)
|
||||
{
|
||||
var artifactOption = new Option<FileInfo>("--artifact", "DSSE envelope or artifact file")
|
||||
{
|
||||
IsRequired = true
|
||||
};
|
||||
artifactOption.AddAlias("-a");
|
||||
|
||||
var tstOption = new Option<FileInfo?>("--tst", "Timestamp token file");
|
||||
tstOption.AddAlias("-t");
|
||||
|
||||
var rekorOption = new Option<FileInfo?>("--rekor-bundle", "Rekor bundle JSON file");
|
||||
rekorOption.AddAlias("-r");
|
||||
|
||||
var chainOption = new Option<FileInfo?>("--tsa-chain", "TSA certificate chain PEM file");
|
||||
chainOption.AddAlias("-c");
|
||||
|
||||
var ocspOption = new Option<FileInfo?>("--ocsp", "Stapled OCSP response file");
|
||||
|
||||
var crlOption = new Option<FileInfo?>("--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<ILogger<TimestampCliCommandModule>>();
|
||||
|
||||
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<IEvidenceStore>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// stella evidence export --artifact <digest> --out <directory>
|
||||
/// </summary>
|
||||
public static Command BuildExportCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption)
|
||||
{
|
||||
var artifactOption = new Option<string>("--artifact", "Artifact digest to export evidence for")
|
||||
{
|
||||
IsRequired = true
|
||||
};
|
||||
artifactOption.AddAlias("-a");
|
||||
|
||||
var outOption = new Option<DirectoryInfo>("--out", "Output directory for evidence bundle")
|
||||
{
|
||||
IsRequired = true
|
||||
};
|
||||
outOption.AddAlias("-o");
|
||||
|
||||
var formatOption = new Option<string>("--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<ILogger<TimestampCliCommandModule>>();
|
||||
|
||||
try
|
||||
{
|
||||
var evidenceStore = services.GetService<IEvidenceStore>();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Evidence storage service.
|
||||
/// </summary>
|
||||
public interface IEvidenceStore
|
||||
{
|
||||
Task<EvidenceStoreResult> StoreAsync(EvidenceStoreRequest request, CancellationToken ct);
|
||||
Task<EvidenceExportResult> ExportAsync(string artifactDigest, string outputPath, string format, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence store request.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence store result.
|
||||
/// </summary>
|
||||
public sealed record EvidenceStoreResult
|
||||
{
|
||||
public required Guid EvidenceId { get; init; }
|
||||
public Guid? TimestampId { get; init; }
|
||||
public required DateTimeOffset StoredAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence export result.
|
||||
/// </summary>
|
||||
public sealed record EvidenceExportResult
|
||||
{
|
||||
public required int FileCount { get; init; }
|
||||
public required IReadOnlyList<string> Files { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user