Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.WebService/Services/DeltaScanRequestHandler.cs
master 4391f35d8a Refactor SurfaceCacheValidator to simplify oldest entry calculation
Add global using for Xunit in test project

Enhance ImportValidatorTests with async validation and quarantine checks

Implement FileSystemQuarantineServiceTests for quarantine functionality

Add integration tests for ImportValidator to check monotonicity

Create BundleVersionTests to validate version parsing and comparison logic

Implement VersionMonotonicityCheckerTests for monotonicity checks and activation logic
2025-12-16 10:44:00 +02:00

261 lines
9.5 KiB
C#

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;
/// <summary>
/// Handles delta scan requests triggered by runtime DRIFT events.
/// </summary>
internal interface IDeltaScanRequestHandler
{
/// <summary>
/// Processes a batch of runtime events and triggers scans for DRIFT events when enabled.
/// </summary>
Task<DeltaScanResult> ProcessAsync(
IReadOnlyList<RuntimeEventEnvelope> envelopes,
CancellationToken cancellationToken);
}
/// <summary>
/// Result of delta scan processing.
/// </summary>
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<long> DeltaScanTriggered = DeltaScanMeter.CreateCounter<long>(
"scanner_delta_scan_triggered_total",
unit: "1",
description: "Total delta scans triggered from runtime DRIFT events.");
private static readonly Counter<long> DeltaScanSkipped = DeltaScanMeter.CreateCounter<long>(
"scanner_delta_scan_skipped_total",
unit: "1",
description: "Total delta scans skipped (feature disabled, rate limited, or missing data).");
private static readonly Counter<long> DeltaScanDeduped = DeltaScanMeter.CreateCounter<long>(
"scanner_delta_scan_deduped_total",
unit: "1",
description: "Total delta scans deduplicated within cooldown window.");
private static readonly Histogram<double> DeltaScanLatencyMs = DeltaScanMeter.CreateHistogram<double>(
"scanner_delta_scan_latency_ms",
unit: "ms",
description: "Latency for delta scan trigger processing.");
// Deduplication cache: imageDigest -> last trigger time
private readonly ConcurrentDictionary<string, DateTimeOffset> _recentTriggers = new(StringComparer.OrdinalIgnoreCase);
private readonly IScanCoordinator _scanCoordinator;
private readonly IOptionsMonitor<ScannerWebServiceOptions> _optionsMonitor;
private readonly TimeProvider _timeProvider;
private readonly ILogger<DeltaScanRequestHandler> _logger;
public DeltaScanRequestHandler(
IScanCoordinator scanCoordinator,
IOptionsMonitor<ScannerWebServiceOptions> optionsMonitor,
TimeProvider timeProvider,
ILogger<DeltaScanRequestHandler> 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<DeltaScanResult> ProcessAsync(
IReadOnlyList<RuntimeEventEnvelope> 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<string, string>
{
["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;
}
}