Implementation of two completed sprints: Sprint 1: Astra Linux Connector (SPRINT_20251229_005_CONCEL_astra_connector) - Research complete: OVAL XML format identified - Connector foundation implemented (IFeedConnector interface) - Configuration options with validation (AstraOptions.cs) - Trust vectors for FSTEC-certified source (AstraTrustDefaults.cs) - Comprehensive documentation (README.md, IMPLEMENTATION_NOTES.md) - Unit tests: 8 passing, 6 pending OVAL parser implementation - Build: 0 warnings, 0 errors - Files: 9 files (~800 lines) Sprint 2: E2E CLI Verify Bundle (SPRINT_20251229_004_E2E_replayable_verdict) - CLI verify bundle command implemented (CommandHandlers.VerifyBundle.cs) - Hash validation for SBOM, feeds, VEX, policy inputs - Bundle manifest loading (ReplayManifest v2 format) - JSON and table output formats with Spectre.Console - Exit codes: 0 (pass), 7 (file not found), 8 (validation failed), 9 (not implemented) - Tests: 6 passing - Files: 4 files (~750 lines) Total: ~1950 lines across 12 files, all tests passing, clean builds. Sprints archived to docs/implplan/archived/2025-12-29-completed-sprints/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
201 lines
6.7 KiB
C#
201 lines
6.7 KiB
C#
using System.CommandLine;
|
|
using StellaOps.Cli.Extensions;
|
|
|
|
namespace StellaOps.Cli.Commands;
|
|
|
|
internal static class VerifyCommandGroup
|
|
{
|
|
internal static Command BuildVerifyCommand(
|
|
IServiceProvider services,
|
|
Option<bool> 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<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var evidenceDirOption = new Option<string>("--evidence-dir")
|
|
{
|
|
Description = "Path to offline evidence directory (contains keys/, policy/, sboms/, attestations/, tlog/).",
|
|
Required = true
|
|
};
|
|
|
|
var artifactOption = new Option<string>("--artifact")
|
|
{
|
|
Description = "Artifact digest to verify (sha256:<hex>).",
|
|
Required = true
|
|
};
|
|
|
|
var policyOption = new Option<string>("--policy")
|
|
{
|
|
Description = "Policy file path (YAML or JSON). If relative, resolves under evidence-dir.",
|
|
Required = true
|
|
};
|
|
|
|
var outputDirOption = new Option<string?>("--output-dir")
|
|
{
|
|
Description = "Directory to write deterministic reconciliation outputs."
|
|
};
|
|
|
|
var outputOption = new Option<string?>("--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<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var referenceArg = new Argument<string>("reference")
|
|
{
|
|
Description = "Image reference (registry/repo@sha256:digest or registry/repo:tag)"
|
|
};
|
|
|
|
var requireOption = new Option<string[]>("--require", "-r")
|
|
{
|
|
Description = "Required attestation types: sbom, vex, decision, approval",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
requireOption.SetDefaultValue(new[] { "sbom", "vex", "decision" });
|
|
|
|
var trustPolicyOption = new Option<string?>("--trust-policy")
|
|
{
|
|
Description = "Path to trust policy file (YAML or JSON)"
|
|
};
|
|
|
|
var outputOption = new Option<string>("--output", "-o")
|
|
{
|
|
Description = "Output format: table, json, sarif"
|
|
}.SetDefaultValue("table").FromAmong("table", "json", "sarif");
|
|
|
|
var strictOption = new Option<bool>("--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<string>();
|
|
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<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var bundleOption = new Option<string>("--bundle")
|
|
{
|
|
Description = "Path to evidence bundle (directory or .tar.gz file).",
|
|
Required = true
|
|
};
|
|
|
|
var skipReplayOption = new Option<bool>("--skip-replay")
|
|
{
|
|
Description = "Skip verdict replay (only validate input hashes)."
|
|
};
|
|
|
|
var outputOption = new Option<string?>("--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;
|
|
}
|
|
}
|