Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Unknowns.Configuration;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
using StellaOps.Policy.Unknowns.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Unknowns.Tests.Services;
|
||||
|
||||
public sealed class UnknownBudgetServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetBudgetForEnvironment_KnownEnv_ReturnsBudget()
|
||||
{
|
||||
var service = CreateService(new UnknownBudget
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalLimit = 3
|
||||
});
|
||||
|
||||
var budget = service.GetBudgetForEnvironment("prod");
|
||||
|
||||
budget.TotalLimit.Should().Be(3);
|
||||
budget.Environment.Should().Be("prod");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudget_WithinLimit_ReturnsSuccess()
|
||||
{
|
||||
var service = CreateService(new UnknownBudget
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalLimit = 3,
|
||||
Action = BudgetAction.Block
|
||||
});
|
||||
|
||||
var result = service.CheckBudget("prod", CreateUnknowns(count: 2));
|
||||
|
||||
result.IsWithinBudget.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudget_ExceedsTotal_ReturnsViolation()
|
||||
{
|
||||
var service = CreateService(new UnknownBudget
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalLimit = 3,
|
||||
Action = BudgetAction.Block
|
||||
});
|
||||
|
||||
var result = service.CheckBudget("prod", CreateUnknowns(count: 5));
|
||||
|
||||
result.IsWithinBudget.Should().BeFalse();
|
||||
result.RecommendedAction.Should().Be(BudgetAction.Block);
|
||||
result.TotalUnknowns.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudget_ExceedsReasonLimit_ReturnsSpecificViolation()
|
||||
{
|
||||
var service = CreateService(new UnknownBudget
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalLimit = 5,
|
||||
ReasonLimits = new Dictionary<UnknownReasonCode, int>
|
||||
{
|
||||
[UnknownReasonCode.Reachability] = 0
|
||||
},
|
||||
Action = BudgetAction.Block
|
||||
});
|
||||
|
||||
var unknowns = CreateUnknowns(reachability: 2, identity: 1);
|
||||
var result = service.CheckBudget("prod", unknowns);
|
||||
|
||||
result.Violations.Should().ContainKey(UnknownReasonCode.Reachability);
|
||||
result.Violations[UnknownReasonCode.Reachability].Count.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudgetWithEscalation_ExceptionCovers_AllowsOperation()
|
||||
{
|
||||
var service = CreateService(new UnknownBudget
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalLimit = 1,
|
||||
ReasonLimits = new Dictionary<UnknownReasonCode, int>
|
||||
{
|
||||
[UnknownReasonCode.Reachability] = 0
|
||||
},
|
||||
Action = BudgetAction.WarnUnlessException
|
||||
});
|
||||
|
||||
var unknowns = CreateUnknowns(reachability: 1);
|
||||
var exceptions = new[] { CreateException(UnknownReasonCode.Reachability) };
|
||||
|
||||
var result = service.CheckBudgetWithEscalation("prod", unknowns, exceptions);
|
||||
|
||||
result.IsWithinBudget.Should().BeTrue();
|
||||
result.Message.Should().Contain("covered by approved exceptions");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldBlock_BlockAction_ReturnsTrue()
|
||||
{
|
||||
var service = CreateService(new UnknownBudget { Environment = "prod" });
|
||||
|
||||
var result = new BudgetCheckResult
|
||||
{
|
||||
IsWithinBudget = false,
|
||||
RecommendedAction = BudgetAction.Block,
|
||||
TotalUnknowns = 4
|
||||
};
|
||||
|
||||
service.ShouldBlock(result).Should().BeTrue();
|
||||
}
|
||||
|
||||
private static UnknownBudgetService CreateService(UnknownBudget prodBudget)
|
||||
{
|
||||
var options = new UnknownBudgetOptions
|
||||
{
|
||||
Budgets = new Dictionary<string, UnknownBudget>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["prod"] = prodBudget,
|
||||
["default"] = new UnknownBudget
|
||||
{
|
||||
Environment = "default",
|
||||
TotalLimit = 5,
|
||||
Action = BudgetAction.Warn
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return new UnknownBudgetService(
|
||||
new TestOptionsMonitor<UnknownBudgetOptions>(options),
|
||||
NullLogger<UnknownBudgetService>.Instance);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Unknown> CreateUnknowns(
|
||||
int count = 0,
|
||||
int reachability = 0,
|
||||
int identity = 0)
|
||||
{
|
||||
var results = new List<Unknown>();
|
||||
|
||||
results.AddRange(Enumerable.Range(0, reachability).Select(_ => CreateUnknown(UnknownReasonCode.Reachability)));
|
||||
results.AddRange(Enumerable.Range(0, identity).Select(_ => CreateUnknown(UnknownReasonCode.Identity)));
|
||||
|
||||
var remaining = Math.Max(0, count - results.Count);
|
||||
results.AddRange(Enumerable.Range(0, remaining).Select(_ => CreateUnknown(UnknownReasonCode.FeedGap)));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static Unknown CreateUnknown(UnknownReasonCode reasonCode)
|
||||
{
|
||||
var timestamp = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
return new Unknown
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = Guid.NewGuid(),
|
||||
PackageId = "pkg:npm/lodash",
|
||||
PackageVersion = "4.17.21",
|
||||
Band = UnknownBand.Hot,
|
||||
Score = 80m,
|
||||
UncertaintyFactor = 0.5m,
|
||||
ExploitPressure = 0.7m,
|
||||
ReasonCode = reasonCode,
|
||||
FirstSeenAt = timestamp,
|
||||
LastEvaluatedAt = timestamp,
|
||||
CreatedAt = timestamp,
|
||||
UpdatedAt = timestamp
|
||||
};
|
||||
}
|
||||
|
||||
private static ExceptionObject CreateException(UnknownReasonCode reasonCode)
|
||||
{
|
||||
var now = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
return new ExceptionObject
|
||||
{
|
||||
ExceptionId = "EXC-UNKNOWN-001",
|
||||
Version = 1,
|
||||
Status = ExceptionStatus.Approved,
|
||||
Type = ExceptionType.Unknown,
|
||||
Scope = new ExceptionScope
|
||||
{
|
||||
Environments = ImmutableArray.Create("prod")
|
||||
},
|
||||
OwnerId = "owner",
|
||||
RequesterId = "requester",
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
ExpiresAt = now.AddDays(30),
|
||||
ReasonCode = ExceptionReason.AcceptedRisk,
|
||||
Rationale = "Approved exception for unknown budget coverage",
|
||||
Metadata = ImmutableDictionary<string, string>.Empty
|
||||
.Add("unknown_reason_codes", reasonCode.ToString())
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestOptionsMonitor<T>(T current) : IOptionsMonitor<T>
|
||||
{
|
||||
private readonly T _current = current;
|
||||
|
||||
public T CurrentValue => _current;
|
||||
public T Get(string? name) => _current;
|
||||
public IDisposable OnChange(Action<T, string?> listener) => NoopDisposable.Instance;
|
||||
}
|
||||
|
||||
private sealed class NoopDisposable : IDisposable
|
||||
{
|
||||
public static readonly NoopDisposable Instance = new();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
using StellaOps.Policy.Unknowns.Services;
|
||||
|
||||
@@ -10,6 +11,7 @@ namespace StellaOps.Policy.Unknowns.Tests.Services;
|
||||
public class UnknownRankerTests
|
||||
{
|
||||
private readonly UnknownRanker _ranker = new();
|
||||
private static readonly DateTimeOffset DefaultAsOf = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
@@ -17,7 +19,7 @@ public class UnknownRankerTests
|
||||
public void Rank_SameInput_ReturnsSameResult()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: true,
|
||||
@@ -38,7 +40,7 @@ public class UnknownRankerTests
|
||||
public void Rank_MultipleExecutions_ProducesIdenticalScores()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: false,
|
||||
@@ -67,7 +69,7 @@ public class UnknownRankerTests
|
||||
public void ComputeUncertainty_MissingVex_Adds040()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false, // Missing VEX = +0.40
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -87,7 +89,7 @@ public class UnknownRankerTests
|
||||
public void ComputeUncertainty_MissingReachability_Adds030()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: false, // Missing reachability = +0.30
|
||||
HasConflictingSources: false,
|
||||
@@ -107,7 +109,7 @@ public class UnknownRankerTests
|
||||
public void ComputeUncertainty_ConflictingSources_Adds020()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: true, // Conflicts = +0.20
|
||||
@@ -127,7 +129,7 @@ public class UnknownRankerTests
|
||||
public void ComputeUncertainty_StaleAdvisory_Adds010()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -147,7 +149,7 @@ public class UnknownRankerTests
|
||||
public void ComputeUncertainty_AllFactors_SumsTo100()
|
||||
{
|
||||
// Arrange - All uncertainty factors active (0.40 + 0.30 + 0.20 + 0.10 = 1.00)
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: true,
|
||||
@@ -167,7 +169,7 @@ public class UnknownRankerTests
|
||||
public void ComputeUncertainty_NoFactors_ReturnsZero()
|
||||
{
|
||||
// Arrange - All uncertainty factors inactive
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -191,7 +193,7 @@ public class UnknownRankerTests
|
||||
public void ComputeExploitPressure_InKev_Adds050()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -211,7 +213,7 @@ public class UnknownRankerTests
|
||||
public void ComputeExploitPressure_HighEpss_Adds030()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -231,7 +233,7 @@ public class UnknownRankerTests
|
||||
public void ComputeExploitPressure_MediumEpss_Adds015()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -251,7 +253,7 @@ public class UnknownRankerTests
|
||||
public void ComputeExploitPressure_CriticalCvss_Adds005()
|
||||
{
|
||||
// Arrange
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -271,7 +273,7 @@ public class UnknownRankerTests
|
||||
public void ComputeExploitPressure_AllFactors_SumsCorrectly()
|
||||
{
|
||||
// Arrange - KEV (0.50) + high EPSS (0.30) + critical CVSS (0.05) = 0.85
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -291,7 +293,7 @@ public class UnknownRankerTests
|
||||
public void ComputeExploitPressure_EpssThresholds_AreMutuallyExclusive()
|
||||
{
|
||||
// Arrange - High EPSS should NOT also add medium EPSS bonus
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -318,7 +320,7 @@ public class UnknownRankerTests
|
||||
// Uncertainty: 0.40 (missing VEX)
|
||||
// Pressure: 0.50 (KEV)
|
||||
// Expected: (0.40 × 50) + (0.50 × 50) = 20 + 25 = 45
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -341,7 +343,7 @@ public class UnknownRankerTests
|
||||
// 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(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: true,
|
||||
@@ -361,7 +363,7 @@ public class UnknownRankerTests
|
||||
public void Rank_MinimumScore_IsZero()
|
||||
{
|
||||
// Arrange - No uncertainty, no pressure
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -379,6 +381,198 @@ public class UnknownRankerTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reason Code Tests
|
||||
|
||||
[Fact]
|
||||
public void Rank_AnalyzerUnsupported_AssignsAnalyzerLimit()
|
||||
{
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
IsStaleAdvisory: false,
|
||||
IsInKev: false,
|
||||
EpssScore: 0,
|
||||
CvssScore: 0,
|
||||
IsAnalyzerSupported: false);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.ReasonCode.Should().Be(UnknownReasonCode.AnalyzerLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rank_MissingReachability_AssignsReachability()
|
||||
{
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: false,
|
||||
IsStaleAdvisory: false,
|
||||
IsInKev: false,
|
||||
EpssScore: 0,
|
||||
CvssScore: 0);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.ReasonCode.Should().Be(UnknownReasonCode.Reachability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rank_MissingDigest_AssignsIdentity()
|
||||
{
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
IsStaleAdvisory: false,
|
||||
IsInKev: false,
|
||||
EpssScore: 0,
|
||||
CvssScore: 0,
|
||||
HasPackageDigest: false);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.ReasonCode.Should().Be(UnknownReasonCode.Identity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Decay Factor Tests
|
||||
|
||||
[Fact]
|
||||
public void ComputeDecay_NullLastEvaluated_Returns100Percent()
|
||||
{
|
||||
var input = CreateInputWithAge(lastEvaluatedAt: null);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.DecayFactor.Should().Be(1.00m);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 1.00)]
|
||||
[InlineData(7, 1.00)]
|
||||
[InlineData(8, 0.90)]
|
||||
[InlineData(30, 0.90)]
|
||||
[InlineData(31, 0.75)]
|
||||
[InlineData(90, 0.75)]
|
||||
[InlineData(91, 0.60)]
|
||||
[InlineData(180, 0.60)]
|
||||
[InlineData(181, 0.40)]
|
||||
[InlineData(365, 0.40)]
|
||||
[InlineData(366, 0.20)]
|
||||
[InlineData(1000, 0.20)]
|
||||
public void ComputeDecay_AgeBuckets_ReturnsCorrectMultiplier(int ageDays, decimal expected)
|
||||
{
|
||||
var input = CreateInputWithAge(ageDays: ageDays);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.DecayFactor.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rank_WithDecay_AppliesMultiplierToScore()
|
||||
{
|
||||
var input = CreateHighScoreInput(ageDays: 100);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.Score.Should().Be(30.00m);
|
||||
result.DecayFactor.Should().Be(0.60m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rank_DecayDisabled_ReturnsFullScore()
|
||||
{
|
||||
var options = new UnknownRankerOptions { EnableDecay = false };
|
||||
var ranker = new UnknownRanker(Options.Create(options));
|
||||
var input = CreateHighScoreInput(ageDays: 100);
|
||||
|
||||
var result = ranker.Rank(input);
|
||||
|
||||
result.DecayFactor.Should().Be(1.0m);
|
||||
result.Score.Should().Be(50.00m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rank_Decay_Determinism_SameInputSameOutput()
|
||||
{
|
||||
var input = CreateInputWithAge(ageDays: 45);
|
||||
|
||||
var results = Enumerable.Range(0, 100)
|
||||
.Select(_ => _ranker.Rank(input))
|
||||
.ToList();
|
||||
|
||||
results.Should().AllBeEquivalentTo(results[0]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Containment Reduction Tests
|
||||
|
||||
[Fact]
|
||||
public void ComputeContainmentReduction_NullInputs_ReturnsZero()
|
||||
{
|
||||
var input = CreateInputWithContainment(blastRadius: null, containment: null);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.ContainmentReduction.Should().Be(0m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeContainmentReduction_IsolatedPackage_Returns15Percent()
|
||||
{
|
||||
var blast = new BlastRadius { Dependents = 0, NetFacing = true };
|
||||
var input = CreateInputWithContainment(blastRadius: blast);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.ContainmentReduction.Should().Be(0.15m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeContainmentReduction_AllContainmentFactors_CapsAt40Percent()
|
||||
{
|
||||
var blast = new BlastRadius { Dependents = 0, NetFacing = false, Privilege = "none" };
|
||||
var contain = new ContainmentSignals { Seccomp = "enforced", FileSystem = "ro", NetworkPolicy = "isolated" };
|
||||
var input = CreateInputWithContainment(blastRadius: blast, containment: contain);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.ContainmentReduction.Should().Be(0.40m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rank_WithContainment_AppliesReductionToScore()
|
||||
{
|
||||
var blast = new BlastRadius { Dependents = 0 };
|
||||
var input = CreateHighScoreInputWithContainment(blast);
|
||||
|
||||
var result = _ranker.Rank(input);
|
||||
|
||||
result.Score.Should().Be(48.00m);
|
||||
result.ContainmentReduction.Should().Be(0.20m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rank_ContainmentDisabled_NoReduction()
|
||||
{
|
||||
var options = new UnknownRankerOptions { EnableContainmentReduction = false };
|
||||
var ranker = new UnknownRanker(Options.Create(options));
|
||||
var blast = new BlastRadius { Dependents = 0 };
|
||||
var input = CreateHighScoreInputWithContainment(blast);
|
||||
|
||||
var result = ranker.Rank(input);
|
||||
|
||||
result.ContainmentReduction.Should().Be(0m);
|
||||
result.Score.Should().Be(60.00m);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Band Assignment Tests
|
||||
|
||||
[Theory]
|
||||
@@ -404,7 +598,7 @@ public class UnknownRankerTests
|
||||
public void Rank_ScoreAbove75_AssignsHotBand()
|
||||
{
|
||||
// Arrange - Score = (1.00 × 50) + (0.50 × 50) = 75.00
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: true,
|
||||
@@ -426,7 +620,7 @@ public class UnknownRankerTests
|
||||
{
|
||||
// Arrange - Score = (0.70 × 50) + (0.50 × 50) = 35 + 25 = 60
|
||||
// Uncertainty: 0.70 (missing VEX + missing reachability)
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: false,
|
||||
@@ -447,7 +641,7 @@ public class UnknownRankerTests
|
||||
public void Rank_ScoreBetween25And50_AssignsColdBand()
|
||||
{
|
||||
// Arrange - Score = (0.40 × 50) + (0.15 × 50) = 20 + 7.5 = 27.5
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -468,7 +662,7 @@ public class UnknownRankerTests
|
||||
public void Rank_ScoreBelow25_AssignsResolvedBand()
|
||||
{
|
||||
// Arrange - Score = (0.10 × 50) + (0.05 × 50) = 5 + 2.5 = 7.5
|
||||
var input = new UnknownRankInput(
|
||||
var input = CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
@@ -486,4 +680,113 @@ public class UnknownRankerTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static UnknownRankInput CreateInput(
|
||||
bool HasVexStatement,
|
||||
bool HasReachabilityData,
|
||||
bool HasConflictingSources,
|
||||
bool IsStaleAdvisory,
|
||||
bool IsInKev,
|
||||
decimal EpssScore,
|
||||
decimal CvssScore,
|
||||
DateTimeOffset? FirstSeenAt = null,
|
||||
DateTimeOffset? LastEvaluatedAt = null,
|
||||
DateTimeOffset? AsOfDateTime = null,
|
||||
BlastRadius? BlastRadius = null,
|
||||
ContainmentSignals? Containment = null,
|
||||
bool HasPackageDigest = true,
|
||||
bool HasProvenanceAttestation = true,
|
||||
bool HasVexConflicts = false,
|
||||
bool HasFeedCoverage = true,
|
||||
bool HasConfigVisibility = true,
|
||||
bool IsAnalyzerSupported = true)
|
||||
{
|
||||
var asOf = AsOfDateTime ?? DefaultAsOf;
|
||||
|
||||
return new UnknownRankInput(
|
||||
HasVexStatement,
|
||||
HasReachabilityData,
|
||||
HasConflictingSources,
|
||||
IsStaleAdvisory,
|
||||
IsInKev,
|
||||
EpssScore,
|
||||
CvssScore,
|
||||
FirstSeenAt,
|
||||
LastEvaluatedAt,
|
||||
asOf,
|
||||
BlastRadius,
|
||||
Containment,
|
||||
HasPackageDigest,
|
||||
HasProvenanceAttestation,
|
||||
HasVexConflicts,
|
||||
HasFeedCoverage,
|
||||
HasConfigVisibility,
|
||||
IsAnalyzerSupported);
|
||||
}
|
||||
|
||||
private static UnknownRankInput CreateInputWithAge(
|
||||
int? ageDays = null,
|
||||
DateTimeOffset? lastEvaluatedAt = null,
|
||||
DateTimeOffset? asOfDateTime = null)
|
||||
{
|
||||
var asOf = asOfDateTime ?? DefaultAsOf;
|
||||
var evaluatedAt = lastEvaluatedAt ?? (ageDays.HasValue ? asOf.AddDays(-ageDays.Value) : null);
|
||||
|
||||
return CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
IsStaleAdvisory: false,
|
||||
IsInKev: false,
|
||||
EpssScore: 0,
|
||||
CvssScore: 0,
|
||||
LastEvaluatedAt: evaluatedAt,
|
||||
AsOfDateTime: asOf);
|
||||
}
|
||||
|
||||
private static UnknownRankInput CreateHighScoreInput(int ageDays)
|
||||
{
|
||||
var asOf = DefaultAsOf;
|
||||
|
||||
return CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: true,
|
||||
IsStaleAdvisory: true,
|
||||
IsInKev: false,
|
||||
EpssScore: 0,
|
||||
CvssScore: 0,
|
||||
LastEvaluatedAt: asOf.AddDays(-ageDays),
|
||||
AsOfDateTime: asOf);
|
||||
}
|
||||
|
||||
private static UnknownRankInput CreateInputWithContainment(
|
||||
BlastRadius? blastRadius = null,
|
||||
ContainmentSignals? containment = null)
|
||||
{
|
||||
return CreateInput(
|
||||
HasVexStatement: true,
|
||||
HasReachabilityData: true,
|
||||
HasConflictingSources: false,
|
||||
IsStaleAdvisory: false,
|
||||
IsInKev: false,
|
||||
EpssScore: 0,
|
||||
CvssScore: 0,
|
||||
BlastRadius: blastRadius,
|
||||
Containment: containment);
|
||||
}
|
||||
|
||||
private static UnknownRankInput CreateHighScoreInputWithContainment(BlastRadius blastRadius)
|
||||
{
|
||||
return CreateInput(
|
||||
HasVexStatement: false,
|
||||
HasReachabilityData: false,
|
||||
HasConflictingSources: false,
|
||||
IsStaleAdvisory: false,
|
||||
IsInKev: true,
|
||||
EpssScore: 0,
|
||||
CvssScore: 0,
|
||||
BlastRadius: blastRadius);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user