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