up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
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.Id,
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user