214 lines
7.1 KiB
C#
214 lines
7.1 KiB
C#
namespace StellaOps.PolicyDsl;
|
|
|
|
/// <summary>
|
|
/// Factory for creating policy evaluation engines from compiled policy documents.
|
|
/// </summary>
|
|
public sealed class PolicyEngineFactory
|
|
{
|
|
private readonly PolicyCompiler _compiler = new();
|
|
|
|
/// <summary>
|
|
/// Creates a policy engine from source code.
|
|
/// </summary>
|
|
/// <param name="source">The policy DSL source code.</param>
|
|
/// <returns>A policy engine if compilation succeeds, otherwise null with diagnostics.</returns>
|
|
public PolicyEngineResult CreateFromSource(string source)
|
|
{
|
|
var compilation = _compiler.Compile(source);
|
|
if (!compilation.Success || compilation.Document is null)
|
|
{
|
|
return new PolicyEngineResult(null, compilation.Diagnostics);
|
|
}
|
|
|
|
var engine = new PolicyEngine(compilation.Document, compilation.Checksum!);
|
|
return new PolicyEngineResult(engine, compilation.Diagnostics);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a policy engine from a pre-compiled IR document.
|
|
/// </summary>
|
|
/// <param name="document">The compiled policy IR document.</param>
|
|
/// <param name="checksum">The policy checksum.</param>
|
|
/// <returns>A policy engine.</returns>
|
|
public PolicyEngine CreateFromDocument(PolicyIrDocument document, string checksum)
|
|
{
|
|
return new PolicyEngine(document, checksum);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of creating a policy engine.
|
|
/// </summary>
|
|
public sealed record PolicyEngineResult(
|
|
PolicyEngine? Engine,
|
|
System.Collections.Immutable.ImmutableArray<StellaOps.Policy.PolicyIssue> Diagnostics);
|
|
|
|
/// <summary>
|
|
/// A lightweight policy evaluation engine.
|
|
/// </summary>
|
|
public sealed class PolicyEngine
|
|
{
|
|
internal PolicyEngine(PolicyIrDocument document, string checksum)
|
|
{
|
|
Document = document;
|
|
Checksum = checksum;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the compiled policy document.
|
|
/// </summary>
|
|
public PolicyIrDocument Document { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the policy checksum (SHA-256 of canonical representation).
|
|
/// </summary>
|
|
public string Checksum { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the policy name.
|
|
/// </summary>
|
|
public string Name => Document.Name;
|
|
|
|
/// <summary>
|
|
/// Gets the policy syntax version.
|
|
/// </summary>
|
|
public string Syntax => Document.Syntax;
|
|
|
|
/// <summary>
|
|
/// Gets the number of rules in the policy.
|
|
/// </summary>
|
|
public int RuleCount => Document.Rules.Length;
|
|
|
|
/// <summary>
|
|
/// Evaluates the policy against the given signal context.
|
|
/// </summary>
|
|
/// <param name="context">The signal context to evaluate against.</param>
|
|
/// <returns>The evaluation result.</returns>
|
|
public PolicyEvaluationResult Evaluate(SignalContext context)
|
|
{
|
|
if (context is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
var matchedRules = new List<string>();
|
|
var actions = new List<EvaluatedAction>();
|
|
|
|
foreach (var rule in Document.Rules.OrderByDescending(r => r.Priority))
|
|
{
|
|
var matched = EvaluateExpression(rule.When, context);
|
|
if (matched)
|
|
{
|
|
matchedRules.Add(rule.Name);
|
|
foreach (var action in rule.ThenActions)
|
|
{
|
|
actions.Add(new EvaluatedAction(rule.Name, action, WasElseBranch: false));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var action in rule.ElseActions)
|
|
{
|
|
actions.Add(new EvaluatedAction(rule.Name, action, WasElseBranch: true));
|
|
}
|
|
}
|
|
}
|
|
|
|
return new PolicyEvaluationResult(
|
|
PolicyName: Name,
|
|
PolicyChecksum: Checksum,
|
|
MatchedRules: matchedRules.ToArray(),
|
|
Actions: actions.ToArray());
|
|
}
|
|
|
|
private static bool EvaluateExpression(PolicyExpression expression, SignalContext context)
|
|
{
|
|
return expression switch
|
|
{
|
|
PolicyBinaryExpression binary => EvaluateBinary(binary, context),
|
|
PolicyUnaryExpression unary => EvaluateUnary(unary, context),
|
|
PolicyLiteralExpression literal => literal.Value is bool b && b,
|
|
PolicyIdentifierExpression identifier => context.HasSignal(identifier.Name),
|
|
PolicyMemberAccessExpression member => EvaluateMemberAccess(member, context),
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
private static bool EvaluateBinary(PolicyBinaryExpression binary, SignalContext context)
|
|
{
|
|
return binary.Operator switch
|
|
{
|
|
PolicyBinaryOperator.And => EvaluateExpression(binary.Left, context) && EvaluateExpression(binary.Right, context),
|
|
PolicyBinaryOperator.Or => EvaluateExpression(binary.Left, context) || EvaluateExpression(binary.Right, context),
|
|
PolicyBinaryOperator.Equal => EvaluateEquality(binary.Left, binary.Right, context, negate: false),
|
|
PolicyBinaryOperator.NotEqual => EvaluateEquality(binary.Left, binary.Right, context, negate: true),
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
private static bool EvaluateUnary(PolicyUnaryExpression unary, SignalContext context)
|
|
{
|
|
return unary.Operator switch
|
|
{
|
|
PolicyUnaryOperator.Not => !EvaluateExpression(unary.Operand, context),
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
private static bool EvaluateMemberAccess(PolicyMemberAccessExpression member, SignalContext context)
|
|
{
|
|
var value = ResolveValue(member.Target, context);
|
|
if (value is IDictionary<string, object?> dict)
|
|
{
|
|
return dict.TryGetValue(member.Member, out var v) && v is bool b && b;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool EvaluateEquality(PolicyExpression left, PolicyExpression right, SignalContext context, bool negate)
|
|
{
|
|
var leftValue = ResolveValue(left, context);
|
|
var rightValue = ResolveValue(right, context);
|
|
var equal = Equals(leftValue, rightValue);
|
|
return negate ? !equal : equal;
|
|
}
|
|
|
|
private static object? ResolveValue(PolicyExpression expression, SignalContext context)
|
|
{
|
|
return expression switch
|
|
{
|
|
PolicyLiteralExpression literal => literal.Value,
|
|
PolicyIdentifierExpression identifier => context.GetSignal(identifier.Name),
|
|
PolicyMemberAccessExpression member => ResolveMemberValue(member, context),
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
private static object? ResolveMemberValue(PolicyMemberAccessExpression member, SignalContext context)
|
|
{
|
|
var target = ResolveValue(member.Target, context);
|
|
if (target is IDictionary<string, object?> dict)
|
|
{
|
|
return dict.TryGetValue(member.Member, out var v) ? v : null;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of evaluating a policy.
|
|
/// </summary>
|
|
public sealed record PolicyEvaluationResult(
|
|
string PolicyName,
|
|
string PolicyChecksum,
|
|
string[] MatchedRules,
|
|
EvaluatedAction[] Actions);
|
|
|
|
/// <summary>
|
|
/// An action that was evaluated as part of policy execution.
|
|
/// </summary>
|
|
public sealed record EvaluatedAction(
|
|
string RuleName,
|
|
PolicyIrAction Action,
|
|
bool WasElseBranch);
|