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.
468 lines
15 KiB
C#
468 lines
15 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Policy.RiskProfile.Lifecycle;
|
|
using StellaOps.Policy.RiskProfile.Models;
|
|
|
|
namespace StellaOps.Policy.Engine.Notifications;
|
|
|
|
/// <summary>
|
|
/// Service for publishing policy profile lifecycle notifications.
|
|
/// Integrates with the RiskProfileLifecycleService to emit events.
|
|
/// </summary>
|
|
public sealed class PolicyProfileNotificationService
|
|
{
|
|
private readonly IPolicyProfileNotificationPublisher _publisher;
|
|
private readonly PolicyProfileNotificationFactory _factory;
|
|
private readonly PolicyProfileNotificationOptions _options;
|
|
private readonly ILogger<PolicyProfileNotificationService> _logger;
|
|
|
|
public PolicyProfileNotificationService(
|
|
IPolicyProfileNotificationPublisher publisher,
|
|
PolicyProfileNotificationFactory factory,
|
|
IOptions<PolicyProfileNotificationOptions> options,
|
|
ILogger<PolicyProfileNotificationService> logger)
|
|
{
|
|
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
|
|
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
|
_options = options?.Value ?? new PolicyProfileNotificationOptions();
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies that a new profile version was created.
|
|
/// </summary>
|
|
public async Task NotifyProfileCreatedAsync(
|
|
string tenantId,
|
|
RiskProfileModel profile,
|
|
string? actorId,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(profile);
|
|
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var scope = ExtractEffectiveScope(profile);
|
|
var notification = _factory.CreateProfileCreatedEvent(
|
|
tenantId,
|
|
profile.Id,
|
|
profile.Version,
|
|
actorId,
|
|
hash,
|
|
scope);
|
|
|
|
await _publisher.PublishAsync(notification, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish profile created notification for {ProfileId}@{Version}",
|
|
profile.Id, profile.Version);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies that a profile version was activated.
|
|
/// </summary>
|
|
public async Task NotifyProfileActivatedAsync(
|
|
string tenantId,
|
|
RiskProfileModel profile,
|
|
string? actorId,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(profile);
|
|
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var scope = ExtractEffectiveScope(profile);
|
|
var notification = _factory.CreateProfileActivatedEvent(
|
|
tenantId,
|
|
profile.Id,
|
|
profile.Version,
|
|
actorId,
|
|
hash,
|
|
scope);
|
|
|
|
await _publisher.PublishAsync(notification, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish profile activated notification for {ProfileId}@{Version}",
|
|
profile.Id, profile.Version);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies that a profile version was deactivated (deprecated or archived).
|
|
/// </summary>
|
|
public async Task NotifyProfileDeactivatedAsync(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
string? reason,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var notification = _factory.CreateProfileDeactivatedEvent(
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
actorId,
|
|
reason,
|
|
hash);
|
|
|
|
await _publisher.PublishAsync(notification, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish profile deactivated notification for {ProfileId}@{Version}",
|
|
profileId, profileVersion);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies that risk thresholds were changed.
|
|
/// </summary>
|
|
public async Task NotifyThresholdChangedAsync(
|
|
string tenantId,
|
|
RiskProfileModel profile,
|
|
string? actorId,
|
|
string? reason,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(profile);
|
|
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var thresholds = ExtractThresholds(profile);
|
|
var scope = ExtractEffectiveScope(profile);
|
|
var notification = _factory.CreateThresholdChangedEvent(
|
|
tenantId,
|
|
profile.Id,
|
|
profile.Version,
|
|
actorId,
|
|
reason,
|
|
thresholds,
|
|
hash,
|
|
scope);
|
|
|
|
await _publisher.PublishAsync(notification, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish threshold changed notification for {ProfileId}@{Version}",
|
|
profile.Id, profile.Version);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies that an override was added to a profile.
|
|
/// </summary>
|
|
public async Task NotifyOverrideAddedAsync(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
string overrideId,
|
|
string overrideType,
|
|
string? target,
|
|
string? action,
|
|
string? justification,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var overrideDetails = new NotificationOverrideDetails
|
|
{
|
|
OverrideId = overrideId,
|
|
OverrideType = overrideType,
|
|
Target = target,
|
|
Action = action,
|
|
Justification = justification
|
|
};
|
|
|
|
var notification = _factory.CreateOverrideAddedEvent(
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
actorId,
|
|
overrideDetails,
|
|
hash);
|
|
|
|
await _publisher.PublishAsync(notification, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish override added notification for {ProfileId}@{Version}",
|
|
profileId, profileVersion);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies that an override was removed from a profile.
|
|
/// </summary>
|
|
public async Task NotifyOverrideRemovedAsync(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string? actorId,
|
|
string overrideId,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var overrideDetails = new NotificationOverrideDetails
|
|
{
|
|
OverrideId = overrideId
|
|
};
|
|
|
|
var notification = _factory.CreateOverrideRemovedEvent(
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
actorId,
|
|
overrideDetails,
|
|
hash);
|
|
|
|
await _publisher.PublishAsync(notification, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish override removed notification for {ProfileId}@{Version}",
|
|
profileId, profileVersion);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies that simulation results are ready for consumption.
|
|
/// </summary>
|
|
public async Task NotifySimulationReadyAsync(
|
|
string tenantId,
|
|
string profileId,
|
|
string profileVersion,
|
|
string simulationId,
|
|
int findingsCount,
|
|
int highImpactCount,
|
|
DateTimeOffset completedAt,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var simulationDetails = new NotificationSimulationDetails
|
|
{
|
|
SimulationId = simulationId,
|
|
FindingsCount = findingsCount,
|
|
HighImpactCount = highImpactCount,
|
|
CompletedAt = completedAt
|
|
};
|
|
|
|
var notification = _factory.CreateSimulationReadyEvent(
|
|
tenantId,
|
|
profileId,
|
|
profileVersion,
|
|
simulationDetails,
|
|
hash);
|
|
|
|
await _publisher.PublishAsync(notification, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish simulation ready notification for {ProfileId}@{Version}",
|
|
profileId, profileVersion);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies based on a lifecycle event from the RiskProfileLifecycleService.
|
|
/// </summary>
|
|
public async Task NotifyFromLifecycleEventAsync(
|
|
string tenantId,
|
|
RiskProfileLifecycleEvent lifecycleEvent,
|
|
RiskProfileModel? profile,
|
|
string? hash,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(lifecycleEvent);
|
|
|
|
if (!_options.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (lifecycleEvent.EventType)
|
|
{
|
|
case RiskProfileLifecycleEventType.Created:
|
|
if (profile is not null)
|
|
{
|
|
await NotifyProfileCreatedAsync(tenantId, profile, lifecycleEvent.Actor, hash, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
break;
|
|
|
|
case RiskProfileLifecycleEventType.Activated:
|
|
if (profile is not null)
|
|
{
|
|
await NotifyProfileActivatedAsync(tenantId, profile, lifecycleEvent.Actor, hash, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
break;
|
|
|
|
case RiskProfileLifecycleEventType.Deprecated:
|
|
case RiskProfileLifecycleEventType.Archived:
|
|
await NotifyProfileDeactivatedAsync(
|
|
tenantId,
|
|
lifecycleEvent.ProfileId,
|
|
lifecycleEvent.Version,
|
|
lifecycleEvent.Actor,
|
|
lifecycleEvent.Reason,
|
|
hash,
|
|
cancellationToken).ConfigureAwait(false);
|
|
break;
|
|
|
|
case RiskProfileLifecycleEventType.Restored:
|
|
// Restored profiles go back to deprecated status; no dedicated notification
|
|
_logger.LogDebug("Profile {ProfileId}@{Version} restored; no notification emitted",
|
|
lifecycleEvent.ProfileId, lifecycleEvent.Version);
|
|
break;
|
|
|
|
default:
|
|
_logger.LogDebug("Unhandled lifecycle event type {EventType} for {ProfileId}@{Version}",
|
|
lifecycleEvent.EventType, lifecycleEvent.ProfileId, lifecycleEvent.Version);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static NotificationEffectiveScope? ExtractEffectiveScope(RiskProfileModel profile)
|
|
{
|
|
// Extract scope information from profile metadata if available
|
|
var metadata = profile.Metadata;
|
|
if (metadata is null || metadata.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var scope = new NotificationEffectiveScope();
|
|
var hasAny = false;
|
|
|
|
if (metadata.TryGetValue("tenants", out var tenantsObj) && tenantsObj is IEnumerable<object> tenants)
|
|
{
|
|
scope = scope with { Tenants = tenants.Select(t => t.ToString()!).ToList() };
|
|
hasAny = true;
|
|
}
|
|
|
|
if (metadata.TryGetValue("projects", out var projectsObj) && projectsObj is IEnumerable<object> projects)
|
|
{
|
|
scope = scope with { Projects = projects.Select(p => p.ToString()!).ToList() };
|
|
hasAny = true;
|
|
}
|
|
|
|
if (metadata.TryGetValue("purl_patterns", out var purlObj) && purlObj is IEnumerable<object> purls)
|
|
{
|
|
scope = scope with { PurlPatterns = purls.Select(p => p.ToString()!).ToList() };
|
|
hasAny = true;
|
|
}
|
|
|
|
if (metadata.TryGetValue("cpe_patterns", out var cpeObj) && cpeObj is IEnumerable<object> cpes)
|
|
{
|
|
scope = scope with { CpePatterns = cpes.Select(c => c.ToString()!).ToList() };
|
|
hasAny = true;
|
|
}
|
|
|
|
if (metadata.TryGetValue("tags", out var tagsObj) && tagsObj is IEnumerable<object> tags)
|
|
{
|
|
scope = scope with { Tags = tags.Select(t => t.ToString()!).ToList() };
|
|
hasAny = true;
|
|
}
|
|
|
|
return hasAny ? scope : null;
|
|
}
|
|
|
|
private static NotificationThresholds ExtractThresholds(RiskProfileModel profile)
|
|
{
|
|
// Extract thresholds from profile overrides
|
|
var thresholds = new NotificationThresholds();
|
|
|
|
// Map severity overrides to threshold values
|
|
foreach (var severityOverride in profile.Overrides.Severity)
|
|
{
|
|
var targetSeverity = severityOverride.Set.ToString().ToLowerInvariant();
|
|
var threshold = ExtractThresholdValue(severityOverride.When);
|
|
|
|
thresholds = targetSeverity switch
|
|
{
|
|
"info" or "informational" => thresholds with { Info = threshold },
|
|
"low" => thresholds with { Low = threshold },
|
|
"medium" => thresholds with { Medium = threshold },
|
|
"high" => thresholds with { High = threshold },
|
|
"critical" => thresholds with { Critical = threshold },
|
|
_ => thresholds
|
|
};
|
|
}
|
|
|
|
return thresholds;
|
|
}
|
|
|
|
private static double? ExtractThresholdValue(Dictionary<string, object> conditions)
|
|
{
|
|
// Try to extract a numeric threshold from conditions
|
|
if (conditions.TryGetValue("score_gte", out var scoreGte) && scoreGte is double d1)
|
|
{
|
|
return d1;
|
|
}
|
|
|
|
if (conditions.TryGetValue("score_gt", out var scoreGt) && scoreGt is double d2)
|
|
{
|
|
return d2;
|
|
}
|
|
|
|
if (conditions.TryGetValue("threshold", out var threshold) && threshold is double d3)
|
|
{
|
|
return d3;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|