From eeba3a704d29060783312cfc6bd7cbb1362161c6 Mon Sep 17 00:00:00 2001 From: PTah Date: Wed, 27 May 2026 12:21:03 +1000 Subject: [PATCH] feat: Telegram notifications from SAC on ingest Send Telegram alerts for high/critical events and new Problems. Configurable via TELEGRAM_* env vars; ingest never fails on send errors. --- backend/app/api/v1/events.py | 7 ++- backend/app/config.py | 4 ++ backend/app/services/problems.py | 10 ++-- backend/app/services/telegram_notify.py | 70 +++++++++++++++++++++++++ deploy/.env.example | 6 +++ deploy/env.native.example | 6 +++ docs/install-ubuntu-24.04-native.md | 1 + docs/operations-prod-status.md | 5 +- docs/work-plan.md | 2 +- 9 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 backend/app/services/telegram_notify.py diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index a7fa27e..3aa2b8d 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -15,6 +15,7 @@ from app.schemas.list_models import EventDetail, EventListResponse, EventSummary from app.services.ingest import ingest_event from app.services.problems import maybe_create_problem from app.services.schema_validate import validate_event_payload +from app.services.telegram_notify import notify_event, notify_problem router = APIRouter(prefix="/events", tags=["events"]) @@ -39,8 +40,12 @@ def post_event( event, created = ingest_event(db, payload) problem = None + problem_created = False if created: - problem = maybe_create_problem(db, event) + problem, problem_created = maybe_create_problem(db, event) + notify_event(event) + if problem is not None and problem_created: + notify_problem(problem, event) db.commit() settings = get_settings() diff --git a/backend/app/config.py b/backend/app/config.py index e0ccc78..7a13271 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -45,6 +45,10 @@ class Settings(BaseSettings): event_schema_path: str = str(SCHEMA_PATH) cors_origins: str = "*" + telegram_enabled: bool = False + telegram_bot_token: str = "" + telegram_chat_id: str = "" + telegram_min_severity: str = "high" @lru_cache diff --git a/backend/app/services/problems.py b/backend/app/services/problems.py index 0bf4906..aed1914 100644 --- a/backend/app/services/problems.py +++ b/backend/app/services/problems.py @@ -16,9 +16,9 @@ PROBLEM_TYPES = frozenset( HIGH_SEVERITIES = frozenset({"high", "critical"}) -def maybe_create_problem(db: Session, event: Event) -> Problem | None: +def maybe_create_problem(db: Session, event: Event) -> tuple[Problem | None, bool]: if event.severity not in HIGH_SEVERITIES and event.type not in PROBLEM_TYPES: - return None + return None, False rule_id = "high_severity" if event.severity in HIGH_SEVERITIES else f"type:{event.type}" @@ -34,7 +34,7 @@ def maybe_create_problem(db: Session, event: Event) -> Problem | None: .limit(1) ) if existing: - return existing + return existing, False # dedupe: one open problem per host+rule (append event link) open_problem = db.scalar( @@ -51,7 +51,7 @@ def maybe_create_problem(db: Session, event: Event) -> Problem | None: db.add(ProblemEvent(problem_id=open_problem.id, event_id=event.id)) open_problem.updated_at = event.received_at db.flush() - return open_problem + return open_problem, False problem = Problem( host_id=event.host_id, @@ -65,4 +65,4 @@ def maybe_create_problem(db: Session, event: Event) -> Problem | None: db.flush() db.add(ProblemEvent(problem_id=problem.id, event_id=event.id)) db.flush() - return problem + return problem, True diff --git a/backend/app/services/telegram_notify.py b/backend/app/services/telegram_notify.py new file mode 100644 index 0000000..1a9e8e5 --- /dev/null +++ b/backend/app/services/telegram_notify.py @@ -0,0 +1,70 @@ +import logging + +import httpx + +from app.config import get_settings +from app.models import Event, Problem + +logger = logging.getLogger(__name__) + +SEVERITY_ORDER = {"info": 10, "warning": 20, "high": 30, "critical": 40} + + +def _severity_value(value: str) -> int: + return SEVERITY_ORDER.get(value, 0) + + +def _is_enabled() -> bool: + settings = get_settings() + return bool( + settings.telegram_enabled + and settings.telegram_bot_token.strip() + and settings.telegram_chat_id.strip() + ) + + +def _send_text(message: str) -> None: + settings = get_settings() + if not _is_enabled(): + return + url = f"https://api.telegram.org/bot{settings.telegram_bot_token}/sendMessage" + payload = {"chat_id": settings.telegram_chat_id, "text": message, "disable_web_page_preview": True} + try: + with httpx.Client(timeout=8.0) as client: + response = client.post(url, json=payload) + response.raise_for_status() + except Exception: + logger.exception("telegram send failed") + + +def notify_event(event: Event) -> None: + settings = get_settings() + min_level = _severity_value(settings.telegram_min_severity) + if _severity_value(event.severity) < min_level: + return + host = event.host.hostname if event.host else "unknown" + message = ( + f"🚨 SAC событие\n" + f"Host: {host}\n" + f"Severity: {event.severity}\n" + f"Type: {event.type}\n" + f"Title: {event.title}\n" + f"Summary: {event.summary}" + ) + _send_text(message) + + +def notify_problem(problem: Problem, event: Event | None = None) -> None: + host = problem.host.hostname if problem.host else "unknown" + related = "" + if event is not None: + related = f"\nEvent: {event.type} ({event.severity})" + message = ( + f"🔥 SAC Problem opened\n" + f"Host: {host}\n" + f"Severity: {problem.severity}\n" + f"Rule: {problem.rule_id or '-'}\n" + f"Title: {problem.title}\n" + f"Summary: {problem.summary}{related}" + ) + _send_text(message) diff --git a/deploy/.env.example b/deploy/.env.example index 25b1c36..885e984 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -21,4 +21,10 @@ SAC_BOOTSTRAP_API_KEY=sac_replace_with_generated_key SAC_ADMIN_USERNAME=admin SAC_ADMIN_PASSWORD= +# Telegram notifications from SAC (phase 1C.4) +TELEGRAM_ENABLED=false +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= +TELEGRAM_MIN_SEVERITY=high + CORS_ORIGINS=* diff --git a/deploy/env.native.example b/deploy/env.native.example index cc94991..a6fe51b 100644 --- a/deploy/env.native.example +++ b/deploy/env.native.example @@ -17,4 +17,10 @@ EVENT_SCHEMA_PATH=/opt/security-alert-center/schemas/event-schema-v1.json SAC_ADMIN_USERNAME=admin SAC_ADMIN_PASSWORD= +# Telegram notifications from SAC (phase 1C.4) +TELEGRAM_ENABLED=false +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= +TELEGRAM_MIN_SEVERITY=high + CORS_ORIGINS=* diff --git a/docs/install-ubuntu-24.04-native.md b/docs/install-ubuntu-24.04-native.md index 29e2928..d6d2167 100644 --- a/docs/install-ubuntu-24.04-native.md +++ b/docs/install-ubuntu-24.04-native.md @@ -148,6 +148,7 @@ sudo chmod 600 /opt/security-alert-center/config/sac-api.env - `SAC_BOOTSTRAP_API_KEY` — `python3.12 -c "import secrets; print('sac_'+secrets.token_urlsafe(32))"` - `SAC_ADMIN_PASSWORD` — пароль входа в веб-UI (отдельно от API key агентов) - `EVENT_SCHEMA_PATH=/opt/security-alert-center/schemas/event-schema-v1.json` +- (опц.) Telegram из SAC: `TELEGRAM_ENABLED=true`, `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TELEGRAM_MIN_SEVERITY=high` --- diff --git a/docs/operations-prod-status.md b/docs/operations-prod-status.md index 653dcee..f3045ea 100644 --- a/docs/operations-prod-status.md +++ b/docs/operations-prod-status.md @@ -52,8 +52,9 @@ sudo /opt/sac-deploy.sh 1. **Деплой SAC** — `sudo /opt/sac-deploy.sh`, `alembic upgrade head` (миграция `002_problems`) 2. **ssh-monitor на ubabuba** — `update_ssh_monitor.sh` (10.10.36.9), маппинг `notify_or_sac` уже в `main` -3. **1C** — Telegram из SAC, dashboard -4. RDP-login-monitor — аналог UseSAC +3. **1C.4 Telegram из SAC (MVP backend)** — заполнить `TELEGRAM_*` в `config/sac-api.env`, затем `sudo systemctl restart sac-api` +4. **1C.5** — dashboard +5. RDP-login-monitor — аналог UseSAC --- diff --git a/docs/work-plan.md b/docs/work-plan.md index 17496a1..8d54048 100644 --- a/docs/work-plan.md +++ b/docs/work-plan.md @@ -52,7 +52,7 @@ | 1C.1 | Frontend Vue: Events, Hosts | ✅ prod | | 1C.2 | Auth JWT, admin bootstrap | ✅ prod | | 1C.3 | Problems (базовые правила) | ✅ API + UI (деплой ⏳) | -| 1C.4 | Telegram из SAC | ⏳ | +| 1C.4 | Telegram из SAC | 🔄 backend MVP (деплой ⏳) | | 1C.5 | Dashboard (3 виджета), SSE | ⏳ | **Выход:** тег `v0.1.0-mvp`.