This commit is contained in:
StellaOps Bot
2025-12-13 02:22:15 +02:00
parent 564df71bfb
commit 999e26a48e
395 changed files with 25045 additions and 2224 deletions

View 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; }
}

View 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"
};
}

View 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;
}