Catalyst / admin/Syanpse-Vanguard 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Syanpse-Vanguard

public
Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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],
        ))