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
}