Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
489
src/Policy/StellaOps.Policy.Engine/Gates/VexTrustGate.cs
Normal file
489
src/Policy/StellaOps.Policy.Engine/Gates/VexTrustGate.cs
Normal file
@@ -0,0 +1,489 @@
|
||||
// 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 DateTimeOffset EvaluatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <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 static 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 = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user