using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using StellaOps.Policy.Engine.Compilation; namespace StellaOps.Policy.Engine.Evaluation; internal sealed class PolicyExpressionEvaluator { private static readonly IReadOnlyDictionary SeverityOrder = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["critical"] = 5m, ["high"] = 4m, ["medium"] = 3m, ["moderate"] = 3m, ["low"] = 2m, ["informational"] = 1m, ["info"] = 1m, ["none"] = 0m, ["unknown"] = -1m, }; private readonly PolicyEvaluationContext context; public PolicyExpressionEvaluator(PolicyEvaluationContext context) { this.context = context ?? throw new ArgumentNullException(nameof(context)); } public EvaluationValue Evaluate(PolicyExpression expression, EvaluationScope? scope = null) { scope ??= EvaluationScope.Root(context); return expression switch { PolicyLiteralExpression literal => new EvaluationValue(literal.Value), PolicyListExpression list => new EvaluationValue(list.Items.Select(item => Evaluate(item, scope).Raw).ToImmutableArray()), PolicyIdentifierExpression identifier => ResolveIdentifier(identifier.Name, scope), PolicyMemberAccessExpression member => EvaluateMember(member, scope), PolicyInvocationExpression invocation => EvaluateInvocation(invocation, scope), PolicyIndexerExpression indexer => EvaluateIndexer(indexer, scope), PolicyUnaryExpression unary => EvaluateUnary(unary, scope), PolicyBinaryExpression binary => EvaluateBinary(binary, scope), _ => EvaluationValue.Null, }; } public bool EvaluateBoolean(PolicyExpression expression, EvaluationScope? scope = null) => Evaluate(expression, scope).AsBoolean(); private EvaluationValue ResolveIdentifier(string name, EvaluationScope scope) { if (scope.TryGetLocal(name, out var local)) { return new EvaluationValue(local); } return name switch { "severity" => new EvaluationValue(new SeverityScope(context.Severity)), "env" => new EvaluationValue(new EnvironmentScope(context.Environment)), "vex" => new EvaluationValue(new VexScope(this, context.Vex)), "advisory" => new EvaluationValue(new AdvisoryScope(context.Advisory)), "sbom" => new EvaluationValue(new SbomScope(context.Sbom)), "true" => EvaluationValue.True, "false" => EvaluationValue.False, _ => EvaluationValue.Null, }; } private EvaluationValue EvaluateMember(PolicyMemberAccessExpression member, EvaluationScope scope) { var target = Evaluate(member.Target, scope); var raw = target.Raw; if (raw is SeverityScope severity) { return severity.Get(member.Member); } if (raw is EnvironmentScope env) { return env.Get(member.Member); } if (raw is VexScope vex) { return vex.Get(member.Member); } if (raw is AdvisoryScope advisory) { return advisory.Get(member.Member); } if (raw is SbomScope sbom) { return sbom.Get(member.Member); } if (raw is ImmutableDictionary dict && dict.TryGetValue(member.Member, out var value)) { return new EvaluationValue(value); } if (raw is PolicyEvaluationVexStatement stmt) { return member.Member switch { "status" => new EvaluationValue(stmt.Status), "justification" => new EvaluationValue(stmt.Justification), "statementId" => new EvaluationValue(stmt.StatementId), _ => EvaluationValue.Null, }; } return EvaluationValue.Null; } private EvaluationValue EvaluateInvocation(PolicyInvocationExpression invocation, EvaluationScope scope) { if (invocation.Target is PolicyIdentifierExpression identifier) { switch (identifier.Name) { case "severity_band": var arg = invocation.Arguments.Length > 0 ? Evaluate(invocation.Arguments[0], scope).AsString() : null; return new EvaluationValue(arg ?? string.Empty); } } if (invocation.Target is PolicyMemberAccessExpression member && member.Target is PolicyIdentifierExpression root) { if (root.Name == "vex") { var vex = Evaluate(member.Target, scope); if (vex.Raw is VexScope vexScope) { return member.Member switch { "any" => new EvaluationValue(vexScope.Any(invocation.Arguments, scope)), "latest" => new EvaluationValue(vexScope.Latest()), _ => EvaluationValue.Null, }; } } if (root.Name == "sbom") { var sbom = Evaluate(member.Target, scope); if (sbom.Raw is SbomScope sbomScope) { return member.Member switch { "has_tag" => sbomScope.HasTag(invocation.Arguments, scope, this), _ => EvaluationValue.Null, }; } } if (root.Name == "advisory") { var advisory = Evaluate(member.Target, scope); if (advisory.Raw is AdvisoryScope advisoryScope) { return advisoryScope.Invoke(member.Member, invocation.Arguments, scope, this); } } } return EvaluationValue.Null; } private EvaluationValue EvaluateIndexer(PolicyIndexerExpression indexer, EvaluationScope scope) { var target = Evaluate(indexer.Target, scope).Raw; var index = Evaluate(indexer.Index, scope).Raw; if (target is ImmutableArray array && index is int i && i >= 0 && i < array.Length) { return new EvaluationValue(array[i]); } return EvaluationValue.Null; } private EvaluationValue EvaluateUnary(PolicyUnaryExpression unary, EvaluationScope scope) { var operand = Evaluate(unary.Operand, scope); return unary.Operator switch { PolicyUnaryOperator.Not => new EvaluationValue(!operand.AsBoolean()), _ => EvaluationValue.Null, }; } private EvaluationValue EvaluateBinary(PolicyBinaryExpression binary, EvaluationScope scope) { return binary.Operator switch { PolicyBinaryOperator.And => new EvaluationValue(EvaluateBoolean(binary.Left, scope) && EvaluateBoolean(binary.Right, scope)), PolicyBinaryOperator.Or => new EvaluationValue(EvaluateBoolean(binary.Left, scope) || EvaluateBoolean(binary.Right, scope)), PolicyBinaryOperator.Equal => Compare(binary.Left, binary.Right, scope, static (a, b) => Equals(a, b)), PolicyBinaryOperator.NotEqual => Compare(binary.Left, binary.Right, scope, static (a, b) => !Equals(a, b)), PolicyBinaryOperator.In => Contains(binary.Left, binary.Right, scope), PolicyBinaryOperator.NotIn => new EvaluationValue(!Contains(binary.Left, binary.Right, scope).AsBoolean()), PolicyBinaryOperator.LessThan => CompareNumeric(binary.Left, binary.Right, scope, static (a, b) => a < b), PolicyBinaryOperator.LessThanOrEqual => CompareNumeric(binary.Left, binary.Right, scope, static (a, b) => a <= b), PolicyBinaryOperator.GreaterThan => CompareNumeric(binary.Left, binary.Right, scope, static (a, b) => a > b), PolicyBinaryOperator.GreaterThanOrEqual => CompareNumeric(binary.Left, binary.Right, scope, static (a, b) => a >= b), _ => EvaluationValue.Null, }; } private EvaluationValue Compare(PolicyExpression left, PolicyExpression right, EvaluationScope scope, Func comparer) { var leftValue = Evaluate(left, scope).Raw; var rightValue = Evaluate(right, scope).Raw; return new EvaluationValue(comparer(leftValue, rightValue)); } private EvaluationValue CompareNumeric(PolicyExpression left, PolicyExpression right, EvaluationScope scope, Func comparer) { var leftValue = Evaluate(left, scope); var rightValue = Evaluate(right, scope); if (!TryGetComparableNumber(leftValue, out var leftNumber) || !TryGetComparableNumber(rightValue, out var rightNumber)) { return EvaluationValue.False; } return new EvaluationValue(comparer(leftNumber, rightNumber)); } private static bool TryGetComparableNumber(EvaluationValue value, out decimal number) { var numeric = value.AsDecimal(); if (numeric.HasValue) { number = numeric.Value; return true; } if (value.Raw is string text && SeverityOrder.TryGetValue(text.Trim(), out var mapped)) { number = mapped; return true; } number = 0m; return false; } private EvaluationValue Contains(PolicyExpression needleExpr, PolicyExpression haystackExpr, EvaluationScope scope) { var needle = Evaluate(needleExpr, scope).Raw; var haystack = Evaluate(haystackExpr, scope).Raw; if (haystack is ImmutableArray array) { return new EvaluationValue(array.Any(item => Equals(item, needle))); } if (haystack is string str && needle is string needleString) { return new EvaluationValue(str.Contains(needleString, StringComparison.OrdinalIgnoreCase)); } return new EvaluationValue(false); } internal readonly struct EvaluationValue { public static readonly EvaluationValue Null = new(null); public static readonly EvaluationValue True = new(true); public static readonly EvaluationValue False = new(false); public EvaluationValue(object? raw) { Raw = raw; } public object? Raw { get; } public bool AsBoolean() { return Raw switch { bool b => b, string s => !string.IsNullOrWhiteSpace(s), ImmutableArray array => !array.IsDefaultOrEmpty, null => false, _ => true, }; } public string? AsString() { return Raw switch { null => null, string s => s, decimal dec => dec.ToString("G", CultureInfo.InvariantCulture), double d => d.ToString("G", CultureInfo.InvariantCulture), int i => i.ToString(CultureInfo.InvariantCulture), _ => Raw.ToString(), }; } public decimal? AsDecimal() { return Raw switch { decimal dec => dec, double dbl => (decimal)dbl, float fl => (decimal)fl, int i => i, long l => l, string s when decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) => value, _ => null, }; } } internal sealed class EvaluationScope { private readonly IReadOnlyDictionary locals; private EvaluationScope(IReadOnlyDictionary locals, PolicyEvaluationContext globals) { this.locals = locals; Globals = globals; } public static EvaluationScope Root(PolicyEvaluationContext globals) => new EvaluationScope(new Dictionary(StringComparer.OrdinalIgnoreCase), globals); public static EvaluationScope FromLocals(PolicyEvaluationContext globals, IReadOnlyDictionary locals) => new EvaluationScope(locals, globals); public bool TryGetLocal(string name, out object? value) { if (locals.TryGetValue(name, out value)) { return true; } value = null; return false; } public PolicyEvaluationContext Globals { get; } } private sealed class SeverityScope { private readonly PolicyEvaluationSeverity severity; public SeverityScope(PolicyEvaluationSeverity severity) { this.severity = severity; } public EvaluationValue Get(string member) => member switch { "normalized" => new EvaluationValue(severity.Normalized), "score" => new EvaluationValue(severity.Score), _ => EvaluationValue.Null, }; } private sealed class EnvironmentScope { private readonly PolicyEvaluationEnvironment environment; public EnvironmentScope(PolicyEvaluationEnvironment environment) { this.environment = environment; } public EvaluationValue Get(string member) { var value = environment.Get(member) ?? environment.Get(member.ToLowerInvariant()); return new EvaluationValue(value); } } private sealed class AdvisoryScope { private readonly PolicyEvaluationAdvisory advisory; public AdvisoryScope(PolicyEvaluationAdvisory advisory) { this.advisory = advisory; } public EvaluationValue Get(string member) => member switch { "source" => new EvaluationValue(advisory.Source), _ => advisory.Metadata.TryGetValue(member, out var value) ? new EvaluationValue(value) : EvaluationValue.Null, }; public EvaluationValue Invoke(string member, ImmutableArray arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator) { if (member.Equals("has_metadata", StringComparison.OrdinalIgnoreCase)) { var key = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null; if (string.IsNullOrEmpty(key)) { return EvaluationValue.False; } return new EvaluationValue(advisory.Metadata.ContainsKey(key!)); } return EvaluationValue.Null; } } private sealed class SbomScope { private readonly PolicyEvaluationSbom sbom; public SbomScope(PolicyEvaluationSbom sbom) { this.sbom = sbom; } public EvaluationValue Get(string member) { if (member.Equals("tags", StringComparison.OrdinalIgnoreCase)) { return new EvaluationValue(sbom.Tags.ToImmutableArray()); } return EvaluationValue.Null; } public EvaluationValue HasTag(ImmutableArray arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator) { var tag = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null; if (string.IsNullOrWhiteSpace(tag)) { return EvaluationValue.False; } return new EvaluationValue(sbom.HasTag(tag!)); } } private sealed class VexScope { private readonly PolicyExpressionEvaluator evaluator; private readonly PolicyEvaluationVexEvidence vex; public VexScope(PolicyExpressionEvaluator evaluator, PolicyEvaluationVexEvidence vex) { this.evaluator = evaluator; this.vex = vex; } public EvaluationValue Get(string member) => member switch { "status" => new EvaluationValue(vex.Statements.IsDefaultOrEmpty ? null : vex.Statements[0].Status), "justification" => new EvaluationValue(vex.Statements.IsDefaultOrEmpty ? null : vex.Statements[0].Justification), _ => EvaluationValue.Null, }; public bool Any(ImmutableArray arguments, EvaluationScope scope) { if (arguments.Length == 0 || vex.Statements.IsDefaultOrEmpty) { return false; } var predicate = arguments[0]; foreach (var statement in vex.Statements) { var locals = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["status"] = statement.Status, ["justification"] = statement.Justification, ["statement"] = statement, ["statementId"] = statement.StatementId, }; var nestedScope = EvaluationScope.FromLocals(scope.Globals, locals); if (evaluator.EvaluateBoolean(predicate, nestedScope)) { return true; } } return false; } public PolicyEvaluationVexStatement? Latest() { if (vex.Statements.IsDefaultOrEmpty) { return null; } return vex.Statements[^1]; } } }