342 lines
11 KiB
C#
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()
|
|
};
|
|
}
|
|
}
|