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
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
- Implemented PqSoftCryptoProvider for software-only post-quantum algorithms (Dilithium3, Falcon512) using BouncyCastle. - Added PqSoftProviderOptions and PqSoftKeyOptions for configuration. - Created unit tests for Dilithium3 and Falcon512 signing and verification. - Introduced EcdsaPolicyCryptoProvider for compliance profiles (FIPS/eIDAS) with explicit allow-lists. - Added KcmvpHashOnlyProvider for KCMVP baseline compliance. - Updated project files and dependencies for new libraries and testing frameworks.
397 lines
13 KiB
C#
397 lines
13 KiB
C#
using System.Diagnostics;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace StellaOps.Policy.Engine.Notifications;
|
|
|
|
/// <summary>
|
|
/// Interface for publishing policy profile notification events.
|
|
/// </summary>
|
|
public interface IPolicyProfileNotificationPublisher
|
|
{
|
|
/// <summary>
|
|
/// Publishes a notification event to the configured transport.
|
|
/// </summary>
|
|
Task PublishAsync(PolicyProfileNotificationEvent notification, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Delivers a notification via webhook with HMAC signature.
|
|
/// </summary>
|
|
Task<bool> DeliverWebhookAsync(WebhookDeliveryRequest request, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logging-based notification publisher for policy profile events.
|
|
/// Logs notifications as structured events for downstream consumption.
|
|
/// </summary>
|
|
internal sealed class LoggingPolicyProfileNotificationPublisher : IPolicyProfileNotificationPublisher
|
|
{
|
|
private readonly ILogger<LoggingPolicyProfileNotificationPublisher> _logger;
|
|
private readonly PolicyProfileNotificationOptions _options;
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
|
WriteIndented = false
|
|
};
|
|
|
|
public LoggingPolicyProfileNotificationPublisher(
|
|
ILogger<LoggingPolicyProfileNotificationPublisher> logger,
|
|
IOptions<PolicyProfileNotificationOptions> options,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_options = options?.Value ?? new PolicyProfileNotificationOptions();
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
public Task PublishAsync(PolicyProfileNotificationEvent notification, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(notification);
|
|
|
|
if (!_options.Enabled)
|
|
{
|
|
_logger.LogDebug(
|
|
"Policy profile notifications disabled; skipping event {EventId} type {EventType}",
|
|
notification.EventId,
|
|
notification.EventType);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var payload = JsonSerializer.Serialize(notification, JsonOptions);
|
|
|
|
_logger.LogInformation(
|
|
"PolicyProfileNotification topic={Topic} event_id={EventId} event_type={EventType} tenant={TenantId} profile={ProfileId}@{ProfileVersion} payload={Payload}",
|
|
_options.TopicName,
|
|
notification.EventId,
|
|
notification.EventType,
|
|
notification.TenantId,
|
|
notification.ProfileId,
|
|
notification.ProfileVersion,
|
|
payload);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<bool> DeliverWebhookAsync(WebhookDeliveryRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var payload = JsonSerializer.Serialize(request.Event, JsonOptions);
|
|
var signature = ComputeHmacSignature(payload, request.SharedSecret);
|
|
|
|
_logger.LogInformation(
|
|
"PolicyProfileWebhook url={Url} event_id={EventId} event_type={EventType} signature={Signature}",
|
|
request.Url,
|
|
request.Event.EventId,
|
|
request.Event.EventType,
|
|
signature ?? "(no secret)");
|
|
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
private static string? ComputeHmacSignature(string payload, string? sharedSecret)
|
|
{
|
|
if (string.IsNullOrEmpty(sharedSecret))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var keyBytes = Encoding.UTF8.GetBytes(sharedSecret);
|
|
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
|
|
|
using var hmac = new HMACSHA256(keyBytes);
|
|
var hashBytes = hmac.ComputeHash(payloadBytes);
|
|
return Convert.ToHexStringLower(hashBytes);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory for creating policy profile notification events.
|
|
/// </summary>
|
|
public sealed class PolicyProfileNotificationFactory
|
|
{
|
|
private readonly TimeProvider _timeProvider;
|
|
private readonly PolicyProfileNotificationOptions _options;
|
|
|
|
public PolicyProfileNotificationFactory(
|
|
TimeProvider? timeProvider = null,
|
|
PolicyProfileNotificationOptions? options = null)
|
|
{
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
_options = options ?? new PolicyProfileNotificationOptions();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a profile created notification event.
|
|
/// </summary>
|
|
public PolicyProfileNotificationEvent CreateProfileCreatedEvent(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
string? hash,
|
|
NotificationEffectiveScope? scope = null)
|
|
{
|
|
return CreateEvent(
|
|
PolicyProfileNotificationEventTypes.ProfileCreated,
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
"New profile draft created",
|
|
actorId,
|
|
hash,
|
|
scope: scope);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a profile activated notification event.
|
|
/// </summary>
|
|
public PolicyProfileNotificationEvent CreateProfileActivatedEvent(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
string? hash,
|
|
NotificationEffectiveScope? scope = null)
|
|
{
|
|
return CreateEvent(
|
|
PolicyProfileNotificationEventTypes.ProfileActivated,
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
"Profile version activated",
|
|
actorId,
|
|
hash,
|
|
scope: scope);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a profile deactivated notification event.
|
|
/// </summary>
|
|
public PolicyProfileNotificationEvent CreateProfileDeactivatedEvent(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
string? reason,
|
|
string? hash)
|
|
{
|
|
return CreateEvent(
|
|
PolicyProfileNotificationEventTypes.ProfileDeactivated,
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
reason ?? "Profile version deactivated",
|
|
actorId,
|
|
hash);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a threshold changed notification event.
|
|
/// </summary>
|
|
public PolicyProfileNotificationEvent CreateThresholdChangedEvent(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
string? reason,
|
|
NotificationThresholds thresholds,
|
|
string? hash,
|
|
NotificationEffectiveScope? scope = null)
|
|
{
|
|
return CreateEvent(
|
|
PolicyProfileNotificationEventTypes.ThresholdChanged,
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
reason ?? "Risk thresholds updated",
|
|
actorId,
|
|
hash,
|
|
thresholds: thresholds,
|
|
scope: scope);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an override added notification event.
|
|
/// </summary>
|
|
public PolicyProfileNotificationEvent CreateOverrideAddedEvent(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
NotificationOverrideDetails overrideDetails,
|
|
string? hash)
|
|
{
|
|
return CreateEvent(
|
|
PolicyProfileNotificationEventTypes.OverrideAdded,
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
$"Override added: {overrideDetails.OverrideType}",
|
|
actorId,
|
|
hash,
|
|
overrideDetails: overrideDetails);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an override removed notification event.
|
|
/// </summary>
|
|
public PolicyProfileNotificationEvent CreateOverrideRemovedEvent(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
NotificationOverrideDetails overrideDetails,
|
|
string? hash)
|
|
{
|
|
return CreateEvent(
|
|
PolicyProfileNotificationEventTypes.OverrideRemoved,
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
$"Override removed: {overrideDetails.OverrideId}",
|
|
actorId,
|
|
hash,
|
|
overrideDetails: overrideDetails);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a simulation ready notification event.
|
|
/// </summary>
|
|
public PolicyProfileNotificationEvent CreateSimulationReadyEvent(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
NotificationSimulationDetails simulationDetails,
|
|
string? hash)
|
|
{
|
|
return CreateEvent(
|
|
PolicyProfileNotificationEventTypes.SimulationReady,
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
"Simulation results available",
|
|
actorId: null,
|
|
hash,
|
|
simulationDetails: simulationDetails);
|
|
}
|
|
|
|
private PolicyProfileNotificationEvent CreateEvent(
|
|
string eventType,
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string changeReason,
|
|
string? actorId,
|
|
string? hash,
|
|
NotificationThresholds? thresholds = null,
|
|
NotificationEffectiveScope? scope = null,
|
|
NotificationOverrideDetails? overrideDetails = null,
|
|
NotificationSimulationDetails? simulationDetails = null)
|
|
{
|
|
var eventId = GenerateUuidV7();
|
|
var now = _timeProvider.GetUtcNow();
|
|
|
|
NotificationActor? actor = null;
|
|
if (!string.IsNullOrWhiteSpace(actorId))
|
|
{
|
|
actor = new NotificationActor
|
|
{
|
|
Type = actorId.Contains('@') ? "user" : "system",
|
|
Id = actorId
|
|
};
|
|
}
|
|
|
|
NotificationHash? hashInfo = null;
|
|
if (!string.IsNullOrWhiteSpace(hash))
|
|
{
|
|
hashInfo = new NotificationHash
|
|
{
|
|
Algorithm = "sha256",
|
|
Value = hash
|
|
};
|
|
}
|
|
|
|
NotificationLinks? links = null;
|
|
if (!string.IsNullOrWhiteSpace(_options.BaseUrl))
|
|
{
|
|
links = new NotificationLinks
|
|
{
|
|
ProfileUrl = $"{_options.BaseUrl}/api/risk/profiles/{profileId}",
|
|
DiffUrl = $"{_options.BaseUrl}/api/risk/profiles/{profileId}/diff",
|
|
SimulationUrl = simulationDetails?.SimulationId is not null
|
|
? $"{_options.BaseUrl}/api/risk/simulations/results/{simulationDetails.SimulationId}"
|
|
: null
|
|
};
|
|
}
|
|
|
|
NotificationTraceContext? trace = null;
|
|
if (_options.IncludeTraceContext)
|
|
{
|
|
var activity = Activity.Current;
|
|
if (activity is not null)
|
|
{
|
|
trace = new NotificationTraceContext
|
|
{
|
|
TraceId = activity.TraceId.ToString(),
|
|
SpanId = activity.SpanId.ToString()
|
|
};
|
|
}
|
|
}
|
|
|
|
return new PolicyProfileNotificationEvent
|
|
{
|
|
EventId = eventId,
|
|
EventType = eventType,
|
|
EmittedAt = now,
|
|
TenantId = tenantId,
|
|
ProfileId = profileId,
|
|
ProfileVersion = profileVersion,
|
|
ChangeReason = changeReason,
|
|
Actor = actor,
|
|
Thresholds = thresholds,
|
|
EffectiveScope = scope,
|
|
Hash = hashInfo,
|
|
Links = links,
|
|
Trace = trace,
|
|
OverrideDetails = overrideDetails,
|
|
SimulationDetails = simulationDetails
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a UUIDv7 (time-ordered UUID) for event identification.
|
|
/// </summary>
|
|
private string GenerateUuidV7()
|
|
{
|
|
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
|
|
var randomBytes = new byte[10];
|
|
RandomNumberGenerator.Fill(randomBytes);
|
|
|
|
var bytes = new byte[16];
|
|
|
|
// First 6 bytes: timestamp (48 bits)
|
|
bytes[0] = (byte)((timestamp >> 40) & 0xFF);
|
|
bytes[1] = (byte)((timestamp >> 32) & 0xFF);
|
|
bytes[2] = (byte)((timestamp >> 24) & 0xFF);
|
|
bytes[3] = (byte)((timestamp >> 16) & 0xFF);
|
|
bytes[4] = (byte)((timestamp >> 8) & 0xFF);
|
|
bytes[5] = (byte)(timestamp & 0xFF);
|
|
|
|
// Version 7 (4 bits) + random (12 bits)
|
|
bytes[6] = (byte)(0x70 | (randomBytes[0] & 0x0F));
|
|
bytes[7] = randomBytes[1];
|
|
|
|
// Variant (2 bits) + random (62 bits)
|
|
bytes[8] = (byte)(0x80 | (randomBytes[2] & 0x3F));
|
|
Array.Copy(randomBytes, 3, bytes, 9, 7);
|
|
|
|
return new Guid(bytes).ToString();
|
|
}
|
|
}
|