using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Zastava.Core.Contracts;
namespace StellaOps.Scanner.WebService.Services;
///
/// Handles delta scan requests triggered by runtime DRIFT events.
///
internal interface IDeltaScanRequestHandler
{
///
/// Processes a batch of runtime events and triggers scans for DRIFT events when enabled.
///
Task ProcessAsync(
IReadOnlyList envelopes,
CancellationToken cancellationToken);
}
///
/// Result of delta scan processing.
///
internal readonly record struct DeltaScanResult(
int DriftEventsDetected,
int ScansTriggered,
int ScansSkipped,
int ScansDeduped);
internal sealed class DeltaScanRequestHandler : IDeltaScanRequestHandler
{
private static readonly Meter DeltaScanMeter = new("StellaOps.Scanner.DeltaScan", "1.0.0");
private static readonly Counter DeltaScanTriggered = DeltaScanMeter.CreateCounter(
"scanner_delta_scan_triggered_total",
unit: "1",
description: "Total delta scans triggered from runtime DRIFT events.");
private static readonly Counter DeltaScanSkipped = DeltaScanMeter.CreateCounter(
"scanner_delta_scan_skipped_total",
unit: "1",
description: "Total delta scans skipped (feature disabled, rate limited, or missing data).");
private static readonly Counter DeltaScanDeduped = DeltaScanMeter.CreateCounter(
"scanner_delta_scan_deduped_total",
unit: "1",
description: "Total delta scans deduplicated within cooldown window.");
private static readonly Histogram DeltaScanLatencyMs = DeltaScanMeter.CreateHistogram(
"scanner_delta_scan_latency_ms",
unit: "ms",
description: "Latency for delta scan trigger processing.");
// Deduplication cache: imageDigest -> last trigger time
private readonly ConcurrentDictionary _recentTriggers = new(StringComparer.OrdinalIgnoreCase);
private readonly IScanCoordinator _scanCoordinator;
private readonly IOptionsMonitor _optionsMonitor;
private readonly TimeProvider _timeProvider;
private readonly ILogger _logger;
public DeltaScanRequestHandler(
IScanCoordinator scanCoordinator,
IOptionsMonitor optionsMonitor,
TimeProvider timeProvider,
ILogger logger)
{
_scanCoordinator = scanCoordinator ?? throw new ArgumentNullException(nameof(scanCoordinator));
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task ProcessAsync(
IReadOnlyList envelopes,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(envelopes);
var stopwatch = Stopwatch.StartNew();
var options = _optionsMonitor.CurrentValue.Runtime ?? new ScannerWebServiceOptions.RuntimeOptions();
// Check if autoscan is enabled
if (!options.AutoScanEnabled)
{
var driftCount = envelopes.Count(e => e.Event.Kind == RuntimeEventKind.Drift);
if (driftCount > 0)
{
DeltaScanSkipped.Add(driftCount);
_logger.LogDebug(
"Delta scan disabled, skipping {DriftCount} DRIFT events",
driftCount);
}
return new DeltaScanResult(driftCount, 0, driftCount, 0);
}
var driftEvents = envelopes
.Where(e => e.Event.Kind == RuntimeEventKind.Drift)
.ToList();
if (driftEvents.Count == 0)
{
return new DeltaScanResult(0, 0, 0, 0);
}
var now = _timeProvider.GetUtcNow();
var cooldownWindow = TimeSpan.FromSeconds(options.AutoScanCooldownSeconds);
var triggered = 0;
var skipped = 0;
var deduped = 0;
// Cleanup old entries from dedup cache
CleanupDeduplicationCache(now, cooldownWindow);
foreach (var envelope in driftEvents)
{
var runtimeEvent = envelope.Event;
var imageDigest = ExtractImageDigest(runtimeEvent);
if (string.IsNullOrWhiteSpace(imageDigest))
{
_logger.LogWarning(
"DRIFT event {EventId} has no image digest, skipping auto-scan",
runtimeEvent.EventId);
DeltaScanSkipped.Add(1);
skipped++;
continue;
}
// Check deduplication
if (_recentTriggers.TryGetValue(imageDigest, out var lastTrigger))
{
if (now - lastTrigger < cooldownWindow)
{
_logger.LogDebug(
"DRIFT event {EventId} for image {ImageDigest} within cooldown window, deduplicating",
runtimeEvent.EventId,
imageDigest);
DeltaScanDeduped.Add(1);
deduped++;
continue;
}
}
// Trigger scan
var scanTarget = new ScanTarget(
runtimeEvent.Workload.ImageRef,
imageDigest);
var metadata = new Dictionary
{
["stellaops:trigger"] = "drift",
["stellaops:drift.eventId"] = runtimeEvent.EventId,
["stellaops:drift.tenant"] = runtimeEvent.Tenant,
["stellaops:drift.node"] = runtimeEvent.Node
};
if (runtimeEvent.Delta?.BaselineImageDigest is { } baseline)
{
metadata["stellaops:drift.baselineDigest"] = baseline;
}
if (runtimeEvent.Delta?.ChangedFiles is { Count: > 0 } changedFiles)
{
metadata["stellaops:drift.changedFilesCount"] = changedFiles.Count.ToString();
}
if (runtimeEvent.Delta?.NewBinaries is { Count: > 0 } newBinaries)
{
metadata["stellaops:drift.newBinariesCount"] = newBinaries.Count.ToString();
}
var submission = new ScanSubmission(
scanTarget.Normalize(),
Force: false,
ClientRequestId: $"drift:{runtimeEvent.EventId}",
Metadata: metadata);
try
{
var result = await _scanCoordinator.SubmitAsync(submission, cancellationToken).ConfigureAwait(false);
_recentTriggers[imageDigest] = now;
DeltaScanTriggered.Add(1);
triggered++;
_logger.LogInformation(
"Delta scan triggered for DRIFT event {EventId}: scanId={ScanId}, created={Created}",
runtimeEvent.EventId,
result.Snapshot.ScanId,
result.Created);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Failed to trigger delta scan for DRIFT event {EventId}, image {ImageDigest}",
runtimeEvent.EventId,
imageDigest);
DeltaScanSkipped.Add(1);
skipped++;
}
}
stopwatch.Stop();
DeltaScanLatencyMs.Record(stopwatch.Elapsed.TotalMilliseconds);
_logger.LogInformation(
"Delta scan processing complete: {DriftCount} DRIFT events, {Triggered} triggered, {Skipped} skipped, {Deduped} deduped",
driftEvents.Count,
triggered,
skipped,
deduped);
return new DeltaScanResult(driftEvents.Count, triggered, skipped, deduped);
}
private void CleanupDeduplicationCache(DateTimeOffset now, TimeSpan cooldownWindow)
{
var expiredKeys = _recentTriggers
.Where(kvp => now - kvp.Value > cooldownWindow * 2)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expiredKeys)
{
_recentTriggers.TryRemove(key, out _);
}
}
private static string? ExtractImageDigest(RuntimeEvent runtimeEvent)
{
// Prefer baseline digest from Delta for DRIFT events
var digest = runtimeEvent.Delta?.BaselineImageDigest?.Trim().ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(digest))
{
return digest;
}
// Fall back to extracting from ImageRef
var imageRef = runtimeEvent.Workload.ImageRef;
if (string.IsNullOrWhiteSpace(imageRef))
{
return null;
}
var trimmed = imageRef.Trim();
var atIndex = trimmed.LastIndexOf('@');
if (atIndex >= 0 && atIndex < trimmed.Length - 1)
{
var candidate = trimmed[(atIndex + 1)..].Trim().ToLowerInvariant();
if (candidate.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
{
return candidate;
}
}
return null;
}
}