save dev progress
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
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
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
}
|
||||
Reference in New Issue
Block a user