Add post-quantum cryptography support with PqSoftCryptoProvider
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
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.
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user