diff --git a/backend/app/services/notify_dispatch.py b/backend/app/services/notify_dispatch.py index 8d03c4e..b13cbe0 100644 --- a/backend/app/services/notify_dispatch.py +++ b/backend/app/services/notify_dispatch.py @@ -15,6 +15,14 @@ from app.services.notification_severity import severity_meets_minimum logger = logging.getLogger(__name__) LIFECYCLE_EVENT_TYPE = "agent.lifecycle" +PRIVILEGE_SUDO_TYPE = "privilege.sudo.command" +SUDO_MAINTENANCE_MARKERS = ( + "update_ssh_monitor.sh", + "update_via_sac", + "update_script.log", + "/opt/scripts/update", + "agent-update-in-progress", +) DAILY_REPORT_EVENT_TYPES = frozenset({"report.daily.ssh", "report.daily.rdp"}) AUTH_LOGIN_SUCCESS_TYPES = frozenset({"rdp.login.success", "ssh.login.success"}) RDG_CONNECTION_TYPES = frozenset({ @@ -67,6 +75,8 @@ def notify_event(event: Event, *, db: Session | None = None) -> None: # Heartbeat — только для UI/статуса хоста, не для Telegram/email/push. if event.type == HEARTBEAT_TYPE: return + if _should_suppress_sudo_notify(event, db=db): + return if _skip_notifications_for_hidden_event(event, db): return policy = get_effective_notification_policy(db) @@ -112,7 +122,7 @@ def notify_daily_report(event: Event, *, db: Session | None = None) -> None: def _host_sac_update_running(event: Event, db: Session | None) -> bool: - """Пока SAC выполняет agent-update на хосте — не слать lifecycle в Telegram.""" + """Пока SAC выполняет agent-update на хосте — не слать шумные TG.""" if db is None or not event.host_id: return False host = db.get(Host, event.host_id) @@ -120,7 +130,8 @@ def _host_sac_update_running(event: Event, db: Session | None) -> bool: return False if (host.agent_update_state or "").strip().lower() == "running": logger.info( - "notify lifecycle skipped host update running host_id=%s event_id=%s", + "notify skipped (host update running) type=%s host_id=%s event_id=%s", + event.type, host.id, event.event_id, ) @@ -128,6 +139,30 @@ def _host_sac_update_running(event: Event, db: Session | None) -> bool: return False +def _sudo_event_is_agent_maintenance(event: Event) -> bool: + details = event.details if isinstance(event.details, dict) else {} + cmd = str(details.get("command") or "").strip() + if not cmd: + cmd = str(event.summary or "").strip() + text = cmd.casefold() + return any(marker in text for marker in SUDO_MAINTENANCE_MARKERS) + + +def _should_suppress_sudo_notify(event: Event, *, db: Session | None) -> bool: + if event.type != PRIVILEGE_SUDO_TYPE: + return False + if _host_sac_update_running(event, db): + return True + if _sudo_event_is_agent_maintenance(event): + logger.info( + "notify sudo skipped maintenance command event_id=%s host_id=%s", + event.event_id, + event.host_id, + ) + return True + return False + + def _lifecycle_suppress_notifications(event: Event, *, db: Session | None = None) -> bool: """Не слать TG при штатном SAC/cron update (lifecycle с trigger deploy_recycle).""" if _host_sac_update_running(event, db): diff --git a/backend/app/services/ssh_connect.py b/backend/app/services/ssh_connect.py index 617bc49..ffbe2bb 100644 --- a/backend/app/services/ssh_connect.py +++ b/backend/app/services/ssh_connect.py @@ -9,6 +9,7 @@ from dataclasses import dataclass from app.config import get_settings from app.models import Host +SSH_MONITOR_UPDATE_STATE_FILE = "/var/lib/ssh-monitor/agent-update-in-progress" SSH_MONITOR_UPDATE_LOG_PATH = "/var/log/update_script.log" SSH_MONITOR_UPDATE_SCRIPT = "/opt/scripts/update_ssh_monitor.sh" SSH_MONITOR_UPDATE_DIR = "/opt/scripts/update" @@ -454,6 +455,15 @@ def _ssh_monitor_bootstrap_command(repo_url: str, *, git_branch: str = "main") - ) +def _ssh_monitor_mark_update_begin_prefix() -> str: + """State-file до updater: lifecycle/sudo на агенте глушатся раньше bootstrap.""" + return ( + f"mkdir -p /var/lib/ssh-monitor && " + f"date +%s > {SSH_MONITOR_UPDATE_STATE_FILE} && " + f"chmod 600 {SSH_MONITOR_UPDATE_STATE_FILE} 2>/dev/null; " + ) + + def run_ssh_monitor_update( *, target: str, @@ -498,7 +508,7 @@ def run_ssh_monitor_update( target=target, user=user, password=password, - remote_cmd=bootstrap_cmd, + remote_cmd=_ssh_monitor_mark_update_begin_prefix() + bootstrap_cmd, command_timeout_sec=900, need_root=True, login_shell=False, @@ -509,7 +519,7 @@ def run_ssh_monitor_update( target=target, user=user, password=password, - remote_cmd=update_cmd, + remote_cmd=_ssh_monitor_mark_update_begin_prefix() + update_cmd, command_timeout_sec=900, need_root=True, login_shell=False, diff --git a/backend/app/version.py b/backend/app/version.py index 6e2a940..41ec9ee 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.5.2" +APP_VERSION = "0.5.3" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/docs/ingest-mass-update-backlog.ru.md b/docs/ingest-mass-update-backlog.ru.md index 9d1e71e..77d5030 100644 --- a/docs/ingest-mass-update-backlog.ru.md +++ b/docs/ingest-mass-update-backlog.ru.md @@ -33,7 +33,8 @@ - [ ] При **shutdown** / `SIGTERM` не увеличивать `sac-fail.count` (или отдельный флаг «update in progress»). - [ ] После успешного heartbeat сбрасывать fail counter агрессивнее (уже частично есть). - [x] Watchdog: не слать Telegram при штатном restart во время SAC-update (state file `/var/lib/ssh-monitor/agent-update-in-progress` от updater) — **2.2.1-SAC** -- [x] Lifecycle stop/start при SAC-update: подавление на агенте + state file — **2.2.1-SAC** +- [x] Sudo bootstrap/update через SAC: не слать Telegram (`ssh_monitor_sudo_is_sac_maintenance`, state-file раньше) — **2.2.3-SAC** +- [x] SAC: suppress `privilege.sudo.command` при `agent_update_state=running` / maintenance command — **0.5.3** - [ ] Документировать рекомендуемый `SAC_TIMEOUT_SEC` при тяжёлом ingest (сейчас 45s). ---