Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.PatchVerification.Tests/PatchVerificationOrchestratorTests.cs
2026-01-12 12:24:17 +02:00

342 lines
11 KiB
C#

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<IBinaryFingerprinter> _mockFingerprinter;
private readonly InMemoryPatchSignatureStore _signatureStore;
private readonly PatchVerificationOrchestrator _orchestrator;
public PatchVerificationOrchestratorTests()
{
_mockFingerprinter = new Mock<IBinaryFingerprinter>();
_mockFingerprinter.Setup(f => f.Method).Returns(FingerprintMethod.SectionHash);
_signatureStore = new InMemoryPatchSignatureStore();
_orchestrator = new PatchVerificationOrchestrator(
[_mockFingerprinter.Object],
_signatureStore,
TimeProvider.System,
NullLogger<PatchVerificationOrchestrator>.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<string>(),
It.IsAny<BinaryFingerprint>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new FingerprintMatchResult
{
IsMatch = true,
Similarity = 0.95,
Confidence = 0.90,
Method = FingerprintMethod.SectionHash
});
var context = CreateContext(
["CVE-2024-001"],
new Dictionary<string, string> { ["/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<string>(),
It.IsAny<BinaryFingerprint>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new FingerprintMatchResult
{
IsMatch = false,
Similarity = 0.20,
Confidence = 0.90,
Method = FingerprintMethod.SectionHash
});
var context = CreateContext(
["CVE-2024-001"],
new Dictionary<string, string> { ["/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<string>(),
It.IsAny<BinaryFingerprint>(),
It.IsAny<CancellationToken>()))
.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<string, string> { ["/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<string>(),
It.IsAny<BinaryFingerprint>(),
It.IsAny<CancellationToken>()))
.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<string, string>
{
["/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<string>(),
It.IsAny<BinaryFingerprint>(),
It.IsAny<CancellationToken>()))
.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<string, string>
{
["/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<string>(),
It.IsAny<BinaryFingerprint>(),
It.IsAny<CancellationToken>()))
.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<string> cveIds,
Dictionary<string, string>? 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<string, string>(),
Options = options ?? new PatchVerificationOptions()
};
}
}