// SPDX-License-Identifier: BUSL-1.1 // SPDX-FileCopyrightText: 2025 StellaOps Contributors using FluentAssertions; using StellaOps.TestKit.Assertions; using Xunit; namespace StellaOps.Policy.Engine.Tests.Snapshots; /// /// Snapshot tests for policy evaluation trace summaries. /// Ensures evaluation traces have stable structure for debugging and auditing. /// public sealed class PolicyEvaluationTraceSnapshotTests { private static readonly DateTimeOffset FrozenTime = DateTimeOffset.Parse("2025-12-24T12:00:00Z"); /// /// Verifies that a simple evaluation trace produces stable structure. /// [Fact] public void SimpleEvaluationTrace_ProducesStableStructure() { // Arrange var trace = CreateSimpleEvaluationTrace(); // Act SnapshotAssert.MatchesSnapshot(trace, "SimpleEvaluationTrace"); } /// /// Verifies that an evaluation trace with multiple rule evaluations produces stable structure. /// [Fact] public void MultiRuleEvaluationTrace_ProducesStableStructure() { // Arrange var trace = CreateMultiRuleEvaluationTrace(); // Act SnapshotAssert.MatchesSnapshot(trace, "MultiRuleEvaluationTrace"); } /// /// Verifies that an evaluation trace with VEX resolution produces stable structure. /// [Fact] public void VexResolutionTrace_ProducesStableStructure() { // Arrange var trace = CreateVexResolutionTrace(); // Act SnapshotAssert.MatchesSnapshot(trace, "VexResolutionTrace"); } /// /// Verifies that an evaluation trace with profile application produces stable structure. /// [Fact] public void ProfileApplicationTrace_ProducesStableStructure() { // Arrange var trace = CreateProfileApplicationTrace(); // Act SnapshotAssert.MatchesSnapshot(trace, "ProfileApplicationTrace"); } /// /// Verifies that an evaluation trace with severity escalation produces stable structure. /// [Fact] public void SeverityEscalationTrace_ProducesStableStructure() { // Arrange var trace = CreateSeverityEscalationTrace(); // Act SnapshotAssert.MatchesSnapshot(trace, "SeverityEscalationTrace"); } /// /// Verifies that evaluation trace steps are ordered by priority. /// [Fact] public void EvaluationTrace_StepsOrderedByPriority() { // Arrange var trace = CreateMultiRuleEvaluationTrace(); // Assert var priorities = trace.Steps.Select(s => s.Priority).ToList(); priorities.Should().BeInDescendingOrder("Steps should be ordered by priority (highest first)"); } /// /// Verifies that evaluation trace includes timing information. /// [Fact] public void EvaluationTrace_IncludesTimingInformation() { // Arrange var trace = CreateSimpleEvaluationTrace(); // Assert trace.StartedAt.Should().Be(FrozenTime); trace.CompletedAt.Should().BeAfter(trace.StartedAt); trace.DurationMs.Should().BeGreaterThanOrEqualTo(0); trace.Steps.Should().AllSatisfy(s => s.DurationMs.Should().BeGreaterThanOrEqualTo(0)); } #region Trace Factories private static PolicyEvaluationTrace CreateSimpleEvaluationTrace() { return new PolicyEvaluationTrace { TraceId = "TRACE-2025-001", PolicyId = "POL-PROD-001", PolicyName = "Production Baseline Policy", EvaluationContext = new EvaluationContext { DigestEvaluated = "sha256:abc123def456", TenantId = "TENANT-001", Environment = "production", Exposure = "internet" }, StartedAt = FrozenTime, CompletedAt = FrozenTime.AddMilliseconds(42), DurationMs = 42, Outcome = "Pass", Steps = [ new EvaluationStep { StepNumber = 1, RuleName = "block_critical", Priority = 5, Phase = EvaluationPhase.RuleMatch, Condition = "severity.normalized >= \"Critical\"", ConditionResult = false, Action = null, Explanation = "No critical vulnerabilities found in scan", DurationMs = 15 }, new EvaluationStep { StepNumber = 2, RuleName = "allow_low_severity", Priority = 1, Phase = EvaluationPhase.RuleMatch, Condition = "severity.normalized <= \"Low\"", ConditionResult = true, Action = "status := \"allowed\"", Explanation = "All findings are Low severity or below", DurationMs = 12 } ], FinalStatus = "allowed", MatchedRuleCount = 1, TotalRuleCount = 2 }; } private static PolicyEvaluationTrace CreateMultiRuleEvaluationTrace() { return new PolicyEvaluationTrace { TraceId = "TRACE-2025-002", PolicyId = "POL-COMPLEX-001", PolicyName = "Complex Multi-Rule Policy", EvaluationContext = new EvaluationContext { DigestEvaluated = "sha256:xyz789", TenantId = "TENANT-002", Environment = "staging", Exposure = "internal" }, StartedAt = FrozenTime, CompletedAt = FrozenTime.AddMilliseconds(89), DurationMs = 89, Outcome = "Warn", Steps = [ new EvaluationStep { StepNumber = 1, RuleName = "block_critical", Priority = 5, Phase = EvaluationPhase.RuleMatch, Condition = "severity.normalized >= \"Critical\"", ConditionResult = false, Action = null, Explanation = "No critical vulnerabilities", DurationMs = 10 }, new EvaluationStep { StepNumber = 2, RuleName = "escalate_high_internet", Priority = 4, Phase = EvaluationPhase.RuleMatch, Condition = "severity.normalized == \"High\" and env.exposure == \"internet\"", ConditionResult = false, Action = null, Explanation = "Not internet-exposed, skipping escalation", DurationMs = 8 }, new EvaluationStep { StepNumber = 3, RuleName = "block_ruby_dev", Priority = 4, Phase = EvaluationPhase.RuleMatch, Condition = "sbom.any_component(ruby.group(\"development\"))", ConditionResult = false, Action = null, Explanation = "No development-only Ruby gems", DurationMs = 12 }, new EvaluationStep { StepNumber = 4, RuleName = "require_vex_justification", Priority = 3, Phase = EvaluationPhase.RuleMatch, Condition = "vex.any(status in [\"not_affected\",\"fixed\"])", ConditionResult = true, Action = "status := vex.status", Explanation = "VEX statement found: not_affected (component_not_present)", DurationMs = 25 }, new EvaluationStep { StepNumber = 5, RuleName = "warn_eol_runtime", Priority = 1, Phase = EvaluationPhase.RuleMatch, Condition = "severity.normalized <= \"Medium\" and sbom.has_tag(\"runtime:eol\")", ConditionResult = true, Action = "warn message \"Runtime marked as EOL; upgrade recommended.\"", Explanation = "EOL runtime detected: python3.9", DurationMs = 15 } ], FinalStatus = "warning", MatchedRuleCount = 2, TotalRuleCount = 5 }; } private static PolicyEvaluationTrace CreateVexResolutionTrace() { return new PolicyEvaluationTrace { TraceId = "TRACE-2025-003", PolicyId = "POL-VEX-001", PolicyName = "VEX-Aware Policy", EvaluationContext = new EvaluationContext { DigestEvaluated = "sha256:vex123", TenantId = "TENANT-001", Environment = "production", Exposure = "internet" }, StartedAt = FrozenTime, CompletedAt = FrozenTime.AddMilliseconds(67), DurationMs = 67, Outcome = "Pass", Steps = [ new EvaluationStep { StepNumber = 1, RuleName = "vex_merge_resolution", Priority = 10, Phase = EvaluationPhase.VexMerge, Condition = "vex.statements.count > 1", ConditionResult = true, Action = null, Explanation = "Multiple VEX statements found; merging via K4 lattice", DurationMs = 20, VexMergeDetail = new VexMergeDetail { VulnerabilityId = "CVE-2024-0001", StatementCount = 3, Sources = [ new VexSourceInfo { Source = "vendor", Status = "not_affected", Trust = 1.0m }, new VexSourceInfo { Source = "maintainer", Status = "affected", Trust = 0.9m }, new VexSourceInfo { Source = "scanner", Status = "unknown", Trust = 0.5m } ], WinningSource = "vendor", WinningStatus = "not_affected", ResolutionReason = "TrustWeight", ConflictsResolved = 2 } }, new EvaluationStep { StepNumber = 2, RuleName = "require_vex_justification", Priority = 3, Phase = EvaluationPhase.RuleMatch, Condition = "vex.justification in [\"component_not_present\",\"vulnerable_code_not_present\"]", ConditionResult = true, Action = "status := vex.status", Explanation = "VEX justification accepted: component_not_present", DurationMs = 12 } ], FinalStatus = "allowed", MatchedRuleCount = 2, TotalRuleCount = 2 }; } private static PolicyEvaluationTrace CreateProfileApplicationTrace() { return new PolicyEvaluationTrace { TraceId = "TRACE-2025-004", PolicyId = "POL-PROFILE-001", PolicyName = "Profile-Based Policy", EvaluationContext = new EvaluationContext { DigestEvaluated = "sha256:profile123", TenantId = "TENANT-003", Environment = "production", Exposure = "internet" }, StartedAt = FrozenTime, CompletedAt = FrozenTime.AddMilliseconds(55), DurationMs = 55, Outcome = "Pass", Steps = [ new EvaluationStep { StepNumber = 1, RuleName = "profile_severity", Priority = 100, Phase = EvaluationPhase.ProfileApplication, Condition = "profile.severity.enabled", ConditionResult = true, Action = null, Explanation = "Applying severity profile adjustments", DurationMs = 18, ProfileApplicationDetail = new ProfileApplicationDetail { ProfileName = "severity", Adjustments = [ new ProfileAdjustment { Finding = "CVE-2024-0001", OriginalSeverity = "High", AdjustedSeverity = "Critical", Reason = "GHSA source weight +0.5, internet exposure +0.5" } ] } }, new EvaluationStep { StepNumber = 2, RuleName = "block_critical", Priority = 5, Phase = EvaluationPhase.RuleMatch, Condition = "severity.normalized >= \"Critical\"", ConditionResult = false, Action = null, Explanation = "Post-profile: no critical vulnerabilities (VEX override applied)", DurationMs = 10 } ], FinalStatus = "allowed", MatchedRuleCount = 1, TotalRuleCount = 2 }; } private static PolicyEvaluationTrace CreateSeverityEscalationTrace() { return new PolicyEvaluationTrace { TraceId = "TRACE-2025-005", PolicyId = "POL-ESCALATE-001", PolicyName = "Escalation Policy", EvaluationContext = new EvaluationContext { DigestEvaluated = "sha256:escalate123", TenantId = "TENANT-001", Environment = "production", Exposure = "internet" }, StartedAt = FrozenTime, CompletedAt = FrozenTime.AddMilliseconds(45), DurationMs = 45, Outcome = "Fail", Steps = [ new EvaluationStep { StepNumber = 1, RuleName = "escalate_high_internet", Priority = 4, Phase = EvaluationPhase.SeverityEscalation, Condition = "severity.normalized == \"High\" and env.exposure == \"internet\"", ConditionResult = true, Action = "escalate to severity_band(\"Critical\")", Explanation = "High severity on internet-exposed asset escalated to Critical", DurationMs = 15, EscalationDetail = new EscalationDetail { Finding = "CVE-2024-0001", OriginalSeverity = "High", EscalatedSeverity = "Critical", Reason = "Internet exposure triggers escalation per policy rule" } }, new EvaluationStep { StepNumber = 2, RuleName = "block_critical", Priority = 5, Phase = EvaluationPhase.RuleMatch, Condition = "severity.normalized >= \"Critical\"", ConditionResult = true, Action = "status := \"blocked\"", Explanation = "Critical severity (post-escalation) triggers block", DurationMs = 10 } ], FinalStatus = "blocked", MatchedRuleCount = 2, TotalRuleCount = 2 }; } #endregion } #region Trace Models public sealed record PolicyEvaluationTrace { public required string TraceId { get; init; } public required string PolicyId { get; init; } public required string PolicyName { get; init; } public required EvaluationContext EvaluationContext { get; init; } public required DateTimeOffset StartedAt { get; init; } public required DateTimeOffset CompletedAt { get; init; } public required int DurationMs { get; init; } public required string Outcome { get; init; } public required IReadOnlyList Steps { get; init; } public required string FinalStatus { get; init; } public required int MatchedRuleCount { get; init; } public required int TotalRuleCount { get; init; } } public sealed record EvaluationContext { public required string DigestEvaluated { get; init; } public required string TenantId { get; init; } public required string Environment { get; init; } public required string Exposure { get; init; } } public sealed record EvaluationStep { public required int StepNumber { get; init; } public required string RuleName { get; init; } public required int Priority { get; init; } public required EvaluationPhase Phase { get; init; } public required string Condition { get; init; } public required bool ConditionResult { get; init; } public string? Action { get; init; } public required string Explanation { get; init; } public required int DurationMs { get; init; } public VexMergeDetail? VexMergeDetail { get; init; } public ProfileApplicationDetail? ProfileApplicationDetail { get; init; } public EscalationDetail? EscalationDetail { get; init; } } public enum EvaluationPhase { ProfileApplication, VexMerge, SeverityEscalation, RuleMatch } public sealed record VexMergeDetail { public required string VulnerabilityId { get; init; } public required int StatementCount { get; init; } public required IReadOnlyList Sources { get; init; } public required string WinningSource { get; init; } public required string WinningStatus { get; init; } public required string ResolutionReason { get; init; } public required int ConflictsResolved { get; init; } } public sealed record VexSourceInfo { public required string Source { get; init; } public required string Status { get; init; } public required decimal Trust { get; init; } } public sealed record ProfileApplicationDetail { public required string ProfileName { get; init; } public required IReadOnlyList Adjustments { get; init; } } public sealed record ProfileAdjustment { public required string Finding { get; init; } public required string OriginalSeverity { get; init; } public required string AdjustedSeverity { get; init; } public required string Reason { get; init; } } public sealed record EscalationDetail { public required string Finding { get; init; } public required string OriginalSeverity { get; init; } public required string EscalatedSeverity { get; init; } public required string Reason { get; init; } } #endregion