495 lines
19 KiB
C#
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
|
|
}
|