Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -5,6 +5,7 @@
// -----------------------------------------------------------------------------
using FluentAssertions;
using StellaOps.BinaryIndex.Core.Models;
using StellaOps.BinaryIndex.FixIndex.Models;
using StellaOps.BinaryIndex.FixIndex.Parsers;
using Xunit;

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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; }
}