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,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);xUnit1051</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Attestor.FixChain\StellaOps.Attestor.FixChain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,158 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
using System.Collections.Immutable;
using FluentAssertions;
using Xunit;
namespace StellaOps.Attestor.FixChain.Tests.Unit;
[Trait("Category", "Unit")]
public sealed class FixChainPredicateTests
{
[Fact]
public void PredicateType_IsCorrect()
{
// Assert
FixChainPredicate.PredicateType.Should().Be("https://stella-ops.org/predicates/fix-chain/v1");
}
[Fact]
public void FixChainPredicate_CanBeCreated()
{
// Arrange & Act
var predicate = CreateValidPredicate();
// Assert
predicate.CveId.Should().Be("CVE-2024-1234");
predicate.Component.Should().Be("openssl");
predicate.GoldenSetRef.Digest.Should().StartWith("sha256:");
predicate.VulnerableBinary.Sha256.Should().HaveLength(64);
predicate.PatchedBinary.Sha256.Should().HaveLength(64);
}
[Theory]
[InlineData(FixChainVerdict.StatusFixed)]
[InlineData(FixChainVerdict.StatusPartial)]
[InlineData(FixChainVerdict.StatusNotFixed)]
[InlineData(FixChainVerdict.StatusInconclusive)]
public void FixChainVerdict_StatusConstants_AreDefined(string status)
{
// Assert
status.Should().NotBeNullOrEmpty();
}
[Fact]
public void ContentRef_StoresDigestAndUri()
{
// Arrange & Act
var contentRef = new ContentRef("sha256:abc123", "https://example.com/artifact");
// Assert
contentRef.Digest.Should().Be("sha256:abc123");
contentRef.Uri.Should().Be("https://example.com/artifact");
}
[Fact]
public void ContentRef_UriIsOptional()
{
// Arrange & Act
var contentRef = new ContentRef("sha256:abc123");
// Assert
contentRef.Uri.Should().BeNull();
}
[Fact]
public void BinaryRef_StoresAllProperties()
{
// Arrange & Act
var binaryRef = new BinaryRef(
"abcd1234" + new string('0', 56),
"x86_64",
"build-12345",
"pkg:generic/openssl@3.0.0");
// Assert
binaryRef.Sha256.Should().HaveLength(64);
binaryRef.Architecture.Should().Be("x86_64");
binaryRef.BuildId.Should().Be("build-12345");
binaryRef.Purl.Should().Be("pkg:generic/openssl@3.0.0");
}
[Fact]
public void SignatureDiffSummary_StoresCounts()
{
// Arrange & Act
var summary = new SignatureDiffSummary(
VulnerableFunctionsRemoved: 2,
VulnerableFunctionsModified: 3,
VulnerableEdgesEliminated: 5,
SanitizersInserted: 1,
Details: ["Function foo removed", "Edge bb0->bb1 eliminated"]);
// Assert
summary.VulnerableFunctionsRemoved.Should().Be(2);
summary.VulnerableFunctionsModified.Should().Be(3);
summary.VulnerableEdgesEliminated.Should().Be(5);
summary.SanitizersInserted.Should().Be(1);
summary.Details.Should().HaveCount(2);
}
[Fact]
public void ReachabilityOutcome_StoresPathCounts()
{
// Arrange & Act
var outcome = new ReachabilityOutcome(
PrePathCount: 5,
PostPathCount: 0,
Eliminated: true,
Reason: "All paths eliminated");
// Assert
outcome.PrePathCount.Should().Be(5);
outcome.PostPathCount.Should().Be(0);
outcome.Eliminated.Should().BeTrue();
outcome.Reason.Should().Be("All paths eliminated");
}
[Fact]
public void AnalyzerMetadata_StoresAllProperties()
{
// Arrange & Act
var metadata = new AnalyzerMetadata(
"StellaOps.BinaryIndex",
"1.0.0",
"sha256:sourcedigest");
// Assert
metadata.Name.Should().Be("StellaOps.BinaryIndex");
metadata.Version.Should().Be("1.0.0");
metadata.SourceDigest.Should().Be("sha256:sourcedigest");
}
private static FixChainPredicate CreateValidPredicate()
{
return new FixChainPredicate
{
CveId = "CVE-2024-1234",
Component = "openssl",
GoldenSetRef = new ContentRef("sha256:goldenset123"),
SbomRef = new ContentRef("sha256:sbom456"),
VulnerableBinary = new BinaryRef(
new string('a', 64),
"x86_64",
"build-pre",
null),
PatchedBinary = new BinaryRef(
new string('b', 64),
"x86_64",
"build-post",
"pkg:generic/openssl@3.0.1"),
SignatureDiff = new SignatureDiffSummary(1, 2, 3, 0, []),
Reachability = new ReachabilityOutcome(5, 0, true, "All paths eliminated"),
Verdict = new FixChainVerdict(FixChainVerdict.StatusFixed, 0.95m, ["Vulnerability fixed"]),
Analyzer = new AnalyzerMetadata("Test", "1.0.0", "sha256:test"),
AnalyzedAt = DateTimeOffset.UtcNow
};
}
}

