sprints enhancements

This commit is contained in:
StellaOps Bot
2025-12-25 19:52:30 +02:00
parent ef6ac36323
commit b8b2d83f4a
138 changed files with 25133 additions and 594 deletions

View File

@@ -0,0 +1,439 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Sprint: SPRINT_8200_0012_0003_policy_engine_integration
// Task: PINT-8200-041 - Determinism test: same finding + policy → same EWS in verdict
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Engine.Scoring.EvidenceWeightedScore;
using StellaOps.Signals.EvidenceWeightedScore;
using StellaOps.Signals.EvidenceWeightedScore.Normalizers;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Integration;
/// <summary>
/// Determinism tests verifying that same finding + policy → same EWS in verdict.
/// These tests ensure that EWS calculation is fully deterministic and produces
/// identical results across multiple evaluations.
/// </summary>
[Trait("Category", "Determinism")]
[Trait("Category", "Integration")]
[Trait("Sprint", "8200.0012.0003")]
[Trait("Task", "PINT-8200-041")]
public sealed class EwsVerdictDeterminismTests
{
private static ServiceCollection CreateServicesWithConfiguration()
{
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection()
.Build();
services.AddSingleton<IConfiguration>(configuration);
return services;
}
#region Score Determinism Tests
[Fact(DisplayName = "Same finding evidence produces identical EWS across multiple calculations")]
public void SameFindingEvidence_ProducesIdenticalEws_AcrossMultipleCalculations()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = CreateTestInput("determinism-test-001");
// Act - Calculate 100 times
var results = Enumerable.Range(0, 100)
.Select(_ => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction))
.ToList();
// Assert - All results should be byte-identical
var firstScore = results[0].Score;
var firstBucket = results[0].Bucket;
var firstDimensions = results[0].Dimensions;
results.Should().AllSatisfy(r =>
{
r.Score.Should().Be(firstScore, "score must be deterministic");
r.Bucket.Should().Be(firstBucket, "bucket must be deterministic");
r.Dimensions.Should().BeEquivalentTo(firstDimensions, "dimensions must be deterministic");
});
}
[Fact(DisplayName = "Same finding produces identical EWS through enricher pipeline")]
public void SameFinding_ProducesIdenticalEws_ThroughEnricherPipeline()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts =>
{
opts.Enabled = true;
opts.EnableCaching = false; // Disable caching to test actual calculation determinism
});
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
var evidence = CreateTestEvidence("pipeline-determinism-test");
// Act - Enrich 50 times
var results = Enumerable.Range(0, 50)
.Select(_ => enricher.Enrich(evidence))
.ToList();
// Assert
var firstResult = results[0];
results.Should().AllSatisfy(r =>
{
r.Score!.Score.Should().Be(firstResult.Score!.Score, "enriched score must be deterministic");
r.Score!.Bucket.Should().Be(firstResult.Score!.Bucket, "enriched bucket must be deterministic");
});
}
[Fact(DisplayName = "Floating point precision is maintained across calculations")]
public void FloatingPointPrecision_IsMaintained_AcrossCalculations()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
// Input with fractional values that could cause floating point issues
var input = new EvidenceWeightedScoreInput
{
FindingId = "float-precision-test",
Rch = 0.333333333333333,
Rts = 0.666666666666666,
Bkp = 0.111111111111111,
Xpl = 0.777777777777777,
Src = 0.222222222222222,
Mit = 0.888888888888888
};
// Act - Calculate many times
var results = Enumerable.Range(0, 100)
.Select(_ => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction))
.ToList();
// Assert - All scores should be exactly equal (not just approximately)
var firstScore = results[0].Score;
results.Should().AllSatisfy(r => r.Score.Should().Be(firstScore));
}
#endregion
#region Policy Variation Tests
[Fact(DisplayName = "Same evidence with same policy produces identical EWS")]
public void SameEvidenceAndPolicy_ProducesIdenticalEws()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = CreateTestInput("policy-consistency-test");
var policy = EvidenceWeightPolicy.DefaultProduction;
// Act - Multiple calculations with same policy
var result1 = calculator.Calculate(input, policy);
var result2 = calculator.Calculate(input, policy);
var result3 = calculator.Calculate(input, policy);
// Assert
result1.Score.Should().Be(result2.Score);
result2.Score.Should().Be(result3.Score);
result1.Bucket.Should().Be(result2.Bucket);
result2.Bucket.Should().Be(result3.Bucket);
}
[Fact(DisplayName = "Different policies produce different EWS for same evidence")]
public void DifferentPolicies_ProduceDifferentEws_ForSameEvidence()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = CreateTestInput("multi-policy-test");
// Custom policy with different weights
var customPolicy = new EvidenceWeightPolicy
{
PolicyId = "custom-test-policy",
Version = "1.0",
Weights = new EvidenceWeights
{
Reachability = 0.50, // Much higher weight on reachability
Runtime = 0.10,
Backport = 0.05,
Exploit = 0.20,
Source = 0.10,
Mitigation = 0.05
},
Buckets = EvidenceWeightPolicy.DefaultProduction.Buckets
};
// Act
var defaultResult = calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction);
var customResult = calculator.Calculate(input, customPolicy);
// Assert - Different policies should produce different scores
// (unless the evidence happens to result in same weighted sum)
// The test validates that policy changes affect output
(defaultResult.Score == customResult.Score &&
defaultResult.Bucket == customResult.Bucket)
.Should().BeFalse("different weight distributions should generally produce different scores");
}
#endregion
#region Serialization Determinism Tests
[Fact(DisplayName = "EWS JSON serialization is deterministic")]
public void EwsJsonSerialization_IsDeterministic()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = CreateTestInput("serialization-test");
var result = calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction);
// Act - Serialize multiple times
var serializations = Enumerable.Range(0, 10)
.Select(_ => System.Text.Json.JsonSerializer.Serialize(result))
.ToList();
// Assert - All serializations should be identical
var first = serializations[0];
serializations.Should().AllBeEquivalentTo(first);
}
[Fact(DisplayName = "EWS round-trips correctly through JSON")]
public void EwsRoundTrip_ThroughJson_IsCorrect()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = CreateTestInput("roundtrip-test");
var original = calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction);
// Act - Round-trip through JSON
var json = System.Text.Json.JsonSerializer.Serialize(original);
var deserialized = System.Text.Json.JsonSerializer.Deserialize<EvidenceWeightedScoreResult>(json);
// Assert
deserialized.Should().NotBeNull();
deserialized!.Score.Should().Be(original.Score);
deserialized.Bucket.Should().Be(original.Bucket);
deserialized.FindingId.Should().Be(original.FindingId);
}
#endregion
#region Edge Case Determinism Tests
[Fact(DisplayName = "Zero values produce deterministic EWS")]
public void ZeroValues_ProduceDeterministicEws()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = new EvidenceWeightedScoreInput
{
FindingId = "zero-test",
Rch = 0.0,
Rts = 0.0,
Bkp = 0.0,
Xpl = 0.0,
Src = 0.0,
Mit = 0.0
};
// Act
var results = Enumerable.Range(0, 20)
.Select(_ => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction))
.ToList();
// Assert
var first = results[0];
results.Should().AllSatisfy(r => r.Score.Should().Be(first.Score));
}
[Fact(DisplayName = "Maximum values produce deterministic EWS")]
public void MaximumValues_ProduceDeterministicEws()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = new EvidenceWeightedScoreInput
{
FindingId = "max-test",
Rch = 1.0,
Rts = 1.0,
Bkp = 1.0,
Xpl = 1.0,
Src = 1.0,
Mit = 1.0
};
// Act
var results = Enumerable.Range(0, 20)
.Select(_ => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction))
.ToList();
// Assert
var first = results[0];
results.Should().AllSatisfy(r => r.Score.Should().Be(first.Score));
}
[Fact(DisplayName = "Boundary values produce deterministic EWS")]
public void BoundaryValues_ProduceDeterministicEws()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
// Values at bucket boundaries
var inputs = new[]
{
new EvidenceWeightedScoreInput { FindingId = "boundary-0", Rch = 0.0, Rts = 0.0, Bkp = 0.0, Xpl = 0.0, Src = 0.0, Mit = 0.0 },
new EvidenceWeightedScoreInput { FindingId = "boundary-25", Rch = 0.25, Rts = 0.25, Bkp = 0.25, Xpl = 0.25, Src = 0.25, Mit = 0.25 },
new EvidenceWeightedScoreInput { FindingId = "boundary-50", Rch = 0.5, Rts = 0.5, Bkp = 0.5, Xpl = 0.5, Src = 0.5, Mit = 0.5 },
new EvidenceWeightedScoreInput { FindingId = "boundary-75", Rch = 0.75, Rts = 0.75, Bkp = 0.75, Xpl = 0.75, Src = 0.75, Mit = 0.75 },
new EvidenceWeightedScoreInput { FindingId = "boundary-100", Rch = 1.0, Rts = 1.0, Bkp = 1.0, Xpl = 1.0, Src = 1.0, Mit = 1.0 }
};
foreach (var input in inputs)
{
// Act - Calculate same input multiple times
var results = Enumerable.Range(0, 10)
.Select(_ => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction))
.ToList();
// Assert - All results for same input should be identical
var first = results[0];
results.Should().AllSatisfy(r =>
{
r.Score.Should().Be(first.Score, $"boundary input {input.FindingId} must be deterministic");
r.Bucket.Should().Be(first.Bucket, $"boundary input {input.FindingId} must be deterministic");
});
}
}
#endregion
#region Concurrent Determinism Tests
[Fact(DisplayName = "Concurrent calculations produce identical results")]
public async Task ConcurrentCalculations_ProduceIdenticalResults()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = CreateTestInput("concurrent-test");
// Act - Calculate concurrently
var tasks = Enumerable.Range(0, 100)
.Select(_ => Task.Run(() => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction)))
.ToArray();
var results = await Task.WhenAll(tasks);
// Assert
var first = results[0];
results.Should().AllSatisfy(r =>
{
r.Score.Should().Be(first.Score, "concurrent calculations must be deterministic");
r.Bucket.Should().Be(first.Bucket, "concurrent calculations must be deterministic");
});
}
[Fact(DisplayName = "Concurrent enricher calls produce identical results")]
public async Task ConcurrentEnricherCalls_ProduceIdenticalResults()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts =>
{
opts.Enabled = true;
opts.EnableCaching = false; // Test actual calculation, not cache
});
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
var evidence = CreateTestEvidence("concurrent-enricher-test");
// Act - Enrich concurrently
var tasks = Enumerable.Range(0, 50)
.Select(_ => Task.Run(() => enricher.Enrich(evidence)))
.ToArray();
var results = await Task.WhenAll(tasks);
// Assert
var first = results[0];
results.Should().AllSatisfy(r =>
{
r.Score!.Score.Should().Be(first.Score!.Score, "concurrent enrichments must be deterministic");
r.Score!.Bucket.Should().Be(first.Score!.Bucket, "concurrent enrichments must be deterministic");
});
}
#endregion
#region Hash Determinism Tests
[Fact(DisplayName = "Finding hash is deterministic")]
public void FindingHash_IsDeterministic()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = CreateTestInput("hash-test");
// Act
var results = Enumerable.Range(0, 20)
.Select(_ => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction))
.ToList();
// Assert - If FindingId is the same, results should be consistent
results.Should().AllSatisfy(r => r.FindingId.Should().Be("hash-test"));
}
#endregion
#region Test Helpers
private static EvidenceWeightedScoreInput CreateTestInput(string findingId)
{
return new EvidenceWeightedScoreInput
{
FindingId = findingId,
Rch = 0.75,
Rts = 0.60,
Bkp = 0.40,
Xpl = 0.55,
Src = 0.65,
Mit = 0.20
};
}
private static FindingEvidence CreateTestEvidence(string findingId)
{
return new FindingEvidence
{
FindingId = findingId,
Reachability = new ReachabilityInput
{
State = StellaOps.Signals.EvidenceWeightedScore.ReachabilityState.DynamicReachable,
Confidence = 0.85
},
Runtime = new RuntimeInput
{
Posture = StellaOps.Signals.EvidenceWeightedScore.RuntimePosture.ActiveTracing,
ObservationCount = 3,
RecencyFactor = 0.75
},
Exploit = new ExploitInput
{
EpssScore = 0.45,
EpssPercentile = 75,
KevStatus = KevStatus.NotInKev,
PublicExploitAvailable = false
}
};
}
#endregion
}

