This commit is contained in:
master
2026-01-07 10:25:34 +02:00
726 changed files with 147397 additions and 1364 deletions

View File

@@ -0,0 +1,222 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Policy;
using StellaOps.Policy.Determinization;
using StellaOps.Policy.Determinization.Models;
using StellaOps.Policy.Engine.Gates;
using StellaOps.Policy.Engine.Gates.Determinization;
using StellaOps.Policy.Engine.Policies;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
namespace StellaOps.Policy.Engine.Tests.Gates.Determinization;
public class DeterminizationGateTests
{
private readonly Mock<ISignalSnapshotBuilder> _snapshotBuilderMock;
private readonly Mock<IUncertaintyScoreCalculator> _uncertaintyCalculatorMock;
private readonly Mock<IDecayedConfidenceCalculator> _decayCalculatorMock;
private readonly Mock<TrustScoreAggregator> _trustAggregatorMock;
private readonly DeterminizationGate _gate;
public DeterminizationGateTests()
{
_snapshotBuilderMock = new Mock<ISignalSnapshotBuilder>();
_uncertaintyCalculatorMock = new Mock<IUncertaintyScoreCalculator>();
_decayCalculatorMock = new Mock<IDecayedConfidenceCalculator>();
_trustAggregatorMock = new Mock<TrustScoreAggregator>();
var options = Options.Create(new DeterminizationOptions());
var policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
_gate = new DeterminizationGate(
policy,
_uncertaintyCalculatorMock.Object,
_decayCalculatorMock.Object,
_trustAggregatorMock.Object,
_snapshotBuilderMock.Object,
NullLogger<DeterminizationGate>.Instance);
}
[Fact]
public async Task EvaluateAsync_BuildsCorrectMetadata()
{
// Arrange
var snapshot = CreateSnapshot();
var uncertaintyScore = new UncertaintyScore
{
Entropy = 0.45,
Tier = UncertaintyTier.Moderate,
Completeness = 0.55,
MissingSignals = []
};
_snapshotBuilderMock
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(snapshot);
_uncertaintyCalculatorMock
.Setup(x => x.Calculate(It.IsAny<SignalSnapshot>()))
.Returns(uncertaintyScore);
_decayCalculatorMock
.Setup(x => x.Calculate(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
.Returns(0.85);
_trustAggregatorMock
.Setup(x => x.Aggregate(It.IsAny<SignalSnapshot>(), It.IsAny<UncertaintyScore>()))
.Returns(0.7);
var context = new PolicyGateContext
{
CveId = "CVE-2024-0001",
SubjectKey = "pkg:npm/test@1.0.0",
Environment = "development"
};
var mergeResult = new MergeResult
{
FinalScore = 0.5,
FinalTrustLevel = TrustLevel.Medium,
Claims = []
};
// Act
var result = await _gate.EvaluateAsync(mergeResult, context);
// Assert
result.Details.Should().ContainKey("uncertainty_entropy");
result.Details["uncertainty_entropy"].Should().Be(0.45);
result.Details.Should().ContainKey("uncertainty_tier");
result.Details["uncertainty_tier"].Should().Be("Moderate");
result.Details.Should().ContainKey("uncertainty_completeness");
result.Details["uncertainty_completeness"].Should().Be(0.55);
result.Details.Should().ContainKey("trust_score");
result.Details["trust_score"].Should().Be(0.7);
result.Details.Should().ContainKey("decay_multiplier");
result.Details.Should().ContainKey("decay_is_stale");
result.Details.Should().ContainKey("decay_age_days");
}
[Fact]
public async Task EvaluateAsync_WithGuardRails_IncludesGuardrailsMetadata()
{
// Arrange
var snapshot = CreateSnapshot();
var uncertaintyScore = new UncertaintyScore
{
Entropy = 0.5,
Tier = UncertaintyTier.Moderate,
Completeness = 0.5,
MissingSignals = []
};
_snapshotBuilderMock
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(snapshot);
_uncertaintyCalculatorMock
.Setup(x => x.Calculate(It.IsAny<SignalSnapshot>()))
.Returns(uncertaintyScore);
_decayCalculatorMock
.Setup(x => x.Calculate(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
.Returns(0.85);
_trustAggregatorMock
.Setup(x => x.Aggregate(It.IsAny<SignalSnapshot>(), It.IsAny<UncertaintyScore>()))
.Returns(0.3);
var context = new PolicyGateContext
{
CveId = "CVE-2024-0001",
SubjectKey = "pkg:npm/test@1.0.0",
Environment = "development"
};
var mergeResult = new MergeResult
{
FinalScore = 0.5,
FinalTrustLevel = TrustLevel.Medium,
Claims = []
};
// Act
var result = await _gate.EvaluateAsync(mergeResult, context);
// Assert
result.Details.Should().ContainKey("guardrails_monitoring");
result.Details.Should().ContainKey("guardrails_reeval_after");
}
[Fact]
public async Task EvaluateAsync_WithMatchedRule_IncludesRuleName()
{
// Arrange
var snapshot = CreateSnapshot();
var uncertaintyScore = new UncertaintyScore
{
Entropy = 0.2,
Tier = UncertaintyTier.Low,
Completeness = 0.8,
MissingSignals = []
};
_snapshotBuilderMock
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(snapshot);
_uncertaintyCalculatorMock
.Setup(x => x.Calculate(It.IsAny<SignalSnapshot>()))
.Returns(uncertaintyScore);
_decayCalculatorMock
.Setup(x => x.Calculate(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
.Returns(0.9);
_trustAggregatorMock
.Setup(x => x.Aggregate(It.IsAny<SignalSnapshot>(), It.IsAny<UncertaintyScore>()))
.Returns(0.8);
var context = new PolicyGateContext
{
CveId = "CVE-2024-0001",
SubjectKey = "pkg:npm/test@1.0.0",
Environment = "production"
};
var mergeResult = new MergeResult
{
FinalScore = 0.8,
FinalTrustLevel = TrustLevel.High,
Claims = []
};
// Act
var result = await _gate.EvaluateAsync(mergeResult, context);
// Assert
result.Details.Should().ContainKey("matched_rule");
result.Details["matched_rule"].Should().NotBeNull();
}
private static SignalSnapshot CreateSnapshot() => new()
{
Cve = "CVE-2024-0001",
Purl = "pkg:npm/test@1.0.0",
Epss = SignalState<EpssEvidence>.NotQueried(),
Vex = SignalState<VexClaimSummary>.NotQueried(),
Reachability = SignalState<ReachabilityEvidence>.NotQueried(),
Runtime = SignalState<RuntimeEvidence>.NotQueried(),
Backport = SignalState<BackportEvidence>.NotQueried(),
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
Cvss = SignalState<CvssEvidence>.NotQueried(),
SnapshotAt = DateTimeOffset.UtcNow
};
}

View File

@@ -0,0 +1,543 @@
// -----------------------------------------------------------------------------
// FacetQuotaGateIntegrationTests.cs
// Sprint: SPRINT_20260105_002_003_FACET (QTA-015)
// Task: QTA-015 - Integration tests for facet quota gate pipeline
// Description: End-to-end tests for facet drift detection and quota enforcement
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Facet;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Gates;
/// <summary>
/// Integration tests for the facet quota gate pipeline.
/// Tests end-to-end flow from drift reports through gate evaluation.
/// </summary>
[Trait("Category", "Integration")]
public sealed class FacetQuotaGateIntegrationTests
{
private readonly InMemoryFacetSealStore _sealStore;
private readonly Mock<IFacetDriftDetector> _driftDetector;
private readonly FacetSealer _sealer;
public FacetQuotaGateIntegrationTests()
{
_sealStore = new InMemoryFacetSealStore();
_driftDetector = new Mock<IFacetDriftDetector>();
_sealer = new FacetSealer();
}
#region Full Pipeline Tests
[Fact]
public async Task FullPipeline_FirstScan_NoBaseline_PassesWithWarning()
{
// Arrange: No baseline seal exists
var options = new FacetQuotaGateOptions
{
Enabled = true,
NoSealAction = NoSealAction.Warn
};
var gate = CreateGate(options);
var context = new PolicyGateContext { Environment = "production" };
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeTrue();
result.Reason.Should().Be("no_baseline_seal");
result.Details.Should().ContainKey("action");
result.Details["action"].Should().Be("warn");
}
[Fact]
public async Task FullPipeline_WithBaseline_NoDrift_Passes()
{
// Arrange: Create baseline seal
var imageDigest = "sha256:abc123";
var baselineSeal = CreateSeal(imageDigest, 100);
await _sealStore.SaveAsync(baselineSeal);
// Setup drift detector to return no drift
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.Ok);
SetupDriftDetector(driftReport);
var options = new FacetQuotaGateOptions { Enabled = true };
var gate = CreateGate(options);
var context = CreateContextWithDriftReport(driftReport);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeTrue();
result.Reason.Should().Be("quota_ok");
}
[Fact]
public async Task FullPipeline_ExceedWarningThreshold_PassesWithWarning()
{
// Arrange
var imageDigest = "sha256:def456";
var baselineSeal = CreateSeal(imageDigest, 100);
await _sealStore.SaveAsync(baselineSeal);
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.Warning);
SetupDriftDetector(driftReport);
var options = new FacetQuotaGateOptions
{
Enabled = true,
DefaultMaxChurnPercent = 10.0m
};
var gate = CreateGate(options);
var context = CreateContextWithDriftReport(driftReport);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeTrue();
result.Reason.Should().Be("quota_warning");
result.Details.Should().ContainKey("breachedFacets");
}
[Fact]
public async Task FullPipeline_ExceedBlockThreshold_Blocks()
{
// Arrange
var imageDigest = "sha256:ghi789";
var baselineSeal = CreateSeal(imageDigest, 100);
await _sealStore.SaveAsync(baselineSeal);
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.Blocked);
SetupDriftDetector(driftReport);
var options = new FacetQuotaGateOptions
{
Enabled = true,
DefaultAction = QuotaExceededAction.Block
};
var gate = CreateGate(options);
var context = CreateContextWithDriftReport(driftReport);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeFalse();
result.Reason.Should().Be("quota_exceeded");
}
[Fact]
public async Task FullPipeline_RequiresVex_BlocksUntilVexProvided()
{
// Arrange
var imageDigest = "sha256:jkl012";
var baselineSeal = CreateSeal(imageDigest, 100);
await _sealStore.SaveAsync(baselineSeal);
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.RequiresVex);
SetupDriftDetector(driftReport);
var options = new FacetQuotaGateOptions
{
Enabled = true,
DefaultAction = QuotaExceededAction.RequireVex
};
var gate = CreateGate(options);
var context = CreateContextWithDriftReport(driftReport);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeFalse();
result.Reason.Should().Be("requires_vex_authorization");
result.Details.Should().ContainKey("vexRequired");
((bool)result.Details["vexRequired"]).Should().BeTrue();
}
#endregion
#region Multi-Facet Tests
[Fact]
public async Task MultiFacet_MixedVerdicts_ReportsWorstCase()
{
// Arrange: Multiple facets with different verdicts
var imageDigest = "sha256:multi123";
var baselineSeal = CreateSeal(imageDigest, 100);
await _sealStore.SaveAsync(baselineSeal);
var facetDrifts = new[]
{
CreateFacetDrift("os-packages", QuotaVerdict.Ok),
CreateFacetDrift("app-dependencies", QuotaVerdict.Warning),
CreateFacetDrift("config-files", QuotaVerdict.Blocked)
};
var driftReport = new FacetDriftReport
{
ImageDigest = imageDigest,
BaselineSealId = baselineSeal.CombinedMerkleRoot,
AnalyzedAt = DateTimeOffset.UtcNow,
FacetDrifts = [.. facetDrifts],
OverallVerdict = QuotaVerdict.Blocked // Worst case
};
SetupDriftDetector(driftReport);
var options = new FacetQuotaGateOptions { Enabled = true };
var gate = CreateGate(options);
var context = CreateContextWithDriftReport(driftReport);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeFalse();
result.Reason.Should().Be("quota_exceeded");
}
[Fact]
public async Task MultiFacet_AllWithinQuota_Passes()
{
// Arrange
var imageDigest = "sha256:allok456";
var baselineSeal = CreateSeal(imageDigest, 100);
await _sealStore.SaveAsync(baselineSeal);
var facetDrifts = new[]
{
CreateFacetDrift("os-packages", QuotaVerdict.Ok),
CreateFacetDrift("app-dependencies", QuotaVerdict.Ok),
CreateFacetDrift("config-files", QuotaVerdict.Ok)
};
var driftReport = new FacetDriftReport
{
ImageDigest = imageDigest,
BaselineSealId = baselineSeal.CombinedMerkleRoot,
AnalyzedAt = DateTimeOffset.UtcNow,
FacetDrifts = [.. facetDrifts],
OverallVerdict = QuotaVerdict.Ok
};
SetupDriftDetector(driftReport);
var options = new FacetQuotaGateOptions { Enabled = true };
var gate = CreateGate(options);
var context = CreateContextWithDriftReport(driftReport);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeTrue();
}
#endregion
#region Seal Store Integration
[Fact]
public async Task SealStore_SaveAndRetrieve_WorksCorrectly()
{
// Arrange
var imageDigest = "sha256:store123";
var seal = CreateSeal(imageDigest, 50);
// Act
await _sealStore.SaveAsync(seal);
var retrieved = await _sealStore.GetLatestSealAsync(imageDigest);
// Assert
retrieved.Should().NotBeNull();
retrieved!.ImageDigest.Should().Be(imageDigest);
retrieved.CombinedMerkleRoot.Should().Be(seal.CombinedMerkleRoot);
}
[Fact]
public async Task SealStore_MultipleSeals_ReturnsLatest()
{
// Arrange
var imageDigest = "sha256:multi789";
var seal1 = CreateSealWithTimestamp(imageDigest, 50, DateTimeOffset.UtcNow.AddHours(-2));
var seal2 = CreateSealWithTimestamp(imageDigest, 55, DateTimeOffset.UtcNow.AddHours(-1));
var seal3 = CreateSealWithTimestamp(imageDigest, 60, DateTimeOffset.UtcNow);
await _sealStore.SaveAsync(seal1);
await _sealStore.SaveAsync(seal2);
await _sealStore.SaveAsync(seal3);
// Act
var latest = await _sealStore.GetLatestSealAsync(imageDigest);
// Assert
latest.Should().NotBeNull();
latest!.CreatedAt.Should().Be(seal3.CreatedAt);
}
[Fact]
public async Task SealStore_History_ReturnsInDescendingOrder()
{
// Arrange
var imageDigest = "sha256:history123";
var seal1 = CreateSealWithTimestamp(imageDigest, 50, DateTimeOffset.UtcNow.AddHours(-2));
var seal2 = CreateSealWithTimestamp(imageDigest, 55, DateTimeOffset.UtcNow.AddHours(-1));
var seal3 = CreateSealWithTimestamp(imageDigest, 60, DateTimeOffset.UtcNow);
await _sealStore.SaveAsync(seal1);
await _sealStore.SaveAsync(seal2);
await _sealStore.SaveAsync(seal3);
// Act
var history = await _sealStore.GetHistoryAsync(imageDigest, limit: 10);
// Assert
history.Should().HaveCount(3);
history[0].CreatedAt.Should().Be(seal3.CreatedAt);
history[1].CreatedAt.Should().Be(seal2.CreatedAt);
history[2].CreatedAt.Should().Be(seal1.CreatedAt);
}
#endregion
#region Configuration Tests
[Fact]
public async Task Configuration_PerFacetOverride_AppliesCorrectly()
{
// Arrange: os-packages has higher threshold
var imageDigest = "sha256:override123";
var baselineSeal = CreateSeal(imageDigest, 100);
await _sealStore.SaveAsync(baselineSeal);
var driftReport = CreateDriftReportWithChurn(imageDigest, baselineSeal.CombinedMerkleRoot, "os-packages", 25m);
var options = new FacetQuotaGateOptions
{
Enabled = true,
DefaultMaxChurnPercent = 10.0m,
FacetOverrides = new Dictionary<string, FacetQuotaOverride>
{
["os-packages"] = new FacetQuotaOverride
{
MaxChurnPercent = 30m, // Higher threshold for OS packages
Action = QuotaExceededAction.Warn
}
}
};
var gate = CreateGate(options);
var context = CreateContextWithDriftReport(driftReport);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert: 25% churn is within the 30% override threshold
result.Passed.Should().BeTrue();
}
[Fact]
public async Task Configuration_DisabledGate_BypassesAllChecks()
{
// Arrange
var options = new FacetQuotaGateOptions { Enabled = false };
var gate = CreateGate(options);
var context = new PolicyGateContext { Environment = "production" };
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
result.Passed.Should().BeTrue();
result.Reason.Should().Be("Gate disabled");
}
#endregion
#region Helper Methods
private FacetQuotaGate CreateGate(FacetQuotaGateOptions options)
{
return new FacetQuotaGate(options, _driftDetector.Object, NullLogger<FacetQuotaGate>.Instance);
}
private void SetupDriftDetector(FacetDriftReport report)
{
_driftDetector
.Setup(d => d.DetectDriftAsync(It.IsAny<FacetSeal>(), It.IsAny<FacetSeal>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(report);
}
private static PolicyGateContext CreateContextWithDriftReport(FacetDriftReport report)
{
var json = JsonSerializer.Serialize(report);
return new PolicyGateContext
{
Environment = "production",
Metadata = new Dictionary<string, string>
{
["FacetDriftReport"] = json
}
};
}
private static MergeResult CreateMergeResult(VexStatus status)
{
var claim = new ScoredClaim
{
SourceId = "test",
Status = status,
OriginalScore = 1.0,
AdjustedScore = 1.0,
ScopeSpecificity = 1,
Accepted = true,
Reason = "test"
};
return new MergeResult
{
Status = status,
Confidence = 0.9,
HasConflicts = false,
AllClaims = [claim],
WinningClaim = claim,
Conflicts = []
};
}
private FacetSeal CreateSeal(string imageDigest, int fileCount)
{
return CreateSealWithTimestamp(imageDigest, fileCount, DateTimeOffset.UtcNow);
}
private FacetSeal CreateSealWithTimestamp(string imageDigest, int fileCount, DateTimeOffset createdAt)
{
var files = Enumerable.Range(0, fileCount)
.Select(i => new FacetFileEntry($"/file{i}.txt", $"sha256:{i:x8}", 100, null))
.ToImmutableArray();
var facetEntry = new FacetEntry(
FacetId: "test-facet",
Files: files,
MerkleRoot: $"sha256:facet{fileCount:x8}");
return new FacetSeal
{
ImageDigest = imageDigest,
SchemaVersion = "1.0.0",
CreatedAt = createdAt,
Facets = [facetEntry],
CombinedMerkleRoot = $"sha256:combined{imageDigest.GetHashCode():x8}{createdAt.Ticks:x8}"
};
}
private static FacetDriftReport CreateDriftReport(string imageDigest, string baselineSealId, QuotaVerdict verdict)
{
return new FacetDriftReport
{
ImageDigest = imageDigest,
BaselineSealId = baselineSealId,
AnalyzedAt = DateTimeOffset.UtcNow,
FacetDrifts = [CreateFacetDrift("test-facet", verdict)],
OverallVerdict = verdict
};
}
private static FacetDriftReport CreateDriftReportWithChurn(
string imageDigest,
string baselineSealId,
string facetId,
decimal churnPercent)
{
var addedCount = (int)(churnPercent * 100 / 100); // For 100 baseline files
var addedFiles = Enumerable.Range(0, addedCount)
.Select(i => new FacetFileEntry($"/added{i}.txt", $"sha256:added{i}", 100, null))
.ToImmutableArray();
var verdict = churnPercent switch
{
< 10 => QuotaVerdict.Ok,
< 20 => QuotaVerdict.Warning,
_ => QuotaVerdict.Blocked
};
var facetDrift = new FacetDrift
{
FacetId = facetId,
Added = addedFiles,
Removed = [],
Modified = [],
DriftScore = churnPercent,
QuotaVerdict = verdict,
BaselineFileCount = 100
};
return new FacetDriftReport
{
ImageDigest = imageDigest,
BaselineSealId = baselineSealId,
AnalyzedAt = DateTimeOffset.UtcNow,
FacetDrifts = [facetDrift],
OverallVerdict = verdict
};
}
private static FacetDrift CreateFacetDrift(string facetId, QuotaVerdict verdict)
{
var addedCount = verdict switch
{
QuotaVerdict.Warning => 15,
QuotaVerdict.Blocked => 35,
QuotaVerdict.RequiresVex => 50,
_ => 0
};
var addedFiles = Enumerable.Range(0, addedCount)
.Select(i => new FacetFileEntry($"/added{i}.txt", $"sha256:added{i}", 100, null))
.ToImmutableArray();
return new FacetDrift
{
FacetId = facetId,
Added = addedFiles,
Removed = [],
Modified = [],
DriftScore = addedCount,
QuotaVerdict = verdict,
BaselineFileCount = 100
};
}
#endregion
}

View File

@@ -0,0 +1,276 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.Policy.Determinization;
using StellaOps.Policy.Determinization.Models;
using StellaOps.Policy.Engine.Policies;
namespace StellaOps.Policy.Engine.Tests.Policies;
public class DeterminizationPolicyTests
{
private readonly DeterminizationPolicy _policy;
public DeterminizationPolicyTests()
{
var options = Options.Create(new DeterminizationOptions());
_policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
}
[Fact]
public void Evaluate_RuntimeEvidenceLoaded_ReturnsEscalated()
{
// Arrange
var context = CreateContext(
runtime: new SignalState<RuntimeEvidence>
{
HasValue = true,
Value = new RuntimeEvidence { ObservedLoaded = true }
});
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Escalated);
result.MatchedRule.Should().Be("RuntimeEscalation");
result.Reason.Should().Contain("Runtime evidence shows vulnerable code loaded");
}
[Fact]
public void Evaluate_HighEpss_ReturnsQuarantined()
{
// Arrange
var context = CreateContext(
epss: new SignalState<EpssEvidence>
{
HasValue = true,
Value = new EpssEvidence { Score = 0.8 }
},
environment: DeploymentEnvironment.Production);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Blocked);
result.MatchedRule.Should().Be("EpssQuarantine");
result.Reason.Should().Contain("EPSS score");
}
[Fact]
public void Evaluate_ReachableCode_ReturnsQuarantined()
{
// Arrange
var context = CreateContext(
reachability: new SignalState<ReachabilityEvidence>
{
HasValue = true,
Value = new ReachabilityEvidence { IsReachable = true, Confidence = 0.9 }
});
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Blocked);
result.MatchedRule.Should().Be("ReachabilityQuarantine");
result.Reason.Should().Contain("reachable");
}
[Fact]
public void Evaluate_HighEntropyInProduction_ReturnsQuarantined()
{
// Arrange
var context = CreateContext(
entropy: 0.5,
environment: DeploymentEnvironment.Production);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Blocked);
result.MatchedRule.Should().Be("ProductionEntropyBlock");
result.Reason.Should().Contain("High uncertainty");
}
[Fact]
public void Evaluate_StaleEvidence_ReturnsDeferred()
{
// Arrange
var context = CreateContext(
isStale: true);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Deferred);
result.MatchedRule.Should().Be("StaleEvidenceDefer");
result.Reason.Should().Contain("stale");
}
[Fact]
public void Evaluate_ModerateUncertaintyInDev_ReturnsGuardedPass()
{
// Arrange
var context = CreateContext(
entropy: 0.5,
trustScore: 0.3,
environment: DeploymentEnvironment.Development);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.GuardedPass);
result.MatchedRule.Should().Be("GuardedAllowNonProd");
result.GuardRails.Should().NotBeNull();
result.GuardRails!.EnableMonitoring.Should().BeTrue();
}
[Fact]
public void Evaluate_UnreachableWithHighConfidence_ReturnsAllowed()
{
// Arrange
var context = CreateContext(
reachability: new SignalState<ReachabilityEvidence>
{
HasValue = true,
Value = new ReachabilityEvidence { IsReachable = false, Confidence = 0.9 }
},
trustScore: 0.8);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Pass);
result.MatchedRule.Should().Be("UnreachableAllow");
result.Reason.Should().Contain("unreachable");
}
[Fact]
public void Evaluate_VexNotAffected_ReturnsAllowed()
{
// Arrange
var context = CreateContext(
vex: new SignalState<VexClaimSummary>
{
HasValue = true,
Value = new VexClaimSummary { IsNotAffected = true, IssuerTrust = 0.9 }
},
trustScore: 0.8);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Pass);
result.MatchedRule.Should().Be("VexNotAffectedAllow");
result.Reason.Should().Contain("not_affected");
}
[Fact]
public void Evaluate_SufficientEvidenceLowEntropy_ReturnsAllowed()
{
// Arrange
var context = CreateContext(
entropy: 0.2,
trustScore: 0.8,
environment: DeploymentEnvironment.Production);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Pass);
result.MatchedRule.Should().Be("SufficientEvidenceAllow");
result.Reason.Should().Contain("Sufficient evidence");
}
[Fact]
public void Evaluate_ModerateUncertaintyTier_ReturnsGuardedPass()
{
// Arrange
var context = CreateContext(
tier: UncertaintyTier.Moderate,
trustScore: 0.5,
entropy: 0.5);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.GuardedPass);
result.MatchedRule.Should().Be("GuardedAllowModerateUncertainty");
result.GuardRails.Should().NotBeNull();
}
[Fact]
public void Evaluate_NoMatchingRule_ReturnsDeferred()
{
// Arrange
var context = CreateContext(
entropy: 0.9,
trustScore: 0.1,
environment: DeploymentEnvironment.Production);
// Act
var result = _policy.Evaluate(context);
// Assert
result.Status.Should().Be(PolicyVerdictStatus.Deferred);
result.MatchedRule.Should().Be("DefaultDefer");
result.Reason.Should().Contain("Insufficient evidence");
}
private static DeterminizationContext CreateContext(
SignalState<EpssEvidence>? epss = null,
SignalState<VexClaimSummary>? vex = null,
SignalState<ReachabilityEvidence>? reachability = null,
SignalState<RuntimeEvidence>? runtime = null,
double entropy = 0.0,
double trustScore = 0.0,
UncertaintyTier tier = UncertaintyTier.Minimal,
DeploymentEnvironment environment = DeploymentEnvironment.Development,
bool isStale = false)
{
var snapshot = new SignalSnapshot
{
Cve = "CVE-2024-0001",
Purl = "pkg:npm/test@1.0.0",
Epss = epss ?? SignalState<EpssEvidence>.NotQueried(),
Vex = vex ?? SignalState<VexClaimSummary>.NotQueried(),
Reachability = reachability ?? SignalState<ReachabilityEvidence>.NotQueried(),
Runtime = runtime ?? SignalState<RuntimeEvidence>.NotQueried(),
Backport = SignalState<BackportEvidence>.NotQueried(),
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
Cvss = SignalState<CvssEvidence>.NotQueried(),
SnapshotAt = DateTimeOffset.UtcNow
};
return new DeterminizationContext
{
SignalSnapshot = snapshot,
UncertaintyScore = new UncertaintyScore
{
Entropy = entropy,
Tier = tier,
Completeness = 1.0 - entropy,
MissingSignals = []
},
Decay = new ObservationDecay
{
LastSignalUpdate = DateTimeOffset.UtcNow.AddDays(-1),
AgeDays = 1,
DecayedMultiplier = isStale ? 0.3 : 0.9,
IsStale = isStale
},
TrustScore = trustScore,
Environment = environment
};
}
}