View File

@@ -0,0 +1,305 @@
// 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 Moq;
using Xunit;
namespace StellaOps.Attestor.FixChain.Tests.Unit;
[Trait("Category", "Unit")]
public sealed class FixChainStatementBuilderTests
{
private readonly FixChainStatementBuilder _builder;
private readonly Mock<TimeProvider> _timeProvider;
private readonly DateTimeOffset _fixedTime = new(2026, 1, 10, 12, 0, 0, TimeSpan.Zero);
public FixChainStatementBuilderTests()
{
_timeProvider = new Mock<TimeProvider>();
_timeProvider.Setup(t => t.GetUtcNow()).Returns(_fixedTime);
var options = Options.Create(new FixChainOptions
{
AnalyzerName = "TestAnalyzer",
AnalyzerVersion = "1.0.0",
AnalyzerSourceDigest = "sha256:testsource"
});
_builder = new FixChainStatementBuilder(
_timeProvider.Object,
options,
NullLogger<FixChainStatementBuilder>.Instance);
}
[Fact]
public async Task BuildAsync_CreatesValidStatement()
{
// Arrange
var request = CreateValidRequest();
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Should().NotBeNull();
result.Statement.Should().NotBeNull();
result.Predicate.Should().NotBeNull();
result.ContentDigest.Should().NotBeNullOrEmpty();
result.ContentDigest.Should().HaveLength(64); // SHA-256 hex
}
[Fact]
public async Task BuildAsync_SetsCorrectCveAndComponent()
{
// Arrange
var request = CreateValidRequest();
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Predicate.CveId.Should().Be("CVE-2024-1234");
result.Predicate.Component.Should().Be("openssl");
}
[Fact]
public async Task BuildAsync_FormatsDigestsWithPrefix()
{
// Arrange
var request = CreateValidRequest();
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Predicate.GoldenSetRef.Digest.Should().StartWith("sha256:");
result.Predicate.SbomRef.Digest.Should().StartWith("sha256:");
}
[Fact]
public async Task BuildAsync_SetsBinaryReferences()
{
// Arrange
var request = CreateValidRequest();
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Predicate.VulnerableBinary.Sha256.Should().Be(request.VulnerableBinary.Sha256);
result.Predicate.VulnerableBinary.Architecture.Should().Be("x86_64");
result.Predicate.PatchedBinary.Sha256.Should().Be(request.PatchedBinary.Sha256);
result.Predicate.PatchedBinary.Purl.Should().Be(request.ComponentPurl);
}
[Fact]
public async Task BuildAsync_SetsAnalyzerMetadata()
{
// Arrange
var request = CreateValidRequest();
// 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:testsource");
}
[Fact]
public async Task BuildAsync_SetsAnalyzedAtTimestamp()
{
// Arrange
var request = CreateValidRequest();
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Predicate.AnalyzedAt.Should().Be(_fixedTime);
}
[Fact]
public async Task BuildAsync_BuildsSignatureDiffSummary()
{
// Arrange
var request = CreateValidRequest();
request = request with
{
DiffResult = request.DiffResult with
{
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_BuildsReachabilityOutcome()
{
// Arrange
var request = CreateValidRequest();
request = request with
{
DiffResult = request.DiffResult with
{
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();
}
[Theory]
[InlineData("Fixed", 0.90, "fixed")]
[InlineData("PartialFix", 0.70, "partial")]
[InlineData("StillVulnerable", 0.20, "not_fixed")]
[InlineData("Inconclusive", 0.30, "inconclusive")]
public async Task BuildAsync_SetsCorrectVerdictStatus(string inputVerdict, decimal confidence, string expectedStatus)
{
// Arrange
var request = CreateValidRequest();
request = request with
{
DiffResult = request.DiffResult with
{
Verdict = inputVerdict,
Confidence = confidence
}
};
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Predicate.Verdict.Status.Should().Be(expectedStatus);
}
[Fact]
public async Task BuildAsync_IncludesRationaleForFunctionsRemoved()
{
// Arrange
var request = CreateValidRequest();
request = request with
{
DiffResult = request.DiffResult with
{
FunctionsRemoved = 2
}
};
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Predicate.Verdict.Rationale.Should().Contain(r => r.Contains("2") && r.Contains("removed"));
}
[Fact]
public async Task BuildAsync_IncludesRationaleForPathsEliminated()
{
// Arrange
var request = CreateValidRequest();
request = request with
{
DiffResult = request.DiffResult with
{
PrePathCount = 5,
PostPathCount = 0
}
};
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Predicate.Verdict.Rationale.Should().Contain(r => r.Contains("path") && r.Contains("eliminated"));
}
[Fact]
public async Task BuildAsync_SetsStatementSubject()
{
// Arrange
var request = CreateValidRequest();
// Act
var result = await _builder.BuildAsync(request);
// Assert
result.Statement.Subject.Should().HaveCount(1);
result.Statement.Subject[0].Name.Should().Be(request.ComponentPurl);
result.Statement.Subject[0].Digest["sha256"].Should().Be(request.PatchedBinary.Sha256);
}
[Fact]
public async Task BuildAsync_ContentDigestIsDeterministic()
{
// Arrange
var request = CreateValidRequest();
// Act
var result1 = await _builder.BuildAsync(request);
var result2 = await _builder.BuildAsync(request);
// Assert
result1.ContentDigest.Should().Be(result2.ContentDigest);
}
private static FixChainBuildRequest CreateValidRequest()
{
return new FixChainBuildRequest
{
CveId = "CVE-2024-1234",
Component = "openssl",
GoldenSetDigest = "goldenset123",
SbomDigest = "sbom456",
ComponentPurl = "pkg:generic/openssl@3.0.1",
VulnerableBinary = new BinaryIdentity
{
Sha256 = new string('a', 64),
Architecture = "x86_64",
BuildId = "build-pre"
},
PatchedBinary = new BinaryIdentity
{
Sha256 = new string('b', 64),
Architecture = "x86_64",
BuildId = "build-post"
},
DiffResult = new PatchDiffInput
{
Verdict = "Fixed",
Confidence = 0.95m,
FunctionsRemoved = 1,
FunctionsModified = 0,
EdgesEliminated = 3,
TaintGatesAdded = 0,
PrePathCount = 5,
PostPathCount = 0,
Evidence = ["Edge bb0->bb1 eliminated"]
}
};
}
}

View File

@@ -0,0 +1,310 @@
// 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;
[Trait("Category", "Unit")]
public sealed class FixChainValidatorTests
{
private readonly FixChainValidator _validator = new();
[Fact]
public void Validate_ValidPredicate_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_MissingCveId_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_InvalidCveIdFormat_ReturnsError()
{
// Arrange
var predicate = CreateValidPredicate() with { CveId = "INVALID-1234" };
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("CVE-"));
}
[Fact]
public void Validate_MissingComponent_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_MissingGoldenSetDigest_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_InvalidDigestFormat_ReturnsError()
{
// Arrange
var predicate = CreateValidPredicate() with
{
GoldenSetRef = new ContentRef("invaliddigest")
};
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("algorithm"));
}
[Fact]
public void Validate_InvalidBinarySha256Length_ReturnsError()
{
// Arrange
var predicate = CreateValidPredicate() with
{
VulnerableBinary = new BinaryRef("short", "x86_64", null, null)
};
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("sha256") && e.Contains("64"));
}
[Fact]
public void Validate_MissingArchitecture_ReturnsError()
{
// Arrange
var predicate = CreateValidPredicate() with
{
PatchedBinary = new BinaryRef(new string('a', 64), "", null, null)
};
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("architecture"));
}
[Fact]
public void Validate_InvalidVerdictStatus_ReturnsError()
{
// Arrange
var predicate = CreateValidPredicate() with
{
Verdict = new FixChainVerdict("invalid_status", 0.9m, [])
};
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("status"));
}
[Fact]
public void Validate_InvalidConfidence_ReturnsError()
{
// Arrange
var predicate = CreateValidPredicate() with
{
Verdict = new FixChainVerdict(FixChainVerdict.StatusFixed, 1.5m, [])
};
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("confidence"));
}
[Fact]
public void Validate_MissingAnalyzerName_ReturnsError()
{
// Arrange
var predicate = CreateValidPredicate() with
{
Analyzer = new AnalyzerMetadata("", "1.0.0", "sha256:source")
};
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("analyzer.name"));
}
[Fact]
public void Validate_DefaultTimestamp_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_ValidJson_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_InvalidJson_ReturnsError()
{
// Arrange
var json = JsonDocument.Parse("{}").RootElement;
// Act
var result = _validator.ValidateJson(json);
// Assert
result.IsValid.Should().BeFalse();
}
[Fact]
public void ValidateJson_MalformedJson_ReturnsParseError()
{
// Arrange
var json = JsonDocument.Parse("{\"cveId\": 12345}").RootElement;
// Act
var result = _validator.ValidateJson(json);
// Assert
result.IsValid.Should().BeFalse();
}
[Theory]
[InlineData(FixChainVerdict.StatusFixed)]
[InlineData(FixChainVerdict.StatusPartial)]
[InlineData(FixChainVerdict.StatusNotFixed)]
[InlineData(FixChainVerdict.StatusInconclusive)]
public void Validate_AllValidStatusValues_AreAccepted(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_MultipleErrors_ReturnsAll()
{
// Arrange
var predicate = CreateValidPredicate() with
{
CveId = "",
Component = "",
GoldenSetRef = new ContentRef("")
};
// Act
var result = _validator.Validate(predicate);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().HaveCountGreaterThan(1);
}
private static FixChainPredicate CreateValidPredicate()
{
return new FixChainPredicate
{
CveId = "CVE-2024-1234",
Component = "openssl",
GoldenSetRef = new ContentRef("sha256:goldenset123"),
SbomRef = new ContentRef("sha256:sbom456"),
VulnerableBinary = new BinaryRef(
new string('a', 64),
"x86_64",
"build-pre",
null),
PatchedBinary = new BinaryRef(
new string('b', 64),
"x86_64",
"build-post",
"pkg:generic/openssl@3.0.1"),
SignatureDiff = new SignatureDiffSummary(1, 2, 3, 0, []),
Reachability = new ReachabilityOutcome(5, 0, true, "All paths eliminated"),
Verdict = new FixChainVerdict(FixChainVerdict.StatusFixed, 0.95m, ["Vulnerability fixed"]),
Analyzer = new AnalyzerMetadata("Test", "1.0.0", "sha256:test"),
AnalyzedAt = DateTimeOffset.UtcNow
};
}
}