admin / Syanpse-Vanguard
public
Syanpse-Vanguard / synapse-vanguard-v3 / sv / suppressions.py
4392 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | """False-positive tuning (suppressions). An Admin or Analyst marks an event as a false positive; that creates a suppression matching some combination of alert name / host / source type / message substring. On the ingest path, an event that matches an enabled suppression is NOT escalated by the alert rules (it is still stored, just not tagged as an Alert / bumped in severity / used to fire a webhook). Cached in-process (short TTL) so the per-event check stays cheap.""" from __future__ import annotations import time import uuid from datetime import datetime, timezone from sv.config import DEFAULT_TENANT_ID from sv.storage.base import Query, StorageEngine _COLUMNS = ("id, tenant_id, alert_name, host, source_type, message_contains, " "note, created_by, enabled, created_ts, hits, last_hit_ts") _CACHE: dict[str, tuple[float, list[dict]]] = {} _TTL = 5.0 # match fields an analyst can key a suppression on (at least one required) MATCH_FIELDS = ("alert_name", "host", "source_type", "message_contains") def _now() -> datetime: return datetime.now(timezone.utc).replace(tzinfo=None) # --- CRUD ------------------------------------------------------------------- async def list_suppressions(engine: StorageEngine, tenant_id=DEFAULT_TENANT_ID) -> list[dict]: return await engine.fetchall(Query( f"SELECT {_COLUMNS} FROM suppressions WHERE tenant_id = ? ORDER BY created_ts DESC", [str(tenant_id)], )) async def create_suppression(engine: StorageEngine, fields: dict, created_by: str, tenant_id=DEFAULT_TENANT_ID) -> str: sid = str(uuid.uuid4()) vals = {f: (fields.get(f) or None) for f in MATCH_FIELDS} await engine.run(Query( "INSERT INTO suppressions (id, tenant_id, alert_name, host, source_type, " "message_contains, note, created_by, enabled, created_ts, hits) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, ?, 0)", [sid, str(tenant_id), vals["alert_name"], vals["host"], vals["source_type"], vals["message_contains"], (fields.get("note") or None), created_by, _now()], )) _CACHE.pop(str(tenant_id), None) return sid async def set_enabled(engine: StorageEngine, sid: str, enabled: bool, tenant_id=DEFAULT_TENANT_ID) -> None: await engine.run(Query( "UPDATE suppressions SET enabled = ? WHERE id = ? AND tenant_id = ?", [enabled, sid, str(tenant_id)], )) _CACHE.pop(str(tenant_id), None) async def delete_suppressions(engine: StorageEngine, ids: list[str], tenant_id=DEFAULT_TENANT_ID) -> int: if not ids: return 0 placeholders = ", ".join("?" for _ in ids) await engine.run(Query( f"DELETE FROM suppressions WHERE tenant_id = ? AND id IN ({placeholders})", [str(tenant_id), *ids], )) _CACHE.pop(str(tenant_id), None) return len(ids) # --- matching (hot path) ---------------------------------------------------- async def load_enabled(engine: StorageEngine, tenant_id: str) -> list[dict]: cached = _CACHE.get(tenant_id) if cached and cached[0] > time.monotonic(): return cached[1] rows = await engine.fetchall(Query( f"SELECT {_COLUMNS} FROM suppressions WHERE tenant_id = ? AND enabled = TRUE", [tenant_id], )) _CACHE[tenant_id] = (time.monotonic() + _TTL, rows) return rows def _one_matches(supp: dict, ev, alert_name: str) -> bool: if supp.get("alert_name") and supp["alert_name"] != alert_name: return False if supp.get("host") and supp["host"] != ev.host: return False if supp.get("source_type") and supp["source_type"] != ev.source_type: return False mc = supp.get("message_contains") if mc and mc.lower() not in (ev.message or "").lower(): return False return True def match_any(supps: list[dict], ev, alert_name: str) -> str | None: """Return the id of the first suppression that matches, else None.""" for supp in supps: if _one_matches(supp, ev, alert_name): return supp["id"] return None async def record_hits(engine: StorageEngine, counts: dict[str, int]) -> None: for sid, n in counts.items(): await engine.run(Query( "UPDATE suppressions SET hits = hits + ?, last_hit_ts = ? WHERE id = ?", [n, _now(), sid], )) |