Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,627 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ReproducibleBuildJobIntegrationTests.cs
|
||||
// Sprint: SPRINT_1227_0002_0001_LB_reproducible_builders
|
||||
// Task: T11 — Integration tests with sample packages
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.BinaryIndex.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for ReproducibleBuildJob with sample packages.
|
||||
/// Tests end-to-end flow: CVE → build → fingerprint → claim.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Category", "BinaryIndex")]
|
||||
public class ReproducibleBuildJobIntegrationTests
|
||||
{
|
||||
#region Builder Selection Tests
|
||||
|
||||
[Fact(DisplayName = "Selects correct builder for Debian distro")]
|
||||
public void SelectBuilder_Debian_ReturnsDebianBuilder()
|
||||
{
|
||||
// Arrange
|
||||
var builders = CreateMockBuilders();
|
||||
var cve = CreateTestCve("debian", "bookworm", "openssl", "3.0.7-1", "3.0.7-1+deb12u1");
|
||||
|
||||
// Act
|
||||
var selectedBuilder = builders.FirstOrDefault(b =>
|
||||
b.Distro.Equals(cve.Distro, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Assert
|
||||
selectedBuilder.Should().NotBeNull();
|
||||
selectedBuilder!.Distro.Should().Be("debian");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Selects correct builder for Alpine distro")]
|
||||
public void SelectBuilder_Alpine_ReturnsAlpineBuilder()
|
||||
{
|
||||
// Arrange
|
||||
var builders = CreateMockBuilders();
|
||||
var cve = CreateTestCve("alpine", "3.19", "openssl", "3.0.7-r0", "3.0.7-r1");
|
||||
|
||||
// Act
|
||||
var selectedBuilder = builders.FirstOrDefault(b =>
|
||||
b.Distro.Equals(cve.Distro, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Assert
|
||||
selectedBuilder.Should().NotBeNull();
|
||||
selectedBuilder!.Distro.Should().Be("alpine");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Selects correct builder for RHEL distro")]
|
||||
public void SelectBuilder_Rhel_ReturnsRhelBuilder()
|
||||
{
|
||||
// Arrange
|
||||
var builders = CreateMockBuilders();
|
||||
var cve = CreateTestCve("rhel", "9", "openssl", "3.0.7-1.el9", "3.0.7-1.el9_1");
|
||||
|
||||
// Act
|
||||
var selectedBuilder = builders.FirstOrDefault(b =>
|
||||
b.Distro.Equals(cve.Distro, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Assert
|
||||
selectedBuilder.Should().NotBeNull();
|
||||
selectedBuilder!.Distro.Should().Be("rhel");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Returns null for unsupported distro")]
|
||||
public void SelectBuilder_UnsupportedDistro_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var builders = CreateMockBuilders();
|
||||
var cve = CreateTestCve("centos", "7", "openssl", "1.0.2k", "1.0.2k-fips");
|
||||
|
||||
// Act
|
||||
var selectedBuilder = builders.FirstOrDefault(b =>
|
||||
b.Distro.Equals(cve.Distro, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Assert
|
||||
selectedBuilder.Should().BeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region OpenSSL Package Tests
|
||||
|
||||
[Fact(DisplayName = "OpenSSL CVE-2024-0001: processes vulnerable and patched versions")]
|
||||
public async Task ProcessCve_OpenSslCve_BuildsBothVersions()
|
||||
{
|
||||
// Arrange
|
||||
var mockBuilder = new Mock<IReproducibleBuilder>();
|
||||
mockBuilder.Setup(b => b.Distro).Returns("debian");
|
||||
mockBuilder.Setup(b => b.BuildAsync(It.IsAny<BuildRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BuildRequest req, CancellationToken _) => CreateSuccessfulBuildResult(req));
|
||||
|
||||
var mockDiffEngine = new Mock<IPatchDiffEngine>();
|
||||
mockDiffEngine.Setup(d => d.ComputeDiff(
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<DiffOptions>()))
|
||||
.Returns(CreateOpenSslDiffResult());
|
||||
|
||||
var job = CreateBuildJob(new[] { mockBuilder.Object }, mockDiffEngine.Object);
|
||||
var cve = CreateTestCve("debian", "bookworm", "openssl", "3.0.7-1", "3.0.7-1+deb12u1");
|
||||
cve = cve with { CveId = "CVE-2024-0001", PatchCommit = "abc123" };
|
||||
|
||||
// Act
|
||||
await job.ProcessCveAsync(cve, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
mockBuilder.Verify(b => b.BuildAsync(
|
||||
It.Is<BuildRequest>(r => r.Version == "3.0.7-1"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
mockBuilder.Verify(b => b.BuildAsync(
|
||||
It.Is<BuildRequest>(r => r.Version == "3.0.7-1+deb12u1"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "OpenSSL: extracts ssl3_get_record as modified function")]
|
||||
public async Task ProcessCve_OpenSsl_IdentifiesModifiedFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var mockBuilder = new Mock<IReproducibleBuilder>();
|
||||
mockBuilder.Setup(b => b.Distro).Returns("debian");
|
||||
mockBuilder.Setup(b => b.BuildAsync(It.IsAny<BuildRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BuildRequest req, CancellationToken _) => CreateSuccessfulBuildResult(req));
|
||||
|
||||
var diffResult = CreateOpenSslDiffResult();
|
||||
var mockDiffEngine = new Mock<IPatchDiffEngine>();
|
||||
mockDiffEngine.Setup(d => d.ComputeDiff(
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<DiffOptions>()))
|
||||
.Returns(diffResult);
|
||||
|
||||
// Assert that the diff result contains expected functions
|
||||
diffResult.Changes.Should().Contain(c => c.FunctionName == "ssl3_get_record");
|
||||
diffResult.Changes.Should().Contain(c => c.Type == ChangeType.Modified);
|
||||
diffResult.ModifiedCount.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Curl Package Tests
|
||||
|
||||
[Fact(DisplayName = "Curl CVE-2024-0002: processes vulnerable and patched versions")]
|
||||
public async Task ProcessCve_CurlCve_BuildsBothVersions()
|
||||
{
|
||||
// Arrange
|
||||
var mockBuilder = new Mock<IReproducibleBuilder>();
|
||||
mockBuilder.Setup(b => b.Distro).Returns("debian");
|
||||
mockBuilder.Setup(b => b.BuildAsync(It.IsAny<BuildRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BuildRequest req, CancellationToken _) => CreateSuccessfulBuildResult(req, "curl"));
|
||||
|
||||
var mockDiffEngine = new Mock<IPatchDiffEngine>();
|
||||
mockDiffEngine.Setup(d => d.ComputeDiff(
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<DiffOptions>()))
|
||||
.Returns(CreateCurlDiffResult());
|
||||
|
||||
var job = CreateBuildJob(new[] { mockBuilder.Object }, mockDiffEngine.Object);
|
||||
var cve = CreateTestCve("debian", "bookworm", "curl", "7.88.1-1", "7.88.1-1+deb12u1");
|
||||
cve = cve with { CveId = "CVE-2024-0002" };
|
||||
|
||||
// Act
|
||||
await job.ProcessCveAsync(cve, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
mockBuilder.Verify(b => b.BuildAsync(
|
||||
It.Is<BuildRequest>(r => r.SourcePackage == "curl"),
|
||||
It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Curl: extracts Curl_ssl_connect as modified function")]
|
||||
public void CurlDiff_IdentifiesModifiedFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var diffResult = CreateCurlDiffResult();
|
||||
|
||||
// Assert
|
||||
diffResult.Changes.Should().Contain(c => c.FunctionName == "Curl_ssl_connect");
|
||||
diffResult.Changes.Should().Contain(c => c.Type == ChangeType.Modified);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Zlib Package Tests
|
||||
|
||||
[Fact(DisplayName = "Zlib CVE-2024-0003: processes vulnerable and patched versions")]
|
||||
public async Task ProcessCve_ZlibCve_BuildsBothVersions()
|
||||
{
|
||||
// Arrange
|
||||
var mockBuilder = new Mock<IReproducibleBuilder>();
|
||||
mockBuilder.Setup(b => b.Distro).Returns("alpine");
|
||||
mockBuilder.Setup(b => b.BuildAsync(It.IsAny<BuildRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BuildRequest req, CancellationToken _) => CreateSuccessfulBuildResult(req, "zlib"));
|
||||
|
||||
var mockDiffEngine = new Mock<IPatchDiffEngine>();
|
||||
mockDiffEngine.Setup(d => d.ComputeDiff(
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<DiffOptions>()))
|
||||
.Returns(CreateZlibDiffResult());
|
||||
|
||||
var job = CreateBuildJob(new[] { mockBuilder.Object }, mockDiffEngine.Object);
|
||||
var cve = CreateTestCve("alpine", "3.19", "zlib", "1.2.13-r0", "1.2.13-r1");
|
||||
cve = cve with { CveId = "CVE-2024-0003" };
|
||||
|
||||
// Act
|
||||
await job.ProcessCveAsync(cve, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
mockBuilder.Verify(b => b.BuildAsync(
|
||||
It.Is<BuildRequest>(r => r.SourcePackage == "zlib"),
|
||||
It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Zlib: extracts inflate as modified function")]
|
||||
public void ZlibDiff_IdentifiesModifiedFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var diffResult = CreateZlibDiffResult();
|
||||
|
||||
// Assert
|
||||
diffResult.Changes.Should().Contain(c => c.FunctionName == "inflate");
|
||||
diffResult.Changes.Should().Contain(c => c.Type == ChangeType.Modified);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fingerprint Claim Tests
|
||||
|
||||
[Fact(DisplayName = "Creates fingerprint claims for modified functions")]
|
||||
public async Task ProcessCve_CreatesClaimsForModifiedFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var createdClaims = new List<FingerprintClaim>();
|
||||
var mockClaimRepo = new Mock<IFingerprintClaimRepository>();
|
||||
mockClaimRepo.Setup(r => r.CreateClaimsBatchAsync(
|
||||
It.IsAny<IEnumerable<FingerprintClaim>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<FingerprintClaim>, CancellationToken>((claims, _) =>
|
||||
createdClaims.AddRange(claims))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var mockBuilder = new Mock<IReproducibleBuilder>();
|
||||
mockBuilder.Setup(b => b.Distro).Returns("debian");
|
||||
mockBuilder.Setup(b => b.BuildAsync(It.IsAny<BuildRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BuildRequest req, CancellationToken _) => CreateSuccessfulBuildResult(req));
|
||||
|
||||
var mockDiffEngine = new Mock<IPatchDiffEngine>();
|
||||
mockDiffEngine.Setup(d => d.ComputeDiff(
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<DiffOptions>()))
|
||||
.Returns(CreateOpenSslDiffResult());
|
||||
|
||||
var job = CreateBuildJob(
|
||||
new[] { mockBuilder.Object },
|
||||
mockDiffEngine.Object,
|
||||
mockClaimRepo.Object);
|
||||
|
||||
var cve = CreateTestCve("debian", "bookworm", "openssl", "3.0.7-1", "3.0.7-1+deb12u1");
|
||||
cve = cve with { CveId = "CVE-2024-0001" };
|
||||
|
||||
// Act
|
||||
await job.ProcessCveAsync(cve, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
createdClaims.Should().NotBeEmpty();
|
||||
createdClaims.Should().Contain(c => c.CveId == "CVE-2024-0001");
|
||||
createdClaims.Should().Contain(c => c.Verdict == ClaimVerdict.Fixed);
|
||||
createdClaims.Should().Contain(c => c.Verdict == ClaimVerdict.Vulnerable);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Claim evidence contains changed function names")]
|
||||
public async Task ProcessCve_ClaimEvidenceContainsFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var createdClaims = new List<FingerprintClaim>();
|
||||
var mockClaimRepo = new Mock<IFingerprintClaimRepository>();
|
||||
mockClaimRepo.Setup(r => r.CreateClaimsBatchAsync(
|
||||
It.IsAny<IEnumerable<FingerprintClaim>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<FingerprintClaim>, CancellationToken>((claims, _) =>
|
||||
createdClaims.AddRange(claims))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var mockBuilder = new Mock<IReproducibleBuilder>();
|
||||
mockBuilder.Setup(b => b.Distro).Returns("debian");
|
||||
mockBuilder.Setup(b => b.BuildAsync(It.IsAny<BuildRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BuildRequest req, CancellationToken _) => CreateSuccessfulBuildResult(req));
|
||||
|
||||
var mockDiffEngine = new Mock<IPatchDiffEngine>();
|
||||
mockDiffEngine.Setup(d => d.ComputeDiff(
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<DiffOptions>()))
|
||||
.Returns(CreateOpenSslDiffResult());
|
||||
|
||||
var job = CreateBuildJob(
|
||||
new[] { mockBuilder.Object },
|
||||
mockDiffEngine.Object,
|
||||
mockClaimRepo.Object);
|
||||
|
||||
var cve = CreateTestCve("debian", "bookworm", "openssl", "3.0.7-1", "3.0.7-1+deb12u1");
|
||||
cve = cve with { CveId = "CVE-2024-0001" };
|
||||
|
||||
// Act
|
||||
await job.ProcessCveAsync(cve, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var fixedClaim = createdClaims.FirstOrDefault(c => c.Verdict == ClaimVerdict.Fixed);
|
||||
fixedClaim.Should().NotBeNull();
|
||||
fixedClaim!.Evidence.ChangedFunctions.Should().Contain("ssl3_get_record");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Fact(DisplayName = "Continues processing on build failure")]
|
||||
public async Task ProcessCve_BuildFailure_ContinuesWithNextCve()
|
||||
{
|
||||
// Arrange
|
||||
var mockBuilder = new Mock<IReproducibleBuilder>();
|
||||
mockBuilder.Setup(b => b.Distro).Returns("debian");
|
||||
mockBuilder.SetupSequence(b => b.BuildAsync(It.IsAny<BuildRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BuildResult { Success = false, ErrorMessage = "Build failed" })
|
||||
.ReturnsAsync(CreateSuccessfulBuildResult(new BuildRequest { SourcePackage = "curl", Version = "7.88.1-1+deb12u1", Release = "bookworm" }));
|
||||
|
||||
var advisoryMonitor = new Mock<IAdvisoryFeedMonitor>();
|
||||
advisoryMonitor.Setup(a => a.GetPendingCvesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<CveAttribution>
|
||||
{
|
||||
CreateTestCve("debian", "bookworm", "openssl", "3.0.7-1", "3.0.7-1+deb12u1")
|
||||
with { CveId = "CVE-2024-0001" },
|
||||
CreateTestCve("debian", "bookworm", "curl", "7.88.1-1", "7.88.1-1+deb12u1")
|
||||
with { CveId = "CVE-2024-0002" }
|
||||
});
|
||||
|
||||
var job = CreateBuildJob(
|
||||
new[] { mockBuilder.Object },
|
||||
advisoryMonitor: advisoryMonitor.Object);
|
||||
|
||||
// Act - should not throw
|
||||
await job.ExecuteAsync(CancellationToken.None);
|
||||
|
||||
// Assert - verify both CVEs were attempted
|
||||
mockBuilder.Verify(b => b.BuildAsync(
|
||||
It.IsAny<BuildRequest>(),
|
||||
It.IsAny<CancellationToken>()), Times.AtLeast(2));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Logs warning for unsupported distro")]
|
||||
public async Task ProcessCve_UnsupportedDistro_LogsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var mockLogger = new Mock<ILogger<ReproducibleBuildJob>>();
|
||||
var builders = CreateMockBuilders(); // debian, alpine, rhel only
|
||||
|
||||
var job = CreateBuildJob(
|
||||
builders,
|
||||
logger: mockLogger.Object);
|
||||
|
||||
var cve = CreateTestCve("centos", "7", "openssl", "1.0.2k", "1.0.2k-fips");
|
||||
|
||||
// Act
|
||||
await job.ProcessCveAsync(cve, CancellationToken.None);
|
||||
|
||||
// Assert - builder should not be called
|
||||
// No exception thrown, job completes gracefully
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static IEnumerable<IReproducibleBuilder> CreateMockBuilders()
|
||||
{
|
||||
var debianBuilder = new Mock<IReproducibleBuilder>();
|
||||
debianBuilder.Setup(b => b.Distro).Returns("debian");
|
||||
|
||||
var alpineBuilder = new Mock<IReproducibleBuilder>();
|
||||
alpineBuilder.Setup(b => b.Distro).Returns("alpine");
|
||||
|
||||
var rhelBuilder = new Mock<IReproducibleBuilder>();
|
||||
rhelBuilder.Setup(b => b.Distro).Returns("rhel");
|
||||
|
||||
return new[] { debianBuilder.Object, alpineBuilder.Object, rhelBuilder.Object };
|
||||
}
|
||||
|
||||
private static CveAttribution CreateTestCve(
|
||||
string distro,
|
||||
string release,
|
||||
string package,
|
||||
string vulnVersion,
|
||||
string fixedVersion)
|
||||
{
|
||||
return new CveAttribution
|
||||
{
|
||||
CveId = "CVE-2024-TEST",
|
||||
SourcePackage = package,
|
||||
Distro = distro,
|
||||
Release = release,
|
||||
VulnerableVersion = vulnVersion,
|
||||
FixedVersion = fixedVersion
|
||||
};
|
||||
}
|
||||
|
||||
private static BuildResult CreateSuccessfulBuildResult(BuildRequest request, string? packageOverride = null)
|
||||
{
|
||||
var package = packageOverride ?? request.SourcePackage;
|
||||
return new BuildResult
|
||||
{
|
||||
Success = true,
|
||||
Duration = TimeSpan.FromMinutes(5),
|
||||
BuildLogRef = $"builds/{package}/{request.Version}",
|
||||
Binaries = new List<BuiltBinary>
|
||||
{
|
||||
new BuiltBinary
|
||||
{
|
||||
Path = $"/output/{package}.so",
|
||||
BuildId = Guid.NewGuid().ToString("N"),
|
||||
TextSha256 = new byte[32],
|
||||
Fingerprint = new byte[64],
|
||||
Functions = CreateSampleFunctions(package)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<FunctionFingerprint> CreateSampleFunctions(string package)
|
||||
{
|
||||
return package.ToLowerInvariant() switch
|
||||
{
|
||||
"openssl" => new List<FunctionFingerprint>
|
||||
{
|
||||
CreateFunction("ssl3_get_record", 0x1000, 256),
|
||||
CreateFunction("tls1_enc", 0x2000, 512),
|
||||
CreateFunction("ssl_verify_cert_chain", 0x3000, 384)
|
||||
},
|
||||
"curl" => new List<FunctionFingerprint>
|
||||
{
|
||||
CreateFunction("Curl_ssl_connect", 0x1000, 384),
|
||||
CreateFunction("Curl_http_done", 0x2000, 256),
|
||||
CreateFunction("Curl_getformdata", 0x3000, 512)
|
||||
},
|
||||
"zlib" => new List<FunctionFingerprint>
|
||||
{
|
||||
CreateFunction("inflate", 0x1000, 1024),
|
||||
CreateFunction("deflate", 0x2000, 896),
|
||||
CreateFunction("crc32", 0x3000, 128)
|
||||
},
|
||||
_ => new List<FunctionFingerprint>
|
||||
{
|
||||
CreateFunction("main", 0x1000, 64)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FunctionFingerprint CreateFunction(string name, long offset, int size)
|
||||
{
|
||||
return new FunctionFingerprint
|
||||
{
|
||||
Name = name,
|
||||
Offset = offset,
|
||||
Size = size,
|
||||
BasicBlockHash = new byte[32],
|
||||
CfgHash = new byte[32],
|
||||
StringRefsHash = new byte[32],
|
||||
Callees = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private static FunctionDiffResult CreateOpenSslDiffResult()
|
||||
{
|
||||
return new FunctionDiffResult
|
||||
{
|
||||
TotalFunctionsVulnerable = 1500,
|
||||
TotalFunctionsPatched = 1502,
|
||||
Changes = new List<FunctionChange>
|
||||
{
|
||||
new FunctionChange
|
||||
{
|
||||
FunctionName = "ssl3_get_record",
|
||||
Type = ChangeType.Modified,
|
||||
SimilarityScore = 0.94m
|
||||
},
|
||||
new FunctionChange
|
||||
{
|
||||
FunctionName = "tls1_enc",
|
||||
Type = ChangeType.Modified,
|
||||
SimilarityScore = 0.91m
|
||||
},
|
||||
new FunctionChange
|
||||
{
|
||||
FunctionName = "ssl_check_bounds",
|
||||
Type = ChangeType.Added
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FunctionDiffResult CreateCurlDiffResult()
|
||||
{
|
||||
return new FunctionDiffResult
|
||||
{
|
||||
TotalFunctionsVulnerable = 800,
|
||||
TotalFunctionsPatched = 801,
|
||||
Changes = new List<FunctionChange>
|
||||
{
|
||||
new FunctionChange
|
||||
{
|
||||
FunctionName = "Curl_ssl_connect",
|
||||
Type = ChangeType.Modified,
|
||||
SimilarityScore = 0.88m
|
||||
},
|
||||
new FunctionChange
|
||||
{
|
||||
FunctionName = "Curl_verify_host",
|
||||
Type = ChangeType.Modified,
|
||||
SimilarityScore = 0.95m
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FunctionDiffResult CreateZlibDiffResult()
|
||||
{
|
||||
return new FunctionDiffResult
|
||||
{
|
||||
TotalFunctionsVulnerable = 200,
|
||||
TotalFunctionsPatched = 200,
|
||||
Changes = new List<FunctionChange>
|
||||
{
|
||||
new FunctionChange
|
||||
{
|
||||
FunctionName = "inflate",
|
||||
Type = ChangeType.Modified,
|
||||
SimilarityScore = 0.97m
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static IReproducibleBuildJob CreateBuildJob(
|
||||
IEnumerable<IReproducibleBuilder>? builders = null,
|
||||
IPatchDiffEngine? diffEngine = null,
|
||||
IFingerprintClaimRepository? claimRepository = null,
|
||||
IAdvisoryFeedMonitor? advisoryMonitor = null,
|
||||
ILogger<ReproducibleBuildJob>? logger = null)
|
||||
{
|
||||
var mockLogger = logger ?? Mock.Of<ILogger<ReproducibleBuildJob>>();
|
||||
var mockOptions = Options.Create(new ReproducibleBuildOptions
|
||||
{
|
||||
BuildTimeout = TimeSpan.FromMinutes(30),
|
||||
DefaultArchitecture = "amd64",
|
||||
MinFunctionSize = 16
|
||||
});
|
||||
|
||||
var mockBuilders = builders ?? CreateMockBuilders();
|
||||
|
||||
var mockDiffEngine = diffEngine;
|
||||
if (mockDiffEngine == null)
|
||||
{
|
||||
var diff = new Mock<IPatchDiffEngine>();
|
||||
diff.Setup(d => d.ComputeDiff(
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<IReadOnlyList<FunctionFingerprint>>(),
|
||||
It.IsAny<DiffOptions>()))
|
||||
.Returns(new FunctionDiffResult
|
||||
{
|
||||
Changes = new List<FunctionChange>(),
|
||||
TotalFunctionsVulnerable = 0,
|
||||
TotalFunctionsPatched = 0
|
||||
});
|
||||
mockDiffEngine = diff.Object;
|
||||
}
|
||||
|
||||
var mockFingerprintExtractor = new Mock<IFunctionFingerprintExtractor>();
|
||||
mockFingerprintExtractor.Setup(e => e.ExtractAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<ExtractionOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<FunctionFingerprint>());
|
||||
|
||||
var mockClaimRepo = claimRepository;
|
||||
if (mockClaimRepo == null)
|
||||
{
|
||||
var repo = new Mock<IFingerprintClaimRepository>();
|
||||
repo.Setup(r => r.CreateClaimsBatchAsync(
|
||||
It.IsAny<IEnumerable<FingerprintClaim>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
mockClaimRepo = repo.Object;
|
||||
}
|
||||
|
||||
var mockAdvisoryMonitor = advisoryMonitor;
|
||||
if (mockAdvisoryMonitor == null)
|
||||
{
|
||||
var monitor = new Mock<IAdvisoryFeedMonitor>();
|
||||
monitor.Setup(m => m.GetPendingCvesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<CveAttribution>());
|
||||
mockAdvisoryMonitor = monitor.Object;
|
||||
}
|
||||
|
||||
return new ReproducibleBuildJob(
|
||||
mockLogger,
|
||||
mockOptions,
|
||||
mockBuilders,
|
||||
mockFingerprintExtractor.Object,
|
||||
mockDiffEngine,
|
||||
mockClaimRepo,
|
||||
mockAdvisoryMonitor);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="Testcontainers" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.Builders\StellaOps.BinaryIndex.Builders.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
// FeatureExtractorTests.cs
|
||||
// Sprint: SPRINT_20251226_011_BINIDX_known_build_catalog
|
||||
// Task: BINCAT-17 - Unit tests for identity extraction (ELF, PE, Mach-O)
|
||||
@@ -509,7 +509,6 @@ public class BinaryIdentityDeterminismTests
|
||||
using var stream1 = new MemoryStream(content1);
|
||||
using var stream2 = new MemoryStream(content2);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var identity1 = await extractor.ExtractIdentityAsync(stream1);
|
||||
var identity2 = await extractor.ExtractIdentityAsync(stream2);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.FixIndex.Models;
|
||||
using StellaOps.BinaryIndex.FixIndex.Services;
|
||||
using Xunit;
|
||||
@@ -338,7 +339,7 @@ public class FixIndexBuilderIntegrationTests
|
||||
// Assert - Both are returned (patch with higher confidence overrides)
|
||||
// The implementation allows both but prefers patch evidence
|
||||
var cve5555 = results.Where(e => e.CveId == "CVE-2024-5555").ToList();
|
||||
cve5555.Should().HaveCountGreaterOrEqualTo(1);
|
||||
cve5555.Should().HaveCountGreaterThanOrEqualTo(1);
|
||||
cve5555.Should().Contain(e => e.Method == FixMethod.PatchHeader);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.FixIndex.Models;
|
||||
using StellaOps.BinaryIndex.FixIndex.Parsers;
|
||||
using Xunit;
|
||||
|
||||
@@ -9,17 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -28,4 +18,4 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -207,7 +207,7 @@ public class FingerprintMatcherTests
|
||||
result.Details.Should().NotBeNull();
|
||||
result.Details!.MatchingAlgorithm.Should().Be(FingerprintAlgorithm.BasicBlock);
|
||||
result.Details.CandidatesEvaluated.Should().Be(1);
|
||||
result.Details.MatchTimeMs.Should().BeGreaterOrEqualTo(0);
|
||||
result.Details.MatchTimeMs.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
private static VulnFingerprint CreateStoredFingerprint(byte[] hash, bool validated = false)
|
||||
|
||||
@@ -9,17 +9,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.Fingerprints\StellaOps.BinaryIndex.Fingerprints.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -7,6 +7,7 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.Persistence.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Persistence.Tests;
|
||||
@@ -19,7 +20,6 @@ public sealed class BinaryIdentityRepositoryTests
|
||||
{
|
||||
private readonly BinaryIndexIntegrationFixture _fixture;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public BinaryIdentityRepositoryTests(BinaryIndexIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
@@ -8,6 +8,7 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.BinaryIndex.Corpus;
|
||||
using StellaOps.BinaryIndex.Persistence.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Persistence.Tests;
|
||||
@@ -20,7 +21,6 @@ public sealed class CorpusSnapshotRepositoryTests
|
||||
{
|
||||
private readonly BinaryIndexIntegrationFixture _fixture;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public CorpusSnapshotRepositoryTests(BinaryIndexIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -24,4 +24,4 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.VexBridge\StellaOps.BinaryIndex.VexBridge.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,281 @@
|
||||
// VexBridge Integration Tests with Mock Excititor
|
||||
// Sprint: SPRINT_1227_0001_0001 (Binary VEX Generator)
|
||||
// Task: T8 - Integration test with mock Excititor
|
||||
//
|
||||
// Tests end-to-end flow from binary match to VEX observation persistence
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using StellaOps.Excititor.Core;
|
||||
using StellaOps.Excititor.Core.Observations;
|
||||
|
||||
namespace StellaOps.BinaryIndex.VexBridge.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for VexBridge with mock Excititor services.
|
||||
/// </summary>
|
||||
public class VexBridgeIntegrationTests
|
||||
{
|
||||
private readonly VexEvidenceGenerator _generator;
|
||||
private readonly VexBridgeOptions _options;
|
||||
private readonly Mock<IVexObservationStore> _mockObservationStore;
|
||||
|
||||
public VexBridgeIntegrationTests()
|
||||
{
|
||||
_options = new VexBridgeOptions
|
||||
{
|
||||
MinConfidenceThreshold = 0.70m,
|
||||
SignWithDsse = false,
|
||||
MaxBatchSize = 1000
|
||||
};
|
||||
|
||||
_mockObservationStore = new Mock<IVexObservationStore>();
|
||||
_mockObservationStore
|
||||
.Setup(x => x.AppendAsync(It.IsAny<VexObservation>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((VexObservation obs, CancellationToken _) => obs.ObservationId);
|
||||
|
||||
_generator = new VexEvidenceGenerator(
|
||||
NullLogger<VexEvidenceGenerator>.Instance,
|
||||
Options.Create(_options));
|
||||
}
|
||||
|
||||
#region End-to-End Flow Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EndToEnd_BinaryMatch_To_VexObservation_Flow()
|
||||
{
|
||||
// Arrange - Simulate complete binary vulnerability detection flow
|
||||
var match = new BinaryVulnMatch
|
||||
{
|
||||
CveId = "CVE-2024-0001",
|
||||
VulnerablePurl = "pkg:deb/debian/openssl@3.0.7",
|
||||
Method = MatchMethod.BuildIdCatalog,
|
||||
Confidence = 0.98m,
|
||||
Evidence = new MatchEvidence
|
||||
{
|
||||
BuildId = "abc123def456789",
|
||||
Similarity = 0.98m,
|
||||
MatchedFunction = "ssl3_get_record"
|
||||
}
|
||||
};
|
||||
|
||||
var identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = "abc123def456789",
|
||||
BuildId = "abc123def456789",
|
||||
FileSha256 = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
TextSha256 = "sha256:abc123def456789",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
var fixStatus = new FixStatusResult
|
||||
{
|
||||
State = FixState.Fixed,
|
||||
FixedVersion = "3.0.7-1+deb12u1",
|
||||
Method = FixMethod.SecurityFeed,
|
||||
Confidence = 0.95m,
|
||||
EvidenceId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
var context = new VexGenerationContext
|
||||
{
|
||||
TenantId = "tenant-001",
|
||||
ScanId = "scan-001",
|
||||
ProductKey = "pkg:deb/debian/openssl@3.0.7",
|
||||
DistroRelease = "debian:bookworm",
|
||||
SignWithDsse = false
|
||||
};
|
||||
|
||||
// Act - Generate VEX observation
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, fixStatus, context);
|
||||
|
||||
// Assert - Verify complete observation structure
|
||||
observation.Should().NotBeNull();
|
||||
observation.ObservationId.Should().NotBeEmpty();
|
||||
|
||||
// Verify statement
|
||||
observation.Statements.Should().HaveCount(1);
|
||||
var statement = observation.Statements[0];
|
||||
statement.Status.Should().Be(VexClaimStatus.NotAffected);
|
||||
statement.Justification.Should().Be(VexJustification.VulnerableCodeNotPresent);
|
||||
statement.FixedVersion.Should().Be("3.0.7-1+deb12u1");
|
||||
|
||||
// Verify linkset
|
||||
observation.Linkset.Aliases.Should().Contain("cve-2024-0001");
|
||||
observation.Linkset.Purls.Should().Contain("pkg:deb/debian/openssl@3.0.7");
|
||||
observation.Linkset.References.Should().NotBeEmpty();
|
||||
|
||||
// Verify content has evidence payload
|
||||
observation.Content.Should().NotBeNull();
|
||||
observation.Content.Format.Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EndToEnd_BatchProcessing_MultiplePackages()
|
||||
{
|
||||
// Arrange - Multiple packages from same scan
|
||||
var packages = new[]
|
||||
{
|
||||
("openssl", "CVE-2024-0001", FixState.Fixed),
|
||||
("curl", "CVE-2024-0002", FixState.Vulnerable),
|
||||
("zlib", "CVE-2024-0003", FixState.NotAffected)
|
||||
};
|
||||
|
||||
var matchesWithContext = packages.Select((p, i) => new BinaryMatchWithContext
|
||||
{
|
||||
Match = new BinaryVulnMatch
|
||||
{
|
||||
CveId = p.Item2,
|
||||
VulnerablePurl = $"pkg:deb/debian/{p.Item1}@1.0.0",
|
||||
Method = MatchMethod.FingerprintMatch,
|
||||
Confidence = 0.90m,
|
||||
Evidence = null
|
||||
},
|
||||
Identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = $"build-id-{i}",
|
||||
BuildId = $"build-id-{i}",
|
||||
FileSha256 = $"sha256:{i:x64}",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64"
|
||||
},
|
||||
FixStatus = new FixStatusResult
|
||||
{
|
||||
State = p.Item3,
|
||||
FixedVersion = p.Item3 == FixState.Fixed ? "1.0.1" : null,
|
||||
Method = FixMethod.SecurityFeed,
|
||||
Confidence = 0.90m
|
||||
},
|
||||
Context = new VexGenerationContext
|
||||
{
|
||||
TenantId = "tenant-001",
|
||||
ScanId = "batch-scan-001",
|
||||
ProductKey = $"pkg:deb/debian/{p.Item1}@1.0.0",
|
||||
DistroRelease = "debian:bookworm",
|
||||
SignWithDsse = false
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
// Act
|
||||
var observations = await _generator.GenerateBatchAsync(matchesWithContext);
|
||||
|
||||
// Assert
|
||||
observations.Should().HaveCount(3);
|
||||
|
||||
// Fixed -> NotAffected
|
||||
observations[0].Statements[0].Status.Should().Be(VexClaimStatus.NotAffected);
|
||||
|
||||
// Vulnerable -> Affected
|
||||
observations[1].Statements[0].Status.Should().Be(VexClaimStatus.Affected);
|
||||
|
||||
// NotAffected -> NotAffected
|
||||
observations[2].Statements[0].Status.Should().Be(VexClaimStatus.NotAffected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EndToEnd_ObservationStore_MockPersistence()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateSimpleMatch("CVE-2024-PERSIST");
|
||||
var identity = CreateSimpleIdentity();
|
||||
var context = CreateSimpleContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, null, context);
|
||||
|
||||
// Simulate persistence
|
||||
var persistedId = await _mockObservationStore.Object.AppendAsync(observation);
|
||||
|
||||
// Assert
|
||||
persistedId.Should().Be(observation.ObservationId);
|
||||
_mockObservationStore.Verify(
|
||||
x => x.AppendAsync(It.Is<VexObservation>(o => o.ObservationId == observation.ObservationId), It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dependency Injection Tests
|
||||
|
||||
[Fact]
|
||||
public void DI_Registration_ResolvesGenerator()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.Configure<VexBridgeOptions>(opts =>
|
||||
{
|
||||
opts.MinConfidenceThreshold = 0.75m;
|
||||
opts.SignWithDsse = false;
|
||||
});
|
||||
services.AddSingleton<IVexEvidenceGenerator, VexEvidenceGenerator>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var generator = provider.GetService<IVexEvidenceGenerator>();
|
||||
|
||||
// Assert
|
||||
generator.Should().NotBeNull();
|
||||
generator.Should().BeOfType<VexEvidenceGenerator>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static BinaryVulnMatch CreateSimpleMatch(string cveId)
|
||||
{
|
||||
return new BinaryVulnMatch
|
||||
{
|
||||
CveId = cveId,
|
||||
VulnerablePurl = "pkg:deb/debian/test-pkg@1.0.0",
|
||||
Method = MatchMethod.FingerprintMatch,
|
||||
Confidence = 0.90m,
|
||||
Evidence = null
|
||||
};
|
||||
}
|
||||
|
||||
private static BinaryIdentity CreateSimpleIdentity()
|
||||
{
|
||||
return new BinaryIdentity
|
||||
{
|
||||
BinaryKey = "test-build-id",
|
||||
BuildId = "test-build-id",
|
||||
FileSha256 = "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
}
|
||||
|
||||
private static VexGenerationContext CreateSimpleContext()
|
||||
{
|
||||
return new VexGenerationContext
|
||||
{
|
||||
TenantId = "tenant-test",
|
||||
ScanId = "scan-test",
|
||||
ProductKey = "pkg:deb/debian/test-pkg@1.0.0",
|
||||
DistroRelease = "debian:bookworm",
|
||||
SignWithDsse = false
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock interface for VEX observation persistence.
|
||||
/// </summary>
|
||||
public interface IVexObservationStore
|
||||
{
|
||||
Task<string> AppendAsync(VexObservation observation, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.BinaryIndex.VexBridge.Tests;
|
||||
|
||||
public class VexEvidenceGeneratorTests
|
||||
{
|
||||
private readonly VexEvidenceGenerator _generator;
|
||||
private readonly VexBridgeOptions _options;
|
||||
|
||||
public VexEvidenceGeneratorTests()
|
||||
{
|
||||
_options = new VexBridgeOptions
|
||||
{
|
||||
MinConfidenceThreshold = 0.70m,
|
||||
SignWithDsse = false,
|
||||
MaxBatchSize = 1000
|
||||
};
|
||||
|
||||
_generator = new VexEvidenceGenerator(
|
||||
NullLogger<VexEvidenceGenerator>.Instance,
|
||||
Options.Create(_options));
|
||||
}
|
||||
|
||||
#region GenerateFromBinaryMatchAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_WithFixedStatus_ReturnsNotAffectedObservation()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-1234", confidence: 0.95m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var fixStatus = CreateFixStatus(FixState.Fixed, "1.2.3-1");
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, fixStatus, context);
|
||||
|
||||
// Assert
|
||||
observation.Should().NotBeNull();
|
||||
observation.Statements.Should().HaveCount(1);
|
||||
observation.Statements[0].Status.Should().Be(VexClaimStatus.NotAffected);
|
||||
observation.Statements[0].Justification.Should().Be(VexJustification.VulnerableCodeNotPresent);
|
||||
observation.Statements[0].FixedVersion.Should().Be("1.2.3-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_WithVulnerableStatus_ReturnsAffectedObservation()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-5678", confidence: 0.90m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var fixStatus = CreateFixStatus(FixState.Vulnerable);
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, fixStatus, context);
|
||||
|
||||
// Assert
|
||||
observation.Statements[0].Status.Should().Be(VexClaimStatus.Affected);
|
||||
observation.Statements[0].Justification.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_WithUnknownStatus_ReturnsUnderInvestigation()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-9999", confidence: 0.85m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var fixStatus = CreateFixStatus(FixState.Unknown);
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, fixStatus, context);
|
||||
|
||||
// Assert
|
||||
observation.Statements[0].Status.Should().Be(VexClaimStatus.UnderInvestigation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_WithNullFixStatus_ReturnsUnderInvestigation()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-0000", confidence: 0.80m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, null, context);
|
||||
|
||||
// Assert
|
||||
observation.Statements[0].Status.Should().Be(VexClaimStatus.UnderInvestigation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_BelowConfidenceThreshold_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-LOW", confidence: 0.50m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var act = () => _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, null, context);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*below minimum threshold*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_WithWontfixStatus_ReturnsNotAffectedWithMitigation()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-WONT", confidence: 0.95m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var fixStatus = CreateFixStatus(FixState.Wontfix);
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, fixStatus, context);
|
||||
|
||||
// Assert
|
||||
observation.Statements[0].Status.Should().Be(VexClaimStatus.NotAffected);
|
||||
observation.Statements[0].Justification.Should().Be(VexJustification.InlineMitigationsAlreadyExist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_SetsCorrectProviderAndStream()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-TEST", confidence: 0.95m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var context = CreateContext(providerId: "test.provider", streamId: "test.stream");
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, null, context);
|
||||
|
||||
// Assert
|
||||
observation.ProviderId.Should().Be("test.provider");
|
||||
observation.StreamId.Should().Be("test.stream");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GenerateObservationId Tests
|
||||
|
||||
[Fact]
|
||||
public void GenerateObservationId_SameInputs_ReturnsSameId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var id1 = _generator.GenerateObservationId("tenant1", "CVE-2024-1234", "pkg:deb/debian/openssl@1.0", "scan-001");
|
||||
var id2 = _generator.GenerateObservationId("tenant1", "CVE-2024-1234", "pkg:deb/debian/openssl@1.0", "scan-001");
|
||||
|
||||
// Assert
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateObservationId_DifferentCve_ReturnsDifferentId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var id1 = _generator.GenerateObservationId("tenant1", "CVE-2024-1234", "pkg:deb/debian/openssl@1.0", "scan-001");
|
||||
var id2 = _generator.GenerateObservationId("tenant1", "CVE-2024-5678", "pkg:deb/debian/openssl@1.0", "scan-001");
|
||||
|
||||
// Assert
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateObservationId_CaseInsensitiveTenant()
|
||||
{
|
||||
// Arrange & Act
|
||||
var id1 = _generator.GenerateObservationId("Tenant1", "CVE-2024-1234", "pkg:test", "scan-001");
|
||||
var id2 = _generator.GenerateObservationId("tenant1", "CVE-2024-1234", "pkg:test", "scan-001");
|
||||
|
||||
// Assert
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateObservationId_ReturnsValidGuidFormat()
|
||||
{
|
||||
// Arrange & Act
|
||||
var id = _generator.GenerateObservationId("tenant", "CVE-2024-1234", "pkg:test", "scan");
|
||||
|
||||
// Assert
|
||||
Guid.TryParse(id, out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GenerateBatchAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateBatchAsync_ProcessesAllItems_InDeterministicOrder()
|
||||
{
|
||||
// Arrange
|
||||
var matches = new[]
|
||||
{
|
||||
CreateBinaryMatchWithContext("CVE-2024-003", "scan-001"),
|
||||
CreateBinaryMatchWithContext("CVE-2024-001", "scan-001"),
|
||||
CreateBinaryMatchWithContext("CVE-2024-002", "scan-001")
|
||||
};
|
||||
|
||||
// Act
|
||||
var observations = await _generator.GenerateBatchAsync(matches);
|
||||
|
||||
// Assert
|
||||
observations.Should().HaveCount(3);
|
||||
// Should be ordered by observation ID
|
||||
observations.Select(o => o.ObservationId)
|
||||
.Should().BeInAscendingOrder();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateBatchAsync_SkipsItemsBelowThreshold()
|
||||
{
|
||||
// Arrange
|
||||
var matches = new[]
|
||||
{
|
||||
CreateBinaryMatchWithContext("CVE-2024-HIGH", "scan-001", confidence: 0.95m),
|
||||
CreateBinaryMatchWithContext("CVE-2024-LOW", "scan-001", confidence: 0.50m),
|
||||
CreateBinaryMatchWithContext("CVE-2024-MED", "scan-001", confidence: 0.80m)
|
||||
};
|
||||
|
||||
// Act
|
||||
var observations = await _generator.GenerateBatchAsync(matches);
|
||||
|
||||
// Assert
|
||||
observations.Should().HaveCount(2);
|
||||
observations.Should().NotContain(o => o.Statements.Any(s => s.VulnerabilityId == "CVE-2024-LOW"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateBatchAsync_RespectsMaxBatchSize()
|
||||
{
|
||||
// Arrange - Create more items than max batch size
|
||||
_options.MaxBatchSize = 5;
|
||||
var generator = new VexEvidenceGenerator(
|
||||
NullLogger<VexEvidenceGenerator>.Instance,
|
||||
Options.Create(_options));
|
||||
|
||||
var matches = Enumerable.Range(1, 10)
|
||||
.Select(i => CreateBinaryMatchWithContext($"CVE-2024-{i:D4}", $"scan-{i}"))
|
||||
.ToList();
|
||||
|
||||
// Act
|
||||
var observations = await generator.GenerateBatchAsync(matches);
|
||||
|
||||
// Assert
|
||||
observations.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateBatchAsync_EmptyInput_ReturnsEmptyList()
|
||||
{
|
||||
// Act
|
||||
var observations = await _generator.GenerateBatchAsync(Array.Empty<BinaryMatchWithContext>());
|
||||
|
||||
// Assert
|
||||
observations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Evidence Content Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_EvidenceContainsRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-TEST",
|
||||
confidence: 0.95m,
|
||||
method: MatchMethod.FingerprintMatch);
|
||||
var identity = CreateBinaryIdentity(
|
||||
buildId: "build123",
|
||||
fileSha256: "sha256:abc123",
|
||||
textSha256: "sha256:def456");
|
||||
var fixStatus = CreateFixStatus(FixState.Fixed, "1.0.0-fix1");
|
||||
var context = CreateContext(distroRelease: "debian:bookworm");
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, fixStatus, context);
|
||||
|
||||
// Assert
|
||||
var content = observation.Content.Raw;
|
||||
content.Should().NotBeNull();
|
||||
|
||||
// Check that evidence contains expected fields
|
||||
var json = content.AsObject();
|
||||
json["type"]?.GetValue<string>().Should().Be("binary_fingerprint_match");
|
||||
json["match_type"]?.GetValue<string>().Should().Be("fingerprint");
|
||||
json["build_id"]?.GetValue<string>().Should().Be("build123");
|
||||
json["distro_release"]?.GetValue<string>().Should().Be("debian:bookworm");
|
||||
json["fixed_version"]?.GetValue<string>().Should().Be("1.0.0-fix1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_WithBuildIdMatch_SetsCorrectMatchType()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-BUILD",
|
||||
confidence: 0.99m,
|
||||
method: MatchMethod.BuildIdCatalog);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, null, context);
|
||||
|
||||
// Assert
|
||||
var json = observation.Content.Raw.AsObject();
|
||||
json["match_type"]?.GetValue<string>().Should().Be("build_id");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Linkset Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_LinksContainVulnerabilityReference()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-LINK", confidence: 0.90m);
|
||||
var identity = CreateBinaryIdentity();
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, null, context);
|
||||
|
||||
// Assert
|
||||
// Note: VexObservationLinkset normalizes aliases to lowercase for case-insensitive comparison
|
||||
observation.Linkset.Aliases.Should().Contain("cve-2024-link");
|
||||
observation.Linkset.Purls.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateFromBinaryMatchAsync_IncludesBuildIdReference_WhenPresent()
|
||||
{
|
||||
// Arrange
|
||||
var match = CreateBinaryVulnMatch("CVE-2024-BUILDREF", confidence: 0.90m);
|
||||
var identity = CreateBinaryIdentity(buildId: "test-build-id-12345");
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
var observation = await _generator.GenerateFromBinaryMatchAsync(
|
||||
match, identity, null, context);
|
||||
|
||||
// Assert
|
||||
observation.Linkset.References
|
||||
.Should().Contain(r => r.Type == "build_id" && r.Url.Contains("test-build-id-12345"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static BinaryVulnMatch CreateBinaryVulnMatch(
|
||||
string cveId,
|
||||
decimal confidence = 0.90m,
|
||||
MatchMethod method = MatchMethod.FingerprintMatch)
|
||||
{
|
||||
return new BinaryVulnMatch
|
||||
{
|
||||
CveId = cveId,
|
||||
VulnerablePurl = $"pkg:deb/debian/test-package@1.0.0",
|
||||
Method = method,
|
||||
Confidence = confidence,
|
||||
Evidence = new MatchEvidence
|
||||
{
|
||||
BuildId = null,
|
||||
Similarity = confidence,
|
||||
MatchedFunction = null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static BinaryIdentity CreateBinaryIdentity(
|
||||
string? buildId = null,
|
||||
string fileSha256 = "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
string? textSha256 = null)
|
||||
{
|
||||
return new BinaryIdentity
|
||||
{
|
||||
BinaryKey = buildId ?? fileSha256,
|
||||
BuildId = buildId,
|
||||
FileSha256 = fileSha256,
|
||||
TextSha256 = textSha256,
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
}
|
||||
|
||||
private static FixStatusResult CreateFixStatus(
|
||||
FixState state,
|
||||
string? fixedVersion = null)
|
||||
{
|
||||
return new FixStatusResult
|
||||
{
|
||||
State = state,
|
||||
FixedVersion = fixedVersion,
|
||||
Method = FixMethod.SecurityFeed,
|
||||
Confidence = 0.95m,
|
||||
EvidenceId = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
private static VexGenerationContext CreateContext(
|
||||
string tenantId = "test-tenant",
|
||||
string scanId = "scan-001",
|
||||
string productKey = "pkg:deb/debian/test-package@1.0.0",
|
||||
string? distroRelease = null,
|
||||
string providerId = "stellaops.binaryindex",
|
||||
string streamId = "binary_resolution")
|
||||
{
|
||||
return new VexGenerationContext
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ScanId = scanId,
|
||||
ProductKey = productKey,
|
||||
DistroRelease = distroRelease,
|
||||
SignWithDsse = false,
|
||||
ProviderId = providerId,
|
||||
StreamId = streamId
|
||||
};
|
||||
}
|
||||
|
||||
private static BinaryMatchWithContext CreateBinaryMatchWithContext(
|
||||
string cveId,
|
||||
string scanId,
|
||||
decimal confidence = 0.90m)
|
||||
{
|
||||
return new BinaryMatchWithContext
|
||||
{
|
||||
Match = CreateBinaryVulnMatch(cveId, confidence),
|
||||
Identity = CreateBinaryIdentity(),
|
||||
FixStatus = null,
|
||||
Context = CreateContext(scanId: scanId)
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ResolutionControllerIntegrationTests.cs
|
||||
// Sprint: SPRINT_1227_0001_0002_BE_resolution_api
|
||||
// Task: T9 — Integration tests for resolution API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.BinaryIndex.Contracts.Resolution;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.WebService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the Resolution API endpoints.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Category", "BinaryIndex")]
|
||||
public class ResolutionControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public ResolutionControllerIntegrationTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Add test-specific services if needed
|
||||
});
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
#region Single Resolution Tests
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln returns 200 for valid request")]
|
||||
public async Task ResolveVuln_ValidRequest_Returns200()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "abc123def456789",
|
||||
DistroRelease = "debian:bookworm"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Package.Should().Be("pkg:deb/debian/openssl@3.0.7");
|
||||
result.Status.Should().BeOneOf(ResolutionStatus.Fixed, ResolutionStatus.Vulnerable,
|
||||
ResolutionStatus.NotAffected, ResolutionStatus.Unknown);
|
||||
result.ResolvedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln returns 400 for missing package")]
|
||||
public async Task ResolveVuln_MissingPackage_Returns400()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { BuildId = "abc123" }; // Missing required Package field
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln with CVE returns targeted resolution")]
|
||||
public async Task ResolveVuln_WithCveId_ReturnsTargetedResolution()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
CveId = "CVE-2024-0001",
|
||||
BuildId = "abc123def456789"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Resolution includes cache headers")]
|
||||
public async Task ResolveVuln_IncludesCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.Headers.Should().ContainKey("X-RateLimit-Limit");
|
||||
response.Headers.Should().ContainKey("X-RateLimit-Remaining");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Resolution Tests
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln/batch handles multiple items")]
|
||||
public async Task ResolveBatch_MultipleItems_ReturnsAllResults()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BatchVulnResolutionRequest
|
||||
{
|
||||
Items = new[]
|
||||
{
|
||||
new VulnResolutionRequest { Package = "pkg:deb/debian/openssl@3.0.7" },
|
||||
new VulnResolutionRequest { Package = "pkg:deb/debian/libcurl@7.88.1" },
|
||||
new VulnResolutionRequest { Package = "pkg:deb/debian/zlib@1.2.13" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<BatchVulnResolutionResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Results.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Batch resolution respects size limit")]
|
||||
public async Task ResolveBatch_ExceedsSizeLimit_Returns400()
|
||||
{
|
||||
// Arrange - Create 501 items (assuming 500 is the limit)
|
||||
var items = Enumerable.Range(0, 501)
|
||||
.Select(i => new VulnResolutionRequest { Package = $"pkg:npm/package{i}@1.0.0" })
|
||||
.ToArray();
|
||||
|
||||
var request = new BatchVulnResolutionRequest { Items = items };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Batch resolution performance under 500ms for 100 cached items")]
|
||||
public async Task ResolveBatch_CachedItems_PerformanceAcceptable()
|
||||
{
|
||||
// Arrange
|
||||
var items = Enumerable.Range(0, 100)
|
||||
.Select(i => new VulnResolutionRequest
|
||||
{
|
||||
Package = $"pkg:deb/debian/test-package{i}@1.0.0",
|
||||
BuildId = $"build-{i}"
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var request = new BatchVulnResolutionRequest { Items = items };
|
||||
|
||||
// Warm up cache with first request
|
||||
await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
|
||||
// Act
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
stopwatch.Stop();
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
stopwatch.ElapsedMilliseconds.Should().BeLessThan(500,
|
||||
"Cached batch resolution should complete in under 500ms");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cache Tests
|
||||
|
||||
[Fact(DisplayName = "Second request returns cached result")]
|
||||
public async Task ResolveVuln_SecondRequest_ReturnsCachedResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "cache-test-build-id"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response1 = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result1 = await response1.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
var response2 = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result2 = await response2.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
result1.Should().NotBeNull();
|
||||
result2.Should().NotBeNull();
|
||||
result2!.FromCache.Should().BeTrue();
|
||||
result1!.Status.Should().Be(result2.Status);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Bypass cache option works")]
|
||||
public async Task ResolveVuln_BypassCache_FreshResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "bypass-cache-test"
|
||||
};
|
||||
|
||||
// First request to populate cache
|
||||
await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Second request with bypass
|
||||
_client.DefaultRequestHeaders.Add("X-Bypass-Cache", "true");
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.FromCache.Should().BeFalse();
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Bypass-Cache");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DSSE Attestation Tests
|
||||
|
||||
[Fact(DisplayName = "Response includes DSSE attestation when requested")]
|
||||
public async Task ResolveVuln_WithDsseRequest_IncludesAttestation()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "dsse-test-build"
|
||||
};
|
||||
|
||||
_client.DefaultRequestHeaders.Add("X-Include-Attestation", "true");
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
// Note: Attestation may be null if signing is not configured
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Include-Attestation");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DSSE attestation is valid base64")]
|
||||
public async Task ResolveVuln_DsseAttestation_IsValidBase64()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "dsse-validation-test"
|
||||
};
|
||||
|
||||
_client.DefaultRequestHeaders.Add("X-Include-Attestation", "true");
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
if (!string.IsNullOrEmpty(result?.AttestationDsse))
|
||||
{
|
||||
// Should not throw
|
||||
var bytes = Convert.FromBase64String(result.AttestationDsse);
|
||||
bytes.Should().NotBeEmpty();
|
||||
|
||||
// Should be valid JSON
|
||||
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
doc.RootElement.TryGetProperty("payload", out _).Should().BeTrue();
|
||||
doc.RootElement.TryGetProperty("payloadType", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Include-Attestation");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rate Limiting Tests
|
||||
|
||||
[Fact(DisplayName = "Rate limiting returns 429 when exceeded")]
|
||||
public async Task ResolveVuln_RateLimitExceeded_Returns429()
|
||||
{
|
||||
// Arrange - This test depends on rate limit configuration
|
||||
// Create a client with test tenant that has low rate limit
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:npm/rate-limit-test@1.0.0"
|
||||
};
|
||||
|
||||
_client.DefaultRequestHeaders.Add("X-Tenant-Id", "rate-limit-test-tenant");
|
||||
|
||||
// Act - Send many requests quickly
|
||||
var tasks = Enumerable.Range(0, 150)
|
||||
.Select(_ => _client.PostAsJsonAsync("/api/v1/resolve/vuln", request));
|
||||
|
||||
var responses = await Task.WhenAll(tasks);
|
||||
|
||||
// Assert - At least some should be rate limited
|
||||
var rateLimited = responses.Where(r => r.StatusCode == HttpStatusCode.TooManyRequests);
|
||||
// Note: This may pass or fail depending on actual rate limit config
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Tenant-Id");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Rate limit headers are present")]
|
||||
public async Task ResolveVuln_RateLimitHeaders_Present()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:npm/headers-test@1.0.0"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.Headers.Contains("X-RateLimit-Limit").Should().BeTrue();
|
||||
response.Headers.Contains("X-RateLimit-Remaining").Should().BeTrue();
|
||||
response.Headers.Contains("X-RateLimit-Reset").Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Evidence Tests
|
||||
|
||||
[Fact(DisplayName = "Fixed resolution includes evidence")]
|
||||
public async Task ResolveVuln_FixedStatus_IncludesEvidence()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7-1+deb12u1",
|
||||
BuildId = "fixed-binary-build-id",
|
||||
DistroRelease = "debian:bookworm"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
if (result?.Status == ResolutionStatus.Fixed)
|
||||
{
|
||||
result.Evidence.Should().NotBeNull();
|
||||
result.Evidence!.MatchType.Should().NotBeNullOrEmpty();
|
||||
result.Evidence.Confidence.Should().BeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for batch request if not in Contracts.
|
||||
/// </summary>
|
||||
public record BatchVulnResolutionRequest
|
||||
{
|
||||
public VulnResolutionRequest[] Items { get; init; } = Array.Empty<VulnResolutionRequest>();
|
||||
public ResolutionOptions? Options { get; init; }
|
||||
}
|
||||
|
||||
public record BatchVulnResolutionResponse
|
||||
{
|
||||
public VulnResolutionResponse[] Results { get; init; } = Array.Empty<VulnResolutionResponse>();
|
||||
public int TotalCount { get; init; }
|
||||
public int SuccessCount { get; init; }
|
||||
public int ErrorCount { get; init; }
|
||||
}
|
||||
|
||||
public record ResolutionOptions
|
||||
{
|
||||
public bool BypassCache { get; init; }
|
||||
public bool IncludeDsseAttestation { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user