sprints and audit work
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CachingVexObservationProvider.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: Caching wrapper for VEX observation provider with batch prefetch.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Caching wrapper for <see cref="IVexObservationProvider"/> that supports batch prefetch.
|
||||
/// Implements short TTL bounded cache for gate throughput optimization.
|
||||
/// </summary>
|
||||
public sealed class CachingVexObservationProvider : IVexObservationBatchProvider, IDisposable
|
||||
{
|
||||
private readonly IVexObservationQuery _query;
|
||||
private readonly string _tenantId;
|
||||
private readonly MemoryCache _cache;
|
||||
private readonly TimeSpan _cacheTtl;
|
||||
private readonly ILogger<CachingVexObservationProvider> _logger;
|
||||
private readonly SemaphoreSlim _prefetchLock = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Default cache size limit (number of entries).
|
||||
/// </summary>
|
||||
public const int DefaultCacheSizeLimit = 10_000;
|
||||
|
||||
/// <summary>
|
||||
/// Default cache TTL.
|
||||
/// </summary>
|
||||
public static readonly TimeSpan DefaultCacheTtl = TimeSpan.FromMinutes(5);
|
||||
|
||||
public CachingVexObservationProvider(
|
||||
IVexObservationQuery query,
|
||||
string tenantId,
|
||||
ILogger<CachingVexObservationProvider> logger,
|
||||
TimeSpan? cacheTtl = null,
|
||||
int? cacheSizeLimit = null)
|
||||
{
|
||||
_query = query;
|
||||
_tenantId = tenantId;
|
||||
_logger = logger;
|
||||
_cacheTtl = cacheTtl ?? DefaultCacheTtl;
|
||||
|
||||
_cache = new MemoryCache(new MemoryCacheOptions
|
||||
{
|
||||
SizeLimit = cacheSizeLimit ?? DefaultCacheSizeLimit,
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VexObservationResult?> GetVexStatusAsync(
|
||||
string vulnerabilityId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var cacheKey = BuildCacheKey(vulnerabilityId, purl);
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out VexObservationResult? cached))
|
||||
{
|
||||
_logger.LogTrace("VEX cache hit: {VulnerabilityId} / {Purl}", vulnerabilityId, purl);
|
||||
return cached;
|
||||
}
|
||||
|
||||
_logger.LogTrace("VEX cache miss: {VulnerabilityId} / {Purl}", vulnerabilityId, purl);
|
||||
|
||||
var queryResult = await _query.GetEffectiveStatusAsync(
|
||||
_tenantId,
|
||||
vulnerabilityId,
|
||||
purl,
|
||||
cancellationToken);
|
||||
|
||||
if (queryResult is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = MapToObservationResult(queryResult);
|
||||
CacheResult(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<VexStatementInfo>> GetStatementsAsync(
|
||||
string vulnerabilityId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var statements = await _query.GetStatementsAsync(
|
||||
_tenantId,
|
||||
vulnerabilityId,
|
||||
purl,
|
||||
cancellationToken);
|
||||
|
||||
return statements
|
||||
.Select(s => new VexStatementInfo
|
||||
{
|
||||
StatementId = s.StatementId,
|
||||
IssuerId = s.IssuerId,
|
||||
Status = s.Status,
|
||||
Timestamp = s.Timestamp,
|
||||
TrustWeight = s.TrustWeight,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PrefetchAsync(
|
||||
IReadOnlyList<VexLookupKey> keys,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (keys.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate and find keys not in cache
|
||||
var uncachedKeys = keys
|
||||
.DistinctBy(k => BuildCacheKey(k.VulnerabilityId, k.Purl))
|
||||
.Where(k => !_cache.TryGetValue(BuildCacheKey(k.VulnerabilityId, k.Purl), out _))
|
||||
.Select(k => new VexQueryKey(k.VulnerabilityId, k.Purl))
|
||||
.ToList();
|
||||
|
||||
if (uncachedKeys.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("Prefetch: all {Count} keys already cached", keys.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Prefetch: fetching {UncachedCount} of {TotalCount} keys",
|
||||
uncachedKeys.Count,
|
||||
keys.Count);
|
||||
|
||||
await _prefetchLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// Double-check after acquiring lock
|
||||
uncachedKeys = uncachedKeys
|
||||
.Where(k => !_cache.TryGetValue(BuildCacheKey(k.VulnerabilityId, k.ProductId), out _))
|
||||
.ToList();
|
||||
|
||||
if (uncachedKeys.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var batchResults = await _query.BatchLookupAsync(
|
||||
_tenantId,
|
||||
uncachedKeys,
|
||||
cancellationToken);
|
||||
|
||||
foreach (var (key, result) in batchResults)
|
||||
{
|
||||
var cacheKey = BuildCacheKey(key.VulnerabilityId, key.ProductId);
|
||||
var observationResult = MapToObservationResult(result);
|
||||
CacheResult(cacheKey, observationResult);
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Prefetch: cached {ResultCount} results",
|
||||
batchResults.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_prefetchLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets cache statistics.
|
||||
/// </summary>
|
||||
public CacheStatistics GetStatistics() => new()
|
||||
{
|
||||
CurrentEntryCount = _cache.Count,
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
_prefetchLock.Dispose();
|
||||
}
|
||||
|
||||
private static string BuildCacheKey(string vulnerabilityId, string productId) =>
|
||||
string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
"vex:{0}:{1}",
|
||||
vulnerabilityId.ToUpperInvariant(),
|
||||
productId.ToLowerInvariant());
|
||||
|
||||
private static VexObservationResult MapToObservationResult(VexObservationQueryResult queryResult) =>
|
||||
new()
|
||||
{
|
||||
Status = queryResult.Status,
|
||||
Justification = queryResult.Justification,
|
||||
Confidence = queryResult.Confidence,
|
||||
BackportHints = queryResult.BackportHints,
|
||||
};
|
||||
|
||||
private void CacheResult(string cacheKey, VexObservationResult result)
|
||||
{
|
||||
var options = new MemoryCacheEntryOptions
|
||||
{
|
||||
Size = 1,
|
||||
SlidingExpiration = _cacheTtl,
|
||||
AbsoluteExpirationRelativeToNow = _cacheTtl * 2,
|
||||
};
|
||||
|
||||
_cache.Set(cacheKey, result, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache statistics for monitoring.
|
||||
/// </summary>
|
||||
public sealed record CacheStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Current number of entries in cache.
|
||||
/// </summary>
|
||||
public int CurrentEntryCount { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IVexGateService.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: Interface for VEX gate evaluation service.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Service for evaluating findings against VEX evidence and policy rules.
|
||||
/// Determines whether findings should pass, warn, or block before triage.
|
||||
/// </summary>
|
||||
public interface IVexGateService
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates a single finding against VEX evidence and policy rules.
|
||||
/// </summary>
|
||||
/// <param name="finding">Finding to evaluate.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Gate evaluation result.</returns>
|
||||
Task<VexGateResult> EvaluateAsync(
|
||||
VexGateFinding finding,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates multiple findings in batch for efficiency.
|
||||
/// </summary>
|
||||
/// <param name="findings">Findings to evaluate.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Gate evaluation results for each finding.</returns>
|
||||
Task<ImmutableArray<GatedFinding>> EvaluateBatchAsync(
|
||||
IReadOnlyList<VexGateFinding> findings,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for pluggable VEX gate policy evaluation.
|
||||
/// </summary>
|
||||
public interface IVexGatePolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current policy configuration.
|
||||
/// </summary>
|
||||
VexGatePolicy Policy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates evidence against policy rules and returns the decision.
|
||||
/// </summary>
|
||||
/// <param name="evidence">Evidence to evaluate.</param>
|
||||
/// <returns>Tuple of (decision, matched rule ID, rationale).</returns>
|
||||
(VexGateDecision Decision, string RuleId, string Rationale) Evaluate(VexGateEvidence evidence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input finding for VEX gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record VexGateFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the finding.
|
||||
/// </summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE or vulnerability identifier.
|
||||
/// </summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL of the affected component.
|
||||
/// </summary>
|
||||
public required string Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Image digest containing the component.
|
||||
/// </summary>
|
||||
public required string ImageDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Severity level from the advisory.
|
||||
/// </summary>
|
||||
public string? SeverityLevel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether reachability has been analyzed.
|
||||
/// </summary>
|
||||
public bool? IsReachable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether compensating controls are in place.
|
||||
/// </summary>
|
||||
public bool? HasCompensatingControl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vulnerability is known to be exploitable.
|
||||
/// </summary>
|
||||
public bool? IsExploitable { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finding with gate evaluation result.
|
||||
/// </summary>
|
||||
public sealed record GatedFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference to the original finding.
|
||||
/// </summary>
|
||||
public required VexGateFinding Finding { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate evaluation result.
|
||||
/// </summary>
|
||||
public required VexGateResult GateResult { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IVexObservationQuery.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: Query interface for VEX observations used by gate service.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Query interface for VEX observations.
|
||||
/// Abstracts data access for gate service lookups.
|
||||
/// </summary>
|
||||
public interface IVexObservationQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Looks up the effective VEX status for a vulnerability/product combination.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Tenant identifier.</param>
|
||||
/// <param name="vulnerabilityId">CVE or vulnerability ID.</param>
|
||||
/// <param name="productId">PURL or product identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>VEX observation result or null if not found.</returns>
|
||||
Task<VexObservationQueryResult?> GetEffectiveStatusAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all VEX statements for a vulnerability/product combination.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Tenant identifier.</param>
|
||||
/// <param name="vulnerabilityId">CVE or vulnerability ID.</param>
|
||||
/// <param name="productId">PURL or product identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of VEX statement information.</returns>
|
||||
Task<IReadOnlyList<VexStatementQueryResult>> GetStatementsAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Performs batch lookup of VEX statuses for multiple vulnerability/product pairs.
|
||||
/// More efficient than individual lookups for gate evaluation.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Tenant identifier.</param>
|
||||
/// <param name="queries">List of vulnerability/product pairs to look up.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Dictionary mapping query keys to results.</returns>
|
||||
Task<IReadOnlyDictionary<VexQueryKey, VexObservationQueryResult>> BatchLookupAsync(
|
||||
string tenantId,
|
||||
IReadOnlyList<VexQueryKey> queries,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key for VEX query lookups.
|
||||
/// </summary>
|
||||
public sealed record VexQueryKey(string VulnerabilityId, string ProductId)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a normalized key for consistent lookup.
|
||||
/// </summary>
|
||||
public string ToNormalizedKey() =>
|
||||
string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
"{0}|{1}",
|
||||
VulnerabilityId.ToUpperInvariant(),
|
||||
ProductId.ToLowerInvariant());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from VEX observation query.
|
||||
/// </summary>
|
||||
public sealed record VexObservationQueryResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Effective VEX status.
|
||||
/// </summary>
|
||||
public required VexStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification if status is NotAffected.
|
||||
/// </summary>
|
||||
public VexJustification? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score for this status (0.0 to 1.0).
|
||||
/// </summary>
|
||||
public double Confidence { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Backport hints if status is Fixed.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> BackportHints { get; init; } = ImmutableArray<string>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Source of the statement (vendor name or issuer).
|
||||
/// </summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the effective status was last updated.
|
||||
/// </summary>
|
||||
public DateTimeOffset LastUpdated { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual VEX statement query result.
|
||||
/// </summary>
|
||||
public sealed record VexStatementQueryResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Statement identifier.
|
||||
/// </summary>
|
||||
public required string StatementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Issuer of the statement.
|
||||
/// </summary>
|
||||
public required string IssuerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX status in the statement.
|
||||
/// </summary>
|
||||
public required VexStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification if status is NotAffected.
|
||||
/// </summary>
|
||||
public VexJustification? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the statement was issued.
|
||||
/// </summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust weight for this statement.
|
||||
/// </summary>
|
||||
public double TrustWeight { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Source URL for the statement.
|
||||
/// </summary>
|
||||
public string? SourceUrl { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>StellaOps.Scanner.Gate</RootNamespace>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,305 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateAuditLogger.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Task: T023
|
||||
// Description: Audit logging for VEX gate decisions (compliance requirement).
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for audit logging VEX gate decisions.
|
||||
/// </summary>
|
||||
public interface IVexGateAuditLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs a gate evaluation event.
|
||||
/// </summary>
|
||||
void LogEvaluation(VexGateAuditEntry entry);
|
||||
|
||||
/// <summary>
|
||||
/// Logs a batch gate evaluation summary.
|
||||
/// </summary>
|
||||
void LogBatchSummary(VexGateBatchAuditEntry entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit entry for a single gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record VexGateAuditEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique audit entry ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("auditId")]
|
||||
public required string AuditId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan job ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scanId")]
|
||||
public required string ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenantId")]
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Finding ID that was evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("findingId")]
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability ID (CVE).
|
||||
/// </summary>
|
||||
[JsonPropertyName("vulnerabilityId")]
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL of the affected component.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public string? Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate decision made.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decision")]
|
||||
public required VexGateDecision Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy rule that matched.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyRuleMatched")]
|
||||
public required string PolicyRuleMatched { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyVersion")]
|
||||
public string? PolicyVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rationale for the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rationale")]
|
||||
public required string Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence that contributed to the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidence")]
|
||||
public VexGateEvidenceSummary? Evidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of VEX statements consulted.
|
||||
/// </summary>
|
||||
[JsonPropertyName("statementCount")]
|
||||
public int StatementCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score of the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidenceScore")]
|
||||
public double ConfidenceScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the evaluation was performed (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("evaluatedAt")]
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source IP or identifier of the requester (for compliance).
|
||||
/// </summary>
|
||||
[JsonPropertyName("sourceContext")]
|
||||
public string? SourceContext { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summarized evidence for audit logging.
|
||||
/// </summary>
|
||||
public sealed record VexGateEvidenceSummary
|
||||
{
|
||||
[JsonPropertyName("vendorStatus")]
|
||||
public string? VendorStatus { get; init; }
|
||||
|
||||
[JsonPropertyName("isReachable")]
|
||||
public bool IsReachable { get; init; }
|
||||
|
||||
[JsonPropertyName("isExploitable")]
|
||||
public bool IsExploitable { get; init; }
|
||||
|
||||
[JsonPropertyName("hasCompensatingControl")]
|
||||
public bool HasCompensatingControl { get; init; }
|
||||
|
||||
[JsonPropertyName("severityLevel")]
|
||||
public string? SeverityLevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit entry for a batch gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record VexGateBatchAuditEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique audit entry ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("auditId")]
|
||||
public required string AuditId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan job ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scanId")]
|
||||
public required string ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenantId")]
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total findings evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalFindings")]
|
||||
public int TotalFindings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number that passed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("passedCount")]
|
||||
public int PassedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number with warnings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("warnedCount")]
|
||||
public int WarnedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number blocked.
|
||||
/// </summary>
|
||||
[JsonPropertyName("blockedCount")]
|
||||
public int BlockedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyVersion")]
|
||||
public string? PolicyVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether gate was bypassed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("bypassed")]
|
||||
public bool Bypassed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluation duration in milliseconds.
|
||||
/// </summary>
|
||||
[JsonPropertyName("durationMs")]
|
||||
public double DurationMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the batch evaluation was performed (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("evaluatedAt")]
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source context for compliance.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sourceContext")]
|
||||
public string? SourceContext { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation using structured logging.
|
||||
/// </summary>
|
||||
public sealed class VexGateAuditLogger : IVexGateAuditLogger
|
||||
{
|
||||
private readonly ILogger<VexGateAuditLogger> _logger;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public VexGateAuditLogger(ILogger<VexGateAuditLogger> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LogEvaluation(VexGateAuditEntry entry)
|
||||
{
|
||||
// Log as structured event for compliance systems to consume
|
||||
_logger.LogInformation(
|
||||
"VEX_GATE_AUDIT: {AuditId} | Scan={ScanId} | Finding={FindingId} | CVE={VulnerabilityId} | " +
|
||||
"Decision={Decision} | Rule={PolicyRuleMatched} | Confidence={ConfidenceScore:F2} | " +
|
||||
"Evidence=[Reachable={IsReachable}, Exploitable={IsExploitable}]",
|
||||
entry.AuditId,
|
||||
entry.ScanId,
|
||||
entry.FindingId,
|
||||
entry.VulnerabilityId,
|
||||
entry.Decision,
|
||||
entry.PolicyRuleMatched,
|
||||
entry.ConfidenceScore,
|
||||
entry.Evidence?.IsReachable ?? false,
|
||||
entry.Evidence?.IsExploitable ?? false);
|
||||
|
||||
// Also log full JSON for audit trail
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(entry, JsonOptions);
|
||||
_logger.LogDebug("VEX_GATE_AUDIT_DETAIL: {AuditJson}", json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LogBatchSummary(VexGateBatchAuditEntry entry)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"VEX_GATE_BATCH_AUDIT: {AuditId} | Scan={ScanId} | Total={TotalFindings} | " +
|
||||
"Passed={PassedCount} | Warned={WarnedCount} | Blocked={BlockedCount} | " +
|
||||
"Bypassed={Bypassed} | Duration={DurationMs}ms",
|
||||
entry.AuditId,
|
||||
entry.ScanId,
|
||||
entry.TotalFindings,
|
||||
entry.PassedCount,
|
||||
entry.WarnedCount,
|
||||
entry.BlockedCount,
|
||||
entry.Bypassed,
|
||||
entry.DurationMs);
|
||||
|
||||
// Full JSON for audit trail
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(entry, JsonOptions);
|
||||
_logger.LogDebug("VEX_GATE_BATCH_AUDIT_DETAIL: {AuditJson}", json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No-op audit logger for testing or when auditing is disabled.
|
||||
/// </summary>
|
||||
public sealed class NullVexGateAuditLogger : IVexGateAuditLogger
|
||||
{
|
||||
public static readonly NullVexGateAuditLogger Instance = new();
|
||||
|
||||
private NullVexGateAuditLogger() { }
|
||||
|
||||
public void LogEvaluation(VexGateAuditEntry entry) { }
|
||||
public void LogBatchSummary(VexGateBatchAuditEntry entry) { }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateDecision.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: VEX gate decision enum for pre-triage filtering.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Decision outcome from VEX gate evaluation.
|
||||
/// Determines whether a finding proceeds to triage and with what flags.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<VexGateDecision>))]
|
||||
public enum VexGateDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Finding cleared by VEX evidence - no action needed.
|
||||
/// Typically when vendor status is NotAffected with sufficient trust.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("pass")]
|
||||
Pass,
|
||||
|
||||
/// <summary>
|
||||
/// Finding has partial evidence - proceed with caution.
|
||||
/// Used when evidence is inconclusive or conditions partially met.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("warn")]
|
||||
Warn,
|
||||
|
||||
/// <summary>
|
||||
/// Finding requires immediate attention - exploitable and reachable.
|
||||
/// Highest priority for triage queue.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("block")]
|
||||
Block
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateExcititorAdapter.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: Adapter bridging VexGateService with Excititor VEX statements.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Adapter that implements <see cref="IVexObservationQuery"/> by querying Excititor.
|
||||
/// This is a reference implementation that can be used when Excititor is available.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The actual Excititor integration requires a project reference to Excititor.Persistence.
|
||||
/// This adapter provides the contract and can be implemented in a separate assembly
|
||||
/// that has access to both Scanner.Gate and Excititor.Persistence.
|
||||
/// </remarks>
|
||||
public sealed class VexGateExcititorAdapter : IVexObservationQuery
|
||||
{
|
||||
private readonly IVexStatementDataSource _dataSource;
|
||||
private readonly ILogger<VexGateExcititorAdapter> _logger;
|
||||
|
||||
public VexGateExcititorAdapter(
|
||||
IVexStatementDataSource dataSource,
|
||||
ILogger<VexGateExcititorAdapter> logger)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VexObservationQueryResult?> GetEffectiveStatusAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Looking up effective VEX status: tenant={TenantId}, vuln={VulnerabilityId}, product={ProductId}",
|
||||
tenantId, vulnerabilityId, productId);
|
||||
|
||||
var statement = await _dataSource.GetEffectiveStatementAsync(
|
||||
tenantId,
|
||||
vulnerabilityId,
|
||||
productId,
|
||||
cancellationToken);
|
||||
|
||||
if (statement is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VexObservationQueryResult
|
||||
{
|
||||
Status = MapStatus(statement.Status),
|
||||
Justification = MapJustification(statement.Justification),
|
||||
Confidence = statement.TrustWeight,
|
||||
BackportHints = statement.BackportHints,
|
||||
Source = statement.Source,
|
||||
LastUpdated = statement.LastUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<VexStatementQueryResult>> GetStatementsAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var statements = await _dataSource.GetStatementsAsync(
|
||||
tenantId,
|
||||
vulnerabilityId,
|
||||
productId,
|
||||
cancellationToken);
|
||||
|
||||
return statements
|
||||
.Select(s => new VexStatementQueryResult
|
||||
{
|
||||
StatementId = s.StatementId,
|
||||
IssuerId = s.IssuerId,
|
||||
Status = MapStatus(s.Status),
|
||||
Justification = MapJustification(s.Justification),
|
||||
Timestamp = s.Timestamp,
|
||||
TrustWeight = s.TrustWeight,
|
||||
SourceUrl = s.SourceUrl,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyDictionary<VexQueryKey, VexObservationQueryResult>> BatchLookupAsync(
|
||||
string tenantId,
|
||||
IReadOnlyList<VexQueryKey> queries,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (queries.Count == 0)
|
||||
{
|
||||
return ImmutableDictionary<VexQueryKey, VexObservationQueryResult>.Empty;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Batch lookup of {Count} VEX queries for tenant {TenantId}",
|
||||
queries.Count, tenantId);
|
||||
|
||||
var results = new Dictionary<VexQueryKey, VexObservationQueryResult>();
|
||||
|
||||
// Use batch lookup if data source supports it
|
||||
if (_dataSource is IVexStatementBatchDataSource batchSource)
|
||||
{
|
||||
var batchKeys = queries
|
||||
.Select(q => new VexBatchKey(q.VulnerabilityId, q.ProductId))
|
||||
.ToList();
|
||||
|
||||
var batchResults = await batchSource.BatchLookupAsync(
|
||||
tenantId,
|
||||
batchKeys,
|
||||
cancellationToken);
|
||||
|
||||
foreach (var (key, statement) in batchResults)
|
||||
{
|
||||
var queryKey = new VexQueryKey(key.VulnerabilityId, key.ProductId);
|
||||
results[queryKey] = new VexObservationQueryResult
|
||||
{
|
||||
Status = MapStatus(statement.Status),
|
||||
Justification = MapJustification(statement.Justification),
|
||||
Confidence = statement.TrustWeight,
|
||||
BackportHints = statement.BackportHints,
|
||||
Source = statement.Source,
|
||||
LastUpdated = statement.LastUpdated,
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to individual lookups
|
||||
foreach (var query in queries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var result = await GetEffectiveStatusAsync(
|
||||
tenantId,
|
||||
query.VulnerabilityId,
|
||||
query.ProductId,
|
||||
cancellationToken);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
results[query] = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static VexStatus MapStatus(VexStatementStatus status) => status switch
|
||||
{
|
||||
VexStatementStatus.NotAffected => VexStatus.NotAffected,
|
||||
VexStatementStatus.Affected => VexStatus.Affected,
|
||||
VexStatementStatus.Fixed => VexStatus.Fixed,
|
||||
VexStatementStatus.UnderInvestigation => VexStatus.UnderInvestigation,
|
||||
_ => VexStatus.UnderInvestigation,
|
||||
};
|
||||
|
||||
private static VexJustification? MapJustification(VexStatementJustification? justification) =>
|
||||
justification switch
|
||||
{
|
||||
VexStatementJustification.ComponentNotPresent => VexJustification.ComponentNotPresent,
|
||||
VexStatementJustification.VulnerableCodeNotPresent => VexJustification.VulnerableCodeNotPresent,
|
||||
VexStatementJustification.VulnerableCodeNotInExecutePath => VexJustification.VulnerableCodeNotInExecutePath,
|
||||
VexStatementJustification.VulnerableCodeCannotBeControlledByAdversary => VexJustification.VulnerableCodeCannotBeControlledByAdversary,
|
||||
VexStatementJustification.InlineMitigationsAlreadyExist => VexJustification.InlineMitigationsAlreadyExist,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data source abstraction for VEX statements.
|
||||
/// Implemented by Excititor persistence layer.
|
||||
/// </summary>
|
||||
public interface IVexStatementDataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the effective VEX statement for a vulnerability/product combination.
|
||||
/// </summary>
|
||||
Task<VexStatementData?> GetEffectiveStatementAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all VEX statements for a vulnerability/product combination.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexStatementData>> GetStatementsAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended interface for batch data source operations.
|
||||
/// </summary>
|
||||
public interface IVexStatementBatchDataSource : IVexStatementDataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs batch lookup of VEX statements.
|
||||
/// </summary>
|
||||
Task<IReadOnlyDictionary<VexBatchKey, VexStatementData>> BatchLookupAsync(
|
||||
string tenantId,
|
||||
IReadOnlyList<VexBatchKey> keys,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key for batch VEX lookups.
|
||||
/// </summary>
|
||||
public sealed record VexBatchKey(string VulnerabilityId, string ProductId);
|
||||
|
||||
/// <summary>
|
||||
/// VEX statement data transfer object.
|
||||
/// </summary>
|
||||
public sealed record VexStatementData
|
||||
{
|
||||
public required string StatementId { get; init; }
|
||||
public required string IssuerId { get; init; }
|
||||
public required VexStatementStatus Status { get; init; }
|
||||
public VexStatementJustification? Justification { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public DateTimeOffset LastUpdated { get; init; }
|
||||
public double TrustWeight { get; init; } = 1.0;
|
||||
public string? Source { get; init; }
|
||||
public string? SourceUrl { get; init; }
|
||||
public ImmutableArray<string> BackportHints { get; init; } = ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX statement status (mirrors Excititor's VexStatus).
|
||||
/// </summary>
|
||||
public enum VexStatementStatus
|
||||
{
|
||||
NotAffected,
|
||||
Affected,
|
||||
Fixed,
|
||||
UnderInvestigation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX statement justification (mirrors Excititor's VexJustification).
|
||||
/// </summary>
|
||||
public enum VexStatementJustification
|
||||
{
|
||||
ComponentNotPresent,
|
||||
VulnerableCodeNotPresent,
|
||||
VulnerableCodeNotInExecutePath,
|
||||
VulnerableCodeCannotBeControlledByAdversary,
|
||||
InlineMitigationsAlreadyExist
|
||||
}
|
||||
379
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateOptions.cs
Normal file
379
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateOptions.cs
Normal file
@@ -0,0 +1,379 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateOptions.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Task: T028 - Add gate policy to tenant configuration
|
||||
// Description: Configuration options for VEX gate, bindable from YAML/JSON config.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for VEX gate service.
|
||||
/// Binds to "VexGate" section in configuration files.
|
||||
/// </summary>
|
||||
public sealed class VexGateOptions : IValidatableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name.
|
||||
/// </summary>
|
||||
public const string SectionName = "VexGate";
|
||||
|
||||
/// <summary>
|
||||
/// Enable VEX-first gating. Default: false.
|
||||
/// When disabled, all findings pass through to triage unchanged.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Default decision when no rules match. Default: Warn.
|
||||
/// </summary>
|
||||
public string DefaultDecision { get; set; } = "Warn";
|
||||
|
||||
/// <summary>
|
||||
/// Policy version for audit/replay purposes.
|
||||
/// Should be incremented when rules change.
|
||||
/// </summary>
|
||||
public string PolicyVersion { get; set; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Evaluation rules (ordered by priority, highest first).
|
||||
/// </summary>
|
||||
public List<VexGateRuleOptions> Rules { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Caching settings for VEX observation lookups.
|
||||
/// </summary>
|
||||
public VexGateCacheOptions Cache { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Audit logging settings.
|
||||
/// </summary>
|
||||
public VexGateAuditOptions Audit { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Metrics settings.
|
||||
/// </summary>
|
||||
public VexGateMetricsOptions Metrics { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Bypass settings for emergency scans.
|
||||
/// </summary>
|
||||
public VexGateBypassOptions Bypass { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Converts this options instance to a VexGatePolicy.
|
||||
/// </summary>
|
||||
public VexGatePolicy ToPolicy()
|
||||
{
|
||||
var defaultDecision = ParseDecision(DefaultDecision);
|
||||
var rules = Rules
|
||||
.Select(r => r.ToRule())
|
||||
.OrderByDescending(r => r.Priority)
|
||||
.ToImmutableArray();
|
||||
|
||||
return new VexGatePolicy
|
||||
{
|
||||
DefaultDecision = defaultDecision,
|
||||
Rules = rules,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates options from a VexGatePolicy.
|
||||
/// </summary>
|
||||
public static VexGateOptions FromPolicy(VexGatePolicy policy)
|
||||
{
|
||||
return new VexGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultDecision = policy.DefaultDecision.ToString(),
|
||||
Rules = policy.Rules.Select(r => VexGateRuleOptions.FromRule(r)).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
private static VexGateDecision ParseDecision(string value)
|
||||
{
|
||||
return value.ToUpperInvariant() switch
|
||||
{
|
||||
"PASS" => VexGateDecision.Pass,
|
||||
"WARN" => VexGateDecision.Warn,
|
||||
"BLOCK" => VexGateDecision.Block,
|
||||
_ => VexGateDecision.Warn,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (Enabled && Rules.Count == 0)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"At least one rule is required when VexGate is enabled",
|
||||
[nameof(Rules)]);
|
||||
}
|
||||
|
||||
var ruleIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var rule in Rules)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rule.RuleId))
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Rule ID is required for all rules",
|
||||
[nameof(Rules)]);
|
||||
}
|
||||
else if (!ruleIds.Add(rule.RuleId))
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
$"Duplicate rule ID: {rule.RuleId}",
|
||||
[nameof(Rules)]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Cache.TtlSeconds <= 0)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Cache TTL must be positive",
|
||||
[nameof(Cache)]);
|
||||
}
|
||||
|
||||
if (Cache.MaxEntries <= 0)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Cache max entries must be positive",
|
||||
[nameof(Cache)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for a single VEX gate rule.
|
||||
/// </summary>
|
||||
public sealed class VexGateRuleOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this rule.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string RuleId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Priority order (higher values evaluated first).
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Decision to apply when this rule matches.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Decision { get; set; } = "Warn";
|
||||
|
||||
/// <summary>
|
||||
/// Condition that must match for this rule to apply.
|
||||
/// </summary>
|
||||
public VexGateConditionOptions Condition { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Converts to a VexGatePolicyRule.
|
||||
/// </summary>
|
||||
public VexGatePolicyRule ToRule()
|
||||
{
|
||||
return new VexGatePolicyRule
|
||||
{
|
||||
RuleId = RuleId,
|
||||
Priority = Priority,
|
||||
Decision = ParseDecision(Decision),
|
||||
Condition = Condition.ToCondition(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates options from a VexGatePolicyRule.
|
||||
/// </summary>
|
||||
public static VexGateRuleOptions FromRule(VexGatePolicyRule rule)
|
||||
{
|
||||
return new VexGateRuleOptions
|
||||
{
|
||||
RuleId = rule.RuleId,
|
||||
Priority = rule.Priority,
|
||||
Decision = rule.Decision.ToString(),
|
||||
Condition = VexGateConditionOptions.FromCondition(rule.Condition),
|
||||
};
|
||||
}
|
||||
|
||||
private static VexGateDecision ParseDecision(string value)
|
||||
{
|
||||
return value.ToUpperInvariant() switch
|
||||
{
|
||||
"PASS" => VexGateDecision.Pass,
|
||||
"WARN" => VexGateDecision.Warn,
|
||||
"BLOCK" => VexGateDecision.Block,
|
||||
_ => VexGateDecision.Warn,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for a rule condition.
|
||||
/// </summary>
|
||||
public sealed class VexGateConditionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Required VEX vendor status.
|
||||
/// Options: not_affected, fixed, affected, under_investigation.
|
||||
/// </summary>
|
||||
public string? VendorStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vulnerability must be exploitable.
|
||||
/// </summary>
|
||||
public bool? IsExploitable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vulnerable code must be reachable.
|
||||
/// </summary>
|
||||
public bool? IsReachable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether compensating controls must be present.
|
||||
/// </summary>
|
||||
public bool? HasCompensatingControl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the CVE is in KEV (Known Exploited Vulnerabilities).
|
||||
/// </summary>
|
||||
public bool? IsKnownExploited { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Required severity levels (any match).
|
||||
/// </summary>
|
||||
public List<string>? SeverityLevels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence score required.
|
||||
/// </summary>
|
||||
public double? ConfidenceThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts to a VexGatePolicyCondition.
|
||||
/// </summary>
|
||||
public VexGatePolicyCondition ToCondition()
|
||||
{
|
||||
return new VexGatePolicyCondition
|
||||
{
|
||||
VendorStatus = ParseVexStatus(VendorStatus),
|
||||
IsExploitable = IsExploitable,
|
||||
IsReachable = IsReachable,
|
||||
HasCompensatingControl = HasCompensatingControl,
|
||||
SeverityLevels = SeverityLevels?.ToArray(),
|
||||
MinConfidence = ConfidenceThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates options from a VexGatePolicyCondition.
|
||||
/// </summary>
|
||||
public static VexGateConditionOptions FromCondition(VexGatePolicyCondition condition)
|
||||
{
|
||||
return new VexGateConditionOptions
|
||||
{
|
||||
VendorStatus = condition.VendorStatus?.ToString().ToLowerInvariant(),
|
||||
IsExploitable = condition.IsExploitable,
|
||||
IsReachable = condition.IsReachable,
|
||||
HasCompensatingControl = condition.HasCompensatingControl,
|
||||
SeverityLevels = condition.SeverityLevels?.ToList(),
|
||||
ConfidenceThreshold = condition.MinConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
private static VexStatus? ParseVexStatus(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
|
||||
return value.ToLowerInvariant() switch
|
||||
{
|
||||
"not_affected" or "notaffected" => VexStatus.NotAffected,
|
||||
"fixed" => VexStatus.Fixed,
|
||||
"affected" => VexStatus.Affected,
|
||||
"under_investigation" or "underinvestigation" => VexStatus.UnderInvestigation,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache configuration options.
|
||||
/// </summary>
|
||||
public sealed class VexGateCacheOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// TTL for cached VEX observations (seconds). Default: 300.
|
||||
/// </summary>
|
||||
public int TtlSeconds { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum cache entries. Default: 10000.
|
||||
/// </summary>
|
||||
public int MaxEntries { get; set; } = 10000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit logging configuration options.
|
||||
/// </summary>
|
||||
public sealed class VexGateAuditOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable structured audit logging for compliance. Default: true.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include full evidence in audit logs. Default: true.
|
||||
/// </summary>
|
||||
public bool IncludeEvidence { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Log level for gate decisions. Default: Information.
|
||||
/// </summary>
|
||||
public string LogLevel { get; set; } = "Information";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metrics configuration options.
|
||||
/// </summary>
|
||||
public sealed class VexGateMetricsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable OpenTelemetry metrics. Default: true.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Histogram buckets for evaluation latency (milliseconds).
|
||||
/// </summary>
|
||||
public List<double> LatencyBuckets { get; set; } = [1, 5, 10, 25, 50, 100, 250];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bypass configuration options.
|
||||
/// </summary>
|
||||
public sealed class VexGateBypassOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Allow gate bypass via CLI flag (--bypass-gate). Default: true.
|
||||
/// </summary>
|
||||
public bool AllowCliBypass { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Require specific reason when bypassing. Default: false.
|
||||
/// </summary>
|
||||
public bool RequireReason { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Emit warning when bypass is used. Default: true.
|
||||
/// </summary>
|
||||
public bool WarnOnBypass { get; set; } = true;
|
||||
}
|
||||
201
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGatePolicy.cs
Normal file
201
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGatePolicy.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGatePolicy.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: VEX gate policy configuration models.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// VEX gate policy defining rules for gate decisions.
|
||||
/// Rules are evaluated in priority order (highest first).
|
||||
/// </summary>
|
||||
public sealed record VexGatePolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Ordered list of policy rules.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rules")]
|
||||
public required ImmutableArray<VexGatePolicyRule> Rules { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Default decision when no rules match.
|
||||
/// </summary>
|
||||
[JsonPropertyName("defaultDecision")]
|
||||
public required VexGateDecision DefaultDecision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the default gate policy per product advisory.
|
||||
/// </summary>
|
||||
public static VexGatePolicy Default => new()
|
||||
{
|
||||
DefaultDecision = VexGateDecision.Warn,
|
||||
Rules = ImmutableArray.Create(
|
||||
new VexGatePolicyRule
|
||||
{
|
||||
RuleId = "block-exploitable-reachable",
|
||||
Priority = 100,
|
||||
Condition = new VexGatePolicyCondition
|
||||
{
|
||||
IsExploitable = true,
|
||||
IsReachable = true,
|
||||
HasCompensatingControl = false,
|
||||
},
|
||||
Decision = VexGateDecision.Block,
|
||||
},
|
||||
new VexGatePolicyRule
|
||||
{
|
||||
RuleId = "warn-high-not-reachable",
|
||||
Priority = 90,
|
||||
Condition = new VexGatePolicyCondition
|
||||
{
|
||||
SeverityLevels = ["critical", "high"],
|
||||
IsReachable = false,
|
||||
},
|
||||
Decision = VexGateDecision.Warn,
|
||||
},
|
||||
new VexGatePolicyRule
|
||||
{
|
||||
RuleId = "pass-vendor-not-affected",
|
||||
Priority = 80,
|
||||
Condition = new VexGatePolicyCondition
|
||||
{
|
||||
VendorStatus = VexStatus.NotAffected,
|
||||
},
|
||||
Decision = VexGateDecision.Pass,
|
||||
},
|
||||
new VexGatePolicyRule
|
||||
{
|
||||
RuleId = "pass-backport-confirmed",
|
||||
Priority = 70,
|
||||
Condition = new VexGatePolicyCondition
|
||||
{
|
||||
VendorStatus = VexStatus.Fixed,
|
||||
},
|
||||
Decision = VexGateDecision.Pass,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single policy rule for VEX gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record VexGatePolicyRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this rule.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ruleId")]
|
||||
public required string RuleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Condition that must match for this rule to apply.
|
||||
/// </summary>
|
||||
[JsonPropertyName("condition")]
|
||||
public required VexGatePolicyCondition Condition { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Decision to apply when this rule matches.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decision")]
|
||||
public required VexGateDecision Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Priority order (higher values evaluated first).
|
||||
/// </summary>
|
||||
[JsonPropertyName("priority")]
|
||||
public required int Priority { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Condition for a policy rule to match.
|
||||
/// All non-null properties must match for the condition to be satisfied.
|
||||
/// </summary>
|
||||
public sealed record VexGatePolicyCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// Required VEX vendor status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vendorStatus")]
|
||||
public VexStatus? VendorStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vulnerability must be exploitable.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isExploitable")]
|
||||
public bool? IsExploitable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vulnerable code must be reachable.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isReachable")]
|
||||
public bool? IsReachable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether compensating controls must be present.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hasCompensatingControl")]
|
||||
public bool? HasCompensatingControl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Required severity levels (any match).
|
||||
/// </summary>
|
||||
[JsonPropertyName("severityLevels")]
|
||||
public string[]? SeverityLevels { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence score required.
|
||||
/// </summary>
|
||||
[JsonPropertyName("minConfidence")]
|
||||
public double? MinConfidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Required VEX justification type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("justification")]
|
||||
public VexJustification? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates whether the evidence matches this condition.
|
||||
/// </summary>
|
||||
/// <param name="evidence">Evidence to evaluate.</param>
|
||||
/// <returns>True if all specified conditions match.</returns>
|
||||
public bool Matches(VexGateEvidence evidence)
|
||||
{
|
||||
if (VendorStatus is not null && evidence.VendorStatus != VendorStatus)
|
||||
return false;
|
||||
|
||||
if (IsExploitable is not null && evidence.IsExploitable != IsExploitable)
|
||||
return false;
|
||||
|
||||
if (IsReachable is not null && evidence.IsReachable != IsReachable)
|
||||
return false;
|
||||
|
||||
if (HasCompensatingControl is not null && evidence.HasCompensatingControl != HasCompensatingControl)
|
||||
return false;
|
||||
|
||||
if (SeverityLevels is not null && SeverityLevels.Length > 0)
|
||||
{
|
||||
if (evidence.SeverityLevel is null)
|
||||
return false;
|
||||
|
||||
var matchesSeverity = SeverityLevels.Any(s =>
|
||||
string.Equals(s, evidence.SeverityLevel, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!matchesSeverity)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MinConfidence is not null && evidence.ConfidenceScore < MinConfidence)
|
||||
return false;
|
||||
|
||||
if (Justification is not null && evidence.Justification != Justification)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGatePolicyEvaluator.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: Policy evaluator for VEX gate decisions.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IVexGatePolicy"/>.
|
||||
/// Evaluates evidence against policy rules in priority order.
|
||||
/// </summary>
|
||||
public sealed class VexGatePolicyEvaluator : IVexGatePolicy
|
||||
{
|
||||
private readonly ILogger<VexGatePolicyEvaluator> _logger;
|
||||
private readonly VexGatePolicy _policy;
|
||||
|
||||
public VexGatePolicyEvaluator(
|
||||
IOptions<VexGatePolicyOptions> options,
|
||||
ILogger<VexGatePolicyEvaluator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_policy = options.Value.Policy ?? VexGatePolicy.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an evaluator with the default policy.
|
||||
/// </summary>
|
||||
public VexGatePolicyEvaluator(ILogger<VexGatePolicyEvaluator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_policy = VexGatePolicy.Default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public VexGatePolicy Policy => _policy;
|
||||
|
||||
/// <inheritdoc />
|
||||
public (VexGateDecision Decision, string RuleId, string Rationale) Evaluate(VexGateEvidence evidence)
|
||||
{
|
||||
// Sort rules by priority descending and evaluate in order
|
||||
var sortedRules = _policy.Rules
|
||||
.OrderByDescending(r => r.Priority)
|
||||
.ToList();
|
||||
|
||||
foreach (var rule in sortedRules)
|
||||
{
|
||||
if (rule.Condition.Matches(evidence))
|
||||
{
|
||||
var rationale = BuildRationale(rule, evidence);
|
||||
|
||||
_logger.LogDebug(
|
||||
"VEX gate rule matched: {RuleId} -> {Decision} for evidence with vendor status {VendorStatus}",
|
||||
rule.RuleId,
|
||||
rule.Decision,
|
||||
evidence.VendorStatus);
|
||||
|
||||
return (rule.Decision, rule.RuleId, rationale);
|
||||
}
|
||||
}
|
||||
|
||||
// No rule matched, return default
|
||||
var defaultRationale = "No policy rule matched; applying default decision";
|
||||
|
||||
_logger.LogDebug(
|
||||
"No VEX gate rule matched; defaulting to {Decision}",
|
||||
_policy.DefaultDecision);
|
||||
|
||||
return (_policy.DefaultDecision, "default", defaultRationale);
|
||||
}
|
||||
|
||||
private static string BuildRationale(VexGatePolicyRule rule, VexGateEvidence evidence)
|
||||
{
|
||||
return rule.RuleId switch
|
||||
{
|
||||
"block-exploitable-reachable" =>
|
||||
"Exploitable + reachable, no compensating control",
|
||||
|
||||
"warn-high-not-reachable" =>
|
||||
string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
"{0} severity but not reachable from entrypoints",
|
||||
evidence.SeverityLevel ?? "High"),
|
||||
|
||||
"pass-vendor-not-affected" =>
|
||||
"Vendor VEX statement declares not_affected",
|
||||
|
||||
"pass-backport-confirmed" =>
|
||||
"Vendor VEX statement confirms fixed via backport",
|
||||
|
||||
_ => string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
"Policy rule '{0}' matched",
|
||||
rule.RuleId)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for VEX gate policy configuration.
|
||||
/// </summary>
|
||||
public sealed class VexGatePolicyOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom policy to use instead of default.
|
||||
/// </summary>
|
||||
public VexGatePolicy? Policy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the gate is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
144
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateResult.cs
Normal file
144
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexGateResult.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateResult.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: VEX gate evaluation result with evidence.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Result of VEX gate evaluation for a single finding.
|
||||
/// Contains the decision, rationale, and supporting evidence.
|
||||
/// </summary>
|
||||
public sealed record VexGateResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate decision: Pass, Warn, or Block.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decision")]
|
||||
public required VexGateDecision Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable explanation of why this decision was made.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rationale")]
|
||||
public required string Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the policy rule that matched and produced this decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyRuleMatched")]
|
||||
public required string PolicyRuleMatched { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX statements that contributed to this decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("contributingStatements")]
|
||||
public required ImmutableArray<VexStatementRef> ContributingStatements { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed evidence supporting the decision.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidence")]
|
||||
public required VexGateEvidence Evidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this evaluation was performed (UTC ISO-8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("evaluatedAt")]
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence collected during VEX gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record VexGateEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// VEX status from vendor or authoritative source.
|
||||
/// Null if no VEX statement found.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vendorStatus")]
|
||||
public VexStatus? VendorStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification type from VEX statement.
|
||||
/// </summary>
|
||||
[JsonPropertyName("justification")]
|
||||
public VexJustification? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vulnerable code is reachable from entrypoints.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isReachable")]
|
||||
public bool IsReachable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether compensating controls mitigate the vulnerability.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hasCompensatingControl")]
|
||||
public bool HasCompensatingControl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score in the gate decision (0.0 to 1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidenceScore")]
|
||||
public double ConfidenceScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hints about backport fixes detected.
|
||||
/// </summary>
|
||||
[JsonPropertyName("backportHints")]
|
||||
public ImmutableArray<string> BackportHints { get; init; } = ImmutableArray<string>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vulnerability is exploitable based on available intelligence.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isExploitable")]
|
||||
public bool IsExploitable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Severity level from the advisory.
|
||||
/// </summary>
|
||||
[JsonPropertyName("severityLevel")]
|
||||
public string? SeverityLevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a VEX statement that contributed to a gate decision.
|
||||
/// </summary>
|
||||
public sealed record VexStatementRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the VEX statement.
|
||||
/// </summary>
|
||||
[JsonPropertyName("statementId")]
|
||||
public required string StatementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Issuer of the VEX statement.
|
||||
/// </summary>
|
||||
[JsonPropertyName("issuerId")]
|
||||
public required string IssuerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX status declared in the statement.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required VexStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the statement was issued.
|
||||
/// </summary>
|
||||
[JsonPropertyName("timestamp")]
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust weight of this statement in consensus (0.0 to 1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("trustWeight")]
|
||||
public double TrustWeight { get; init; }
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateServiceCollectionExtensions.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Task: T028 - Add gate policy to tenant configuration
|
||||
// Description: Service collection extensions for registering VEX gate services.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering VEX gate services.
|
||||
/// </summary>
|
||||
public static class VexGateServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds VEX gate services with configuration from the specified section.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration root.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddVexGate(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
// Bind and validate options
|
||||
services.AddOptions<VexGateOptions>()
|
||||
.Bind(configuration.GetSection(VexGateOptions.SectionName))
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
// Register policy from options
|
||||
services.AddSingleton<VexGatePolicy>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<VexGateOptions>>();
|
||||
if (!options.Value.Enabled)
|
||||
{
|
||||
// Return a permissive policy when disabled
|
||||
return new VexGatePolicy
|
||||
{
|
||||
DefaultDecision = VexGateDecision.Pass,
|
||||
Rules = [],
|
||||
};
|
||||
}
|
||||
|
||||
return options.Value.ToPolicy();
|
||||
});
|
||||
|
||||
// Register core services
|
||||
services.AddSingleton<IVexGatePolicy, VexGatePolicyEvaluator>();
|
||||
|
||||
// Register caching with configured limits
|
||||
services.AddSingleton<IMemoryCache>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<VexGateOptions>>();
|
||||
return new MemoryCache(new MemoryCacheOptions
|
||||
{
|
||||
SizeLimit = options.Value.Cache.MaxEntries,
|
||||
});
|
||||
});
|
||||
|
||||
// Register VEX gate service
|
||||
services.AddSingleton<IVexGateService, VexGateService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds VEX gate services with explicit options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configureOptions">The options configuration action.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddVexGate(
|
||||
this IServiceCollection services,
|
||||
Action<VexGateOptions> configureOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configureOptions);
|
||||
|
||||
// Configure and validate options
|
||||
services.AddOptions<VexGateOptions>()
|
||||
.Configure(configureOptions)
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
// Register policy from options
|
||||
services.AddSingleton<VexGatePolicy>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<VexGateOptions>>();
|
||||
if (!options.Value.Enabled)
|
||||
{
|
||||
return new VexGatePolicy
|
||||
{
|
||||
DefaultDecision = VexGateDecision.Pass,
|
||||
Rules = [],
|
||||
};
|
||||
}
|
||||
|
||||
return options.Value.ToPolicy();
|
||||
});
|
||||
|
||||
// Register core services
|
||||
services.AddSingleton<IVexGatePolicy, VexGatePolicyEvaluator>();
|
||||
|
||||
// Register caching with configured limits
|
||||
services.AddSingleton<IMemoryCache>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<VexGateOptions>>();
|
||||
return new MemoryCache(new MemoryCacheOptions
|
||||
{
|
||||
SizeLimit = options.Value.Cache.MaxEntries,
|
||||
});
|
||||
});
|
||||
|
||||
// Register VEX gate service
|
||||
services.AddSingleton<IVexGateService, VexGateService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds VEX gate services with default policy.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddVexGateWithDefaultPolicy(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
// Configure with default options
|
||||
services.AddOptions<VexGateOptions>()
|
||||
.Configure(options =>
|
||||
{
|
||||
options.Enabled = true;
|
||||
var defaultPolicy = VexGatePolicy.Default;
|
||||
options.DefaultDecision = defaultPolicy.DefaultDecision.ToString();
|
||||
options.Rules = defaultPolicy.Rules
|
||||
.Select(VexGateRuleOptions.FromRule)
|
||||
.ToList();
|
||||
})
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
// Register default policy
|
||||
services.AddSingleton<VexGatePolicy>(_ => VexGatePolicy.Default);
|
||||
|
||||
// Register core services
|
||||
services.AddSingleton<IVexGatePolicy, VexGatePolicyEvaluator>();
|
||||
|
||||
// Register caching with default limits
|
||||
services.AddSingleton<IMemoryCache>(_ => new MemoryCache(new MemoryCacheOptions
|
||||
{
|
||||
SizeLimit = 10000,
|
||||
}));
|
||||
|
||||
// Register VEX gate service
|
||||
services.AddSingleton<IVexGateService, VexGateService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
78
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexTypes.cs
Normal file
78
src/Scanner/__Libraries/StellaOps.Scanner.Gate/VexTypes.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexTypes.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: Local VEX type definitions for gate service independence.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Gate;
|
||||
|
||||
/// <summary>
|
||||
/// VEX status values per OpenVEX specification.
|
||||
/// Local definition to avoid dependency on SmartDiff/Excititor.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<VexStatus>))]
|
||||
public enum VexStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The vulnerability is not exploitable in this context.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("not_affected")]
|
||||
NotAffected,
|
||||
|
||||
/// <summary>
|
||||
/// The vulnerability is exploitable.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("affected")]
|
||||
Affected,
|
||||
|
||||
/// <summary>
|
||||
/// The vulnerability has been fixed.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("fixed")]
|
||||
Fixed,
|
||||
|
||||
/// <summary>
|
||||
/// The vulnerability is under investigation.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("under_investigation")]
|
||||
UnderInvestigation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX justification codes per OpenVEX specification.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<VexJustification>))]
|
||||
public enum VexJustification
|
||||
{
|
||||
/// <summary>
|
||||
/// The vulnerable component is not present.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("component_not_present")]
|
||||
ComponentNotPresent,
|
||||
|
||||
/// <summary>
|
||||
/// The vulnerable code is not present.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("vulnerable_code_not_present")]
|
||||
VulnerableCodeNotPresent,
|
||||
|
||||
/// <summary>
|
||||
/// The vulnerable code is not in the execute path.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("vulnerable_code_not_in_execute_path")]
|
||||
VulnerableCodeNotInExecutePath,
|
||||
|
||||
/// <summary>
|
||||
/// The vulnerable code cannot be controlled by an adversary.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("vulnerable_code_cannot_be_controlled_by_adversary")]
|
||||
VulnerableCodeCannotBeControlledByAdversary,
|
||||
|
||||
/// <summary>
|
||||
/// Inline mitigations already exist.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("inline_mitigations_already_exist")]
|
||||
InlineMitigationsAlreadyExist
|
||||
}
|
||||
Reference in New Issue
Block a user