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; } }