// 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);
}
}