Files
git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/BackportProvenanceE2ETests.cs

495 lines
19 KiB
C#

// -----------------------------------------------------------------------------
// 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;
/// <summary>
/// End-to-end tests for ingesting distro advisories with backport information
/// and verifying provenance scope is correctly created.
/// </summary>
/// <remarks>
/// Task 26 (BACKPORT-8200-026) from SPRINT_8200_0015_0001:
/// End-to-end test: ingest distro advisory with backport, verify provenance
/// </remarks>
public sealed class BackportProvenanceE2ETests
{
#region Test Infrastructure
private readonly Mock<IProvenanceScopeStore> _provenanceStoreMock;
private readonly Mock<IBackportEvidenceResolver> _evidenceResolverMock;
private readonly Mock<IProofGenerator> _proofGeneratorMock;
private readonly Mock<IMergeEventStore> _mergeEventStoreMock;
private readonly ProvenanceScopeService _provenanceService;
private readonly BackportEvidenceResolver _backportResolver;
private readonly MergeEventWriter _mergeEventWriter;
public BackportProvenanceE2ETests()
{
_provenanceStoreMock = new Mock<IProvenanceScopeStore>();
_evidenceResolverMock = new Mock<IBackportEvidenceResolver>();
_proofGeneratorMock = new Mock<IProofGenerator>();
_mergeEventStoreMock = new Mock<IMergeEventStore>();
_provenanceService = new ProvenanceScopeService(
_provenanceStoreMock.Object,
NullLogger<ProvenanceScopeService>.Instance,
_evidenceResolverMock.Object);
_backportResolver = new BackportEvidenceResolver(
_proofGeneratorMock.Object,
NullLogger<BackportEvidenceResolver>.Instance);
var hashCalculator = new CanonicalHashCalculator();
_mergeEventWriter = new MergeEventWriter(
_mergeEventStoreMock.Object,
hashCalculator,
TimeProvider.System,
NullLogger<MergeEventWriter>.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<CancellationToken>()))
.ReturnsAsync(proofResult);
// Set up provenance store
_provenanceStoreMock
.Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProvenanceScope?)null);
var createdScopeId = Guid.NewGuid();
ProvenanceScope? capturedScope = null;
_provenanceStoreMock
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
.Callback<ProvenanceScope, CancellationToken>((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<CancellationToken>()))
.ReturnsAsync(proofResult);
_provenanceStoreMock
.Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProvenanceScope?)null);
ProvenanceScope? capturedScope = null;
_provenanceStoreMock
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
.Callback<ProvenanceScope, CancellationToken>((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<ProvenanceScope>();
foreach (var (purl, source, fixedVersion, expectedDistro) in distros)
{
_provenanceStoreMock
.Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, expectedDistro, It.IsAny<CancellationToken>()))
.ReturnsAsync((ProvenanceScope?)null);
}
_provenanceStoreMock
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
.Callback<ProvenanceScope, CancellationToken>((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<BackportEvidence>
{
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<MergeEventRecord>(), It.IsAny<CancellationToken>()))
.Callback<MergeEventRecord, CancellationToken>((record, _) => capturedRecord = record)
.Returns(Task.CompletedTask);
// Act
await _mergeEventWriter.AppendAsync(
advisoryKey,
before,
after,
inputDocumentIds: Array.Empty<Guid>(),
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<CancellationToken>()))
.ReturnsAsync(existingScope);
ProvenanceScope? updatedScope = null;
_provenanceStoreMock
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
.Callback<ProvenanceScope, CancellationToken>((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<ProvenanceScope>
{
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<CancellationToken>()))
.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<string, string>
{
["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
}