Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

@@ -0,0 +1,97 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Calibration;
public sealed class CalibrationComparisonEngineTests
{
[Fact]
public async Task CompareAsync_ComputesAccuracyAndBias()
{
var dataset = new FakeDatasetProvider();
var engine = new CalibrationComparisonEngine(dataset);
var results = await engine.CompareAsync("tenant-a",
DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
DateTimeOffset.Parse("2025-02-01T00:00:00Z"));
results.Should().HaveCount(2);
results[0].SourceId.Should().Be("source-a");
results[0].FalseNegatives.Should().Be(2);
results[0].DetectedBias.Should().Be(CalibrationBias.OptimisticBias);
results[1].SourceId.Should().Be("source-b");
results[1].FalsePositives.Should().Be(2);
results[1].DetectedBias.Should().Be(CalibrationBias.PessimisticBias);
}
private sealed class FakeDatasetProvider : ICalibrationDatasetProvider
{
public Task<IReadOnlyList<CalibrationObservation>> GetObservationsAsync(
string tenant,
DateTimeOffset epochStart,
DateTimeOffset epochEnd,
CancellationToken ct = default)
{
var observations = new List<CalibrationObservation>
{
new()
{
SourceId = "source-a",
VulnerabilityId = "CVE-1",
AssetDigest = "sha256:asset1",
Status = VexClaimStatus.NotAffected,
},
new()
{
SourceId = "source-a",
VulnerabilityId = "CVE-2",
AssetDigest = "sha256:asset2",
Status = VexClaimStatus.NotAffected,
},
new()
{
SourceId = "source-a",
VulnerabilityId = "CVE-3",
AssetDigest = "sha256:asset3",
Status = VexClaimStatus.Affected,
},
new()
{
SourceId = "source-b",
VulnerabilityId = "CVE-4",
AssetDigest = "sha256:asset4",
Status = VexClaimStatus.Affected,
},
new()
{
SourceId = "source-b",
VulnerabilityId = "CVE-5",
AssetDigest = "sha256:asset5",
Status = VexClaimStatus.Affected,
},
};
return Task.FromResult<IReadOnlyList<CalibrationObservation>>(observations);
}
public Task<IReadOnlyList<CalibrationTruth>> GetTruthAsync(
string tenant,
DateTimeOffset epochStart,
DateTimeOffset epochEnd,
CancellationToken ct = default)
{
var truths = new List<CalibrationTruth>
{
new() { VulnerabilityId = "CVE-1", AssetDigest = "sha256:asset1", Status = VexClaimStatus.Affected },
new() { VulnerabilityId = "CVE-2", AssetDigest = "sha256:asset2", Status = VexClaimStatus.Affected },
new() { VulnerabilityId = "CVE-3", AssetDigest = "sha256:asset3", Status = VexClaimStatus.Affected },
new() { VulnerabilityId = "CVE-4", AssetDigest = "sha256:asset4", Status = VexClaimStatus.NotAffected },
new() { VulnerabilityId = "CVE-5", AssetDigest = "sha256:asset5", Status = VexClaimStatus.NotAffected },
};
return Task.FromResult<IReadOnlyList<CalibrationTruth>>(truths);
}
}
}

View File

@@ -0,0 +1,33 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Calibration;
public sealed class DefaultTrustVectorsTests
{
[Fact]
public void DefaultTrustVectors_MatchSpecification()
{
DefaultTrustVectors.Vendor.Should().Be(new Core.TrustVector
{
Provenance = 0.90,
Coverage = 0.70,
Replayability = 0.60,
});
DefaultTrustVectors.Distro.Should().Be(new Core.TrustVector
{
Provenance = 0.80,
Coverage = 0.85,
Replayability = 0.60,
});
DefaultTrustVectors.Internal.Should().Be(new Core.TrustVector
{
Provenance = 0.85,
Coverage = 0.95,
Replayability = 0.90,
});
}
}

View File

@@ -0,0 +1,52 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Calibration;
public sealed class SourceClassificationServiceTests
{
[Fact]
public void Classification_UsesOverridesFirst()
{
var service = new SourceClassificationService();
service.RegisterOverride("vendor-*", VexProviderKind.Vendor);
var result = service.Classify("vendor-foo", "example.com", null, "csaf");
result.Kind.Should().Be(VexProviderKind.Vendor);
result.IsOverride.Should().BeTrue();
result.Confidence.Should().Be(1.0);
}
[Fact]
public void Classification_DetectsDistroDomains()
{
var service = new SourceClassificationService();
var result = service.Classify("ubuntu", "ubuntu.com", null, "csaf");
result.Kind.Should().Be(VexProviderKind.Distro);
result.Reason.Should().Contain("distro");
}
[Fact]
public void Classification_DetectsAttestations()
{
var service = new SourceClassificationService();
var result = service.Classify("sigstore", "example.org", "dsse", "oci_attestation");
result.Kind.Should().Be(VexProviderKind.Attestation);
}
[Fact]
public void Classification_FallsBackToHub()
{
var service = new SourceClassificationService();
var result = service.Classify("random", "unknown.example", null, "openvex");
result.Kind.Should().Be(VexProviderKind.Hub);
}
}

View File

