up
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
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
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Engine.IncrementalOrchestrator;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.Engine.Telemetry;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for publishing policy effective events.
|
||||
/// </summary>
|
||||
public interface IPolicyEffectiveEventPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes a policy effective updated event.
|
||||
/// </summary>
|
||||
Task PublishEffectiveUpdatedAsync(PolicyEffectiveUpdatedEvent evt, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a batch completed event.
|
||||
/// </summary>
|
||||
Task PublishBatchCompletedAsync(PolicyBatchCompletedEvent evt, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a handler for effective events.
|
||||
/// </summary>
|
||||
void RegisterHandler(Func<PolicyEffectiveEvent, Task> handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for scheduling re-evaluation jobs.
|
||||
/// </summary>
|
||||
public interface IReEvaluationJobScheduler
|
||||
{
|
||||
/// <summary>
|
||||
/// Schedules a re-evaluation job.
|
||||
/// </summary>
|
||||
Task<string> ScheduleAsync(ReEvaluationJobRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets pending job count.
|
||||
/// </summary>
|
||||
int GetPendingJobCount();
|
||||
|
||||
/// <summary>
|
||||
/// Gets job by ID.
|
||||
/// </summary>
|
||||
ReEvaluationJobRequest? GetJob(string jobId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes policy change events, schedules re-evaluations, and emits effective events.
|
||||
/// </summary>
|
||||
public sealed class PolicyEventProcessor : IPolicyEffectiveEventPublisher, IReEvaluationJobScheduler
|
||||
{
|
||||
private readonly ILogger<PolicyEventProcessor> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ConcurrentQueue<ReEvaluationJobRequest> _jobQueue;
|
||||
private readonly ConcurrentDictionary<string, ReEvaluationJobRequest> _jobIndex;
|
||||
private readonly ConcurrentQueue<PolicyEffectiveEvent> _eventStream;
|
||||
private readonly List<Func<PolicyEffectiveEvent, Task>> _eventHandlers;
|
||||
private readonly object _handlersLock = new();
|
||||
|
||||
private const int MaxQueueSize = 10000;
|
||||
private const int MaxEventStreamSize = 50000;
|
||||
|
||||
public PolicyEventProcessor(
|
||||
ILogger<PolicyEventProcessor> logger,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_jobQueue = new ConcurrentQueue<ReEvaluationJobRequest>();
|
||||
_jobIndex = new ConcurrentDictionary<string, ReEvaluationJobRequest>(StringComparer.OrdinalIgnoreCase);
|
||||
_eventStream = new ConcurrentQueue<PolicyEffectiveEvent>();
|
||||
_eventHandlers = new List<Func<PolicyEffectiveEvent, Task>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a policy change event and schedules re-evaluation if needed.
|
||||
/// </summary>
|
||||
public async Task<string?> ProcessChangeEventAsync(
|
||||
PolicyChangeEvent changeEvent,
|
||||
string packId,
|
||||
int packVersion,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(changeEvent);
|
||||
|
||||
using var activity = PolicyEngineTelemetry.ActivitySource.StartActivity("policy_event.process", ActivityKind.Internal);
|
||||
activity?.SetTag("event.id", changeEvent.EventId);
|
||||
activity?.SetTag("event.type", changeEvent.ChangeType.ToString());
|
||||
activity?.SetTag("tenant.id", changeEvent.TenantId);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Processing policy change event {EventId}: {ChangeType} for tenant {TenantId}",
|
||||
changeEvent.EventId, changeEvent.ChangeType, changeEvent.TenantId);
|
||||
|
||||
// Skip if event targets no subjects
|
||||
if (changeEvent.AffectedPurls.IsDefaultOrEmpty &&
|
||||
changeEvent.AffectedSbomIds.IsDefaultOrEmpty &&
|
||||
changeEvent.AffectedProductKeys.IsDefaultOrEmpty)
|
||||
{
|
||||
_logger.LogDebug("Skipping event {EventId}: no affected subjects", changeEvent.EventId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create re-evaluation job request
|
||||
var jobId = ReEvaluationJobRequest.CreateJobId(
|
||||
changeEvent.TenantId,
|
||||
packId,
|
||||
packVersion,
|
||||
changeEvent.ChangeType.ToString(),
|
||||
_timeProvider.GetUtcNow());
|
||||
|
||||
var jobRequest = new ReEvaluationJobRequest(
|
||||
JobId: jobId,
|
||||
TenantId: changeEvent.TenantId,
|
||||
PackId: packId,
|
||||
PackVersion: packVersion,
|
||||
TriggerType: changeEvent.ChangeType.ToString(),
|
||||
CorrelationId: changeEvent.CorrelationId,
|
||||
CreatedAt: _timeProvider.GetUtcNow(),
|
||||
Priority: MapPriority(changeEvent.Priority),
|
||||
AdvisoryIds: changeEvent.AdvisoryId is not null
|
||||
? ImmutableArray.Create(changeEvent.AdvisoryId)
|
||||
: ImmutableArray<string>.Empty,
|
||||
SubjectPurls: changeEvent.AffectedPurls,
|
||||
SbomIds: changeEvent.AffectedSbomIds,
|
||||
Metadata: changeEvent.Metadata);
|
||||
|
||||
// Schedule the job
|
||||
var scheduledId = await ScheduleAsync(jobRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("job.id", scheduledId);
|
||||
PolicyEngineTelemetry.PolicyEventsProcessed.Add(1);
|
||||
|
||||
return scheduledId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes results from a re-evaluation and emits effective events.
|
||||
/// </summary>
|
||||
public async Task ProcessReEvaluationResultsAsync(
|
||||
string jobId,
|
||||
string tenantId,
|
||||
string packId,
|
||||
int packVersion,
|
||||
string triggerType,
|
||||
string? correlationId,
|
||||
IReadOnlyList<PolicyDecisionChange> changes,
|
||||
long durationMs,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = PolicyEngineTelemetry.ActivitySource.StartActivity("policy_event.emit_results", ActivityKind.Internal);
|
||||
activity?.SetTag("job.id", jobId);
|
||||
activity?.SetTag("changes.count", changes.Count);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var changedCount = 0;
|
||||
|
||||
// Emit individual effective events for each changed decision
|
||||
foreach (var change in changes)
|
||||
{
|
||||
if (!change.HasChanged)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
changedCount++;
|
||||
|
||||
var diff = PolicyDecisionDiff.Create(
|
||||
change.OldStatus, change.NewStatus,
|
||||
change.OldSeverity, change.NewSeverity,
|
||||
change.OldRule, change.NewRule,
|
||||
change.OldPriority, change.NewPriority,
|
||||
change.OldAnnotations, change.NewAnnotations);
|
||||
|
||||
var evt = new PolicyEffectiveUpdatedEvent(
|
||||
EventId: GenerateEventId(),
|
||||
TenantId: tenantId,
|
||||
Timestamp: now,
|
||||
CorrelationId: correlationId,
|
||||
PackId: packId,
|
||||
PackVersion: packVersion,
|
||||
SubjectPurl: change.SubjectPurl,
|
||||
AdvisoryId: change.AdvisoryId,
|
||||
TriggerType: triggerType,
|
||||
Diff: diff);
|
||||
|
||||
await PublishEffectiveUpdatedAsync(evt, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Emit batch completed event
|
||||
var summary = ComputeBatchSummary(changes);
|
||||
var batchEvent = new PolicyBatchCompletedEvent(
|
||||
EventId: GenerateEventId(),
|
||||
TenantId: tenantId,
|
||||
Timestamp: now,
|
||||
CorrelationId: correlationId,
|
||||
BatchId: jobId,
|
||||
TriggerType: triggerType,
|
||||
SubjectsEvaluated: changes.Count,
|
||||
DecisionsChanged: changedCount,
|
||||
DurationMs: durationMs,
|
||||
Summary: summary);
|
||||
|
||||
await PublishBatchCompletedAsync(batchEvent, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("decisions.changed", changedCount);
|
||||
_logger.LogInformation(
|
||||
"Re-evaluation {JobId} completed: {Evaluated} subjects, {Changed} decisions changed in {Duration}ms",
|
||||
jobId, changes.Count, changedCount, durationMs);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PublishEffectiveUpdatedAsync(
|
||||
PolicyEffectiveUpdatedEvent evt,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await PublishEventAsync(evt).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PublishBatchCompletedAsync(
|
||||
PolicyBatchCompletedEvent evt,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await PublishEventAsync(evt).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterHandler(Func<PolicyEffectiveEvent, Task> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
|
||||
lock (_handlersLock)
|
||||
{
|
||||
_eventHandlers.Add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<string> ScheduleAsync(ReEvaluationJobRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
// Check for duplicate
|
||||
if (_jobIndex.ContainsKey(request.JobId))
|
||||
{
|
||||
_logger.LogDebug("Duplicate job {JobId} ignored", request.JobId);
|
||||
return Task.FromResult(request.JobId);
|
||||
}
|
||||
|
||||
// Enforce queue limit
|
||||
if (_jobQueue.Count >= MaxQueueSize)
|
||||
{
|
||||
_logger.LogWarning("Job queue full, rejecting job {JobId}", request.JobId);
|
||||
throw new InvalidOperationException("Re-evaluation job queue is full");
|
||||
}
|
||||
|
||||
_jobIndex[request.JobId] = request;
|
||||
_jobQueue.Enqueue(request);
|
||||
|
||||
PolicyEngineTelemetry.ReEvaluationJobsScheduled.Add(1);
|
||||
_logger.LogDebug(
|
||||
"Scheduled re-evaluation job {JobId}: {TriggerType} for {TenantId}/{PackId}@{Version}",
|
||||
request.JobId, request.TriggerType, request.TenantId, request.PackId, request.PackVersion);
|
||||
|
||||
return Task.FromResult(request.JobId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetPendingJobCount() => _jobQueue.Count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReEvaluationJobRequest? GetJob(string jobId)
|
||||
{
|
||||
_jobIndex.TryGetValue(jobId, out var job);
|
||||
return job;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues the next job for processing.
|
||||
/// </summary>
|
||||
public ReEvaluationJobRequest? DequeueJob()
|
||||
{
|
||||
if (_jobQueue.TryDequeue(out var job))
|
||||
{
|
||||
_jobIndex.TryRemove(job.JobId, out _);
|
||||
return job;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets recent effective events.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PolicyEffectiveEvent> GetRecentEvents(int limit = 100)
|
||||
{
|
||||
return _eventStream
|
||||
.ToArray()
|
||||
.OrderByDescending(e => e.Timestamp)
|
||||
.Take(limit)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
private async Task PublishEventAsync(PolicyEffectiveEvent evt)
|
||||
{
|
||||
// Add to stream
|
||||
_eventStream.Enqueue(evt);
|
||||
|
||||
// Trim if too large
|
||||
while (_eventStream.Count > MaxEventStreamSize)
|
||||
{
|
||||
_eventStream.TryDequeue(out _);
|
||||
}
|
||||
|
||||
// Invoke handlers
|
||||
List<Func<PolicyEffectiveEvent, Task>> handlers;
|
||||
lock (_handlersLock)
|
||||
{
|
||||
handlers = _eventHandlers.ToList();
|
||||
}
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler(evt).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking event handler for {EventType}", evt.EventType);
|
||||
}
|
||||
}
|
||||
|
||||
PolicyEngineTelemetry.PolicyEffectiveEventsPublished.Add(1);
|
||||
}
|
||||
|
||||
private static PolicyBatchSummary ComputeBatchSummary(IReadOnlyList<PolicyDecisionChange> changes)
|
||||
{
|
||||
var statusUpgrades = 0;
|
||||
var statusDowngrades = 0;
|
||||
var newBlocks = 0;
|
||||
var blocksRemoved = 0;
|
||||
var advisories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var purls = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var change in changes)
|
||||
{
|
||||
advisories.Add(change.AdvisoryId);
|
||||
purls.Add(change.SubjectPurl);
|
||||
|
||||
if (!change.HasChanged)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var severityChange = CompareSeverity(change.OldStatus, change.NewStatus);
|
||||
if (severityChange > 0)
|
||||
{
|
||||
statusUpgrades++;
|
||||
}
|
||||
else if (severityChange < 0)
|
||||
{
|
||||
statusDowngrades++;
|
||||
}
|
||||
|
||||
if (IsBlockStatus(change.NewStatus) && !IsBlockStatus(change.OldStatus))
|
||||
{
|
||||
newBlocks++;
|
||||
}
|
||||
else if (IsBlockStatus(change.OldStatus) && !IsBlockStatus(change.NewStatus))
|
||||
{
|
||||
blocksRemoved++;
|
||||
}
|
||||
}
|
||||
|
||||
return new PolicyBatchSummary(
|
||||
StatusUpgrades: statusUpgrades,
|
||||
StatusDowngrades: statusDowngrades,
|
||||
NewBlocks: newBlocks,
|
||||
BlocksRemoved: blocksRemoved,
|
||||
AffectedAdvisories: advisories.OrderBy(a => a).ToImmutableArray(),
|
||||
AffectedPurls: purls.OrderBy(p => p).Take(100).ToImmutableArray());
|
||||
}
|
||||
|
||||
private static int CompareSeverity(string? oldStatus, string? newStatus)
|
||||
{
|
||||
var oldSeverity = GetStatusSeverityLevel(oldStatus);
|
||||
var newSeverity = GetStatusSeverityLevel(newStatus);
|
||||
return newSeverity.CompareTo(oldSeverity);
|
||||
}
|
||||
|
||||
private static int GetStatusSeverityLevel(string? status) => status?.ToLowerInvariant() switch
|
||||
{
|
||||
"blocked" => 4,
|
||||
"deny" => 4,
|
||||
"warn" => 3,
|
||||
"affected" => 2,
|
||||
"allow" => 1,
|
||||
"ignored" => 0,
|
||||
_ => 1
|
||||
};
|
||||
|
||||
private static bool IsBlockStatus(string? status) =>
|
||||
string.Equals(status, "blocked", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(status, "deny", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static Events.PolicyChangePriority MapPriority(IncrementalOrchestrator.PolicyChangePriority priority) =>
|
||||
priority switch
|
||||
{
|
||||
IncrementalOrchestrator.PolicyChangePriority.Emergency => Events.PolicyChangePriority.Emergency,
|
||||
IncrementalOrchestrator.PolicyChangePriority.High => Events.PolicyChangePriority.High,
|
||||
_ => Events.PolicyChangePriority.Normal
|
||||
};
|
||||
|
||||
private static string GenerateEventId()
|
||||
{
|
||||
var guid = Guid.NewGuid().ToByteArray();
|
||||
return $"pee-{Convert.ToHexStringLower(guid)[..16]}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a change in policy decision for a subject.
|
||||
/// </summary>
|
||||
public sealed record PolicyDecisionChange(
|
||||
string SubjectPurl,
|
||||
string AdvisoryId,
|
||||
string? OldStatus,
|
||||
string NewStatus,
|
||||
string? OldSeverity,
|
||||
string? NewSeverity,
|
||||
string? OldRule,
|
||||
string? NewRule,
|
||||
int? OldPriority,
|
||||
int? NewPriority,
|
||||
ImmutableDictionary<string, string>? OldAnnotations,
|
||||
ImmutableDictionary<string, string>? NewAnnotations)
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the decision has changed.
|
||||
/// </summary>
|
||||
public bool HasChanged =>
|
||||
!string.Equals(OldStatus, NewStatus, StringComparison.Ordinal) ||
|
||||
!string.Equals(OldSeverity, NewSeverity, StringComparison.Ordinal) ||
|
||||
!string.Equals(OldRule, NewRule, StringComparison.Ordinal);
|
||||
}
|
||||
Reference in New Issue
Block a user