// ----------------------------------------------------------------------------- // VexGateService.cs // Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service // Description: VEX gate service implementation. // ----------------------------------------------------------------------------- using System.Collections.Immutable; using Microsoft.Extensions.Logging; namespace StellaOps.Scanner.Gate; /// /// Default implementation of . /// Evaluates findings against VEX evidence and policy rules. /// public sealed class VexGateService : IVexGateService { private readonly IVexGatePolicy _policyEvaluator; private readonly IVexObservationProvider? _vexProvider; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; public VexGateService( IVexGatePolicy policyEvaluator, TimeProvider timeProvider, ILogger logger, IVexObservationProvider? vexProvider = null) { _policyEvaluator = policyEvaluator; _vexProvider = vexProvider; _timeProvider = timeProvider; _logger = logger; } /// public async Task EvaluateAsync( VexGateFinding finding, CancellationToken cancellationToken = default) { _logger.LogDebug( "Evaluating VEX gate for finding {FindingId} ({VulnerabilityId})", finding.FindingId, finding.VulnerabilityId); // Collect evidence from VEX provider and finding context var evidence = await BuildEvidenceAsync(finding, cancellationToken); // Evaluate against policy rules var (decision, ruleId, rationale) = _policyEvaluator.Evaluate(evidence); // Build statement references if we have VEX data var contributingStatements = evidence.VendorStatus is not null ? await GetContributingStatementsAsync( finding.VulnerabilityId, finding.Purl, cancellationToken) : ImmutableArray.Empty; return new VexGateResult { Decision = decision, Rationale = rationale, PolicyRuleMatched = ruleId, ContributingStatements = contributingStatements, Evidence = evidence, EvaluatedAt = _timeProvider.GetUtcNow(), }; } /// public async Task> EvaluateBatchAsync( IReadOnlyList findings, CancellationToken cancellationToken = default) { if (findings.Count == 0) { return ImmutableArray.Empty; } _logger.LogDebug("Evaluating VEX gate for {Count} findings in batch", findings.Count); // Pre-fetch VEX data for all findings if provider supports batch if (_vexProvider is IVexObservationBatchProvider batchProvider) { var queries = findings .Select(f => new VexLookupKey(f.VulnerabilityId, f.Purl)) .Distinct() .ToList(); await batchProvider.PrefetchAsync(queries, cancellationToken); } // Evaluate each finding var results = new List(findings.Count); foreach (var finding in findings) { cancellationToken.ThrowIfCancellationRequested(); var gateResult = await EvaluateAsync(finding, cancellationToken); results.Add(new GatedFinding { Finding = finding, GateResult = gateResult, }); } _logger.LogInformation( "VEX gate batch complete: {Pass} passed, {Warn} warned, {Block} blocked", results.Count(r => r.GateResult.Decision == VexGateDecision.Pass), results.Count(r => r.GateResult.Decision == VexGateDecision.Warn), results.Count(r => r.GateResult.Decision == VexGateDecision.Block)); return results.ToImmutableArray(); } private async Task BuildEvidenceAsync( VexGateFinding finding, CancellationToken cancellationToken) { VexStatus? vendorStatus = null; VexJustification? justification = null; var backportHints = ImmutableArray.Empty; var confidenceScore = 0.5; // Default confidence // Query VEX provider if available if (_vexProvider is not null) { var vexResult = await _vexProvider.GetVexStatusAsync( finding.VulnerabilityId, finding.Purl, cancellationToken); if (vexResult is not null) { vendorStatus = vexResult.Status; justification = vexResult.Justification; confidenceScore = vexResult.Confidence; backportHints = vexResult.BackportHints; } } // Use exploitability from finding or infer from VEX status var isExploitable = finding.IsExploitable ?? (vendorStatus == VexStatus.Affected); return new VexGateEvidence { VendorStatus = vendorStatus, Justification = justification, IsReachable = finding.IsReachable ?? true, // Conservative: assume reachable if unknown HasCompensatingControl = finding.HasCompensatingControl ?? false, ConfidenceScore = confidenceScore, BackportHints = backportHints, IsExploitable = isExploitable, SeverityLevel = finding.SeverityLevel, }; } private async Task> GetContributingStatementsAsync( string vulnerabilityId, string purl, CancellationToken cancellationToken) { if (_vexProvider is null) { return ImmutableArray.Empty; } var statements = await _vexProvider.GetStatementsAsync( vulnerabilityId, purl, cancellationToken); return statements .Select(s => new VexStatementRef { StatementId = s.StatementId, IssuerId = s.IssuerId, Status = s.Status, Timestamp = s.Timestamp, TrustWeight = s.TrustWeight, }) .ToImmutableArray(); } } /// /// Key for VEX lookups. /// public sealed record VexLookupKey(string VulnerabilityId, string Purl); /// /// Result from VEX observation provider. /// public sealed record VexObservationResult { public required VexStatus Status { get; init; } public VexJustification? Justification { get; init; } public double Confidence { get; init; } = 1.0; public ImmutableArray BackportHints { get; init; } = ImmutableArray.Empty; } /// /// VEX statement info for contributing statements. /// public sealed record VexStatementInfo { public required string StatementId { get; init; } public required string IssuerId { get; init; } public required VexStatus Status { get; init; } public required DateTimeOffset Timestamp { get; init; } public double TrustWeight { get; init; } = 1.0; } /// /// Interface for VEX observation data provider. /// Abstracts access to VEX statements from Excititor or other sources. /// public interface IVexObservationProvider { /// /// Gets the VEX status for a vulnerability and component. /// Task GetVexStatusAsync( string vulnerabilityId, string purl, CancellationToken cancellationToken = default); /// /// Gets all VEX statements for a vulnerability and component. /// Task> GetStatementsAsync( string vulnerabilityId, string purl, CancellationToken cancellationToken = default); } /// /// Extended interface for batch VEX observation prefetching. /// public interface IVexObservationBatchProvider : IVexObservationProvider { /// /// Prefetches VEX data for multiple lookups. /// Task PrefetchAsync( IReadOnlyList keys, CancellationToken cancellationToken = default); }