@@ -0,0 +1,171 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Storage;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Calibration;
public sealed class TrustCalibrationServiceTests
{
[Fact]
public async Task RunEpochAsync_StoresManifestAndAdjustments()
{
var comparisonEngine = new FakeComparisonEngine();
var providerStore = new InMemoryProviderStore(new VexProvider(
"provider-a",
"Provider A",
VexProviderKind.Vendor,
Array.Empty<Uri>(),
VexProviderDiscovery.Empty,
new VexProviderTrust(1.0, cosign: null, vector: new Core.TrustVector
{
Provenance = 0.8,
Coverage = 0.7,
Replayability = 0.6,
}),
enabled: true));
var manifestStore = new InMemoryManifestStore();
var service = new TrustCalibrationService(
comparisonEngine,
new TrustVectorCalibrator { MomentumFactor = 0.0 },
providerStore,
manifestStore,
signer: new NullCalibrationManifestSigner(),
idGenerator: new FixedCalibrationIdGenerator("manifest-1"),
options: new TrustCalibrationOptions { EpochDuration = TimeSpan.FromDays(30) });
var manifest = await service.RunEpochAsync("tenant-a", DateTimeOffset.Parse("2025-02-01T00:00:00Z"));
manifest.ManifestId.Should().Be("manifest-1");
manifest.Adjustments.Should().HaveCount(1);
(await manifestStore.GetLatestAsync("tenant-a")).Should().NotBeNull();
}
[Fact]
public async Task ApplyCalibrationAsync_UpdatesProviderVectors()
{
var comparisonEngine = new FakeComparisonEngine();
var providerStore = new InMemoryProviderStore(new VexProvider(
"provider-a",
"Provider A",
VexProviderKind.Vendor,
Array.Empty<Uri>(),
VexProviderDiscovery.Empty,
new VexProviderTrust(1.0, cosign: null, vector: new Core.TrustVector
{
Provenance = 0.9,
Coverage = 0.9,
Replayability = 0.9,
}),
enabled: true));
var manifestStore = new InMemoryManifestStore();
var service = new TrustCalibrationService(
comparisonEngine,
new TrustVectorCalibrator { MomentumFactor = 0.0 },
providerStore,
manifestStore,
signer: new NullCalibrationManifestSigner(),
idGenerator: new FixedCalibrationIdGenerator("manifest-2"));
var manifest = await service.RunEpochAsync("tenant-a", DateTimeOffset.Parse("2025-02-01T00:00:00Z"));
await service.ApplyCalibrationAsync("tenant-a", manifest.ManifestId);
var updated = await providerStore.FindAsync("provider-a", CancellationToken.None);
updated!.Trust.Vector!.Provenance.Should().BeLessThan(0.9);
}
private sealed class FakeComparisonEngine : ICalibrationComparisonEngine
{
public Task<IReadOnlyList<ComparisonResult>> CompareAsync(
string tenant,
DateTimeOffset epochStart,
DateTimeOffset epochEnd,
CancellationToken ct = default)
{
IReadOnlyList<ComparisonResult> results = new[]
{
new ComparisonResult
{
SourceId = "provider-a",
TotalPredictions = 10,
CorrectPredictions = 5,
FalseNegatives = 2,
FalsePositives = 0,
Accuracy = 0.5,
ConfidenceInterval = 0.2,
DetectedBias = CalibrationBias.OptimisticBias,
}
};
return Task.FromResult(results);
}
}
private sealed class InMemoryManifestStore : ICalibrationManifestStore
{
private readonly List<CalibrationManifest> _manifests = new();
public Task StoreAsync(CalibrationManifest manifest, CancellationToken ct = default)
{
_manifests.Add(manifest);
return Task.CompletedTask;
}
public Task<CalibrationManifest?> GetByIdAsync(string tenant, string manifestId, CancellationToken ct = default)
{
var match = _manifests.LastOrDefault(m => m.Tenant == tenant && m.ManifestId == manifestId);
return Task.FromResult(match);
}
public Task<CalibrationManifest?> GetLatestAsync(string tenant, CancellationToken ct = default)
{
var match = _manifests
.Where(m => m.Tenant == tenant)
.OrderByDescending(m => m.EpochNumber)
.FirstOrDefault();
return Task.FromResult(match);
}
}
private sealed class InMemoryProviderStore : IVexProviderStore
{
private readonly Dictionary<string, VexProvider> _providers;
public InMemoryProviderStore(params VexProvider[] providers)
{
_providers = providers.ToDictionary(p => p.Id, StringComparer.Ordinal);
}
public ValueTask<VexProvider?> FindAsync(string id, CancellationToken cancellationToken)
{
_providers.TryGetValue(id, out var provider);
return ValueTask.FromResult(provider);
}
public ValueTask SaveAsync(VexProvider provider, CancellationToken cancellationToken)
{
_providers[provider.Id] = provider;
return ValueTask.CompletedTask;
}
public ValueTask<IReadOnlyCollection<VexProvider>> ListAsync(CancellationToken cancellationToken)
{
IReadOnlyCollection<VexProvider> values = _providers.Values.ToArray();
return ValueTask.FromResult(values);
}
}
private sealed class FixedCalibrationIdGenerator : ICalibrationIdGenerator
{
private readonly string _value;
public FixedCalibrationIdGenerator(string value)
{
_value = value;
}
public string NextId() => _value;
}
}

View File

