// // Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later. // using System; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Xunit; namespace StellaOps.E2E.ReplayableVerdict; /// /// E2E tests for reproducible verdict generation and replay /// Sprint: SPRINT_20251229_004_005_E2E /// [Trait("Category", "E2E")] [Trait("Category", "Determinism")] public sealed class ReplayableVerdictE2ETests : IAsyncLifetime { private const string BundlePath = "../../../fixtures/e2e/bundle-0001"; private GoldenBundle? _bundle; public async Task InitializeAsync() { _bundle = await GoldenBundle.LoadAsync(BundlePath); } public Task DisposeAsync() { return Task.CompletedTask; } [Fact(Skip = "E2E-002: Requires full pipeline integration")] public async Task FullPipeline_ProducesConsistentVerdict() { // Arrange _bundle.Should().NotBeNull(); // This test requires: // - Scanner service to process SBOM // - VexLens to compute consensus // - Verdict builder to generate final verdict // Currently skipped until services are integrated // Act // var scanResult = await Scanner.ScanAsync(_bundle.ImageDigest); // var vexConsensus = await VexLens.ComputeConsensusAsync(scanResult.SbomDigest, _bundle.FeedSnapshot); // var verdict = await VerdictBuilder.BuildAsync(evidencePack, _bundle.PolicyLock); // Assert // verdict.CgsHash.Should().Be(_bundle.ExpectedVerdictHash); } [Fact(Skip = "E2E-003: Requires verdict builder service")] public async Task ReplayFromBundle_ProducesIdenticalVerdict() { // Arrange _bundle.Should().NotBeNull(); var originalVerdictHash = _bundle!.Manifest.ExpectedOutputs.VerdictHash; // Act // var replayedVerdict = await VerdictBuilder.ReplayAsync(_bundle.Manifest); // Assert // replayedVerdict.CgsHash.Should().Be(originalVerdictHash); } [Fact] public void Bundle_LoadsSuccessfully() { // Assert _bundle.Should().NotBeNull(); _bundle!.Manifest.Should().NotBeNull(); _bundle.Manifest.SchemaVersion.Should().Be("2.0"); _bundle.Manifest.BundleId.Should().Be("bundle-0001"); } [Fact] public void Bundle_HasValidInputs() { // Assert _bundle.Should().NotBeNull(); _bundle!.Manifest.Inputs.Should().NotBeNull(); _bundle.Manifest.Inputs.Sbom.Should().NotBeNull(); _bundle.Manifest.Inputs.Feeds.Should().NotBeNull(); _bundle.Manifest.Inputs.Vex.Should().NotBeNull(); _bundle.Manifest.Inputs.Policy.Should().NotBeNull(); } [Fact] public void Bundle_SbomFile_Exists() { // Arrange var sbomPath = Path.Combine(BundlePath, _bundle!.Manifest.Inputs.Sbom.Path); // Assert File.Exists(sbomPath).Should().BeTrue($"SBOM file should exist at {sbomPath}"); } [Fact] public async Task Bundle_SbomFile_IsValidJson() { // Arrange var sbomPath = Path.Combine(BundlePath, _bundle!.Manifest.Inputs.Sbom.Path); var json = await File.ReadAllTextAsync(sbomPath); // Act var doc = JsonDocument.Parse(json); // Assert doc.RootElement.TryGetProperty("bomFormat", out var bomFormat).Should().BeTrue(); bomFormat.GetString().Should().Be("CycloneDX"); doc.RootElement.TryGetProperty("components", out var components).Should().BeTrue(); components.GetArrayLength().Should().BeGreaterThan(0); } [Fact(Skip = "E2E-004: Requires verdict builder with delta support")] public async Task DeltaVerdict_ShowsExpectedChanges() { // This test requires two bundles (v1 and v2) to compare // var bundleV1 = await GoldenBundle.LoadAsync("../../../fixtures/e2e/bundle-0001"); // var bundleV2 = await GoldenBundle.LoadAsync("../../../fixtures/e2e/bundle-0002"); // var verdictV1 = await VerdictBuilder.BuildAsync(bundleV1.ToEvidencePack(), bundleV1.PolicyLock); // var verdictV2 = await VerdictBuilder.BuildAsync(bundleV2.ToEvidencePack(), bundleV2.PolicyLock); // var delta = await VerdictBuilder.DiffAsync(verdictV1.CgsHash, verdictV2.CgsHash); // delta.AddedVulns.Should().Contain("CVE-2024-NEW"); // delta.RemovedVulns.Should().Contain("CVE-2024-FIXED"); } [Fact(Skip = "E2E-005: Requires DSSE signing service")] public async Task Verdict_HasValidDsseSignature() { // var verdict = await VerdictBuilder.BuildAsync(_bundle.ToEvidencePack(), _bundle.PolicyLock); // var dsseEnvelope = await Signer.SignAsync(verdict); // var verificationResult = await Signer.VerifyAsync(dsseEnvelope, _bundle.PublicKey); // verificationResult.IsValid.Should().BeTrue(); // verificationResult.SignedBy.Should().Be("test-keypair"); } [Fact(Skip = "E2E-006: Requires network isolation support")] public async Task OfflineReplay_ProducesIdenticalVerdict() { // This test should run with network disabled // AssertNoNetworkCalls(); // var verdict = await VerdictBuilder.ReplayAsync(_bundle.Manifest); // verdict.CgsHash.Should().Be(_bundle.ExpectedVerdictHash); } [Fact(Skip = "E2E-008: Requires cross-platform CI")] public async Task CrossPlatformReplay_ProducesIdenticalHash() { // This test runs on multiple CI runners (Ubuntu, Alpine, Debian) // var platform = Environment.OSVersion; // var verdict = await VerdictBuilder.BuildAsync(_bundle.ToEvidencePack(), _bundle.PolicyLock); // verdict.CgsHash.Should().Be(_bundle.ExpectedVerdictHash, // $"verdict on {platform} should match golden hash"); } } /// /// Golden bundle loader /// internal sealed class GoldenBundle { public required BundleManifest Manifest { get; init; } public required string BasePath { get; init; } public static async Task LoadAsync(string path) { var manifestPath = Path.Combine(path, "manifest.json"); if (!File.Exists(manifestPath)) { throw new FileNotFoundException($"Bundle manifest not found: {manifestPath}"); } var json = await File.ReadAllTextAsync(manifestPath); var manifest = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("Failed to deserialize manifest"); return new GoldenBundle { Manifest = manifest, BasePath = path }; } } /// /// Bundle manifest schema (ReplayManifest v2) /// internal sealed record BundleManifest { public required string SchemaVersion { get; init; } public required string BundleId { get; init; } public string? Description { get; init; } public required string CreatedAt { get; init; } public required ScanInfo Scan { get; init; } public required BundleInputs Inputs { get; init; } public required BundleOutputs ExpectedOutputs { get; init; } public string? Notes { get; init; } } internal sealed record ScanInfo { public required string Id { get; init; } public required string ImageDigest { get; init; } public required string PolicyDigest { get; init; } public required string ScorePolicyDigest { get; init; } public required string FeedSnapshotDigest { get; init; } public required string Toolchain { get; init; } public required string AnalyzerSetDigest { get; init; } } internal sealed record BundleInputs { public required InputFile Sbom { get; init; } public required InputFile Feeds { get; init; } public required InputFile Vex { get; init; } public required InputFile Policy { get; init; } } internal sealed record InputFile { public required string Path { get; init; } public required string Sha256 { get; init; } } internal sealed record BundleOutputs { public required InputFile Verdict { get; init; } public required string VerdictHash { get; init; } }