From aedd35033213f826095ae761e829c548ab525d03 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 1 May 2026 10:11:52 +0200 Subject: [PATCH] Monitoring: Threshold-Debounce auf sliding-window (statt floor-to-hour) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: zweimal CRITICAL-Alert für dieselbe Brute-Force-Erkennung kam an. Ursache: detectThresholds() hat als Cutoff für den "existing"-Check floor(now, hour) genutzt. Bei Stundenwechsel resettete der Bucket und der nächste Cron-Lauf fand nichts mehr "in der aktuellen Stunde" → erzeugte zweites SUSPICIOUS-Event → zweite Mail. Fix: gleitendes 60min-Fenster (now - 60min). Pro IP gibt es jetzt zuverlässig max. 1 CRITICAL-Alert pro Stunde, unabhängig von der absoluten Uhrzeit. Live-verifiziert in DB: zwei Alerts kamen um 07:41 und 08:00 – genau das Pattern, das der Stunden-Reset erzeugt. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/src/services/securityAlert.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/services/securityAlert.service.ts b/backend/src/services/securityAlert.service.ts index 961302d7..f3a54991 100644 --- a/backend/src/services/securityAlert.service.ts +++ b/backend/src/services/securityAlert.service.ts @@ -155,14 +155,16 @@ export async function detectThresholds(): Promise { }); for (const g of grouped) { if ((g._count as number) < b.threshold) continue; - // Prüfen ob wir für diese (IP+Type+Stunde) schon einen CRITICAL emittiert haben - const hourBucket = new Date(now.getTime() - (now.getTime() % (60 * 60 * 1000))); + // Debounce: pro IP max. 1 SUSPICIOUS-Alert pro 60min (sliding window). + // Vorher: floor(now, hour) → resettete bei Stundenwechsel und produzierte + // doppelte Alerts (Bug aus Runde 10). + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); const existing = await prisma.securityEvent.findFirst({ where: { type: 'SUSPICIOUS', severity: 'CRITICAL', ipAddress: g.ipAddress, - createdAt: { gte: hourBucket }, + createdAt: { gte: oneHourAgo }, }, }); if (existing) continue;