// VexTrustGate - Policy gate for VEX trust verification // Part of SPRINT_1227_0004_0003: VexTrustGate Policy Integration using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace StellaOps.Policy.Engine.Gates; /// /// Policy gate that enforces VEX trust thresholds. /// Evaluates trust score, issuer verification, and freshness. /// public interface IVexTrustGate { /// /// Evaluate VEX trust for a status transition request. /// Task EvaluateAsync( VexTrustGateRequest request, CancellationToken ct = default); } /// /// Request for VEX trust gate evaluation. /// public sealed record VexTrustGateRequest { /// /// Requested VEX status (not_affected, fixed, etc.). /// public required string RequestedStatus { get; init; } /// /// Target environment (production, staging, development). /// public required string Environment { get; init; } /// /// VEX trust status with scores and breakdown. /// public VexTrustStatus? VexTrustStatus { get; init; } /// /// Tenant identifier for tenant-specific overrides. /// public string? TenantId { get; init; } } /// /// VEX trust status containing scores and verification details. /// public sealed record VexTrustStatus { /// /// Composite trust score (0.0 - 1.0). /// public decimal TrustScore { get; init; } /// /// Policy threshold for this context. /// public decimal PolicyTrustThreshold { get; init; } /// /// Whether the trust score meets the policy threshold. /// public bool MeetsPolicyThreshold { get; init; } /// /// Trust score breakdown by factor. /// public TrustScoreBreakdown? TrustBreakdown { get; init; } /// /// Issuer display name. /// public string? IssuerName { get; init; } /// /// Issuer identifier. /// public string? IssuerId { get; init; } /// /// Whether the signature was cryptographically verified. /// public bool? SignatureVerified { get; init; } /// /// Signature method used (dsse, cosign, pgp, etc.). /// public string? SignatureMethod { get; init; } /// /// Rekor transparency log index. /// public long? RekorLogIndex { get; init; } /// /// Rekor log identifier. /// public string? RekorLogId { get; init; } /// /// Freshness status (fresh, stale, superseded, expired). /// public string? Freshness { get; init; } /// /// When the VEX was verified. /// public DateTimeOffset? VerifiedAt { get; init; } } /// /// Trust score breakdown by factor. /// public sealed record TrustScoreBreakdown { public decimal? OriginScore { get; init; } public decimal? FreshnessScore { get; init; } public decimal? AccuracyScore { get; init; } public decimal? VerificationScore { get; init; } public decimal? AuthorityScore { get; init; } public decimal? CoverageScore { get; init; } } /// /// Result of VEX trust gate evaluation. /// public sealed record VexTrustGateResult { /// /// Gate identifier. /// public required string GateId { get; init; } /// /// Gate decision (allow, warn, block). /// public required VexTrustGateDecision Decision { get; init; } /// /// Reason code for the decision. /// public required string Reason { get; init; } /// /// Checks that were evaluated. /// public IReadOnlyList Checks { get; init; } = Array.Empty(); /// /// Trust score observed. /// public decimal? TrustScore { get; init; } /// /// Trust tier computed from score. /// public string? TrustTier { get; init; } /// /// Issuer identifier. /// public string? IssuerId { get; init; } /// /// Whether signature was verified. /// public bool? SignatureVerified { get; init; } /// /// Suggestion for resolving a block. /// public string? Suggestion { get; init; } /// /// Timestamp when decision was made. /// public required DateTimeOffset EvaluatedAt { get; init; } /// /// Additional details for audit. /// public ImmutableDictionary? Details { get; init; } } /// /// Individual trust check result. /// public sealed record VexTrustCheck( string Name, bool Passed, string Reason); /// /// VEX trust gate decision type. /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum VexTrustGateDecision { /// Trust is adequate, allow transition. Allow, /// Trust is below threshold but transition allowed with warning. Warn, /// Trust is insufficient, block transition. Block } /// /// Default implementation of VEX trust gate. /// public sealed class VexTrustGate : IVexTrustGate { private readonly IOptionsMonitor _options; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public const string GateIdPrefix = "vex-trust"; public const int GateOrder = 250; // After LatticeState (200), before UncertaintyTier (300) public VexTrustGate( IOptionsMonitor options, ILogger logger, TimeProvider? timeProvider = null) { _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timeProvider = timeProvider ?? TimeProvider.System; } /// public Task EvaluateAsync( VexTrustGateRequest request, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(request); var options = _options.CurrentValue; var now = _timeProvider.GetUtcNow(); var gateId = $"{GateIdPrefix}:{request.RequestedStatus}:{now:O}"; // Check if gate is enabled if (!options.Enabled) { return Task.FromResult(CreateAllowResult(gateId, "gate_disabled", request.VexTrustStatus)); } // Check if status applies to this gate if (!options.ApplyToStatuses.Contains(request.RequestedStatus, StringComparer.OrdinalIgnoreCase)) { return Task.FromResult(CreateAllowResult(gateId, "status_not_applicable", request.VexTrustStatus)); } // Check if trust data is present var trustStatus = request.VexTrustStatus; if (trustStatus is null) { return Task.FromResult(HandleMissingTrust(gateId, options, request)); } // Get environment-specific thresholds var thresholds = GetThresholds(request.Environment, options); // Evaluate trust checks var checks = new List(); // Check 1: Composite score var compositeCheck = new VexTrustCheck( "composite_score", trustStatus.TrustScore >= thresholds.MinCompositeScore, $"Score {trustStatus.TrustScore:F2} vs required {thresholds.MinCompositeScore:F2}"); checks.Add(compositeCheck); // Check 2: Issuer verified if (thresholds.RequireIssuerVerified) { var verifiedCheck = new VexTrustCheck( "issuer_verified", trustStatus.SignatureVerified == true, trustStatus.SignatureVerified == true ? "Signature verified" : "Signature not verified"); checks.Add(verifiedCheck); } // Check 3: Freshness var freshnessCheck = new VexTrustCheck( "freshness", IsAcceptableFreshness(trustStatus.Freshness, thresholds), $"Freshness: {trustStatus.Freshness ?? "unknown"}"); checks.Add(freshnessCheck); // Check 4: Accuracy rate (optional) if (thresholds.MinAccuracyRate.HasValue && trustStatus.TrustBreakdown?.AccuracyScore.HasValue == true) { var accuracyCheck = new VexTrustCheck( "accuracy_rate", trustStatus.TrustBreakdown.AccuracyScore >= thresholds.MinAccuracyRate, $"Accuracy {trustStatus.TrustBreakdown.AccuracyScore:P0} vs required {thresholds.MinAccuracyRate:P0}"); checks.Add(accuracyCheck); } // Aggregate results var failedChecks = checks.Where(c => !c.Passed).ToList(); if (failedChecks.Count > 0) { var decision = thresholds.FailureAction == FailureAction.Block ? VexTrustGateDecision.Block : VexTrustGateDecision.Warn; _logger.LogInformation( "VexTrustGate {Decision} for status {Status} in {Environment}: {FailedChecks}", decision, request.RequestedStatus, request.Environment, string.Join(", ", failedChecks.Select(c => c.Name))); return Task.FromResult(new VexTrustGateResult { GateId = gateId, Decision = decision, Reason = "vex_trust_below_threshold", Checks = checks, TrustScore = trustStatus.TrustScore, TrustTier = ComputeTier(trustStatus.TrustScore), IssuerId = trustStatus.IssuerId, SignatureVerified = trustStatus.SignatureVerified, Suggestion = BuildSuggestion(failedChecks, request), EvaluatedAt = now, Details = ImmutableDictionary.Empty .Add("failed_checks", failedChecks.Select(c => c.Name).ToList()) .Add("threshold", thresholds.MinCompositeScore) .Add("environment", request.Environment) }); } // All checks passed _logger.LogDebug( "VexTrustGate allowed status {Status} for issuer {Issuer} with score {Score}", request.RequestedStatus, trustStatus.IssuerId, trustStatus.TrustScore); return Task.FromResult(new VexTrustGateResult { GateId = gateId, Decision = VexTrustGateDecision.Allow, Reason = "vex_trust_adequate", Checks = checks, TrustScore = trustStatus.TrustScore, TrustTier = ComputeTier(trustStatus.TrustScore), IssuerId = trustStatus.IssuerId, SignatureVerified = trustStatus.SignatureVerified, EvaluatedAt = now, Details = ImmutableDictionary.Empty .Add("issuer", trustStatus.IssuerName ?? "unknown") .Add("verified", trustStatus.SignatureVerified ?? false) }); } private VexTrustGateResult HandleMissingTrust( string gateId, VexTrustGateOptions options, VexTrustGateRequest request) { var decision = options.MissingTrustBehavior switch { MissingTrustBehavior.Block => VexTrustGateDecision.Block, MissingTrustBehavior.Warn => VexTrustGateDecision.Warn, MissingTrustBehavior.Allow => VexTrustGateDecision.Allow, _ => VexTrustGateDecision.Warn }; _logger.LogInformation( "VexTrustGate {Decision} for status {Status}: missing trust data", decision, request.RequestedStatus); return new VexTrustGateResult { GateId = gateId, Decision = decision, Reason = "missing_vex_trust_data", Checks = Array.Empty(), Suggestion = "Ensure VEX document has signature verification and issuer is in IssuerDirectory", EvaluatedAt = _timeProvider.GetUtcNow() }; } private VexTrustGateResult CreateAllowResult( string gateId, string reason, VexTrustStatus? trustStatus) { return new VexTrustGateResult { GateId = gateId, Decision = VexTrustGateDecision.Allow, Reason = reason, TrustScore = trustStatus?.TrustScore, TrustTier = trustStatus is not null ? ComputeTier(trustStatus.TrustScore) : null, IssuerId = trustStatus?.IssuerId, EvaluatedAt = _timeProvider.GetUtcNow() }; } private static VexTrustThresholds GetThresholds( string environment, VexTrustGateOptions options) { if (options.Thresholds.TryGetValue(environment, out var thresholds)) { return thresholds; } // Fallback to default thresholds return options.Thresholds.TryGetValue("default", out var defaultThresholds) ? defaultThresholds : new VexTrustThresholds(); } private static bool IsAcceptableFreshness( string? freshness, VexTrustThresholds thresholds) { if (string.IsNullOrEmpty(freshness)) { // Unknown freshness - treat as acceptable if fresh is not required return thresholds.AcceptableFreshness.Count == 0 || thresholds.AcceptableFreshness.Contains("unknown", StringComparer.OrdinalIgnoreCase); } return thresholds.AcceptableFreshness.Contains(freshness, StringComparer.OrdinalIgnoreCase); } private static string ComputeTier(decimal score) { return score switch { >= 0.9m => "VeryHigh", >= 0.7m => "High", >= 0.5m => "Medium", >= 0.3m => "Low", _ => "VeryLow" }; } private static string BuildSuggestion( List failedChecks, VexTrustGateRequest request) { var suggestions = new List(); foreach (var check in failedChecks) { var suggestion = check.Name switch { "composite_score" => "Obtain VEX from a higher-trust source or verify the existing VEX signature", "issuer_verified" => "Ensure VEX document is signed and the signing key is registered in IssuerDirectory", "freshness" => "Obtain a more recent VEX statement; the current one may be stale or superseded", "accuracy_rate" => "The issuer's historical accuracy is below threshold; consider alternative sources", _ => $"Address {check.Name} check failure" }; suggestions.Add(suggestion); } return string.Join("; ", suggestions); } }