feat(trust-lattice): complete Sprint 7100 VEX Trust Lattice implementation
Sprint 7100 - VEX Trust Lattice for Explainable, Replayable Decisioning Completed all 6 sprints (54 tasks): - 7100.0001.0001: Trust Vector Foundation (TrustVector P/C/R, ClaimScoreCalculator) - 7100.0001.0002: Verdict Manifest & Replay (VerdictManifest, DSSE signing) - 7100.0002.0001: Policy Gates & Merge (MinimumConfidence, SourceQuota, UnknownsBudget) - 7100.0002.0002: Source Defaults & Calibration (DefaultTrustVectors, TrustCalibrationService) - 7100.0003.0001: UI Trust Algebra Panel (Angular components with WCAG 2.1 AA accessibility) - 7100.0003.0002: Integration & Documentation (specs, schemas, E2E tests, training docs) Key deliverables: - Trust vector model with P/C/R components and configurable weights - Claim scoring: ClaimScore = BaseTrust(S) * M * F - Policy gates for minimum confidence, source quotas, reachability requirements - Verdict manifests with DSSE signing and deterministic replay - Angular Trust Algebra UI with accessibility improvements - Comprehensive E2E integration tests (9 scenarios) - Full documentation and training materials 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Excititor: Trust vectors, claim scoring, calibration -->
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
|
||||
<ProjectReference Include="../../../Excititor/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
|
||||
<!-- Policy: Gates, merge, trust lattice engine -->
|
||||
<ProjectReference Include="../../../Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
|
||||
<!-- Authority: Verdict manifests, signing, replay -->
|
||||
<ProjectReference Include="../../../Authority/__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,648 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Authority.Core.Verdicts;
|
||||
using StellaOps.Excititor.Core;
|
||||
using StellaOps.Policy.Gates;
|
||||
using Xunit;
|
||||
|
||||
// Disambiguate types that exist in both Excititor.Core and Policy.TrustLattice
|
||||
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
|
||||
using ClaimScoreResult = StellaOps.Policy.TrustLattice.ClaimScoreResult;
|
||||
using MergeResult = StellaOps.Policy.TrustLattice.MergeResult;
|
||||
using ScoredClaim = StellaOps.Policy.TrustLattice.ScoredClaim;
|
||||
using ClaimScoreMerger = StellaOps.Policy.TrustLattice.ClaimScoreMerger;
|
||||
using MergePolicy = StellaOps.Policy.TrustLattice.MergePolicy;
|
||||
using VexClaim = StellaOps.Policy.TrustLattice.VexClaim;
|
||||
using ConflictRecord = StellaOps.Policy.TrustLattice.ConflictRecord;
|
||||
using AuthorityVexStatus = StellaOps.Authority.Core.Verdicts.VexStatus;
|
||||
|
||||
namespace StellaOps.Scanner.Integration.Tests.TrustLattice;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end integration tests for the Trust Lattice flow.
|
||||
/// Tests the full pipeline: VEX ingest -> score -> merge -> verdict -> sign -> replay
|
||||
/// </summary>
|
||||
public sealed class TrustLatticeE2ETests
|
||||
{
|
||||
private static readonly DateTimeOffset TestClock = DateTimeOffset.Parse("2025-01-15T12:00:00Z");
|
||||
private static readonly DateTimeOffset RecentClaim = DateTimeOffset.Parse("2025-01-10T00:00:00Z");
|
||||
private static readonly DateTimeOffset OldClaim = DateTimeOffset.Parse("2024-07-01T00:00:00Z");
|
||||
|
||||
#region Scenario 1: Single source, high confidence -> PASS
|
||||
|
||||
[Fact]
|
||||
public async Task SingleSource_HighConfidence_ShouldPass()
|
||||
{
|
||||
// Arrange: Single vendor VEX claim with high trust score
|
||||
var merger = new ClaimScoreMerger();
|
||||
var gates = CreatePolicyGates();
|
||||
|
||||
// Simulate high-confidence vendor claim (pre-scored)
|
||||
var claim = new VexClaim
|
||||
{
|
||||
SourceId = "vendor:acme",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 3,
|
||||
IssuedAt = RecentClaim,
|
||||
};
|
||||
var score = CreateScore(0.85, 0.82, 0.90, 0.95);
|
||||
|
||||
// Merge (single claim)
|
||||
var mergeResult = merger.Merge(
|
||||
new List<(VexClaim, ClaimScoreResult)> { (claim, score) },
|
||||
new MergePolicy());
|
||||
|
||||
// Evaluate gates
|
||||
var context = new PolicyGateContext { Environment = "production" };
|
||||
var gateResults = await EvaluateAllGatesAsync(gates, mergeResult, context);
|
||||
|
||||
// Assert
|
||||
mergeResult.Confidence.Should().BeGreaterThan(0.75);
|
||||
mergeResult.HasConflicts.Should().BeFalse();
|
||||
gateResults.Should().OnlyContain(r => r.Passed, "all gates should pass for high-confidence single source");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scenario 2: Multiple agreeing sources -> PASS with merged confidence
|
||||
|
||||
[Fact]
|
||||
public async Task MultipleAgreeingSources_ShouldPass()
|
||||
{
|
||||
// Arrange: Multiple sources agreeing on NotAffected
|
||||
var merger = new ClaimScoreMerger();
|
||||
var gates = CreatePolicyGates();
|
||||
|
||||
var claims = new List<(VexClaim, ClaimScoreResult)>
|
||||
{
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "vendor:acme",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 2,
|
||||
IssuedAt = RecentClaim,
|
||||
}, CreateScore(0.80, 0.78, 0.80, 0.95)),
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "distro:debian",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 3,
|
||||
IssuedAt = RecentClaim,
|
||||
}, CreateScore(0.78, 0.76, 0.80, 0.95)),
|
||||
};
|
||||
|
||||
// Merge
|
||||
var mergeResult = merger.Merge(claims, new MergePolicy());
|
||||
|
||||
// Evaluate gates
|
||||
var context = new PolicyGateContext { Environment = "production" };
|
||||
var gateResults = await EvaluateAllGatesAsync(gates, mergeResult, context);
|
||||
|
||||
// Assert
|
||||
mergeResult.HasConflicts.Should().BeFalse("agreeing sources should not conflict");
|
||||
mergeResult.Status.Should().Be(VexStatus.NotAffected);
|
||||
gateResults.Should().OnlyContain(r => r.Passed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scenario 3: Conflicting sources -> Conflict penalty applied
|
||||
|
||||
[Fact]
|
||||
public void ConflictingSources_ShouldApplyConflictPenalty()
|
||||
{
|
||||
// Arrange: Two sources with opposing statuses
|
||||
var merger = new ClaimScoreMerger();
|
||||
|
||||
var claims = new List<(VexClaim, ClaimScoreResult)>
|
||||
{
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "vendor:acme",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 2,
|
||||
IssuedAt = RecentClaim,
|
||||
}, CreateScore(0.80, 0.78, 0.80, 0.95)),
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "hub:osv",
|
||||
Status = VexStatus.Affected,
|
||||
ScopeSpecificity = 1,
|
||||
IssuedAt = RecentClaim,
|
||||
}, CreateScore(0.65, 0.60, 0.70, 0.90)),
|
||||
};
|
||||
|
||||
// Merge with conflict penalty
|
||||
var mergePolicy = new MergePolicy { ConflictPenalty = 0.25 };
|
||||
var mergeResult = merger.Merge(claims, mergePolicy);
|
||||
|
||||
// Assert
|
||||
mergeResult.HasConflicts.Should().BeTrue("opposing statuses should create conflict");
|
||||
mergeResult.Conflicts.Should().NotBeEmpty();
|
||||
mergeResult.RequiresReplayProof.Should().BeTrue("conflicts require replay proof");
|
||||
|
||||
// Higher-trust source (vendor) should win
|
||||
mergeResult.WinningClaim.SourceId.Should().Be("vendor:acme");
|
||||
mergeResult.Status.Should().Be(VexStatus.NotAffected);
|
||||
|
||||
// Loser should have penalty applied
|
||||
var losingClaim = mergeResult.AllClaims.First(c => c.SourceId == "hub:osv");
|
||||
losingClaim.AdjustedScore.Should().BeLessThan(losingClaim.OriginalScore);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scenario 4: Below minimum confidence -> FAIL gate
|
||||
|
||||
[Fact]
|
||||
public async Task BelowMinimumConfidence_ShouldFailGate()
|
||||
{
|
||||
// Arrange: Low-confidence claim
|
||||
var merger = new ClaimScoreMerger();
|
||||
|
||||
var claim = new VexClaim
|
||||
{
|
||||
SourceId = "hub:community",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 1,
|
||||
IssuedAt = OldClaim,
|
||||
};
|
||||
// Low score due to low trust and old claim
|
||||
var score = CreateScore(0.35, 0.40, 0.50, 0.50);
|
||||
|
||||
var mergeResult = merger.Merge(
|
||||
new List<(VexClaim, ClaimScoreResult)> { (claim, score) },
|
||||
new MergePolicy());
|
||||
|
||||
// Evaluate MinimumConfidenceGate
|
||||
var context = new PolicyGateContext { Environment = "production" };
|
||||
var gate = new MinimumConfidenceGate();
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
mergeResult.Confidence.Should().BeLessThan(0.75, "low trust + old claim should have low confidence");
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("confidence_below_threshold");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scenario 5: Source quota exceeded -> FAIL gate (no corroboration)
|
||||
|
||||
[Fact]
|
||||
public async Task SourceQuotaExceeded_ShouldFailGate()
|
||||
{
|
||||
// Arrange: Single dominant source without corroboration
|
||||
var winner = new ScoredClaim
|
||||
{
|
||||
SourceId = "vendor:monopoly",
|
||||
Status = VexStatus.NotAffected,
|
||||
OriginalScore = 0.95,
|
||||
AdjustedScore = 0.95,
|
||||
ScopeSpecificity = 3,
|
||||
Accepted = true,
|
||||
Reason = "winner",
|
||||
};
|
||||
|
||||
var weak = new ScoredClaim
|
||||
{
|
||||
SourceId = "hub:weak",
|
||||
Status = VexStatus.NotAffected,
|
||||
OriginalScore = 0.05,
|
||||
AdjustedScore = 0.05,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = false,
|
||||
Reason = "low_score",
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.95,
|
||||
HasConflicts = false,
|
||||
RequiresReplayProof = false,
|
||||
WinningClaim = winner,
|
||||
AllClaims = ImmutableArray.Create(winner, weak),
|
||||
Conflicts = ImmutableArray<ConflictRecord>.Empty,
|
||||
};
|
||||
|
||||
// Evaluate SourceQuotaGate (requires corroboration within 10% delta)
|
||||
var gate = new SourceQuotaGate(new SourceQuotaGateOptions
|
||||
{
|
||||
MaxInfluencePercent = 60,
|
||||
CorroborationDelta = 0.10,
|
||||
});
|
||||
var result = await gate.EvaluateAsync(mergeResult, new PolicyGateContext());
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeFalse("single source with >60% influence without corroboration should fail");
|
||||
result.Reason.Should().Be("source_quota_exceeded");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scenario 6: Critical CVE without reachability -> FAIL gate
|
||||
|
||||
[Fact]
|
||||
public async Task CriticalCveWithoutReachability_ShouldFailGate()
|
||||
{
|
||||
// Arrange: High-confidence NotAffected claim but critical severity without proof
|
||||
var mergeResult = CreateHighConfidenceMergeResult(VexStatus.NotAffected, 0.90);
|
||||
|
||||
var gate = new ReachabilityRequirementGate();
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
Severity = "CRITICAL",
|
||||
HasReachabilityProof = false,
|
||||
};
|
||||
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeFalse("critical CVE without reachability proof should fail");
|
||||
result.Reason.Should().Be("reachability_proof_missing");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CriticalCveWithReachability_ShouldPassGate()
|
||||
{
|
||||
// Arrange: Same as above but with reachability proof
|
||||
var mergeResult = CreateHighConfidenceMergeResult(VexStatus.NotAffected, 0.90);
|
||||
|
||||
var gate = new ReachabilityRequirementGate();
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
Severity = "CRITICAL",
|
||||
HasReachabilityProof = true,
|
||||
};
|
||||
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scenario 7 & 8: Verdict replay verification
|
||||
|
||||
[Fact]
|
||||
public void VerdictReplay_IdenticalInputs_ShouldSucceed()
|
||||
{
|
||||
// Arrange: Build a verdict manifest
|
||||
var clock = DateTimeOffset.Parse("2025-01-15T12:00:00Z");
|
||||
var inputClock = DateTimeOffset.Parse("2025-01-15T00:00:00Z");
|
||||
|
||||
var manifest = BuildVerdictManifest(clock, inputClock, VexStatus.NotAffected, 0.85);
|
||||
|
||||
// Rebuild with same inputs (simulating replay)
|
||||
var replayedManifest = BuildVerdictManifest(clock, inputClock, VexStatus.NotAffected, 0.85);
|
||||
|
||||
// Assert: Digests should match (deterministic)
|
||||
replayedManifest.ManifestDigest.Should().Be(manifest.ManifestDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictReplay_ChangedInputs_ShouldProduceDifferentDigest()
|
||||
{
|
||||
// Arrange: Build original verdict
|
||||
var clock = DateTimeOffset.Parse("2025-01-15T12:00:00Z");
|
||||
var inputClock = DateTimeOffset.Parse("2025-01-15T00:00:00Z");
|
||||
|
||||
var originalManifest = BuildVerdictManifest(clock, inputClock, VexStatus.NotAffected, 0.85);
|
||||
|
||||
// Rebuild with different VEX document (simulating changed input)
|
||||
var changedManifest = new VerdictManifestBuilder(() => "manifest-1")
|
||||
.WithTenant("tenant-1")
|
||||
.WithAsset("sha256:asset123", "CVE-2025-1234")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:sbom1" },
|
||||
vulnFeedSnapshotIds: new[] { "feed-1" },
|
||||
vexDocumentDigests: new[] { "sha256:vex1", "sha256:vex2-NEW" }, // Changed!
|
||||
clockCutoff: inputClock)
|
||||
.WithResult(
|
||||
status: VexStatus.NotAffected,
|
||||
confidence: 0.85,
|
||||
explanations: CreateExplanations())
|
||||
.WithPolicy("sha256:policy1", "1.0.0")
|
||||
.WithClock(clock)
|
||||
.Build();
|
||||
|
||||
// Assert: Digests should NOT match
|
||||
changedManifest.ManifestDigest.Should().NotBe(originalManifest.ManifestDigest);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scenario 9: Calibration epoch adjustments
|
||||
|
||||
[Fact]
|
||||
public void CalibrationEpoch_ShouldAdjustTrustVector()
|
||||
{
|
||||
// Arrange: Initial trust vector and calibration config
|
||||
var initialVector = new Excititor.Core.TrustVector
|
||||
{
|
||||
Provenance = 0.80,
|
||||
Coverage = 0.75,
|
||||
Replayability = 0.60,
|
||||
};
|
||||
|
||||
var calibrator = new TrustVectorCalibrator
|
||||
{
|
||||
LearningRate = 0.02,
|
||||
MaxAdjustmentPerEpoch = 0.05,
|
||||
MinValue = 0.10,
|
||||
MaxValue = 1.00,
|
||||
};
|
||||
|
||||
// Simulate comparison result showing optimistic bias
|
||||
var comparisonResult = new ComparisonResult
|
||||
{
|
||||
SourceId = "vendor:test",
|
||||
Accuracy = 0.85,
|
||||
TotalPredictions = 100,
|
||||
CorrectPredictions = 85,
|
||||
FalsePositives = 5,
|
||||
FalseNegatives = 10,
|
||||
ConfidenceInterval = 0.07,
|
||||
DetectedBias = CalibrationBias.OptimisticBias,
|
||||
};
|
||||
|
||||
// Calibrate
|
||||
var calibratedVector = calibrator.Calibrate(initialVector, comparisonResult, comparisonResult.DetectedBias);
|
||||
|
||||
// Assert: Provenance should decrease due to optimistic bias
|
||||
calibratedVector.Provenance.Should().BeLessThan(initialVector.Provenance);
|
||||
|
||||
// Values should stay within bounds
|
||||
calibratedVector.Provenance.Should().BeGreaterThanOrEqualTo(0.10);
|
||||
calibratedVector.Provenance.Should().BeLessThanOrEqualTo(1.00);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalibrationEpoch_HighAccuracy_ShouldNotAdjust()
|
||||
{
|
||||
// Arrange: High accuracy source shouldn't need adjustment
|
||||
var initialVector = new Excititor.Core.TrustVector
|
||||
{
|
||||
Provenance = 0.90,
|
||||
Coverage = 0.85,
|
||||
Replayability = 0.70,
|
||||
};
|
||||
|
||||
var calibrator = new TrustVectorCalibrator
|
||||
{
|
||||
LearningRate = 0.02,
|
||||
MaxAdjustmentPerEpoch = 0.05,
|
||||
};
|
||||
|
||||
var comparisonResult = new ComparisonResult
|
||||
{
|
||||
SourceId = "vendor:excellent",
|
||||
Accuracy = 0.98,
|
||||
TotalPredictions = 100,
|
||||
CorrectPredictions = 98,
|
||||
FalsePositives = 1,
|
||||
FalseNegatives = 1,
|
||||
ConfidenceInterval = 0.03,
|
||||
DetectedBias = CalibrationBias.None,
|
||||
};
|
||||
|
||||
// Calibrate - high accuracy (>= 0.95) should result in no adjustment
|
||||
var calibratedVector = calibrator.Calibrate(initialVector, comparisonResult, comparisonResult.DetectedBias);
|
||||
|
||||
// Assert: Should remain unchanged (above threshold)
|
||||
calibratedVector.Provenance.Should().Be(initialVector.Provenance);
|
||||
calibratedVector.Coverage.Should().Be(initialVector.Coverage);
|
||||
calibratedVector.Replayability.Should().Be(initialVector.Replayability);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Flow Integration Test
|
||||
|
||||
[Fact]
|
||||
public async Task FullFlow_MergeToVerdict_ShouldProduceDeterministicResult()
|
||||
{
|
||||
// This test validates the complete merge-to-verdict flow:
|
||||
// Scored claims -> Merge -> Gate evaluation -> Verdict manifest
|
||||
|
||||
var merger = new ClaimScoreMerger();
|
||||
var gates = CreatePolicyGates();
|
||||
|
||||
// Pre-scored claims (simulating Excititor scoring output)
|
||||
var claims = new List<(VexClaim, ClaimScoreResult)>
|
||||
{
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "vendor:acme",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 3,
|
||||
IssuedAt = RecentClaim,
|
||||
}, CreateScore(0.85, 0.82, 0.90, 0.95)),
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "distro:debian",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 3,
|
||||
IssuedAt = RecentClaim,
|
||||
}, CreateScore(0.80, 0.78, 0.80, 0.95)),
|
||||
};
|
||||
|
||||
// Merge claims
|
||||
var mergeResult = merger.Merge(claims, new MergePolicy { ConflictPenalty = 0.25 });
|
||||
|
||||
// Evaluate gates
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
Environment = "production",
|
||||
Severity = "HIGH",
|
||||
HasReachabilityProof = true,
|
||||
};
|
||||
var gateResults = await EvaluateAllGatesAsync(gates, mergeResult, context);
|
||||
var allPassed = gateResults.All(r => r.Passed);
|
||||
|
||||
// Build verdict manifest
|
||||
var manifest = new VerdictManifestBuilder(() => "verd:tenant:asset:CVE-2025-1234:1705323600")
|
||||
.WithTenant("tenant-1")
|
||||
.WithAsset("sha256:asset123", "CVE-2025-1234")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:sbom1" },
|
||||
vulnFeedSnapshotIds: new[] { "feed-snapshot-1" },
|
||||
vexDocumentDigests: new[] { "sha256:vex-vendor", "sha256:vex-distro" },
|
||||
clockCutoff: TestClock)
|
||||
.WithResult(
|
||||
status: mergeResult.Status,
|
||||
confidence: mergeResult.Confidence,
|
||||
explanations: mergeResult.AllClaims.Select(c => new VerdictExplanation
|
||||
{
|
||||
SourceId = c.SourceId,
|
||||
Reason = c.Reason,
|
||||
ProvenanceScore = 0.85,
|
||||
CoverageScore = 0.80,
|
||||
ReplayabilityScore = 0.70,
|
||||
StrengthMultiplier = 0.90,
|
||||
FreshnessMultiplier = 0.95,
|
||||
ClaimScore = c.AdjustedScore,
|
||||
AssertedStatus = c.Status,
|
||||
Accepted = c.Accepted,
|
||||
}))
|
||||
.WithPolicy("sha256:policy123", "1.0.0")
|
||||
.WithClock(TestClock)
|
||||
.Build();
|
||||
|
||||
// Verify replay determinism
|
||||
var replayManifest = new VerdictManifestBuilder(() => "verd:tenant:asset:CVE-2025-1234:1705323600")
|
||||
.WithTenant("tenant-1")
|
||||
.WithAsset("sha256:asset123", "CVE-2025-1234")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:sbom1" },
|
||||
vulnFeedSnapshotIds: new[] { "feed-snapshot-1" },
|
||||
vexDocumentDigests: new[] { "sha256:vex-vendor", "sha256:vex-distro" },
|
||||
clockCutoff: TestClock)
|
||||
.WithResult(
|
||||
status: mergeResult.Status,
|
||||
confidence: mergeResult.Confidence,
|
||||
explanations: mergeResult.AllClaims.Select(c => new VerdictExplanation
|
||||
{
|
||||
SourceId = c.SourceId,
|
||||
Reason = c.Reason,
|
||||
ProvenanceScore = 0.85,
|
||||
CoverageScore = 0.80,
|
||||
ReplayabilityScore = 0.70,
|
||||
StrengthMultiplier = 0.90,
|
||||
FreshnessMultiplier = 0.95,
|
||||
ClaimScore = c.AdjustedScore,
|
||||
AssertedStatus = c.Status,
|
||||
Accepted = c.Accepted,
|
||||
}))
|
||||
.WithPolicy("sha256:policy123", "1.0.0")
|
||||
.WithClock(TestClock)
|
||||
.Build();
|
||||
|
||||
// Assertions
|
||||
mergeResult.Status.Should().Be(VexStatus.NotAffected);
|
||||
mergeResult.HasConflicts.Should().BeFalse();
|
||||
allPassed.Should().BeTrue("all gates should pass for corroborated high-confidence claims");
|
||||
manifest.ManifestDigest.Should().StartWith("sha256:");
|
||||
manifest.ManifestDigest.Should().Be(replayManifest.ManifestDigest, "replay should be deterministic");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static ClaimScoreResult CreateScore(double score, double baseTrust, double strength, double freshness)
|
||||
{
|
||||
return new ClaimScoreResult
|
||||
{
|
||||
Score = score,
|
||||
BaseTrust = baseTrust,
|
||||
StrengthMultiplier = strength,
|
||||
FreshnessMultiplier = freshness,
|
||||
};
|
||||
}
|
||||
|
||||
private static List<IPolicyGate> CreatePolicyGates()
|
||||
{
|
||||
return new List<IPolicyGate>
|
||||
{
|
||||
new MinimumConfidenceGate(),
|
||||
new UnknownsBudgetGate(new UnknownsBudgetGateOptions { MaxUnknownCount = 5, MaxCumulativeUncertainty = 1.0 }),
|
||||
new SourceQuotaGate(new SourceQuotaGateOptions { MaxInfluencePercent = 80, CorroborationDelta = 0.15 }),
|
||||
new ReachabilityRequirementGate(),
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<List<GateResult>> EvaluateAllGatesAsync(
|
||||
List<IPolicyGate> gates,
|
||||
MergeResult mergeResult,
|
||||
PolicyGateContext context)
|
||||
{
|
||||
var results = new List<GateResult>();
|
||||
foreach (var gate in gates)
|
||||
{
|
||||
results.Add(await gate.EvaluateAsync(mergeResult, context));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static MergeResult CreateHighConfidenceMergeResult(VexStatus status, double confidence)
|
||||
{
|
||||
var winner = new ScoredClaim
|
||||
{
|
||||
SourceId = "vendor:trusted",
|
||||
Status = status,
|
||||
OriginalScore = confidence,
|
||||
AdjustedScore = confidence,
|
||||
ScopeSpecificity = 3,
|
||||
Accepted = true,
|
||||
Reason = "winner",
|
||||
};
|
||||
|
||||
return new MergeResult
|
||||
{
|
||||
Status = status,
|
||||
Confidence = confidence,
|
||||
HasConflicts = false,
|
||||
RequiresReplayProof = false,
|
||||
WinningClaim = winner,
|
||||
AllClaims = ImmutableArray.Create(winner),
|
||||
Conflicts = ImmutableArray<ConflictRecord>.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private static VerdictManifest BuildVerdictManifest(
|
||||
DateTimeOffset clock,
|
||||
DateTimeOffset inputClock,
|
||||
VexStatus status,
|
||||
double confidence)
|
||||
{
|
||||
return new VerdictManifestBuilder(() => "manifest-1")
|
||||
.WithTenant("tenant-1")
|
||||
.WithAsset("sha256:asset123", "CVE-2025-1234")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:sbom1" },
|
||||
vulnFeedSnapshotIds: new[] { "feed-1" },
|
||||
vexDocumentDigests: new[] { "sha256:vex1" },
|
||||
clockCutoff: inputClock)
|
||||
.WithResult(
|
||||
status: ToAuthorityStatus(status),
|
||||
confidence: confidence,
|
||||
explanations: CreateExplanations())
|
||||
.WithPolicy("sha256:policy1", "1.0.0")
|
||||
.WithClock(clock)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static AuthorityVexStatus ToAuthorityStatus(VexStatus status) => status switch
|
||||
{
|
||||
VexStatus.Affected => AuthorityVexStatus.Affected,
|
||||
VexStatus.NotAffected => AuthorityVexStatus.NotAffected,
|
||||
VexStatus.Fixed => AuthorityVexStatus.Fixed,
|
||||
VexStatus.UnderInvestigation => AuthorityVexStatus.UnderInvestigation,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status)),
|
||||
};
|
||||
|
||||
private static IEnumerable<VerdictExplanation> CreateExplanations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new VerdictExplanation
|
||||
{
|
||||
SourceId = "vendor:test",
|
||||
Reason = "Official VEX",
|
||||
ProvenanceScore = 0.90,
|
||||
CoverageScore = 0.85,
|
||||
ReplayabilityScore = 0.70,
|
||||
StrengthMultiplier = 0.90,
|
||||
FreshnessMultiplier = 0.95,
|
||||
ClaimScore = 0.85,
|
||||
AssertedStatus = AuthorityVexStatus.NotAffected,
|
||||
Accepted = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user