346 lines
11 KiB
C#
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 < 0.8).
|
|
/// </summary>
|
|
public bool IsMediumConfidence => Confidence >= 0.5m && Confidence < 0.8m;
|
|
|
|
/// <summary>
|
|
/// Whether this reachability data has low confidence (< 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;
|
|
}
|