490 lines
15 KiB
C#
490 lines
15 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Policy gate that enforces VEX trust thresholds.
|
|
/// Evaluates trust score, issuer verification, and freshness.
|
|
/// </summary>
|
|
public interface IVexTrustGate
|
|
{
|
|
/// <summary>
|
|
/// Evaluate VEX trust for a status transition request.
|
|
/// </summary>
|
|
Task<VexTrustGateResult> EvaluateAsync(
|
|
VexTrustGateRequest request,
|
|
CancellationToken ct = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request for VEX trust gate evaluation.
|
|
/// </summary>
|
|
public sealed record VexTrustGateRequest
|
|
{
|
|
/// <summary>
|
|
/// Requested VEX status (not_affected, fixed, etc.).
|
|
/// </summary>
|
|
public required string RequestedStatus { get; init; }
|
|
|
|
/// <summary>
|
|
/// Target environment (production, staging, development).
|
|
/// </summary>
|
|
public required string Environment { get; init; }
|
|
|
|
/// <summary>
|
|
/// VEX trust status with scores and breakdown.
|
|
/// </summary>
|
|
public VexTrustStatus? VexTrustStatus { get; init; }
|
|
|
|
/// <summary>
|
|
/// Tenant identifier for tenant-specific overrides.
|
|
/// </summary>
|
|
public string? TenantId { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// VEX trust status containing scores and verification details.
|
|
/// </summary>
|
|
public sealed record VexTrustStatus
|
|
{
|
|
/// <summary>
|
|
/// Composite trust score (0.0 - 1.0).
|
|
/// </summary>
|
|
public decimal TrustScore { get; init; }
|
|
|
|
/// <summary>
|
|
/// Policy threshold for this context.
|
|
/// </summary>
|
|
public decimal PolicyTrustThreshold { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether the trust score meets the policy threshold.
|
|
/// </summary>
|
|
public bool MeetsPolicyThreshold { get; init; }
|
|
|
|
/// <summary>
|
|
/// Trust score breakdown by factor.
|
|
/// </summary>
|
|
public TrustScoreBreakdown? TrustBreakdown { get; init; }
|
|
|
|
/// <summary>
|
|
/// Issuer display name.
|
|
/// </summary>
|
|
public string? IssuerName { get; init; }
|
|
|
|
/// <summary>
|
|
/// Issuer identifier.
|
|
/// </summary>
|
|
public string? IssuerId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether the signature was cryptographically verified.
|
|
/// </summary>
|
|
public bool? SignatureVerified { get; init; }
|
|
|
|
/// <summary>
|
|
/// Signature method used (dsse, cosign, pgp, etc.).
|
|
/// </summary>
|
|
public string? SignatureMethod { get; init; }
|
|
|
|
/// <summary>
|
|
/// Rekor transparency log index.
|
|
/// </summary>
|
|
public long? RekorLogIndex { get; init; }
|
|
|
|
/// <summary>
|
|
/// Rekor log identifier.
|
|
/// </summary>
|
|
public string? RekorLogId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Freshness status (fresh, stale, superseded, expired).
|
|
/// </summary>
|
|
public string? Freshness { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the VEX was verified.
|
|
/// </summary>
|
|
public DateTimeOffset? VerifiedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trust score breakdown by factor.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of VEX trust gate evaluation.
|
|
/// </summary>
|
|
public sealed record VexTrustGateResult
|
|
{
|
|
/// <summary>
|
|
/// Gate identifier.
|
|
/// </summary>
|
|
public required string GateId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gate decision (allow, warn, block).
|
|
/// </summary>
|
|
public required VexTrustGateDecision Decision { get; init; }
|
|
|
|
/// <summary>
|
|
/// Reason code for the decision.
|
|
/// </summary>
|
|
public required string Reason { get; init; }
|
|
|
|
/// <summary>
|
|
/// Checks that were evaluated.
|
|
/// </summary>
|
|
public IReadOnlyList<VexTrustCheck> Checks { get; init; } = Array.Empty<VexTrustCheck>();
|
|
|
|
/// <summary>
|
|
/// Trust score observed.
|
|
/// </summary>
|
|
public decimal? TrustScore { get; init; }
|
|
|
|
/// <summary>
|
|
/// Trust tier computed from score.
|
|
/// </summary>
|
|
public string? TrustTier { get; init; }
|
|
|
|
/// <summary>
|
|
/// Issuer identifier.
|
|
/// </summary>
|
|
public string? IssuerId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether signature was verified.
|
|
/// </summary>
|
|
public bool? SignatureVerified { get; init; }
|
|
|
|
/// <summary>
|
|
/// Suggestion for resolving a block.
|
|
/// </summary>
|
|
public string? Suggestion { get; init; }
|
|
|
|
/// <summary>
|
|
/// Timestamp when decision was made.
|
|
/// </summary>
|
|
public required DateTimeOffset EvaluatedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Additional details for audit.
|
|
/// </summary>
|
|
public ImmutableDictionary<string, object>? Details { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Individual trust check result.
|
|
/// </summary>
|
|
public sealed record VexTrustCheck(
|
|
string Name,
|
|
bool Passed,
|
|
string Reason);
|
|
|
|
/// <summary>
|
|
/// VEX trust gate decision type.
|
|
/// </summary>
|
|
[JsonConverter(typeof(JsonStringEnumConverter<VexTrustGateDecision>))]
|
|
public enum VexTrustGateDecision
|
|
{
|
|
/// <summary>Trust is adequate, allow transition.</summary>
|
|
Allow,
|
|
|
|
/// <summary>Trust is below threshold but transition allowed with warning.</summary>
|
|
Warn,
|
|
|
|
/// <summary>Trust is insufficient, block transition.</summary>
|
|
Block
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default implementation of VEX trust gate.
|
|
/// </summary>
|
|
public sealed class VexTrustGate : IVexTrustGate
|
|
{
|
|
private readonly IOptionsMonitor<VexTrustGateOptions> _options;
|
|
private readonly ILogger<VexTrustGate> _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<VexTrustGateOptions> options,
|
|
ILogger<VexTrustGate> logger,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<VexTrustGateResult> 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<VexTrustCheck>();
|
|
|
|
// 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<string, object>.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<string, object>.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<VexTrustCheck>(),
|
|
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<VexTrustCheck> failedChecks,
|
|
VexTrustGateRequest request)
|
|
{
|
|
var suggestions = new List<string>();
|
|
|
|
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);
|
|
}
|
|
}
|