up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.PolicyDsl;
namespace StellaOps.Policy.Engine.Evaluation;
internal sealed record PolicyEvaluationRequest(
PolicyIrDocument Document,
PolicyEvaluationContext Context);
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.PolicyDsl;
namespace StellaOps.Policy.Engine.Evaluation;
internal sealed record PolicyEvaluationRequest(
PolicyIrDocument Document,
PolicyEvaluationContext Context);
internal sealed record PolicyEvaluationContext(
PolicyEvaluationSeverity Severity,
PolicyEvaluationEnvironment Environment,
@@ -21,173 +21,173 @@ internal sealed record PolicyEvaluationContext(
PolicyEvaluationReachability Reachability,
PolicyEvaluationEntropy Entropy,
DateTimeOffset? EvaluationTimestamp = null)
{
/// <summary>
/// Gets the evaluation timestamp for deterministic time-based operations.
/// This value is injected at evaluation time rather than using DateTime.UtcNow
/// to ensure deterministic, reproducible results.
/// </summary>
public DateTimeOffset Now => EvaluationTimestamp ?? DateTimeOffset.MinValue;
/// <summary>
/// Creates a context without reachability data (for backwards compatibility).
/// </summary>
public PolicyEvaluationContext(
PolicyEvaluationSeverity severity,
PolicyEvaluationEnvironment environment,
PolicyEvaluationAdvisory advisory,
PolicyEvaluationVexEvidence vex,
{
/// <summary>
/// Gets the evaluation timestamp for deterministic time-based operations.
/// This value is injected at evaluation time rather than using DateTime.UtcNow
/// to ensure deterministic, reproducible results.
/// </summary>
public DateTimeOffset Now => EvaluationTimestamp ?? DateTimeOffset.MinValue;
/// <summary>
/// Creates a context without reachability data (for backwards compatibility).
/// </summary>
public PolicyEvaluationContext(
PolicyEvaluationSeverity severity,
PolicyEvaluationEnvironment environment,
PolicyEvaluationAdvisory advisory,
PolicyEvaluationVexEvidence vex,
PolicyEvaluationSbom sbom,
PolicyEvaluationExceptions exceptions,
DateTimeOffset? evaluationTimestamp = null)
: this(severity, environment, advisory, vex, sbom, exceptions, PolicyEvaluationReachability.Unknown, PolicyEvaluationEntropy.Unknown, evaluationTimestamp)
{
}
}
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,
ImmutableArray<PolicyEvaluationComponent> Components)
{
public PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
: this(Tags, ImmutableArray<PolicyEvaluationComponent>.Empty)
{
}
public static readonly PolicyEvaluationSbom Empty = new(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableArray<PolicyEvaluationComponent>.Empty);
public bool HasTag(string tag) => Tags.Contains(tag);
}
internal sealed record PolicyEvaluationComponent(
string Name,
string Version,
string Type,
string? Purl,
ImmutableDictionary<string, string> Metadata);
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);
/// <summary>
/// Reachability evidence for policy evaluation.
/// </summary>
}
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,
ImmutableArray<PolicyEvaluationComponent> Components)
{
public PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
: this(Tags, ImmutableArray<PolicyEvaluationComponent>.Empty)
{
}
public static readonly PolicyEvaluationSbom Empty = new(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableArray<PolicyEvaluationComponent>.Empty);
public bool HasTag(string tag) => Tags.Contains(tag);
}
internal sealed record PolicyEvaluationComponent(
string Name,
string Version,
string Type,
string? Purl,
ImmutableDictionary<string, string> Metadata);
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);
/// <summary>
/// Reachability evidence for policy evaluation.
/// </summary>
internal sealed record PolicyEvaluationReachability(
string State,
decimal Confidence,
@@ -197,85 +197,85 @@ internal sealed record PolicyEvaluationReachability(
string? Method,
string? EvidenceRef)
{
/// <summary>
/// Default unknown reachability state.
/// </summary>
public static readonly PolicyEvaluationReachability Unknown = new(
State: "unknown",
Confidence: 0m,
Score: 0m,
HasRuntimeEvidence: false,
Source: null,
Method: null,
EvidenceRef: null);
/// <summary>
/// Reachable state.
/// </summary>
public static PolicyEvaluationReachability Reachable(
decimal confidence = 1m,
decimal score = 1m,
bool hasRuntimeEvidence = false,
string? source = null,
string? method = null) => new(
State: "reachable",
Confidence: confidence,
Score: score,
HasRuntimeEvidence: hasRuntimeEvidence,
Source: source,
Method: method,
EvidenceRef: null);
/// <summary>
/// Unreachable state.
/// </summary>
public static PolicyEvaluationReachability Unreachable(
decimal confidence = 1m,
bool hasRuntimeEvidence = false,
string? source = null,
string? method = null) => new(
State: "unreachable",
Confidence: confidence,
Score: 0m,
HasRuntimeEvidence: hasRuntimeEvidence,
Source: source,
Method: method,
EvidenceRef: null);
/// <summary>
/// Whether the reachability state is definitively reachable.
/// </summary>
public bool IsReachable => State.Equals("reachable", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether the reachability state is definitively unreachable.
/// </summary>
public bool IsUnreachable => State.Equals("unreachable", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether the reachability state is unknown.
/// </summary>
public bool IsUnknown => State.Equals("unknown", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether the reachability state is under investigation.
/// </summary>
public bool IsUnderInvestigation => State.Equals("under_investigation", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether this reachability data has high confidence (>= 0.8).
/// </summary>
public bool IsHighConfidence => Confidence >= 0.8m;
/// <summary>
/// Whether this reachability data has medium confidence (>= 0.5 and &lt; 0.8).
/// </summary>
public bool IsMediumConfidence => Confidence >= 0.5m && Confidence < 0.8m;
/// <summary>
/// Whether this reachability data has low confidence (&lt; 0.5).
/// </summary>
public bool IsLowConfidence => Confidence < 0.5m;
/// <summary>
/// Default unknown reachability state.
/// </summary>
public static readonly PolicyEvaluationReachability Unknown = new(
State: "unknown",
Confidence: 0m,
Score: 0m,
HasRuntimeEvidence: false,
Source: null,
Method: null,
EvidenceRef: null);
/// <summary>
/// Reachable state.
/// </summary>
public static PolicyEvaluationReachability Reachable(
decimal confidence = 1m,
decimal score = 1m,
bool hasRuntimeEvidence = false,
string? source = null,
string? method = null) => new(
State: "reachable",
Confidence: confidence,
Score: score,
HasRuntimeEvidence: hasRuntimeEvidence,
Source: source,
Method: method,
EvidenceRef: null);
/// <summary>
/// Unreachable state.
/// </summary>
public static PolicyEvaluationReachability Unreachable(
decimal confidence = 1m,
bool hasRuntimeEvidence = false,
string? source = null,
string? method = null) => new(
State: "unreachable",
Confidence: confidence,
Score: 0m,
HasRuntimeEvidence: hasRuntimeEvidence,
Source: source,
Method: method,
EvidenceRef: null);
/// <summary>
/// Whether the reachability state is definitively reachable.
/// </summary>
public bool IsReachable => State.Equals("reachable", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether the reachability state is definitively unreachable.
/// </summary>
public bool IsUnreachable => State.Equals("unreachable", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether the reachability state is unknown.
/// </summary>
public bool IsUnknown => State.Equals("unknown", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether the reachability state is under investigation.
/// </summary>
public bool IsUnderInvestigation => State.Equals("under_investigation", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether this reachability data has high confidence (>= 0.8).
/// </summary>
public bool IsHighConfidence => Confidence >= 0.8m;
/// <summary>
/// Whether this reachability data has medium confidence (>= 0.5 and &lt; 0.8).
/// </summary>
public bool IsMediumConfidence => Confidence >= 0.5m && Confidence < 0.8m;
/// <summary>
/// Whether this reachability data has low confidence (&lt; 0.5).
/// </summary>
public bool IsLowConfidence => Confidence < 0.5m;
}
/// <summary>

View File

@@ -1,420 +1,420 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using StellaOps.Policy;
using StellaOps.PolicyDsl;
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,
};
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using StellaOps.Policy;
using StellaOps.PolicyDsl;
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,
};
}
}