feat: SAC Telegram settings view, notify_problem severity gate, exclusive docs
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import auth, dashboards, events, health, hosts, problems, stream
|
||||
from app.api.v1 import auth, dashboards, events, health, hosts, problems, settings, stream
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router)
|
||||
@@ -9,4 +9,5 @@ api_router.include_router(events.router)
|
||||
api_router.include_router(hosts.router)
|
||||
api_router.include_router(problems.router)
|
||||
api_router.include_router(dashboards.router)
|
||||
api_router.include_router(settings.router)
|
||||
api_router.include_router(stream.router)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth.jwt_auth import get_current_user
|
||||
from app.config import get_settings
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
|
||||
def _mask_secret(value: str, *, visible_tail: int = 4) -> str | None:
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return None
|
||||
if len(text) <= visible_tail:
|
||||
return "*" * len(text)
|
||||
return f"{'*' * 8}…{text[-visible_tail:]}"
|
||||
|
||||
|
||||
class TelegramSettingsResponse(BaseModel):
|
||||
enabled: bool
|
||||
configured: bool
|
||||
chat_id_hint: str | None = None
|
||||
bot_token_hint: str | None = None
|
||||
min_severity: str
|
||||
source: str = Field(default="env", description="env until UI persistence (notif-12)")
|
||||
|
||||
|
||||
class NotificationSettingsResponse(BaseModel):
|
||||
telegram: TelegramSettingsResponse
|
||||
|
||||
|
||||
@router.get("/notifications", response_model=NotificationSettingsResponse)
|
||||
def get_notification_settings(
|
||||
_user: str = Depends(get_current_user),
|
||||
) -> NotificationSettingsResponse:
|
||||
settings = get_settings()
|
||||
token = settings.telegram_bot_token.strip()
|
||||
chat_id = settings.telegram_chat_id.strip()
|
||||
configured = bool(token and chat_id)
|
||||
return NotificationSettingsResponse(
|
||||
telegram=TelegramSettingsResponse(
|
||||
enabled=bool(settings.telegram_enabled and configured),
|
||||
configured=configured,
|
||||
chat_id_hint=_mask_secret(chat_id),
|
||||
bot_token_hint=_mask_secret(token),
|
||||
min_severity=settings.telegram_min_severity,
|
||||
source="env",
|
||||
)
|
||||
)
|
||||
@@ -37,10 +37,14 @@ def _send_text(message: str) -> None:
|
||||
logger.exception("telegram send failed")
|
||||
|
||||
|
||||
def notify_event(event: Event) -> None:
|
||||
def _should_notify_severity(severity: str) -> bool:
|
||||
settings = get_settings()
|
||||
min_level = _severity_value(settings.telegram_min_severity)
|
||||
if _severity_value(event.severity) < min_level:
|
||||
return _severity_value(severity) >= min_level
|
||||
|
||||
|
||||
def notify_event(event: Event) -> None:
|
||||
if not _should_notify_severity(event.severity):
|
||||
return
|
||||
host = event.host.hostname if event.host else "unknown"
|
||||
message = (
|
||||
@@ -55,6 +59,8 @@ def notify_event(event: Event) -> None:
|
||||
|
||||
|
||||
def notify_problem(problem: Problem, event: Event | None = None) -> None:
|
||||
if not _should_notify_severity(problem.severity):
|
||||
return
|
||||
host = problem.host.hostname if problem.host else "unknown"
|
||||
related = ""
|
||||
if event is not None:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""GET /api/v1/settings/notifications"""
|
||||
|
||||
from app.services.telegram_notify import get_settings
|
||||
|
||||
|
||||
def test_get_notification_settings_masks_secrets(jwt_headers, client, monkeypatch):
|
||||
monkeypatch.setenv("TELEGRAM_ENABLED", "true")
|
||||
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "1234567890:ABCDEFghij")
|
||||
monkeypatch.setenv("TELEGRAM_CHAT_ID", "2843230")
|
||||
monkeypatch.setenv("TELEGRAM_MIN_SEVERITY", "warning")
|
||||
get_settings.cache_clear()
|
||||
|
||||
response = client.get("/api/v1/settings/notifications", headers=jwt_headers)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["telegram"]["enabled"] is True
|
||||
assert body["telegram"]["configured"] is True
|
||||
assert body["telegram"]["min_severity"] == "warning"
|
||||
assert body["telegram"]["source"] == "env"
|
||||
assert body["telegram"]["bot_token_hint"].endswith("ghij")
|
||||
assert "1234567890" not in body["telegram"]["bot_token_hint"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for SAC Telegram notification helpers."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models import Event, Host, Problem
|
||||
from app.services import telegram_notify
|
||||
|
||||
|
||||
def test_notify_event_respects_min_severity(monkeypatch):
|
||||
sent: list[str] = []
|
||||
|
||||
def fake_send(message: str) -> None:
|
||||
sent.append(message)
|
||||
|
||||
monkeypatch.setenv("TELEGRAM_ENABLED", "true")
|
||||
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "test-token")
|
||||
monkeypatch.setenv("TELEGRAM_CHAT_ID", "123")
|
||||
monkeypatch.setenv("TELEGRAM_MIN_SEVERITY", "warning")
|
||||
telegram_notify.get_settings.cache_clear()
|
||||
|
||||
event = Event(
|
||||
event_id="00000000-0000-4000-8000-000000000001",
|
||||
host_id=1,
|
||||
occurred_at=datetime(2026, 5, 28, 12, 0, tzinfo=timezone.utc),
|
||||
category="auth",
|
||||
type="rdp.login.success",
|
||||
severity="info",
|
||||
title="ok",
|
||||
summary="info event",
|
||||
payload={},
|
||||
)
|
||||
|
||||
with patch.object(telegram_notify, "_send_text", side_effect=fake_send):
|
||||
telegram_notify.notify_event(event)
|
||||
assert sent == []
|
||||
|
||||
event.severity = "warning"
|
||||
with patch.object(telegram_notify, "_send_text", side_effect=fake_send):
|
||||
telegram_notify.notify_event(event)
|
||||
assert len(sent) == 1
|
||||
|
||||
|
||||
def test_notify_problem_respects_min_severity(monkeypatch):
|
||||
sent: list[str] = []
|
||||
|
||||
def fake_send(message: str) -> None:
|
||||
sent.append(message)
|
||||
|
||||
monkeypatch.setenv("TELEGRAM_ENABLED", "true")
|
||||
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "test-token")
|
||||
monkeypatch.setenv("TELEGRAM_CHAT_ID", "123")
|
||||
monkeypatch.setenv("TELEGRAM_MIN_SEVERITY", "high")
|
||||
telegram_notify.get_settings.cache_clear()
|
||||
|
||||
problem = Problem(
|
||||
title="brute",
|
||||
summary="burst",
|
||||
severity="warning",
|
||||
status="open",
|
||||
fingerprint="fp1",
|
||||
)
|
||||
|
||||
with patch.object(telegram_notify, "_send_text", side_effect=fake_send):
|
||||
telegram_notify.notify_problem(problem)
|
||||
assert sent == []
|
||||
|
||||
problem.severity = "high"
|
||||
with patch.object(telegram_notify, "_send_text", side_effect=fake_send):
|
||||
telegram_notify.notify_problem(problem)
|
||||
assert len(sent) == 1
|
||||
Reference in New Issue
Block a user