@@ -0,0 +1,105 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Calibration;
public sealed class TrustVectorCalibratorTests
{
[Fact]
public void Calibrate_NoChangeWhenAccuracyHigh()
{
var calibrator = new TrustVectorCalibrator();
var current = new Core.TrustVector
{
Provenance = 0.8,
Coverage = 0.8,
Replayability = 0.8,
};
var comparison = new ComparisonResult
{
SourceId = "source",
TotalPredictions = 100,
CorrectPredictions = 98,
FalseNegatives = 1,
FalsePositives = 1,
Accuracy = 0.98,
ConfidenceInterval = 0.01,
DetectedBias = CalibrationBias.None,
};
calibrator.Calibrate(current, comparison, comparison.DetectedBias).Should().Be(current);
}
[Fact]
public void Calibrate_AdjustsWithinBounds()
{
var calibrator = new TrustVectorCalibrator
{
LearningRate = 0.02,
MaxAdjustmentPerEpoch = 0.05,
MinValue = 0.1,
MaxValue = 1.0,
MomentumFactor = 0.0,
};
var current = new Core.TrustVector
{
Provenance = 0.2,
Coverage = 0.5,
Replayability = 0.7,
};
var comparison = new ComparisonResult
{
SourceId = "source",
TotalPredictions = 10,
CorrectPredictions = 5,
FalseNegatives = 2,
FalsePositives = 0,
Accuracy = 0.5,
ConfidenceInterval = 0.2,
DetectedBias = CalibrationBias.OptimisticBias,
};
var updated = calibrator.Calibrate(current, comparison, comparison.DetectedBias);
updated.Provenance.Should().BeLessThan(current.Provenance);
updated.Coverage.Should().Be(current.Coverage);
updated.Replayability.Should().Be(current.Replayability);
updated.Provenance.Should().BeGreaterThanOrEqualTo(0.1);
}
[Fact]
public void Calibrate_IsDeterministic()
{
var comparison = new ComparisonResult
{
SourceId = "source",
TotalPredictions = 10,
CorrectPredictions = 5,
FalseNegatives = 2,
FalsePositives = 0,
Accuracy = 0.5,
ConfidenceInterval = 0.2,
DetectedBias = CalibrationBias.OptimisticBias,
};
var current = new Core.TrustVector
{
Provenance = 0.7,
Coverage = 0.6,
Replayability = 0.5,
};
var expected = new TrustVectorCalibrator { MomentumFactor = 0.0 }
.Calibrate(current, comparison, comparison.DetectedBias);
for (var i = 0; i < 1000; i++)
{
var result = new TrustVectorCalibrator { MomentumFactor = 0.0 }
.Calibrate(current, comparison, comparison.DetectedBias);
result.Should().Be(expected);
}
}
}

View File

@@ -0,0 +1,121 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Lattice;
namespace StellaOps.Excititor.Core.Tests.Lattice;
public sealed class PolicyLatticeAdapterTests
{
private static readonly DateTimeOffset Older = DateTimeOffset.Parse("2025-10-01T00:00:00Z");
private static readonly DateTimeOffset Newer = DateTimeOffset.Parse("2025-10-02T00:00:00Z");
[Theory]
[InlineData(VexClaimStatus.Affected, VexClaimStatus.NotAffected, VexClaimStatus.Affected)]
[InlineData(VexClaimStatus.Fixed, VexClaimStatus.NotAffected, VexClaimStatus.Affected)]
[InlineData(VexClaimStatus.UnderInvestigation, VexClaimStatus.Fixed, VexClaimStatus.Fixed)]
public void Join_ReturnsExpectedK4Result(
VexClaimStatus left,
VexClaimStatus right,
VexClaimStatus expected)
{
var adapter = CreateAdapter();
var leftStmt = CreateClaim(left, "source1", Older, Older);
var rightStmt = CreateClaim(right, "source2", Older, Older);
var result = adapter.Join(leftStmt, rightStmt);
result.ResultStatus.Should().Be(expected);
}
[Fact]
public void ResolveConflict_TrustWeightWins()
{
var adapter = CreateAdapter();
var vendor = CreateClaim(VexClaimStatus.NotAffected, "vendor", Older, Older);
var community = CreateClaim(VexClaimStatus.Affected, "community", Older, Older);
var result = adapter.ResolveConflict(vendor, community);
result.Winner.Should().Be(vendor);
result.Reason.Should().Be(ConflictResolutionReason.TrustWeight);
}
[Fact]
public void ResolveConflict_EqualTrust_UsesLatticePosition()
{
var registry = CreateRegistry();
registry.RegisterWeight("vendor-a", 0.9m);
registry.RegisterWeight("vendor-b", 0.9m);
var adapter = CreateAdapter(registry);
var affected = CreateClaim(VexClaimStatus.Affected, "vendor-a", Older, Older);
var notAffected = CreateClaim(VexClaimStatus.NotAffected, "vendor-b", Older, Older);
var result = adapter.ResolveConflict(affected, notAffected);
result.Winner.Status.Should().Be(VexClaimStatus.Affected);
result.Reason.Should().Be(ConflictResolutionReason.LatticePosition);
}
[Fact]
public void ResolveConflict_EqualTrustAndStatus_UsesFreshness()
{
var adapter = CreateAdapter();
var older = CreateClaim(VexClaimStatus.Affected, "vendor", Older, Older);
var newer = CreateClaim(VexClaimStatus.Affected, "vendor", Older, Newer);
var result = adapter.ResolveConflict(older, newer);
result.Winner.Should().Be(newer);
result.Reason.Should().Be(ConflictResolutionReason.Freshness);
}
[Fact]
public void ResolveConflict_GeneratesTrace()
{
var adapter = CreateAdapter();
var left = CreateClaim(VexClaimStatus.Affected, "vendor", Older, Older);
var right = CreateClaim(VexClaimStatus.NotAffected, "distro", Older, Older);
var result = adapter.ResolveConflict(left, right);
result.Trace.Should().NotBeNull();
result.Trace.LeftSource.Should().Be("vendor");
result.Trace.RightSource.Should().Be("distro");
result.Trace.Explanation.Should().NotBeNullOrEmpty();
}
private static PolicyLatticeAdapter CreateAdapter(ITrustWeightRegistry? registry = null)
{
registry ??= CreateRegistry();
return new PolicyLatticeAdapter(registry, NullLogger<PolicyLatticeAdapter>.Instance);
}
private static TrustWeightRegistry CreateRegistry()
{
return new TrustWeightRegistry(
Options.Create(new TrustWeightOptions()),
NullLogger<TrustWeightRegistry>.Instance);
}
private static VexClaim CreateClaim(
VexClaimStatus status,
string providerId,
DateTimeOffset firstSeen,
DateTimeOffset lastSeen)
{
return new VexClaim(
"CVE-2025-1234",
providerId,
new VexProduct("pkg:demo/app@1.0.0", "Demo App", "1.0.0"),
status,
new VexClaimDocument(
VexDocumentFormat.OpenVex,
$"sha256:{providerId}",
new Uri($"https://example.com/{providerId}")),
firstSeen,
lastSeen);
}
}

