feat: notification cooldown dedup for events and problems (notif-31)
Store last notify time by dedup_key/fingerprint, gate dispatch before channels; config SAC_NOTIFY_*_COOLDOWN_SEC, migration 008. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""Notification cooldown / dedup (notif-31)."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models import Event
|
||||
from app.models.notification_cooldown import NotificationCooldown
|
||||
from app.services.notification_cooldown import (
|
||||
allow_notify,
|
||||
build_event_cooldown_key,
|
||||
build_problem_cooldown_key,
|
||||
should_notify_event,
|
||||
)
|
||||
from app.models import Problem
|
||||
|
||||
|
||||
def test_build_event_cooldown_key_prefers_dedup_key():
|
||||
event = Event(
|
||||
event_id="00000000-0000-4000-8000-000000000601",
|
||||
host_id=1,
|
||||
occurred_at=datetime(2026, 5, 29, 16, 0, tzinfo=timezone.utc),
|
||||
category="auth",
|
||||
type="rdp.login.failed",
|
||||
severity="warning",
|
||||
title="t",
|
||||
summary="s",
|
||||
dedup_key="rdp|host|failed|10.0.0.1|admin",
|
||||
payload={},
|
||||
)
|
||||
assert build_event_cooldown_key(event) == "event:rdp|host|failed|10.0.0.1|admin"
|
||||
|
||||
|
||||
def test_allow_notify_blocks_within_cooldown(db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_NOTIFY_COOLDOWN_ENABLED", "true")
|
||||
monkeypatch.setenv("SAC_NOTIFY_EVENT_COOLDOWN_SEC", "90")
|
||||
get_settings.cache_clear()
|
||||
|
||||
key = "event:test-key"
|
||||
past = datetime.now(timezone.utc) - timedelta(seconds=30)
|
||||
db_session.add(NotificationCooldown(cooldown_key=key, kind="event", last_notified_at=past))
|
||||
db_session.commit()
|
||||
|
||||
assert allow_notify(db_session, key=key, kind="event") is False
|
||||
|
||||
|
||||
def test_allow_notify_allows_after_cooldown(db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_NOTIFY_COOLDOWN_ENABLED", "true")
|
||||
monkeypatch.setenv("SAC_NOTIFY_EVENT_COOLDOWN_SEC", "60")
|
||||
get_settings.cache_clear()
|
||||
|
||||
key = "event:old"
|
||||
past = datetime.now(timezone.utc) - timedelta(seconds=120)
|
||||
db_session.add(NotificationCooldown(cooldown_key=key, kind="event", last_notified_at=past))
|
||||
db_session.commit()
|
||||
|
||||
assert allow_notify(db_session, key=key, kind="event") is True
|
||||
|
||||
|
||||
def test_should_notify_event_exempt_agent_test(db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_NOTIFY_EVENT_COOLDOWN_SEC", "90")
|
||||
get_settings.cache_clear()
|
||||
|
||||
event = Event(
|
||||
event_id="00000000-0000-4000-8000-000000000602",
|
||||
host_id=1,
|
||||
occurred_at=datetime(2026, 5, 29, 16, 0, tzinfo=timezone.utc),
|
||||
category="agent",
|
||||
type="agent.test",
|
||||
severity="info",
|
||||
title="t",
|
||||
summary="s",
|
||||
payload={},
|
||||
)
|
||||
assert should_notify_event(event, db_session) is True
|
||||
|
||||
|
||||
def test_build_problem_cooldown_key():
|
||||
problem = Problem(
|
||||
title="brute",
|
||||
summary="x",
|
||||
severity="high",
|
||||
status="open",
|
||||
fingerprint="host:1|rdp.login.failed|rule:brute",
|
||||
)
|
||||
assert build_problem_cooldown_key(problem).startswith("problem:")
|
||||
Reference in New Issue
Block a user