Files
git.stella-ops.org/src/Policy/__Libraries/StellaOps.Policy/Gates/PolicyGateAbstractions.cs
2026-02-01 21:37:40 +02:00

186 lines
5.7 KiB
C#

using StellaOps.Policy.Gates.Opa;
using StellaOps.Policy.TrustLattice;
using System.Collections.Immutable;
namespace StellaOps.Policy.Gates;
public record PolicyGateContext
{
public string Environment { get; init; } = "production";
public int UnknownCount { get; init; }
public IReadOnlyList<double> UnknownClaimScores { get; init; } = Array.Empty<double>();
public IReadOnlyDictionary<string, double> SourceInfluence { get; init; } = new Dictionary<string, double>(StringComparer.Ordinal);
public bool HasReachabilityProof { get; init; }
public string? Severity { get; init; }
public IReadOnlyCollection<string> ReasonCodes { get; init; } = Array.Empty<string>();
/// <summary>
/// Subgraph slice for reachability proof.
/// Required for high-severity findings when RequireSubgraphProofForHighSeverity is enabled.
/// </summary>
public SubgraphSlice? SubgraphSlice { get; init; }
/// <summary>
/// Subject key for Signals lookup.
/// </summary>
public string? SubjectKey { get; init; }
/// <summary>
/// CVE ID if applicable.
/// </summary>
public string? CveId { get; init; }
/// <summary>
/// Mutable metadata for audit trail.
/// Gates can add metadata here for later inspection.
/// </summary>
public Dictionary<string, string>? Metadata { get; init; }
/// <summary>
/// Optional supply chain evidence bundle for OPA policy evaluation.
/// When provided, OPA policies can access artifact metadata, SBOMs,
/// attestations, Rekor receipts, and VEX merge decisions.
/// </summary>
public OpaSupplyChainEvidence? SupplyChainEvidence { get; init; }
}
public sealed record GateResult
{
public required string GateName { get; init; }
public required bool Passed { get; init; }
public required string? Reason { get; init; }
public required ImmutableDictionary<string, object> Details { get; init; }
/// <summary>
/// Creates a passing gate result.
/// </summary>
public static GateResult Pass(string gateName, string reason, IEnumerable<string>? warnings = null)
{
var details = ImmutableDictionary<string, object>.Empty;
if (warnings != null)
{
var warningList = warnings.ToList();
if (warningList.Count > 0)
{
details = details.Add("warnings", warningList);
}
}
return new GateResult
{
GateName = gateName,
Passed = true,
Reason = reason,
Details = details
};
}
/// <summary>
/// Creates a passing gate result with child gate results.
/// </summary>
public static GateResult Pass(string gateName, string reason, IReadOnlyList<GateResult>? childResults)
{
var details = childResults != null && childResults.Count > 0
? ImmutableDictionary<string, object>.Empty.Add("childResults", childResults)
: ImmutableDictionary<string, object>.Empty;
return new GateResult
{
GateName = gateName,
Passed = true,
Reason = reason,
Details = details
};
}
/// <summary>
/// Creates a failing gate result.
/// </summary>
public static GateResult Fail(string gateName, string reason, ImmutableDictionary<string, object>? details = null)
{
return new GateResult
{
GateName = gateName,
Passed = false,
Reason = reason,
Details = details ?? ImmutableDictionary<string, object>.Empty
};
}
/// <summary>
/// Creates a failing gate result with child gate results.
/// </summary>
public static GateResult Fail(string gateName, string reason, IReadOnlyList<GateResult>? childResults)
{
var details = childResults != null && childResults.Count > 0
? ImmutableDictionary<string, object>.Empty.Add("childResults", childResults)
: ImmutableDictionary<string, object>.Empty;
return new GateResult
{
GateName = gateName,
Passed = false,
Reason = reason,
Details = details
};
}
}
public sealed record GateEvaluationResult
{
public required bool AllPassed { get; init; }
public required ImmutableArray<GateResult> Results { get; init; }
public GateResult? FirstFailure => Results.FirstOrDefault(r => !r.Passed);
}
/// <summary>
/// Policy gate interface for gates that require MergeResult.
/// </summary>
public interface IPolicyGate
{
Task<GateResult> EvaluateAsync(
MergeResult mergeResult,
PolicyGateContext context,
CancellationToken ct = default);
}
/// <summary>
/// Simplified policy gate interface for context-only evaluation.
/// Used by attestation, runtime witness, and CVE gates that don't require MergeResult.
/// </summary>
public interface IContextPolicyGate
{
/// <summary>
/// Gate identifier.
/// </summary>
string Id { get; }
/// <summary>
/// Display name for the gate.
/// </summary>
string DisplayName { get; }
/// <summary>
/// Description of what the gate checks.
/// </summary>
string Description { get; }
/// <summary>
/// Evaluates the gate against the given context.
/// </summary>
Task<GateResult> EvaluateAsync(PolicyGateContext context, CancellationToken ct = default);
}
public sealed record PolicyGateRegistryOptions
{
public bool StopOnFirstFailure { get; init; } = true;
}
public interface IPolicyGateRegistry
{
void Register<TGate>(string name) where TGate : IPolicyGate;
Task<GateEvaluationResult> EvaluateAsync(
MergeResult mergeResult,
PolicyGateContext context,
CancellationToken ct = default);
}