// ----------------------------------------------------------------------------- // BackportProvenanceE2ETests.cs // Sprint: SPRINT_8200_0015_0001_CONCEL_backport_integration // Task: BACKPORT-8200-026 // Description: End-to-end tests for distro advisory ingest with backport provenance // ----------------------------------------------------------------------------- using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Moq; using StellaOps.Concelier.Merge.Backport; using StellaOps.Concelier.Merge.Identity; using StellaOps.Concelier.Merge.Services; using StellaOps.Concelier.Models; using StellaOps.Concelier.Storage.MergeEvents; using Xunit; using StellaOps.TestKit; namespace StellaOps.Concelier.Merge.Tests; /// /// End-to-end tests for ingesting distro advisories with backport information /// and verifying provenance scope is correctly created. /// /// /// Task 26 (BACKPORT-8200-026) from SPRINT_8200_0015_0001: /// End-to-end test: ingest distro advisory with backport, verify provenance /// public sealed class BackportProvenanceE2ETests { #region Test Infrastructure private readonly Mock _provenanceStoreMock; private readonly Mock _evidenceResolverMock; private readonly Mock _proofGeneratorMock; private readonly Mock _mergeEventStoreMock; private readonly ProvenanceScopeService _provenanceService; private readonly BackportEvidenceResolver _backportResolver; private readonly MergeEventWriter _mergeEventWriter; public BackportProvenanceE2ETests() { _provenanceStoreMock = new Mock(); _evidenceResolverMock = new Mock(); _proofGeneratorMock = new Mock(); _mergeEventStoreMock = new Mock(); _provenanceService = new ProvenanceScopeService( _provenanceStoreMock.Object, NullLogger.Instance, _evidenceResolverMock.Object); _backportResolver = new BackportEvidenceResolver( _proofGeneratorMock.Object, NullLogger.Instance); var hashCalculator = new CanonicalHashCalculator(); _mergeEventWriter = new MergeEventWriter( _mergeEventStoreMock.Object, hashCalculator, TimeProvider.System, NullLogger.Instance); } #endregion #region E2E: Debian Backport Advisory Flow [Trait("Category", TestCategories.Unit)] [Fact] public async Task E2E_IngestDebianAdvisoryWithBackport_CreatesProvenanceScope() { // Arrange: Simulate Debian security advisory for CVE-2024-1234 var canonicalId = Guid.NewGuid(); var cveId = "CVE-2024-1234"; var packagePurl = "pkg:deb/debian/openssl@1.1.1n-0+deb11u5"; var fixedVersion = "1.1.1n-0+deb11u6"; var patchCommit = "abc123def456abc123def456abc123def456abcd"; // Simulate proof generation returning evidence with ChangelogMention tier // Note: ChangelogMention tier extracts PatchId, DistroAdvisory tier does not var proofResult = CreateMockProofResult(cveId, packagePurl, patchCommit, BackportEvidenceTier.ChangelogMention, 0.95); _proofGeneratorMock .Setup(x => x.GenerateProofAsync(cveId, packagePurl, It.IsAny())) .ReturnsAsync(proofResult); // Set up provenance store _provenanceStoreMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny(), It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); var createdScopeId = Guid.NewGuid(); ProvenanceScope? capturedScope = null; _provenanceStoreMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .Callback((scope, _) => capturedScope = scope) .ReturnsAsync(createdScopeId); // Act: Step 1 - Resolve backport evidence var evidence = await _backportResolver.ResolveAsync(cveId, packagePurl); // Act: Step 2 - Create provenance scope from evidence var scopeRequest = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = cveId, PackagePurl = packagePurl, Source = "debian", FixedVersion = fixedVersion, PatchLineage = patchCommit, ResolveEvidence = false // Evidence already resolved }; var result = await _provenanceService.CreateOrUpdateAsync(scopeRequest); // Assert: Verify the flow completed successfully evidence.Should().NotBeNull(); evidence!.Tier.Should().Be(BackportEvidenceTier.ChangelogMention); evidence.Confidence.Should().Be(0.95); evidence.PatchId.Should().Be(patchCommit); result.Success.Should().BeTrue(); result.WasCreated.Should().BeTrue(); result.ProvenanceScopeId.Should().Be(createdScopeId); // Verify provenance scope was created with correct data capturedScope.Should().NotBeNull(); capturedScope!.CanonicalId.Should().Be(canonicalId); capturedScope.DistroRelease.Should().Contain("debian"); capturedScope.BackportSemver.Should().Be(fixedVersion); capturedScope.PatchId.Should().Be(patchCommit); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task E2E_IngestRhelAdvisoryWithBackport_CreatesProvenanceScopeWithDistroOrigin() { // Arrange: Simulate RHEL security advisory with distro-specific patch var canonicalId = Guid.NewGuid(); var cveId = "CVE-2024-5678"; var packagePurl = "pkg:rpm/redhat/nginx@1.20.1-14.el9"; var fixedVersion = "1.20.1-14.el9_2.1"; var rhelPatchId = "rhel-specific-patch-001"; // Simulate proof generation returning distro-specific evidence var proofResult = CreateMockProofResult(cveId, packagePurl, rhelPatchId, BackportEvidenceTier.ChangelogMention, 0.85); _proofGeneratorMock .Setup(x => x.GenerateProofAsync(cveId, packagePurl, It.IsAny())) .ReturnsAsync(proofResult); _provenanceStoreMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny(), It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); ProvenanceScope? capturedScope = null; _provenanceStoreMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .Callback((scope, _) => capturedScope = scope) .ReturnsAsync(Guid.NewGuid()); // Act: Resolve evidence and create provenance scope var evidence = await _backportResolver.ResolveAsync(cveId, packagePurl); var scopeRequest = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = cveId, PackagePurl = packagePurl, Source = "redhat", FixedVersion = fixedVersion, PatchLineage = rhelPatchId }; var result = await _provenanceService.CreateOrUpdateAsync(scopeRequest); // Assert evidence.Should().NotBeNull(); evidence!.Tier.Should().Be(BackportEvidenceTier.ChangelogMention); evidence.DistroRelease.Should().Contain("redhat"); result.Success.Should().BeTrue(); capturedScope.Should().NotBeNull(); capturedScope!.DistroRelease.Should().Contain("redhat"); capturedScope.PatchId.Should().Be(rhelPatchId); } #endregion #region E2E: Multiple Distro Backports for Same CVE [Trait("Category", TestCategories.Unit)] [Fact] public async Task E2E_SameCveMultipleDistros_CreatesSeparateProvenanceScopes() { // Arrange: Same CVE with Debian and Ubuntu backports var canonicalId = Guid.NewGuid(); var cveId = "CVE-2024-MULTI"; var distros = new[] { ("pkg:deb/debian/curl@7.64.0-4+deb11u1", "debian", "7.64.0-4+deb11u2", "debian:bullseye"), ("pkg:deb/ubuntu/curl@7.81.0-1ubuntu1.14~22.04", "ubuntu", "7.81.0-1ubuntu1.15~22.04", "ubuntu:22.04") }; var capturedScopes = new List(); foreach (var (purl, source, fixedVersion, expectedDistro) in distros) { _provenanceStoreMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, expectedDistro, It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); } _provenanceStoreMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .Callback((scope, _) => capturedScopes.Add(scope)) .ReturnsAsync(Guid.NewGuid); // Act: Create provenance scopes for each distro foreach (var (purl, source, fixedVersion, _) in distros) { var request = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = cveId, PackagePurl = purl, Source = source, FixedVersion = fixedVersion }; await _provenanceService.CreateOrUpdateAsync(request); } // Assert: Two separate provenance scopes created capturedScopes.Should().HaveCount(2); capturedScopes.Should().Contain(s => s.DistroRelease.Contains("debian")); capturedScopes.Should().Contain(s => s.DistroRelease.Contains("ubuntu")); capturedScopes.Select(s => s.CanonicalId).Should().AllBeEquivalentTo(canonicalId); } #endregion #region E2E: Merge Event with Backport Evidence [Trait("Category", TestCategories.Unit)] [Fact] public async Task E2E_MergeWithBackportEvidence_RecordsInAuditLog() { // Arrange var advisoryKey = "CVE-2024-MERGE-TEST"; var before = CreateMockAdvisory(advisoryKey, "Initial version"); var after = CreateMockAdvisory(advisoryKey, "Merged version"); var backportEvidence = new List { new() { CveId = advisoryKey, PackagePurl = "pkg:deb/debian/test@1.0", DistroRelease = "debian:bookworm", Tier = BackportEvidenceTier.DistroAdvisory, Confidence = 0.95, PatchId = "upstream-commit-abc123", PatchOrigin = PatchOrigin.Upstream, EvidenceDate = DateTimeOffset.UtcNow } }; MergeEventRecord? capturedRecord = null; _mergeEventStoreMock .Setup(x => x.AppendAsync(It.IsAny(), It.IsAny())) .Callback((record, _) => capturedRecord = record) .Returns(Task.CompletedTask); // Act await _mergeEventWriter.AppendAsync( advisoryKey, before, after, inputDocumentIds: Array.Empty(), fieldDecisions: null, backportEvidence: backportEvidence, CancellationToken.None); // Assert capturedRecord.Should().NotBeNull(); capturedRecord!.AdvisoryKey.Should().Be(advisoryKey); capturedRecord.BackportEvidence.Should().NotBeNull(); capturedRecord.BackportEvidence.Should().HaveCount(1); var auditEvidence = capturedRecord.BackportEvidence![0]; auditEvidence.CveId.Should().Be(advisoryKey); auditEvidence.DistroRelease.Should().Be("debian:bookworm"); auditEvidence.EvidenceTier.Should().Be("DistroAdvisory"); auditEvidence.Confidence.Should().Be(0.95); auditEvidence.PatchOrigin.Should().Be("Upstream"); } #endregion #region E2E: Evidence Tier Upgrade [Trait("Category", TestCategories.Unit)] [Fact] public async Task E2E_EvidenceUpgrade_UpdatesProvenanceScope() { // Arrange: Start with low-tier evidence, then upgrade var canonicalId = Guid.NewGuid(); var distroRelease = "debian:bookworm"; // Initial low-tier evidence (BinaryFingerprint) var existingScope = new ProvenanceScope { Id = Guid.NewGuid(), CanonicalId = canonicalId, DistroRelease = distroRelease, Confidence = 0.6, // Low confidence from binary fingerprint PatchId = null, CreatedAt = DateTimeOffset.UtcNow.AddHours(-1), UpdatedAt = DateTimeOffset.UtcNow.AddHours(-1) }; _provenanceStoreMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, distroRelease, It.IsAny())) .ReturnsAsync(existingScope); ProvenanceScope? updatedScope = null; _provenanceStoreMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .Callback((scope, _) => updatedScope = scope) .ReturnsAsync(existingScope.Id); // Act: New high-tier evidence arrives (DistroAdvisory) var betterEvidence = new BackportEvidence { CveId = "CVE-2024-UPGRADE", PackagePurl = "pkg:deb/debian/test@1.0", DistroRelease = distroRelease, Tier = BackportEvidenceTier.DistroAdvisory, Confidence = 0.95, PatchId = "verified-commit-sha", BackportVersion = "1.0-fixed", PatchOrigin = PatchOrigin.Upstream, EvidenceDate = DateTimeOffset.UtcNow }; var result = await _provenanceService.UpdateFromEvidenceAsync(canonicalId, betterEvidence); // Assert result.Success.Should().BeTrue(); result.WasCreated.Should().BeFalse(); // Updated, not created updatedScope.Should().NotBeNull(); updatedScope!.Confidence.Should().Be(0.95); // Upgraded confidence updatedScope.PatchId.Should().Be("verified-commit-sha"); updatedScope.BackportSemver.Should().Be("1.0-fixed"); } #endregion #region E2E: Provenance Retrieval [Trait("Category", TestCategories.Unit)] [Fact] public async Task E2E_RetrieveProvenanceForCanonical_ReturnsAllDistroScopes() { // Arrange var canonicalId = Guid.NewGuid(); var scopes = new List { new() { Id = Guid.NewGuid(), CanonicalId = canonicalId, DistroRelease = "debian:bookworm", BackportSemver = "1.0-1+deb12u1", PatchId = "debian-patch", PatchOrigin = PatchOrigin.Upstream, Confidence = 0.95, CreatedAt = DateTimeOffset.UtcNow.AddDays(-1), UpdatedAt = DateTimeOffset.UtcNow.AddDays(-1) }, new() { Id = Guid.NewGuid(), CanonicalId = canonicalId, DistroRelease = "ubuntu:22.04", BackportSemver = "1.0-1ubuntu0.22.04.1", PatchId = "ubuntu-patch", PatchOrigin = PatchOrigin.Distro, Confidence = 0.90, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }, new() { Id = Guid.NewGuid(), CanonicalId = canonicalId, DistroRelease = "redhat:9", BackportSemver = "1.0-1.el9", PatchId = null, // No patch ID available Confidence = 0.7, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow } }; _provenanceStoreMock .Setup(x => x.GetByCanonicalIdAsync(canonicalId, It.IsAny())) .ReturnsAsync(scopes); // Act var result = await _provenanceService.GetByCanonicalIdAsync(canonicalId); // Assert result.Should().HaveCount(3); result.Should().Contain(s => s.DistroRelease == "debian:bookworm" && s.PatchOrigin == PatchOrigin.Upstream); result.Should().Contain(s => s.DistroRelease == "ubuntu:22.04" && s.PatchOrigin == PatchOrigin.Distro); result.Should().Contain(s => s.DistroRelease == "redhat:9" && s.PatchId == null); // Verify ordering by confidence result.OrderByDescending(s => s.Confidence) .First().DistroRelease.Should().Be("debian:bookworm"); } #endregion #region Helper Methods private static ProofResult CreateMockProofResult( string cveId, string packagePurl, string patchId, BackportEvidenceTier tier, double confidence) { var evidenceType = tier switch { BackportEvidenceTier.DistroAdvisory => "DistroAdvisory", BackportEvidenceTier.ChangelogMention => "ChangelogMention", BackportEvidenceTier.PatchHeader => "PatchHeader", BackportEvidenceTier.BinaryFingerprint => "BinaryFingerprint", _ => "Unknown" }; return new ProofResult { ProofId = Guid.NewGuid().ToString(), SubjectId = $"{cveId}:{packagePurl}", Confidence = confidence, CreatedAt = DateTimeOffset.UtcNow, Evidences = [ new ProofEvidenceItem { EvidenceId = Guid.NewGuid().ToString(), Type = evidenceType, Source = "test", Timestamp = DateTimeOffset.UtcNow, Data = new Dictionary { ["commit_sha"] = patchId } } ] }; } private static Advisory CreateMockAdvisory(string advisoryKey, string title) { return new Advisory( advisoryKey, title, summary: "Test advisory", language: "en", published: DateTimeOffset.UtcNow.AddDays(-1), modified: DateTimeOffset.UtcNow, severity: "high", exploitKnown: false, aliases: null, credits: null, references: null, affectedPackages: null, cvssMetrics: null, provenance: null, description: "Test description", cwes: null, canonicalMetricId: null, mergeHash: null); } #endregion }