Implement VEX document verification system with issuer management and signature verification

- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation.
- Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments.
- Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats.
- Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats.
- Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction.
- Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
This commit is contained in:
StellaOps Bot
2025-12-06 13:41:22 +02:00
parent 2141196496
commit 5e514532df
112 changed files with 24861 additions and 211 deletions

View File

@@ -0,0 +1,291 @@
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
namespace StellaOps.VexLens.Integration;
/// <summary>
/// Integration interface for Policy Engine consumption of VEX consensus.
/// </summary>
public interface IPolicyEngineIntegration
{
/// <summary>
/// Gets the VEX consensus status for a vulnerability-product pair for policy evaluation.
/// </summary>
Task<PolicyVexStatusResult> GetVexStatusForPolicyAsync(
string vulnerabilityId,
string productKey,
PolicyVexContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets VEX status for multiple vulnerability-product pairs in batch.
/// </summary>
Task<IReadOnlyList<PolicyVexStatusResult>> GetVexStatusBatchAsync(
IEnumerable<PolicyVexQuery> queries,
PolicyVexContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if a vulnerability is suppressed by VEX for a product.
/// </summary>
Task<VexSuppressionResult> CheckVexSuppressionAsync(
string vulnerabilityId,
string productKey,
PolicyVexContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets VEX-adjusted severity for policy scoring.
/// </summary>
Task<VexAdjustedSeverityResult> GetVexAdjustedSeverityAsync(
string vulnerabilityId,
string productKey,
double baseSeverity,
PolicyVexContext context,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Context for policy VEX queries.
/// </summary>
public sealed record PolicyVexContext(
string? TenantId,
string? PolicyId,
double MinimumConfidenceThreshold,
bool RequireSignedVex,
DateTimeOffset EvaluationTime);
/// <summary>
/// Query for policy VEX status.
/// </summary>
public sealed record PolicyVexQuery(
string VulnerabilityId,
string ProductKey);
/// <summary>
/// Result of VEX status for policy evaluation.
/// </summary>
public sealed record PolicyVexStatusResult(
string VulnerabilityId,
string ProductKey,
bool HasVexData,
VexStatus? Status,
VexJustification? Justification,
double? ConfidenceScore,
bool MeetsConfidenceThreshold,
string? ProjectionId,
PolicyVexEvidenceSummary? Evidence);
/// <summary>
/// Summary of VEX evidence for policy.
/// </summary>
public sealed record PolicyVexEvidenceSummary(
int StatementCount,
int IssuerCount,
int ConflictCount,
string? PrimaryIssuer,
DateTimeOffset? MostRecentStatement,
IReadOnlyList<string> IssuerNames);
/// <summary>
/// Result of VEX suppression check.
/// </summary>
public sealed record VexSuppressionResult(
string VulnerabilityId,
string ProductKey,
bool IsSuppressed,
VexSuppressionReason? Reason,
VexStatus? Status,
VexJustification? Justification,
double? ConfidenceScore,
string? SuppressedBy,
DateTimeOffset? SuppressedAt);
/// <summary>
/// Reason for VEX suppression.
/// </summary>
public enum VexSuppressionReason
{
/// <summary>
/// VEX indicates not_affected.
/// </summary>
NotAffected,
/// <summary>
/// VEX indicates fixed.
/// </summary>
Fixed,
/// <summary>
/// VEX provides justification for not_affected.
/// </summary>
JustifiedNotAffected
}
/// <summary>
/// Result of VEX-adjusted severity calculation.
/// </summary>
public sealed record VexAdjustedSeverityResult(
string VulnerabilityId,
string ProductKey,
double BaseSeverity,
double AdjustedSeverity,
double AdjustmentFactor,
VexStatus? VexStatus,
string? AdjustmentReason);
/// <summary>
/// Integration interface for Vuln Explorer consumption of VEX consensus.
/// </summary>
public interface IVulnExplorerIntegration
{
/// <summary>
/// Enriches a vulnerability with VEX consensus data.
/// </summary>
Task<VulnVexEnrichment> EnrichVulnerabilityAsync(
string vulnerabilityId,
string? productKey,
VulnVexContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets VEX timeline for a vulnerability.
/// </summary>
Task<VexTimelineResult> GetVexTimelineAsync(
string vulnerabilityId,
string productKey,
VulnVexContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets VEX summary statistics for a vulnerability.
/// </summary>
Task<VulnVexSummary> GetVexSummaryAsync(
string vulnerabilityId,
VulnVexContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Searches VEX data for vulnerabilities matching criteria.
/// </summary>
Task<VexSearchResult> SearchVexAsync(
VexSearchQuery query,
VulnVexContext context,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Context for Vuln Explorer VEX queries.
/// </summary>
public sealed record VulnVexContext(
string? TenantId,
bool IncludeRawStatements,
bool IncludeHistory,
int? HistoryLimit);
/// <summary>
/// VEX enrichment data for a vulnerability.
/// </summary>
public sealed record VulnVexEnrichment(
string VulnerabilityId,
bool HasVexData,
VexStatus? ConsensusStatus,
VexJustification? Justification,
double? ConfidenceScore,
int ProductCount,
IReadOnlyList<ProductVexStatus> ProductStatuses,
IReadOnlyList<VexIssuerSummary> Issuers,
DateTimeOffset? LastVexUpdate);
/// <summary>
/// VEX status for a specific product.
/// </summary>
public sealed record ProductVexStatus(
string ProductKey,
string? ProductName,
VexStatus Status,
VexJustification? Justification,
double ConfidenceScore,
string? PrimaryIssuer,
DateTimeOffset? ComputedAt);
/// <summary>
/// Summary of a VEX issuer.
/// </summary>
public sealed record VexIssuerSummary(
string IssuerId,
string Name,
string Category,
int StatementCount,
VexStatus? MostCommonStatus);
/// <summary>
/// VEX timeline for a vulnerability-product pair.
/// </summary>
public sealed record VexTimelineResult(
string VulnerabilityId,
string ProductKey,
IReadOnlyList<VexTimelineEntry> Entries,
VexStatus? CurrentStatus,
int StatusChangeCount);
/// <summary>
/// Entry in VEX timeline.
/// </summary>
public sealed record VexTimelineEntry(
DateTimeOffset Timestamp,
VexStatus Status,
VexJustification? Justification,
string? IssuerId,
string? IssuerName,
string EventType,
string? Notes);
/// <summary>
/// Summary of VEX data for a vulnerability.
/// </summary>
public sealed record VulnVexSummary(
string VulnerabilityId,
int TotalStatements,
int TotalProducts,
int TotalIssuers,
IReadOnlyDictionary<VexStatus, int> StatusCounts,
IReadOnlyDictionary<VexJustification, int> JustificationCounts,
double AverageConfidence,
DateTimeOffset? FirstVexStatement,
DateTimeOffset? LatestVexStatement);
/// <summary>
/// Query for searching VEX data.
/// </summary>
public sealed record VexSearchQuery(
string? VulnerabilityIdPattern,
string? ProductKeyPattern,
VexStatus? Status,
VexJustification? Justification,
string? IssuerId,
double? MinimumConfidence,
DateTimeOffset? UpdatedAfter,
int Limit,
int Offset);
/// <summary>
/// Result of VEX search.
/// </summary>
public sealed record VexSearchResult(
IReadOnlyList<VexSearchHit> Hits,
int TotalCount,
int Offset,
int Limit);
/// <summary>
/// Search hit for VEX data.
/// </summary>
public sealed record VexSearchHit(
string VulnerabilityId,
string ProductKey,
VexStatus Status,
VexJustification? Justification,
double ConfidenceScore,
string? PrimaryIssuer,
DateTimeOffset ComputedAt);

View File

@@ -0,0 +1,427 @@
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Storage;
namespace StellaOps.VexLens.Integration;
/// <summary>
/// Default implementation of <see cref="IPolicyEngineIntegration"/>.
/// </summary>
public sealed class PolicyEngineIntegration : IPolicyEngineIntegration
{
private readonly IConsensusProjectionStore _projectionStore;
public PolicyEngineIntegration(IConsensusProjectionStore projectionStore)
{
_projectionStore = projectionStore;
}
public async Task<PolicyVexStatusResult> GetVexStatusForPolicyAsync(
string vulnerabilityId,
string productKey,
PolicyVexContext context,
CancellationToken cancellationToken = default)
{
var projection = await _projectionStore.GetLatestAsync(
vulnerabilityId,
productKey,
context.TenantId,
cancellationToken);
if (projection == null)
{
return new PolicyVexStatusResult(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
HasVexData: false,
Status: null,
Justification: null,
ConfidenceScore: null,
MeetsConfidenceThreshold: false,
ProjectionId: null,
Evidence: null);
}
var meetsThreshold = projection.ConfidenceScore >= context.MinimumConfidenceThreshold;
return new PolicyVexStatusResult(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
HasVexData: true,
Status: projection.Status,
Justification: projection.Justification,
ConfidenceScore: projection.ConfidenceScore,
MeetsConfidenceThreshold: meetsThreshold,
ProjectionId: projection.ProjectionId,
Evidence: new PolicyVexEvidenceSummary(
StatementCount: projection.StatementCount,
IssuerCount: 1, // Simplified; would need full projection data
ConflictCount: projection.ConflictCount,
PrimaryIssuer: null,
MostRecentStatement: projection.ComputedAt,
IssuerNames: []));
}
public async Task<IReadOnlyList<PolicyVexStatusResult>> GetVexStatusBatchAsync(
IEnumerable<PolicyVexQuery> queries,
PolicyVexContext context,
CancellationToken cancellationToken = default)
{
var results = new List<PolicyVexStatusResult>();
foreach (var query in queries)
{
cancellationToken.ThrowIfCancellationRequested();
var result = await GetVexStatusForPolicyAsync(
query.VulnerabilityId,
query.ProductKey,
context,
cancellationToken);
results.Add(result);
}
return results;
}
public async Task<VexSuppressionResult> CheckVexSuppressionAsync(
string vulnerabilityId,
string productKey,
PolicyVexContext context,
CancellationToken cancellationToken = default)
{
var statusResult = await GetVexStatusForPolicyAsync(
vulnerabilityId,
productKey,
context,
cancellationToken);
if (!statusResult.HasVexData || !statusResult.MeetsConfidenceThreshold)
{
return new VexSuppressionResult(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
IsSuppressed: false,
Reason: null,
Status: statusResult.Status,
Justification: statusResult.Justification,
ConfidenceScore: statusResult.ConfidenceScore,
SuppressedBy: null,
SuppressedAt: null);
}
var isSuppressed = statusResult.Status == VexStatus.NotAffected ||
statusResult.Status == VexStatus.Fixed;
VexSuppressionReason? reason = null;
if (isSuppressed)
{
reason = statusResult.Status switch
{
VexStatus.NotAffected when statusResult.Justification.HasValue =>
VexSuppressionReason.JustifiedNotAffected,
VexStatus.NotAffected => VexSuppressionReason.NotAffected,
VexStatus.Fixed => VexSuppressionReason.Fixed,
_ => null
};
}
return new VexSuppressionResult(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
IsSuppressed: isSuppressed,
Reason: reason,
Status: statusResult.Status,
Justification: statusResult.Justification,
ConfidenceScore: statusResult.ConfidenceScore,
SuppressedBy: statusResult.Evidence?.PrimaryIssuer,
SuppressedAt: statusResult.Evidence?.MostRecentStatement);
}
public async Task<VexAdjustedSeverityResult> GetVexAdjustedSeverityAsync(
string vulnerabilityId,
string productKey,
double baseSeverity,
PolicyVexContext context,
CancellationToken cancellationToken = default)
{
var statusResult = await GetVexStatusForPolicyAsync(
vulnerabilityId,
productKey,
context,
cancellationToken);
if (!statusResult.HasVexData || !statusResult.MeetsConfidenceThreshold)
{
return new VexAdjustedSeverityResult(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
BaseSeverity: baseSeverity,
AdjustedSeverity: baseSeverity,
AdjustmentFactor: 1.0,
VexStatus: statusResult.Status,
AdjustmentReason: "No qualifying VEX data");
}
var (adjustmentFactor, reason) = statusResult.Status switch
{
VexStatus.NotAffected => (0.0, "VEX indicates not affected"),
VexStatus.Fixed => (0.0, "VEX indicates fixed"),
VexStatus.Affected => (1.0, "VEX confirms affected"),
VexStatus.UnderInvestigation => (0.8, "VEX indicates under investigation"),
_ => (1.0, "Unknown VEX status")
};
// Apply confidence scaling
var confidenceScale = statusResult.ConfidenceScore ?? 0.5;
if (adjustmentFactor < 1.0)
{
// For suppression, blend toward base severity based on confidence
adjustmentFactor = adjustmentFactor + (1.0 - adjustmentFactor) * (1.0 - confidenceScale);
}
var adjustedSeverity = baseSeverity * adjustmentFactor;
return new VexAdjustedSeverityResult(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
BaseSeverity: baseSeverity,
AdjustedSeverity: adjustedSeverity,
AdjustmentFactor: adjustmentFactor,
VexStatus: statusResult.Status,
AdjustmentReason: $"{reason} (confidence: {confidenceScale:P0})");
}
}
/// <summary>
/// Default implementation of <see cref="IVulnExplorerIntegration"/>.
/// </summary>
public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
{
private readonly IConsensusProjectionStore _projectionStore;
public VulnExplorerIntegration(IConsensusProjectionStore projectionStore)
{
_projectionStore = projectionStore;
}
public async Task<VulnVexEnrichment> EnrichVulnerabilityAsync(
string vulnerabilityId,
string? productKey,
VulnVexContext context,
CancellationToken cancellationToken = default)
{
var query = new ProjectionQuery(
TenantId: context.TenantId,
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
Status: null,
Outcome: null,
MinimumConfidence: null,
ComputedAfter: null,
ComputedBefore: null,
StatusChanged: null,
Limit: 100,
Offset: 0,
SortBy: ProjectionSortField.ComputedAt,
SortDescending: true);
var result = await _projectionStore.ListAsync(query, cancellationToken);
if (result.Projections.Count == 0)
{
return new VulnVexEnrichment(
VulnerabilityId: vulnerabilityId,
HasVexData: false,
ConsensusStatus: null,
Justification: null,
ConfidenceScore: null,
ProductCount: 0,
ProductStatuses: [],
Issuers: [],
LastVexUpdate: null);
}
var productStatuses = result.Projections
.GroupBy(p => p.ProductKey)
.Select(g => g.First())
.Select(p => new ProductVexStatus(
ProductKey: p.ProductKey,
ProductName: null,
Status: p.Status,
Justification: p.Justification,
ConfidenceScore: p.ConfidenceScore,
PrimaryIssuer: null,
ComputedAt: p.ComputedAt))
.ToList();
// Determine overall consensus (most common status)
var statusCounts = productStatuses
.GroupBy(p => p.Status)
.ToDictionary(g => g.Key, g => g.Count());
var consensusStatus = statusCounts
.OrderByDescending(kv => kv.Value)
.First().Key;
var avgConfidence = productStatuses.Average(p => p.ConfidenceScore);
var lastUpdate = productStatuses.Max(p => p.ComputedAt);
return new VulnVexEnrichment(
VulnerabilityId: vulnerabilityId,
HasVexData: true,
ConsensusStatus: consensusStatus,
Justification: null,
ConfidenceScore: avgConfidence,
ProductCount: productStatuses.Count,
ProductStatuses: productStatuses,
Issuers: [],
LastVexUpdate: lastUpdate);
}
public async Task<VexTimelineResult> GetVexTimelineAsync(
string vulnerabilityId,
string productKey,
VulnVexContext context,
CancellationToken cancellationToken = default)
{
var history = await _projectionStore.GetHistoryAsync(
vulnerabilityId,
productKey,
context.TenantId,
context.HistoryLimit,
cancellationToken);
var entries = new List<VexTimelineEntry>();
VexStatus? previousStatus = null;
foreach (var projection in history.OrderBy(p => p.ComputedAt))
{
var eventType = previousStatus == null
? "initial"
: projection.Status != previousStatus
? "status_change"
: "update";
entries.Add(new VexTimelineEntry(
Timestamp: projection.ComputedAt,
Status: projection.Status,
Justification: projection.Justification,
IssuerId: null,
IssuerName: null,
EventType: eventType,
Notes: projection.RationaleSummary));
previousStatus = projection.Status;
}
var statusChangeCount = entries.Count(e => e.EventType == "status_change");
return new VexTimelineResult(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
Entries: entries,
CurrentStatus: history.FirstOrDefault()?.Status,
StatusChangeCount: statusChangeCount);
}
public async Task<VulnVexSummary> GetVexSummaryAsync(
string vulnerabilityId,
VulnVexContext context,
CancellationToken cancellationToken = default)
{
var query = new ProjectionQuery(
TenantId: context.TenantId,
VulnerabilityId: vulnerabilityId,
ProductKey: null,
Status: null,
Outcome: null,
MinimumConfidence: null,
ComputedAfter: null,
ComputedBefore: null,
StatusChanged: null,
Limit: 1000,
Offset: 0,
SortBy: ProjectionSortField.ComputedAt,
SortDescending: true);
var result = await _projectionStore.ListAsync(query, cancellationToken);
if (result.Projections.Count == 0)
{
return new VulnVexSummary(
VulnerabilityId: vulnerabilityId,
TotalStatements: 0,
TotalProducts: 0,
TotalIssuers: 0,
StatusCounts: new Dictionary<VexStatus, int>(),
JustificationCounts: new Dictionary<VexJustification, int>(),
AverageConfidence: 0,
FirstVexStatement: null,
LatestVexStatement: null);
}
var statusCounts = result.Projections
.GroupBy(p => p.Status)
.ToDictionary(g => g.Key, g => g.Count());
var justificationCounts = result.Projections
.Where(p => p.Justification.HasValue)
.GroupBy(p => p.Justification!.Value)
.ToDictionary(g => g.Key, g => g.Count());
var totalStatements = result.Projections.Sum(p => p.StatementCount);
var products = result.Projections.Select(p => p.ProductKey).Distinct().Count();
var avgConfidence = result.Projections.Average(p => p.ConfidenceScore);
var first = result.Projections.Min(p => p.ComputedAt);
var latest = result.Projections.Max(p => p.ComputedAt);
return new VulnVexSummary(
VulnerabilityId: vulnerabilityId,
TotalStatements: totalStatements,
TotalProducts: products,
TotalIssuers: 0, // Would need to track in projections
StatusCounts: statusCounts,
JustificationCounts: justificationCounts,
AverageConfidence: avgConfidence,
FirstVexStatement: first,
LatestVexStatement: latest);
}
public async Task<VexSearchResult> SearchVexAsync(
VexSearchQuery searchQuery,
VulnVexContext context,
CancellationToken cancellationToken = default)
{
var query = new ProjectionQuery(
TenantId: context.TenantId,
VulnerabilityId: searchQuery.VulnerabilityIdPattern,
ProductKey: searchQuery.ProductKeyPattern,
Status: searchQuery.Status,
Outcome: null,
MinimumConfidence: searchQuery.MinimumConfidence,
ComputedAfter: searchQuery.UpdatedAfter,
ComputedBefore: null,
StatusChanged: null,
Limit: searchQuery.Limit,
Offset: searchQuery.Offset,
SortBy: ProjectionSortField.ComputedAt,
SortDescending: true);
var result = await _projectionStore.ListAsync(query, cancellationToken);
var hits = result.Projections.Select(p => new VexSearchHit(
VulnerabilityId: p.VulnerabilityId,
ProductKey: p.ProductKey,
Status: p.Status,
Justification: p.Justification,
ConfidenceScore: p.ConfidenceScore,
PrimaryIssuer: null,
ComputedAt: p.ComputedAt)).ToList();
return new VexSearchResult(
Hits: hits,
TotalCount: result.TotalCount,
Offset: result.Offset,
Limit: result.Limit);
}
}