// ----------------------------------------------------------------------------- // ProofChainIntegrationTests.cs // Sprint: SPRINT_3500_0004_0003_integration_tests_corpus // Task: T1 - Proof Chain Integration Tests // Description: End-to-end tests for complete proof chain workflow: // scan → manifest → score → proof bundle → verify // ----------------------------------------------------------------------------- using System.Net; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; using System.Text.Json; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace StellaOps.Integration.ProofChain; /// /// End-to-end integration tests for the proof chain workflow. /// Tests the complete flow: scan submission → manifest creation → score computation /// → proof bundle generation → verification. /// [Collection("ProofChainIntegration")] public class ProofChainIntegrationTests : IAsyncLifetime { private readonly ProofChainTestFixture _fixture; private HttpClient _client = null!; public ProofChainIntegrationTests(ProofChainTestFixture fixture) { _fixture = fixture; } public async Task InitializeAsync() { _client = await _fixture.CreateClientAsync(); } public Task DisposeAsync() { _client.Dispose(); return Task.CompletedTask; } #region T1-AC1: Test scan submission creates manifest [Fact] public async Task ScanSubmission_CreatesManifest_WithCorrectHashes() { // Arrange var sbomContent = CreateMinimalSbom(); var scanRequest = new { sbom = sbomContent, policyId = "default", metadata = new { source = "integration-test" } }; // Act var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); var scanResult = await response.Content.ReadFromJsonAsync(); scanResult.Should().NotBeNull(); scanResult!.ScanId.Should().NotBeEmpty(); // Verify manifest was created var manifestResponse = await _client.GetAsync($"/api/v1/scans/{scanResult.ScanId}/manifest"); manifestResponse.StatusCode.Should().Be(HttpStatusCode.OK); var manifest = await manifestResponse.Content.ReadFromJsonAsync(); manifest.Should().NotBeNull(); manifest!.SbomHash.Should().StartWith("sha256:"); manifest.ManifestHash.Should().StartWith("sha256:"); } #endregion #region T1-AC2: Test score computation produces deterministic results [Fact] public async Task ScoreComputation_IsDeterministic_WithSameInputs() { // Arrange var sbomContent = CreateSbomWithVulnerability("CVE-2024-12345"); var scanRequest = new { sbom = sbomContent, policyId = "default" }; // Act - Run scan twice with identical inputs var response1 = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest); var scan1 = await response1.Content.ReadFromJsonAsync(); var response2 = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest); var scan2 = await response2.Content.ReadFromJsonAsync(); // Assert - Both scans should produce identical manifest hashes var manifest1 = await GetManifestAsync(scan1!.ScanId); var manifest2 = await GetManifestAsync(scan2!.ScanId); manifest1.SbomHash.Should().Be(manifest2.SbomHash); manifest1.RulesHash.Should().Be(manifest2.RulesHash); manifest1.PolicyHash.Should().Be(manifest2.PolicyHash); } #endregion #region T1-AC3: Test proof bundle generation and signing [Fact] public async Task ProofBundle_IsGenerated_WithValidDsseEnvelope() { // Arrange var sbomContent = CreateMinimalSbom(); var scanRequest = new { sbom = sbomContent, policyId = "default" }; // Act var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest); var scan = await response.Content.ReadFromJsonAsync(); // Get proof bundle var proofsResponse = await _client.GetAsync($"/api/v1/scans/{scan!.ScanId}/proofs"); // Assert proofsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var proofs = await proofsResponse.Content.ReadFromJsonAsync(); proofs.Should().NotBeNull(); proofs!.Items.Should().NotBeEmpty(); var proof = proofs.Items.First(); proof.RootHash.Should().StartWith("sha256:"); proof.DsseEnvelopeValid.Should().BeTrue(); } #endregion #region T1-AC4: Test proof verification succeeds for valid bundles [Fact] public async Task ProofVerification_Succeeds_ForValidBundle() { // Arrange var sbomContent = CreateMinimalSbom(); var scanRequest = new { sbom = sbomContent, policyId = "default" }; var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest); var scan = await response.Content.ReadFromJsonAsync(); var proofsResponse = await _client.GetAsync($"/api/v1/scans/{scan!.ScanId}/proofs"); var proofs = await proofsResponse.Content.ReadFromJsonAsync(); var rootHash = proofs!.Items.First().RootHash; // Act var verifyResponse = await _client.PostAsJsonAsync( $"/api/v1/scans/{scan.ScanId}/proofs/{rootHash}/verify", new { }); // Assert verifyResponse.StatusCode.Should().Be(HttpStatusCode.OK); var verifyResult = await verifyResponse.Content.ReadFromJsonAsync(); verifyResult.Should().NotBeNull(); verifyResult!.Valid.Should().BeTrue(); verifyResult.Checks.Should().Contain(c => c.Name == "dsse_signature" && c.Passed); verifyResult.Checks.Should().Contain(c => c.Name == "merkle_root" && c.Passed); } #endregion #region T1-AC5: Test verification fails for tampered bundles [Fact] public async Task ProofVerification_Fails_ForTamperedBundle() { // Arrange var sbomContent = CreateMinimalSbom(); var scanRequest = new { sbom = sbomContent, policyId = "default" }; var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest); var scan = await response.Content.ReadFromJsonAsync(); // Get a valid proof then tamper with the hash var proofsResponse = await _client.GetAsync($"/api/v1/scans/{scan!.ScanId}/proofs"); var proofs = await proofsResponse.Content.ReadFromJsonAsync(); var originalHash = proofs!.Items.First().RootHash; var tamperedHash = "sha256:" + new string('0', 64); // Tampered hash // Act var verifyResponse = await _client.PostAsJsonAsync( $"/api/v1/scans/{scan.ScanId}/proofs/{tamperedHash}/verify", new { }); // Assert verifyResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } #endregion #region T1-AC6: Test replay produces identical scores [Fact] public async Task ScoreReplay_ProducesIdenticalScore_WithSameManifest() { // Arrange var sbomContent = CreateSbomWithVulnerability("CVE-2024-99999"); var scanRequest = new { sbom = sbomContent, policyId = "default" }; var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest); var scan = await response.Content.ReadFromJsonAsync(); var manifest = await GetManifestAsync(scan!.ScanId); var originalProofs = await GetProofsAsync(scan.ScanId); var originalRootHash = originalProofs.Items.First().RootHash; // Act - Replay the score computation var replayResponse = await _client.PostAsJsonAsync( $"/api/v1/scans/{scan.ScanId}/score/replay", new { manifestHash = manifest.ManifestHash }); // Assert replayResponse.StatusCode.Should().Be(HttpStatusCode.OK); var replayResult = await replayResponse.Content.ReadFromJsonAsync(); replayResult.Should().NotBeNull(); replayResult!.RootHash.Should().Be(originalRootHash); replayResult.Deterministic.Should().BeTrue(); } #endregion #region Helper Methods private static string CreateMinimalSbom() { return JsonSerializer.Serialize(new { bomFormat = "CycloneDX", specVersion = "1.5", version = 1, metadata = new { timestamp = DateTimeOffset.UtcNow.ToString("O"), component = new { type = "application", name = "integration-test-app", version = "1.0.0" } }, components = Array.Empty() }); } private static string CreateSbomWithVulnerability(string cveId) { return JsonSerializer.Serialize(new { bomFormat = "CycloneDX", specVersion = "1.5", version = 1, metadata = new { timestamp = DateTimeOffset.UtcNow.ToString("O"), component = new { type = "application", name = "vuln-test-app", version = "1.0.0" } }, components = new[] { new { type = "library", name = "vulnerable-package", version = "1.0.0", purl = "pkg:npm/vulnerable-package@1.0.0" } }, vulnerabilities = new[] { new { id = cveId, source = new { name = "NVD" }, ratings = new[] { new { severity = "high", score = 7.5, method = "CVSSv31" } }, affects = new[] { new { @ref = "pkg:npm/vulnerable-package@1.0.0" } } } } }); } private async Task GetManifestAsync(string scanId) { var response = await _client.GetAsync($"/api/v1/scans/{scanId}/manifest"); response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } private async Task GetProofsAsync(string scanId) { var response = await _client.GetAsync($"/api/v1/scans/{scanId}/proofs"); response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } #endregion #region DTOs private sealed record ScanResponse( string ScanId, string Status, DateTimeOffset CreatedAt); private sealed record ManifestResponse( string ManifestHash, string SbomHash, string RulesHash, string FeedHash, string PolicyHash, DateTimeOffset CreatedAt); private sealed record ProofsListResponse( IReadOnlyList Items); private sealed record ProofItem( string RootHash, string BundleUri, bool DsseEnvelopeValid, DateTimeOffset CreatedAt); private sealed record VerifyResponse( bool Valid, string RootHash, IReadOnlyList Checks); private sealed record VerifyCheck( string Name, bool Passed, string? Message); private sealed record ReplayResponse( string RootHash, double Score, bool Deterministic, DateTimeOffset ReplayedAt); #endregion } /// /// Collection definition for proof chain integration tests. /// [CollectionDefinition("ProofChainIntegration")] public class ProofChainIntegrationCollection : ICollectionFixture { }