View File

@@ -0,0 +1,52 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core.Lattice;
namespace StellaOps.Excititor.Core.Tests.Lattice;
public sealed class TrustWeightRegistryTests
{
[Fact]
public void GetWeight_KnownSource_ReturnsConfiguredWeight()
{
var registry = CreateRegistry();
registry.GetWeight("vendor").Should().Be(1.0m);
}
[Fact]
public void GetWeight_UnknownSource_ReturnsFallback()
{
var registry = CreateRegistry();
registry.GetWeight("mystery").Should().Be(0.3m);
}
[Fact]
public void GetWeight_CategoryMatch_ReturnsCategoryWeight()
{
var registry = CreateRegistry();
registry.GetWeight("red-hat-vendor").Should().Be(1.0m);
}
[Fact]
public void RegisterWeight_ClampsRange()
{
var registry = CreateRegistry();
registry.RegisterWeight("custom", 2.5m);
registry.RegisterWeight("low", -1.0m);
registry.GetWeight("custom").Should().Be(1.0m);
registry.GetWeight("low").Should().Be(0.0m);
}
private static TrustWeightRegistry CreateRegistry()
{
return new TrustWeightRegistry(
Options.Create(new TrustWeightOptions()),
NullLogger<TrustWeightRegistry>.Instance);
}
}

View File

@@ -0,0 +1,51 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.TrustVectorTests;
public sealed class ClaimScoreCalculatorTests
{
[Fact]
public void ClaimScoreCalculator_ComputesScore()
{
var vector = new Core.TrustVector
{
Provenance = 0.9,
Coverage = 0.8,
Replayability = 0.7,
};
var weights = new TrustWeights { WP = 0.45, WC = 0.35, WR = 0.20 };
var calculator = new ClaimScoreCalculator(new FreshnessCalculator { HalfLifeDays = 90, Floor = 0.35 });
var issuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var cutoff = issuedAt.AddDays(45);
var result = calculator.Compute(vector, weights, ClaimStrength.ConfigWithEvidence, issuedAt, cutoff);
result.BaseTrust.Should().BeApproximately(0.82, 0.0001);
result.StrengthMultiplier.Should().Be(0.8);
result.FreshnessMultiplier.Should().BeGreaterThan(0.7);
result.Score.Should().BeApproximately(result.BaseTrust * result.StrengthMultiplier * result.FreshnessMultiplier, 0.0001);
}
[Fact]
public void ClaimScoreCalculator_IsDeterministic()
{
var vector = new Core.TrustVector
{
Provenance = 0.7,
Coverage = 0.6,
Replayability = 0.5,
};
var weights = new TrustWeights { WP = 0.4, WC = 0.4, WR = 0.2 };
var calculator = new ClaimScoreCalculator(new FreshnessCalculator { HalfLifeDays = 60, Floor = 0.4 });
var issuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var cutoff = issuedAt.AddDays(30);
var first = calculator.Compute(vector, weights, ClaimStrength.VendorBlanket, issuedAt, cutoff).Score;
for (var i = 0; i < 1000; i++)
{
calculator.Compute(vector, weights, ClaimStrength.VendorBlanket, issuedAt, cutoff).Score.Should().Be(first);
}
}
}

View File

@@ -0,0 +1,38 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.TrustVectorTests;
public sealed class FreshnessCalculatorTests
{
[Fact]
public void FreshnessCalculator_ReturnsFullForFutureDates()
{
var calculator = new FreshnessCalculator();
var issuedAt = DateTimeOffset.Parse("2025-12-20T00:00:00Z");
var cutoff = DateTimeOffset.Parse("2025-12-10T00:00:00Z");
calculator.Compute(issuedAt, cutoff).Should().Be(1.0);
}
[Fact]
public void FreshnessCalculator_DecaysWithHalfLife()
{
var calculator = new FreshnessCalculator { HalfLifeDays = 90, Floor = 0.35 };
var issuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var cutoff = issuedAt.AddDays(90);
calculator.Compute(issuedAt, cutoff).Should().BeApproximately(0.5, 0.0001);
}
[Fact]
public void FreshnessCalculator_RespectsFloor()
{
var calculator = new FreshnessCalculator { HalfLifeDays = 10, Floor = 0.35 };
var issuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var cutoff = issuedAt.AddDays(365);
calculator.Compute(issuedAt, cutoff).Should().Be(0.35);
}
}

View File

@@ -0,0 +1,58 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.TrustVectorTests;
public sealed class ScorersTests
{
[Fact]
public void ProvenanceScorer_ReturnsExpectedTiers()
{
var scorer = new ProvenanceScorer();
scorer.Score(new ProvenanceSignal
{
DsseSigned = true,
HasTransparencyLog = true,
KeyAllowListed = true,
}).Should().Be(ProvenanceScores.FullyAttested);
scorer.Score(new ProvenanceSignal
{
DsseSigned = true,
PublicKeyKnown = true,
}).Should().Be(ProvenanceScores.SignedNoLog);
scorer.Score(new ProvenanceSignal
{
AuthenticatedUnsigned = true,
}).Should().Be(ProvenanceScores.AuthenticatedUnsigned);
scorer.Score(new ProvenanceSignal
{
ManualImport = true,
}).Should().Be(ProvenanceScores.ManualImport);
}
[Fact]
public void CoverageScorer_ReturnsExpectedTiers()
{
var scorer = new CoverageScorer();
scorer.Score(new CoverageSignal { Level = CoverageLevel.ExactWithContext }).Should().Be(1.00);
scorer.Score(new CoverageSignal { Level = CoverageLevel.VersionRangePartialContext }).Should().Be(0.75);
scorer.Score(new CoverageSignal { Level = CoverageLevel.ProductLevel }).Should().Be(0.50);
scorer.Score(new CoverageSignal { Level = CoverageLevel.FamilyHeuristic }).Should().Be(0.25);
}
[Fact]
public void ReplayabilityScorer_ReturnsExpectedTiers()
{
var scorer = new ReplayabilityScorer();
scorer.Score(new ReplayabilitySignal { Level = ReplayabilityLevel.FullyPinned }).Should().Be(1.00);
scorer.Score(new ReplayabilitySignal { Level = ReplayabilityLevel.MostlyPinned }).Should().Be(0.60);
scorer.Score(new ReplayabilitySignal { Level = ReplayabilityLevel.Ephemeral }).Should().Be(0.20);
}
}

