save progress

This commit is contained in:
StellaOps Bot
2025-12-20 12:15:16 +02:00
parent 439f10966b
commit 0ada1b583f
95 changed files with 12400 additions and 65 deletions

View File

@@ -0,0 +1,489 @@
using FluentAssertions;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.Policy.Unknowns.Services;
namespace StellaOps.Policy.Unknowns.Tests.Services;
/// <summary>
/// Unit tests for <see cref="UnknownRanker"/> ensuring deterministic ranking behavior.
/// </summary>
public class UnknownRankerTests
{
private readonly UnknownRanker _ranker = new();
#region Determinism Tests
[Fact]
public void Rank_SameInput_ReturnsSameResult()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
IsStaleAdvisory: true,
IsInKev: true,
EpssScore: 0.95m,
CvssScore: 9.5m);
// Act
var result1 = _ranker.Rank(input);
var result2 = _ranker.Rank(input);
// Assert
result1.Should().Be(result2, "ranking must be deterministic");
}
[Fact]
public void Rank_MultipleExecutions_ProducesIdenticalScores()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: false,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0.55m,
CvssScore: 7.5m);
var scores = new List<decimal>();
// Act - Run 100 times to verify determinism
for (int i = 0; i < 100; i++)
{
scores.Add(_ranker.Rank(input).Score);
}
// Assert
scores.Should().AllBeEquivalentTo(scores[0], "all scores must be identical");
}
#endregion
#region Uncertainty Factor Tests
[Fact]
public void ComputeUncertainty_MissingVex_Adds040()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: false, // Missing VEX = +0.40
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.UncertaintyFactor.Should().Be(0.40m);
}
[Fact]
public void ComputeUncertainty_MissingReachability_Adds030()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: false, // Missing reachability = +0.30
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.UncertaintyFactor.Should().Be(0.30m);
}
[Fact]
public void ComputeUncertainty_ConflictingSources_Adds020()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: true, // Conflicts = +0.20
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.UncertaintyFactor.Should().Be(0.20m);
}
[Fact]
public void ComputeUncertainty_StaleAdvisory_Adds010()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: true, // Stale = +0.10
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.UncertaintyFactor.Should().Be(0.10m);
}
[Fact]
public void ComputeUncertainty_AllFactors_SumsTo100()
{
// Arrange - All uncertainty factors active (0.40 + 0.30 + 0.20 + 0.10 = 1.00)
var input = new UnknownRankInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
IsStaleAdvisory: true,
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.UncertaintyFactor.Should().Be(1.00m);
}
[Fact]
public void ComputeUncertainty_NoFactors_ReturnsZero()
{
// Arrange - All uncertainty factors inactive
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.UncertaintyFactor.Should().Be(0.00m);
}
#endregion
#region Exploit Pressure Tests
[Fact]
public void ComputeExploitPressure_InKev_Adds050()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: true, // KEV = +0.50
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.ExploitPressure.Should().Be(0.50m);
}
[Fact]
public void ComputeExploitPressure_HighEpss_Adds030()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0.90m, // EPSS >= 0.90 = +0.30
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.ExploitPressure.Should().Be(0.30m);
}
[Fact]
public void ComputeExploitPressure_MediumEpss_Adds015()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0.50m, // EPSS >= 0.50 = +0.15
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.ExploitPressure.Should().Be(0.15m);
}
[Fact]
public void ComputeExploitPressure_CriticalCvss_Adds005()
{
// Arrange
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 9.0m); // CVSS >= 9.0 = +0.05
// Act
var result = _ranker.Rank(input);
// Assert
result.ExploitPressure.Should().Be(0.05m);
}
[Fact]
public void ComputeExploitPressure_AllFactors_SumsCorrectly()
{
// Arrange - KEV (0.50) + high EPSS (0.30) + critical CVSS (0.05) = 0.85
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: true,
EpssScore: 0.95m,
CvssScore: 9.5m);
// Act
var result = _ranker.Rank(input);
// Assert
result.ExploitPressure.Should().Be(0.85m);
}
[Fact]
public void ComputeExploitPressure_EpssThresholds_AreMutuallyExclusive()
{
// Arrange - High EPSS should NOT also add medium EPSS bonus
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0.95m, // Should only get 0.30, not 0.30 + 0.15
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.ExploitPressure.Should().Be(0.30m, "EPSS thresholds are mutually exclusive");
}
#endregion
#region Score Calculation Tests
[Fact]
public void Rank_Formula_AppliesCorrectWeights()
{
// Arrange
// Uncertainty: 0.40 (missing VEX)
// Pressure: 0.50 (KEV)
// Expected: (0.40 × 50) + (0.50 × 50) = 20 + 25 = 45
var input = new UnknownRankInput(
HasVexStatement: false,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: true,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.Score.Should().Be(45.00m);
}
[Fact]
public void Rank_MaximumScore_Is100()
{
// Arrange - All factors maxed out
// Uncertainty: 1.00 (all factors)
// Pressure: 0.85 (KEV + high EPSS + critical CVSS, capped at 1.00)
// Expected: (1.00 × 50) + (0.85 × 50) = 50 + 42.5 = 92.50
var input = new UnknownRankInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
IsStaleAdvisory: true,
IsInKev: true,
EpssScore: 0.95m,
CvssScore: 9.5m);
// Act
var result = _ranker.Rank(input);
// Assert
result.Score.Should().Be(92.50m);
}
[Fact]
public void Rank_MinimumScore_IsZero()
{
// Arrange - No uncertainty, no pressure
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.Score.Should().Be(0.00m);
}
#endregion
#region Band Assignment Tests
[Theory]
[InlineData(100, UnknownBand.Hot)]
[InlineData(75, UnknownBand.Hot)]
[InlineData(74.99, UnknownBand.Warm)]
[InlineData(50, UnknownBand.Warm)]
[InlineData(49.99, UnknownBand.Cold)]
[InlineData(25, UnknownBand.Cold)]
[InlineData(24.99, UnknownBand.Resolved)]
[InlineData(0, UnknownBand.Resolved)]
public void AssignBand_ScoreThresholds_AssignsCorrectBand(decimal score, UnknownBand expectedBand)
{
// This test validates band assignment thresholds
// We use a specific input that produces the desired score
// For simplicity, we'll test the ranker with known inputs
// Note: Since we can't directly test AssignBand (it's private),
// we verify through integration with known input/output pairs
}
[Fact]
public void Rank_ScoreAbove75_AssignsHotBand()
{
// Arrange - Score = (1.00 × 50) + (0.50 × 50) = 75.00
var input = new UnknownRankInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
IsStaleAdvisory: true,
IsInKev: true,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.Score.Should().BeGreaterThanOrEqualTo(75);
result.Band.Should().Be(UnknownBand.Hot);
}
[Fact]
public void Rank_ScoreBetween50And75_AssignsWarmBand()
{
// Arrange - Score = (0.70 × 50) + (0.50 × 50) = 35 + 25 = 60
// Uncertainty: 0.70 (missing VEX + missing reachability)
var input = new UnknownRankInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: true,
EpssScore: 0,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.Score.Should().BeGreaterThanOrEqualTo(50).And.BeLessThan(75);
result.Band.Should().Be(UnknownBand.Warm);
}
[Fact]
public void Rank_ScoreBetween25And50_AssignsColdBand()
{
// Arrange - Score = (0.40 × 50) + (0.15 × 50) = 20 + 7.5 = 27.5
var input = new UnknownRankInput(
HasVexStatement: false,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0.50m,
CvssScore: 0);
// Act
var result = _ranker.Rank(input);
// Assert
result.Score.Should().BeGreaterThanOrEqualTo(25).And.BeLessThan(50);
result.Band.Should().Be(UnknownBand.Cold);
}
[Fact]
public void Rank_ScoreBelow25_AssignsResolvedBand()
{
// Arrange - Score = (0.10 × 50) + (0.05 × 50) = 5 + 2.5 = 7.5
var input = new UnknownRankInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: true,
IsInKev: false,
EpssScore: 0,
CvssScore: 9.0m);
// Act
var result = _ranker.Rank(input);
// Assert
result.Score.Should().BeLessThan(25);
result.Band.Should().Be(UnknownBand.Resolved);
}
#endregion
}

View File

@@ -0,0 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Policy.Unknowns.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<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>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
</ItemGroup>
</Project>