Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
namespace StellaOps.Policy.Engine.Evaluation;
internal sealed record PolicyEvaluationRequest(
PolicyIrDocument Document,
PolicyEvaluationContext Context);
internal sealed record PolicyEvaluationContext(
PolicyEvaluationSeverity Severity,
PolicyEvaluationEnvironment Environment,
PolicyEvaluationAdvisory Advisory,
PolicyEvaluationVexEvidence Vex,
PolicyEvaluationSbom Sbom,
PolicyEvaluationExceptions Exceptions);
internal sealed record PolicyEvaluationSeverity(string Normalized, decimal? Score = null);
internal sealed record PolicyEvaluationEnvironment(
ImmutableDictionary<string, string> Properties)
{
public string? Get(string key) => Properties.TryGetValue(key, out var value) ? value : null;
}
internal sealed record PolicyEvaluationAdvisory(
string Source,
ImmutableDictionary<string, string> Metadata);
internal sealed record PolicyEvaluationVexEvidence(
ImmutableArray<PolicyEvaluationVexStatement> Statements)
{
public static readonly PolicyEvaluationVexEvidence Empty = new(ImmutableArray<PolicyEvaluationVexStatement>.Empty);
}
internal sealed record PolicyEvaluationVexStatement(
string Status,
string Justification,
string StatementId,
DateTimeOffset? Timestamp = null);
internal sealed record PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
{
public bool HasTag(string tag) => Tags.Contains(tag);
}
internal sealed record PolicyEvaluationResult(
bool Matched,
string Status,
string? Severity,
string? RuleName,
int? Priority,
ImmutableDictionary<string, string> Annotations,
ImmutableArray<string> Warnings,
PolicyExceptionApplication? AppliedException)
{
public static PolicyEvaluationResult CreateDefault(string? severity) => new(
Matched: false,
Status: "affected",
Severity: severity,
RuleName: null,
Priority: null,
Annotations: ImmutableDictionary<string, string>.Empty,
Warnings: ImmutableArray<string>.Empty,
AppliedException: null);
}
internal sealed record PolicyEvaluationExceptions(
ImmutableDictionary<string, PolicyExceptionEffect> Effects,
ImmutableArray<PolicyEvaluationExceptionInstance> Instances)
{
public static readonly PolicyEvaluationExceptions Empty = new(
ImmutableDictionary<string, PolicyExceptionEffect>.Empty,
ImmutableArray<PolicyEvaluationExceptionInstance>.Empty);
public bool IsEmpty => Instances.IsDefaultOrEmpty || Instances.Length == 0;
}
internal sealed record PolicyEvaluationExceptionInstance(
string Id,
string EffectId,
PolicyEvaluationExceptionScope Scope,
DateTimeOffset CreatedAt,
ImmutableDictionary<string, string> Metadata);
internal sealed record PolicyEvaluationExceptionScope(
ImmutableHashSet<string> RuleNames,
ImmutableHashSet<string> Severities,
ImmutableHashSet<string> Sources,
ImmutableHashSet<string> Tags)
{
public static PolicyEvaluationExceptionScope Empty { get; } = new(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase));
public bool IsEmpty => RuleNames.Count == 0
&& Severities.Count == 0
&& Sources.Count == 0
&& Tags.Count == 0;
public static PolicyEvaluationExceptionScope Create(
IEnumerable<string>? ruleNames = null,
IEnumerable<string>? severities = null,
IEnumerable<string>? sources = null,
IEnumerable<string>? tags = null)
{
return new PolicyEvaluationExceptionScope(
Normalize(ruleNames),
Normalize(severities),
Normalize(sources),
Normalize(tags));
}
private static ImmutableHashSet<string> Normalize(IEnumerable<string>? values)
{
if (values is null)
{
return ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase);
}
return values
.Where(static value => !string.IsNullOrWhiteSpace(value))
.Select(static value => value.Trim())
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
}
}
internal sealed record PolicyExceptionApplication(
string ExceptionId,
string EffectId,
PolicyExceptionEffectType EffectType,
string OriginalStatus,
string? OriginalSeverity,
string AppliedStatus,
string? AppliedSeverity,
ImmutableDictionary<string, string> Metadata);

View File

@@ -0,0 +1,420 @@
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;
/// <summary>
/// Deterministically evaluates compiled policy IR against advisory/VEX/SBOM inputs.
/// </summary>
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<string, string> Annotations { get; } = new(StringComparer.OrdinalIgnoreCase);
public List<string> 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<string, string>(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,
};
}
}

View File

@@ -0,0 +1,509 @@
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];
}
}
}