Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/Evaluation/PolicyEvaluationContext.cs
2026-01-14 18:39:19 +02:00

346 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.PolicyDsl;
using StellaOps.Signals.EvidenceWeightedScore;
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,
ImmutableArray<Unknown> Unknowns,
ImmutableArray<ExceptionObject> ExceptionObjects,
PolicyEvaluationReachability Reachability,
PolicyEvaluationEntropy Entropy,
DateTimeOffset? EvaluationTimestamp = null,
string? PolicyDigest = null,
bool? ProvenanceAttested = 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,
PolicyEvaluationSbom sbom,
PolicyEvaluationExceptions exceptions,
ImmutableArray<Unknown>? unknowns = null,
ImmutableArray<ExceptionObject>? exceptionObjects = null,
DateTimeOffset? evaluationTimestamp = null,
string? policyDigest = null,
bool? provenanceAttested = null)
: this(
severity,
environment,
advisory,
vex,
sbom,
exceptions,
unknowns ?? ImmutableArray<Unknown>.Empty,
exceptionObjects ?? ImmutableArray<ExceptionObject>.Empty,
PolicyEvaluationReachability.Unknown,
PolicyEvaluationEntropy.Unknown,
evaluationTimestamp,
policyDigest,
provenanceAttested)
{
}
}
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,
ConfidenceScore? Confidence,
PolicyFailureReason? FailureReason = null,
string? FailureMessage = null,
BudgetStatusSummary? UnknownBudgetStatus = null,
EvidenceWeightedScoreResult? EvidenceWeightedScore = null)
{
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,
Confidence: null,
EvidenceWeightedScore: null);
}
internal enum PolicyFailureReason
{
UnknownBudgetExceeded
}
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,
decimal Score,
bool HasRuntimeEvidence,
string? Source,
string? Method,
string? EvidenceRef,
// Sprint: SPRINT_20260112_007_POLICY_path_gate_inputs (PW-POL-002)
string? PathHash = null,
ImmutableArray<string>? NodeHashes = null,
string? EntryNodeHash = null,
string? SinkNodeHash = null,
DateTimeOffset? RuntimeEvidenceAt = null,
bool? ObservedAtRuntime = null)
{
/// <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>
/// Entropy evidence for policy evaluation.
/// </summary>
internal sealed record PolicyEvaluationEntropy(
decimal Penalty,
decimal ImageOpaqueRatio,
bool Blocked,
bool Warned,
bool Capped,
decimal? TopFileOpaqueRatio)
{
public static PolicyEvaluationEntropy Unknown { get; } = new(
Penalty: 0m,
ImageOpaqueRatio: 0m,
Blocked: false,
Warned: false,
Capped: false,
TopFileOpaqueRatio: null);
public bool HasData => Penalty != 0m || ImageOpaqueRatio != 0m || Warned || Blocked;
}