save progress
This commit is contained in:
310
src/Policy/__Libraries/StellaOps.Policy/Gates/VexProofGate.cs
Normal file
310
src/Policy/__Libraries/StellaOps.Policy/Gates/VexProofGate.cs
Normal file
@@ -0,0 +1,310 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
|
||||
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Options for the VEX proof gate.
|
||||
/// </summary>
|
||||
public sealed record VexProofGateOptions
|
||||
{
|
||||
/// <summary>Whether the gate is enabled.</summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum required confidence tier.
|
||||
/// Values: high, medium, low. Gate passes if proof confidence tier meets or exceeds this.
|
||||
/// </summary>
|
||||
public string MinimumConfidenceTier { get; init; } = "medium";
|
||||
|
||||
/// <summary>
|
||||
/// Whether a proof is required for NotAffected status.
|
||||
/// </summary>
|
||||
public bool RequireProofForNotAffected { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a proof is required for Fixed status.
|
||||
/// </summary>
|
||||
public bool RequireProofForFixed { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of allowed conflicts in the proof.
|
||||
/// Set to -1 to allow unlimited conflicts.
|
||||
/// </summary>
|
||||
public int MaxAllowedConflicts { get; init; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum age (in hours) for the proof to be considered valid.
|
||||
/// Set to -1 for no age limit.
|
||||
/// </summary>
|
||||
public int MaxProofAgeHours { get; init; } = 168; // 7 days
|
||||
|
||||
/// <summary>
|
||||
/// Whether to require signature verification on all input statements.
|
||||
/// </summary>
|
||||
public bool RequireSignedStatements { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of input statements required for the proof to be valid.
|
||||
/// </summary>
|
||||
public int MinimumInputStatements { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Environment-specific overrides for minimum confidence tier.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> EnvironmentConfidenceTiers { get; init; } =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["production"] = "high",
|
||||
["staging"] = "medium",
|
||||
["development"] = "low",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context providing VEX proof data to the gate.
|
||||
/// Extended from PolicyGateContext.
|
||||
/// </summary>
|
||||
public sealed record VexProofGateContext
|
||||
{
|
||||
/// <summary>Whether a VEX proof exists for this finding.</summary>
|
||||
public bool HasProof { get; init; }
|
||||
|
||||
/// <summary>Confidence tier of the proof (high, medium, low).</summary>
|
||||
public string? ProofConfidenceTier { get; init; }
|
||||
|
||||
/// <summary>Confidence score from the proof.</summary>
|
||||
public double? ProofConfidenceScore { get; init; }
|
||||
|
||||
/// <summary>Number of conflicts detected in the proof.</summary>
|
||||
public int? ConflictCount { get; init; }
|
||||
|
||||
/// <summary>Number of input statements used in the proof.</summary>
|
||||
public int? InputStatementCount { get; init; }
|
||||
|
||||
/// <summary>Whether all input statements were signed.</summary>
|
||||
public bool? AllStatementsSigned { get; init; }
|
||||
|
||||
/// <summary>When the proof was computed.</summary>
|
||||
public DateTimeOffset? ProofComputedAt { get; init; }
|
||||
|
||||
/// <summary>The proof ID for audit trail.</summary>
|
||||
public string? ProofId { get; init; }
|
||||
|
||||
/// <summary>Consensus outcome from the proof.</summary>
|
||||
public string? ConsensusOutcome { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gate that validates VEX proof objects meet policy requirements.
|
||||
/// </summary>
|
||||
public sealed class VexProofGate : IPolicyGate
|
||||
{
|
||||
private readonly VexProofGateOptions _options;
|
||||
|
||||
// Confidence tier ordering for comparison
|
||||
private static readonly IReadOnlyDictionary<string, int> ConfidenceTierOrder =
|
||||
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["low"] = 1,
|
||||
["medium"] = 2,
|
||||
["high"] = 3,
|
||||
};
|
||||
|
||||
public VexProofGate(VexProofGateOptions? options = null)
|
||||
{
|
||||
_options = options ?? new VexProofGateOptions();
|
||||
}
|
||||
|
||||
public Task<GateResult> EvaluateAsync(
|
||||
MergeResult mergeResult,
|
||||
PolicyGateContext context,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
return Task.FromResult(Pass("disabled"));
|
||||
}
|
||||
|
||||
// Check if proof is required for this status
|
||||
var requiresProof = RequiresProofForStatus(mergeResult.Status);
|
||||
if (!requiresProof)
|
||||
{
|
||||
return Task.FromResult(Pass("proof_not_required_for_status"));
|
||||
}
|
||||
|
||||
// Try to get VEX proof context from metadata
|
||||
var proofContext = ExtractProofContext(context);
|
||||
|
||||
if (!proofContext.HasProof)
|
||||
{
|
||||
return Task.FromResult(Fail("proof_required_but_missing",
|
||||
ImmutableDictionary<string, object>.Empty
|
||||
.Add("status", mergeResult.Status.ToString())
|
||||
.Add("requiresProof", true)));
|
||||
}
|
||||
|
||||
var details = new Dictionary<string, object>
|
||||
{
|
||||
["proofId"] = proofContext.ProofId ?? "unknown",
|
||||
["status"] = mergeResult.Status.ToString(),
|
||||
};
|
||||
|
||||
// Validate confidence tier
|
||||
var requiredTier = GetRequiredConfidenceTier(context.Environment);
|
||||
if (!string.IsNullOrEmpty(proofContext.ProofConfidenceTier))
|
||||
{
|
||||
details["proofConfidenceTier"] = proofContext.ProofConfidenceTier;
|
||||
details["requiredConfidenceTier"] = requiredTier;
|
||||
|
||||
if (!MeetsConfidenceTierRequirement(proofContext.ProofConfidenceTier, requiredTier))
|
||||
{
|
||||
return Task.FromResult(Fail("confidence_tier_too_low", details.ToImmutableDictionary()));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate confidence score
|
||||
if (proofContext.ProofConfidenceScore.HasValue)
|
||||
{
|
||||
details["proofConfidenceScore"] = proofContext.ProofConfidenceScore.Value;
|
||||
}
|
||||
|
||||
// Validate conflict count
|
||||
if (proofContext.ConflictCount.HasValue && _options.MaxAllowedConflicts >= 0)
|
||||
{
|
||||
details["conflictCount"] = proofContext.ConflictCount.Value;
|
||||
details["maxAllowedConflicts"] = _options.MaxAllowedConflicts;
|
||||
|
||||
if (proofContext.ConflictCount.Value > _options.MaxAllowedConflicts)
|
||||
{
|
||||
return Task.FromResult(Fail("too_many_conflicts", details.ToImmutableDictionary()));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate input statement count
|
||||
if (proofContext.InputStatementCount.HasValue)
|
||||
{
|
||||
details["inputStatementCount"] = proofContext.InputStatementCount.Value;
|
||||
details["minimumInputStatements"] = _options.MinimumInputStatements;
|
||||
|
||||
if (proofContext.InputStatementCount.Value < _options.MinimumInputStatements)
|
||||
{
|
||||
return Task.FromResult(Fail("insufficient_input_statements", details.ToImmutableDictionary()));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate signature requirement
|
||||
if (_options.RequireSignedStatements && proofContext.AllStatementsSigned == false)
|
||||
{
|
||||
details["allStatementsSigned"] = false;
|
||||
details["requireSignedStatements"] = true;
|
||||
return Task.FromResult(Fail("unsigned_statements", details.ToImmutableDictionary()));
|
||||
}
|
||||
|
||||
// Validate proof age
|
||||
if (_options.MaxProofAgeHours >= 0 && proofContext.ProofComputedAt.HasValue)
|
||||
{
|
||||
var proofAge = DateTimeOffset.UtcNow - proofContext.ProofComputedAt.Value;
|
||||
details["proofAgeHours"] = proofAge.TotalHours;
|
||||
details["maxProofAgeHours"] = _options.MaxProofAgeHours;
|
||||
|
||||
if (proofAge.TotalHours > _options.MaxProofAgeHours)
|
||||
{
|
||||
return Task.FromResult(Fail("proof_too_old", details.ToImmutableDictionary()));
|
||||
}
|
||||
}
|
||||
|
||||
// Add consensus outcome if available
|
||||
if (!string.IsNullOrEmpty(proofContext.ConsensusOutcome))
|
||||
{
|
||||
details["consensusOutcome"] = proofContext.ConsensusOutcome;
|
||||
}
|
||||
|
||||
return Task.FromResult(new GateResult
|
||||
{
|
||||
GateName = nameof(VexProofGate),
|
||||
Passed = true,
|
||||
Reason = "proof_valid",
|
||||
Details = details.ToImmutableDictionary(),
|
||||
});
|
||||
}
|
||||
|
||||
private bool RequiresProofForStatus(VexStatus status) => status switch
|
||||
{
|
||||
VexStatus.NotAffected => _options.RequireProofForNotAffected,
|
||||
VexStatus.Fixed => _options.RequireProofForFixed,
|
||||
_ => false, // Affected and UnderInvestigation don't require proof
|
||||
};
|
||||
|
||||
private string GetRequiredConfidenceTier(string environment)
|
||||
{
|
||||
if (_options.EnvironmentConfidenceTiers.TryGetValue(environment, out var tier))
|
||||
{
|
||||
return tier;
|
||||
}
|
||||
|
||||
return _options.MinimumConfidenceTier;
|
||||
}
|
||||
|
||||
private static bool MeetsConfidenceTierRequirement(string actualTier, string requiredTier)
|
||||
{
|
||||
if (!ConfidenceTierOrder.TryGetValue(actualTier, out var actualOrder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ConfidenceTierOrder.TryGetValue(requiredTier, out var requiredOrder))
|
||||
{
|
||||
return true; // Unknown required tier, pass by default
|
||||
}
|
||||
|
||||
return actualOrder >= requiredOrder;
|
||||
}
|
||||
|
||||
private static VexProofGateContext ExtractProofContext(PolicyGateContext context)
|
||||
{
|
||||
var proofContext = new VexProofGateContext();
|
||||
|
||||
if (context.Metadata == null)
|
||||
{
|
||||
return proofContext;
|
||||
}
|
||||
|
||||
return new VexProofGateContext
|
||||
{
|
||||
HasProof = context.Metadata.TryGetValue("vex_proof_id", out _),
|
||||
ProofId = context.Metadata.GetValueOrDefault("vex_proof_id"),
|
||||
ProofConfidenceTier = context.Metadata.GetValueOrDefault("vex_proof_confidence_tier"),
|
||||
ProofConfidenceScore = context.Metadata.TryGetValue("vex_proof_confidence_score", out var scoreStr) &&
|
||||
double.TryParse(scoreStr, out var score) ? score : null,
|
||||
ConflictCount = context.Metadata.TryGetValue("vex_proof_conflict_count", out var conflictStr) &&
|
||||
int.TryParse(conflictStr, out var conflicts) ? conflicts : null,
|
||||
InputStatementCount = context.Metadata.TryGetValue("vex_proof_statement_count", out var stmtStr) &&
|
||||
int.TryParse(stmtStr, out var stmtCount) ? stmtCount : null,
|
||||
AllStatementsSigned = context.Metadata.TryGetValue("vex_proof_all_signed", out var signedStr) &&
|
||||
bool.TryParse(signedStr, out var signed) ? signed : null,
|
||||
ProofComputedAt = context.Metadata.TryGetValue("vex_proof_computed_at", out var timeStr) &&
|
||||
DateTimeOffset.TryParse(timeStr, out var time) ? time : null,
|
||||
ConsensusOutcome = context.Metadata.GetValueOrDefault("vex_proof_consensus_outcome"),
|
||||
};
|
||||
}
|
||||
|
||||
private static GateResult Pass(string reason) => new()
|
||||
{
|
||||
GateName = nameof(VexProofGate),
|
||||
Passed = true,
|
||||
Reason = reason,
|
||||
Details = ImmutableDictionary<string, object>.Empty,
|
||||
};
|
||||
|
||||
private static GateResult Fail(string reason, ImmutableDictionary<string, object>? details = null) => new()
|
||||
{
|
||||
GateName = nameof(VexProofGate),
|
||||
Passed = false,
|
||||
Reason = reason,
|
||||
Details = details ?? ImmutableDictionary<string, object>.Empty,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user