sprints and audit work
This commit is contained in:
249
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateService.cs
Normal file
249
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateService.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IVexGateService"/>.
|
||||
/// Evaluates findings against VEX evidence and policy rules.
|
||||
/// </summary>
|
||||
public sealed class VexGateService : IVexGateService
|
||||
{
|
||||
private readonly IVexGatePolicy _policyEvaluator;
|
||||
private readonly IVexObservationProvider? _vexProvider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<VexGateService> _logger;
|
||||
|
||||
public VexGateService(
|
||||
IVexGatePolicy policyEvaluator,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<VexGateService> logger,
|
||||
IVexObservationProvider? vexProvider = null)
|
||||
{
|
||||
_policyEvaluator = policyEvaluator;
|
||||
_vexProvider = vexProvider;
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VexGateResult> 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<VexStatementRef>.Empty;
|
||||
|
||||
return new VexGateResult
|
||||
{
|
||||
Decision = decision,
|
||||
Rationale = rationale,
|
||||
PolicyRuleMatched = ruleId,
|
||||
ContributingStatements = contributingStatements,
|
||||
Evidence = evidence,
|
||||
EvaluatedAt = _timeProvider.GetUtcNow(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ImmutableArray<GatedFinding>> EvaluateBatchAsync(
|
||||
IReadOnlyList<VexGateFinding> findings,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (findings.Count == 0)
|
||||
{
|
||||
return ImmutableArray<GatedFinding>.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<GatedFinding>(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<VexGateEvidence> BuildEvidenceAsync(
|
||||
VexGateFinding finding,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
VexStatus? vendorStatus = null;
|
||||
VexJustification? justification = null;
|
||||
var backportHints = ImmutableArray<string>.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<ImmutableArray<VexStatementRef>> GetContributingStatementsAsync(
|
||||
string vulnerabilityId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_vexProvider is null)
|
||||
{
|
||||
return ImmutableArray<VexStatementRef>.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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key for VEX lookups.
|
||||
/// </summary>
|
||||
public sealed record VexLookupKey(string VulnerabilityId, string Purl);
|
||||
|
||||
/// <summary>
|
||||
/// Result from VEX observation provider.
|
||||
/// </summary>
|
||||
public sealed record VexObservationResult
|
||||
{
|
||||
public required VexStatus Status { get; init; }
|
||||
public VexJustification? Justification { get; init; }
|
||||
public double Confidence { get; init; } = 1.0;
|
||||
public ImmutableArray<string> BackportHints { get; init; } = ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX statement info for contributing statements.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for VEX observation data provider.
|
||||
/// Abstracts access to VEX statements from Excititor or other sources.
|
||||
/// </summary>
|
||||
public interface IVexObservationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the VEX status for a vulnerability and component.
|
||||
/// </summary>
|
||||
Task<VexObservationResult?> GetVexStatusAsync(
|
||||
string vulnerabilityId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all VEX statements for a vulnerability and component.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexStatementInfo>> GetStatementsAsync(
|
||||
string vulnerabilityId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended interface for batch VEX observation prefetching.
|
||||
/// </summary>
|
||||
public interface IVexObservationBatchProvider : IVexObservationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefetches VEX data for multiple lookups.
|
||||
/// </summary>
|
||||
Task PrefetchAsync(
|
||||
IReadOnlyList<VexLookupKey> keys,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user