warnings fixes, tests fixes, sprints completions
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Secrets;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes secret alerts to the Notify service queue.
|
||||
/// Transforms SecretFindingAlertEvent to NotifyEvent format.
|
||||
/// </summary>
|
||||
public sealed class NotifySecretAlertPublisher : ISecretAlertPublisher
|
||||
{
|
||||
private readonly INotifyEventQueue _notifyQueue;
|
||||
private readonly ILogger<NotifySecretAlertPublisher> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public NotifySecretAlertPublisher(
|
||||
INotifyEventQueue notifyQueue,
|
||||
ILogger<NotifySecretAlertPublisher> logger,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_notifyQueue = notifyQueue ?? throw new ArgumentNullException(nameof(notifyQueue));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public async ValueTask PublishAsync(
|
||||
SecretFindingAlertEvent alertEvent,
|
||||
SecretAlertDestination destination,
|
||||
SecretAlertSettings settings,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var payload = BuildPayload(alertEvent, settings);
|
||||
|
||||
var notifyEvent = new NotifyEventDto
|
||||
{
|
||||
EventId = alertEvent.EventId,
|
||||
Kind = SecretFindingAlertEvent.EventKind,
|
||||
Tenant = alertEvent.TenantId,
|
||||
Ts = alertEvent.DetectedAt,
|
||||
Payload = payload,
|
||||
Scope = new NotifyEventScopeDto
|
||||
{
|
||||
ImageRef = alertEvent.ImageRef,
|
||||
Digest = alertEvent.ArtifactDigest
|
||||
},
|
||||
Attributes = new Dictionary<string, string>
|
||||
{
|
||||
["severity"] = alertEvent.Severity.ToString().ToLowerInvariant(),
|
||||
["ruleId"] = alertEvent.RuleId,
|
||||
["channelType"] = destination.ChannelType.ToString().ToLowerInvariant(),
|
||||
["destinationId"] = destination.Id.ToString()
|
||||
}
|
||||
};
|
||||
|
||||
await _notifyQueue.EnqueueAsync(notifyEvent, ct);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Published secret alert {EventId} to {ChannelType}:{ChannelId}",
|
||||
alertEvent.EventId,
|
||||
destination.ChannelType,
|
||||
destination.ChannelId);
|
||||
}
|
||||
|
||||
public async ValueTask PublishSummaryAsync(
|
||||
SecretFindingSummaryEvent summary,
|
||||
SecretAlertDestination destination,
|
||||
SecretAlertSettings settings,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var payload = BuildSummaryPayload(summary, settings);
|
||||
|
||||
var notifyEvent = new NotifyEventDto
|
||||
{
|
||||
EventId = summary.EventId,
|
||||
Kind = SecretFindingSummaryEvent.EventKind,
|
||||
Tenant = summary.TenantId,
|
||||
Ts = summary.DetectedAt,
|
||||
Payload = payload,
|
||||
Scope = new NotifyEventScopeDto
|
||||
{
|
||||
ImageRef = summary.ImageRef
|
||||
},
|
||||
Attributes = new Dictionary<string, string>
|
||||
{
|
||||
["totalFindings"] = summary.TotalFindings.ToString(CultureInfo.InvariantCulture),
|
||||
["channelType"] = destination.ChannelType.ToString().ToLowerInvariant(),
|
||||
["destinationId"] = destination.Id.ToString()
|
||||
}
|
||||
};
|
||||
|
||||
await _notifyQueue.EnqueueAsync(notifyEvent, ct);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Published secret summary alert {EventId} with {Count} findings to {ChannelType}",
|
||||
summary.EventId,
|
||||
summary.TotalFindings,
|
||||
destination.ChannelType);
|
||||
}
|
||||
|
||||
private static JsonNode BuildPayload(SecretFindingAlertEvent alert, SecretAlertSettings settings)
|
||||
{
|
||||
var payload = new JsonObject
|
||||
{
|
||||
["eventId"] = alert.EventId.ToString(),
|
||||
["scanId"] = alert.ScanId.ToString(),
|
||||
["severity"] = alert.Severity.ToString(),
|
||||
["confidence"] = alert.Confidence.ToString(),
|
||||
["ruleId"] = alert.RuleId,
|
||||
["ruleName"] = alert.RuleName,
|
||||
["detectedAt"] = alert.DetectedAt.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
if (settings.IncludeFilePath)
|
||||
{
|
||||
payload["filePath"] = alert.FilePath;
|
||||
payload["lineNumber"] = alert.LineNumber;
|
||||
}
|
||||
|
||||
if (settings.IncludeMaskedValue)
|
||||
{
|
||||
payload["maskedValue"] = alert.MaskedValue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(alert.RuleCategory))
|
||||
{
|
||||
payload["ruleCategory"] = alert.RuleCategory;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(alert.ScanTriggeredBy))
|
||||
{
|
||||
payload["triggeredBy"] = alert.ScanTriggeredBy;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(alert.BundleVersion))
|
||||
{
|
||||
payload["bundleVersion"] = alert.BundleVersion;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static JsonNode BuildSummaryPayload(SecretFindingSummaryEvent summary, SecretAlertSettings settings)
|
||||
{
|
||||
var severityBreakdown = new JsonObject();
|
||||
foreach (var (severity, count) in summary.FindingsBySeverity)
|
||||
{
|
||||
severityBreakdown[severity.ToString().ToLowerInvariant()] = count;
|
||||
}
|
||||
|
||||
var categoryBreakdown = new JsonObject();
|
||||
foreach (var (category, count) in summary.FindingsByCategory)
|
||||
{
|
||||
categoryBreakdown[category] = count;
|
||||
}
|
||||
|
||||
var topFindings = new JsonArray();
|
||||
foreach (var finding in summary.TopFindings)
|
||||
{
|
||||
var findingNode = new JsonObject
|
||||
{
|
||||
["ruleId"] = finding.RuleId,
|
||||
["severity"] = finding.Severity.ToString()
|
||||
};
|
||||
|
||||
if (settings.IncludeFilePath)
|
||||
{
|
||||
findingNode["filePath"] = finding.FilePath;
|
||||
findingNode["lineNumber"] = finding.LineNumber;
|
||||
}
|
||||
|
||||
if (settings.IncludeMaskedValue)
|
||||
{
|
||||
findingNode["maskedValue"] = finding.MaskedValue;
|
||||
}
|
||||
|
||||
topFindings.Add(findingNode);
|
||||
}
|
||||
|
||||
return new JsonObject
|
||||
{
|
||||
["eventId"] = summary.EventId.ToString(),
|
||||
["scanId"] = summary.ScanId.ToString(),
|
||||
["totalFindings"] = summary.TotalFindings,
|
||||
["severityBreakdown"] = severityBreakdown,
|
||||
["categoryBreakdown"] = categoryBreakdown,
|
||||
["topFindings"] = topFindings,
|
||||
["detectedAt"] = summary.DetectedAt.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for queuing events to the Notify service.
|
||||
/// </summary>
|
||||
public interface INotifyEventQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// Enqueues an event for delivery to Notify.
|
||||
/// </summary>
|
||||
ValueTask EnqueueAsync(NotifyEventDto eventDto, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for events to be sent to Notify service.
|
||||
/// </summary>
|
||||
public sealed record NotifyEventDto
|
||||
{
|
||||
public required Guid EventId { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public required string Tenant { get; init; }
|
||||
public required DateTimeOffset Ts { get; init; }
|
||||
public JsonNode? Payload { get; init; }
|
||||
public NotifyEventScopeDto? Scope { get; init; }
|
||||
public Dictionary<string, string>? Attributes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scope DTO for Notify events.
|
||||
/// </summary>
|
||||
public sealed record NotifyEventScopeDto
|
||||
{
|
||||
public string? ImageRef { get; init; }
|
||||
public string? Digest { get; init; }
|
||||
public string? Namespace { get; init; }
|
||||
public string? Repository { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null implementation of INotifyEventQueue for when Notify is not configured.
|
||||
/// </summary>
|
||||
public sealed class NullNotifyEventQueue : INotifyEventQueue
|
||||
{
|
||||
private readonly ILogger<NullNotifyEventQueue> _logger;
|
||||
|
||||
public NullNotifyEventQueue(ILogger<NullNotifyEventQueue> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public ValueTask EnqueueAsync(NotifyEventDto eventDto, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Notify not configured, dropping event {EventId} of kind {Kind}",
|
||||
eventDto.EventId,
|
||||
eventDto.Kind);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user