new advisories work and features gaps work
This commit is contained in:
@@ -108,6 +108,28 @@ public static class VexTimelineEventTypes
|
||||
/// An attestation was verified.
|
||||
/// </summary>
|
||||
public const string AttestationVerified = "vex.attestation.verified";
|
||||
|
||||
// Sprint: SPRINT_20260112_006_EXCITITOR_vex_change_events (EXC-VEX-001)
|
||||
|
||||
/// <summary>
|
||||
/// A VEX statement was added.
|
||||
/// </summary>
|
||||
public const string StatementAdded = "vex.statement.added";
|
||||
|
||||
/// <summary>
|
||||
/// A VEX statement was superseded by a newer statement.
|
||||
/// </summary>
|
||||
public const string StatementSuperseded = "vex.statement.superseded";
|
||||
|
||||
/// <summary>
|
||||
/// A VEX statement conflict was detected (multiple conflicting statuses).
|
||||
/// </summary>
|
||||
public const string StatementConflict = "vex.statement.conflict";
|
||||
|
||||
/// <summary>
|
||||
/// VEX status changed for a CVE+product combination.
|
||||
/// </summary>
|
||||
public const string StatusChanged = "vex.status.changed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// <copyright file="VexStatementChangeEvent.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_006_EXCITITOR_vex_change_events (EXC-VEX-001)
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Excititor.Core.Observations;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when a VEX statement changes (added, superseded, or conflict detected).
|
||||
/// Used to drive policy reanalysis.
|
||||
/// </summary>
|
||||
public sealed record VexStatementChangeEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique event identifier (deterministic based on content).
|
||||
/// </summary>
|
||||
public required string EventId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event type from <see cref="VexTimelineEventTypes"/>.
|
||||
/// </summary>
|
||||
public required string EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
public required string Tenant { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE identifier affected by this change.
|
||||
/// </summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Product key (PURL or product identifier) affected by this change.
|
||||
/// </summary>
|
||||
public required string ProductKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New VEX status after this change (e.g., "affected", "not_affected", "under_investigation").
|
||||
/// </summary>
|
||||
public required string NewStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous VEX status before this change (null for new statements).
|
||||
/// </summary>
|
||||
public string? PreviousStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider that issued this statement.
|
||||
/// </summary>
|
||||
public required string ProviderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Observation ID of the statement.
|
||||
/// </summary>
|
||||
public required string ObservationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Statement ID that supersedes the current one (if applicable).
|
||||
/// </summary>
|
||||
public string? SupersededBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Statement IDs that this statement supersedes.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> Supersedes { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Provenance metadata about the statement source.
|
||||
/// </summary>
|
||||
public VexStatementProvenance? Provenance { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Conflict details if this is a conflict event.
|
||||
/// </summary>
|
||||
public VexConflictDetails? ConflictDetails { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when this event occurred.
|
||||
/// </summary>
|
||||
public required DateTimeOffset OccurredAtUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
public string? TraceId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provenance metadata for a VEX statement change.
|
||||
/// </summary>
|
||||
public sealed record VexStatementProvenance
|
||||
{
|
||||
/// <summary>
|
||||
/// Source document hash (e.g., OpenVEX document digest).
|
||||
/// </summary>
|
||||
public string? DocumentHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source document URI.
|
||||
/// </summary>
|
||||
public string? DocumentUri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp from the source document.
|
||||
/// </summary>
|
||||
public DateTimeOffset? SourceTimestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Author of the statement.
|
||||
/// </summary>
|
||||
public string? Author { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust score assigned to this provider (0.0-1.0).
|
||||
/// </summary>
|
||||
public double? TrustScore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details about a VEX statement conflict.
|
||||
/// </summary>
|
||||
public sealed record VexConflictDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of conflict (status_mismatch, trust_tie, supersession_conflict).
|
||||
/// </summary>
|
||||
public required string ConflictType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Conflicting statuses from different providers.
|
||||
/// </summary>
|
||||
public required ImmutableArray<VexConflictingStatus> ConflictingStatuses { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution strategy applied (if any).
|
||||
/// </summary>
|
||||
public string? ResolutionStrategy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the conflict was auto-resolved by policy.
|
||||
/// </summary>
|
||||
public bool AutoResolved { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A conflicting status from a specific provider.
|
||||
/// </summary>
|
||||
public sealed record VexConflictingStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Provider that issued this status.
|
||||
/// </summary>
|
||||
public required string ProviderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The status value.
|
||||
/// </summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for the status.
|
||||
/// </summary>
|
||||
public string? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Trust score of this provider.
|
||||
/// </summary>
|
||||
public double? TrustScore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating deterministic VEX statement change events.
|
||||
/// </summary>
|
||||
public static class VexStatementChangeEventFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a statement added event with a deterministic event ID.
|
||||
/// </summary>
|
||||
public static VexStatementChangeEvent CreateStatementAdded(
|
||||
string tenant,
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
string status,
|
||||
string providerId,
|
||||
string observationId,
|
||||
DateTimeOffset occurredAtUtc,
|
||||
VexStatementProvenance? provenance = null,
|
||||
string? traceId = null)
|
||||
{
|
||||
// Deterministic event ID based on content
|
||||
var eventId = ComputeEventId(
|
||||
VexTimelineEventTypes.StatementAdded,
|
||||
tenant,
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
observationId,
|
||||
occurredAtUtc);
|
||||
|
||||
return new VexStatementChangeEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
EventType = VexTimelineEventTypes.StatementAdded,
|
||||
Tenant = tenant,
|
||||
VulnerabilityId = vulnerabilityId,
|
||||
ProductKey = productKey,
|
||||
NewStatus = status,
|
||||
PreviousStatus = null,
|
||||
ProviderId = providerId,
|
||||
ObservationId = observationId,
|
||||
Provenance = provenance,
|
||||
OccurredAtUtc = occurredAtUtc,
|
||||
TraceId = traceId
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a statement superseded event with a deterministic event ID.
|
||||
/// </summary>
|
||||
public static VexStatementChangeEvent CreateStatementSuperseded(
|
||||
string tenant,
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
string newStatus,
|
||||
string? previousStatus,
|
||||
string providerId,
|
||||
string observationId,
|
||||
string supersededBy,
|
||||
DateTimeOffset occurredAtUtc,
|
||||
VexStatementProvenance? provenance = null,
|
||||
string? traceId = null)
|
||||
{
|
||||
var eventId = ComputeEventId(
|
||||
VexTimelineEventTypes.StatementSuperseded,
|
||||
tenant,
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
observationId,
|
||||
occurredAtUtc);
|
||||
|
||||
return new VexStatementChangeEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
EventType = VexTimelineEventTypes.StatementSuperseded,
|
||||
Tenant = tenant,
|
||||
VulnerabilityId = vulnerabilityId,
|
||||
ProductKey = productKey,
|
||||
NewStatus = newStatus,
|
||||
PreviousStatus = previousStatus,
|
||||
ProviderId = providerId,
|
||||
ObservationId = observationId,
|
||||
SupersededBy = supersededBy,
|
||||
Provenance = provenance,
|
||||
OccurredAtUtc = occurredAtUtc,
|
||||
TraceId = traceId
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a conflict detected event with a deterministic event ID.
|
||||
/// </summary>
|
||||
public static VexStatementChangeEvent CreateConflictDetected(
|
||||
string tenant,
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
string providerId,
|
||||
string observationId,
|
||||
VexConflictDetails conflictDetails,
|
||||
DateTimeOffset occurredAtUtc,
|
||||
string? traceId = null)
|
||||
{
|
||||
var eventId = ComputeEventId(
|
||||
VexTimelineEventTypes.StatementConflict,
|
||||
tenant,
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
observationId,
|
||||
occurredAtUtc);
|
||||
|
||||
return new VexStatementChangeEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
EventType = VexTimelineEventTypes.StatementConflict,
|
||||
Tenant = tenant,
|
||||
VulnerabilityId = vulnerabilityId,
|
||||
ProductKey = productKey,
|
||||
NewStatus = "conflict",
|
||||
ProviderId = providerId,
|
||||
ObservationId = observationId,
|
||||
ConflictDetails = conflictDetails,
|
||||
OccurredAtUtc = occurredAtUtc,
|
||||
TraceId = traceId
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeEventId(
|
||||
string eventType,
|
||||
string tenant,
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
string observationId,
|
||||
DateTimeOffset occurredAtUtc)
|
||||
{
|
||||
// Use SHA256 for deterministic event IDs
|
||||
var input = $"{eventType}|{tenant}|{vulnerabilityId}|{productKey}|{observationId}|{occurredAtUtc:O}";
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(
|
||||
System.Text.Encoding.UTF8.GetBytes(input));
|
||||
return $"evt-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user