namespace StellaOps.Offline.E2E.Tests; using StellaOps.Testing.AirGap; [Trait("Category", "AirGap")] [Trait("Category", "E2E")] public class OfflineE2ETests : NetworkIsolatedTestBase { [Fact] public async Task Scan_WithOfflineBundle_ProducesVerdict() { // Arrange var bundlePath = GetOfflineBundlePath(); var imageTarball = Path.Combine(bundlePath, "images", "test-image.tar"); // Skip if bundle doesn't exist (local dev) if (!Directory.Exists(bundlePath)) { // Skip - requires offline bundle return; } // Act // TODO: Implement scanner offline execution var result = await SimulateScanAsync(imageTarball, bundlePath); // Assert result.Success.Should().BeTrue(); result.Verdict.Should().NotBeNull(); AssertNoNetworkCalls(); } [Fact] public async Task Scan_ProducesSbom_WithOfflineBundle() { var bundlePath = GetOfflineBundlePath(); var imageTarball = Path.Combine(bundlePath, "images", "test-image.tar"); if (!Directory.Exists(bundlePath)) { return; } var result = await SimulateScanAsync(imageTarball, bundlePath); result.Sbom.Should().NotBeNull(); result.Sbom?.Components.Should().NotBeEmpty(); AssertNoNetworkCalls(); } [Fact] public async Task Attestation_SignAndVerify_WithOfflineBundle() { var bundlePath = GetOfflineBundlePath(); var imageTarball = Path.Combine(bundlePath, "images", "test-image.tar"); if (!Directory.Exists(bundlePath)) { return; } // Scan and generate attestation var scanResult = await SimulateScanAsync(imageTarball, bundlePath); // Sign attestation (offline with local keys) var keyPath = Path.Combine(bundlePath, "keys", "signing-key.pem"); var signResult = await SimulateSignAttestationAsync( scanResult.Sbom!, keyPath); signResult.Success.Should().BeTrue(); // Verify signature (offline with local trust roots) var trustRootPath = Path.Combine(bundlePath, "certs", "trust-root.pem"); var verifyResult = await SimulateVerifyAttestationAsync( signResult.Attestation, trustRootPath); verifyResult.Valid.Should().BeTrue(); AssertNoNetworkCalls(); } [Fact] public async Task PolicyEvaluation_WithOfflineBundle_Works() { var bundlePath = GetOfflineBundlePath(); var imageTarball = Path.Combine(bundlePath, "images", "vuln-image.tar"); if (!Directory.Exists(bundlePath)) { return; } var scanResult = await SimulateScanAsync(imageTarball, bundlePath); // Policy evaluation should work offline var policyPath = Path.Combine(bundlePath, "policies", "default.rego"); var policyResult = await SimulatePolicyEvaluationAsync( scanResult.Verdict, policyPath); policyResult.Should().NotBeNull(); policyResult?.Decision.Should().BeOneOf("allow", "deny", "warn"); AssertNoNetworkCalls(); } [Fact] public async Task VexApplication_WithOfflineBundle_Works() { var bundlePath = GetOfflineBundlePath(); var imageTarball = Path.Combine(bundlePath, "images", "vuln-with-vex.tar"); if (!Directory.Exists(bundlePath)) { return; } var scanResult = await SimulateScanAsync(imageTarball, bundlePath); // VEX should be applied from offline bundle var vexApplied = scanResult.Verdict?.VexStatements?.Any() ?? false; vexApplied.Should().BeTrue("VEX from offline bundle should be applied"); AssertNoNetworkCalls(); } // Simulation methods for testing infrastructure private static async Task SimulateScanAsync(string imagePath, string bundlePath) { await Task.CompletedTask; return new ScanResult { Success = true, Verdict = new Verdict { VexStatements = [] }, Sbom = new Sbom { Components = ["test-component"] } }; } private static async Task SimulateSignAttestationAsync(Sbom sbom, string keyPath) { await Task.CompletedTask; return new SignResult { Success = true, Attestation = "mock-attestation" }; } private static async Task SimulateVerifyAttestationAsync(string attestation, string trustRoot) { await Task.CompletedTask; return new VerifyResult { Valid = true }; } private static async Task SimulatePolicyEvaluationAsync(Verdict? verdict, string policyPath) { await Task.CompletedTask; return new PolicyResult { Decision = "allow" }; } } // Mock types for testing public record ScanResult { public bool Success { get; init; } public Verdict? Verdict { get; init; } public Sbom? Sbom { get; init; } } public record Verdict { public IReadOnlyList? VexStatements { get; init; } } public record Sbom { public IReadOnlyList Components { get; init; } = []; } public record SignResult { public bool Success { get; init; } public string? Attestation { get; init; } } public record VerifyResult { public bool Valid { get; init; } } public record PolicyResult { public string? Decision { get; init; } }