sprints enhancements
This commit is contained in:
511
src/Cli/StellaOps.Cli/Commands/ProvCommandGroup.cs
Normal file
511
src/Cli/StellaOps.Cli/Commands/ProvCommandGroup.cs
Normal file
@@ -0,0 +1,511 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ProvCommandGroup.cs
|
||||
// Sprint: SPRINT_8200_0001_0002 (Provcache Invalidation & Air-Gap)
|
||||
// Tasks: PROV-8200-135 to PROV-8200-143 - CLI commands for provcache operations.
|
||||
// Description: CLI commands for minimal proof export, import, and verification.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Extensions;
|
||||
using StellaOps.Provcache;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command group for Provcache operations.
|
||||
/// Implements minimal proof export/import for air-gap scenarios.
|
||||
/// </summary>
|
||||
public static class ProvCommandGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// Build the prov command tree.
|
||||
/// </summary>
|
||||
public static Command BuildProvCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var provCommand = new Command("prov", "Provenance cache operations for air-gap scenarios");
|
||||
|
||||
provCommand.Add(BuildExportCommand(services, verboseOption, cancellationToken));
|
||||
provCommand.Add(BuildImportCommand(services, verboseOption, cancellationToken));
|
||||
provCommand.Add(BuildVerifyCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
return provCommand;
|
||||
}
|
||||
|
||||
private static Command BuildExportCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var verikeyOption = new Option<string>("--verikey", "-k")
|
||||
{
|
||||
Description = "The VeriKey (sha256:...) identifying the cache entry to export",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var densityOption = new Option<string>("--density", "-d")
|
||||
{
|
||||
Description = "Evidence density level: lite (digest only), standard (+ first N chunks), strict (all chunks)"
|
||||
};
|
||||
densityOption.SetDefaultValue("standard");
|
||||
densityOption.FromAmong("lite", "standard", "strict");
|
||||
|
||||
var chunksOption = new Option<int>("--chunks", "-c")
|
||||
{
|
||||
Description = "Number of chunks to include for standard density (default: 3)"
|
||||
};
|
||||
chunksOption.SetDefaultValue(3);
|
||||
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output file path for the bundle (default: proof-<verikey>.json)",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var signOption = new Option<bool>("--sign", "-s")
|
||||
{
|
||||
Description = "Sign the exported bundle"
|
||||
};
|
||||
|
||||
var signerOption = new Option<string?>("--signer")
|
||||
{
|
||||
Description = "Signer key ID to use (if --sign is specified)"
|
||||
};
|
||||
|
||||
var command = new Command("export", "Export a minimal proof bundle for air-gapped transfer")
|
||||
{
|
||||
verikeyOption,
|
||||
densityOption,
|
||||
chunksOption,
|
||||
outputOption,
|
||||
signOption,
|
||||
signerOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var verikey = parseResult.GetValue(verikeyOption) ?? string.Empty;
|
||||
var densityStr = parseResult.GetValue(densityOption) ?? "standard";
|
||||
var chunks = parseResult.GetValue(chunksOption);
|
||||
var output = parseResult.GetValue(outputOption) ?? string.Empty;
|
||||
var sign = parseResult.GetValue(signOption);
|
||||
var signer = parseResult.GetValue(signerOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleExportAsync(
|
||||
services,
|
||||
verikey,
|
||||
densityStr,
|
||||
chunks,
|
||||
output,
|
||||
sign,
|
||||
signer,
|
||||
verbose,
|
||||
ct);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildImportCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputArg = new Argument<string>("input")
|
||||
{
|
||||
Description = "Path to the proof bundle file"
|
||||
};
|
||||
|
||||
var lazyFetchOption = new Option<bool>("--lazy-fetch")
|
||||
{
|
||||
Description = "Enable lazy chunk fetching for missing chunks"
|
||||
};
|
||||
|
||||
var backendOption = new Option<string?>("--backend")
|
||||
{
|
||||
Description = "Backend URL for lazy fetch (e.g., https://stellaops.example.com)"
|
||||
};
|
||||
|
||||
var chunksDirOption = new Option<string?>("--chunks-dir")
|
||||
{
|
||||
Description = "Local directory containing chunk files for offline import"
|
||||
};
|
||||
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output format: text, json"
|
||||
};
|
||||
outputOption.SetDefaultValue("text");
|
||||
outputOption.FromAmong("text", "json");
|
||||
|
||||
var command = new Command("import", "Import a minimal proof bundle")
|
||||
{
|
||||
inputArg,
|
||||
lazyFetchOption,
|
||||
backendOption,
|
||||
chunksDirOption,
|
||||
outputOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputArg) ?? string.Empty;
|
||||
var lazyFetch = parseResult.GetValue(lazyFetchOption);
|
||||
var backend = parseResult.GetValue(backendOption);
|
||||
var chunksDir = parseResult.GetValue(chunksDirOption);
|
||||
var output = parseResult.GetValue(outputOption) ?? "text";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleImportAsync(
|
||||
services,
|
||||
input,
|
||||
lazyFetch,
|
||||
backend,
|
||||
chunksDir,
|
||||
output,
|
||||
verbose,
|
||||
ct);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildVerifyCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputArg = new Argument<string>("input")
|
||||
{
|
||||
Description = "Path to the proof bundle file to verify"
|
||||
};
|
||||
|
||||
var signerCertOption = new Option<string?>("--signer-cert")
|
||||
{
|
||||
Description = "Path to signer certificate for signature verification"
|
||||
};
|
||||
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output format: text, json"
|
||||
};
|
||||
outputOption.SetDefaultValue("text");
|
||||
outputOption.FromAmong("text", "json");
|
||||
|
||||
var command = new Command("verify", "Verify a proof bundle without importing")
|
||||
{
|
||||
inputArg,
|
||||
signerCertOption,
|
||||
outputOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputArg) ?? string.Empty;
|
||||
var signerCert = parseResult.GetValue(signerCertOption);
|
||||
var output = parseResult.GetValue(outputOption) ?? "text";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleVerifyAsync(
|
||||
services,
|
||||
input,
|
||||
signerCert,
|
||||
output,
|
||||
verbose,
|
||||
ct);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
#region Handlers
|
||||
|
||||
private static async Task<int> HandleExportAsync(
|
||||
IServiceProvider services,
|
||||
string verikey,
|
||||
string densityStr,
|
||||
int chunks,
|
||||
string output,
|
||||
bool sign,
|
||||
string? signer,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger("ProvCommands");
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogInformation("Exporting proof bundle for {VeriKey} with density {Density}",
|
||||
verikey, densityStr);
|
||||
}
|
||||
|
||||
var density = densityStr.ToLowerInvariant() switch
|
||||
{
|
||||
"lite" => ProofDensity.Lite,
|
||||
"standard" => ProofDensity.Standard,
|
||||
"strict" => ProofDensity.Strict,
|
||||
_ => ProofDensity.Standard
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var exporter = services.GetService<IMinimalProofExporter>();
|
||||
if (exporter is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Provcache services not configured.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var options = new MinimalProofExportOptions
|
||||
{
|
||||
Density = density,
|
||||
StandardDensityChunkCount = chunks,
|
||||
Sign = sign,
|
||||
SigningKeyId = signer,
|
||||
ExportedBy = Environment.MachineName
|
||||
};
|
||||
|
||||
Console.WriteLine($"Exporting proof bundle: {verikey}");
|
||||
Console.WriteLine($" Density: {density}");
|
||||
Console.WriteLine($" Output: {output}");
|
||||
|
||||
using var fileStream = File.Create(output);
|
||||
await exporter.ExportToStreamAsync(verikey, options, fileStream, cancellationToken);
|
||||
|
||||
var fileInfo = new FileInfo(output);
|
||||
Console.WriteLine($" Size: {fileInfo.Length:N0} bytes");
|
||||
Console.WriteLine("[green]Export complete.[/]");
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Export failed: {ex.Message}");
|
||||
if (verbose)
|
||||
{
|
||||
Console.Error.WriteLine(ex.ToString());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> HandleImportAsync(
|
||||
IServiceProvider services,
|
||||
string input,
|
||||
bool lazyFetch,
|
||||
string? backend,
|
||||
string? chunksDir,
|
||||
string output,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger("ProvCommands");
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: File not found: {input}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogInformation("Importing proof bundle from {Input}", input);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var exporter = services.GetService<IMinimalProofExporter>();
|
||||
if (exporter is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Provcache services not configured.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Importing proof bundle: {input}");
|
||||
|
||||
using var fileStream = File.OpenRead(input);
|
||||
var result = await exporter.ImportFromStreamAsync(fileStream, cancellationToken);
|
||||
|
||||
if (output == "json")
|
||||
{
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
|
||||
});
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" Success: {result.Success}");
|
||||
Console.WriteLine($" Chunks imported: {result.ChunksImported}");
|
||||
Console.WriteLine($" Chunks pending: {result.ChunksPending}");
|
||||
Console.WriteLine($" Merkle valid: {result.Verification.MerkleRootValid}");
|
||||
Console.WriteLine($" Digest valid: {result.Verification.DigestValid}");
|
||||
Console.WriteLine($" Chunks valid: {result.Verification.ChunksValid}");
|
||||
|
||||
if (result.Verification.SignatureValid.HasValue)
|
||||
{
|
||||
Console.WriteLine($" Signature valid: {result.Verification.SignatureValid.Value}");
|
||||
}
|
||||
|
||||
if (result.Warnings.Count > 0)
|
||||
{
|
||||
Console.WriteLine(" Warnings:");
|
||||
foreach (var warning in result.Warnings)
|
||||
{
|
||||
Console.WriteLine($" - {warning}");
|
||||
}
|
||||
}
|
||||
|
||||
if (result.ChunksPending > 0 && lazyFetch)
|
||||
{
|
||||
Console.WriteLine($"\n Lazy fetch enabled: {result.ChunksPending} chunks can be fetched on demand.");
|
||||
if (!string.IsNullOrEmpty(backend))
|
||||
{
|
||||
Console.WriteLine($" Backend: {backend}");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(chunksDir))
|
||||
{
|
||||
Console.WriteLine($" Chunks dir: {chunksDir}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.Success ? 0 : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Import failed: {ex.Message}");
|
||||
if (verbose)
|
||||
{
|
||||
Console.Error.WriteLine(ex.ToString());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> HandleVerifyAsync(
|
||||
IServiceProvider services,
|
||||
string input,
|
||||
string? signerCert,
|
||||
string output,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory?.CreateLogger("ProvCommands");
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: File not found: {input}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
logger?.LogInformation("Verifying proof bundle: {Input}", input);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var exporter = services.GetService<IMinimalProofExporter>();
|
||||
if (exporter is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Provcache services not configured.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Verifying proof bundle: {input}");
|
||||
|
||||
var jsonBytes = await File.ReadAllBytesAsync(input, cancellationToken);
|
||||
var bundle = System.Text.Json.JsonSerializer.Deserialize<MinimalProofBundle>(jsonBytes,
|
||||
new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
if (bundle is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Failed to parse bundle file.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var verification = await exporter.VerifyAsync(bundle, cancellationToken);
|
||||
|
||||
if (output == "json")
|
||||
{
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(verification, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
|
||||
});
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" Digest valid: {verification.DigestValid}");
|
||||
Console.WriteLine($" Merkle root valid: {verification.MerkleRootValid}");
|
||||
Console.WriteLine($" Chunks valid: {verification.ChunksValid}");
|
||||
|
||||
if (verification.SignatureValid.HasValue)
|
||||
{
|
||||
Console.WriteLine($" Signature valid: {verification.SignatureValid.Value}");
|
||||
}
|
||||
|
||||
if (verification.FailedChunkIndices.Count > 0)
|
||||
{
|
||||
Console.WriteLine($" Failed chunks: {string.Join(", ", verification.FailedChunkIndices)}");
|
||||
}
|
||||
|
||||
var overall = verification.DigestValid &&
|
||||
verification.MerkleRootValid &&
|
||||
verification.ChunksValid &&
|
||||
(verification.SignatureValid ?? true);
|
||||
|
||||
Console.WriteLine();
|
||||
if (overall)
|
||||
{
|
||||
Console.WriteLine("[green]Verification PASSED[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[red]Verification FAILED[/]");
|
||||
}
|
||||
}
|
||||
|
||||
var success = verification.DigestValid &&
|
||||
verification.MerkleRootValid &&
|
||||
verification.ChunksValid;
|
||||
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Verification failed: {ex.Message}");
|
||||
if (verbose)
|
||||
{
|
||||
Console.Error.WriteLine(ex.ToString());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -82,6 +82,7 @@
|
||||
<ProjectReference Include="../../ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/StellaOps.ExportCenter.Client.csproj" />
|
||||
<ProjectReference Include="../../ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.AuditPack/StellaOps.AuditPack.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Provcache/StellaOps.Provcache.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- GOST Crypto Plugins (Russia distribution) -->
|
||||
|
||||
Reference in New Issue
Block a user