Complete batch 012 (golden set diff) and 013 (advisory chat), fix build errors
Sprints completed: - SPRINT_20260110_012_* (golden set diff layer - 10 sprints) - SPRINT_20260110_013_* (advisory chat - 4 sprints) Build fixes applied: - Fix namespace conflicts with Microsoft.Extensions.Options.Options.Create - Fix VexDecisionReachabilityIntegrationTests API drift (major rewrite) - Fix VexSchemaValidationTests FluentAssertions method name - Fix FixChainGateIntegrationTests ambiguous type references - Fix AdvisoryAI test files required properties and namespace aliases - Add stub types for CveMappingController (ICveSymbolMappingService) - Fix VerdictBuilderService static context issue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
427
src/Policy/__Libraries/StellaOps.Policy/Gates/FixChainGate.cs
Normal file
427
src/Policy/__Libraries/StellaOps.Policy/Gates/FixChainGate.cs
Normal file
@@ -0,0 +1,427 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
|
||||
// Sprint: SPRINT_20260110_012_008_POLICY
|
||||
// Task: FCG-001 through FCG-003 - FixChain Gate Predicate
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Options for the FixChain verification gate.
|
||||
/// </summary>
|
||||
public sealed record FixChainGateOptions
|
||||
{
|
||||
/// <summary>Whether the gate is enabled.</summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Severities that require fix verification.
|
||||
/// Default: critical and high.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> RequiredSeverities { get; init; } = ["critical", "high"];
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence for the fix to be considered verified.
|
||||
/// </summary>
|
||||
public decimal MinimumConfidence { get; init; } = 0.85m;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow inconclusive verdicts to pass (with warning).
|
||||
/// </summary>
|
||||
public bool AllowInconclusive { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Grace period (days) after CVE publication before requiring fix verification.
|
||||
/// Allows time for golden set creation.
|
||||
/// </summary>
|
||||
public int GracePeriodDays { get; init; } = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum age (hours) for cached fix verification status.
|
||||
/// </summary>
|
||||
public int MaxStatusAgeHours { get; init; } = 24;
|
||||
|
||||
/// <summary>
|
||||
/// Environment-specific minimum confidence overrides.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, decimal> EnvironmentConfidence { get; init; } =
|
||||
new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["production"] = 0.95m,
|
||||
["staging"] = 0.85m,
|
||||
["development"] = 0.70m,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Environment-specific severity requirements.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> EnvironmentSeverities { get; init; } =
|
||||
new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["production"] = ["critical", "high"],
|
||||
["staging"] = ["critical"],
|
||||
["development"] = [],
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context providing FixChain verification data to the gate.
|
||||
/// </summary>
|
||||
public sealed record FixChainGateContext
|
||||
{
|
||||
/// <summary>Whether a FixChain attestation exists for this finding.</summary>
|
||||
public bool HasAttestation { get; init; }
|
||||
|
||||
/// <summary>Verdict from FixChain verification: fixed, partial, not_fixed, inconclusive.</summary>
|
||||
public string? Verdict { get; init; }
|
||||
|
||||
/// <summary>Confidence score from the verification (0.0 - 1.0).</summary>
|
||||
public decimal? Confidence { get; init; }
|
||||
|
||||
/// <summary>When the verification was performed.</summary>
|
||||
public DateTimeOffset? VerifiedAt { get; init; }
|
||||
|
||||
/// <summary>Attestation digest for audit trail.</summary>
|
||||
public string? AttestationDigest { get; init; }
|
||||
|
||||
/// <summary>Golden set ID used for verification.</summary>
|
||||
public string? GoldenSetId { get; init; }
|
||||
|
||||
/// <summary>CVE publication date (for grace period calculation).</summary>
|
||||
public DateTimeOffset? CvePublishedAt { get; init; }
|
||||
|
||||
/// <summary>Rationale from the verification.</summary>
|
||||
public IReadOnlyList<string> Rationale { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of FixChain gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record FixChainGateResult
|
||||
{
|
||||
/// <summary>Whether the gate passed.</summary>
|
||||
public required bool Passed { get; init; }
|
||||
|
||||
/// <summary>Gate decision: allow, warn, block.</summary>
|
||||
public required string Decision { get; init; }
|
||||
|
||||
/// <summary>Human-readable reason for the decision.</summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>Detailed evidence for the decision.</summary>
|
||||
public ImmutableDictionary<string, object> Details { get; init; } =
|
||||
ImmutableDictionary<string, object>.Empty;
|
||||
|
||||
/// <summary>Decision constants.</summary>
|
||||
public const string DecisionAllow = "allow";
|
||||
public const string DecisionWarn = "warn";
|
||||
public const string DecisionBlock = "block";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy gate that requires fix verification for critical vulnerabilities.
|
||||
/// </summary>
|
||||
public sealed class FixChainGate : IPolicyGate
|
||||
{
|
||||
private readonly FixChainGateOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IFixChainStatusProvider? _statusProvider;
|
||||
|
||||
public FixChainGate(FixChainGateOptions options, TimeProvider timeProvider)
|
||||
: this(options, timeProvider, null)
|
||||
{
|
||||
}
|
||||
|
||||
public FixChainGate(
|
||||
FixChainGateOptions options,
|
||||
TimeProvider timeProvider,
|
||||
IFixChainStatusProvider? statusProvider)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_statusProvider = statusProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<GateResult> EvaluateAsync(
|
||||
MergeResult mergeResult,
|
||||
PolicyGateContext context,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mergeResult);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
return CreateResult(true, "FixChain gate disabled");
|
||||
}
|
||||
|
||||
// Determine severity requirements for this environment
|
||||
var requiredSeverities = GetRequiredSeverities(context.Environment);
|
||||
if (requiredSeverities.Count == 0)
|
||||
{
|
||||
return CreateResult(true, "No severity requirements for this environment");
|
||||
}
|
||||
|
||||
// Check if this finding's severity requires verification
|
||||
var severity = context.Severity?.ToLowerInvariant() ?? "unknown";
|
||||
if (!requiredSeverities.Contains(severity, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return CreateResult(true, $"Severity '{severity}' does not require fix verification");
|
||||
}
|
||||
|
||||
// Get FixChain context from metadata or provider
|
||||
var fixChainContext = await GetFixChainContextAsync(context, ct);
|
||||
|
||||
// Check grace period
|
||||
if (IsInGracePeriod(fixChainContext))
|
||||
{
|
||||
return CreateResult(
|
||||
true,
|
||||
"Within grace period for golden set creation",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["gracePeriodDays"] = _options.GracePeriodDays,
|
||||
["cvePublishedAt"] = fixChainContext.CvePublishedAt?.ToString("o", CultureInfo.InvariantCulture) ?? "unknown"
|
||||
});
|
||||
}
|
||||
|
||||
// No attestation found
|
||||
if (!fixChainContext.HasAttestation)
|
||||
{
|
||||
return CreateResult(
|
||||
false,
|
||||
$"Fix verification required for {severity} vulnerability but no FixChain attestation found",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["severity"] = severity,
|
||||
["cveId"] = context.CveId ?? "unknown"
|
||||
});
|
||||
}
|
||||
|
||||
// Evaluate verdict
|
||||
return EvaluateVerdict(fixChainContext, context.Environment, severity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a FixChain context directly (for standalone use).
|
||||
/// </summary>
|
||||
public FixChainGateResult EvaluateDirect(
|
||||
FixChainGateContext fixChainContext,
|
||||
string environment,
|
||||
string severity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fixChainContext);
|
||||
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
return new FixChainGateResult
|
||||
{
|
||||
Passed = true,
|
||||
Decision = FixChainGateResult.DecisionAllow,
|
||||
Reason = "FixChain gate disabled"
|
||||
};
|
||||
}
|
||||
|
||||
var requiredSeverities = GetRequiredSeverities(environment);
|
||||
if (!requiredSeverities.Contains(severity, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return new FixChainGateResult
|
||||
{
|
||||
Passed = true,
|
||||
Decision = FixChainGateResult.DecisionAllow,
|
||||
Reason = $"Severity '{severity}' does not require fix verification"
|
||||
};
|
||||
}
|
||||
|
||||
if (IsInGracePeriod(fixChainContext))
|
||||
{
|
||||
return new FixChainGateResult
|
||||
{
|
||||
Passed = true,
|
||||
Decision = FixChainGateResult.DecisionAllow,
|
||||
Reason = "Within grace period for golden set creation"
|
||||
};
|
||||
}
|
||||
|
||||
if (!fixChainContext.HasAttestation)
|
||||
{
|
||||
return new FixChainGateResult
|
||||
{
|
||||
Passed = false,
|
||||
Decision = FixChainGateResult.DecisionBlock,
|
||||
Reason = $"Fix verification required for {severity} vulnerability but no FixChain attestation found"
|
||||
};
|
||||
}
|
||||
|
||||
var gateResult = EvaluateVerdict(fixChainContext, environment, severity);
|
||||
return new FixChainGateResult
|
||||
{
|
||||
Passed = gateResult.Passed,
|
||||
Decision = gateResult.Passed ? FixChainGateResult.DecisionAllow :
|
||||
(fixChainContext.Verdict == "inconclusive" && _options.AllowInconclusive
|
||||
? FixChainGateResult.DecisionWarn
|
||||
: FixChainGateResult.DecisionBlock),
|
||||
Reason = gateResult.Reason ?? "Unknown",
|
||||
Details = gateResult.Details
|
||||
};
|
||||
}
|
||||
|
||||
private GateResult EvaluateVerdict(
|
||||
FixChainGateContext fixChainContext,
|
||||
string environment,
|
||||
string severity)
|
||||
{
|
||||
var verdict = fixChainContext.Verdict?.ToLowerInvariant() ?? "unknown";
|
||||
var confidence = fixChainContext.Confidence ?? 0m;
|
||||
var minConfidence = GetMinimumConfidence(environment);
|
||||
|
||||
var details = new Dictionary<string, object>
|
||||
{
|
||||
["verdict"] = verdict,
|
||||
["confidence"] = confidence,
|
||||
["minConfidence"] = minConfidence,
|
||||
["severity"] = severity,
|
||||
["attestationDigest"] = fixChainContext.AttestationDigest ?? "none",
|
||||
["goldenSetId"] = fixChainContext.GoldenSetId ?? "none"
|
||||
};
|
||||
|
||||
return verdict switch
|
||||
{
|
||||
"fixed" when confidence >= minConfidence =>
|
||||
CreateResult(true, "Fix verified with sufficient confidence", details),
|
||||
|
||||
"fixed" =>
|
||||
CreateResult(
|
||||
false,
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Fix verified but confidence {0:P0} below minimum {1:P0}",
|
||||
confidence, minConfidence),
|
||||
details),
|
||||
|
||||
"partial" =>
|
||||
CreateResult(
|
||||
false,
|
||||
"Partial fix detected - vulnerability not fully addressed",
|
||||
details),
|
||||
|
||||
"not_fixed" =>
|
||||
CreateResult(
|
||||
false,
|
||||
"Verification confirms vulnerability is NOT fixed",
|
||||
details),
|
||||
|
||||
"inconclusive" when _options.AllowInconclusive =>
|
||||
CreateResult(
|
||||
true,
|
||||
"Verification inconclusive but allowed by policy",
|
||||
details),
|
||||
|
||||
"inconclusive" =>
|
||||
CreateResult(
|
||||
false,
|
||||
"Verification inconclusive and inconclusive verdicts not allowed",
|
||||
details),
|
||||
|
||||
_ => CreateResult(
|
||||
false,
|
||||
$"Unknown verdict: {verdict}",
|
||||
details)
|
||||
};
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> GetRequiredSeverities(string environment)
|
||||
{
|
||||
if (_options.EnvironmentSeverities.TryGetValue(environment, out var severities))
|
||||
{
|
||||
return severities;
|
||||
}
|
||||
return _options.RequiredSeverities;
|
||||
}
|
||||
|
||||
private decimal GetMinimumConfidence(string environment)
|
||||
{
|
||||
if (_options.EnvironmentConfidence.TryGetValue(environment, out var confidence))
|
||||
{
|
||||
return confidence;
|
||||
}
|
||||
return _options.MinimumConfidence;
|
||||
}
|
||||
|
||||
private bool IsInGracePeriod(FixChainGateContext context)
|
||||
{
|
||||
if (_options.GracePeriodDays <= 0 || !context.CvePublishedAt.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var deadline = context.CvePublishedAt.Value.AddDays(_options.GracePeriodDays);
|
||||
return now < deadline;
|
||||
}
|
||||
|
||||
private async Task<FixChainGateContext> GetFixChainContextAsync(
|
||||
PolicyGateContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Try to get from status provider if available
|
||||
if (_statusProvider != null && !string.IsNullOrEmpty(context.CveId))
|
||||
{
|
||||
var status = await _statusProvider.GetStatusAsync(context.CveId, context.SubjectKey, ct);
|
||||
if (status != null)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to metadata if available
|
||||
if (context.Metadata != null)
|
||||
{
|
||||
return new FixChainGateContext
|
||||
{
|
||||
HasAttestation = context.Metadata.TryGetValue("fixchain.hasAttestation", out var hasAtt)
|
||||
&& bool.TryParse(hasAtt, out var b) && b,
|
||||
Verdict = context.Metadata.GetValueOrDefault("fixchain.verdict"),
|
||||
Confidence = context.Metadata.TryGetValue("fixchain.confidence", out var conf)
|
||||
&& decimal.TryParse(conf, CultureInfo.InvariantCulture, out var c) ? c : null,
|
||||
AttestationDigest = context.Metadata.GetValueOrDefault("fixchain.attestationDigest"),
|
||||
GoldenSetId = context.Metadata.GetValueOrDefault("fixchain.goldenSetId"),
|
||||
CvePublishedAt = context.Metadata.TryGetValue("fixchain.cvePublishedAt", out var pub)
|
||||
&& DateTimeOffset.TryParse(pub, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var d) ? d : null
|
||||
};
|
||||
}
|
||||
|
||||
return new FixChainGateContext { HasAttestation = false };
|
||||
}
|
||||
|
||||
private static GateResult CreateResult(
|
||||
bool passed,
|
||||
string reason,
|
||||
Dictionary<string, object>? details = null)
|
||||
{
|
||||
return new GateResult
|
||||
{
|
||||
GateName = "FixChainGate",
|
||||
Passed = passed,
|
||||
Reason = reason,
|
||||
Details = details?.ToImmutableDictionary() ?? ImmutableDictionary<string, object>.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provider interface for retrieving FixChain verification status.
|
||||
/// </summary>
|
||||
public interface IFixChainStatusProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the FixChain verification status for a CVE and subject.
|
||||
/// </summary>
|
||||
Task<FixChainGateContext?> GetStatusAsync(
|
||||
string cveId,
|
||||
string? subjectKey,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
Reference in New Issue
Block a user