View File

@@ -0,0 +1,435 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
// Sprint: SPRINT_8200_0012_0003_policy_engine_integration
// Task: PINT-8200-040 - Integration tests for full policy→EWS pipeline
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Engine.Scoring.EvidenceWeightedScore;
using StellaOps.Signals.EvidenceWeightedScore;
using StellaOps.Signals.EvidenceWeightedScore.Normalizers;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Integration;
/// <summary>
/// Integration tests for the full policy evaluation → EWS calculation pipeline.
/// Tests DI wiring and component integration.
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "8200.0012.0003")]
[Trait("Task", "PINT-8200-040")]
public sealed class PolicyEwsPipelineIntegrationTests
{
private static ServiceCollection CreateServicesWithConfiguration()
{
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection()
.Build();
services.AddSingleton<IConfiguration>(configuration);
return services;
}
#region DI Wiring Tests
[Fact(DisplayName = "AddEvidenceWeightedScore registers all required services")]
public void AddEvidenceWeightedScore_RegistersAllServices()
{
// Arrange
var services = CreateServicesWithConfiguration();
// Act
services.AddLogging();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore();
var provider = services.BuildServiceProvider();
// Assert: All services should be resolvable
provider.GetService<IEvidenceWeightedScoreCalculator>().Should().NotBeNull();
provider.GetService<IFindingScoreEnricher>().Should().NotBeNull();
provider.GetService<IScoreEnrichmentCache>().Should().NotBeNull();
provider.GetService<IDualEmitVerdictEnricher>().Should().NotBeNull();
provider.GetService<IMigrationTelemetryService>().Should().NotBeNull();
provider.GetService<IEwsTelemetryService>().Should().NotBeNull();
provider.GetService<ConfidenceToEwsAdapter>().Should().NotBeNull();
}
[Fact(DisplayName = "AddEvidenceWeightedScore with configure action applies options")]
public void AddEvidenceWeightedScore_WithConfigure_AppliesOptions()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts =>
{
opts.Enabled = true;
opts.EnableCaching = true;
});
// Act
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<PolicyEvidenceWeightedScoreOptions>>();
// Assert
options.Value.Enabled.Should().BeTrue();
options.Value.EnableCaching.Should().BeTrue();
}
[Fact(DisplayName = "Services are registered as singletons")]
public void Services_AreRegisteredAsSingletons()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore();
var provider = services.BuildServiceProvider();
// Act
var enricher1 = provider.GetRequiredService<IFindingScoreEnricher>();
var enricher2 = provider.GetRequiredService<IFindingScoreEnricher>();
// Assert: Same instance (singleton)
enricher1.Should().BeSameAs(enricher2);
}
#endregion
#region Calculator Integration Tests
[Fact(DisplayName = "Calculator produces valid EWS result from normalized inputs")]
public void Calculator_ProducesValidResult_FromNormalizedInputs()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = new EvidenceWeightedScoreInput
{
FindingId = "CVE-2024-CALC@pkg:test/calc@1.0",
Rch = 0.8,
Rts = 0.7,
Bkp = 0.3,
Xpl = 0.6,
Src = 0.5,
Mit = 0.1
};
// Act
var result = calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction);
// Assert
result.Should().NotBeNull();
result.Score.Should().BeInRange(0, 100);
result.Bucket.Should().BeDefined();
result.FindingId.Should().Be("CVE-2024-CALC@pkg:test/calc@1.0");
}
[Fact(DisplayName = "Calculator is deterministic for same inputs")]
public void Calculator_IsDeterministic_ForSameInputs()
{
// Arrange
var calculator = new EvidenceWeightedScoreCalculator();
var input = new EvidenceWeightedScoreInput
{
FindingId = "determinism-test",
Rch = 0.75, Rts = 0.60, Bkp = 0.40, Xpl = 0.55, Src = 0.65, Mit = 0.20
};
// Act - Calculate multiple times
var results = Enumerable.Range(0, 10)
.Select(_ => calculator.Calculate(input, EvidenceWeightPolicy.DefaultProduction))
.ToList();
// Assert - All results should be identical
var firstScore = results[0].Score;
results.Should().AllSatisfy(r => r.Score.Should().Be(firstScore));
}
#endregion
#region Enricher Integration Tests
[Fact(DisplayName = "Enricher with enabled feature calculates scores")]
public void Enricher_WithEnabledFeature_CalculatesScores()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts => opts.Enabled = true);
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
var evidence = new FindingEvidence
{
FindingId = "CVE-2024-TEST@pkg:test/enricher@1.0",
Reachability = new ReachabilityInput
{
State = StellaOps.Signals.EvidenceWeightedScore.ReachabilityState.DynamicReachable,
Confidence = 0.85
}
};
// Act
var result = enricher.Enrich(evidence);
// Assert
result.Should().NotBeNull();
result.IsSuccess.Should().BeTrue();
result.Score.Should().NotBeNull();
result.Score!.Score.Should().BeInRange(0, 100);
result.FindingId.Should().Be("CVE-2024-TEST@pkg:test/enricher@1.0");
}
[Fact(DisplayName = "Enricher with disabled feature returns skipped")]
public void Enricher_WithDisabledFeature_ReturnsSkipped()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts => opts.Enabled = false);
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
var evidence = new FindingEvidence { FindingId = "test-finding" };
// Act
var result = enricher.Enrich(evidence);
// Assert
result.IsSuccess.Should().BeFalse();
result.Score.Should().BeNull();
}
#endregion
#region Caching Integration Tests
[Fact(DisplayName = "Cache returns cached result on second call")]
public void Cache_ReturnsCachedResult_OnSecondCall()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts =>
{
opts.Enabled = true;
opts.EnableCaching = true;
});
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
var evidence = new FindingEvidence { FindingId = "cache-test" };
// Act
var result1 = enricher.Enrich(evidence);
var result2 = enricher.Enrich(evidence);
// Assert
result1.FromCache.Should().BeFalse();
result2.FromCache.Should().BeTrue();
result1.Score!.Score.Should().Be(result2.Score!.Score);
}
[Fact(DisplayName = "Cache stores different findings separately")]
public void Cache_StoresDifferentFindings_Separately()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts =>
{
opts.Enabled = true;
opts.EnableCaching = true;
});
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
var evidence1 = new FindingEvidence
{
FindingId = "finding-A",
Reachability = new ReachabilityInput
{
State = StellaOps.Signals.EvidenceWeightedScore.ReachabilityState.DynamicReachable,
Confidence = 0.9
}
};
var evidence2 = new FindingEvidence
{
FindingId = "finding-B",
Reachability = new ReachabilityInput
{
State = StellaOps.Signals.EvidenceWeightedScore.ReachabilityState.Unknown,
Confidence = 0.1
}
};
// Act
var result1 = enricher.Enrich(evidence1);
var result2 = enricher.Enrich(evidence2);
// Assert
result1.FromCache.Should().BeFalse();
result2.FromCache.Should().BeFalse();
result1.FindingId.Should().Be("finding-A");
result2.FindingId.Should().Be("finding-B");
}
#endregion
#region Adapter Integration Tests
[Fact(DisplayName = "Adapter converts Confidence to EWS")]
public void Adapter_ConvertsConfidenceToEws()
{
// Arrange
var adapter = new ConfidenceToEwsAdapter();
var confidence = new ConfidenceScore
{
Value = 0.35m, // Lower confidence = higher risk
Factors =
[
new ConfidenceFactor
{
Type = ConfidenceFactorType.Reachability,
Weight = 0.5m,
RawValue = 0.35m,
Reason = "Test"
}
],
Explanation = "Test confidence score"
};
// Act
var result = adapter.Adapt(confidence, "adapter-test-finding");
// Assert
result.Should().NotBeNull();
result.EwsResult.Should().NotBeNull();
result.OriginalConfidence.Should().Be(confidence);
// Low confidence → High EWS (inverted scale)
result.EwsResult.Score.Should().BeGreaterThan(50);
}
[Fact(DisplayName = "Adapter preserves ranking relationship")]
public void Adapter_PreservesRankingRelationship()
{
// Arrange
var adapter = new ConfidenceToEwsAdapter();
// Higher confidence = safer = lower EWS
var highConfidence = new ConfidenceScore
{
Value = 0.85m,
Factors = [],
Explanation = "High confidence"
};
// Lower confidence = riskier = higher EWS
var lowConfidence = new ConfidenceScore
{
Value = 0.25m,
Factors = [],
Explanation = "Low confidence"
};
// Act
var highResult = adapter.Adapt(highConfidence, "high-conf");
var lowResult = adapter.Adapt(lowConfidence, "low-conf");
// Assert - Ranking should be preserved (inverted): low confidence = higher risk = higher or equal EWS
lowResult.EwsResult.Score.Should().BeGreaterThanOrEqualTo(highResult.EwsResult.Score,
"lower confidence should produce equal or higher EWS (inverted scale)");
}
#endregion
#region End-to-End Pipeline Tests
[Fact(DisplayName = "Full pipeline produces actionable results")]
public void FullPipeline_ProducesActionableResults()
{
// Arrange - Build a complete pipeline via DI
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts =>
{
opts.Enabled = true;
opts.EnableCaching = true;
});
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
// Simulate real finding evidence
var evidence = new FindingEvidence
{
FindingId = "CVE-2024-12345@pkg:npm/vulnerable-lib@1.0.0",
Reachability = new ReachabilityInput
{
State = StellaOps.Signals.EvidenceWeightedScore.ReachabilityState.DynamicReachable,
Confidence = 0.90
},
Runtime = new RuntimeInput
{
Posture = StellaOps.Signals.EvidenceWeightedScore.RuntimePosture.ActiveTracing,
ObservationCount = 5,
RecencyFactor = 0.85
},
Exploit = new ExploitInput
{
EpssScore = 0.75,
EpssPercentile = 90,
KevStatus = KevStatus.InKev,
PublicExploitAvailable = true
}
};
// Act
var result = enricher.Enrich(evidence);
// Assert
result.Should().NotBeNull();
result.IsSuccess.Should().BeTrue();
result.Score.Should().NotBeNull();
result.Score!.Score.Should().BeGreaterThan(50, "high-risk evidence should produce elevated EWS");
result.FindingId.Should().Be("CVE-2024-12345@pkg:npm/vulnerable-lib@1.0.0");
}
[Fact(DisplayName = "Pipeline handles missing evidence gracefully")]
public void Pipeline_HandlesMissingEvidence_Gracefully()
{
// Arrange
var services = CreateServicesWithConfiguration();
services.AddEvidenceWeightedScoring();
services.AddEvidenceNormalizers();
services.AddEvidenceWeightedScore(opts => opts.Enabled = true);
var provider = services.BuildServiceProvider();
var enricher = provider.GetRequiredService<IFindingScoreEnricher>();
// Minimal evidence - only finding ID
var evidence = new FindingEvidence { FindingId = "minimal-finding" };
// Act
var result = enricher.Enrich(evidence);
// Assert - Should still produce a valid result with defaults
result.Should().NotBeNull();
result.IsSuccess.Should().BeTrue();
result.Score.Should().NotBeNull();
result.Score!.Score.Should().BeInRange(0, 100);
}
#endregion
}