new advisories work and features gaps work

This commit is contained in:
master
2026-01-14 18:39:19 +02:00
parent 95d5898650
commit 15aeac8e8b
148 changed files with 16731 additions and 554 deletions

View File

@@ -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>

View File

@@ -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]}";
}
}