View File

@@ -0,0 +1,48 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.TrustVectorTests;
public sealed class TrustVectorTests
{
[Fact]
public void TrustVector_ValidatesRange()
{
Action action = () => _ = new Core.TrustVector
{
Provenance = -0.1,
Coverage = 0.5,
Replayability = 0.5,
};
action.Should().Throw<ArgumentOutOfRangeException>();
}
[Fact]
public void TrustVector_ComputesBaseTrust()
{
var vector = new Core.TrustVector
{
Provenance = 0.9,
Coverage = 0.6,
Replayability = 0.3,
};
var weights = new TrustWeights { WP = 0.5, WC = 0.3, WR = 0.2 };
var baseTrust = vector.ComputeBaseTrust(weights);
baseTrust.Should().BeApproximately(0.69, 0.0001);
}
[Fact]
public void TrustVector_FromLegacyWeight_MapsAllComponents()
{
var vector = Core.TrustVector.FromLegacyWeight(0.72);
vector.Provenance.Should().Be(0.72);
vector.Coverage.Should().Be(0.72);
vector.Replayability.Should().Be(0.72);
}
}

View File

@@ -0,0 +1,25 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.TrustVectorTests;
public sealed class TrustWeightsTests
{
[Fact]
public void TrustWeights_ValidatesRange()
{
Action action = () => _ = new TrustWeights { WP = 1.2 };
action.Should().Throw<ArgumentOutOfRangeException>();
}
[Fact]
public void TrustWeights_DefaultsMatchSpec()
{
var weights = new TrustWeights();
weights.WP.Should().Be(0.45);
weights.WC.Should().Be(0.35);
weights.WR.Should().Be(0.20);
}
}

View File

@@ -0,0 +1,43 @@
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.TrustVectorTests;
public sealed class VexProviderTrustTests
{
[Fact]
public void EffectiveVector_UsesLegacyWeightWhenVectorMissing()
{
var trust = new VexProviderTrust(0.72, cosign: null);
trust.EffectiveVector.Provenance.Should().Be(0.72);
trust.EffectiveVector.Coverage.Should().Be(0.72);
trust.EffectiveVector.Replayability.Should().Be(0.72);
}
[Fact]
public void EffectiveWeights_FallsBackToDefaults()
{
var trust = new VexProviderTrust(0.9, cosign: null);
trust.EffectiveWeights.WP.Should().Be(0.45);
trust.EffectiveWeights.WC.Should().Be(0.35);
trust.EffectiveWeights.WR.Should().Be(0.20);
}
[Fact]
public void EffectiveVector_UsesConfiguredVector()
{
var vector = new Core.TrustVector
{
Provenance = 0.6,
Coverage = 0.5,
Replayability = 0.4,
};
var trust = new VexProviderTrust(0.9, cosign: null, vector: vector);
trust.EffectiveVector.Should().Be(vector);
}
}

View File

