more features checks. setup improvements

This commit is contained in:
master
2026-02-13 02:04:55 +02:00
parent 9911b7d73c
commit 9ca2de05df
675 changed files with 37550 additions and 1826 deletions

View File

@@ -0,0 +1,443 @@
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.Policy.Confidence.Configuration;
using StellaOps.Policy.Confidence.Services;
using StellaOps.Policy.Engine.Evaluation;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Engine.Scoring.Engines;
using StellaOps.Policy.Engine.Scoring;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Unknowns.Configuration;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.Policy.Unknowns.Services;
using StellaOps.PolicyDsl;
using Xunit;
namespace StellaOps.Policy.Engine.Tests;
/// <summary>
/// Deep verification tests for the declarative multi-modal policy engine feature.
/// Covers end-to-end DSL compilation + evaluation, scoring engine factory,
/// multi-gate integration, and deterministic evaluation.
/// </summary>
public sealed class DeclarativeMultiModalPolicyEngineDeepTests
{
private static readonly string MultiGatePolicy = """
policy "Multi-Gate Production" syntax "stella-dsl@1" {
metadata {
description = "Multi-modal policy with CVSS, VEX, and severity gates"
tags = ["production","multi-gate"]
}
rule block_critical priority 100 {
when severity.normalized >= "Critical"
then status := "blocked"
because "Critical findings must be fixed."
}
rule escalate_high_internet priority 90 {
when severity.normalized == "High"
and env.exposure == "internet"
then escalate to severity_band("Critical")
because "High on internet-exposed asset escalates."
}
rule accept_vex_not_affected priority 80 {
when vex.any(status in ["not_affected","fixed"])
and vex.justification in ["component_not_present","vulnerable_code_not_present"]
then status := vex.status
annotate winning_statement := vex.latest().statementId
because "Respect strong vendor VEX claims."
}
rule warn_medium priority 50 {
when severity.normalized == "Medium"
then warn message "Medium severity finding needs review."
because "Medium findings require attention."
}
rule allow_low priority 10 {
when severity.normalized <= "Low"
then status := "affected"
because "Low severity accepted."
}
}
""";
private readonly PolicyCompiler _compiler = new();
private readonly PolicyEvaluationService _evaluationService = new();
#region End-to-End DSL Compilation + Evaluation
[Fact]
[Trait("Category", "Unit")]
public void CompileAndEvaluate_CriticalSeverity_BlocksWithCorrectRule()
{
var document = CompilePolicy(MultiGatePolicy);
var context = CreateContext(severity: "Critical", exposure: "internal");
var result = _evaluationService.Evaluate(document, context);
result.Matched.Should().BeTrue();
result.RuleName.Should().Be("block_critical");
result.Status.Should().Be("blocked");
}
[Fact]
[Trait("Category", "Unit")]
public void CompileAndEvaluate_HighInternet_EscalatesToCritical()
{
var document = CompilePolicy(MultiGatePolicy);
var context = CreateContext(severity: "High", exposure: "internet");
var result = _evaluationService.Evaluate(document, context);
result.Matched.Should().BeTrue();
result.RuleName.Should().Be("escalate_high_internet");
result.Severity.Should().Be("Critical");
}
[Fact]
[Trait("Category", "Unit")]
public void CompileAndEvaluate_VexNotAffected_SetsStatusAndAnnotation()
{
var document = CompilePolicy(MultiGatePolicy);
var statements = ImmutableArray.Create(
new PolicyEvaluationVexStatement("not_affected", "component_not_present", "stmt-vex-001"));
// Use "High" + "internal" so no lower-priority rule matches first.
// Rules are evaluated in ascending priority order; warn_medium (50)
// would fire before accept_vex_not_affected (80) with "Medium" severity.
var context = CreateContext("High", "internal") with
{
Vex = new PolicyEvaluationVexEvidence(statements)
};
var result = _evaluationService.Evaluate(document, context);
result.Matched.Should().BeTrue();
result.RuleName.Should().Be("accept_vex_not_affected");
result.Status.Should().Be("not_affected");
result.Annotations.Should().ContainKey("winning_statement");
result.Annotations["winning_statement"].Should().Be("stmt-vex-001");
}
[Fact]
[Trait("Category", "Unit")]
public void CompileAndEvaluate_MediumSeverity_EmitsWarning()
{
var document = CompilePolicy(MultiGatePolicy);
var context = CreateContext(severity: "Medium", exposure: "internal");
var result = _evaluationService.Evaluate(document, context);
result.Matched.Should().BeTrue();
result.RuleName.Should().Be("warn_medium");
result.Status.Should().Be("warned");
result.Warnings.Should().Contain(w => w.Contains("Medium severity"));
}
[Fact]
[Trait("Category", "Unit")]
public void CompileAndEvaluate_LowSeverity_Allows()
{
var document = CompilePolicy(MultiGatePolicy);
var context = CreateContext(severity: "Low", exposure: "internal");
var result = _evaluationService.Evaluate(document, context);
result.Matched.Should().BeTrue();
result.RuleName.Should().Be("allow_low");
result.Status.Should().Be("affected");
}
#endregion
#region Policy DSL Compilation Verification
[Fact]
[Trait("Category", "Unit")]
public void Compile_MultiGatePolicy_ParsesAllRulesAndMetadata()
{
var result = _compiler.Compile(MultiGatePolicy);
result.Success.Should().BeTrue();
result.Document.Should().NotBeNull();
result.Document!.Name.Should().Be("Multi-Gate Production");
result.Document.Syntax.Should().Be("stella-dsl@1");
result.Document.Rules.Should().HaveCountGreaterThanOrEqualTo(5);
result.Document.Metadata.Should().ContainKey("description");
result.Checksum.Should().NotBeNullOrEmpty();
}
[Fact]
[Trait("Category", "Unit")]
public void Compile_InvalidPolicy_ReturnsDiagnostics()
{
var invalid = """
policy "broken" syntax "stella-dsl@1" {
rule missing_when priority 1 {
then status := "blocked"
because "missing when clause"
}
}
""";
var result = _compiler.Compile(invalid);
result.Success.Should().BeFalse();
result.Diagnostics.Should().NotBeEmpty();
}
[Fact]
[Trait("Category", "Unit")]
public void Compile_SameSource_ProducesSameChecksum()
{
var result1 = _compiler.Compile(MultiGatePolicy);
var result2 = _compiler.Compile(MultiGatePolicy);
result1.Checksum.Should().Be(result2.Checksum);
}
#endregion
#region Priority Ordering
[Fact]
[Trait("Category", "Unit")]
public void Evaluate_RulesExecuteInPriorityOrder_HighestFirst()
{
// Critical matches block_critical (priority 100) before warn_medium (priority 50)
var document = CompilePolicy(MultiGatePolicy);
var context = CreateContext(severity: "Critical", exposure: "internet");
var result = _evaluationService.Evaluate(document, context);
// block_critical (100) should fire before escalate_high_internet (90) because
// severity >= Critical matches first
result.RuleName.Should().Be("block_critical");
}
#endregion
#region Exception Handling Integration
[Fact]
[Trait("Category", "Unit")]
public void Evaluate_WithSuppressException_SuppressesBlockedFinding()
{
var document = CompilePolicy(MultiGatePolicy);
var effect = new PolicyExceptionEffect(
Id: "suppress-critical",
Name: "Emergency Break Glass",
Effect: PolicyExceptionEffectType.Suppress,
DowngradeSeverity: null,
RequiredControlId: null,
RoutingTemplate: "secops",
MaxDurationDays: 7,
Description: null);
var scope = PolicyEvaluationExceptionScope.Create(ruleNames: new[] { "block_critical" });
var instance = new PolicyEvaluationExceptionInstance(
Id: "exc-deep-001",
EffectId: effect.Id,
Scope: scope,
CreatedAt: new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero),
Metadata: ImmutableDictionary<string, string>.Empty);
var exceptions = new PolicyEvaluationExceptions(
ImmutableDictionary<string, PolicyExceptionEffect>.Empty.Add(effect.Id, effect),
ImmutableArray.Create(instance));
var context = CreateContext("Critical", "internal", exceptions);
var result = _evaluationService.Evaluate(document, context);
result.Matched.Should().BeTrue();
result.Status.Should().Be("suppressed");
result.AppliedException.Should().NotBeNull();
result.AppliedException!.ExceptionId.Should().Be("exc-deep-001");
}
#endregion
#region Scoring Engine Integration
[Fact]
[Trait("Category", "Unit")]
public void SimpleScoringEngine_Profile_ReturnsSimple()
{
var freshnessCalc = new EvidenceFreshnessCalculator();
var engine = new SimpleScoringEngine(freshnessCalc, NullLogger<SimpleScoringEngine>.Instance);
engine.Profile.Should().Be(ScoringProfile.Simple);
}
[Fact]
[Trait("Category", "Unit")]
public void AdvancedScoringEngine_Profile_ReturnsAdvanced()
{
var freshnessCalc = new EvidenceFreshnessCalculator();
var engine = new AdvancedScoringEngine(freshnessCalc, NullLogger<AdvancedScoringEngine>.Instance);
engine.Profile.Should().Be(ScoringProfile.Advanced);
}
#endregion
#region Unknown Budget Integration
[Fact]
[Trait("Category", "Unit")]
public void Evaluate_UnknownBudgetExceeded_BlocksEvaluation()
{
var document = CompilePolicy(MultiGatePolicy);
var budgetService = CreateBudgetService(totalLimit: 0, action: BudgetAction.Block);
var evaluator = new PolicyEvaluator(budgetService: budgetService);
var context = new PolicyEvaluationContext(
new PolicyEvaluationSeverity("High"),
new PolicyEvaluationEnvironment(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["name"] = "prod"
}.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)),
new PolicyEvaluationAdvisory("GHSA", ImmutableDictionary<string, string>.Empty),
PolicyEvaluationVexEvidence.Empty,
PolicyEvaluationSbom.Empty,
PolicyEvaluationExceptions.Empty,
ImmutableArray.Create(CreateUnknown()),
ImmutableArray<ExceptionObject>.Empty,
PolicyEvaluationReachability.Unknown,
PolicyEvaluationEntropy.Unknown);
var result = evaluator.Evaluate(new PolicyEvaluationRequest(document, context));
result.Status.Should().Be("blocked");
result.FailureReason.Should().Be(PolicyFailureReason.UnknownBudgetExceeded);
}
#endregion
#region Determinism
[Fact]
[Trait("Category", "Unit")]
public void Evaluate_100Iterations_ProducesIdenticalResults()
{
var document = CompilePolicy(MultiGatePolicy);
var context = CreateContext(severity: "High", exposure: "internet");
var first = _evaluationService.Evaluate(document, context);
for (var i = 1; i < 100; i++)
{
var current = _evaluationService.Evaluate(document, context);
current.RuleName.Should().Be(first.RuleName, $"iteration {i}");
current.Status.Should().Be(first.Status, $"iteration {i}");
current.Severity.Should().Be(first.Severity, $"iteration {i}");
}
}
[Fact]
[Trait("Category", "Unit")]
public void Compile_100Iterations_ProducesIdenticalChecksum()
{
var first = _compiler.Compile(MultiGatePolicy);
for (var i = 1; i < 100; i++)
{
var current = _compiler.Compile(MultiGatePolicy);
current.Checksum.Should().Be(first.Checksum, $"iteration {i}");
}
}
#endregion
#region Helpers
private PolicyIrDocument CompilePolicy(string source)
{
var result = _compiler.Compile(source);
result.Success.Should().BeTrue(
string.Join("; ", result.Diagnostics.Select(d => $"{d.Severity}:{d.Code}:{d.Message}")));
return (PolicyIrDocument)result.Document!;
}
private static PolicyEvaluationContext CreateContext(
string severity,
string exposure,
PolicyEvaluationExceptions? exceptions = null)
{
return new PolicyEvaluationContext(
new PolicyEvaluationSeverity(severity),
new PolicyEvaluationEnvironment(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["exposure"] = exposure
}.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)),
new PolicyEvaluationAdvisory("GHSA", ImmutableDictionary<string, string>.Empty),
PolicyEvaluationVexEvidence.Empty,
PolicyEvaluationSbom.Empty,
exceptions ?? PolicyEvaluationExceptions.Empty,
ImmutableArray<Unknown>.Empty,
ImmutableArray<ExceptionObject>.Empty,
PolicyEvaluationReachability.Unknown,
PolicyEvaluationEntropy.Unknown);
}
private static UnknownBudgetService CreateBudgetService(int totalLimit, BudgetAction action)
{
var options = new UnknownBudgetOptions
{
Budgets = new Dictionary<string, UnknownBudget>(StringComparer.OrdinalIgnoreCase)
{
["prod"] = new UnknownBudget
{
Environment = "prod",
TotalLimit = totalLimit,
Action = action
}
}
};
return new UnknownBudgetService(
new TestOptionsMonitor<UnknownBudgetOptions>(options),
NullLogger<UnknownBudgetService>.Instance);
}
private static Unknown CreateUnknown()
{
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 = UnknownReasonCode.Reachability,
FirstSeenAt = timestamp,
LastEvaluatedAt = timestamp,
CreatedAt = timestamp,
UpdatedAt = timestamp
};
}
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() { }
}
#endregion
}

