Files
security-alert-center/backend/tests/test_daily_report.py
T

258 lines
8.0 KiB
Python

"""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,
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": "<b>📊 OK</b><br>line"},
payload={},
)
assert format_event_telegram_html(event) == (
"<b>📊 OK</b>\nline\n📡 Оповещение: SAC (Security Alert Center)"
)
def test_sanitize_telegram_html_strips_div_and_br():
raw = '<div class="agent-report">a<br>b</div>'
assert sanitize_telegram_html(raw) == "a\nb"