Monitoring: Threshold-Debounce auf sliding-window (statt floor-to-hour)

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) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-05-01 10:11:52 +02:00
parent 96feb6a663
commit 389b878dbd
1 changed files with 5 additions and 3 deletions

View File

@ -155,14 +155,16 @@ export async function detectThresholds(): Promise<void> {
});
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;