View File

@@ -0,0 +1,521 @@
using FluentAssertions;
using StellaOps.Policy.Engine.DeterminismGuard;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.DeterminismGuard;
/// <summary>
/// Deep verification tests for determinism guards covering pattern detection gaps,
/// ValidateContext, FailOnSeverity threshold, GuardedPolicyEvaluatorBuilder,
/// floating-point/unstable-iteration warnings, socket detection, and scope lifecycle.
/// </summary>
public sealed class DeterminismGuardDeepTests
{
#region Additional Pattern Detection
[Fact]
public void AnalyzeSource_DetectsDateTimeOffsetNow()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var now = DateTimeOffset.Now;";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "DateTimeOffset.Now" &&
v.Category == DeterminismViolationCategory.WallClock);
}
[Fact]
public void AnalyzeSource_DetectsDateTimeOffsetUtcNow()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var now = DateTimeOffset.UtcNow;";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "DateTimeOffset.UtcNow" &&
v.Category == DeterminismViolationCategory.WallClock);
}
[Fact]
public void AnalyzeSource_DetectsCryptoRandom()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var bytes = RandomNumberGenerator.GetBytes(32);";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "RandomNumberGenerator" &&
v.Category == DeterminismViolationCategory.RandomNumber);
}
[Fact]
public void AnalyzeSource_DetectsSocketClasses()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = """
var tcp = new TcpClient("localhost", 80);
var udp = new UdpClient(9090);
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
""";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().HaveCount(3);
result.Violations.Should().OnlyContain(v =>
v.Category == DeterminismViolationCategory.NetworkAccess &&
v.Severity == DeterminismViolationSeverity.Critical);
}
[Fact]
public void AnalyzeSource_DetectsWebClient()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "using var client = new WebClient();";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "WebClient" &&
v.Category == DeterminismViolationCategory.NetworkAccess);
}
[Fact]
public void AnalyzeSource_DetectsEnvironmentMachineName()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var name = Environment.MachineName;";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "Environment.MachineName" &&
v.Category == DeterminismViolationCategory.EnvironmentAccess &&
v.Severity == DeterminismViolationSeverity.Warning);
}
[Fact]
public void AnalyzeSource_DetectsFloatingPointComparison()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "double score == 7.5;";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.Category == DeterminismViolationCategory.FloatingPointHazard &&
v.Severity == DeterminismViolationSeverity.Warning);
}
[Fact]
public void AnalyzeSource_DetectsDictionaryIteration()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "foreach (var item in myDictionary)";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.Category == DeterminismViolationCategory.UnstableIteration);
}
[Fact]
public void AnalyzeSource_DetectsHashSetIteration()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "foreach (var item in myHashSet)";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.Category == DeterminismViolationCategory.UnstableIteration);
}
[Fact]
public void AnalyzeSource_MultipleViolationCategories_ReportsAll()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = """
var now = DateTime.Now;
var rng = new Random();
var id = Guid.NewGuid();
private readonly HttpClient _client = new();
""";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().HaveCountGreaterThanOrEqualTo(4);
result.Violations.Select(v => v.Category).Distinct()
.Should().Contain(DeterminismViolationCategory.WallClock)
.And.Contain(DeterminismViolationCategory.RandomNumber)
.And.Contain(DeterminismViolationCategory.GuidGeneration)
.And.Contain(DeterminismViolationCategory.NetworkAccess);
}
#endregion
#region ValidateContext Tests
[Fact]
public void ValidateContext_NullContext_DetectsViolation()
{
var guard = new DeterminismGuardService();
var result = guard.ValidateContext<object>(null!, "TestContext");
result.Passed.Should().BeFalse();
result.Violations.Should().ContainSingle(v =>
v.Category == DeterminismViolationCategory.Other &&
v.ViolationType == "NullContext" &&
v.Message.Contains("TestContext"));
}
[Fact]
public void ValidateContext_ValidContext_Passes()
{
var guard = new DeterminismGuardService();
var result = guard.ValidateContext(new { Score = 7.5 }, "ScoringContext");
result.Passed.Should().BeTrue();
result.Violations.Should().BeEmpty();
}
[Fact]
public void ValidateContext_EnforcementDisabled_NullContextPassesButReportsViolation()
{
var options = new DeterminismGuardOptions { EnforcementEnabled = false };
var guard = new DeterminismGuardService(options);
var result = guard.ValidateContext<object>(null!, "TestContext");
result.Passed.Should().BeTrue(); // Enforcement disabled = always passes
result.Violations.Should().NotBeEmpty(); // But still reports violations
}
#endregion
#region FailOnSeverity Threshold Tests
[Fact]
public void FailOnSeverity_Error_WarningViolationsDoNotCauseFailure()
{
var options = new DeterminismGuardOptions
{
EnforcementEnabled = true,
FailOnSeverity = DeterminismViolationSeverity.Error
};
var analyzer = new ProhibitedPatternAnalyzer();
// Environment.MachineName is a Warning-level violation
var source = "var name = Environment.MachineName;";
var result = analyzer.AnalyzeSource(source, "test.cs", options);
result.Passed.Should().BeTrue(); // Warning < Error threshold
result.Violations.Should().NotBeEmpty();
}
[Fact]
public void FailOnSeverity_Error_ErrorViolationsCauseFailure()
{
var options = new DeterminismGuardOptions
{
EnforcementEnabled = true,
FailOnSeverity = DeterminismViolationSeverity.Error
};
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var now = DateTime.Now;";
var result = analyzer.AnalyzeSource(source, "test.cs", options);
result.Passed.Should().BeFalse(); // Error >= Error threshold
}
[Fact]
public void FailOnSeverity_Critical_ErrorViolationsDoNotCauseFailure()
{
var options = new DeterminismGuardOptions
{
EnforcementEnabled = true,
FailOnSeverity = DeterminismViolationSeverity.Critical
};
var analyzer = new ProhibitedPatternAnalyzer();
// DateTime.Now is Error severity
var source = "var now = DateTime.Now;";
var result = analyzer.AnalyzeSource(source, "test.cs", options);
result.Passed.Should().BeTrue(); // Error < Critical threshold
}
[Fact]
public void FailOnSeverity_Critical_CriticalViolationsCauseFailure()
{
var options = new DeterminismGuardOptions
{
EnforcementEnabled = true,
FailOnSeverity = DeterminismViolationSeverity.Critical
};
var analyzer = new ProhibitedPatternAnalyzer();
// HttpClient is Critical severity
var source = "private readonly HttpClient _client = new();";
var result = analyzer.AnalyzeSource(source, "test.cs", options);
result.Passed.Should().BeFalse(); // Critical >= Critical threshold
}
#endregion
#region GuardedPolicyEvaluatorBuilder Tests
[Fact]
public void Builder_CreateDevelopment_HasNoEnforcement()
{
var evaluator = GuardedPolicyEvaluatorBuilder.CreateDevelopment();
// Development mode: no enforcement, so reporting a critical violation should not throw
var result = evaluator.Evaluate("dev-scope", DateTimeOffset.UtcNow, scope =>
{
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.NetworkAccess,
ViolationType = "HttpClient",
Message = "Dev mode test",
Severity = DeterminismViolationSeverity.Critical
});
return "ok";
});
result.Succeeded.Should().BeTrue(); // Enforcement disabled in dev mode
result.Result.Should().Be("ok");
result.HasViolations.Should().BeTrue();
}
[Fact]
public void Builder_CreateProduction_HasEnforcement()
{
var evaluator = GuardedPolicyEvaluatorBuilder.CreateProduction();
var result = evaluator.Evaluate("prod-scope", DateTimeOffset.UtcNow, scope =>
{
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "DateTime.Now",
Message = "Wall clock in prod",
Severity = DeterminismViolationSeverity.Error
});
return "should not return";
});
result.Succeeded.Should().BeFalse();
result.WasBlocked.Should().BeTrue();
}
[Fact]
public void Builder_CustomConfiguration_AppliesCorrectly()
{
var evaluator = new GuardedPolicyEvaluatorBuilder()
.WithEnforcement(true)
.FailOnSeverity(DeterminismViolationSeverity.Critical)
.WithRuntimeMonitoring(true)
.ExcludePatterns("test_", "spec_")
.Build();
// Error-level violations should pass since FailOnSeverity is Critical
var result = evaluator.Evaluate("custom-scope", DateTimeOffset.UtcNow, scope =>
{
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "DateTime.Now",
Message = "Error-level warning",
Severity = DeterminismViolationSeverity.Error
});
return 42;
});
result.Succeeded.Should().BeTrue();
result.Result.Should().Be(42);
}
#endregion
#region Scope Lifecycle Tests
[Fact]
public void Scope_Complete_CountsBySeverity()
{
var guard = new DeterminismGuardService(DeterminismGuardOptions.Development);
using var scope = guard.CreateScope("lifecycle-test", DateTimeOffset.UtcNow);
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "Test1",
Message = "Warning 1",
Severity = DeterminismViolationSeverity.Warning
});
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.RandomNumber,
ViolationType = "Test2",
Message = "Warning 2",
Severity = DeterminismViolationSeverity.Warning
});
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.NetworkAccess,
ViolationType = "Test3",
Message = "Error 1",
Severity = DeterminismViolationSeverity.Error
});
var result = scope.Complete();
result.Violations.Should().HaveCount(3);
result.CountBySeverity[DeterminismViolationSeverity.Warning].Should().Be(2);
result.CountBySeverity[DeterminismViolationSeverity.Error].Should().Be(1);
result.AnalysisDurationMs.Should().BeGreaterThanOrEqualTo(0);
}
[Fact]
public void Scope_ScopeId_IsPreserved()
{
var guard = new DeterminismGuardService();
using var scope = guard.CreateScope("my-scope-id", DateTimeOffset.UtcNow);
scope.ScopeId.Should().Be("my-scope-id");
}
[Fact]
public void Scope_NullScopeId_ThrowsArgumentNullException()
{
var guard = new DeterminismGuardService();
FluentActions.Invoking(() => guard.CreateScope(null!, DateTimeOffset.UtcNow))
.Should().Throw<ArgumentNullException>();
}
#endregion
#region DeterministicTimeProvider Tests
[Fact]
public void DeterministicTimeProvider_MultipleCallsReturnSameValue()
{
var fixedTime = new DateTimeOffset(2026, 2, 12, 10, 0, 0, TimeSpan.Zero);
var provider = new DeterministicTimeProvider(fixedTime);
// 100 calls should all return the same value
for (int i = 0; i < 100; i++)
{
provider.GetUtcNow().Should().Be(fixedTime);
}
}
#endregion
#region GuardedEvaluationResult Properties
[Fact]
public void GuardedEvaluationResult_ViolationCountBySeverity_Works()
{
var evaluator = new GuardedPolicyEvaluator(DeterminismGuardOptions.Development);
var result = evaluator.Evaluate("count-test", DateTimeOffset.UtcNow, scope =>
{
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "T1",
Message = "W1",
Severity = DeterminismViolationSeverity.Warning
});
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "T2",
Message = "E1",
Severity = DeterminismViolationSeverity.Error
});
return "done";
});
result.ViolationCountBySeverity.Should().ContainKey(DeterminismViolationSeverity.Warning);
result.ViolationCountBySeverity[DeterminismViolationSeverity.Warning].Should().Be(1);
result.ViolationCountBySeverity[DeterminismViolationSeverity.Error].Should().Be(1);
result.HasViolations.Should().BeTrue();
result.WasBlocked.Should().BeFalse();
result.ScopeId.Should().Be("count-test");
}
[Fact]
public void Evaluate_UnexpectedException_RecordsAsCriticalViolation()
{
var evaluator = new GuardedPolicyEvaluator();
var result = evaluator.Evaluate<string>("exception-test", DateTimeOffset.UtcNow, scope =>
{
throw new InvalidOperationException("Test exception");
});
result.Succeeded.Should().BeFalse();
result.Exception.Should().NotBeNull();
result.Exception.Should().BeOfType<InvalidOperationException>();
result.BlockingViolation.Should().NotBeNull();
result.BlockingViolation!.ViolationType.Should().Be("EvaluationException");
result.BlockingViolation.Severity.Should().Be(DeterminismViolationSeverity.Critical);
}
#endregion
#region DeterminismAnalysisResult.Pass Factory
[Fact]
public void DeterminismAnalysisResult_Pass_CreatesCleanResult()
{
var result = DeterminismAnalysisResult.Pass(42, true);
result.Passed.Should().BeTrue();
result.Violations.Should().BeEmpty();
result.CountBySeverity.Should().BeEmpty();
result.AnalysisDurationMs.Should().Be(42);
result.EnforcementEnabled.Should().BeTrue();
}
#endregion
#region Violation Remediation Messages
[Fact]
public void AnalyzeSource_ViolationsIncludeRemediation()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var now = DateTime.Now;";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle()
.Which.Remediation.Should().NotBeNullOrWhiteSpace();
}
[Fact]
public void AnalyzeSource_FileReadViolation_HasCriticalSeverity()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var text = File.ReadAllText(\"config.json\");";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.Category == DeterminismViolationCategory.FileSystemAccess &&
v.Severity == DeterminismViolationSeverity.Critical);
}
#endregion
}