@@ -1,227 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests;
public sealed class VexConsensusResolverTests
{
private static readonly VexProduct DemoProduct = new(
key: "pkg:demo/app",
name: "Demo App",
version: "1.0.0",
purl: "pkg:demo/app@1.0.0",
cpe: "cpe:2.3:a:demo:app:1.0.0");
[Fact]
public void Resolve_SingleAcceptedClaim_SelectsStatus()
{
var provider = CreateProvider("redhat", VexProviderKind.Vendor);
var claim = CreateClaim(
"CVE-2025-0001",
provider.Id,
VexClaimStatus.Affected,
justification: null);
var resolver = new VexConsensusResolver(new BaselineVexConsensusPolicy());
var result = resolver.Resolve(new VexConsensusRequest(
claim.VulnerabilityId,
DemoProduct,
new[] { claim },
new Dictionary<string, VexProvider> { [provider.Id] = provider },
DateTimeOffset.Parse("2025-10-15T12:00:00Z")));
Assert.Equal(VexConsensusStatus.Affected, result.Consensus.Status);
Assert.Equal("baseline/v1", result.Consensus.PolicyVersion);
Assert.Single(result.Consensus.Sources);
Assert.Empty(result.Consensus.Conflicts);
Assert.NotNull(result.Consensus.Summary);
Assert.Contains("affected", result.Consensus.Summary!, StringComparison.Ordinal);
var decision = Assert.Single(result.DecisionLog);
Assert.True(decision.Included);
Assert.Equal(provider.Id, decision.ProviderId);
Assert.Null(decision.Reason);
}
[Fact]
public void Resolve_NotAffectedWithoutJustification_IsRejected()
{
var provider = CreateProvider("cisco", VexProviderKind.Vendor);
var claim = CreateClaim(
"CVE-2025-0002",
provider.Id,
VexClaimStatus.NotAffected,
justification: null);
var resolver = new VexConsensusResolver(new BaselineVexConsensusPolicy());
var result = resolver.Resolve(new VexConsensusRequest(
claim.VulnerabilityId,
DemoProduct,
new[] { claim },
new Dictionary<string, VexProvider> { [provider.Id] = provider },
DateTimeOffset.Parse("2025-10-15T12:00:00Z")));
Assert.Equal(VexConsensusStatus.UnderInvestigation, result.Consensus.Status);
Assert.Empty(result.Consensus.Sources);
var conflict = Assert.Single(result.Consensus.Conflicts);
Assert.Equal("missing_justification", conflict.Reason);
var decision = Assert.Single(result.DecisionLog);
Assert.False(decision.Included);
Assert.Equal("missing_justification", decision.Reason);
}
[Fact]
public void Resolve_MajorityWeightWins_WithConflictingSources()
{
var vendor = CreateProvider("redhat", VexProviderKind.Vendor);
var distro = CreateProvider("fedora", VexProviderKind.Distro);
var claims = new[]
{
CreateClaim(
"CVE-2025-0003",
vendor.Id,
VexClaimStatus.Affected,
detail: "Vendor advisory",
documentDigest: "sha256:vendor"),
CreateClaim(
"CVE-2025-0003",
distro.Id,
VexClaimStatus.NotAffected,
justification: VexJustification.ComponentNotPresent,
detail: "Distro package not shipped",
documentDigest: "sha256:distro"),
};
var resolver = new VexConsensusResolver(new BaselineVexConsensusPolicy());
var result = resolver.Resolve(new VexConsensusRequest(
"CVE-2025-0003",
DemoProduct,
claims,
new Dictionary<string, VexProvider>
{
[vendor.Id] = vendor,
[distro.Id] = distro,
},
DateTimeOffset.Parse("2025-10-15T12:00:00Z")));
Assert.Equal(VexConsensusStatus.Affected, result.Consensus.Status);
Assert.Equal(2, result.Consensus.Sources.Length);
Assert.Equal(1.0, result.Consensus.Sources.First(s => s.ProviderId == vendor.Id).Weight);
Assert.Contains(result.Consensus.Conflicts, c => c.ProviderId == distro.Id && c.Reason == "status_conflict");
Assert.NotNull(result.Consensus.Summary);
Assert.Contains("affected", result.Consensus.Summary!, StringComparison.Ordinal);
}
[Fact]
public void Resolve_TieFallsBackToUnderInvestigation()
{
var hub = CreateProvider("hub", VexProviderKind.Hub);
var platform = CreateProvider("platform", VexProviderKind.Platform);
var claims = new[]
{
CreateClaim(
"CVE-2025-0004",
hub.Id,
VexClaimStatus.Affected,
detail: "Hub escalation",
documentDigest: "sha256:hub"),
CreateClaim(
"CVE-2025-0004",
platform.Id,
VexClaimStatus.NotAffected,
justification: VexJustification.ProtectedByMitigatingControl,
detail: "Runtime mitigations",
documentDigest: "sha256:platform"),
};
var resolver = new VexConsensusResolver(new BaselineVexConsensusPolicy(
new VexConsensusPolicyOptions(
hubWeight: 0.5,
platformWeight: 0.5)));
var result = resolver.Resolve(new VexConsensusRequest(
"CVE-2025-0004",
DemoProduct,
claims,
new Dictionary<string, VexProvider>
{
[hub.Id] = hub,
[platform.Id] = platform,
},
DateTimeOffset.Parse("2025-10-15T12:00:00Z")));
Assert.Equal(VexConsensusStatus.UnderInvestigation, result.Consensus.Status);
Assert.Equal(2, result.Consensus.Conflicts.Length);
Assert.NotNull(result.Consensus.Summary);
Assert.Contains("No majority consensus", result.Consensus.Summary!, StringComparison.Ordinal);
}
[Fact]
public void Resolve_RespectsRaisedWeightCeiling()
{
var provider = CreateProvider("vendor", VexProviderKind.Vendor);
var claim = CreateClaim(
"CVE-2025-0100",
provider.Id,
VexClaimStatus.Affected,
documentDigest: "sha256:vendor");
var policy = new BaselineVexConsensusPolicy(new VexConsensusPolicyOptions(
vendorWeight: 1.4,
weightCeiling: 2.0));
var resolver = new VexConsensusResolver(policy);
var result = resolver.Resolve(new VexConsensusRequest(
claim.VulnerabilityId,
DemoProduct,
new[] { claim },
new Dictionary<string, VexProvider> { [provider.Id] = provider },
DateTimeOffset.Parse("2025-10-15T12:00:00Z"),
WeightCeiling: 2.0));
var source = Assert.Single(result.Consensus.Sources);
Assert.Equal(1.4, source.Weight);
}
private static VexProvider CreateProvider(string id, VexProviderKind kind)
=> new(
id,
displayName: id.ToUpperInvariant(),
kind,
baseUris: Array.Empty<Uri>(),
trust: new VexProviderTrust(weight: 1.0, cosign: null));
private static VexClaim CreateClaim(
string vulnerabilityId,
string providerId,
VexClaimStatus status,
VexJustification? justification = null,
string? detail = null,
string? documentDigest = null)
=> new(
vulnerabilityId,
providerId,
DemoProduct,
status,
new VexClaimDocument(
VexDocumentFormat.Csaf,
documentDigest ?? $"sha256:{providerId}",
new Uri($"https://example.org/{providerId}/{vulnerabilityId}.json"),
"1"),
firstSeen: DateTimeOffset.Parse("2025-10-10T12:00:00Z"),
lastSeen: DateTimeOffset.Parse("2025-10-11T12:00:00Z"),
justification,
detail,
confidence: null,
additionalMetadata: ImmutableDictionary<string, string>.Empty);
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Excititor.Core;
@@ -20,7 +21,13 @@ public sealed class CycloneDxExporterTests
new VexClaimDocument(VexDocumentFormat.CycloneDx, "sha256:doc1", new Uri("https://example.com/cyclonedx/1")),
new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 10, 11, 0, 0, 0, TimeSpan.Zero),
detail: "Issue resolved in 1.2.3"));
detail: "Issue resolved in 1.2.3",
signals: new VexSignalSnapshot(
new VexSeveritySignal(
scheme: "cvss-4.0",
score: 9.3,
label: "critical",
vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H"))));
var request = new VexExportRequest(
VexQuery.Empty,
@@ -37,9 +44,25 @@ public sealed class CycloneDxExporterTests
var root = document.RootElement;
root.GetProperty("bomFormat").GetString().Should().Be("CycloneDX");
root.GetProperty("specVersion").GetString().Should().Be("1.7");
root.GetProperty("components").EnumerateArray().Should().HaveCount(1);
root.GetProperty("vulnerabilities").EnumerateArray().Should().HaveCount(1);
var vulnerability = root.GetProperty("vulnerabilities").EnumerateArray().Single();
var rating = vulnerability.GetProperty("ratings").EnumerateArray().Single();
rating.GetProperty("method").GetString().Should().Be("CVSSv4");
rating.GetProperty("score").GetDouble().Should().Be(9.3);
rating.GetProperty("severity").GetString().Should().Be("critical");
rating.GetProperty("vector").GetString().Should().Be("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H");
var affect = vulnerability.GetProperty("affects").EnumerateArray().Single();
var affectedVersion = affect.GetProperty("versions").EnumerateArray().Single();
affectedVersion.GetProperty("version").GetString().Should().Be("1.2.3");
var source = vulnerability.GetProperty("source");
source.GetProperty("name").GetString().Should().Be("vendor:demo");
source.GetProperty("url").GetString().Should().Be("pkg:demo/component@1.2.3");
result.Metadata.Should().ContainKey("cyclonedx.vulnerabilityCount");
result.Metadata["cyclonedx.componentCount"].Should().Be("1");
result.Digest.Algorithm.Should().Be("sha256");

View File

@@ -90,4 +90,52 @@ public sealed class CycloneDxNormalizerTests
investigating.Product.Name.Should().Be("pkg:npm/missing/component@2.0.0");
investigating.AdditionalMetadata.Should().ContainKey("cyclonedx.specVersion");
}
[Fact]
public async Task NormalizeAsync_NormalizesSpecVersion()
{
var json = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.7.0",
"metadata": {
"timestamp": "2025-10-15T12:00:00Z"
},
"components": [
{
"bom-ref": "pkg:npm/acme/lib@2.1.0",
"name": "acme-lib",
"version": "2.1.0",
"purl": "pkg:npm/acme/lib@2.1.0"
}
],
"vulnerabilities": [
{
"id": "CVE-2025-2000",
"analysis": { "state": "affected" },
"affects": [
{ "ref": "pkg:npm/acme/lib@2.1.0" }
]
}
]
}
""";
var rawDocument = new VexRawDocument(
"excititor:cyclonedx",
VexDocumentFormat.CycloneDx,
new Uri("https://example.org/vex-17.json"),
new DateTimeOffset(2025, 10, 16, 0, 0, 0, TimeSpan.Zero),
"sha256:dummydigest",
Encoding.UTF8.GetBytes(json),
ImmutableDictionary<string, string>.Empty);
var provider = new VexProvider("excititor:cyclonedx", "CycloneDX Provider", VexProviderKind.Vendor);
var normalizer = new CycloneDxNormalizer(NullLogger<CycloneDxNormalizer>.Instance);
var batch = await normalizer.NormalizeAsync(rawDocument, provider, CancellationToken.None);
batch.Claims.Should().HaveCount(1);
batch.Claims[0].AdditionalMetadata["cyclonedx.specVersion"].Should().Be("1.7");
}
}

View File

@@ -1,15 +1,33 @@
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Lattice;
using StellaOps.Excititor.Formats.OpenVEX;
namespace StellaOps.Excititor.Formats.OpenVEX.Tests;
public sealed class OpenVexStatementMergerTests
{
private static OpenVexStatementMerger CreateMerger()
{
var registry = new TrustWeightRegistry(
Options.Create(new TrustWeightOptions()),
NullLogger<TrustWeightRegistry>.Instance);
var lattice = new PolicyLatticeAdapter(
registry,
NullLogger<PolicyLatticeAdapter>.Instance);
return new OpenVexStatementMerger(
lattice,
NullLogger<OpenVexStatementMerger>.Instance);
}
[Fact]
public void Merge_DetectsConflictsAndSelectsCanonicalStatus()
{
var merger = CreateMerger();
var claims = ImmutableArray.Create(
new VexClaim(
"CVE-2025-4000",
@@ -29,11 +47,82 @@ public sealed class OpenVexStatementMergerTests
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow));
var result = OpenVexStatementMerger.Merge(claims);
var result = merger.Merge(claims);
result.Statements.Should().HaveCount(1);
var statement = result.Statements[0];
statement.Status.Should().Be(VexClaimStatus.Affected);
result.Diagnostics.Should().ContainKey("openvex.status_conflict");
}
[Fact]
public void MergeClaims_NoStatements_ReturnsEmpty()
{
var merger = CreateMerger();
var result = merger.MergeClaims(Array.Empty<VexClaim>());
result.InputCount.Should().Be(0);
result.HadConflicts.Should().BeFalse();
}
[Fact]
public void MergeClaims_SingleStatement_ReturnsSingle()
{
var merger = CreateMerger();
var claim = CreateClaim(VexClaimStatus.NotAffected, "vendor");
var result = merger.MergeClaims(new[] { claim });
result.InputCount.Should().Be(1);
result.ResultStatement.Should().Be(claim);
result.HadConflicts.Should().BeFalse();
}
[Fact]
public void MergeClaims_ConflictingStatements_UsesLattice()
{
var merger = CreateMerger();
var vendor = CreateClaim(VexClaimStatus.NotAffected, "vendor");
var nvd = CreateClaim(VexClaimStatus.Affected, "nvd");
var result = merger.MergeClaims(new[] { vendor, nvd });
result.InputCount.Should().Be(2);
result.HadConflicts.Should().BeTrue();
result.Traces.Should().HaveCount(1);
result.ResultStatement.Status.Should().Be(VexClaimStatus.Affected);
}
[Fact]
public void MergeClaims_MultipleStatements_CollectsAllTraces()
{
var merger = CreateMerger();
var claims = new[]
{
CreateClaim(VexClaimStatus.Affected, "source1"),
CreateClaim(VexClaimStatus.NotAffected, "source2"),
CreateClaim(VexClaimStatus.Fixed, "source3"),
};
var result = merger.MergeClaims(claims);
result.InputCount.Should().Be(3);
result.Traces.Should().NotBeEmpty();
}
private static VexClaim CreateClaim(VexClaimStatus status, string providerId)
{
return new VexClaim(
"CVE-2025-4001",
providerId,
new VexProduct("pkg:demo/app@1.0.0", "Demo App", "1.0.0"),
status,
new VexClaimDocument(
VexDocumentFormat.OpenVex,
$"sha256:{providerId}",
new Uri($"https://example.com/{providerId}")),
DateTimeOffset.Parse("2025-12-01T00:00:00Z"),
DateTimeOffset.Parse("2025-12-02T00:00:00Z"));
}
}

View File

@@ -136,7 +136,14 @@ public sealed class PostgresVexProviderStoreTests : IAsyncLifetime
{
// Arrange
var cosign = new VexCosignTrust("https://accounts.google.com", "@redhat.com$");
var trust = new VexProviderTrust(0.9, cosign, ["ABCD1234", "EFGH5678"]);
var vector = new TrustVector
{
Provenance = 0.9,
Coverage = 0.8,
Replayability = 0.7,
};
var weights = new TrustWeights { WP = 0.4, WC = 0.4, WR = 0.2 };
var trust = new VexProviderTrust(0.9, cosign, ["ABCD1234", "EFGH5678"], vector, weights);
var provider = new VexProvider(
"trusted-provider", "Trusted Provider", VexProviderKind.Attestation,
[], VexProviderDiscovery.Empty, trust, true);
@@ -152,5 +159,7 @@ public sealed class PostgresVexProviderStoreTests : IAsyncLifetime
fetched.Trust.Cosign!.Issuer.Should().Be("https://accounts.google.com");
fetched.Trust.Cosign.IdentityPattern.Should().Be("@redhat.com$");
fetched.Trust.PgpFingerprints.Should().HaveCount(2);
fetched.Trust.Vector.Should().Be(vector);
fetched.Trust.Weights.Should().Be(weights);
}
}

View File

@@ -232,9 +232,9 @@ public sealed class BatchIngestValidationTests : IDisposable
public static IReadOnlyList<VexFixture> CreateBatch()
=> new[]
{
CreateCycloneDxFixture("001", "sha256:batch-cdx-001", "CDX-BATCH-001", "not_affected"),
CreateCycloneDxFixture("002", "sha256:batch-cdx-002", "CDX-BATCH-002", "affected"),
CreateCycloneDxFixture("003", "sha256:batch-cdx-003", "CDX-BATCH-003", "fixed"),
CreateCycloneDxFixture("001", "sha256:batch-cdx-001", "CDX-BATCH-001", "not_affected", "1.7"),
CreateCycloneDxFixture("002", "sha256:batch-cdx-002", "CDX-BATCH-002", "affected", "1.7"),
CreateCycloneDxFixture("003", "sha256:batch-cdx-003", "CDX-BATCH-003", "fixed", "1.6"),
CreateCsafFixture("010", "sha256:batch-csaf-001", "CSAF-BATCH-001", "fixed"),
CreateCsafFixture("011", "sha256:batch-csaf-002", "CSAF-BATCH-002", "known_affected"),
CreateCsafFixture("012", "sha256:batch-csaf-003", "CSAF-BATCH-003", "known_not_affected"),
@@ -243,13 +243,13 @@ public sealed class BatchIngestValidationTests : IDisposable
CreateOpenVexFixture("022", "sha256:batch-openvex-003", "OVX-BATCH-003", "fixed")
};
private static VexFixture CreateCycloneDxFixture(string suffix, string digest, string upstreamId, string state)
private static VexFixture CreateCycloneDxFixture(string suffix, string digest, string upstreamId, string state, string specVersion)
{
var vulnerabilityId = $"CVE-2025-{suffix}";
var raw = $$"""
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"specVersion": "{{specVersion}}",
"version": 1,
"metadata": {
"timestamp": "2025-11-08T00:00:00Z",
@@ -277,7 +277,7 @@ public sealed class BatchIngestValidationTests : IDisposable
connector: "cdx-batch",
stream: "cyclonedx-vex",
format: "cyclonedx",
specVersion: "1.6",
specVersion: specVersion,
rawJson: raw,
digest: digest,
upstreamId: upstreamId,

View File

@@ -101,10 +101,10 @@ public sealed class VexGuardSchemaTests
},
"content": {
"format": "CycloneDX",
"spec_version": "1.6",
"spec_version": "1.7",
"raw": {
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"specVersion": "1.7",
"serialNumber": "urn:uuid:12345678-1234-5678-9abc-def012345678",
"version": 1,
"metadata": {