View File

@@ -0,0 +1,152 @@
using FluentAssertions;
using StellaOps.Policy.Determinization;
using StellaOps.Policy.Engine.Policies;
namespace StellaOps.Policy.Engine.Tests.Policies;
public class DeterminizationRuleSetTests
{
[Fact]
public void Default_RulesAreOrderedByPriority()
{
// Arrange
var options = new DeterminizationOptions();
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
ruleSet.Rules.Should().HaveCountGreaterThan(0);
var priorities = ruleSet.Rules.Select(r => r.Priority).ToList();
priorities.Should().BeInAscendingOrder("rules should be evaluable in priority order");
}
[Fact]
public void Default_RuntimeEscalationHasHighestPriority()
{
// Arrange
var options = new DeterminizationOptions();
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
var runtimeRule = ruleSet.Rules.First(r => r.Name == "RuntimeEscalation");
runtimeRule.Priority.Should().Be(10, "runtime escalation should have highest priority");
var allOtherRules = ruleSet.Rules.Where(r => r.Name != "RuntimeEscalation");
allOtherRules.Should().AllSatisfy(r => r.Priority.Should().BeGreaterThan(10));
}
[Fact]
public void Default_DefaultDeferHasLowestPriority()
{
// Arrange
var options = new DeterminizationOptions();
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
var defaultRule = ruleSet.Rules.First(r => r.Name == "DefaultDefer");
defaultRule.Priority.Should().Be(100, "default defer should be catch-all with lowest priority");
var allOtherRules = ruleSet.Rules.Where(r => r.Name != "DefaultDefer");
allOtherRules.Should().AllSatisfy(r => r.Priority.Should().BeLessThan(100));
}
[Fact]
public void Default_QuarantineRulesBeforeAllowRules()
{
// Arrange
var options = new DeterminizationOptions();
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
var epssQuarantine = ruleSet.Rules.First(r => r.Name == "EpssQuarantine");
var reachabilityQuarantine = ruleSet.Rules.First(r => r.Name == "ReachabilityQuarantine");
var productionBlock = ruleSet.Rules.First(r => r.Name == "ProductionEntropyBlock");
var unreachableAllow = ruleSet.Rules.First(r => r.Name == "UnreachableAllow");
var vexAllow = ruleSet.Rules.First(r => r.Name == "VexNotAffectedAllow");
var sufficientEvidenceAllow = ruleSet.Rules.First(r => r.Name == "SufficientEvidenceAllow");
epssQuarantine.Priority.Should().BeLessThan(unreachableAllow.Priority);
reachabilityQuarantine.Priority.Should().BeLessThan(vexAllow.Priority);
productionBlock.Priority.Should().BeLessThan(sufficientEvidenceAllow.Priority);
}
[Fact]
public void Default_AllRulesHaveUniquePriorities()
{
// Arrange
var options = new DeterminizationOptions();
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
var priorities = ruleSet.Rules.Select(r => r.Priority).ToList();
priorities.Should().OnlyHaveUniqueItems("each rule should have unique priority for deterministic ordering");
}
[Fact]
public void Default_AllRulesHaveNames()
{
// Arrange
var options = new DeterminizationOptions();
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
ruleSet.Rules.Should().AllSatisfy(r =>
{
r.Name.Should().NotBeNullOrWhiteSpace("all rules must have names for audit trail");
});
}
[Fact]
public void Default_Contains11Rules()
{
// Arrange
var options = new DeterminizationOptions();
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
ruleSet.Rules.Should().HaveCount(11, "rule set should contain all 11 specified rules");
}
[Fact]
public void Default_ContainsExpectedRules()
{
// Arrange
var options = new DeterminizationOptions();
var expectedRuleNames = new[]
{
"RuntimeEscalation",
"EpssQuarantine",
"ReachabilityQuarantine",
"ProductionEntropyBlock",
"StaleEvidenceDefer",
"GuardedAllowNonProd",
"UnreachableAllow",
"VexNotAffectedAllow",
"SufficientEvidenceAllow",
"GuardedAllowModerateUncertainty",
"DefaultDefer"
};
// Act
var ruleSet = DeterminizationRuleSet.Default(options);
// Assert
var actualNames = ruleSet.Rules.Select(r => r.Name).ToList();
actualNames.Should().BeEquivalentTo(expectedRuleNames);
}
}