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:
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user