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:
master
2026-01-11 10:09:07 +02:00
parent a3b2f30a11
commit 7f7eb8b228
232 changed files with 58979 additions and 91 deletions

View File

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

View File

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

View File

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

View File

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

View File

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