save progress

This commit is contained in:
StellaOps Bot
2026-01-03 15:27:15 +02:00
parent d486d41a48
commit bc4dd4f377
70 changed files with 8531 additions and 653 deletions

View 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,
};
}