using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Moq; using StellaOps.Feedser.BinaryAnalysis; using StellaOps.Feedser.BinaryAnalysis.Models; using StellaOps.Scanner.PatchVerification.Models; using StellaOps.Scanner.PatchVerification.Services; using Xunit; namespace StellaOps.Scanner.PatchVerification.Tests; [Trait("Category", "Unit")] public sealed class PatchVerificationOrchestratorTests { private readonly Mock _mockFingerprinter; private readonly InMemoryPatchSignatureStore _signatureStore; private readonly PatchVerificationOrchestrator _orchestrator; public PatchVerificationOrchestratorTests() { _mockFingerprinter = new Mock(); _mockFingerprinter.Setup(f => f.Method).Returns(FingerprintMethod.SectionHash); _signatureStore = new InMemoryPatchSignatureStore(); _orchestrator = new PatchVerificationOrchestrator( [_mockFingerprinter.Object], _signatureStore, TimeProvider.System, NullLogger.Instance); } [Fact] public async Task VerifyAsync_NoPatchData_ReturnsNoPatchDataStatus() { // Arrange var context = CreateContext(["CVE-2024-001"]); // Act var result = await _orchestrator.VerifyAsync(context); // Assert result.NoPatchDataCves.Should().Contain("CVE-2024-001"); result.PatchedCves.Should().BeEmpty(); } [Fact] public async Task VerifyAsync_WithPatchData_MatchFound_ReturnsVerified() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/libtest.so"); _mockFingerprinter .Setup(f => f.MatchAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new FingerprintMatchResult { IsMatch = true, Similarity = 0.95, Confidence = 0.90, Method = FingerprintMethod.SectionHash }); var context = CreateContext( ["CVE-2024-001"], new Dictionary { ["/lib/libtest.so"] = "/tmp/extracted/lib/libtest.so" }); // Act var result = await _orchestrator.VerifyAsync(context); // Assert result.PatchedCves.Should().Contain("CVE-2024-001"); result.Evidence.Should().HaveCount(1); result.Evidence[0].Status.Should().Be(PatchVerificationStatus.Verified); } [Fact] public async Task VerifyAsync_WithPatchData_NoMatch_ReturnsNotPatched() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/libtest.so"); _mockFingerprinter .Setup(f => f.MatchAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new FingerprintMatchResult { IsMatch = false, Similarity = 0.20, Confidence = 0.90, Method = FingerprintMethod.SectionHash }); var context = CreateContext( ["CVE-2024-001"], new Dictionary { ["/lib/libtest.so"] = "/tmp/extracted/lib/libtest.so" }); // Act var result = await _orchestrator.VerifyAsync(context); // Assert result.UnpatchedCves.Should().Contain("CVE-2024-001"); result.Evidence[0].Status.Should().Be(PatchVerificationStatus.NotPatched); } [Fact] public async Task VerifyAsync_LowConfidence_ReturnsInconclusive() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/libtest.so"); _mockFingerprinter .Setup(f => f.MatchAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new FingerprintMatchResult { IsMatch = true, Similarity = 0.60, // Below threshold Confidence = 0.50, // Below threshold Method = FingerprintMethod.SectionHash }); var context = CreateContext( ["CVE-2024-001"], new Dictionary { ["/lib/libtest.so"] = "/tmp/extracted/lib/libtest.so" }); // Act var result = await _orchestrator.VerifyAsync(context); // Assert result.InconclusiveCves.Should().Contain("CVE-2024-001"); } [Fact] public async Task VerifyAsync_MultipleCves_ProcessesAll() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/a.so"); await SetupPatchSignature("CVE-2024-002", "/lib/b.so"); _mockFingerprinter .Setup(f => f.MatchAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new FingerprintMatchResult { IsMatch = true, Similarity = 0.95, Confidence = 0.90, Method = FingerprintMethod.SectionHash }); var context = CreateContext( ["CVE-2024-001", "CVE-2024-002", "CVE-2024-003"], new Dictionary { ["/lib/a.so"] = "/tmp/a.so", ["/lib/b.so"] = "/tmp/b.so" }); // Act var result = await _orchestrator.VerifyAsync(context); // Assert result.PatchedCves.Should().HaveCount(2); result.NoPatchDataCves.Should().Contain("CVE-2024-003"); result.TotalCvesProcessed.Should().Be(3); } [Fact] public async Task VerifyAsync_ContinueOnError_DoesNotAbort() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/a.so"); await SetupPatchSignature("CVE-2024-002", "/lib/b.so"); var callCount = 0; _mockFingerprinter .Setup(f => f.MatchAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => { callCount++; if (callCount == 1) { throw new InvalidOperationException("Simulated error"); } return new FingerprintMatchResult { IsMatch = true, Similarity = 0.95, Confidence = 0.90, Method = FingerprintMethod.SectionHash }; }); var context = CreateContext( ["CVE-2024-001", "CVE-2024-002"], new Dictionary { ["/lib/a.so"] = "/tmp/a.so", ["/lib/b.so"] = "/tmp/b.so" }, new PatchVerificationOptions { ContinueOnError = true }); // Act var result = await _orchestrator.VerifyAsync(context); // Assert result.InconclusiveCves.Should().Contain("CVE-2024-001"); result.PatchedCves.Should().Contain("CVE-2024-002"); } [Fact] public async Task HasPatchDataAsync_ReturnsCorrectValue() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/test.so"); // Act & Assert (await _orchestrator.HasPatchDataAsync("CVE-2024-001")).Should().BeTrue(); (await _orchestrator.HasPatchDataAsync("CVE-2024-999")).Should().BeFalse(); } [Fact] public async Task GetCvesWithPatchDataAsync_FiltersCorrectly() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/a.so"); await SetupPatchSignature("CVE-2024-003", "/lib/c.so"); // Act var result = await _orchestrator.GetCvesWithPatchDataAsync( ["CVE-2024-001", "CVE-2024-002", "CVE-2024-003"]); // Assert result.Should().HaveCount(2); result.Should().Contain("CVE-2024-001"); result.Should().Contain("CVE-2024-003"); } [Fact] public async Task VerifySingleAsync_NoPatchData_ReturnsNoPatchDataEvidence() { // Act var evidence = await _orchestrator.VerifySingleAsync( "CVE-2024-999", "/lib/test.so", "pkg:rpm/test@1.0.0"); // Assert evidence.Status.Should().Be(PatchVerificationStatus.NoPatchData); evidence.CveId.Should().Be("CVE-2024-999"); } [Fact] public async Task VerifySingleAsync_WithPatchData_ReturnsVerificationResult() { // Arrange await SetupPatchSignature("CVE-2024-001", "/lib/libtest.so"); _mockFingerprinter .Setup(f => f.MatchAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new FingerprintMatchResult { IsMatch = true, Similarity = 0.95, Confidence = 0.90, Method = FingerprintMethod.SectionHash }); // Act var evidence = await _orchestrator.VerifySingleAsync( "CVE-2024-001", "/tmp/lib/libtest.so", "pkg:rpm/test@1.0.0"); // Assert evidence.Status.Should().Be(PatchVerificationStatus.Verified); evidence.Similarity.Should().Be(0.95); evidence.Confidence.Should().Be(0.90); } private async Task SetupPatchSignature(string cveId, string binaryPath) { await _signatureStore.StoreAsync(new PatchSignatureEntry { EntryId = Guid.NewGuid().ToString("N"), CveId = cveId, Purl = "pkg:rpm/test@1.0.0", BinaryPath = binaryPath, PatchedFingerprint = new BinaryFingerprint { FingerprintId = $"fp:test:{Guid.NewGuid():N}", CveId = cveId, Method = FingerprintMethod.SectionHash, FingerprintValue = "abc123", TargetBinary = binaryPath, Metadata = new FingerprintMetadata { Architecture = "x86_64", Format = "ELF", HasDebugSymbols = true }, ExtractedAt = DateTimeOffset.UtcNow, ExtractorVersion = "1.0.0" }, IssuerId = "test-vendor", CreatedAt = DateTimeOffset.UtcNow }); } private static PatchVerificationContext CreateContext( IEnumerable cveIds, Dictionary? binaryPaths = null, PatchVerificationOptions? options = null) { return new PatchVerificationContext { ScanId = "test-scan-001", TenantId = "test-tenant", ImageDigest = "sha256:abc123", ArtifactPurl = "pkg:oci/test@sha256:abc123", CveIds = cveIds.ToList(), BinaryPaths = binaryPaths ?? new Dictionary(), Options = options ?? new PatchVerificationOptions() }; } }