up
This commit is contained in:
332
src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateDecision.cs
Normal file
332
src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateDecision.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a policy gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record PolicyGateDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this gate decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gateId")]
|
||||
public required string GateId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The VEX status that was requested.
|
||||
/// </summary>
|
||||
[JsonPropertyName("requestedStatus")]
|
||||
public required string RequestedStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject of the decision (vuln, purl, symbol).
|
||||
/// </summary>
|
||||
[JsonPropertyName("subject")]
|
||||
public required PolicyGateSubject Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence used in the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidence")]
|
||||
public required PolicyGateEvidence Evidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual gate results.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gates")]
|
||||
public required ImmutableArray<PolicyGateResult> Gates { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Overall decision (allow, block, warn).
|
||||
/// </summary>
|
||||
[JsonPropertyName("decision")]
|
||||
public required PolicyGateDecisionType Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Advisory message if decision includes warnings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("advisory")]
|
||||
public string? Advisory { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the gate that blocked, if blocked.
|
||||
/// </summary>
|
||||
[JsonPropertyName("blockedBy")]
|
||||
public string? BlockedBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for blocking.
|
||||
/// </summary>
|
||||
[JsonPropertyName("blockReason")]
|
||||
public string? BlockReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Suggestion for resolving a block.
|
||||
/// </summary>
|
||||
[JsonPropertyName("suggestion")]
|
||||
public string? Suggestion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the decision was made.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decidedAt")]
|
||||
public required DateTimeOffset DecidedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject of a policy gate decision.
|
||||
/// </summary>
|
||||
public sealed record PolicyGateSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// Vulnerability identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vulnId")]
|
||||
public string? VulnId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public string? Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("symbolId")]
|
||||
public string? SymbolId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scanId")]
|
||||
public string? ScanId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence used in a policy gate decision.
|
||||
/// </summary>
|
||||
public sealed record PolicyGateEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// v1 lattice state code (U, SR, SU, RO, RU, CR, CU, X).
|
||||
/// </summary>
|
||||
[JsonPropertyName("latticeState")]
|
||||
public string? LatticeState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Uncertainty tier (T1, T2, T3, T4).
|
||||
/// </summary>
|
||||
[JsonPropertyName("uncertaintyTier")]
|
||||
public string? UncertaintyTier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3 hash of the callgraph.
|
||||
/// </summary>
|
||||
[JsonPropertyName("graphHash")]
|
||||
public string? GraphHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk score incorporating uncertainty.
|
||||
/// </summary>
|
||||
[JsonPropertyName("riskScore")]
|
||||
public double? RiskScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reachability confidence (0-1).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public double? Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether runtime evidence exists.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hasRuntimeEvidence")]
|
||||
public bool HasRuntimeEvidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path length from entry point to vulnerable symbol (-1 if unreachable).
|
||||
/// </summary>
|
||||
[JsonPropertyName("pathLength")]
|
||||
public int? PathLength { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a single gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record PolicyGateResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the gate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Result of the gate evaluation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public required PolicyGateResultType Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the result.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reason")]
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional note if result is pass_with_note.
|
||||
/// </summary>
|
||||
[JsonPropertyName("note")]
|
||||
public string? Note { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overall gate decision type.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<PolicyGateDecisionType>))]
|
||||
public enum PolicyGateDecisionType
|
||||
{
|
||||
/// <summary>
|
||||
/// Status change is allowed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("allow")]
|
||||
Allow,
|
||||
|
||||
/// <summary>
|
||||
/// Status change is blocked.
|
||||
/// </summary>
|
||||
[JsonPropertyName("block")]
|
||||
Block,
|
||||
|
||||
/// <summary>
|
||||
/// Status change is allowed with warning.
|
||||
/// </summary>
|
||||
[JsonPropertyName("warn")]
|
||||
Warn
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual gate result type.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<PolicyGateResultType>))]
|
||||
public enum PolicyGateResultType
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate passed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pass")]
|
||||
Pass,
|
||||
|
||||
/// <summary>
|
||||
/// Gate passed with advisory note.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pass_with_note")]
|
||||
PassWithNote,
|
||||
|
||||
/// <summary>
|
||||
/// Gate emitted a warning.
|
||||
/// </summary>
|
||||
[JsonPropertyName("warn")]
|
||||
Warn,
|
||||
|
||||
/// <summary>
|
||||
/// Gate blocked the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("block")]
|
||||
Block,
|
||||
|
||||
/// <summary>
|
||||
/// Gate was skipped (not applicable).
|
||||
/// </summary>
|
||||
[JsonPropertyName("skip")]
|
||||
Skip
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to evaluate policy gates for a VEX status change.
|
||||
/// </summary>
|
||||
public sealed record PolicyGateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability identifier.
|
||||
/// </summary>
|
||||
public string? VulnId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL.
|
||||
/// </summary>
|
||||
public string? Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol identifier.
|
||||
/// </summary>
|
||||
public string? SymbolId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan identifier.
|
||||
/// </summary>
|
||||
public string? ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Requested VEX status (not_affected, affected, under_investigation, fixed).
|
||||
/// </summary>
|
||||
public required string RequestedStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for the status (required for some statuses).
|
||||
/// </summary>
|
||||
public string? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// v1 lattice state code.
|
||||
/// </summary>
|
||||
public string? LatticeState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Uncertainty tier.
|
||||
/// </summary>
|
||||
public string? UncertaintyTier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3 graph hash.
|
||||
/// </summary>
|
||||
public string? GraphHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk score.
|
||||
/// </summary>
|
||||
public double? RiskScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score.
|
||||
/// </summary>
|
||||
public double? Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether runtime evidence exists.
|
||||
/// </summary>
|
||||
public bool HasRuntimeEvidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path length from entry point.
|
||||
/// </summary>
|
||||
public int? PathLength { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow override (requires permission).
|
||||
/// </summary>
|
||||
public bool AllowOverride { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Override justification if AllowOverride is true.
|
||||
/// </summary>
|
||||
public string? OverrideJustification { get; init; }
|
||||
}
|
||||
746
src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateEvaluator.cs
Normal file
746
src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateEvaluator.cs
Normal file
@@ -0,0 +1,746 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates policy gates for VEX status transitions.
|
||||
/// Gates ensure that status changes are backed by sufficient evidence.
|
||||
/// </summary>
|
||||
public interface IPolicyGateEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates all policy gates for a VEX status change request.
|
||||
/// </summary>
|
||||
/// <param name="request">The gate evaluation request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The gate decision.</returns>
|
||||
Task<PolicyGateDecision> EvaluateAsync(PolicyGateRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IPolicyGateEvaluator"/>.
|
||||
/// </summary>
|
||||
public sealed class PolicyGateEvaluator : IPolicyGateEvaluator
|
||||
{
|
||||
private readonly IOptionsMonitor<PolicyGateOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<PolicyGateEvaluator> _logger;
|
||||
|
||||
// VEX statuses
|
||||
private const string StatusNotAffected = "not_affected";
|
||||
private const string StatusAffected = "affected";
|
||||
private const string StatusUnderInvestigation = "under_investigation";
|
||||
private const string StatusFixed = "fixed";
|
||||
|
||||
// Lattice states (v1)
|
||||
private const string LatticeUnknown = "U";
|
||||
private const string LatticeStaticallyReachable = "SR";
|
||||
private const string LatticeStaticallyUnreachable = "SU";
|
||||
private const string LatticeRuntimeObserved = "RO";
|
||||
private const string LatticeRuntimeUnobserved = "RU";
|
||||
private const string LatticeConfirmedReachable = "CR";
|
||||
private const string LatticeConfirmedUnreachable = "CU";
|
||||
private const string LatticeContested = "X";
|
||||
|
||||
// Uncertainty tiers
|
||||
private const string TierT1 = "T1";
|
||||
private const string TierT2 = "T2";
|
||||
private const string TierT3 = "T3";
|
||||
private const string TierT4 = "T4";
|
||||
|
||||
public PolicyGateEvaluator(
|
||||
IOptionsMonitor<PolicyGateOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<PolicyGateEvaluator> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<PolicyGateDecision> EvaluateAsync(PolicyGateRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var options = _options.CurrentValue;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Build gate ID
|
||||
var gateId = $"gate:vex:{request.RequestedStatus}:{now:O}";
|
||||
|
||||
// Build subject
|
||||
var subject = new PolicyGateSubject
|
||||
{
|
||||
VulnId = request.VulnId,
|
||||
Purl = request.Purl,
|
||||
SymbolId = request.SymbolId,
|
||||
ScanId = request.ScanId
|
||||
};
|
||||
|
||||
// Build evidence
|
||||
var evidence = new PolicyGateEvidence
|
||||
{
|
||||
LatticeState = request.LatticeState,
|
||||
UncertaintyTier = request.UncertaintyTier,
|
||||
GraphHash = request.GraphHash,
|
||||
RiskScore = request.RiskScore,
|
||||
Confidence = request.Confidence,
|
||||
HasRuntimeEvidence = request.HasRuntimeEvidence,
|
||||
PathLength = request.PathLength
|
||||
};
|
||||
|
||||
// If gates are disabled, allow everything
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return Task.FromResult(CreateAllowDecision(gateId, request.RequestedStatus, subject, evidence, now, "Gates disabled"));
|
||||
}
|
||||
|
||||
// Evaluate gates in order: Evidence -> Lattice -> Uncertainty -> Confidence
|
||||
var gateResults = new List<PolicyGateResult>(4);
|
||||
string? blockedBy = null;
|
||||
string? blockReason = null;
|
||||
string? suggestion = null;
|
||||
var warnings = new List<string>();
|
||||
|
||||
// 1. Evidence Completeness Gate
|
||||
var evidenceResult = EvaluateEvidenceCompletenessGate(request, options.EvidenceCompleteness);
|
||||
gateResults.Add(evidenceResult);
|
||||
if (evidenceResult.Result == PolicyGateResultType.Block)
|
||||
{
|
||||
blockedBy = evidenceResult.Name;
|
||||
blockReason = evidenceResult.Reason;
|
||||
suggestion = GetEvidenceSuggestion(request.RequestedStatus);
|
||||
}
|
||||
else if (evidenceResult.Result == PolicyGateResultType.Warn || evidenceResult.Result == PolicyGateResultType.PassWithNote)
|
||||
{
|
||||
warnings.Add(evidenceResult.Reason);
|
||||
}
|
||||
|
||||
// 2. Lattice State Gate (only if not already blocked)
|
||||
if (blockedBy is null)
|
||||
{
|
||||
var latticeResult = EvaluateLatticeStateGate(request, options.LatticeState);
|
||||
gateResults.Add(latticeResult);
|
||||
if (latticeResult.Result == PolicyGateResultType.Block)
|
||||
{
|
||||
blockedBy = latticeResult.Name;
|
||||
blockReason = latticeResult.Reason;
|
||||
suggestion = GetLatticeSuggestion(request.LatticeState, request.RequestedStatus);
|
||||
}
|
||||
else if (latticeResult.Result == PolicyGateResultType.Warn || latticeResult.Result == PolicyGateResultType.PassWithNote)
|
||||
{
|
||||
warnings.Add(latticeResult.Note ?? latticeResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Uncertainty Tier Gate (only if not already blocked)
|
||||
if (blockedBy is null)
|
||||
{
|
||||
var uncertaintyResult = EvaluateUncertaintyTierGate(request, options.UncertaintyTier);
|
||||
gateResults.Add(uncertaintyResult);
|
||||
if (uncertaintyResult.Result == PolicyGateResultType.Block)
|
||||
{
|
||||
blockedBy = uncertaintyResult.Name;
|
||||
blockReason = uncertaintyResult.Reason;
|
||||
suggestion = GetUncertaintySuggestion(request.UncertaintyTier);
|
||||
}
|
||||
else if (uncertaintyResult.Result == PolicyGateResultType.Warn || uncertaintyResult.Result == PolicyGateResultType.PassWithNote)
|
||||
{
|
||||
warnings.Add(uncertaintyResult.Note ?? uncertaintyResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Confidence Threshold Gate (only if not already blocked)
|
||||
if (blockedBy is null && request.Confidence.HasValue)
|
||||
{
|
||||
var confidenceResult = EvaluateConfidenceGate(request, options.EvidenceCompleteness);
|
||||
gateResults.Add(confidenceResult);
|
||||
if (confidenceResult.Result == PolicyGateResultType.Warn || confidenceResult.Result == PolicyGateResultType.PassWithNote)
|
||||
{
|
||||
warnings.Add(confidenceResult.Note ?? confidenceResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Build final decision
|
||||
PolicyGateDecisionType decision;
|
||||
string? advisory = null;
|
||||
|
||||
if (blockedBy is not null)
|
||||
{
|
||||
// Check for override
|
||||
if (request.AllowOverride && CanOverride(request, options.Override))
|
||||
{
|
||||
decision = PolicyGateDecisionType.Warn;
|
||||
advisory = $"Override accepted: {request.OverrideJustification}";
|
||||
_logger.LogInformation(
|
||||
"Gate {Gate} overridden for {Status} on {Vuln}/{Purl}: {Justification}",
|
||||
blockedBy, request.RequestedStatus, request.VulnId, request.Purl, request.OverrideJustification);
|
||||
}
|
||||
else
|
||||
{
|
||||
decision = PolicyGateDecisionType.Block;
|
||||
_logger.LogInformation(
|
||||
"Gate {Gate} blocked {Status} on {Vuln}/{Purl}: {Reason}",
|
||||
blockedBy, request.RequestedStatus, request.VulnId, request.Purl, blockReason);
|
||||
}
|
||||
}
|
||||
else if (warnings.Count > 0)
|
||||
{
|
||||
decision = PolicyGateDecisionType.Warn;
|
||||
advisory = string.Join("; ", warnings);
|
||||
}
|
||||
else
|
||||
{
|
||||
decision = PolicyGateDecisionType.Allow;
|
||||
}
|
||||
|
||||
var result = new PolicyGateDecision
|
||||
{
|
||||
GateId = gateId,
|
||||
RequestedStatus = request.RequestedStatus,
|
||||
Subject = subject,
|
||||
Evidence = evidence,
|
||||
Gates = gateResults.ToImmutableArray(),
|
||||
Decision = decision,
|
||||
Advisory = advisory,
|
||||
BlockedBy = blockedBy,
|
||||
BlockReason = blockReason,
|
||||
Suggestion = suggestion,
|
||||
DecidedAt = now
|
||||
};
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateEvidenceCompletenessGate(PolicyGateRequest request, EvidenceCompletenessGateOptions options)
|
||||
{
|
||||
var status = request.RequestedStatus?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
switch (status)
|
||||
{
|
||||
case StatusNotAffected:
|
||||
// Require graph hash
|
||||
if (options.RequireGraphHashForNotAffected && string.IsNullOrWhiteSpace(request.GraphHash))
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "EvidenceCompleteness",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "graphHash (DSSE-attested) is required for not_affected"
|
||||
};
|
||||
}
|
||||
|
||||
// Require path analysis
|
||||
if (options.RequirePathAnalysisForNotAffected && request.PathLength is null)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "EvidenceCompleteness",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "pathAnalysis.pathLength is required for not_affected"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "EvidenceCompleteness",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "Required evidence present for not_affected"
|
||||
};
|
||||
|
||||
case StatusAffected:
|
||||
if (options.WarnNoEvidenceForAffected &&
|
||||
string.IsNullOrWhiteSpace(request.GraphHash) &&
|
||||
!request.HasRuntimeEvidence)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "EvidenceCompleteness",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = "No graphHash or runtimeProbe evidence for affected status"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "EvidenceCompleteness",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "Evidence present for affected"
|
||||
};
|
||||
|
||||
case StatusUnderInvestigation:
|
||||
case StatusFixed:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "EvidenceCompleteness",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = $"No evidence requirements for {status}"
|
||||
};
|
||||
|
||||
default:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "EvidenceCompleteness",
|
||||
Result = PolicyGateResultType.Skip,
|
||||
Reason = $"Unknown status: {status}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateLatticeStateGate(PolicyGateRequest request, LatticeStateGateOptions options)
|
||||
{
|
||||
var status = request.RequestedStatus?.ToLowerInvariant() ?? string.Empty;
|
||||
var latticeState = request.LatticeState?.ToUpperInvariant() ?? LatticeUnknown;
|
||||
|
||||
switch (status)
|
||||
{
|
||||
case StatusNotAffected:
|
||||
return EvaluateLatticeForNotAffected(latticeState, request.Justification, options);
|
||||
|
||||
case StatusAffected:
|
||||
return EvaluateLatticeForAffected(latticeState);
|
||||
|
||||
case StatusUnderInvestigation:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "Any lattice state allows under_investigation (safe default)"
|
||||
};
|
||||
|
||||
case StatusFixed:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "Any lattice state allows fixed (remediation action)"
|
||||
};
|
||||
|
||||
default:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Skip,
|
||||
Reason = $"Unknown status: {status}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateLatticeForNotAffected(string latticeState, string? justification, LatticeStateGateOptions options)
|
||||
{
|
||||
switch (latticeState)
|
||||
{
|
||||
case LatticeConfirmedUnreachable:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "CU (ConfirmedUnreachable) allows not_affected"
|
||||
};
|
||||
|
||||
case LatticeStaticallyUnreachable:
|
||||
if (!options.AllowSUForNotAffected)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "SU (StaticallyUnreachable) not allowed for not_affected (configuration)"
|
||||
};
|
||||
}
|
||||
|
||||
if (options.RequireJustificationForWeakStates && string.IsNullOrWhiteSpace(justification))
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "SU requires justification for not_affected"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.PassWithNote,
|
||||
Reason = "SU allows not_affected with warning",
|
||||
Note = "Static analysis only; no runtime confirmation"
|
||||
};
|
||||
|
||||
case LatticeRuntimeUnobserved:
|
||||
if (!options.AllowRUForNotAffected)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "RU (RuntimeUnobserved) not allowed for not_affected (configuration)"
|
||||
};
|
||||
}
|
||||
|
||||
if (options.RequireJustificationForWeakStates && string.IsNullOrWhiteSpace(justification))
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "RU requires justification for not_affected"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.PassWithNote,
|
||||
Reason = "RU allows not_affected with warning",
|
||||
Note = "Runtime unobserved; may be reachable but untested code path"
|
||||
};
|
||||
|
||||
case LatticeContested:
|
||||
if (options.BlockContestedForDefinitiveStatuses)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "X (Contested) incompatible with not_affected; conflicting static/runtime evidence"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = "X (Contested) requires triage before not_affected"
|
||||
};
|
||||
|
||||
case LatticeUnknown:
|
||||
case LatticeStaticallyReachable:
|
||||
case LatticeRuntimeObserved:
|
||||
case LatticeConfirmedReachable:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = $"{latticeState} incompatible with not_affected"
|
||||
};
|
||||
|
||||
default:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = $"Unknown lattice state {latticeState} cannot justify not_affected"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateLatticeForAffected(string latticeState)
|
||||
{
|
||||
switch (latticeState)
|
||||
{
|
||||
case LatticeConfirmedReachable:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "CR (ConfirmedReachable) confirms affected"
|
||||
};
|
||||
|
||||
case LatticeStaticallyReachable:
|
||||
case LatticeRuntimeObserved:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = $"{latticeState} supports affected"
|
||||
};
|
||||
|
||||
case LatticeUnknown:
|
||||
case LatticeStaticallyUnreachable:
|
||||
case LatticeRuntimeUnobserved:
|
||||
case LatticeConfirmedUnreachable:
|
||||
case LatticeContested:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = $"{latticeState} may indicate false positive for affected",
|
||||
Note = "Consider review: evidence suggests code may not be reachable"
|
||||
};
|
||||
|
||||
default:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "LatticeState",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "Unknown lattice state; allowing affected as safe default"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateUncertaintyTierGate(PolicyGateRequest request, UncertaintyTierGateOptions options)
|
||||
{
|
||||
var status = request.RequestedStatus?.ToLowerInvariant() ?? string.Empty;
|
||||
var tier = request.UncertaintyTier?.ToUpperInvariant() ?? TierT4;
|
||||
|
||||
switch (status)
|
||||
{
|
||||
case StatusNotAffected:
|
||||
return EvaluateUncertaintyForNotAffected(tier, options);
|
||||
|
||||
case StatusAffected:
|
||||
return EvaluateUncertaintyForAffected(tier, options);
|
||||
|
||||
case StatusUnderInvestigation:
|
||||
case StatusFixed:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = $"No uncertainty requirements for {status}"
|
||||
};
|
||||
|
||||
default:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Skip,
|
||||
Reason = $"Unknown status: {status}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateUncertaintyForNotAffected(string tier, UncertaintyTierGateOptions options)
|
||||
{
|
||||
switch (tier)
|
||||
{
|
||||
case TierT1:
|
||||
if (options.BlockT1ForNotAffected)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Block,
|
||||
Reason = "T1 (High) uncertainty blocks not_affected; require human review"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = "T1 (High) uncertainty; not_affected may be premature"
|
||||
};
|
||||
|
||||
case TierT2:
|
||||
if (options.WarnT2ForNotAffected)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = "T2 (Medium) uncertainty; explicit override recommended",
|
||||
Note = "Flag for review; decisions may need re-evaluation"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "T2 (Medium) uncertainty allowed for not_affected"
|
||||
};
|
||||
|
||||
case TierT3:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.PassWithNote,
|
||||
Reason = "T3 (Low) uncertainty allows not_affected",
|
||||
Note = "Advisory: Low uncertainty present"
|
||||
};
|
||||
|
||||
case TierT4:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "T4 (Negligible) uncertainty; not_affected allowed"
|
||||
};
|
||||
|
||||
default:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = $"Unknown uncertainty tier {tier}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateUncertaintyForAffected(string tier, UncertaintyTierGateOptions options)
|
||||
{
|
||||
switch (tier)
|
||||
{
|
||||
case TierT1:
|
||||
if (options.ReviewT1ForAffected)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = "T1 (High) uncertainty for affected; may be false positive",
|
||||
Note = "Review required: high uncertainty suggests reachability analysis incomplete"
|
||||
};
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "T1 (High) uncertainty; affected allowed as safe default"
|
||||
};
|
||||
|
||||
case TierT2:
|
||||
case TierT3:
|
||||
case TierT4:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = $"{tier} uncertainty allows affected"
|
||||
};
|
||||
|
||||
default:
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "UncertaintyTier",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = "Unknown uncertainty tier; allowing affected as safe default"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private PolicyGateResult EvaluateConfidenceGate(PolicyGateRequest request, EvidenceCompletenessGateOptions options)
|
||||
{
|
||||
var status = request.RequestedStatus?.ToLowerInvariant() ?? string.Empty;
|
||||
var confidence = request.Confidence ?? 1.0;
|
||||
|
||||
if (status == StatusNotAffected)
|
||||
{
|
||||
if (confidence < options.MinConfidenceWarning)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "ConfidenceThreshold",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = string.Create(CultureInfo.InvariantCulture, $"Confidence {confidence:P0} below warning threshold {options.MinConfidenceWarning:P0}"),
|
||||
Note = "Low confidence in reachability analysis"
|
||||
};
|
||||
}
|
||||
|
||||
if (confidence < options.MinConfidenceForNotAffected)
|
||||
{
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "ConfidenceThreshold",
|
||||
Result = PolicyGateResultType.Warn,
|
||||
Reason = string.Create(CultureInfo.InvariantCulture, $"Confidence {confidence:P0} below recommended {options.MinConfidenceForNotAffected:P0}"),
|
||||
Note = "Consider gathering additional evidence"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new PolicyGateResult
|
||||
{
|
||||
Name = "ConfidenceThreshold",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = string.Create(CultureInfo.InvariantCulture, $"Confidence {confidence:P0} meets requirements")
|
||||
};
|
||||
}
|
||||
|
||||
private static bool CanOverride(PolicyGateRequest request, OverrideOptions options)
|
||||
{
|
||||
if (!request.AllowOverride)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.RequireJustification)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.OverrideJustification))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.OverrideJustification.Length < options.MinJustificationLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static PolicyGateDecision CreateAllowDecision(
|
||||
string gateId,
|
||||
string requestedStatus,
|
||||
PolicyGateSubject subject,
|
||||
PolicyGateEvidence evidence,
|
||||
DateTimeOffset decidedAt,
|
||||
string reason)
|
||||
{
|
||||
return new PolicyGateDecision
|
||||
{
|
||||
GateId = gateId,
|
||||
RequestedStatus = requestedStatus,
|
||||
Subject = subject,
|
||||
Evidence = evidence,
|
||||
Gates = ImmutableArray.Create(new PolicyGateResult
|
||||
{
|
||||
Name = "Bypass",
|
||||
Result = PolicyGateResultType.Pass,
|
||||
Reason = reason
|
||||
}),
|
||||
Decision = PolicyGateDecisionType.Allow,
|
||||
Advisory = reason,
|
||||
DecidedAt = decidedAt
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetEvidenceSuggestion(string status) => status switch
|
||||
{
|
||||
StatusNotAffected => "Submit DSSE-attested call graph with path analysis",
|
||||
StatusAffected => "Consider providing graph hash or runtime probe evidence",
|
||||
_ => "Provide additional evidence"
|
||||
};
|
||||
|
||||
private static string GetLatticeSuggestion(string? latticeState, string status)
|
||||
{
|
||||
if (status == StatusNotAffected)
|
||||
{
|
||||
return latticeState switch
|
||||
{
|
||||
LatticeContested => "Resolve contested state through triage before claiming not_affected",
|
||||
LatticeStaticallyReachable or LatticeRuntimeObserved or LatticeConfirmedReachable =>
|
||||
"Submit runtime probe evidence showing unreachability or change to under_investigation",
|
||||
LatticeUnknown => "Run reachability analysis to determine lattice state",
|
||||
_ => "Provide evidence to support not_affected claim"
|
||||
};
|
||||
}
|
||||
|
||||
return "Review evidence and adjust status accordingly";
|
||||
}
|
||||
|
||||
private static string GetUncertaintySuggestion(string? tier) => tier switch
|
||||
{
|
||||
TierT1 => "Reduce uncertainty: resolve missing symbol resolution, verify PURL mappings, or provide trusted advisory sources",
|
||||
TierT2 => "Consider providing override with justification or reducing uncertainty through additional analysis",
|
||||
_ => "Review uncertainty sources and address where possible"
|
||||
};
|
||||
}
|
||||
136
src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateOptions.cs
Normal file
136
src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateOptions.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for policy gates.
|
||||
/// </summary>
|
||||
public sealed class PolicyGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name.
|
||||
/// </summary>
|
||||
public const string SectionName = "PolicyGates";
|
||||
|
||||
/// <summary>
|
||||
/// Lattice state gate options.
|
||||
/// </summary>
|
||||
public LatticeStateGateOptions LatticeState { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Uncertainty tier gate options.
|
||||
/// </summary>
|
||||
public UncertaintyTierGateOptions UncertaintyTier { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Evidence completeness gate options.
|
||||
/// </summary>
|
||||
public EvidenceCompletenessGateOptions EvidenceCompleteness { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Override mechanism options.
|
||||
/// </summary>
|
||||
public OverrideOptions Override { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether gates are enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the lattice state gate.
|
||||
/// </summary>
|
||||
public sealed class LatticeStateGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Allow StaticallyUnreachable (SU) state for not_affected with warning.
|
||||
/// </summary>
|
||||
public bool AllowSUForNotAffected { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Allow RuntimeUnobserved (RU) state for not_affected with warning.
|
||||
/// </summary>
|
||||
public bool AllowRUForNotAffected { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Require justification for weak states (SU, RU).
|
||||
/// </summary>
|
||||
public bool RequireJustificationForWeakStates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Block contested (X) state for definitive statuses.
|
||||
/// </summary>
|
||||
public bool BlockContestedForDefinitiveStatuses { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the uncertainty tier gate.
|
||||
/// </summary>
|
||||
public sealed class UncertaintyTierGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Block T1 (High) uncertainty for not_affected.
|
||||
/// </summary>
|
||||
public bool BlockT1ForNotAffected { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Warn for T2 (Medium) uncertainty for not_affected.
|
||||
/// </summary>
|
||||
public bool WarnT2ForNotAffected { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Require explicit override for T1 affected (possible false positive).
|
||||
/// </summary>
|
||||
public bool ReviewT1ForAffected { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the evidence completeness gate.
|
||||
/// </summary>
|
||||
public sealed class EvidenceCompletenessGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Require graph hash for not_affected.
|
||||
/// </summary>
|
||||
public bool RequireGraphHashForNotAffected { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence threshold for not_affected.
|
||||
/// </summary>
|
||||
public double MinConfidenceForNotAffected { get; set; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Confidence threshold that triggers a warning.
|
||||
/// </summary>
|
||||
public double MinConfidenceWarning { get; set; } = 0.6;
|
||||
|
||||
/// <summary>
|
||||
/// Require path analysis for not_affected.
|
||||
/// </summary>
|
||||
public bool RequirePathAnalysisForNotAffected { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Warn if no graph hash or runtime probe for affected.
|
||||
/// </summary>
|
||||
public bool WarnNoEvidenceForAffected { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for override mechanism.
|
||||
/// </summary>
|
||||
public sealed class OverrideOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default expiration period for overrides in days.
|
||||
/// </summary>
|
||||
public int DefaultExpirationDays { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Require justification for all overrides.
|
||||
/// </summary>
|
||||
public bool RequireJustification { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum justification length.
|
||||
/// </summary>
|
||||
public int MinJustificationLength { get; set; } = 20;
|
||||
}
|
||||
Reference in New Issue
Block a user