using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using StellaOps.Policy; using StellaOps.Policy.Engine.Compilation; namespace StellaOps.Policy.Engine.Evaluation; /// /// Deterministically evaluates compiled policy IR against advisory/VEX/SBOM inputs. /// internal sealed class PolicyEvaluator { public PolicyEvaluationResult Evaluate(PolicyEvaluationRequest request) { if (request is null) { throw new ArgumentNullException(nameof(request)); } if (request.Document is null) { throw new ArgumentNullException(nameof(request.Document)); } var evaluator = new PolicyExpressionEvaluator(request.Context); var orderedRules = request.Document.Rules .Select(static (rule, index) => new { rule, index }) .OrderBy(x => x.rule.Priority) .ThenBy(x => x.index) .ToImmutableArray(); foreach (var entry in orderedRules) { var rule = entry.rule; if (!evaluator.EvaluateBoolean(rule.When)) { continue; } var runtime = new PolicyRuntimeState(request.Context.Severity.Normalized); foreach (var action in rule.ThenActions) { ApplyAction(rule.Name, action, evaluator, runtime); } if (runtime.Status is null) { runtime.Status = "affected"; } var baseResult = new PolicyEvaluationResult( Matched: true, Status: runtime.Status, Severity: runtime.Severity, RuleName: rule.Name, Priority: rule.Priority, Annotations: runtime.Annotations.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase), Warnings: runtime.Warnings.ToImmutableArray(), AppliedException: null); return ApplyExceptions(request, baseResult); } var defaultResult = PolicyEvaluationResult.CreateDefault(request.Context.Severity.Normalized); return ApplyExceptions(request, defaultResult); } private static void ApplyAction( string ruleName, PolicyIrAction action, PolicyExpressionEvaluator evaluator, PolicyRuntimeState runtime) { switch (action) { case PolicyIrAssignmentAction assign: ApplyAssignment(assign, evaluator, runtime); break; case PolicyIrAnnotateAction annotate: ApplyAnnotate(annotate, evaluator, runtime); break; case PolicyIrWarnAction warn: ApplyWarn(warn, evaluator, runtime); break; case PolicyIrEscalateAction escalate: ApplyEscalate(escalate, evaluator, runtime); break; case PolicyIrRequireVexAction require: var allSatisfied = true; foreach (var condition in require.Conditions.Values) { if (!evaluator.EvaluateBoolean(condition)) { allSatisfied = false; break; } } runtime.Status ??= allSatisfied ? "affected" : "suppressed"; break; case PolicyIrIgnoreAction ignore: runtime.Status = "ignored"; break; case PolicyIrDeferAction defer: runtime.Status = "deferred"; break; default: runtime.Warnings.Add($"Unhandled action '{action.GetType().Name}' in rule '{ruleName}'."); break; } } private static void ApplyAssignment(PolicyIrAssignmentAction assign, PolicyExpressionEvaluator evaluator, PolicyRuntimeState runtime) { var value = evaluator.Evaluate(assign.Value); var stringValue = value.AsString(); if (assign.Target.Length == 0) { return; } var target = assign.Target[0]; switch (target) { case "status": runtime.Status = stringValue ?? runtime.Status ?? "affected"; break; case "severity": runtime.Severity = stringValue; break; default: runtime.Annotations[target] = stringValue ?? value.Raw?.ToString() ?? string.Empty; break; } } private static void ApplyAnnotate(PolicyIrAnnotateAction annotate, PolicyExpressionEvaluator evaluator, PolicyRuntimeState runtime) { var key = annotate.Target.Length > 0 ? annotate.Target[^1] : "annotation"; var value = evaluator.Evaluate(annotate.Value).AsString() ?? string.Empty; runtime.Annotations[key] = value; } private static void ApplyWarn(PolicyIrWarnAction warn, PolicyExpressionEvaluator evaluator, PolicyRuntimeState runtime) { var message = warn.Message is null ? "" : evaluator.Evaluate(warn.Message).AsString(); if (!string.IsNullOrWhiteSpace(message)) { runtime.Warnings.Add(message!); } else { runtime.Warnings.Add("Policy rule emitted a warning."); } runtime.Status ??= "warned"; } private static void ApplyEscalate(PolicyIrEscalateAction escalate, PolicyExpressionEvaluator evaluator, PolicyRuntimeState runtime) { if (escalate.To is not null) { runtime.Severity = evaluator.Evaluate(escalate.To).AsString() ?? runtime.Severity; } if (escalate.When is not null && !evaluator.EvaluateBoolean(escalate.When)) { return; } } private sealed class PolicyRuntimeState { public PolicyRuntimeState(string? initialSeverity) { Severity = initialSeverity; } public string? Status { get; set; } public string? Severity { get; set; } public Dictionary Annotations { get; } = new(StringComparer.OrdinalIgnoreCase); public List Warnings { get; } = new(); } private static PolicyEvaluationResult ApplyExceptions(PolicyEvaluationRequest request, PolicyEvaluationResult baseResult) { var exceptions = request.Context.Exceptions; if (exceptions.IsEmpty) { return baseResult; } PolicyEvaluationExceptionInstance? winningInstance = null; PolicyExceptionEffect? winningEffect = null; var winningScore = -1; foreach (var instance in exceptions.Instances) { if (!exceptions.Effects.TryGetValue(instance.EffectId, out var effect)) { continue; } if (!MatchesScope(instance.Scope, request, baseResult)) { continue; } var specificity = ComputeSpecificity(instance.Scope); if (specificity < 0) { continue; } if (winningInstance is null || specificity > winningScore || (specificity == winningScore && instance.CreatedAt > winningInstance.CreatedAt) || (specificity == winningScore && instance.CreatedAt == winningInstance!.CreatedAt && string.CompareOrdinal(instance.Id, winningInstance.Id) < 0)) { winningInstance = instance; winningEffect = effect; winningScore = specificity; } } if (winningInstance is null || winningEffect is null) { return baseResult; } return ApplyExceptionEffect(baseResult, winningInstance, winningEffect); } private static bool MatchesScope( PolicyEvaluationExceptionScope scope, PolicyEvaluationRequest request, PolicyEvaluationResult baseResult) { if (scope.RuleNames.Count > 0) { if (string.IsNullOrEmpty(baseResult.RuleName) || !scope.RuleNames.Contains(baseResult.RuleName)) { return false; } } if (scope.Severities.Count > 0) { var severity = request.Context.Severity.Normalized; if (string.IsNullOrEmpty(severity) || !scope.Severities.Contains(severity)) { return false; } } if (scope.Sources.Count > 0) { var source = request.Context.Advisory.Source; if (string.IsNullOrEmpty(source) || !scope.Sources.Contains(source)) { return false; } } if (scope.Tags.Count > 0) { var sbom = request.Context.Sbom; var hasMatch = scope.Tags.Any(sbom.HasTag); if (!hasMatch) { return false; } } return true; } private static int ComputeSpecificity(PolicyEvaluationExceptionScope scope) { var score = 0; if (scope.RuleNames.Count > 0) { score += 1_000 + scope.RuleNames.Count * 25; } if (scope.Severities.Count > 0) { score += 500 + scope.Severities.Count * 10; } if (scope.Sources.Count > 0) { score += 250 + scope.Sources.Count * 10; } if (scope.Tags.Count > 0) { score += 100 + scope.Tags.Count * 5; } return score; } private static PolicyEvaluationResult ApplyExceptionEffect( PolicyEvaluationResult baseResult, PolicyEvaluationExceptionInstance instance, PolicyExceptionEffect effect) { var annotationsBuilder = baseResult.Annotations.ToBuilder(); annotationsBuilder["exception.id"] = instance.Id; annotationsBuilder["exception.effectId"] = effect.Id; annotationsBuilder["exception.effectType"] = effect.Effect.ToString(); if (!string.IsNullOrWhiteSpace(effect.Name)) { annotationsBuilder["exception.effectName"] = effect.Name!; } if (!string.IsNullOrWhiteSpace(effect.RoutingTemplate)) { annotationsBuilder["exception.routingTemplate"] = effect.RoutingTemplate!; } if (effect.MaxDurationDays is int durationDays) { annotationsBuilder["exception.maxDurationDays"] = durationDays.ToString(CultureInfo.InvariantCulture); } foreach (var pair in instance.Metadata) { annotationsBuilder[$"exception.meta.{pair.Key}"] = pair.Value; } var metadataBuilder = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase); if (!string.IsNullOrWhiteSpace(effect.RoutingTemplate)) { metadataBuilder["routingTemplate"] = effect.RoutingTemplate!; } if (effect.MaxDurationDays is int metadataDuration) { metadataBuilder["maxDurationDays"] = metadataDuration.ToString(CultureInfo.InvariantCulture); } if (!string.IsNullOrWhiteSpace(effect.RequiredControlId)) { metadataBuilder["requiredControlId"] = effect.RequiredControlId!; } if (!string.IsNullOrWhiteSpace(effect.Name)) { metadataBuilder["effectName"] = effect.Name!; } foreach (var pair in instance.Metadata) { metadataBuilder[pair.Key] = pair.Value; } var newStatus = baseResult.Status; var newSeverity = baseResult.Severity; var warnings = baseResult.Warnings; switch (effect.Effect) { case PolicyExceptionEffectType.Suppress: newStatus = "suppressed"; annotationsBuilder["exception.status"] = newStatus; break; case PolicyExceptionEffectType.Defer: newStatus = "deferred"; annotationsBuilder["exception.status"] = newStatus; break; case PolicyExceptionEffectType.Downgrade: if (effect.DowngradeSeverity is { } downgradeSeverity) { newSeverity = downgradeSeverity.ToString(); annotationsBuilder["exception.severity"] = newSeverity!; } break; case PolicyExceptionEffectType.RequireControl: if (!string.IsNullOrWhiteSpace(effect.RequiredControlId)) { annotationsBuilder["exception.requiredControl"] = effect.RequiredControlId!; warnings = warnings.Add($"Exception '{instance.Id}' requires control '{effect.RequiredControlId}'."); } break; } var application = new PolicyExceptionApplication( ExceptionId: instance.Id, EffectId: instance.EffectId, EffectType: effect.Effect, OriginalStatus: baseResult.Status, OriginalSeverity: baseResult.Severity, AppliedStatus: newStatus, AppliedSeverity: newSeverity, Metadata: metadataBuilder.ToImmutable()); return baseResult with { Status = newStatus, Severity = newSeverity, Annotations = annotationsBuilder.ToImmutable(), Warnings = warnings, AppliedException = application, }; } }