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
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:
@@ -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 < 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>
|
||||
/// 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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user