"""SAC daily report aggregation (F-NOT-05 / notif-32).""" import uuid from datetime import datetime, timedelta, timezone from unittest.mock import patch from sqlalchemy import select from app.config import Settings from app.models import Event, Host from app.services import notify_dispatch from app.services.daily_report import ( _aggregate_ssh, generate_daily_report_for_host, host_has_report_today, run_daily_reports, ) from app.services.notification_policy import NotificationPolicyConfig from app.services.telegram_templates import format_event_telegram_html, sanitize_telegram_html def _ssh_host(db) -> Host: h = Host( hostname="pilot-ssh", os_family="linux", product="ssh-monitor", last_seen_at=datetime.now(timezone.utc), ) db.add(h) db.flush() return h def _cfg(**overrides) -> Settings: base = { "sac_daily_report_enabled": True, "sac_daily_report_hour": 9, "sac_daily_report_timezone": "UTC", "sac_daily_report_skip_if_agent_sent": False, "sac_daily_report_require_activity": False, } base.update(overrides) return Settings(**base) def test_aggregate_ssh_counts_types(db_session): now = datetime.now(timezone.utc) events = [ Event( event_id=str(uuid.uuid4()), host_id=1, occurred_at=now, category="auth", type="ssh.login.success", severity="info", title="ok", summary="", payload={}, ), Event( event_id=str(uuid.uuid4()), host_id=1, occurred_at=now, category="auth", type="ssh.login.failed", severity="warning", title="fail", summary="", details={"source_ip": "10.0.0.1"}, payload={}, ), Event( event_id=str(uuid.uuid4()), host_id=1, occurred_at=now, category="privilege", type="privilege.sudo.command", severity="warning", title="sudo", summary="", payload={}, ), ] stats = _aggregate_ssh(events) assert stats["successful_ssh"] == 1 assert stats["failed_ssh"] == 1 assert stats["sudo_commands"] == 1 assert "10.0.0.1" in stats["top_failed_ips"][0] def test_skip_when_agent_report_today(db_session): h = _ssh_host(db_session) cfg = _cfg(sac_daily_report_skip_if_agent_sent=True) now = datetime.now(timezone.utc) db_session.add( Event( event_id=str(uuid.uuid4()), host_id=h.id, occurred_at=now, received_at=now, category="report", type="report.daily.ssh", severity="info", title="agent", summary="from agent", details={"generated_by": "agent"}, payload={}, ) ) db_session.commit() assert host_has_report_today(db_session, h.id, "report.daily.ssh", cfg) is True res = generate_daily_report_for_host(db_session, h, cfg) assert res is not None assert res.created is False assert res.skipped_reason == "agent_report_exists_today" def test_generate_creates_report_and_notifies(db_session): h = _ssh_host(db_session) now = datetime.now(timezone.utc) db_session.add( Event( event_id=str(uuid.uuid4()), host_id=h.id, occurred_at=now - timedelta(hours=1), category="auth", type="ssh.login.failed", severity="warning", title="fail", summary="x", details={"source_ip": "1.2.3.4"}, payload={}, ) ) db_session.commit() cfg = _cfg() with patch("app.services.daily_report.notify_daily_report") as mock_notify: res = generate_daily_report_for_host(db_session, h, cfg) assert res is not None assert res.created is True assert res.report_type == "report.daily.ssh" mock_notify.assert_called_once() ev = db_session.scalar(select(Event).where(Event.type == "report.daily.ssh")) assert ev is not None assert ev.details.get("generated_by") == "sac" def test_generate_rdp_report_unified_format(db_session): h = Host( hostname="win-srv", display_name="RDCB", ipv4="10.0.0.5", os_family="windows", product="rdp-login-monitor", last_seen_at=datetime.now(timezone.utc), ) db_session.add(h) db_session.flush() now = datetime.now(timezone.utc) db_session.add( Event( event_id=str(uuid.uuid4()), host_id=h.id, occurred_at=now, category="auth", type="rdp.login.success", severity="info", title="ok", summary="", details={"user": "admin"}, payload={}, ) ) db_session.commit() cfg = _cfg() with patch("app.services.daily_report.notify_daily_report"): res = generate_daily_report_for_host(db_session, h, cfg) assert res and res.created ev = db_session.scalar(select(Event).where(Event.type == "report.daily.rdp")) body = ev.details.get("report_body", "") assert "ЕЖЕДНЕВНЫЙ ОТЧЕТ МОНИТОРИНГА WINDOWS" in body assert "RDCB (10.0.0.5)" in body assert "АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ" in body def test_run_daily_reports_respects_hour(db_session): h = _ssh_host(db_session) db_session.commit() cfg = _cfg(sac_daily_report_hour=23) early = datetime(2026, 5, 29, 8, 0, tzinfo=timezone.utc) with patch("app.services.daily_report._now_in_tz", return_value=early): out = run_daily_reports(db_session, cfg, force=False) assert out == [] def test_run_daily_reports_force_bypasses_hour(db_session): _ssh_host(db_session) db_session.commit() cfg = _cfg(sac_daily_report_hour=23) early = datetime(2026, 5, 29, 8, 0, tzinfo=timezone.utc) with patch("app.services.daily_report._now_in_tz", return_value=early): with patch("app.services.daily_report.generate_daily_report_for_host") as mock_gen: mock_gen.return_value = None run_daily_reports(db_session, cfg, force=True) mock_gen.assert_called() def test_notify_daily_report_bypasses_min_severity(): event = Event( event_id="00000000-0000-4000-8000-000000000601", host_id=1, occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc), category="report", type="report.daily.ssh", severity="info", title="report", summary="s", dedup_key="sac|1|report.daily.ssh|2026-05-29", payload={}, ) policy = NotificationPolicyConfig( min_severity="warning", use_telegram=True, use_webhook=False, use_email=False, use_mobile=False, source="db", ) with patch.object(notify_dispatch, "get_effective_notification_policy", return_value=policy): with patch.object(notify_dispatch, "should_notify_event", return_value=True): with patch.object(notify_dispatch, "telegram_notify") as mock_tg: notify_dispatch.notify_daily_report(event) mock_tg.notify_event.assert_called_once() def test_daily_report_telegram_html_uses_report_html(db_session): h = Host(hostname="h1", display_name="H1", os_family="linux") event = Event( event_id=str(uuid.uuid4()), host_id=1, host=h, occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc), category="report", type="report.daily.ssh", severity="info", title="Отчёт", summary="short", details={"report_html": "📊 OK
line"}, payload={}, ) assert format_event_telegram_html(event) == ( "📊 OK\nline\n📡 Оповещение: SAC (Security Alert Center)" ) def test_sanitize_telegram_html_strips_div_and_br(): raw = '
a
b
' assert sanitize_telegram_html(raw) == "a\nb"