Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/Events/PolicyEffectiveEventModels.cs
StellaOps Bot 05da719048
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
up
2025-11-28 09:41:08 +02:00

185 lines
7.2 KiB
C#

using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Engine.Events;
/// <summary>
/// Type of policy effective event.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PolicyEffectiveEventType>))]
public enum PolicyEffectiveEventType
{
/// <summary>Policy decision changed for a subject.</summary>
[JsonPropertyName("policy.effective.updated")]
EffectiveUpdated,
/// <summary>Policy decision added for new subject.</summary>
[JsonPropertyName("policy.effective.added")]
EffectiveAdded,
/// <summary>Policy decision removed (subject no longer affected).</summary>
[JsonPropertyName("policy.effective.removed")]
EffectiveRemoved,
/// <summary>Batch re-evaluation completed.</summary>
[JsonPropertyName("policy.effective.batch_completed")]
BatchCompleted
}
/// <summary>
/// Base class for policy effective events.
/// </summary>
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);
/// <summary>
/// Event emitted when a policy decision is updated for a subject.
/// </summary>
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);
/// <summary>
/// Diff metadata for policy decision changes.
/// </summary>
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<string> AnnotationsAdded,
[property: JsonPropertyName("annotations_removed")] ImmutableArray<string> AnnotationsRemoved)
{
/// <summary>
/// Creates a diff between two policy decisions.
/// </summary>
public static PolicyDecisionDiff Create(
string? oldStatus, string newStatus,
string? oldSeverity, string? newSeverity,
string? oldRule, string? newRule,
int? oldPriority, int? newPriority,
ImmutableDictionary<string, string>? oldAnnotations,
ImmutableDictionary<string, string>? newAnnotations)
{
var oldKeys = oldAnnotations?.Keys ?? Enumerable.Empty<string>();
var newKeys = newAnnotations?.Keys ?? Enumerable.Empty<string>();
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);
}
}
/// <summary>
/// Event emitted when batch re-evaluation completes.
/// </summary>
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>
/// Summary of changes in a batch re-evaluation.
/// </summary>
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<string> AffectedAdvisories,
[property: JsonPropertyName("affected_purls")] ImmutableArray<string> AffectedPurls);
/// <summary>
/// Request to schedule a re-evaluation job.
/// </summary>
public sealed record ReEvaluationJobRequest(
string JobId,
string TenantId,
string PackId,
int PackVersion,
string TriggerType,
string? CorrelationId,
DateTimeOffset CreatedAt,
PolicyChangePriority Priority,
ImmutableArray<string> AdvisoryIds,
ImmutableArray<string> SubjectPurls,
ImmutableArray<string> SbomIds,
ImmutableDictionary<string, string> Metadata)
{
/// <summary>
/// Creates a deterministic job ID.
/// </summary>
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]}";
}
}
/// <summary>
/// Policy change priority from IncrementalOrchestrator namespace.
/// </summary>
public enum PolicyChangePriority
{
Normal = 0,
High = 1,
Emergency = 2
}