Complete batch 012 (golden set diff) and 013 (advisory chat), fix build errors
Sprints completed: - SPRINT_20260110_012_* (golden set diff layer - 10 sprints) - SPRINT_20260110_013_* (advisory chat - 4 sprints) Build fixes applied: - Fix namespace conflicts with Microsoft.Extensions.Options.Options.Create - Fix VexDecisionReachabilityIntegrationTests API drift (major rewrite) - Fix VexSchemaValidationTests FluentAssertions method name - Fix FixChainGateIntegrationTests ambiguous type references - Fix AdvisoryAI test files required properties and namespace aliases - Add stub types for CveMappingController (ICveSymbolMappingService) - Fix VerdictBuilderService static context issue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.FixChain.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the FixChain attestation workflow.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class FixChainAttestationIntegrationTests
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public FixChainAttestationIntegrationTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 10, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddLogging();
|
||||
services.Configure<FixChainOptions>(opts =>
|
||||
{
|
||||
opts.AnalyzerName = "TestAnalyzer";
|
||||
opts.AnalyzerVersion = "1.0.0";
|
||||
opts.AnalyzerSourceDigest = "sha256:integrationtest";
|
||||
});
|
||||
|
||||
services.AddFixChainAttestation();
|
||||
|
||||
_services = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_CreateAndVerify_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest("CVE-2024-12345", "openssl");
|
||||
|
||||
// Act - Create attestation
|
||||
var createResult = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert - Creation succeeded
|
||||
createResult.Should().NotBeNull();
|
||||
createResult.EnvelopeJson.Should().NotBeNullOrEmpty();
|
||||
createResult.Predicate.CveId.Should().Be("CVE-2024-12345");
|
||||
createResult.Predicate.Component.Should().Be("openssl");
|
||||
|
||||
// Act - Verify attestation
|
||||
var verifyResult = await attestationService.VerifyAsync(createResult.EnvelopeJson);
|
||||
|
||||
// Assert - Verification parses correctly
|
||||
verifyResult.Predicate.Should().NotBeNull();
|
||||
verifyResult.Predicate!.CveId.Should().Be("CVE-2024-12345");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_WithFixedVerdict_ProducesCorrectAttestation()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest(
|
||||
"CVE-2024-0727",
|
||||
"openssl",
|
||||
verdict: "Fixed",
|
||||
confidence: 0.95m,
|
||||
prePathCount: 5,
|
||||
postPathCount: 0);
|
||||
|
||||
// Act
|
||||
var result = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Status.Should().Be("fixed");
|
||||
result.Predicate.Verdict.Confidence.Should().Be(0.95m);
|
||||
result.Predicate.Reachability.Eliminated.Should().BeTrue();
|
||||
result.Predicate.Reachability.PrePathCount.Should().Be(5);
|
||||
result.Predicate.Reachability.PostPathCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_WithPartialFix_ProducesCorrectAttestation()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest(
|
||||
"CVE-2024-0728",
|
||||
"libxml2",
|
||||
verdict: "PartialFix",
|
||||
confidence: 0.60m,
|
||||
prePathCount: 5,
|
||||
postPathCount: 2);
|
||||
|
||||
// Act
|
||||
var result = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Status.Should().Be("partial");
|
||||
result.Predicate.Reachability.Eliminated.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_EnvelopeContainsValidInTotoStatement()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest("CVE-2024-12345", "test");
|
||||
|
||||
// Act
|
||||
var result = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert - Parse envelope
|
||||
var envelope = JsonDocument.Parse(result.EnvelopeJson);
|
||||
envelope.RootElement.GetProperty("payloadType").GetString()
|
||||
.Should().Be("application/vnd.in-toto+json");
|
||||
|
||||
// Decode payload
|
||||
var payloadBase64 = envelope.RootElement.GetProperty("payload").GetString();
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64!);
|
||||
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
|
||||
|
||||
// Parse statement
|
||||
var statement = JsonDocument.Parse(payloadJson);
|
||||
statement.RootElement.GetProperty("_type").GetString()
|
||||
.Should().Be("https://in-toto.io/Statement/v1");
|
||||
statement.RootElement.GetProperty("predicateType").GetString()
|
||||
.Should().Be("https://stella-ops.org/predicates/fix-chain/v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_SubjectMatchesPatchedBinary()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var patchedSha = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
|
||||
var request = CreateTestRequest("CVE-2024-12345", "test", patchedBinarySha256: patchedSha);
|
||||
|
||||
// Act
|
||||
var result = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
var envelope = JsonDocument.Parse(result.EnvelopeJson);
|
||||
var payloadBase64 = envelope.RootElement.GetProperty("payload").GetString();
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64!);
|
||||
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
|
||||
var statement = JsonDocument.Parse(payloadJson);
|
||||
|
||||
var subject = statement.RootElement.GetProperty("subject")[0];
|
||||
subject.GetProperty("digest").GetProperty("sha256").GetString()
|
||||
.Should().Be(patchedSha);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_VerdictRationaleIsPopulated()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest(
|
||||
"CVE-2024-12345",
|
||||
"test",
|
||||
functionsRemoved: 3,
|
||||
edgesEliminated: 5);
|
||||
|
||||
// Act
|
||||
var result = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Rationale.Should().NotBeEmpty();
|
||||
result.Predicate.Verdict.Rationale.Should().ContainMatch("*removed*");
|
||||
result.Predicate.Verdict.Rationale.Should().ContainMatch("*edge*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_AnalyzerMetadataFromOptions()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest("CVE-2024-12345", "test");
|
||||
|
||||
// Act
|
||||
var result = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Analyzer.Name.Should().Be("TestAnalyzer");
|
||||
result.Predicate.Analyzer.Version.Should().Be("1.0.0");
|
||||
result.Predicate.Analyzer.SourceDigest.Should().Be("sha256:integrationtest");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_TimestampFromTimeProvider()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest("CVE-2024-12345", "test");
|
||||
|
||||
// Act
|
||||
var result = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.AnalyzedAt.Should().Be(_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_ContentDigestIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request = CreateTestRequest("CVE-2024-12345", "test");
|
||||
|
||||
// Act
|
||||
var result1 = await attestationService.CreateAsync(request);
|
||||
var result2 = await attestationService.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result1.ContentDigest.Should().Be(result2.ContentDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_DifferentCveProducesDifferentDigest()
|
||||
{
|
||||
// Arrange
|
||||
var attestationService = _services.GetRequiredService<IFixChainAttestationService>();
|
||||
var request1 = CreateTestRequest("CVE-2024-12345", "test");
|
||||
var request2 = CreateTestRequest("CVE-2024-99999", "test");
|
||||
|
||||
// Act
|
||||
var result1 = await attestationService.CreateAsync(request1);
|
||||
var result2 = await attestationService.CreateAsync(request2);
|
||||
|
||||
// Assert
|
||||
result1.ContentDigest.Should().NotBe(result2.ContentDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_InMemoryStore_StoresAndRetrieves()
|
||||
{
|
||||
// Arrange
|
||||
var store = new InMemoryFixChainStore();
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddLogging();
|
||||
services.Configure<FixChainOptions>(opts => { });
|
||||
services.AddFixChainAttestation();
|
||||
services.AddFixChainAttestationStore<InMemoryFixChainStore>();
|
||||
services.AddSingleton(store);
|
||||
|
||||
var sp = services.BuildServiceProvider();
|
||||
var attestationService = sp.GetRequiredService<IFixChainAttestationService>();
|
||||
|
||||
var request = CreateTestRequest("CVE-2024-12345", "test");
|
||||
|
||||
// Act
|
||||
await attestationService.CreateAsync(request);
|
||||
var retrieved = await attestationService.GetAsync("CVE-2024-12345", request.PatchedBinary.Sha256);
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.CveId.Should().Be("CVE-2024-12345");
|
||||
}
|
||||
|
||||
private static FixChainBuildRequest CreateTestRequest(
|
||||
string cveId,
|
||||
string component,
|
||||
string verdict = "Fixed",
|
||||
decimal confidence = 0.90m,
|
||||
int prePathCount = 3,
|
||||
int postPathCount = 0,
|
||||
int functionsRemoved = 1,
|
||||
int edgesEliminated = 2,
|
||||
string patchedBinarySha256 = "2222222222222222222222222222222222222222222222222222222222222222")
|
||||
{
|
||||
return new FixChainBuildRequest
|
||||
{
|
||||
CveId = cveId,
|
||||
Component = component,
|
||||
GoldenSetDigest = "goldenset123",
|
||||
SbomDigest = "sbom456",
|
||||
VulnerableBinary = new BinaryIdentity
|
||||
{
|
||||
Sha256 = "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
Architecture = "x86_64"
|
||||
},
|
||||
PatchedBinary = new BinaryIdentity
|
||||
{
|
||||
Sha256 = patchedBinarySha256,
|
||||
Architecture = "x86_64"
|
||||
},
|
||||
ComponentPurl = $"pkg:deb/debian/{component}@1.0.0",
|
||||
DiffResult = new PatchDiffInput
|
||||
{
|
||||
Verdict = verdict,
|
||||
Confidence = confidence,
|
||||
FunctionsRemoved = functionsRemoved,
|
||||
FunctionsModified = 0,
|
||||
EdgesEliminated = edgesEliminated,
|
||||
TaintGatesAdded = 0,
|
||||
PrePathCount = prePathCount,
|
||||
PostPathCount = postPathCount,
|
||||
Evidence = ["Integration test evidence"]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory store for testing.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryFixChainStore : IFixChainAttestationStore
|
||||
{
|
||||
private readonly Dictionary<string, FixChainAttestationInfo> _store = new();
|
||||
|
||||
public Task StoreAsync(
|
||||
string contentDigest,
|
||||
string cveId,
|
||||
string binarySha256,
|
||||
string componentPurl,
|
||||
string envelopeJson,
|
||||
long? rekorLogIndex,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var key = $"{cveId}:{binarySha256}";
|
||||
_store[key] = new FixChainAttestationInfo
|
||||
{
|
||||
ContentDigest = contentDigest,
|
||||
CveId = cveId,
|
||||
Component = componentPurl,
|
||||
BinarySha256 = binarySha256,
|
||||
VerdictStatus = "fixed",
|
||||
Confidence = 0.95m,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
RekorLogIndex = rekorLogIndex
|
||||
};
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<FixChainAttestationInfo?> GetAsync(
|
||||
string cveId,
|
||||
string binarySha256,
|
||||
string? componentPurl = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var key = $"{cveId}:{binarySha256}";
|
||||
return Task.FromResult(_store.GetValueOrDefault(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Time.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.FixChain\StellaOps.Attestor.FixChain.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,387 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
||||
using Moq;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.FixChain.Tests.Unit;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FixChainAttestationService"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class FixChainAttestationServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly FixChainStatementBuilder _statementBuilder;
|
||||
private readonly FixChainValidator _validator;
|
||||
private readonly FixChainAttestationService _service;
|
||||
|
||||
public FixChainAttestationServiceTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 10, 12, 0, 0, TimeSpan.Zero));
|
||||
var options = Options.Create(new FixChainOptions
|
||||
{
|
||||
AnalyzerName = "TestAnalyzer",
|
||||
AnalyzerVersion = "1.0.0",
|
||||
AnalyzerSourceDigest = "sha256:test123"
|
||||
});
|
||||
|
||||
_statementBuilder = new FixChainStatementBuilder(
|
||||
_timeProvider,
|
||||
options,
|
||||
NullLogger<FixChainStatementBuilder>.Instance);
|
||||
|
||||
_validator = new FixChainValidator();
|
||||
|
||||
_service = new FixChainAttestationService(
|
||||
_statementBuilder,
|
||||
_validator,
|
||||
NullLogger<FixChainAttestationService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithValidRequest_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.EnvelopeJson.Should().NotBeNullOrEmpty();
|
||||
result.ContentDigest.Should().NotBeNullOrEmpty();
|
||||
result.Predicate.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_EnvelopeIsValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
var parseAction = () => JsonDocument.Parse(result.EnvelopeJson);
|
||||
parseAction.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_EnvelopeHasCorrectPayloadType()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
var envelope = JsonDocument.Parse(result.EnvelopeJson);
|
||||
envelope.RootElement.GetProperty("payloadType").GetString()
|
||||
.Should().Be("application/vnd.in-toto+json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_PayloadIsBase64Encoded()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
var envelope = JsonDocument.Parse(result.EnvelopeJson);
|
||||
var payload = envelope.RootElement.GetProperty("payload").GetString();
|
||||
|
||||
var decodeAction = () => Convert.FromBase64String(payload!);
|
||||
decodeAction.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_PredicateMatchesEnvelopeContent()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.CveId.Should().Be(request.CveId);
|
||||
result.Predicate.Component.Should().Be(request.Component);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithNullRequest_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||
_service.CreateAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithCancellation_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
_service.CreateAsync(request, null, cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithValidEnvelope_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
var createResult = await _service.CreateAsync(request);
|
||||
|
||||
// Act
|
||||
var verifyResult = await _service.VerifyAsync(createResult.EnvelopeJson);
|
||||
|
||||
// Assert - Note: unsigned envelope has issues
|
||||
verifyResult.Predicate.Should().NotBeNull();
|
||||
verifyResult.Predicate!.CveId.Should().Be(request.CveId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithInvalidJson_ReturnsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = "{ invalid json }";
|
||||
|
||||
// Act
|
||||
var result = await _service.VerifyAsync(invalidJson);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Issues.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithEmptyString_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() =>
|
||||
_service.VerifyAsync(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithNullString_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() =>
|
||||
_service.VerifyAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithWrongPayloadType_ReturnsIssue()
|
||||
{
|
||||
// Arrange
|
||||
var envelope = new
|
||||
{
|
||||
payloadType = "wrong/type",
|
||||
payload = Convert.ToBase64String("{}"u8.ToArray()),
|
||||
signatures = Array.Empty<object>()
|
||||
};
|
||||
var json = JsonSerializer.Serialize(envelope);
|
||||
|
||||
// Act
|
||||
var result = await _service.VerifyAsync(json);
|
||||
|
||||
// Assert
|
||||
result.Issues.Should().Contain(i => i.Contains("payload type"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithNoSignatures_ReturnsIssue()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
var createResult = await _service.CreateAsync(request);
|
||||
|
||||
// Act
|
||||
var result = await _service.VerifyAsync(createResult.EnvelopeJson);
|
||||
|
||||
// Assert
|
||||
result.Issues.Should().Contain(i => i.Contains("signature") || i.Contains("No signatures"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_WithNoStore_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _service.GetAsync("CVE-2024-12345", "abc123");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithStore_StoresAttestation()
|
||||
{
|
||||
// Arrange
|
||||
var mockStore = new Mock<IFixChainAttestationStore>();
|
||||
var service = new FixChainAttestationService(
|
||||
_statementBuilder,
|
||||
_validator,
|
||||
NullLogger<FixChainAttestationService>.Instance,
|
||||
mockStore.Object);
|
||||
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
await service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
mockStore.Verify(s => s.StoreAsync(
|
||||
It.IsAny<string>(),
|
||||
request.CveId,
|
||||
request.PatchedBinary.Sha256,
|
||||
request.ComponentPurl,
|
||||
It.IsAny<string>(),
|
||||
null,
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithStoreException_ContinuesWithoutError()
|
||||
{
|
||||
// Arrange
|
||||
var mockStore = new Mock<IFixChainAttestationStore>();
|
||||
mockStore.Setup(s => s.StoreAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<long?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new Exception("Store error"));
|
||||
|
||||
var service = new FixChainAttestationService(
|
||||
_statementBuilder,
|
||||
_validator,
|
||||
NullLogger<FixChainAttestationService>.Instance,
|
||||
mockStore.Object);
|
||||
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await service.CreateAsync(request);
|
||||
|
||||
// Assert - Should not throw, should return result
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithArchiveDisabled_SkipsStore()
|
||||
{
|
||||
// Arrange
|
||||
var mockStore = new Mock<IFixChainAttestationStore>();
|
||||
var service = new FixChainAttestationService(
|
||||
_statementBuilder,
|
||||
_validator,
|
||||
NullLogger<FixChainAttestationService>.Instance,
|
||||
mockStore.Object);
|
||||
|
||||
var request = CreateTestRequest();
|
||||
var options = new AttestationCreationOptions { Archive = false };
|
||||
|
||||
// Act
|
||||
await service.CreateAsync(request, options);
|
||||
|
||||
// Assert
|
||||
mockStore.Verify(s => s.StoreAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<long?>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_WithStore_CallsStore()
|
||||
{
|
||||
// Arrange
|
||||
var mockStore = new Mock<IFixChainAttestationStore>();
|
||||
var expectedInfo = new FixChainAttestationInfo
|
||||
{
|
||||
ContentDigest = "sha256:test",
|
||||
CveId = "CVE-2024-12345",
|
||||
Component = "test",
|
||||
BinarySha256 = "abc123",
|
||||
VerdictStatus = "fixed",
|
||||
Confidence = 0.95m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
mockStore.Setup(s => s.GetAsync("CVE-2024-12345", "abc123", null, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedInfo);
|
||||
|
||||
var service = new FixChainAttestationService(
|
||||
_statementBuilder,
|
||||
_validator,
|
||||
NullLogger<FixChainAttestationService>.Instance,
|
||||
mockStore.Object);
|
||||
|
||||
// Act
|
||||
var result = await service.GetAsync("CVE-2024-12345", "abc123");
|
||||
|
||||
// Assert
|
||||
result.Should().Be(expectedInfo);
|
||||
}
|
||||
|
||||
private static FixChainBuildRequest CreateTestRequest()
|
||||
{
|
||||
return new FixChainBuildRequest
|
||||
{
|
||||
CveId = "CVE-2024-12345",
|
||||
Component = "test-component",
|
||||
GoldenSetDigest = "0123456789abcdef",
|
||||
SbomDigest = "fedcba9876543210",
|
||||
VulnerableBinary = new BinaryIdentity
|
||||
{
|
||||
Sha256 = new string('1', 64),
|
||||
Architecture = "x86_64"
|
||||
},
|
||||
PatchedBinary = new BinaryIdentity
|
||||
{
|
||||
Sha256 = new string('2', 64),
|
||||
Architecture = "x86_64"
|
||||
},
|
||||
ComponentPurl = "pkg:deb/debian/test@1.0.0",
|
||||
DiffResult = new PatchDiffInput
|
||||
{
|
||||
Verdict = "Fixed",
|
||||
Confidence = 0.95m,
|
||||
FunctionsRemoved = 1,
|
||||
FunctionsModified = 0,
|
||||
EdgesEliminated = 2,
|
||||
TaintGatesAdded = 0,
|
||||
PrePathCount = 3,
|
||||
PostPathCount = 0,
|
||||
Evidence = ["Test evidence"]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.FixChain.Tests.Unit;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FixChainStatementBuilder"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class FixChainStatementBuilderTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly IOptions<FixChainOptions> _options;
|
||||
private readonly FixChainStatementBuilder _builder;
|
||||
|
||||
public FixChainStatementBuilderTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 10, 12, 0, 0, TimeSpan.Zero));
|
||||
_options = Options.Create(new FixChainOptions
|
||||
{
|
||||
AnalyzerName = "TestAnalyzer",
|
||||
AnalyzerVersion = "1.0.0",
|
||||
AnalyzerSourceDigest = "sha256:test123",
|
||||
FixedConfidenceThreshold = 0.80m,
|
||||
PartialConfidenceThreshold = 0.50m
|
||||
});
|
||||
_builder = new FixChainStatementBuilder(
|
||||
_timeProvider,
|
||||
_options,
|
||||
NullLogger<FixChainStatementBuilder>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithValidRequest_ReturnsStatementResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Statement.Should().NotBeNull();
|
||||
result.ContentDigest.Should().NotBeNullOrEmpty();
|
||||
result.ContentDigest.Should().HaveLength(64); // SHA-256 hex length
|
||||
result.Predicate.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsCorrectCveId()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(cveId: "CVE-2024-12345");
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.CveId.Should().Be("CVE-2024-12345");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsCorrectComponent()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(component: "openssl");
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Component.Should().Be("openssl");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_FormatsDigestWithSha256Prefix()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(goldenSetDigest: "abc123def456");
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.GoldenSetRef.Digest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_PreservesExistingSha256Prefix()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(goldenSetDigest: "sha256:abc123def456");
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.GoldenSetRef.Digest.Should().Be("sha256:abc123def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsBinaryReferences()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.VulnerableBinary.Should().NotBeNull();
|
||||
result.Predicate.VulnerableBinary.Sha256.Should().Be(request.VulnerableBinary.Sha256);
|
||||
result.Predicate.VulnerableBinary.Architecture.Should().Be(request.VulnerableBinary.Architecture);
|
||||
|
||||
result.Predicate.PatchedBinary.Should().NotBeNull();
|
||||
result.Predicate.PatchedBinary.Sha256.Should().Be(request.PatchedBinary.Sha256);
|
||||
result.Predicate.PatchedBinary.Architecture.Should().Be(request.PatchedBinary.Architecture);
|
||||
result.Predicate.PatchedBinary.Purl.Should().Be(request.ComponentPurl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsSignatureDiffSummary()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(
|
||||
functionsRemoved: 2,
|
||||
functionsModified: 3,
|
||||
edgesEliminated: 5,
|
||||
taintGatesAdded: 1);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.SignatureDiff.VulnerableFunctionsRemoved.Should().Be(2);
|
||||
result.Predicate.SignatureDiff.VulnerableFunctionsModified.Should().Be(3);
|
||||
result.Predicate.SignatureDiff.VulnerableEdgesEliminated.Should().Be(5);
|
||||
result.Predicate.SignatureDiff.SanitizersInserted.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsReachabilityOutcome_WhenAllPathsEliminated()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(prePathCount: 5, postPathCount: 0);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Reachability.PrePathCount.Should().Be(5);
|
||||
result.Predicate.Reachability.PostPathCount.Should().Be(0);
|
||||
result.Predicate.Reachability.Eliminated.Should().BeTrue();
|
||||
result.Predicate.Reachability.Reason.Should().Contain("eliminated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsReachabilityOutcome_WhenPathsReduced()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(prePathCount: 5, postPathCount: 2);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Reachability.Eliminated.Should().BeFalse();
|
||||
result.Predicate.Reachability.Reason.Should().Contain("reduced");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_VerdictFixed_WhenHighConfidenceAndFixedVerdict()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(verdict: "Fixed", confidence: 0.95m);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Status.Should().Be(FixChainVerdict.StatusFixed);
|
||||
result.Predicate.Verdict.Confidence.Should().Be(0.95m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_VerdictPartial_WhenMediumConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(verdict: "PartialFix", confidence: 0.60m);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Status.Should().Be(FixChainVerdict.StatusPartial);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_VerdictNotFixed_WhenStillVulnerable()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(verdict: "StillVulnerable", confidence: 0.10m);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Status.Should().Be(FixChainVerdict.StatusNotFixed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_VerdictInconclusive_WhenLowConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(verdict: "Unknown", confidence: 0.20m);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Status.Should().Be(FixChainVerdict.StatusInconclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsAnalyzerMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Analyzer.Name.Should().Be("TestAnalyzer");
|
||||
result.Predicate.Analyzer.Version.Should().Be("1.0.0");
|
||||
result.Predicate.Analyzer.SourceDigest.Should().Be("sha256:test123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SetsAnalyzedAtFromTimeProvider()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.AnalyzedAt.Should().Be(_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_CreatesValidInTotoStatement()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Statement.Type.Should().Be("https://in-toto.io/Statement/v1");
|
||||
result.Statement.PredicateType.Should().Be(FixChainPredicate.PredicateType);
|
||||
result.Statement.Subject.Should().HaveCount(1);
|
||||
result.Statement.Subject[0].Name.Should().Be(request.ComponentPurl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SubjectDigestMatchesPatchedBinary()
|
||||
{
|
||||
// Arrange
|
||||
var patchedSha = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
|
||||
var request = CreateTestRequest(patchedBinarySha256: patchedSha);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Statement.Subject[0].Digest.Should().ContainKey("sha256");
|
||||
result.Statement.Subject[0].Digest["sha256"].Should().Be(patchedSha);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ThrowsOnNullRequest()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||
_builder.BuildAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ThrowsOnCancellation()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
_builder.BuildAsync(request, cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ContentDigestIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result1 = await _builder.BuildAsync(request);
|
||||
var result2 = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result1.ContentDigest.Should().Be(result2.ContentDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_IncludesRationaleForFunctionsRemoved()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(functionsRemoved: 3);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Rationale.Should()
|
||||
.Contain(r => r.Contains("3") && r.Contains("removed"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_IncludesRationaleForEdgesEliminated()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(edgesEliminated: 5);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Rationale.Should()
|
||||
.Contain(r => r.Contains("5") && r.Contains("edge"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_IncludesRationaleForPathsEliminated()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequest(prePathCount: 10, postPathCount: 0);
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Predicate.Verdict.Rationale.Should()
|
||||
.Contain(r => r.Contains("All paths") || r.Contains("eliminated"));
|
||||
}
|
||||
|
||||
private static FixChainBuildRequest CreateTestRequest(
|
||||
string cveId = "CVE-2024-99999",
|
||||
string component = "test-component",
|
||||
string goldenSetDigest = "0123456789abcdef",
|
||||
string sbomDigest = "fedcba9876543210",
|
||||
string vulnerableBinarySha256 = "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
string patchedBinarySha256 = "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
string componentPurl = "pkg:deb/debian/test-component@1.0.0",
|
||||
string verdict = "Fixed",
|
||||
decimal confidence = 0.90m,
|
||||
int functionsRemoved = 1,
|
||||
int functionsModified = 0,
|
||||
int edgesEliminated = 2,
|
||||
int taintGatesAdded = 0,
|
||||
int prePathCount = 3,
|
||||
int postPathCount = 0)
|
||||
{
|
||||
return new FixChainBuildRequest
|
||||
{
|
||||
CveId = cveId,
|
||||
Component = component,
|
||||
GoldenSetDigest = goldenSetDigest,
|
||||
SbomDigest = sbomDigest,
|
||||
VulnerableBinary = new BinaryIdentity
|
||||
{
|
||||
Sha256 = vulnerableBinarySha256,
|
||||
Architecture = "x86_64"
|
||||
},
|
||||
PatchedBinary = new BinaryIdentity
|
||||
{
|
||||
Sha256 = patchedBinarySha256,
|
||||
Architecture = "x86_64"
|
||||
},
|
||||
ComponentPurl = componentPurl,
|
||||
DiffResult = new PatchDiffInput
|
||||
{
|
||||
Verdict = verdict,
|
||||
Confidence = confidence,
|
||||
FunctionsRemoved = functionsRemoved,
|
||||
FunctionsModified = functionsModified,
|
||||
EdgesEliminated = edgesEliminated,
|
||||
TaintGatesAdded = taintGatesAdded,
|
||||
PrePathCount = prePathCount,
|
||||
PostPathCount = postPathCount,
|
||||
Evidence = ["Test evidence"]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.FixChain.Tests.Unit;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FixChainValidator"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class FixChainValidatorTests
|
||||
{
|
||||
private readonly FixChainValidator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithValidPredicate_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate();
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
result.Predicate.Should().Be(predicate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithNullPredicate_ThrowsArgumentNull()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => _validator.Validate(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyCveId_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { CveId = "" };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("cveId"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithInvalidCveIdFormat_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { CveId = "INVALID-123" };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("CVE-"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithValidCveFormat_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { CveId = "CVE-2024-12345" };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyComponent_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { Component = "" };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("component"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithNullGoldenSetRef_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { GoldenSetRef = null! };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("goldenSetRef"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyGoldenSetDigest_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
GoldenSetRef = new ContentRef("")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("goldenSetRef.digest"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithInvalidDigestPrefix_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
GoldenSetRef = new ContentRef("md5:abc123")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("algorithm"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithSha512Digest_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
GoldenSetRef = new ContentRef("sha512:abc123")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithNullVulnerableBinary_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { VulnerableBinary = null! };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("vulnerableBinary"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyBinarySha256_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
VulnerableBinary = new BinaryRef("", "x86_64")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("vulnerableBinary.sha256"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithWrongLengthBinarySha256_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
VulnerableBinary = new BinaryRef("abc123", "x86_64")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("64 hex"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyArchitecture_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
VulnerableBinary = new BinaryRef(new string('a', 64), "")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("architecture"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithNullVerdict_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { Verdict = null! };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("verdict"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyVerdictStatus_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
Verdict = new FixChainVerdict("", 0.5m, [])
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("verdict.status"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithInvalidVerdictStatus_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
Verdict = new FixChainVerdict("invalid_status", 0.5m, [])
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("verdict.status"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("fixed")]
|
||||
[InlineData("partial")]
|
||||
[InlineData("not_fixed")]
|
||||
[InlineData("inconclusive")]
|
||||
public void Validate_WithValidVerdictStatus_ReturnsSuccess(string status)
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
Verdict = new FixChainVerdict(status, 0.5m, [])
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithConfidenceBelowZero_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
Verdict = new FixChainVerdict("fixed", -0.1m, [])
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("confidence"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithConfidenceAboveOne_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
Verdict = new FixChainVerdict("fixed", 1.1m, [])
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("confidence"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithNullAnalyzer_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { Analyzer = null! };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("analyzer"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyAnalyzerName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
Analyzer = new AnalyzerMetadata("", "1.0", "sha256:abc")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("analyzer.name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithDefaultAnalyzedAt_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with { AnalyzedAt = default };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("analyzedAt"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateJson_WithValidJson_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate();
|
||||
var json = JsonSerializer.Serialize(predicate);
|
||||
var element = JsonDocument.Parse(json).RootElement;
|
||||
|
||||
// Act
|
||||
var result = _validator.ValidateJson(element);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateJson_WithInvalidJson_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{ \"invalid\": true }";
|
||||
var element = JsonDocument.Parse(json).RootElement;
|
||||
|
||||
// Act
|
||||
var result = _validator.ValidateJson(element);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithMultipleErrors_ReturnsAllErrors()
|
||||
{
|
||||
// Arrange
|
||||
var predicate = CreateValidPredicate() with
|
||||
{
|
||||
CveId = "",
|
||||
Component = "",
|
||||
Verdict = null!
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().HaveCountGreaterOrEqualTo(3);
|
||||
}
|
||||
|
||||
private static FixChainPredicate CreateValidPredicate()
|
||||
{
|
||||
return new FixChainPredicate
|
||||
{
|
||||
CveId = "CVE-2024-12345",
|
||||
Component = "test-component",
|
||||
GoldenSetRef = new ContentRef("sha256:" + new string('a', 64)),
|
||||
SbomRef = new ContentRef("sha256:" + new string('b', 64)),
|
||||
VulnerableBinary = new BinaryRef(new string('1', 64), "x86_64"),
|
||||
PatchedBinary = new BinaryRef(new string('2', 64), "x86_64"),
|
||||
SignatureDiff = new SignatureDiffSummary(1, 2, 3, 0, []),
|
||||
Reachability = new ReachabilityOutcome(5, 0, true, "All paths eliminated"),
|
||||
Verdict = new FixChainVerdict("fixed", 0.95m, ["Test rationale"]),
|
||||
Analyzer = new AnalyzerMetadata("TestAnalyzer", "1.0.0", "sha256:test"),
|
||||
AnalyzedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user