View File

@@ -0,0 +1,626 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Engine.Gates;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Gates;
/// <summary>
/// Deep verification tests for CVE-aware release policy gates covering
/// VexTrust integration in PolicyGateEvaluator, Contested lattice suggestions,
/// RU lattice with justification, DriftGateEvaluator (KEV, EPSS, CVSS, custom),
/// and StabilityDampingGate (hysteresis, upgrade bypass, pruning).
/// </summary>
public sealed class CveAwareReleasePolicyGatesDeepTests
{
#region PolicyGateEvaluator with VexTrust enabled
[Fact]
public async Task PolicyGate_VexTrustEnabled_LowScore_Blocks()
{
var options = CreatePolicyGateOptions(vexTrustEnabled: true);
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "CU", uncertaintyTier: "T4");
request = request with
{
VexTrustScore = 0.30m, // Below default threshold
VexSignatureVerified = true,
Environment = "production"
};
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Block);
decision.BlockedBy.Should().Be("VexTrust");
}
[Fact]
public async Task PolicyGate_VexTrustEnabled_HighScore_Allows()
{
var options = CreatePolicyGateOptions(vexTrustEnabled: true);
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "CU", uncertaintyTier: "T4");
request = request with
{
VexTrustScore = 0.90m,
VexSignatureVerified = true,
Environment = "production"
};
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Allow);
}
[Fact]
public async Task PolicyGate_VexTrustEnabled_UnverifiedSignature_Blocks()
{
var options = CreatePolicyGateOptions(vexTrustEnabled: true);
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "CU", uncertaintyTier: "T4");
request = request with
{
VexTrustScore = 0.95m,
VexSignatureVerified = false, // Production requires verification
Environment = "production"
};
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Block);
decision.BlockedBy.Should().Be("VexTrust");
}
[Fact]
public async Task PolicyGate_VexTrustEnabled_MissingScore_Warns()
{
var options = CreatePolicyGateOptions(vexTrustEnabled: true);
// Default MissingTrustBehavior is Warn in PolicyGateOptions
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "CU", uncertaintyTier: "T4");
request = request with
{
VexTrustScore = null,
Environment = "production"
};
var decision = await evaluator.EvaluateAsync(request);
// Missing trust data should warn (not block) since the gate evaluates before uncertainty
decision.Decision.Should().BeOneOf(PolicyGateDecisionType.Warn, PolicyGateDecisionType.Block);
}
#endregion
#region Contested Lattice State Suggestions
[Fact]
public async Task PolicyGate_ContestedLattice_SuggestsTriageResolution()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "X", uncertaintyTier: "T4");
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Block);
decision.BlockedBy.Should().Be("LatticeState");
decision.Suggestion.Should().Contain("triage");
}
[Fact]
public async Task PolicyGate_CRLattice_SuggestsSubmitEvidence()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "CR", uncertaintyTier: "T4");
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Block);
(decision.Suggestion!.Contains("runtime probe evidence") || decision.Suggestion.Contains("unreachability")).Should().BeTrue();
}
#endregion
#region RU Lattice with Justification
[Fact]
public async Task PolicyGate_RULattice_WithJustification_AllowsWithWarning()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "RU",
justification: "Verified dead code via manual analysis of runtime traces");
var decision = await evaluator.EvaluateAsync(request);
// RU with justification should pass the lattice gate (PassWithNote -> Warn)
decision.Decision.Should().BeOneOf(PolicyGateDecisionType.Warn, PolicyGateDecisionType.Allow);
decision.BlockedBy.Should().NotBe("LatticeState");
}
[Fact]
public async Task PolicyGate_RULattice_WithoutJustification_Blocks()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "RU");
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Block);
decision.BlockedBy.Should().Be("LatticeState");
}
#endregion
#region Fixed and UnderInvestigation Status Paths
[Fact]
public async Task PolicyGate_Fixed_AllowsWithAnyLatticeState()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
foreach (var state in new[] { "U", "SR", "SU", "RO", "RU", "CR", "CU", "X" })
{
var request = CreateGateRequest("fixed", latticeState: state);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Allow,
$"Fixed status should be allowed with lattice state {state}");
}
}
[Fact]
public async Task PolicyGate_UnderInvestigation_NoEvidenceRequired()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("under_investigation", latticeState: "U",
graphHash: null, pathLength: null);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Allow);
}
#endregion
#region Override with Justification
[Fact]
public async Task PolicyGate_Override_WithValidJustification_BypassesBlock()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "SR");
request = request with
{
AllowOverride = true,
OverrideJustification = "Manual review confirmed dead code path in production environment"
};
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Warn);
decision.Advisory.Should().Contain("Override accepted");
}
[Fact]
public async Task PolicyGate_Override_WithShortJustification_DoesNotBypass()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "SR");
request = request with
{
AllowOverride = true,
OverrideJustification = "short" // < 20 chars
};
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Block);
}
#endregion
#region Gate Short-Circuit Behavior
[Fact]
public async Task PolicyGate_EvidenceBlock_ShortCircuitsBeforeLattice()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "CU",
uncertaintyTier: "T4", graphHash: null); // Missing graph hash blocks
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(PolicyGateDecisionType.Block);
decision.BlockedBy.Should().Be("EvidenceCompleteness");
// LatticeState gate should NOT appear in gate results since it short-circuited
decision.Gates.Should().HaveCount(1);
decision.Gates[0].Name.Should().Be("EvidenceCompleteness");
}
#endregion
#region Determinism
[Fact]
public async Task PolicyGate_100Iterations_DeterministicDecision()
{
var options = CreatePolicyGateOptions();
var evaluator = CreateEvaluator(options);
var request = CreateGateRequest("not_affected", latticeState: "CU", uncertaintyTier: "T4");
var reference = await evaluator.EvaluateAsync(request);
for (int i = 0; i < 100; i++)
{
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(reference.Decision);
decision.BlockedBy.Should().Be(reference.BlockedBy);
decision.Gates.Length.Should().Be(reference.Gates.Length);
}
}
#endregion
#region DriftGateEvaluator Tests
[Fact]
public async Task DriftGate_KevReachable_Blocks()
{
var evaluator = CreateDriftGateEvaluator(blockOnKev: true);
var request = CreateDriftRequest(hasKev: true, deltaReachable: 1);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Block);
decision.BlockedBy.Should().Be("KevReachable");
}
[Fact]
public async Task DriftGate_KevButNoNewReachable_Passes()
{
var evaluator = CreateDriftGateEvaluator(blockOnKev: true);
var request = CreateDriftRequest(hasKev: true, deltaReachable: 0);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Allow);
}
[Fact]
public async Task DriftGate_HighCvss_Blocks()
{
var evaluator = CreateDriftGateEvaluator(cvssThreshold: 9.0);
var request = CreateDriftRequest(maxCvss: 9.5, deltaReachable: 2);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Block);
decision.BlockedBy.Should().Be("CvssThreshold");
}
[Fact]
public async Task DriftGate_HighEpss_Blocks()
{
var evaluator = CreateDriftGateEvaluator(epssThreshold: 0.5);
var request = CreateDriftRequest(maxEpss: 0.75, deltaReachable: 1);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Block);
decision.BlockedBy.Should().Be("EpssThreshold");
}
[Fact]
public async Task DriftGate_AffectedReachable_Blocks()
{
var evaluator = CreateDriftGateEvaluator(blockOnAffectedReachable: true);
var request = CreateDriftRequest(deltaReachable: 3,
vexStatuses: new[] { "affected" });
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Block);
decision.BlockedBy.Should().Be("AffectedReachable");
}
[Fact]
public async Task DriftGate_NoMaterialDrift_Allows()
{
var evaluator = CreateDriftGateEvaluator(blockOnKev: true, cvssThreshold: 7.0);
var request = CreateDriftRequest(hasMaterialDrift: false);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Allow);
decision.Advisory.Should().Contain("No material drift");
}
[Fact]
public async Task DriftGate_Disabled_AllowsEverything()
{
var evaluator = CreateDriftGateEvaluator(enabled: false, blockOnKev: true);
var request = CreateDriftRequest(hasKev: true, deltaReachable: 5);
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Allow);
}
[Fact]
public async Task DriftGate_Override_BypassesBlock()
{
var evaluator = CreateDriftGateEvaluator(blockOnKev: true);
var request = CreateDriftRequest(hasKev: true, deltaReachable: 1);
request = request with
{
AllowOverride = true,
OverrideJustification = "Accepted risk per security review #SR-2025-042"
};
var decision = await evaluator.EvaluateAsync(request);
decision.Decision.Should().Be(DriftGateDecisionType.Warn);
decision.Advisory.Should().Contain("Override accepted");
}
#endregion
#region StabilityDampingGate Tests
[Fact]
public async Task StabilityDamping_FirstVerdict_Surfaces()
{
var gate = CreateStabilityDampingGate();
var request = new StabilityDampingRequest
{
Key = "artifact:CVE-2025-001",
ProposedState = new VerdictState
{
Status = "affected",
Confidence = 0.85,
Timestamp = DateTimeOffset.UtcNow
}
};
var decision = await gate.EvaluateAsync(request);
decision.ShouldSurface.Should().BeTrue();
decision.Reason.Should().Contain("new verdict");
}
[Fact]
public async Task StabilityDamping_SameStatus_SmallDelta_Suppressed()
{
var gate = CreateStabilityDampingGate();
var key = "artifact:CVE-2025-002";
var now = DateTimeOffset.UtcNow;
// Record initial state
await gate.RecordStateAsync(key, new VerdictState
{
Status = "affected",
Confidence = 0.80,
Timestamp = now
});
// Propose same status with small confidence change
var request = new StabilityDampingRequest
{
Key = key,
ProposedState = new VerdictState
{
Status = "affected",
Confidence = 0.82, // Small delta < threshold
Timestamp = now.AddMinutes(5)
}
};
var decision = await gate.EvaluateAsync(request);
decision.ShouldSurface.Should().BeFalse();
}
[Fact]
public async Task StabilityDamping_Disabled_AlwaysSurfaces()
{
var gate = CreateStabilityDampingGate(enabled: false);
var request = new StabilityDampingRequest
{
Key = "test:key",
ProposedState = new VerdictState
{
Status = "affected",
Confidence = 0.5,
Timestamp = DateTimeOffset.UtcNow
}
};
var decision = await gate.EvaluateAsync(request);
decision.ShouldSurface.Should().BeTrue();
decision.Reason.Should().Contain("disabled");
}
[Fact]
public async Task StabilityDamping_PruneHistory_RemovesOldRecords()
{
var gate = CreateStabilityDampingGate();
// Record old state
await gate.RecordStateAsync("old:key", new VerdictState
{
Status = "affected",
Confidence = 0.8,
Timestamp = DateTimeOffset.UtcNow.AddDays(-60) // Very old
});
var pruned = await gate.PruneHistoryAsync();
pruned.Should().BeGreaterThanOrEqualTo(0); // Depends on retention config
}
#endregion
#region Helpers
private static PolicyGateEvaluator CreateEvaluator(PolicyGateOptions options)
{
return new PolicyGateEvaluator(
new TestOptionsMonitor<PolicyGateOptions>(options),
TimeProvider.System,
NullLogger<PolicyGateEvaluator>.Instance);
}
private static PolicyGateOptions CreatePolicyGateOptions(bool vexTrustEnabled = false)
{
var options = new PolicyGateOptions();
options.VexTrust.Enabled = vexTrustEnabled;
if (vexTrustEnabled)
{
options.VexTrust.ApplyToStatuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "not_affected", "fixed" };
options.VexTrust.MissingTrustBehavior = MissingTrustBehavior.Warn;
options.VexTrust.Thresholds["production"] = new VexTrustThresholds
{
MinCompositeScore = 0.80m,
RequireIssuerVerified = true,
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh" },
FailureAction = FailureAction.Block
};
options.VexTrust.Thresholds["default"] = new VexTrustThresholds
{
MinCompositeScore = 0.60m,
RequireIssuerVerified = false,
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh", "stale" },
FailureAction = FailureAction.Warn
};
}
return options;
}
private static PolicyGateRequest CreateGateRequest(
string status,
string? latticeState = null,
string? uncertaintyTier = null,
string? graphHash = "blake3:abc123",
int? pathLength = -1,
bool hasRuntimeEvidence = false,
string? justification = null)
{
return new PolicyGateRequest
{
TenantId = "tenant-1",
VulnId = "CVE-2025-12345",
Purl = "pkg:maven/com.example/foo@1.0.0",
RequestedStatus = status,
LatticeState = latticeState,
UncertaintyTier = uncertaintyTier,
GraphHash = graphHash,
PathLength = pathLength,
HasRuntimeEvidence = hasRuntimeEvidence,
Justification = justification,
Confidence = 0.95,
RiskScore = 0.3
};
}
private static DriftGateEvaluator CreateDriftGateEvaluator(
bool enabled = true,
bool blockOnKev = false,
bool blockOnAffectedReachable = false,
double? cvssThreshold = null,
double? epssThreshold = null)
{
var options = new DriftGateOptions
{
Enabled = enabled,
BlockOnKev = blockOnKev,
BlockOnAffectedReachable = blockOnAffectedReachable,
CvssBlockThreshold = cvssThreshold,
EpssBlockThreshold = epssThreshold
};
return new DriftGateEvaluator(
new TestOptionsMonitor<DriftGateOptions>(options),
TimeProvider.System,
new TestGuidProvider(),
NullLogger<DriftGateEvaluator>.Instance);
}
private static DriftGateRequest CreateDriftRequest(
bool hasMaterialDrift = true,
bool hasKev = false,
int deltaReachable = 0,
double? maxCvss = null,
double? maxEpss = null,
string[]? vexStatuses = null)
{
// HasMaterialDrift is computed: DeltaReachable > 0 || DeltaUnreachable > 0
// When hasMaterialDrift=false, ensure both are 0
// When hasMaterialDrift=true but deltaReachable=0, use DeltaUnreachable=1 to trigger material drift
var deltaUnreachable = (!hasMaterialDrift || deltaReachable > 0) ? 0 : 1;
var effectiveDeltaReachable = hasMaterialDrift ? deltaReachable : 0;
return new DriftGateRequest
{
Context = new DriftGateContext
{
HasKevReachable = hasKev,
DeltaReachable = effectiveDeltaReachable,
DeltaUnreachable = deltaUnreachable,
MaxCvss = maxCvss,
MaxEpss = maxEpss,
NewlyReachableVexStatuses = vexStatuses ?? Array.Empty<string>()
}
};
}
private static StabilityDampingGate CreateStabilityDampingGate(bool enabled = true)
{
var options = new StabilityDampingOptions
{
Enabled = enabled,
MinDurationBeforeChange = TimeSpan.FromHours(24),
MinConfidenceDeltaPercent = 0.10,
OnlyDampDowngrades = false,
DampedStatuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "affected", "not_affected" },
HistoryRetention = TimeSpan.FromDays(30)
};
return new StabilityDampingGate(
new TestOptionsMonitor<StabilityDampingOptions>(options),
TimeProvider.System,
NullLogger<StabilityDampingGate>.Instance);
}
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
{
private readonly T _value;
public TestOptionsMonitor(T value) => _value = value;
public T CurrentValue => _value;
public T Get(string? name) => _value;
public IDisposable? OnChange(Action<T, string?> listener) => null;
}
private sealed class TestGuidProvider : StellaOps.Determinism.IGuidProvider
{
public Guid NewGuid() => new("11111111-2222-3333-4444-555555555555");
}
#endregion
}