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:
master
2026-01-11 10:09:07 +02:00
parent a3b2f30a11
commit 7f7eb8b228
232 changed files with 58979 additions and 91 deletions

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