using System.Collections.Immutable; using System.Security.Cryptography; using System.Text; using System.Text.Json.Serialization; namespace StellaOps.Policy.Engine.Events; /// /// Type of policy effective event. /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum PolicyEffectiveEventType { /// Policy decision changed for a subject. [JsonPropertyName("policy.effective.updated")] EffectiveUpdated, /// Policy decision added for new subject. [JsonPropertyName("policy.effective.added")] EffectiveAdded, /// Policy decision removed (subject no longer affected). [JsonPropertyName("policy.effective.removed")] EffectiveRemoved, /// Batch re-evaluation completed. [JsonPropertyName("policy.effective.batch_completed")] BatchCompleted } /// /// Base class for policy effective events. /// public abstract record PolicyEffectiveEvent( [property: JsonPropertyName("event_id")] string EventId, [property: JsonPropertyName("event_type")] PolicyEffectiveEventType EventType, [property: JsonPropertyName("tenant_id")] string TenantId, [property: JsonPropertyName("timestamp")] DateTimeOffset Timestamp, [property: JsonPropertyName("correlation_id")] string? CorrelationId); /// /// Event emitted when a policy decision is updated for a subject. /// public sealed record PolicyEffectiveUpdatedEvent( string EventId, string TenantId, DateTimeOffset Timestamp, string? CorrelationId, [property: JsonPropertyName("pack_id")] string PackId, [property: JsonPropertyName("pack_version")] int PackVersion, [property: JsonPropertyName("subject_purl")] string SubjectPurl, [property: JsonPropertyName("advisory_id")] string AdvisoryId, [property: JsonPropertyName("trigger_type")] string TriggerType, [property: JsonPropertyName("diff")] PolicyDecisionDiff Diff) : PolicyEffectiveEvent(EventId, PolicyEffectiveEventType.EffectiveUpdated, TenantId, Timestamp, CorrelationId); /// /// Diff metadata for policy decision changes. /// public sealed record PolicyDecisionDiff( [property: JsonPropertyName("old_status")] string? OldStatus, [property: JsonPropertyName("new_status")] string NewStatus, [property: JsonPropertyName("old_severity")] string? OldSeverity, [property: JsonPropertyName("new_severity")] string? NewSeverity, [property: JsonPropertyName("old_rule")] string? OldRule, [property: JsonPropertyName("new_rule")] string? NewRule, [property: JsonPropertyName("old_priority")] int? OldPriority, [property: JsonPropertyName("new_priority")] int? NewPriority, [property: JsonPropertyName("status_changed")] bool StatusChanged, [property: JsonPropertyName("severity_changed")] bool SeverityChanged, [property: JsonPropertyName("rule_changed")] bool RuleChanged, [property: JsonPropertyName("annotations_added")] ImmutableArray AnnotationsAdded, [property: JsonPropertyName("annotations_removed")] ImmutableArray AnnotationsRemoved) { /// /// Creates a diff between two policy decisions. /// public static PolicyDecisionDiff Create( string? oldStatus, string newStatus, string? oldSeverity, string? newSeverity, string? oldRule, string? newRule, int? oldPriority, int? newPriority, ImmutableDictionary? oldAnnotations, ImmutableDictionary? newAnnotations) { var oldKeys = oldAnnotations?.Keys ?? Enumerable.Empty(); var newKeys = newAnnotations?.Keys ?? Enumerable.Empty(); var annotationsAdded = newKeys .Where(k => oldAnnotations?.ContainsKey(k) != true) .OrderBy(k => k) .ToImmutableArray(); var annotationsRemoved = oldKeys .Where(k => newAnnotations?.ContainsKey(k) != true) .OrderBy(k => k) .ToImmutableArray(); return new PolicyDecisionDiff( OldStatus: oldStatus, NewStatus: newStatus, OldSeverity: oldSeverity, NewSeverity: newSeverity, OldRule: oldRule, NewRule: newRule, OldPriority: oldPriority, NewPriority: newPriority, StatusChanged: !string.Equals(oldStatus, newStatus, StringComparison.Ordinal), SeverityChanged: !string.Equals(oldSeverity, newSeverity, StringComparison.Ordinal), RuleChanged: !string.Equals(oldRule, newRule, StringComparison.Ordinal), AnnotationsAdded: annotationsAdded, AnnotationsRemoved: annotationsRemoved); } } /// /// Event emitted when batch re-evaluation completes. /// public sealed record PolicyBatchCompletedEvent( string EventId, string TenantId, DateTimeOffset Timestamp, string? CorrelationId, [property: JsonPropertyName("batch_id")] string BatchId, [property: JsonPropertyName("trigger_type")] string TriggerType, [property: JsonPropertyName("subjects_evaluated")] int SubjectsEvaluated, [property: JsonPropertyName("decisions_changed")] int DecisionsChanged, [property: JsonPropertyName("duration_ms")] long DurationMs, [property: JsonPropertyName("summary")] PolicyBatchSummary Summary) : PolicyEffectiveEvent(EventId, PolicyEffectiveEventType.BatchCompleted, TenantId, Timestamp, CorrelationId); /// /// Summary of changes in a batch re-evaluation. /// public sealed record PolicyBatchSummary( [property: JsonPropertyName("status_upgrades")] int StatusUpgrades, [property: JsonPropertyName("status_downgrades")] int StatusDowngrades, [property: JsonPropertyName("new_blocks")] int NewBlocks, [property: JsonPropertyName("blocks_removed")] int BlocksRemoved, [property: JsonPropertyName("affected_advisories")] ImmutableArray AffectedAdvisories, [property: JsonPropertyName("affected_purls")] ImmutableArray AffectedPurls); /// /// Request to schedule a re-evaluation job. /// public sealed record ReEvaluationJobRequest( string JobId, string TenantId, string PackId, int PackVersion, string TriggerType, string? CorrelationId, DateTimeOffset CreatedAt, PolicyChangePriority Priority, ImmutableArray AdvisoryIds, ImmutableArray SubjectPurls, ImmutableArray SbomIds, ImmutableDictionary Metadata) { /// /// Creates a deterministic job ID. /// public static string CreateJobId( string tenantId, string packId, int packVersion, string triggerType, DateTimeOffset createdAt) { var seed = $"{tenantId}|{packId}|{packVersion}|{triggerType}|{createdAt:O}"; var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(seed)); return $"rej-{Convert.ToHexStringLower(bytes)[..16]}"; } } /// /// Policy change priority from IncrementalOrchestrator namespace. /// public enum PolicyChangePriority { Normal = 0, High = 1, Emergency = 2 }