510 lines
18 KiB
C#
510 lines
18 KiB
C#
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<string, decimal> SeverityOrder = new Dictionary<string, decimal>(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<string, object?> 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<object?> 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<object?, object?, bool> 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<decimal, decimal, bool> 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<object?> 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<object?> 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<string, object?> locals;
|
|
|
|
private EvaluationScope(IReadOnlyDictionary<string, object?> locals, PolicyEvaluationContext globals)
|
|
{
|
|
this.locals = locals;
|
|
Globals = globals;
|
|
}
|
|
|
|
public static EvaluationScope Root(PolicyEvaluationContext globals) =>
|
|
new EvaluationScope(new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase), globals);
|
|
|
|
public static EvaluationScope FromLocals(PolicyEvaluationContext globals, IReadOnlyDictionary<string, object?> 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<PolicyExpression> 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<object?>());
|
|
}
|
|
|
|
return EvaluationValue.Null;
|
|
}
|
|
|
|
public EvaluationValue HasTag(ImmutableArray<PolicyExpression> 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<PolicyExpression> 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<string, object?>(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];
|
|
}
|
|
}
|
|
}
|