Compare commits

..

242 Commits

Author SHA1 Message Date
PapaTramp d95b93992d fix: parse Disc qwinsta rows so RDG flap auto-logoff works (0.5.14)
Empty SESSIONNAME broke session matching; auto-disconnect failed with 'No matching Windows session'.
2026-07-14 10:32:17 +10:00
PapaTramp d20517804d docs: rdp.session.logoff is workstation-only (agent 2.1.12-SAC) 2026-07-13 17:36:31 +10:00
PapaTramp 91bd6afd1f fix: start sac-api via /usr/bin/bash to avoid systemd 203/EXEC
ExecStart no longer relies on shebang when CRLF slips into deploy scripts.
sac-deploy strips CR from all deploy/*.sh and waits for active before health check.
2026-07-13 17:29:28 +10:00
PapaTramp dd2356cf04 fix: LF line endings in deploy shell scripts (systemd 203/EXEC)
CRLF in sac-api-start.sh shebang broke sac-api with status=203/EXEC on Linux.
Add .gitattributes for *.sh and strip CR in sac-deploy before chmod.
2026-07-13 17:23:58 +10:00
PapaTramp 3475c1811b fix: platform-specific placeholder in agent update log modal (v0.5.13)
Windows WinRM updates no longer show the Linux /var/log/update_script.log hint while output is still empty.
2026-07-13 17:18:49 +10:00
PapaTramp 91be282ec6 feat: close rdp.login.success on rdp.session.logoff (v0.5.12)
Ingest correlates direct RDP logoff with open login events on the same host and user. Terminate-session treats missing qwinsta as already logged off.
2026-07-13 17:10:24 +10:00
PapaTramp 310e7f699d fix: ignore stale SSH update job until running or session start (v0.5.10)
Modal finish only after sawRunning or started_at after sessionOpenedAt, so a previous success no longer closes the window instantly while SAC updater is still running on the host.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:05:56 +10:00
PapaTramp e7d5b1c60b fix: SSH update modal polls locally, collapsed log by default (v0.5.9)
Modal owns job poll and completion UI with countdown; log hidden until user clicks Показать лог; db.refresh on remote-job; cache-bust poll requests.
2026-07-08 14:48:31 +10:00
PapaTramp 69fac2f26e fix: live SSH update log re-renders in modal (v0.5.7)
Remove cached openLogs computed that blocked Vue updates to output/loading; preserve log tail on empty poll; auto-scroll log panel.
2026-07-08 14:41:14 +10:00
PapaTramp c222dd7d41 fix: SSH update log modal shows completion and full tail (v0.5.6)
Fetch final update_script.log tail after agent-update, update title/message on success, show log by default; auto-close after 30s unchanged.
2026-07-08 14:26:28 +10:00
PapaTramp 6565b7d679 docs: mark ssh-monitor 4.2 ingest backlog items done
Shutdown sac-fail.count suppress and SAC_TIMEOUT_SEC docs shipped in ssh-monitor 2.3.2-SAC.
2026-07-08 14:12:40 +10:00
PapaTramp b148b558c3 feat: ingest burst hardening — defer lifecycle/auth, workers, nginx (v0.5.5)
Defer notify_lifecycle and notify_auth_login after commit; SAC_UVICORN_WORKERS via sac-api-start.sh; nginx /api/v1/events timeout; mass-update confirm in Hosts UI.
2026-07-08 13:16:13 +10:00
PapaTramp 1ad01534f1 fix: remote job stuck after SSH update completes (v0.5.4)
Treat agent_update_state as source of truth for active job; poll log thread no longer overwrites finished status.
2026-07-08 12:30:14 +10:00
PapaTramp 253b80c500 fix: suppress sudo Telegram during SAC agent update (v0.5.3) 2026-07-08 12:23:29 +10:00
PapaTramp f569293d52 fix: hide update log when user clicks Скрыть лог 2026-07-08 12:07:19 +10:00
PapaTramp d47131cd9f feat: live update log in UI and suppress lifecycle Telegram during SAC update
Poll remote update_script.log while SSH update runs; skip lifecycle notify
when host agent_update_state is running (SAC v0.5.1).
2026-07-08 11:46:53 +10:00
PapaTramp 1639261cde docs: backlog for ingest resilience during mass agent updates
Operational and code TODOs from mass SSH-update incident; does not
block ssh-monitor phase 2.2.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 11:28:57 +10:00
PapaTramp fa1bd41c92 fix: sac-deploy validate sac-api.env without bash source
Preflight pydantic parse catches inline comments in env values; alembic
and stale remote_action reset use SAC_CONFIG_FILE. Document env format
and ssh_known_hosts in runbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 10:21:49 +10:00
PapaTramp 3cabf12f68 fix: SAC-first ssh-monitor update (UPDATE_VIA_SAC, bootstrap --deploy)
Pass UPDATE_VIA_SAC to the remote updater and run bootstrap with --deploy
so Linux hosts need only the SAC update button after initial config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 09:44:24 +10:00
PapaTramp 9bfc5e23f7 fix: harden push-github-snapshot.sh for macOS and secret scan
Strip BOM, skip self-sanitization, replace default DB password in mirror,
and simplify pre-push scan to private keys only.
2026-07-07 19:46:26 +10:00
PapaTramp 9e43de6fc1 chore: release SAC 0.5.0
Bump version, document security hardening in README, add github snapshot
push script with URL/login/IP sanitization for public mirror.
2026-07-07 19:44:08 +10:00
PapaTramp 80320ae698 fix: harden SAC security per audit (0.4.15)
Close audit findings: anti-spoof client IP for login rate limit, JWT/CORS
enforce on startup, SSH host-key verification and sudo via stdin, optional
WinRM HTTPS, SSE httpOnly cookie auth, ingest body limit, ILIKE escaping,
and HMAC API key hashing with legacy SHA-256 fallback.
2026-07-07 19:32:12 +10:00
PapaTramp 939fe122c5 fix: WinRM RDP bundle staging in %TEMP% (0.4.14)
Move zip download and extract to user TEMP to avoid ProgramData access denied on DCs. Improve WinRM ERROR line parsing and add optional SAC_AGENT_BUNDLE_BASE_URL for LAN clients.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 12:11:54 +10:00
PapaTramp c72e510fb0 fix: defer daily report push и увеличить pool БД (0.4.12)
FCM/каналы для report.daily.* после commit ingest (BackgroundTasks);
SAC_DB_POOL_SIZE=15, SAC_DB_MAX_OVERFLOW=25 для штурма 09:00.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 12:00:37 +10:00
PapaTramp 6c43b8637e fix: dedupe host_silence problems under concurrent scans (0.4.11)
Treat acknowledged as active, dedupe by hostname, add PostgreSQL advisory lock and a unique index. Migration closes existing duplicate open problems.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 09:05:28 +10:00
PapaTramp 3765d4d476 feat: RDP flap auto-disconnect and hide break button after reconnect (0.4.10)
Add optional auto logoff of stuck sessions on RDG flap and direct RDP failure. Hide the RDS break button on old flap 302 when the user later reconnects or the workstation session is closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 16:50:03 +10:00
PapaTramp 10ae670962 fix: prevent duplicate host_silence problems per host (0.4.8)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 09:12:10 +10:00
PapaTramp daeb7e965d fix: skip alerts for hidden event types, show them on host card (0.4.7)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 15:16:10 +10:00
PapaTramp 606bf53019 feat: close workstation login on RDG 303 disconnect (0.4.6)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 08:16:06 +10:00
PapaTramp fd1be5e7e4 feat: manual host add from Hosts list via WinRM/SSH (0.4.5)
Add host button probes Windows by hostname or Linux by IP, registers host in SAC and deploys agent in background.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 15:34:12 +10:00
PapaTramp 5a7909bb55 fix: clear stale remote actions on sac-api startup (0.4.4)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 10:44:53 +10:00
PapaTramp 86fd400f0b fix: reset stale remote agent updates after sac-api restart (0.4.3)
Clear zombie running jobs on deploy and add admin API to cancel a stuck job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 10:43:31 +10:00
PapaTramp dd2ae51b43 feat: parallel agent upgrades with per-host log panels (0.4.2)
Allow multiple remote update jobs at once; each upgrade gets its own
bottom-right log dock that auto-closes 30s after success.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 10:22:31 +10:00
PapaTramp 719d5c5719 feat: login IP whitelist, blocks UI and fail2ban sync (0.4.1)
Admin settings for web login whitelist, blocked IP list with unblock,
and optional fail2ban ignoreip sync for SSH jail.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 10:06:10 +10:00
PapaTramp bda02f67eb ui: grey out other upgrade arrows while host update runs (0.4.0)
Show inactive grey arrows on other hosts during a single remote agent update; bump SAC to 0.4.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 15:44:31 +10:00
PapaTramp 9053ef0588 feat: one-click agent upgrade from hosts list (0.3.9)
Add Omada-style git version arrow in the agent version column to trigger WinRM/SSH background updates, and soften host detail live refresh with debounced incremental event patches.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 15:32:09 +10:00
PapaTramp 0601aafd2d fix: hide ????????? after session terminate (0.3.8)
Persist session_terminated in event details, expose in API, and update Events/Dashboard UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:45:33 +10:00
PapaTramp 8b3e43f30d fix: event terminate-session 500 on rdp.login.success (0.3.7)
Use extract_event_actor_user instead of missing ORM field; map WinRM/SSH errors to HTTP 400.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:37:37 +10:00
PapaTramp 6330f31162 docs: sync README version lines with APP_VERSION 0.3.6
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:14:43 +10:00
PapaTramp b47cbf03c5 ui: remove duplicate SAC version from overview page
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 16:35:45 +10:00
PapaTramp 407ef37004 fix: strict RDG 302/303 IP pairing keeps break button visible (0.3.6)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 16:30:16 +10:00
PapaTramp 64b942447b fix: reconcile daily report active users count and list (0.3.5)
Normalize empty-session placeholders; sync stats with report body after ingest.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 14:41:47 +10:00
PapaTramp 8b52c5fe3c fix: show RDS break only on active or flap 302 events (0.3.4)
Hide Obryv RDS on 303 and on 302 after normal session end; keep for open sessions and RDG flap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 20:15:47 +10:00
PapaTramp fbd213f323 chore: bump npm dompurify and vite for audit fixes (0.3.3)
dompurify 3.4.11, vite 6.4.3 — npm audit reports 0 vulnerabilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 20:13:43 +10:00
PapaTramp 1b96a3b2c7 fix: RDG event label and prefix type filter (0.3.2)
Use event 302/303 instead of Win302 in RDG titles. Match event types by prefix in events list (e.g. rdg.connection).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 20:07:22 +10:00
PapaTramp 24ca87a47a feat: RDG 302/303 display path labels and RDS session break (0.3.1)
Show RDS access via RDG-Comp or Haproxy-RDG-Comp, enable qwinsta/logoff on any RDG event with internal_ip.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 19:52:22 +10:00
PapaTramp 4eb41608ea chore: release SAC 0.3.0
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 16:07:54 +10:00
PapaTramp 6e3f0ede4d fix(backend): loginctl JSON parse and filter ephemeral SSH sessions (0.20.34)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 16:00:02 +10:00
PapaTramp fe818e9946 feat(frontend): terminate session button on Overview recent events (0.20.33)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 15:44:49 +10:00
PapaTramp 5e26834e16 feat(frontend): git agent versions plate on Hosts list (0.20.32)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 15:17:47 +10:00
PapaTramp 54c1d96933 fix(backend): non-login SSH and strict loginctl parse, SSH retry (0.20.31)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 10:54:56 +10:00
PapaTramp a51a585b14 fix(frontend): restore host config/sessions row column flex
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 10:31:48 +10:00
PapaTramp fc66b57e78 fix(frontend): match sessions panel height to agent config until loaded (0.20.30)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 10:31:38 +10:00
PapaTramp ad2425b0ff fix(frontend): place host sessions panel beside agent config (0.20.29)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 10:22:20 +10:00
PapaTramp d848d94604 feat: host session list/terminate and event session actions (0.20.28)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 10:13:42 +10:00
PapaTramp ca0255e13b fix(frontend): live refresh Events list via SSE (0.20.27)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 09:11:19 +10:00
PapaTramp d96d9d6333 fix(frontend): remove UTF-8 BOM from package.json — breaks vite build (0.20.26)
PostCSS config load failed with SyntaxError at position 0 on Linux deploy.
2026-06-21 20:50:13 +10:00
PapaTramp 7397ec30cd fix(winrm): resolve System32 path for cmd test on minimal PATH (0.20.25)
WinRM run_cmd(hostname) failed on hosts where System32 is not in PATH.
2026-06-21 20:49:01 +10:00
PapaTramp 75eec35365 fix(frontend): auto-close WinRM update success panel after 30s (0.20.24) 2026-06-21 20:41:57 +10:00
PapaTramp b3d28cd5d8 fix(frontend): auto-close WinRM update success panel after 30s (0.20.24)
Remote action log dock closes on successful fallback; no duplicate inline message.
2026-06-21 20:41:42 +10:00
PapaTramp 5bd1333928 fix(frontend): remove UTF-8 BOM from package.json — breaks vite build
PostCSS config load failed with SyntaxError at position 0 on Linux deploy.
2026-06-21 20:19:42 +10:00
PapaTramp bf7c83c408 feat: hide event types from SAC UI via Settings checkbox (0.20.23)
Per-type «Показывать в событиях» in severity settings; filters lists, dashboard, hosts, and problems.
2026-06-21 20:13:40 +10:00
PapaTramp 87ac1e09b0 fix: widen remote action log panel and wrap long WinRM output (0.20.22) 2026-06-21 11:30:37 +10:00
PapaTramp 8adaf3bfd3 fix: WinRM update panel no longer blocks SAC navigation (0.20.21) 2026-06-21 11:07:28 +10:00
PapaTramp 8a451a0c49 fix: alembic 022 down_revision must be 021 not 021_agent_git_release (0.20.20) 2026-06-21 11:04:07 +10:00
PapaTramp 5635be1322 feat: background remote agent updates survive SAC navigation (0.20.19) 2026-06-21 11:01:26 +10:00
PapaTramp d7bbcc5337 docs: WinRM RDP update flow and SAC 0.20.18 deployment notes
Обновлены README и руководства по fallback WinRM (git→zip→клиент), SAC_PUBLIC_URL и краткому 502 при деплое.
2026-06-20 20:03:57 +10:00
PapaTramp f3a8952947 fix: friendly 502 handling, 2 uvicorn workers, file bundle cache (0.20.18) 2026-06-20 20:01:46 +10:00
PapaTramp 4d9a9e7e2b fix: decode WinRM Cyrillic from CP866 and show deploy.log UTF-8 (0.20.17) 2026-06-20 19:58:54 +10:00
PapaTramp 9484160045 fix: UTF-8 BOM in RDP bundle zip for Russian Windows PowerShell (0.20.16) 2026-06-20 19:54:12 +10:00
PapaTramp 6acbfe22de fix: deliver RDP bundle via SAC HTTP zip download, not WinRM chunks (0.20.15) 2026-06-20 19:51:45 +10:00
PapaTramp ca90f5c296 fix: SAC pushes RDP bundle from git via WinRM, no client git (0.20.14) 2026-06-20 19:49:20 +10:00
PapaTramp 191fb4e2cf fix: WinRM RDP update only via git clone on client (0.20.13) 2026-06-20 19:46:22 +10:00
PapaTramp 8c1a8fddb1 fix: WinRM RDP update via NETLOGON Deploy, git fallback (0.20.12) 2026-06-20 19:43:34 +10:00
PapaTramp 05408e17c3 fix: WinRM RDP update via run_ps, strip CLIXML noise (0.20.11) 2026-06-20 19:41:05 +10:00
PapaTramp d3517a5706 fix: WinRM RDP update PowerShell quoting via EncodedCommand (0.20.10)
Broken -Command quotes caused ParserError on remote hosts; run deploy script through base64-encoded PowerShell.
2026-06-20 19:35:57 +10:00
PapaTramp 2c2f78eba9 fix: show host action log modal immediately on SSH/WinRM update (0.20.9)
Open progress dialog when agent update starts so long-running remote scripts do not look frozen.
2026-06-20 19:34:12 +10:00
PapaTramp ec5ec62240 fix: enforce SAC git URL when updating ssh-monitor agents (0.20.8)
Pass REPO_URL/GIT_BRANCH from agent settings over SSH, preflight git remotes on stale hosts, and stop using GitHub origin on routers.
2026-06-20 19:28:14 +10:00
PapaTramp 115289ef35 fix: show logoff actions in RDG qwinsta modal (0.20.7)
Put session table before raw output, add manual session ID logoff, improve qwinsta line parsing.
2026-06-20 19:20:52 +10:00
PapaTramp fc9d630263 docs: refresh README; allow monitor JWT for RDG qwinsta/logoff (0.20.6)
Update platform docs for current features; Seaca and web operators with
monitor role can trigger WinRM actions via SAC API.
2026-06-20 19:02:58 +10:00
PapaTramp 01d72d0f68 fix: show RDG flap badge on 302 and 303 events in SAC UI (0.20.5)
Seaca surfaced flap via Problems; the web UI only flagged 303 in DB and had
no visible label. Resolve flap pairs at read time and show badge plus qwinsta
on both sides of 302→303.
2026-06-20 17:27:41 +10:00
PapaTramp a35e50ccd1 feat: qwinsta/logoff buttons on events list and detail (0.20.4)
RDG flap actions were only on the dashboard; add shared composable and
modal so operators can reset stuck sessions from События and event detail.
2026-06-20 17:24:16 +10:00
PapaTramp 34a1670ea9 fix: nginx proxy timeout for long SSH/WinRM agent actions (0.20.3)
Raise proxy_read_timeout to 960s for agent-update and related API paths so bootstrap git clone does not hit 504 from nginx.
2026-06-20 17:12:31 +10:00
PapaTramp c21043d845 feat: bootstrap ssh-monitor updater when missing on host (0.20.2)
SAC now clones the ssh-monitor repo and installs update_ssh_monitor.sh before running the update when the remote updater script is absent.
2026-06-20 17:01:06 +10:00
PapaTramp 07b97bd111 fix: inline git test feedback and use form URLs (0.20.1)
Show git check result next to the button, scroll into view, and test unsaved repo URLs from the form.
2026-06-20 16:29:48 +10:00
PapaTramp f411d8f070 feat: git-based agent version reference and unified outdated badge (0.20.0)
SAC reads latest RDP/ssh-monitor versions from configured git repos for outdated detection, update targets, and settings test-git.
2026-06-20 16:25:53 +10:00
PapaTramp d820d941c1 fix: hide SSH/WinRM test feedback after 30s (0.12.1)
WinRM success message now auto-hides like SSH; both use 30 second timeout instead of 60.
2026-06-20 15:57:15 +10:00
PapaTramp cd2f27792d feat: agent updates from SAC, poll config, SSH/WinRM fallback (0.12.0)
Add agent update settings, pending self-update via poll, desired config per host, and automatic or manual SSH/WinRM fallback when agents do not report agent.update events.
2026-06-20 15:52:10 +10:00
PapaTramp 75f4c475df feat: RDG qwinsta/logoff via WinRM on client workstation (0.11.6)
SAC resolves internal_ip to a registered Windows host and runs qwinsta/logoff synchronously over WinRM instead of queueing commands to the gateway agent.
2026-06-20 15:36:12 +10:00
PapaTramp 2eb06acb5b feat: SSH card UX, agent version sync, persistent ssh_admin_ok (0.11.5)
Silent SSH probe on host card open; manual test feedback auto-hides. Persist ssh_admin_ok in DB (migration 019). After agent update read version from host and refresh UI.
2026-06-20 01:01:56 +10:00
PapaTramp 6fea8262fb fix: skip invalid/unresolvable SSH targets before connect (0.11.2)
Do not use human display_name as SSH host; skip short names that do not resolve on SAC (ubabuba -> IPv4 only).
2026-06-20 00:39:29 +10:00
PapaTramp 2e839e0b16 fix: SSH sudo without TTY for non-root Linux admin (0.11.1)
Probe commands run without sudo; privileged update uses sudo -S. Add SSH connect and Linux admin API tests.
2026-06-20 00:36:12 +10:00
PapaTramp cc8f50de89 feat: Linux SSH admin and remote ssh-monitor update (0.11.0) 2026-06-20 00:25:35 +10:00
PapaTramp 279ea925a8 fix: WinRM try host.hostname first like Enter-PSSession (0.10.4) 2026-06-20 00:13:02 +10:00
PapaTramp c8e3eef9d7 fix: WinRM NTLM username normalize and hostname-first targets (0.10.3) 2026-06-20 00:10:52 +10:00
PapaTramp 2fe492b06b fix: WinRM read_timeout must exceed operation_timeout (0.10.2) 2026-06-20 00:06:26 +10:00
PapaTramp 154ba3efc2 feat: Windows admin in Settings UI and WinRM host test (0.10.1)
Store domain admin in ui_settings (DB overrides env); qwinsta/logoff use effective config.
Host card: POST /hosts/{id}/actions/winrm-test via pywinrm.
2026-06-20 00:01:17 +10:00
PapaTramp df4c93d243 chore: release 0.10.0 (agent control plane phase 1)
Minor release: RDG flap, qwinsta/logoff queue, dashboard UI, agent poll API.
2026-06-19 23:42:03 +10:00
PapaTramp db95be39f3 docs: agent poll contract for RDP 2.0.39-SAC 2026-06-19 23:38:02 +10:00
PapaTramp 2aec488226 feat: RDG session flap rule, qwinsta/logoff queue and dashboard UI (0.9.13)
Detect 302→303 within 1–10s, Problem with 30s dedup, rdg_flap flag.
Agent command poll API, admin qwinsta/logoff from Overview. Migration 016.
2026-06-19 23:34:01 +10:00
PapaTramp b328d32f97 docs: agent control plane concept (v0.9.12)
Утверждённая концепция v0.7: RDG flap, qwinsta/logoff, обновления
агентов и desired config. Только документация и bump версии — runtime
без изменений.
2026-06-19 23:25:54 +10:00
PapaTramp 308e075f35 merge: sync github roadmap with kalinamall 0.9.11 2026-06-19 22:34:14 +10:00
PapaTramp 63cc2954af Update roadmap.md 2026-06-19 14:18:58 +10:00
PapaTramp 45f45145f0 roadmap.md: small ToDo 2026-06-18 08:02:00 +10:00
PapaTramp 55f047cd35 fix: Seaca push uses same lifecycle text as Telegram (0.9.11)
Reuse telegram templates for FCM title/body so agent start/restart
reasons reach mobile without Seaca app changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:35:17 +10:00
PapaTramp 8a97bbfb43 merge: sync local backlog with kalinamall production URLs
Integrate diverged kalinamall/main; keep v0.7 agent-update docs and
kalinamall.ru paths in deploy/docs; mirror scripts remain removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:13:35 +10:00
PapaTramp 2069e57f14 docs: backlog for agent version compliance and remote updates
Add agent-update-backlog.md and roadmap v0.7 outline for SAC-driven
agent version tracking and remote update initiation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:07:12 +10:00
PapaTramp 6b288bfa49 chore(github): generic example.com in docs, remove mirror scripts
Replace sac.kalinamall.ru with sac.example.com in public docs/deploy.
Remove Push-Mirror and Rewrite-GitHostUrls scripts from repo.
Simplify workspace-three-repos.md for GitHub-only workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 11:57:02 +10:00
PapaTramp 6bff073108 chore(docs): sync repository URLs for kalinamall mirror 2026-06-16 11:41:07 +10:00
PapaTramp 06c6748274 fix(scripts): force-with-lease on mirror push
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 11:40:59 +10:00
PapaTramp eac1e0b59e fix(scripts): Push-Mirror ignores untracked files
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 11:40:15 +10:00
PapaTramp 36ccfc9471 chore(docs): GitHub URLs in README/docs, remove .cursor from repo
Replace git.kalinamall.ru links with github.com/PTah in public docs.
Add Rewrite-GitHostUrls.ps1 and Push-Mirror.ps1 for kalinamall mirror pushes.
Add .cursor/ to .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 11:37:20 +10:00
PapaTramp 5b2aeb0183 fix(frontend): убрать BOM из package.json — ломал vite build на SAC. 2026-06-13 16:36:34 +10:00
PapaTramp 3d36faa49d fix(mobile): повторный enroll и удаление устройств в UI (0.9.10).
При повторной привязке Seaca переиспользуется запись device_uuid вместо INSERT.
Добавлены POST /devices/{id}/revoke и DELETE для полного удаления записи в настройках SAC.
2026-06-13 16:36:07 +10:00
PapaTramp e022f2f213 docs: sac-api.kalinamall.ru и схема HAProxy для Seaca.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 12:01:06 +10:00
PapaTramp f0bd31c544 fix(nginx): отклонять чужие Host на SAC (default_server 444)
Только sac.kalinamall.ru; ext.kalinamall.ru и прочие имена на IP SAC не отдают UI. ssl_reject_handshake на :443.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 11:24:21 +10:00
PapaTramp 87549731c8 chore: note frontend version.ts in SAC version bump rule
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 10:35:18 +10:00
PapaTramp 7df718eed3 fix: 12h cooldown before reopening manually closed host_silence (0.9.9)
Track resolved_by on problems; suppress scan/ingest recreate after manual
resolve; reopen same problem after cooldown; auto-close via heartbeat unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 10:33:55 +10:00
PapaTramp befaf86bf4 feat: readable report and host detail layout in web UI (0.9.8)
Field grid for event/host cards, human-readable daily report labels,
and normalized plain-text report bodies.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 10:25:53 +10:00
PapaTramp fed21c536b fix: exclude resolved problems from new-in-24h metrics (0.9.7)
Dashboard and created_within_hours list only show open/acknowledged problems
created in the last 24 hours; closed items appear under resolved only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 10:24:12 +10:00
PapaTramp 3f671b9899 fix: FCM data-only push so Seaca controls sound (0.9.6)
Send title and body in the data payload instead of notification block so
onMessageReceived always runs and the app can honour silent mode in background.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 10:03:28 +10:00
PapaTramp 3f7095ed03 fix: host_silence proactive notify when event is None
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 09:26:03 +10:00
PapaTramp 37bffe0e3f fix: SAC version from /health in sidebar and dashboard (0.9.4)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:38:54 +10:00
PapaTramp 0a414a0904 chore: sync frontend version label to 0.9.3
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:37:09 +10:00
PapaTramp e38bd8a52a fix: FCM must not break ingest; add requests dependency (0.9.3)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:30:32 +10:00
PapaTramp f9baf96d88 fix: test push errors, skip heartbeat notify, stale 5h (0.9.2)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:25:16 +10:00
PapaTramp 5e9f0f8110 fix: show copyable enrollment code after issue in Settings
Исправлен v-if, скрывавший весь раздел при success; баннер с полным
кодом, копированием и sessionStorage до Скрыть. Версия 0.9.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 17:07:49 +10:00
PapaTramp e2b5fb7604 docs: Seaca mobile operator guide and FCM setup
Документация для админа и runbook: коды регистрации, устройства,
настройка SAC_FCM на сервере.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 15:34:34 +10:00
PapaTramp 563b836acc feat: Seaca mobile API, enrollment, FCM push and admin UI (0.9.0)
Adds mobile device registration by admin codes, refresh tokens, push channel
in notification policy, and Settings section for managing Seaca clients.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 13:19:14 +10:00
PapaTramp 86c602cf07 docs: add v0.6 Mobile (Seaca) to roadmap and README
Links the new Android client repo and documents server-side mobile API work.
2026-06-10 12:45:52 +10:00
PapaTramp 77e167b6e3 feat: add actor user column to events and dashboard tables
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 10:11:04 +10:00
PapaTramp f9d56621f6 feat: drill-down filters on overview and outdated agent version highlight
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 09:21:21 +10:00
PapaTramp e65daa8c3b chore: sync frontend version.ts with API 0.8.3
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 09:11:41 +10:00
PapaTramp aafb80fa31 feat: proactive host_silence scan for stale agent.heartbeat (0.8.3)
Background scan every 5 min opens rule:host_silence and notifies without
waiting for the next ingest event. CLI job + optional systemd timer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 09:07:32 +10:00
PapaTramp 2a0eb6e90c chore: exclude Cursor workspace file from git (local IDE only)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 14:41:03 +10:00
PapaTramp 459b2babd1 docs: split README (RU) and README_en (EN) for GitHub
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 14:38:53 +10:00
PapaTramp 95c5b9f25a fix(ui): table actions column layout and resolved label
Move flex off td.actions to inner wrapper; show Закрыта for resolved problems. SAC 0.8.2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 14:10:56 +10:00
PapaTramp 02200f1a80 docs: note Exchange WinRM strict mode in agent integration
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 11:07:46 +10:00
PapaTramp 502971367e feat(ui): live host row update via SSE without full reload
On new ingest, patch visible host from GET /hosts/{id}; SAC 0.8.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 10:47:21 +10:00
PapaTramp d9893497ce fix(ui): full-row host click, agent version label on host card
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 10:20:32 +10:00
PapaTramp adfc69c656 fix(ui): host name nav styled like sidebar, plain hostname column
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 10:10:10 +10:00
PapaTramp 7699d8690a fix: visible host card links in Hosts table; bump SAC to 0.8.0
Hostname and display name always link to /hosts/:id with sac-row-link styling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 10:00:11 +10:00
PapaTramp 54337a5917 feat: host inventory (agent.inventory), host detail page, UI columns
Store hardware/software snapshots on hosts; warn on hardware changes. Host card from Hosts list. Add agent version column to Events/Overview; rename Hosts table columns and sort by status.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 09:53:00 +10:00
PapaTramp 5d3cda2ab6 docs: expand README highlights (5140 admin share, agent versions)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 10:17:11 +10:00
PapaTramp 57793d24ba feat: SAC support smb.admin_share.access events from RDP agent
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 10:14:14 +10:00
PapaTramp d324e41eba docs: README highlights list monitored sources (SSH, RDP, WinRM, sudo)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:10:49 +10:00
PapaTramp 1580982d88 fix: rename README screenshots to bust stale CDN/browser cache
Same user-provided PNGs (no blur); new filenames avoid serving old
Gaussian-blurred assets cached at dashboard.png / events.png paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 14:18:07 +10:00
PapaTramp d8ba864e48 docs: drop redundant note about blurred screenshots in README
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 14:14:33 +10:00
PapaTramp 7ab497b300 docs: restore original README screenshots without extra blur
Use user-provided redacted images as-is for dashboard, events, and problems.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 14:13:47 +10:00
PapaTramp 2d39943020 docs: add sanitized UI screenshots to README (0.7.4)
Dashboard, events, and problems screenshots with hostnames and IPs blurred.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 14:11:14 +10:00
PapaTramp 79185d9186 fix: show RD Gateway session duration in Telegram when present
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 10:07:51 +10:00
PapaTramp 47d08b54d2 chore: sync Cursor rules from CursorRules
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 09:31:57 +10:00
PapaTramp b565a259ea chore: add Cursor rule for mandatory agent script version bumps
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 09:29:23 +10:00
PapaTramp 0d4a50cd35 fix: dedupe daily report Telegram in dual mode and show host in summary fallback
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 09:15:21 +10:00
PapaTramp 3053058cdf feat: SAC 0.7.4 sidebar system stats block with UI toggle
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 09:15:15 +10:00
PapaTramp cf5a6f995c docs: update README with current SAC and agent status
Reflect SAC 0.7.3 capabilities, security hardening highlights, and current RDP monitor integration state.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 13:25:16 +10:00
PapaTramp 141fc44099 feat: SAC 0.7.3 custom error pages 401/403/404/429
Centered logo pages with event explanation and allowed actions; router
and apiFetch redirect to them on auth, permission, not-found, and rate-limit errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 11:06:21 +10:00
PapaTramp d3a337992c feat: SAC 0.7.2 security hardening (rate limit, JWT DB check, DOMPurify)
Path traversal fix in SPA fallback, admin-only host delete, login rate limit
with 3 attempts and Telegram alert, JWT validated against active users in DB,
and DOMPurify for agent report HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 11:04:14 +10:00
PapaTramp 06a8ed8614 feat: events date filter and preserve list page in URL (SAC 0.7.1)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 10:32:24 +10:00
PapaTramp de29270a25 feat: SAC 0.7.0 display_name columns and event severity overrides
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 10:21:43 +10:00
PapaTramp 8de5d14cfb feat: SAC 0.6.0 user edit UI, login to dashboard, daily report timer sync
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 09:55:12 +10:00
PapaTramp 56c469ddbd feat: multi-user UI login with admin and monitor roles
Add sac_users table, JWT roles, settings/users API guarded for admin, Users page in UI, and sac_manage_user.py CLI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 09:27:14 +10:00
PapaTramp bc77f82307 feat: notify RD Gateway events in Telegram outside min_severity
Route rdg.connection.* through notify_rdg_connection and add disconnected type.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 08:14:37 +10:00
PapaTramp 7e13be3c38 fix: hide redundant RDP workstation line when it matches server host
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 18:55:11 +10:00
PapaTramp fbef1e2f4b feat: always send RDP/SSH login success to Telegram via SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 18:47:39 +10:00
PapaTramp 4baa2b426f feat: render full agent lifecycle text from notification_body in Telegram
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 18:11:17 +10:00
PapaTramp 9dacbd7944 fix: Telegram footer shows SAC as sender when SAC delivers the message
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 12:41:23 +10:00
PapaTramp 366a9c83f7 feat: always notify agent.lifecycle via SAC with telegram_via routing
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 12:25:48 +10:00
PapaTramp 3d4d1f3c76 feat: show notification source (agent vs SAC) in alerts and reports
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 11:51:53 +10:00
PapaTramp a45b641b8c feat: daily report agent version line and fix active users layout
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 11:28:30 +10:00
PapaTramp 799f8668e6 feat: Telegram templates for RDS shadow and WinRM session events
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 10:20:57 +10:00
PapaTramp 2501ecc4fe fix: normalize daily report layout for Telegram and UI
Ensure server line, compact spacing, and one user per row when ingesting
agent reports or sending SAC-generated daily reports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 09:52:09 +10:00
PapaTramp eba2f0d15c docs: agent daily report toggles and unified template versions
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:18:03 +10:00
PapaTramp 7ebbb9a9d1 feat: unify SSH and Windows daily report layout in SAC and UI
Agent-style report body for SAC aggregation; shared stats cards; RDP ban note in docs

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:03:29 +10:00
PapaTramp 11195ae64f feat: delete host from UI with events and problems cleanup
DELETE /api/v1/hosts/{id} (JWT); host reappears on next agent ingest

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:52:05 +10:00
PapaTramp 2670c8cb46 fix: sanitize daily report HTML for Telegram parse_mode
Strip div/br tags unsupported by Telegram API; reports still store UI HTML in details

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:35:04 +10:00
PapaTramp 0baeacf808 docs: mark notification epic complete in work-plan
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:31:18 +10:00
PapaTramp 42f31b3bb5 fix: dashboard and hosts count SSH and RDP daily reports
analytics-a4: daily_reports_24h uses report.daily.*; sac-deploy enables timers
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:31:09 +10:00
PapaTramp 415d863b3b feat: SAC daily reports from DB aggregation (notif-32)
- Aggregate report.daily.ssh/rdp from 24h ingest; job and systemd timer

- notify_daily_report bypasses NOTIFY_MIN_SEVERITY; ingest routes agent reports

- HTML templates, env SAC_DAILY_REPORT_*, docs and tests (56 cases)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:30:07 +10:00
PapaTramp 4167687dec feat: notification cooldown dedup for events and problems (notif-31)
Store last notify time by dedup_key/fingerprint, gate dispatch before
channels; config SAC_NOTIFY_*_COOLDOWN_SEC, migration 008.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:24:01 +10:00
PapaTramp 1a70980920 feat: HTML Telegram templates for RDP/SSH events (notif-30)
Agent-style messages from event details, parse_mode HTML, templates for
login/sudo/RDG and problems; unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:21:40 +10:00
PapaTramp ebb450be92 feat: global notification policy severity to channels (notif-22)
Singleton notification_policy table, NOTIFY_MIN_SEVERITY/CHANNELS env,
central dispatch gate, Settings policy UI, migration 007.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:19:26 +10:00
PapaTramp 9b177c145b feat: SMTP email notification channel with UI and ingest
Migration 006, email config DB/env, smtplib sender, settings API,
Settings UI section, and dispatch on event/problem ingest.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:12:55 +10:00
PapaTramp 20fa7e2c27 feat: webhook notification channel with UI and ingest dispatch
Add webhook config (DB/env), JSON POST on events/problems, settings API,
Settings UI section, and notify_dispatch for multi-channel ingest.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:09:40 +10:00
PapaTramp f7b5fff04e docs: E2E checklist for SAC Telegram severity (notif-02)
Add synthetic ingest curl steps, update prod next steps for migration 004.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:06:58 +10:00
PapaTramp 79a78dc7c9 feat: Telegram settings in DB with UI edit and test send
Store notification_channels (migration 004), effective config DB over env,
PUT/test API endpoints, Settings form, and pass db session into ingest notify.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:05:32 +10:00
PapaTramp e0ba384705 feat: SAC Telegram settings view, notify_problem severity gate, exclusive docs
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:58:08 +10:00
PapaTramp 117390cbca docs: add 2026-05-29 todo for SAC notifications and Settings UI
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 15:39:21 +10:00
PapaTramp a42a63b8fa docs: add analytics backlog for deferred SAC development
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 15:17:08 +10:00
PapaTramp daec333dbe feat: add SAC app icon to favicon, sidebar, and login page
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 14:39:57 +10:00
PapaTramp ac3c20b7db feat: persistent Zabbix-style sidebar on all pages, rename Dashboard to Обзор
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 13:54:41 +10:00
PapaTramp dce5b88b2f feat: move navigation to dashboard sidebar and bump SAC to 0.5.0
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 13:45:25 +10:00
PapaTramp 81cf9c46b3 feat: improve SAC UI layout, reports state, and hosts sorting
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 13:17:37 +10:00
PapaTramp d4bf6e536c feat: host display_name в ingest, поиск хостов, UI и docs
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 11:49:10 +10:00
PapaTramp 108e1756c4 feat: live последние события на Dashboard; retention, health, runbook
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 11:29:31 +10:00
PapaTramp 9ed0670863 docs: RDP SAC 1.2.12 null+JSON fix
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 10:44:23 +10:00
PapaTramp fcf3da223c test/docs: RDP agent.lifecycle ingest and 422 notes
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 10:34:23 +10:00
PapaTramp f322a46f98 docs: RDP SAC 409 spool loop fix 1.2.10-SAC
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 10:27:25 +10:00
PapaTramp a218ff2581 docs: RDP SAC spool 422 and 1.2.9-SAC notes
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 10:20:31 +10:00
PapaTramp 98055dfdf8 docs: RDP SAC UTF-8 fix note for report.daily.rdp mojibake
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 10:05:49 +10:00
PapaTramp c8f357007f feat: dashboard top hosts/types, problems 24h, drill-down (d2-2)
- Extend /dashboards/summary; Events filters from query (hostname, severity)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 10:01:07 +10:00
PapaTramp 249894d8e1 feat: UI Problems list filters, detail card and event timeline (d2-1)
- Filters status/severity/hostname; /problems/:id with Ack/Resolve and timeline

- Dashboard drill-down to open problems

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:55:28 +10:00
PapaTramp 64b5ef297a feat: problem rules v1 brute-force, privilege spike, host silence (d1-3)
- Threshold rules on ingest; heartbeat auto-resolves host silence

- Config thresholds in sac-api.env; unit tests

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:51:05 +10:00
PapaTramp 83f48dce97 fix: alembic 003 avoid :r bind param in fingerprint backfill SQL
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:47:35 +10:00
PapaTramp a4a224b284 feat: Problems correlation fingerprint, count, last_seen (d1-2)
- Migration 003; windowed host+type+rule correlation on ingest

- GET /problems filters; GET /problems/{id} with event timeline; ack/resolve 409 guard

- Tests and SAC_PROBLEM_CORRELATION_WINDOW_MINUTES config

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:46:12 +10:00
PapaTramp 8eb40cf75d docs: agent version bumps and deploy triggers (1.4)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:34:22 +10:00
PapaTramp 4d603637e5 fix(deploy): sync systemd unit, preflight DB check, health retry
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:28:01 +10:00
PapaTramp e0b066f5e5 fix(deploy): recover from diverging git after force-push on kalinamall
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:26:06 +10:00
PapaTramp 25a485fabb docs: git policy — kalinamall only, push after each feature
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 09:23:10 +10:00
PapaTramp 2b76ae8497 feat: ingest HTTP 201/409 for event_id idempotency
- Validate UUID format; log created/duplicate/rejected

- Tests for 201, 409, 422; update agent-integration and work-plan

- Docs: neutral IDE wording (no product-specific editor names)
2026-05-28 09:13:12 +10:00
PapaTramp 5cf76f492c docs: add display_name tasks for RDP, ssh, and SAC hosts
Document ServerDisplayName parity with ssh SERVER_DISPLAY_NAME and
agent-display-* checklist items for SAC Hosts UI.
2026-05-27 17:02:23 +10:00
PapaTramp d1b89c75dd docs: add 2-day delivery and hourly checklist
Add execution checklist for next two development days and a follow-up week of testing in work-plan.md.
2026-05-27 16:12:56 +10:00
PapaTramp 721de21d5c docs: update agent integration readiness checklist
Mark completed SAC config, check-sac, and spool items for ssh and RDP agents.
2026-05-27 15:33:42 +10:00
PapaTramp 8995a34037 fix: faster sac-api restart (TimeoutStopSec, SSE cancel on shutdown) 2026-05-27 14:53:54 +10:00
PapaTramp 61c9f9b673 feat: reports page with formatted daily report cards 2026-05-27 14:15:15 +10:00
PapaTramp 04301ab359 feat: host heartbeat status and dashboard metrics (2.2)
Track agent.heartbeat and report.daily.ssh per host; show online/stale
on Hosts UI; dashboard counters and SAC_HEARTBEAT_STALE_MINUTES config.
2026-05-27 13:36:17 +10:00
PapaTramp 9ba3c4b63b docs: pilot 2.1 exclusive (Linux) runbook 2026-05-27 13:25:47 +10:00
PapaTramp b404747f8d test: version constants without DB import 2026-05-27 13:21:02 +10:00
PapaTramp c657a65970 feat: v0.2.0 branding, healthz version, SSE dashboard live
Centralize APP_VERSION 0.2.0; /health and /healthz return version;
startup log line; UI title Security Alert Center v.0.2.0;
SSE /api/v1/stream/events for live dashboard counters.
2026-05-27 13:20:39 +10:00
PapaTramp ea221db3c7 feat: Dashboard MVP with summary API and UI
Add GET /api/v1/dashboards/summary (24h events, open problems,
hosts, severity breakdown, recent events) and Vue /dashboard page.
2026-05-27 12:37:06 +10:00
PapaTramp eeba3a704d 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.
2026-05-27 12:21:03 +10:00
PapaTramp 5fce00faae feat: Problems API, ingest hook, and UI 2026-05-27 11:24:19 +10:00
PapaTramp b19cdf2b47 fix: SPA fallback для /events при F5 (index.html) 2026-05-27 11:01:47 +10:00
PapaTramp 947fc4ab98 docs: тестовый агент ubabuba; gitignore deploy helpers 2026-05-27 10:47:54 +10:00
PapaTramp 4265d58c4b chore: gitignore local SSH deploy helper scripts 2026-05-26 22:46:15 +10:00
PapaTramp a4bbc14448 docs: prod status, sac-deploy.sh; update install and deployment guides 2026-05-26 22:03:10 +10:00
PapaTramp bdfc7016e6 feat: phase 1C JWT UI, events/hosts API, Vue frontend 2026-05-26 21:46:34 +10:00
PapaTramp d8a8329397 docs: права /etc/ssl/sac — root:ssl-cert, www-data в группе 2026-05-26 21:37:56 +10:00
PapaTramp a9bea3d276 docs: PFX -legacy на Ubuntu 24.04, scp с другого Linux 2026-05-26 21:27:52 +10:00
PapaTramp 0c5d2537db docs: подробное руководство по копированию wildcard TLS на сервер 2026-05-26 21:15:38 +10:00
PapaTramp 21578b27c9 docs: nginx TLS с корпоративным wildcard VeriSign 2026-05-26 21:14:22 +10:00
PapaTramp 2c12f161a1 fix(nginx): только HTTP до certbot, без ssl без сертификата 2026-05-26 21:12:48 +10:00
PapaTramp 934a5a0c9c fix: загрузка sac-api.env через pydantic, не EnvironmentFile systemd 2026-05-26 21:07:51 +10:00
PapaTramp 539126b75e fix(deploy): конфиг в /opt/security-alert-center/config (sac:sac)
Убирает ошибку Permission denied при каталоге /etc root:root.
2026-05-26 20:59:44 +10:00
PapaTramp 38c137cbd3 fix(docs): chmod 750 на каталог /etc/security-alert-center для группы sac 2026-05-26 20:56:38 +10:00
PapaTramp 6d33669ac2 fix(docs): sac-api.env chmod 640 root:sac для пользователя sac 2026-05-26 20:53:10 +10:00
PapaTramp 69547d83bc docs: URL sac.kalinamall.ru во всех руководствах и примерах конфигов 2026-05-26 20:30:10 +10:00
PapaTramp 9203af90d5 docs: native Ubuntu install as primary; systemd and nginx
- install-ubuntu-24.04-native.md (production) and -docker.md (alternative)

- deploy/systemd/sac-api.service, nginx/sac.conf.example, env.native.example

- deployment, README, INDEX, work-plan, TZ updated
2026-05-26 20:28:05 +10:00
PapaTramp 69a232d08a feat: backend ingest API, Docker Compose, Ubuntu install guide
- FastAPI: POST /api/v1/events, GET /health, JSON Schema validation

- PostgreSQL models, Alembic migration, bootstrap API key

- deploy/docker-compose.yml, .env.example

- docs/install-ubuntu-24.04.md, updated work-plan (agents first)
2026-05-26 20:21:31 +10:00
PapaTramp bf1c1c9fe9 docs: обновить пример путей в workspace 2026-05-26 19:59:51 +10:00
PapaTramp 2f82c7c658 chore: пути репозиториев D:\Soft\Git 2026-05-26 19:58:59 +10:00
PapaTramp 4f3adc9429 docs: ТЗ v1.0, планы и схема SAC (фаза 0)
Security Alert Center — документация без кода приложения.

- TZ, архитектура, интеграция агентов (UseSAC)

- JSON Schema событий v1, deployment Ubuntu 24.04

- План работ, roadmap, multi-root workspace
2026-05-26 19:54:47 +10:00
52 changed files with 117 additions and 1002 deletions
+6 -6
View File
@@ -1,4 +1,4 @@
# Security Alert Center (SAC)
# Security Alert Center (SAC)
Центральная платформа: приём событий от агентов, корреляция, UI, оповещения, управление хостами.
@@ -8,12 +8,12 @@
| Репозиторий | Назначение |
|-------------|------------|
| [ssh-monitor](https://git.papatramp.ru/PapaTramp/ssh-monitor) | Linux-агент |
| [RDP-login-monitor](https://git.papatramp.ru/PapaTramp/RDP-login-monitor) | Windows-агент |
| [ssh-monitor](https://git.kalinamall.ru/PapaTramp/ssh-monitor) | Linux-агент |
| [RDP-login-monitor](https://git.kalinamall.ru/PapaTramp/RDP-login-monitor) | Windows-агент |
| **security-alert-center** | Сервер SAC (Ubuntu 24.04) |
| [seaca](https://git.papatramp.ru/PapaTramp/seaca) | Android-клиент |
| [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android-клиент |
**Версия:** `0.5.16` · **Деплой:** `sudo /opt/sac-deploy.sh`
**Версия:** `0.5.14` · **Деплой:** `sudo /opt/sac-deploy.sh`
## Возможности
@@ -40,7 +40,7 @@
## Быстрый старт
```bash
git clone https://git.papatramp.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
git clone https://git.kalinamall.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
# deploy/env.native.example → config/sac-api.env (JWT_SECRET, CORS_ORIGINS, SAC_SECURITY_ENFORCE)
cd /opt/security-alert-center/backend && python3 -m venv .venv
.venv/bin/pip install -r requirements.txt && .venv/bin/alembic upgrade head
+6 -6
View File
@@ -1,4 +1,4 @@
# Security Alert Center (SAC)
# Security Alert Center (SAC)
Self-hosted hub for security events from Linux and Windows agents: ingest, correlation, UI, notifications, host management.
@@ -8,12 +8,12 @@ Self-hosted hub for security events from Linux and Windows agents: ingest, corre
| Repository | Role |
|------------|------|
| [ssh-monitor](https://git.papatramp.ru/PapaTramp/ssh-monitor) | Linux agent |
| [RDP-login-monitor](https://git.papatramp.ru/PapaTramp/RDP-login-monitor) | Windows agent |
| [ssh-monitor](https://git.kalinamall.ru/PapaTramp/ssh-monitor) | Linux agent |
| [RDP-login-monitor](https://git.kalinamall.ru/PapaTramp/RDP-login-monitor) | Windows agent |
| **security-alert-center** | SAC server (Ubuntu 24.04) |
| [seaca](https://git.papatramp.ru/PapaTramp/seaca) | Android client |
| [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android client |
**Version:** `0.5.16` · **Deploy:** `sudo /opt/sac-deploy.sh`
**Version:** `0.5.14` · **Deploy:** `sudo /opt/sac-deploy.sh`
## Features
@@ -39,7 +39,7 @@ Self-hosted hub for security events from Linux and Windows agents: ingest, corre
## Quick start
```bash
git clone https://git.papatramp.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
git clone https://git.kalinamall.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
# deploy/env.native.example → config/sac-api.env
cd /opt/security-alert-center/backend && python3 -m venv .venv
.venv/bin/pip install -r requirements.txt && .venv/bin/alembic upgrade head
@@ -1,25 +0,0 @@
"""per-host management credentials (SSH / WinRM override)
Revision ID: 027_host_mgmt_credentials
Revises: 026_host_silence_dedupe
Create Date: 2026-07-14
"""
from alembic import op
import sqlalchemy as sa
revision = "027_host_mgmt_credentials"
down_revision = "026_host_silence_dedupe"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("hosts", sa.Column("mgmt_user", sa.String(length=256), nullable=True))
op.add_column("hosts", sa.Column("mgmt_password", sa.Text(), nullable=True))
def downgrade() -> None:
op.drop_column("hosts", "mgmt_password")
op.drop_column("hosts", "mgmt_user")
+4 -6
View File
@@ -28,12 +28,12 @@ from app.services.host_sessions import (
mark_event_session_terminated,
terminate_session_for_event,
)
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.linux_admin_settings import get_effective_linux_admin_config
from app.services.ssh_connect import (
HostNotLinuxError as SshHostNotLinuxError,
HostTargetMissingError as SshHostTargetMissingError,
)
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_config
from app.services.winrm_connect import HostNotWindowsError, HostTargetMissingError
from app.services.ingest import ingest_event
from app.services.event_summary import event_to_summary
@@ -284,10 +284,8 @@ def post_event_terminate_session(
if event_session_terminated(event, db=db):
raise HTTPException(status_code=409, detail="Session already terminated for this event")
if event.host is None:
raise HTTPException(status_code=400, detail="Event has no host")
linux_cfg = get_effective_linux_admin_for_host(db, event.host)
win_cfg = get_effective_win_admin_for_host(db, event.host)
linux_cfg = get_effective_linux_admin_config(db)
win_cfg = get_effective_win_admin_config(db)
sid = (body.session_id if body else None) or event_session_id(event)
try:
+16 -109
View File
@@ -46,12 +46,8 @@ from app.services.host_sessions import (
terminate_windows_session,
)
from app.services.host_delete import delete_host_and_related
from app.services.host_mgmt_credentials import (
get_host_mgmt_access_view,
upsert_host_mgmt_credentials,
)
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.linux_admin_settings import get_effective_linux_admin_config
from app.services.win_admin_settings import get_effective_win_admin_config
from app.services.winrm_connect import (
HostNotWindowsError,
HostTargetMissingError,
@@ -303,23 +299,6 @@ class HostAgentConfigResponse(BaseModel):
settings: dict[str, object]
class HostMgmtAccessResponse(BaseModel):
host_id: int
has_override: bool
user: str | None = None
password_set: bool = False
password_hint: str | None = None
effective_source: str
effective_configured: bool
effective_user: str | None = None
class HostMgmtAccessUpdate(BaseModel):
user: str | None = None
password: str | None = None
clear: bool = False
class HostRemoteActionStartResponse(BaseModel):
status: str
host_id: int
@@ -490,12 +469,9 @@ def test_host_winrm(
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_win_admin_for_host(db, host)
cfg = get_effective_win_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
try:
targets = iter_winrm_targets(host)
@@ -556,12 +532,9 @@ def test_host_ssh(
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_linux_admin_for_host(db, host)
cfg = get_effective_linux_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
response = _run_ssh_action_on_host(host, cfg, action=test_ssh_connection)
_set_ssh_admin_status(host, response.ok)
@@ -583,12 +556,9 @@ def update_host_agent_via_ssh(
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_linux_admin_for_host(db, host)
cfg = get_effective_linux_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
title = "Обновление ssh-monitor (SSH)"
try:
@@ -716,12 +686,9 @@ def list_host_sessions(
from app.services.winrm_connect import is_windows_host
if is_linux_host(host):
cfg = get_effective_linux_admin_for_host(db, host)
cfg = get_effective_linux_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
try:
sessions, result = list_linux_sessions(host, cfg)
except SshHostNotLinuxError as exc:
@@ -736,12 +703,9 @@ def list_host_sessions(
)
if is_windows_host(host):
cfg = get_effective_win_admin_for_host(db, host)
cfg = get_effective_win_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
try:
sessions, result = list_windows_sessions(host, cfg)
except HostNotWindowsError as exc:
@@ -779,12 +743,9 @@ def terminate_host_session(
raise HTTPException(status_code=422, detail="session_id is required")
if is_linux_host(host):
cfg = get_effective_linux_admin_for_host(db, host)
cfg = get_effective_linux_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
try:
result = terminate_linux_session(host, cfg, sid)
except SshHostNotLinuxError as exc:
@@ -800,12 +761,9 @@ def terminate_host_session(
)
if is_windows_host(host):
cfg = get_effective_win_admin_for_host(db, host)
cfg = get_effective_win_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
try:
result = terminate_windows_session(host, cfg, sid)
except HostNotWindowsError as exc:
@@ -845,57 +803,6 @@ def patch_host_agent_config(
)
@router.get("/{host_id}/access", response_model=HostMgmtAccessResponse)
def get_host_access(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostMgmtAccessResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
view = get_host_mgmt_access_view(db, host)
return HostMgmtAccessResponse(
host_id=view.host_id,
has_override=view.has_override,
user=view.user,
password_set=view.password_set,
password_hint=view.password_hint,
effective_source=view.effective_source,
effective_configured=view.effective_configured,
effective_user=view.effective_user,
)
@router.put("/{host_id}/access", response_model=HostMgmtAccessResponse)
def put_host_access(
host_id: int,
body: HostMgmtAccessUpdate,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostMgmtAccessResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
view = upsert_host_mgmt_credentials(
db,
host,
user=body.user,
password=body.password,
clear=body.clear,
)
return HostMgmtAccessResponse(
host_id=view.host_id,
has_override=view.has_override,
user=view.user,
password_set=view.password_set,
password_hint=view.password_hint,
effective_source=view.effective_source,
effective_configured=view.effective_configured,
effective_user=view.effective_user,
)
@router.get("/{host_id}/agent-config", response_model=HostAgentConfigResponse)
def get_host_agent_config(
host_id: int,
+3 -3
View File
@@ -1,4 +1,4 @@
import os
import os
from functools import lru_cache
from pathlib import Path
@@ -138,8 +138,8 @@ class Settings(BaseSettings):
sac_agent_min_rdp_version: str = ""
sac_agent_min_ssh_version: str = ""
sac_win_agent_update_script: str = ""
sac_agent_rdp_git_repo_url: str = "https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git"
sac_agent_ssh_git_repo_url: str = "https://git.papatramp.ru/PapaTramp/ssh-monitor.git"
sac_agent_rdp_git_repo_url: str = "https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git"
sac_agent_ssh_git_repo_url: str = "https://git.kalinamall.ru/PapaTramp/ssh-monitor.git"
sac_agent_git_branch: str = "main"
sac_agent_git_cache_dir: str = "/opt/security-alert-center/cache/agent-repos"
sac_agent_git_cache_ttl_minutes: int = 30
-3
View File
@@ -26,9 +26,6 @@ class Host(Base):
use_sac_mode: Mapped[str | None] = mapped_column(String(32))
ssh_admin_ok: Mapped[bool | None] = mapped_column(nullable=True)
ssh_admin_checked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# Optional override for SSH/WinRM (home PCs, unique local admin). Empty → global Settings.
mgmt_user: Mapped[str | None] = mapped_column(String(256))
mgmt_password: Mapped[str | None] = mapped_column(Text)
pending_agent_update: Mapped[bool] = mapped_column(default=False, server_default="false")
pending_update_requested_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
pending_update_target_version: Mapped[str | None] = mapped_column(String(64))
-1
View File
@@ -78,7 +78,6 @@ class EventSummary(BaseModel):
title: str
summary: str
actor_user: str | None = None
session_duration_sec: int | None = None
rdg_flap: bool = False
rdg_flap_pair_event_id: int | None = None
rdg_flap_qwinsta_event_id: int | None = None
+6 -17
View File
@@ -7,22 +7,17 @@ from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.config import get_settings
from app.models import AgentCommand, Host
from app.services.win_admin_settings import (
get_effective_win_admin_config,
get_effective_win_admin_for_host,
)
from app.services.win_admin_settings import get_effective_win_admin_config
def _win_admin_configured() -> bool:
return get_effective_win_admin_config().configured
def win_admin_run_as(db: Session | None = None, host: Host | None = None) -> dict[str, str] | None:
if host is not None and db is not None:
cfg = get_effective_win_admin_for_host(db, host)
else:
cfg = get_effective_win_admin_config(db)
def win_admin_run_as() -> dict[str, str] | None:
cfg = get_effective_win_admin_config()
if not cfg.configured:
return None
return {
@@ -31,13 +26,7 @@ def win_admin_run_as(db: Session | None = None, host: Host | None = None) -> dic
}
def command_to_dict(
cmd: AgentCommand,
*,
include_run_as: bool = False,
db: Session | None = None,
host: Host | None = None,
) -> dict:
def command_to_dict(cmd: AgentCommand, *, include_run_as: bool = False) -> dict:
out = {
"id": cmd.command_uuid,
"type": cmd.command_type,
@@ -49,7 +38,7 @@ def command_to_dict(
"completed_at": cmd.completed_at.isoformat() if cmd.completed_at else None,
}
if include_run_as and cmd.status == "pending":
run_as = win_admin_run_as(db, host)
run_as = win_admin_run_as()
if run_as:
out["run_as"] = run_as
return out
+1 -1
View File
@@ -27,5 +27,5 @@ def build_agent_poll_payload(db: Session, host: Host) -> dict[str, Any]:
"config_revision": int(host.agent_config_revision or 0),
"config": config_payload,
"update": update_block,
"commands": [command_to_dict(c, include_run_as=True, db=db, host=host) for c in pending],
"commands": [command_to_dict(c, include_run_as=True) for c in pending],
}
+4 -4
View File
@@ -20,7 +20,7 @@ from app.services.agent_version import (
host_version_outdated,
latest_agent_versions_by_product,
)
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.linux_admin_settings import get_effective_linux_admin_config
from app.services.ssh_connect import (
HostNotLinuxError,
HostTargetMissingError,
@@ -28,7 +28,7 @@ from app.services.ssh_connect import (
iter_ssh_targets,
run_ssh_monitor_update,
)
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_config
from app.services.winrm_connect import (
HostNotWindowsError,
HostTargetMissingError as WinRmHostTargetMissingError,
@@ -249,7 +249,7 @@ def execute_agent_update_fallback(db: Session, host: Host) -> AgentUpdateFallbac
product = (host.product or "").strip()
if product == PRODUCT_SSH or is_linux_host(host):
linux_cfg = get_effective_linux_admin_for_host(db, host)
linux_cfg = get_effective_linux_admin_config(db)
agent_cfg = get_effective_agent_update_config(db)
repo_url = (agent_cfg.ssh_git_repo_url or "").strip() or None
branch = (agent_cfg.git_branch or "main").strip() or "main"
@@ -260,7 +260,7 @@ def execute_agent_update_fallback(db: Session, host: Host) -> AgentUpdateFallbac
git_branch=branch,
)
elif product == PRODUCT_RDP or is_windows_host(host):
win_cfg = get_effective_win_admin_for_host(db, host)
win_cfg = get_effective_win_admin_config(db)
result = run_windows_agent_update_fallback(
host,
win_cfg,
-4
View File
@@ -6,7 +6,6 @@ from app.services.event_actor_user import extract_event_actor_user
from app.services.host_sessions import event_session_terminated
from app.services.rdg_display import build_rdg_display
from app.services.rdg_session_flap import resolve_rdg_flap_summary
from app.services.session_duration import extract_session_duration_sec
def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
@@ -45,9 +44,6 @@ def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
title=title,
summary=summary,
actor_user=extract_event_actor_user(event.type, event.details),
session_duration_sec=extract_session_duration_sec(
event.details if isinstance(event.details, dict) else None
),
rdg_flap=rdg_flap,
rdg_flap_pair_event_id=rdg_flap_pair_event_id,
rdg_flap_qwinsta_event_id=rdg_flap_qwinsta_event_id,
@@ -1,116 +0,0 @@
"""Per-host management credentials (override global Win/Linux admin)."""
from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy.orm import Session
from app.models.host import Host
from app.services.linux_admin_settings import (
LinuxAdminConfig,
get_effective_linux_admin_for_host,
)
from app.services.win_admin_settings import (
WinAdminConfig,
get_effective_win_admin_for_host,
normalize_win_admin_user,
)
def mask_secret(value: str | None) -> str | None:
if not value:
return None
text = value.strip()
if not text:
return None
if len(text) <= 4:
return "****"
return f"{text[:2]}{text[-2:]}"
@dataclass(frozen=True)
class HostMgmtAccessView:
host_id: int
has_override: bool
user: str | None
password_set: bool
password_hint: str | None
effective_source: str
effective_configured: bool
effective_user: str | None
def _effective_for_host(db: Session, host: Host) -> WinAdminConfig | LinuxAdminConfig:
os_family = (host.os_family or "").strip().lower()
if os_family == "windows" or (host.product or "").strip().lower() in {"rdp-login-monitor", "rdp"}:
return get_effective_win_admin_for_host(db, host)
if os_family == "linux" or (host.product or "").strip().lower() in {"ssh-monitor", "ssh"}:
return get_effective_linux_admin_for_host(db, host)
# Fallback: try host override first via win helper (same columns), then global win, then linux.
win_cfg = get_effective_win_admin_for_host(db, host)
if win_cfg.configured:
return win_cfg
return get_effective_linux_admin_for_host(db, host)
def get_host_mgmt_access_view(db: Session, host: Host) -> HostMgmtAccessView:
host_user = (host.mgmt_user or "").strip() or None
host_password = (host.mgmt_password or "").strip() or None
has_override = bool(host_user or host_password)
effective = _effective_for_host(db, host)
return HostMgmtAccessView(
host_id=host.id,
has_override=has_override,
user=host_user,
password_set=bool(host_password),
password_hint=mask_secret(host_password),
effective_source=effective.source,
effective_configured=effective.configured,
effective_user=effective.user or None,
)
def upsert_host_mgmt_credentials(
db: Session,
host: Host,
*,
user: str | None = None,
password: str | None = None,
clear: bool = False,
) -> HostMgmtAccessView:
"""Update per-host credentials.
- clear=True → wipe host override (use global Settings).
- user="" → clear username.
- password omitted (None) → keep existing password.
- password="" → clear password.
"""
if clear:
host.mgmt_user = None
host.mgmt_password = None
db.commit()
db.refresh(host)
return get_host_mgmt_access_view(db, host)
if user is not None:
cleaned = user.strip()
if not cleaned:
host.mgmt_user = None
else:
# Preserve DOMAIN\\user form for Windows; for Linux leave as-is after strip.
os_family = (host.os_family or "").strip().lower()
if os_family == "windows" or "\\" in cleaned or "@" in cleaned:
host.mgmt_user = normalize_win_admin_user(cleaned)
else:
host.mgmt_user = cleaned
if password is not None:
if not password.strip():
host.mgmt_password = None
else:
host.mgmt_password = password
db.commit()
db.refresh(host)
return get_host_mgmt_access_view(db, host)
+4 -4
View File
@@ -14,7 +14,7 @@ from app.database import SessionLocal
from app.models import Host
from app.services.agent_update import execute_agent_update_fallback
from app.services.agent_update_types import AgentUpdateFallbackResult
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.linux_admin_settings import get_effective_linux_admin_config
from app.services.agent_update_settings import get_effective_agent_update_config
from app.services.ssh_connect import (
iter_ssh_targets,
@@ -97,7 +97,7 @@ def _completion_message(result: SshCommandResult) -> str:
def _fetch_ssh_update_log_tail(db: Session, host: Host, *, lines: int = 200) -> str:
cfg = get_effective_linux_admin_for_host(db, host)
cfg = get_effective_linux_admin_config(db)
if not cfg.configured:
return ""
try:
@@ -191,7 +191,7 @@ def _poll_ssh_update_log_loop(
payload = dict(row.remote_action or {})
if (payload.get("status") or "").strip().lower() != "running":
continue
cfg = get_effective_linux_admin_for_host(session, row)
cfg = get_effective_linux_admin_config(session)
if not cfg.configured:
continue
try:
@@ -322,7 +322,7 @@ def start_host_remote_action(
def run_ssh_monitor_update_action(db: Session, host: Host) -> SshCommandResult:
cfg = get_effective_linux_admin_for_host(db, host)
cfg = get_effective_linux_admin_config(db)
if not cfg.configured:
return SshCommandResult(
ok=False,
-9
View File
@@ -128,15 +128,6 @@ def ingest_event(db: Session, payload: dict) -> tuple[Event, bool]:
raise
process_agent_update_ingest(db, host, payload.get("type", ""), details)
from app.services.rdg_workstation_session import (
enrich_empty_login_from_rdg_success,
enrich_workstation_login_user_from_rdg,
)
if event.host is None:
event.host = host
enrich_workstation_login_user_from_rdg(db, event)
enrich_empty_login_from_rdg_success(db, event)
close_workstation_session_for_rdg_end(db, event)
close_workstation_session_for_rdp_logoff(db, event)
return event, True
+2 -16
View File
@@ -1,4 +1,4 @@
"""Effective Linux SSH admin credentials (host → DB → env)."""
"""Effective Linux SSH admin credentials (DB overrides env)."""
from __future__ import annotations
@@ -7,7 +7,6 @@ from dataclasses import dataclass
from sqlalchemy.orm import Session
from app.config import get_settings
from app.models.host import Host
from app.models.ui_settings import UI_SETTINGS_ROW_ID, UiSettings
@@ -15,7 +14,7 @@ from app.models.ui_settings import UI_SETTINGS_ROW_ID, UiSettings
class LinuxAdminConfig:
user: str
password: str
source: str # host | env | db
source: str # env | db
@property
def configured(self) -> bool:
@@ -81,16 +80,3 @@ def upsert_linux_admin_settings(
db.commit()
db.refresh(row)
return get_effective_linux_admin_config(db)
def get_effective_linux_admin_for_host(db: Session, host: Host) -> LinuxAdminConfig:
"""Prefer per-host mgmt_* when both user and password are set; else global Settings/env."""
host_user = (host.mgmt_user or "").strip()
host_password = (host.mgmt_password or "").strip()
if host_user and host_password:
return LinuxAdminConfig(user=host_user, password=host_password, source="host")
global_cfg = get_effective_linux_admin_config(db)
if host_user and global_cfg.password.strip():
return LinuxAdminConfig(user=host_user, password=global_cfg.password, source="host")
return global_cfg
+5 -10
View File
@@ -12,7 +12,7 @@ from app.models import AgentCommand, Event, Host
from app.services.rdg_client_host import ClientWorkstationNotFoundError, resolve_client_workstation
from app.services.rdg_display import event_supports_rdg_client_qwinsta
from app.services.rdg_session_flap import event_internal_ip
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_config
from app.services.winrm_connect import (
WinRmCmdResult,
run_winrm_logoff,
@@ -21,17 +21,12 @@ from app.services.winrm_connect import (
)
def _require_win_admin(db: Session, host: Host | None = None):
if host is not None:
cfg = get_effective_win_admin_for_host(db, host)
else:
from app.services.win_admin_settings import get_effective_win_admin_config
def _require_win_admin(db: Session):
cfg = get_effective_win_admin_config(db)
if not cfg.configured:
raise HTTPException(
status_code=503,
detail="Windows admin is not configured (host override or Settings → Windows)",
detail="Windows domain admin is not configured (Settings or SAC_WIN_ADMIN_*)",
)
return cfg
@@ -98,8 +93,8 @@ def _persist_command(
def execute_qwinsta_via_winrm(db: Session, event: Event, *, requested_by: str) -> AgentCommand:
_require_rdg_client_qwinsta(db, event)
cfg = _require_win_admin(db)
client_host = _resolve_client(db, event)
cfg = _require_win_admin(db, client_host)
details = event.details if isinstance(event.details, dict) else {}
user = details.get("user")
@@ -141,8 +136,8 @@ def execute_logoff_via_winrm(
requested_by: str,
) -> AgentCommand:
_require_rdg_client_qwinsta(db, event)
cfg = _require_win_admin(db)
client_host = _resolve_client(db, event)
cfg = _require_win_admin(db, client_host)
details = event.details if isinstance(event.details, dict) else {}
user = details.get("user")
+2 -129
View File
@@ -2,8 +2,6 @@
from __future__ import annotations
from datetime import timedelta, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified
@@ -17,7 +15,6 @@ from app.services.host_sessions import (
from app.services.rdg_client_host import find_windows_host_by_ipv4
from app.services.rdg_session_flap import (
RDG_END_TYPES,
RDG_SUCCESS_TYPE,
_event_user,
event_internal_ip,
find_rdg_success_before_end,
@@ -25,22 +22,10 @@ from app.services.rdg_session_flap import (
SESSION_CLOSED_BY_RDG_AT_KEY = "session_closed_by_rdg_at"
SESSION_CLOSED_BY_RDG_EVENT_ID_KEY = "session_closed_by_rdg_event_id"
USER_ENRICHED_FROM_RDG_EVENT_ID_KEY = "user_enriched_from_rdg_event_id"
# RCM 1149 via RD Gateway often has empty Param1/Param2; RDG 302 has the account.
RDG_LOGIN_USER_ENRICH_WINDOW = timedelta(minutes=5)
WORKSTATION_LOGIN_TYPE = "rdp.login.success"
def _as_utc(dt):
if dt is None:
return None
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def normalize_sam_account(user: str) -> str:
text = (user or "").strip()
if "\\" in text:
@@ -56,115 +41,6 @@ def users_match_rdg(login_user: str, rdg_user: str) -> bool:
return bool(left and right and left == right)
def _login_user_missing(event: Event) -> bool:
details = _details_dict(event)
for key in ("user", "username"):
val = details.get(key)
if val is not None and str(val).strip() not in ("", "-"):
return False
return True
def _apply_rdg_user_to_login(login_event: Event, *, rdg_event: Event, rdg_user: str) -> None:
details = dict(_details_dict(login_event))
details["user"] = rdg_user
details[USER_ENRICHED_FROM_RDG_EVENT_ID_KEY] = rdg_event.id
login_event.details = details
flag_modified(login_event, "details")
summary = (login_event.summary or "").strip()
if summary.startswith("RCM 1149") and rdg_user not in summary:
rest = summary[len("RCM 1149") :].strip()
login_event.summary = f"RCM 1149 {rdg_user} {rest}".strip()
def find_rdg_success_for_workstation_login(db: Session, login_event: Event) -> Event | None:
"""Nearest RDG 302 for this workstation IP within the enrich window."""
if login_event.type != WORKSTATION_LOGIN_TYPE:
return None
host = login_event.host
if host is None or not (host.ipv4 or "").strip():
return None
workstation_ip = host.ipv4.strip()
login_at = _as_utc(login_event.occurred_at)
window_start = login_at - RDG_LOGIN_USER_ENRICH_WINDOW
window_end = login_at + RDG_LOGIN_USER_ENRICH_WINDOW
candidates = db.scalars(
select(Event)
.where(
Event.type == RDG_SUCCESS_TYPE,
Event.occurred_at >= window_start,
Event.occurred_at <= window_end,
Event.id != login_event.id,
)
.order_by(Event.occurred_at.desc())
).all()
best: Event | None = None
best_delta: timedelta | None = None
for rdg in candidates:
if event_internal_ip(rdg) != workstation_ip:
continue
if not _event_user(rdg):
continue
delta = abs(_as_utc(rdg.occurred_at) - login_at)
if best is None or best_delta is None or delta < best_delta:
best = rdg
best_delta = delta
return best
def enrich_workstation_login_user_from_rdg(db: Session, login_event: Event) -> Event | None:
"""Fill details.user when RCM 1149 EventLog left Param1/Param2 empty (seen on some Win10 Pro)."""
if login_event.type != WORKSTATION_LOGIN_TYPE:
return None
if not _login_user_missing(login_event):
return None
rdg = find_rdg_success_for_workstation_login(db, login_event)
if rdg is None:
return None
rdg_user = _event_user(rdg)
if not rdg_user:
return None
_apply_rdg_user_to_login(login_event, rdg_event=rdg, rdg_user=rdg_user)
return login_event
def enrich_empty_login_from_rdg_success(db: Session, rdg_success_event: Event) -> Event | None:
"""Backfill empty workstation 1149 when RDG 302 is ingested after it."""
if rdg_success_event.type != RDG_SUCCESS_TYPE:
return None
internal_ip = event_internal_ip(rdg_success_event)
rdg_user = _event_user(rdg_success_event)
if not internal_ip or not rdg_user:
return None
client_host = find_windows_host_by_ipv4(db, internal_ip)
if client_host is None:
return None
rdg_at = _as_utc(rdg_success_event.occurred_at)
window_start = rdg_at - RDG_LOGIN_USER_ENRICH_WINDOW
window_end = rdg_at + RDG_LOGIN_USER_ENRICH_WINDOW
candidates = db.scalars(
select(Event)
.where(
Event.host_id == client_host.id,
Event.type == WORKSTATION_LOGIN_TYPE,
Event.occurred_at >= window_start,
Event.occurred_at <= window_end,
Event.id != rdg_success_event.id,
)
.order_by(Event.occurred_at.desc())
).all()
for login in candidates:
if not _login_user_missing(login):
continue
_apply_rdg_user_to_login(login, rdg_event=rdg_success_event, rdg_user=rdg_user)
return login
return None
def event_closed_by_rdg(event: Event) -> bool:
details = _details_dict(event)
at = details.get(SESSION_CLOSED_BY_RDG_AT_KEY)
@@ -222,11 +98,8 @@ def find_workstation_login_for_rdg_end(db: Session, rdg_end_event: Event) -> Eve
for login in candidates:
if _login_already_closed(login):
continue
login_user = (_event_login_user(login) or "").strip()
if login_user in ("", "-"):
login_user = ""
# RCM 1149 may lack user (empty Param1); still close by workstation IP + open session.
if login_user and not users_match_rdg(login_user, rdg_user):
login_user = _event_login_user(login)
if not users_match_rdg(login_user, rdg_user):
continue
return login
return None
@@ -35,7 +35,7 @@ from app.services.rdg_workstation_session import (
users_match_rdg,
)
from app.services.rdp_flap_settings import get_effective_rdp_flap_settings
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_config
logger = logging.getLogger("sac.rdp_flap_auto_disconnect")
@@ -143,7 +143,7 @@ def _disconnect_on_workstation(
user: str,
login_event: Event | None,
) -> AutoDisconnectResult:
win_cfg = get_effective_win_admin_for_host(db, workstation)
win_cfg = get_effective_win_admin_config(db)
sessions, qwinsta = list_windows_sessions(workstation, win_cfg)
if qwinsta is None or not qwinsta.ok:
message = qwinsta.message if qwinsta else "qwinsta failed"
@@ -291,6 +291,14 @@ def maybe_auto_disconnect_stuck_rdp_session(db: Session, event: Event) -> AutoDi
if not get_effective_rdp_flap_settings(db).auto_disconnect:
return None
win_cfg = get_effective_win_admin_config(db)
if not win_cfg.configured:
logger.warning(
"auto rdp flap disconnect skipped: win admin not configured (event_id=%s)",
event.event_id,
)
return None
result = _auto_disconnect_rdg_flap(db, event)
if result is not None:
if result.ok:
-37
View File
@@ -1,37 +0,0 @@
"""Format and extract RDP/RDG session_duration_sec for UI."""
from __future__ import annotations
from typing import Any
def extract_session_duration_sec(details: dict[str, Any] | None) -> int | None:
if not isinstance(details, dict):
return None
raw = details.get("session_duration_sec")
if raw is None or raw == "":
return None
try:
value = int(raw)
except (TypeError, ValueError):
return None
if value < 0:
return None
return value
def format_session_duration(seconds: int) -> str:
"""
Human-readable session length for lists:
- under 24h → HH:MM:SS (05:05:24)
- 24h+ → Nд HH:MM:SS (1д 00:01:25)
"""
if seconds < 0:
return ""
days, rem = divmod(int(seconds), 86_400)
hours, rem = divmod(rem, 3600)
minutes, secs = divmod(rem, 60)
clock = f"{hours:02d}:{minutes:02d}:{secs:02d}"
if days:
return f"{days}д {clock}"
return clock
+2 -2
View File
@@ -1,4 +1,4 @@
"""SSH connectivity and remote commands for Linux hosts."""
"""SSH connectivity and remote commands for Linux hosts."""
from __future__ import annotations
@@ -14,7 +14,7 @@ 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"
SSH_MONITOR_REPO_NAME = "ssh-monitor"
SSH_MONITOR_UPDATE_REPO_URL = "https://git.papatramp.ru/PapaTramp/ssh-monitor.git"
SSH_MONITOR_UPDATE_REPO_URL = "https://git.kalinamall.ru/PapaTramp/ssh-monitor.git"
SSH_MONITOR_BINARY = "/usr/local/bin/ssh-monitor"
SSH_MONITOR_VERSION_CMD = (
f"grep -m1 '^SSH_MONITOR_VERSION=' {SSH_MONITOR_BINARY} 2>/dev/null "
+3 -5
View File
@@ -564,11 +564,9 @@ def _format_rdg_html(event: Event) -> str:
err = _detail(details, "gateway_error_code", "error_code", default="")
if err != "-":
msg += _line("⚠️", "Код ошибки", html_escape(err))
from app.services.session_duration import extract_session_duration_sec, format_session_duration
dur_sec = extract_session_duration_sec(details if isinstance(details, dict) else None)
if dur_sec is not None and dur_sec > 0:
msg += _line("⏱️", "Длительность", html_escape(format_session_duration(dur_sec)))
dur = _detail(details, "session_duration_sec", default="")
if dur not in ("-", "0", ""):
msg += _line("⏱️", "Длительность", f"{html_escape(dur)} с")
msg += _line("🕐", "Время", format_time(event.occurred_at))
win_id = _detail(details, "event_id_windows", default="")
if win_id != "-":
+2 -17
View File
@@ -1,4 +1,4 @@
"""Effective Windows domain admin credentials (host → DB → env)."""
"""Effective Windows domain admin credentials (DB overrides env)."""
from __future__ import annotations
@@ -7,7 +7,6 @@ from dataclasses import dataclass
from sqlalchemy.orm import Session
from app.config import get_settings
from app.models.host import Host
from app.models.ui_settings import UI_SETTINGS_ROW_ID, UiSettings
@@ -15,7 +14,7 @@ from app.models.ui_settings import UI_SETTINGS_ROW_ID, UiSettings
class WinAdminConfig:
user: str
password: str
source: str # host | env | db
source: str # env | db
@property
def configured(self) -> bool:
@@ -93,17 +92,3 @@ def upsert_win_admin_settings(
db.commit()
db.refresh(row)
return get_effective_win_admin_config(db)
def get_effective_win_admin_for_host(db: Session, host: Host) -> WinAdminConfig:
"""Prefer per-host mgmt_* when both user and password are set; else global Settings/env."""
host_user = normalize_win_admin_user((host.mgmt_user or "").strip())
host_password = (host.mgmt_password or "").strip()
if host_user and host_password:
return WinAdminConfig(user=host_user, password=host_password, source="host")
global_cfg = get_effective_win_admin_config(db)
# Allow host to override only the username, still using global password (rare).
if host_user and global_cfg.password.strip():
return WinAdminConfig(user=host_user, password=global_cfg.password, source="host")
return global_cfg
+2 -2
View File
@@ -1,4 +1,4 @@
"""WinRM connectivity test for Windows hosts using domain admin credentials."""
"""WinRM connectivity test for Windows hosts using domain admin credentials."""
from __future__ import annotations
@@ -487,7 +487,7 @@ def run_winrm_rdp_monitor_update(
)
effective_repo = (
repo_url or "https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git"
repo_url or "https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git"
).strip()
effective_branch = (git_branch or "main").strip() or "main"
if not effective_repo:
+1 -1
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center"
APP_VERSION = "0.5.17"
APP_VERSION = "0.5.14"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
+1 -1
View File
@@ -14,7 +14,7 @@ from app.services.agent_version import reference_agent_versions_by_product
def test_normalize_git_repo_url():
assert normalize_git_repo_url("git.papatramp.ru/PapaTramp/ssh-monitor").endswith(
assert normalize_git_repo_url("git.kalinamall.ru/PapaTramp/ssh-monitor").endswith(
"ssh-monitor.git"
)
assert normalize_git_repo_url("https://example.com/repo.git") == "https://example.com/repo.git"
@@ -1,64 +0,0 @@
"""Per-host management credential override."""
from app.models.host import Host
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.host_mgmt_credentials import get_host_mgmt_access_view, upsert_host_mgmt_credentials
def test_win_admin_for_host_prefers_override(db_session):
host = Host(
hostname="HOME-PC",
os_family="windows",
product="rdp-login-monitor",
mgmt_user=".\\Admin",
mgmt_password="local-secret",
)
db_session.add(host)
db_session.commit()
db_session.refresh(host)
cfg = get_effective_win_admin_for_host(db_session, host)
assert cfg.configured is True
assert cfg.source == "host"
assert cfg.user == ".\\Admin"
assert cfg.password == "local-secret"
def test_linux_admin_for_host_prefers_override(db_session):
host = Host(
hostname="homeserver",
os_family="linux",
product="ssh-monitor",
mgmt_user="deploy",
mgmt_password="ssh-pass",
)
db_session.add(host)
db_session.commit()
db_session.refresh(host)
cfg = get_effective_linux_admin_for_host(db_session, host)
assert cfg.source == "host"
assert cfg.user == "deploy"
def test_upsert_and_clear_host_access(db_session):
host = Host(hostname="box", os_family="windows", product="rdp-login-monitor")
db_session.add(host)
db_session.commit()
db_session.refresh(host)
view = upsert_host_mgmt_credentials(
db_session,
host,
user="HOME\\user",
password="secret123",
)
assert view.has_override is True
assert view.password_set is True
assert view.user == "HOME\\user"
cleared = upsert_host_mgmt_credentials(db_session, host, clear=True)
assert cleared.has_override is False
assert cleared.password_set is False
assert get_host_mgmt_access_view(db_session, host).user is None
@@ -266,113 +266,3 @@ def test_ip_mismatch_does_not_close_login(db_session, rdg_settings, rdg_hosts):
assert close_workstation_session_for_rdg_end(db_session, end) is None
assert event_session_terminated(login, db=db_session) is False
def test_empty_rcm1149_user_enriched_from_prior_rdg302(db_session, rdg_settings, rdg_hosts):
"""COMM-PC class: EventLog 1149 has empty Param1; RDG 302 already has the account."""
ws, _gw = rdg_hosts
t0 = datetime.now(timezone.utc)
user = r"B26\s.shelkovaya"
internal_ip = ws.ipv4
_ingest(
db_session,
t0,
host={"hostname": "K6A-DC3", "os_family": "windows"},
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
type="rdg.connection.success",
category="auth",
severity="warning",
title="RDG 302",
summary="302",
details={"user": user, "internal_ip": internal_ip},
)
login = _ingest(
db_session,
t0 + timedelta(seconds=3),
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
type="rdp.login.success",
category="auth",
severity="warning",
title="RDP connection (RCM 1149)",
summary="RCM 1149 - 192.168.160.40",
details={"user": "-", "ip_address": "192.168.160.40", "event_id_windows": 1149},
)
db_session.refresh(login)
assert login.details["user"] == user
assert login.details.get("user_enriched_from_rdg_event_id")
summary = event_to_summary(login, db_session)
assert summary.actor_user == user
def test_empty_rcm1149_backfilled_when_rdg302_arrives_later(db_session, rdg_settings, rdg_hosts):
ws, _gw = rdg_hosts
t0 = datetime.now(timezone.utc)
user = r"B26\s.shelkovaya"
internal_ip = ws.ipv4
login = _ingest(
db_session,
t0 + timedelta(seconds=1),
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
type="rdp.login.success",
category="auth",
severity="info",
title="RDP connection (RCM 1149)",
summary="RCM 1149 - 1.2.3.4",
details={"user": "-", "event_id_windows": 1149},
)
assert login.details["user"] == "-"
_ingest(
db_session,
t0,
host={"hostname": "K6A-DC3", "os_family": "windows"},
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
type="rdg.connection.success",
category="auth",
severity="info",
title="RDG 302",
summary="302",
details={"user": user, "internal_ip": internal_ip},
)
db_session.refresh(login)
assert login.details["user"] == user
def test_empty_user_login_still_closed_by_rdg303(db_session, rdg_settings, rdg_hosts):
ws, _gw = rdg_hosts
t0 = datetime.now(timezone.utc)
user = r"B26\s.shelkovaya"
internal_ip = ws.ipv4
login = _ingest(
db_session,
t0,
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
type="rdp.login.success",
category="auth",
severity="info",
title="RDP connection (RCM 1149)",
summary="RCM 1149",
details={"user": "-", "event_id_windows": 1149},
)
end = _ingest(
db_session,
t0 + timedelta(minutes=5),
host={"hostname": "K6A-DC3", "os_family": "windows"},
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
type="rdg.connection.disconnected",
category="auth",
severity="info",
title="303",
summary="303",
details={"user": user, "internal_ip": internal_ip},
)
db_session.refresh(login)
assert login.details.get(SESSION_CLOSED_BY_RDG_AT_KEY) == end.occurred_at.isoformat()
assert event_session_terminated(login, db=db_session) is True
-20
View File
@@ -1,20 +0,0 @@
"""Tests for session_duration_sec extract/format."""
from app.services.session_duration import extract_session_duration_sec, format_session_duration
def test_extract_session_duration_sec():
assert extract_session_duration_sec({"session_duration_sec": 18324}) == 18324
assert extract_session_duration_sec({"session_duration_sec": "42"}) == 42
assert extract_session_duration_sec({"session_duration_sec": ""}) is None
assert extract_session_duration_sec({}) is None
assert extract_session_duration_sec(None) is None
assert extract_session_duration_sec({"session_duration_sec": -1}) is None
def test_format_session_duration():
assert format_session_duration(0) == "00:00:00"
assert format_session_duration(18324) == "05:05:24"
assert format_session_duration(2428) == "00:40:28"
assert format_session_duration(86400 + 85) == "1д 00:01:25"
assert format_session_duration(2 * 86400 + 3661) == "2д 01:01:01"
+2 -2
View File
@@ -1,4 +1,4 @@
"""SSH connect service tests (paramiko mocked via sys.modules)."""
"""SSH connect service tests (paramiko mocked via sys.modules)."""
import sys
from unittest.mock import MagicMock
@@ -323,7 +323,7 @@ def test_run_ssh_monitor_update_runs_script(monkeypatch):
target="ubabuba",
user="root",
password="pw",
repo_url="https://git.papatramp.ru/PapaTramp/ssh-monitor.git",
repo_url="https://git.kalinamall.ru/PapaTramp/ssh-monitor.git",
)
assert result.ok is True
assert result.agent_version == "2.1.0-SAC"
+1 -1
View File
@@ -1,6 +1,6 @@
[Unit]
Description=Security Alert Center API (FastAPI)
Documentation=https://git.papatramp.ru/PapaTramp/security-alert-center
Documentation=https://git.kalinamall.ru/PapaTramp/security-alert-center
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
+5 -7
View File
@@ -183,13 +183,11 @@ sequenceDiagram
## 5. П.2C — Доступ SAC к хостам
### 5.1. Windows — WinRM / учётки
### 5.1. Windows — WinRM
- **Глобальный доменный admin** в **Настройки → Windows**: `DOMAIN\user` + password (encrypted). Аналогично **Linux admin** для SSH.
- **Override на карточке хоста** (**Хосты → WinRM/SSH / доступ к хосту**): `COMPUTER\user` / `.\Administrator` + password (encrypted в `hosts.mgmt_*`). Если пусто — берутся глобальные.
- Effective creds: `get_effective_win_admin_for_host` / `get_effective_linux_admin_for_host` (source: `host` | `db` | `env`).
- API: `GET` / `PUT /api/v1/hosts/{id}/access` (`user`, `password`, `clear`).
- Используются для WinRM/SSH-test, fallback-update, qwinsta/logoff, remote ops; для run_as через агента — те же creds в command poll (TLS + API key, не пишутся на диск агента).
- **Один доменный admin** в **Настройки → Управление хостами → Windows**: `DOMAIN\user` + password (encrypted).
- Override на карточке хоста — опционально позже.
- Используется для fallback-обновления и (при необходимости) remote ops; для qwinsta/logoff MVP — **через агента** с теми же creds, переданными в command poll (TLS + API key, не пишутся на диск агента).
### 5.2. Linux — bootstrap password → SSH key → удалить password
@@ -268,7 +266,7 @@ Authorization: Bearer sac_xxx
| POST | `/api/v1/events/{id}/actions/logoff` | logoff `{ "session_id": 5 }` |
| GET | `/api/v1/events/{id}/actions/{cmd_id}` | Статус / результат |
| PATCH | `/api/v1/hosts/{id}/config` | Desired config |
| GET/PUT | `/api/v1/hosts/{id}/access` | Per-host WinRM/SSH override (mgmt_user/password) |
| PATCH | `/api/v1/hosts/{id}/access` | Bootstrap / WinRM creds |
| GET/PATCH | `/api/v1/settings/agent-updates` | Режим A/B, версии, источники |
| GET/PATCH | `/api/v1/settings/host-management` | Доменный admin Windows |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.papatramp.ru/PapaTramp/security-alert-center/schemas/event-v1.json",
"$id": "https://git.kalinamall.ru/PapaTramp/security-alert-center/schemas/event-v1.json",
"title": "Security Alert Center Event v1",
"description": "Каноническое событие от ssh-monitor или RDP-login-monitor",
"type": "object",
+1 -1
View File
@@ -2,7 +2,7 @@
**Статус:** ToDo (не в текущем релизе).
**Контекст:** массовый SSH-update через SAC (10+ хостов) + `sac-deploy` в одно окно → часть POST в ingest «теряется» с точки зрения агента (`SAC POST HTTP :`), flood в Telegram (fallback + watchdog).
**Связано:** [runbook-ops.md](runbook-ops.md), [agent-integration.md](agent-integration.md), ssh-monitor [security-roadmap.ru.md](https://git.papatramp.ru/PapaTramp/ssh-monitor/src/branch/main/docs/security-roadmap.ru.md).
**Связано:** [runbook-ops.md](runbook-ops.md), [agent-integration.md](agent-integration.md), ssh-monitor [security-roadmap.ru.md](https://git.kalinamall.ru/PapaTramp/ssh-monitor/src/branch/main/docs/security-roadmap.ru.md).
Увеличение `SAC_DB_POOL_SIZE` / defer daily в **0.5.0** лечит штурм суточных отчётов и пул БД; **не** снимает узкие места ниже при одновременном restart многих агентов.
+1 -1
View File
@@ -24,7 +24,7 @@ Docker удобен для локальной отладки или изолир
```bash
sudo apt install -y git
sudo git clone https://git.papatramp.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
sudo git clone https://git.kalinamall.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
sudo chown -R "$USER:$USER" /opt/security-alert-center
```
+1 -1
View File
@@ -71,7 +71,7 @@ nginx -v
```bash
sudo mkdir -p /opt
sudo git clone https://git.papatramp.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
sudo git clone https://git.kalinamall.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
```
---
+1 -1
View File
@@ -14,7 +14,7 @@
```bash
# см. полный чеклист в native-руководстве
sudo apt update && sudo apt install -y git postgresql nginx python3.12 python3.12-venv
sudo git clone https://git.papatramp.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
sudo git clone https://git.kalinamall.ru/PapaTramp/security-alert-center.git /opt/security-alert-center
# … PostgreSQL, venv, config/sac-api.env, systemd, nginx
```
+1 -1
View File
@@ -39,7 +39,7 @@ sudo /opt/sac-deploy.sh
| Параметр | Значение |
|----------|----------|
| Хост | `ubabuba` / `10.10.36.9` |
| Репозиторий | `git.papatramp.ru/PapaTramp/ssh-monitor` (`main`, есть `sac-client.sh`) |
| Репозиторий | `git.kalinamall.ru/PapaTramp/ssh-monitor` (`main`, есть `sac-client.sh`) |
| `UseSAC` | пилот **exclusive** — см. [pilot-2.1-exclusive.md](pilot-2.1-exclusive.md) |
| Сервис | `ssh-monitor.service`**active** |
| `--check-sac` | OK (health + ingest `agent.test` 202) |
+3 -3
View File
@@ -66,7 +66,7 @@
**Цель:** нативный Android-клиент и push по тем же правилам оповещений, что Telegram/email/webhook.
Репозиторий клиента: [seaca](https://git.papatramp.ru/PapaTramp/seaca) (Kotlin, Jetpack Compose, FCM).
Репозиторий клиента: [seaca](https://git.kalinamall.ru/PapaTramp/seaca) (Kotlin, Jetpack Compose, FCM).
**Сервер SAC:**
@@ -77,7 +77,7 @@
**Не в мобильном клиенте:** пользователи SAC, каналы Telegram/SMTP/webhook, админ-настройки.
Подробный план клиента — [seaca/docs/ROADMAP.md](https://git.papatramp.ru/PapaTramp/seaca/src/branch/main/docs/ROADMAP.md).
Подробный план клиента — [seaca/docs/ROADMAP.md](https://git.kalinamall.ru/PapaTramp/seaca/src/branch/main/docs/ROADMAP.md).
---
@@ -112,7 +112,7 @@
| v0.1 | — (не требуется) | — |
| v0.2 | TBD (тег с UseSAC) | TBD |
| v0.3+ | совместимость schema 1.0 | совместимость schema 1.0 |
| v0.6 | — | — (мобильный клиент [seaca](https://git.papatramp.ru/PapaTramp/seaca), не агент) |
| v0.6 | — | — (мобильный клиент [seaca](https://git.kalinamall.ru/PapaTramp/seaca), не агент) |
При breaking change схемы — `schema_version: 1.1` с поддержкой 1.0 на ingest.
+2 -2
View File
@@ -1,6 +1,6 @@
# Seaca — мобильный клиент SAC
Репозиторий приложения: [seaca](https://git.papatramp.ru/PapaTramp/seaca).
Репозиторий приложения: [seaca](https://git.kalinamall.ru/PapaTramp/seaca).
Требуется SAC **≥ 0.9.0** с применённой миграцией `014`.
---
@@ -84,7 +84,7 @@ SAC_FCM_SERVICE_ACCOUNT_JSON=/etc/security-alert-center/fcm-service-account.json
| `sac.kalinamall.ru` | Web UI | yes (`git-sac-allowed`) |
| `sac-api.kalinamall.ru` | Seaca, `/api/v1/mobile/*` | no (enroll + JWT) |
Both resolve to the same SAC server (**192.168.160.145**). nginx must accept both names in `server_name`. See [reverse-proxy/docs/sac-access.md](https://git.papatramp.ru/PapaTramp/reverse-proxy/src/branch/main/docs/sac-access.md).
Both resolve to the same SAC server (**192.168.160.145**). nginx must accept both names in `server_name`. See [reverse-proxy/docs/sac-access.md](https://git.kalinamall.ru/PapaTramp/reverse-proxy/src/branch/main/docs/sac-access.md).
---
+6 -6
View File
@@ -8,9 +8,9 @@
| Имя в workspace | Путь (пример Windows) | Remote |
|-----------------|----------------------|--------|
| `ssh-monitor` | `D:\Soft\Git\ssh-monitor` | git.papatramp.ru/PapaTramp/ssh-monitor |
| `rdp-monitor` | `D:\Soft\Git\RDP-login-monitor` | git.papatramp.ru/PapaTramp/RDP-login-monitor |
| `security-alert-center` | `D:\Soft\Git\security-alert-center` | git.papatramp.ru/PapaTramp/security-alert-center |
| `ssh-monitor` | `D:\Soft\Git\ssh-monitor` | git.kalinamall.ru/PapaTramp/ssh-monitor |
| `rdp-monitor` | `D:\Soft\Git\RDP-login-monitor` | git.kalinamall.ru/PapaTramp/RDP-login-monitor |
| `security-alert-center` | `D:\Soft\Git\security-alert-center` | git.kalinamall.ru/PapaTramp/security-alert-center |
Пути подставьте свои.
@@ -65,9 +65,9 @@ git push kalinamall main
| Проект | URL |
|--------|-----|
| security-alert-center | `https://git.papatramp.ru/PapaTramp/security-alert-center.git` |
| ssh-monitor | `https://git.papatramp.ru/PapaTramp/ssh-monitor.git` |
| RDP-login-monitor | `https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git` |
| security-alert-center | `https://git.kalinamall.ru/PapaTramp/security-alert-center.git` |
| ssh-monitor | `https://git.kalinamall.ru/PapaTramp/ssh-monitor.git` |
| RDP-login-monitor | `https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git` |
---
-26
View File
@@ -192,7 +192,6 @@ export interface EventSummary {
title: string;
summary: string;
actor_user?: string | null;
session_duration_sec?: number | null;
rdg_flap?: boolean;
rdg_flap_pair_event_id?: number | null;
rdg_flap_qwinsta_event_id?: number | null;
@@ -532,31 +531,6 @@ export function updateLinuxAdminSettings(payload: {
});
}
export interface HostMgmtAccess {
host_id: number;
has_override: boolean;
user: string | null;
password_set: boolean;
password_hint: string | null;
effective_source: string;
effective_configured: boolean;
effective_user: string | null;
}
export function fetchHostAccess(hostId: number): Promise<HostMgmtAccess> {
return apiFetch<HostMgmtAccess>(`/api/v1/hosts/${hostId}/access`);
}
export function updateHostAccess(
hostId: number,
payload: { user?: string | null; password?: string | null; clear?: boolean },
): Promise<HostMgmtAccess> {
return apiFetch<HostMgmtAccess>(`/api/v1/hosts/${hostId}/access`, {
method: "PUT",
body: JSON.stringify(payload),
});
}
export interface HostSshActionResult {
ok: boolean;
message: string;
-17
View File
@@ -1,17 +0,0 @@
/** Human-readable session_duration_sec for event tables. */
export function formatSessionDuration(seconds: number | null | undefined): string {
if (seconds == null || !Number.isFinite(seconds) || seconds < 0) {
return "—";
}
const total = Math.floor(seconds);
const days = Math.floor(total / 86_400);
const hours = Math.floor((total % 86_400) / 3600);
const minutes = Math.floor((total % 3600) / 60);
const secs = total % 60;
const clock = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
if (days > 0) {
return `${days}д ${clock}`;
}
return clock;
}
+1 -1
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.5.17";
export const APP_VERSION = "0.5.14";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
-5
View File
@@ -238,8 +238,6 @@
<th>Пользователь</th>
<th title="Длительность сессии (из session_duration_sec)">Длительность</th>
<th>Версия агента</th>
<th>Severity</th>
@@ -278,8 +276,6 @@
<td>{{ e.actor_user || "—" }}</td>
<td>{{ formatSessionDuration(e.session_duration_sec) }}</td>
<td>{{ e.product_version || "—" }}</td>
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
@@ -358,7 +354,6 @@ import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
import { formatServerName } from "../utils/hostDisplay";
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
import { formatSessionDuration } from "../utils/sessionDuration";
import { markEventSessionTerminatedInList, showEventSessionTerminateButton } from "../utils/sessionActions";
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
-3
View File
@@ -34,7 +34,6 @@
<th>Хост</th>
<th>Имя сервера</th>
<th>Пользователь</th>
<th title="Длительность сессии (из session_duration_sec)">Длительность</th>
<th>Версия агента</th>
<th>Severity</th>
<th>Type</th>
@@ -51,7 +50,6 @@
<td>{{ e.hostname }}</td>
<td>{{ formatServerName(e.display_name) }}</td>
<td>{{ e.actor_user || "—" }}</td>
<td>{{ formatSessionDuration(e.session_duration_sec) }}</td>
<td>{{ e.product_version || "—" }}</td>
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
<td><code>{{ e.type }}</code></td>
@@ -122,7 +120,6 @@ import {
type EventsListState,
} from "../utils/eventsListQuery";
import { formatServerName } from "../utils/hostDisplay";
import { formatSessionDuration } from "../utils/sessionDuration";
import { markEventSessionTerminatedInList, showEventSessionTerminateButton } from "../utils/sessionActions";
const route = useRoute();
-180
View File
@@ -69,52 +69,6 @@
<dd>{{ host.event_count ?? 0 }}</dd>
</div>
</dl>
<section v-if="showAgentConfig" class="host-mgmt-access">
<h3>{{ isWindowsHost ? "WinRM / доступ к хосту" : "SSH / доступ к хосту" }}</h3>
<p class="muted">
Переопределение для этого ПК. Если пусто используются глобальные учётные данные из
<RouterLink to="/settings">Настройки</RouterLink>
({{ isWindowsHost ? "Windows admin" : "Linux admin" }}).
Сейчас effective:
<strong>{{ accessEffectiveLabel }}</strong>
</p>
<form class="agent-config-form" @submit.prevent="saveHostAccess">
<label class="config-field">
Логин
<input
v-model="accessForm.user"
type="text"
:placeholder="isWindowsHost ? 'COMPUTER\\user или .\\Administrator' : 'root'"
autocomplete="off"
/>
</label>
<label class="config-field">
Пароль
<input
v-model="accessForm.password"
type="password"
:placeholder="accessForm.passwordSet ? '•••• (оставьте пустым, чтобы не менять)' : ''"
autocomplete="new-password"
/>
</label>
<div class="host-actions-row">
<button type="submit" :disabled="savingAccess">
{{ savingAccess ? "Сохранение…" : "Сохранить доступ" }}
</button>
<button
type="button"
class="secondary"
:disabled="savingAccess || !accessForm.hasOverride"
@click="clearHostAccess"
>
Сбросить (глобальные)
</button>
</div>
<p v-if="accessMessage" :class="accessOk ? 'success' : 'error'">{{ accessMessage }}</p>
</form>
</section>
<div v-if="isWindowsHost" class="host-actions">
<button
type="button"
@@ -342,8 +296,6 @@ import {
import {
apiFetch,
fetchHost,
fetchHostAccess,
updateHostAccess,
fetchRecentEvents,
testHostWinRm,
testHostSsh,
@@ -351,7 +303,6 @@ import {
patchHostAgentConfig,
type EventListResponse,
type HostDetail,
type HostMgmtAccess,
} from "../api";
import { emitHostPatch } from "../utils/hostPatchBus";
import HostSessionsPanel from "../components/HostSessionsPanel.vue";
@@ -403,32 +354,6 @@ const configForm = reactive({
serverDisplayName: "",
getInventory: true,
});
const savingAccess = ref(false);
const accessMessage = ref("");
const accessOk = ref(false);
const accessForm = reactive({
user: "",
password: "",
passwordSet: false,
hasOverride: false,
effectiveSource: "",
effectiveUser: "",
effectiveConfigured: false,
});
const accessEffectiveLabel = computed(() => {
if (!accessForm.effectiveConfigured) {
return "не настроено";
}
const src =
accessForm.effectiveSource === "host"
? "override хоста"
: accessForm.effectiveSource === "db"
? "глобальные (Settings)"
: accessForm.effectiveSource === "env"
? "env"
: accessForm.effectiveSource;
return `${accessForm.effectiveUser || "—"} · ${src}`;
});
let sshTestHideTimer: ReturnType<typeof setTimeout> | null = null;
let winRmTestHideTimer: ReturnType<typeof setTimeout> | null = null;
let sshProbeSeq = 0;
@@ -753,67 +678,6 @@ async function saveAgentConfig() {
}
}
async function applyAccessView(view: HostMgmtAccess) {
accessForm.user = view.user || "";
accessForm.password = "";
accessForm.passwordSet = view.password_set;
accessForm.hasOverride = view.has_override;
accessForm.effectiveSource = view.effective_source;
accessForm.effectiveUser = view.effective_user || "";
accessForm.effectiveConfigured = view.effective_configured;
}
async function loadHostAccess() {
accessMessage.value = "";
try {
const view = await fetchHostAccess(hostId.value);
applyAccessView(view);
} catch (e) {
accessOk.value = false;
accessMessage.value = e instanceof Error ? e.message : "Не удалось загрузить доступ хоста";
}
}
async function saveHostAccess() {
savingAccess.value = true;
accessMessage.value = "";
try {
const payload: { user?: string | null; password?: string | null } = {
user: accessForm.user,
};
if (accessForm.password.trim()) {
payload.password = accessForm.password;
}
const view = await updateHostAccess(hostId.value, payload);
applyAccessView(view);
accessOk.value = true;
accessMessage.value = view.has_override
? "Сохранено (override хоста)"
: "Сохранено (логин очищен — глобальные настройки)";
} catch (e) {
accessOk.value = false;
accessMessage.value = e instanceof Error ? e.message : "Ошибка сохранения доступа";
} finally {
savingAccess.value = false;
}
}
async function clearHostAccess() {
savingAccess.value = true;
accessMessage.value = "";
try {
const view = await updateHostAccess(hostId.value, { clear: true });
applyAccessView(view);
accessOk.value = true;
accessMessage.value = "Override сброшен — используются глобальные настройки";
} catch (e) {
accessOk.value = false;
accessMessage.value = e instanceof Error ? e.message : "Ошибка сброса доступа";
} finally {
savingAccess.value = false;
}
}
async function loadHost() {
loading.value = true;
error.value = "";
@@ -828,7 +692,6 @@ async function loadHost() {
try {
const detail = await fetchHost(hostId.value);
applyHostDetail(detail);
void loadHostAccess();
if (isLinuxHostDetail(detail)) {
void probeSshOnOpen();
}
@@ -985,49 +848,6 @@ watch(
color: #9aa4b2;
}
.host-mgmt-access {
margin: 1rem 0 1.25rem;
padding: 0.85rem 1rem;
border: 1px solid #2a3340;
border-radius: 8px;
background: #141a22;
}
.host-mgmt-access h3 {
margin: 0 0 0.35rem;
font-size: 1rem;
}
.host-mgmt-form {
display: flex;
flex-direction: column;
gap: 0.65rem;
margin-top: 0.75rem;
max-width: 28rem;
}
.host-mgmt-field {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.9rem;
}
.host-mgmt-field input {
padding: 0.4rem 0.55rem;
border-radius: 6px;
border: 1px solid #3a4554;
background: #0f1419;
color: #e8eef5;
}
.host-mgmt-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.host-actions {
margin-top: 1rem;
display: flex;
+5 -7
View File
@@ -1,4 +1,4 @@
<template>
<template>
<div class="settings-page">
<h1>Настройки</h1>
<p class="settings-intro">
@@ -126,9 +126,8 @@
<section class="card settings-card settings-policy">
<h2>Windows (qwinsta / logoff)</h2>
<p class="settings-hint settings-hint-top">
Глобальный (доменный) admin для удалённых команд на Windows-хостах (qwinsta, logoff, WinRM-update).
Доменный admin для удалённых команд на Windows-хостах (qwinsta, logoff через агент).
Формат логина: <code>ДОМЕН\пользователь</code> (например <code>B26\papatramp</code>).
Для отдельного ПК (home/workgroup) задайте override в карточке <strong>Хосты доступ к хосту</strong>.
Пока запись не создана в UI используются <code>SAC_WIN_ADMIN_*</code> из <code>sac-api.env</code>.
</p>
<form class="settings-form" @submit.prevent="saveWinAdminSettings">
@@ -187,9 +186,8 @@
<section class="card settings-card settings-policy">
<h2>Linux (SSH / обновление ssh-monitor)</h2>
<p class="settings-hint settings-hint-top">
Глобальная SSH-учётка для удалённого запуска <code>/opt/scripts/update_ssh_monitor.sh</code> с карточки Linux-хоста.
SSH-учётка для удалённого запуска <code>/opt/scripts/update_ssh_monitor.sh</code> с карточки Linux-хоста.
Рекомендуется <code>root</code> или пользователь с <code>sudo NOPASSWD</code>.
Для отдельного сервера задайте override в карточке <strong>Хосты доступ к хосту</strong>.
Пока запись не создана в UI используются <code>SAC_LINUX_ADMIN_*</code> из <code>sac-api.env</code>.
</p>
<form class="settings-form" @submit.prevent="saveLinuxAdminSettings">
@@ -253,7 +251,7 @@
<input
v-model="agentUpdateForm.rdp_git_repo_url"
type="text"
placeholder="https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git"
placeholder="https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git"
/>
</label>
<label class="settings-field">
@@ -261,7 +259,7 @@
<input
v-model="agentUpdateForm.ssh_git_repo_url"
type="text"
placeholder="https://git.papatramp.ru/PapaTramp/ssh-monitor.git"
placeholder="https://git.kalinamall.ru/PapaTramp/ssh-monitor.git"
/>
</label>
<label class="settings-field">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.papatramp.ru/PapaTramp/security-alert-center/schemas/event-v1.json",
"$id": "https://git.kalinamall.ru/PapaTramp/security-alert-center/schemas/event-v1.json",
"title": "Security Alert Center Event v1",
"description": "Каноническое событие от ssh-monitor или RDP-login-monitor",
"type": "object",
Regular → Executable
View File
+2 -2
View File
@@ -7,8 +7,8 @@ REMOTE_CMD = (
"set -e\n"
"TMP=$(mktemp -d)\n"
'trap "rm -rf \\"$TMP\\"" EXIT\n'
"git clone --depth 1 -q https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git \"$TMP/rdp\"\n"
"git clone --depth 1 -q https://git.papatramp.ru/PapaTramp/ssh-monitor.git \"$TMP/ssh\"\n"
"git clone --depth 1 -q https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git \"$TMP/rdp\"\n"
"git clone --depth 1 -q https://git.kalinamall.ru/PapaTramp/ssh-monitor.git \"$TMP/ssh\"\n"
"echo RDP_version_txt:\n"
"cat \"$TMP/rdp/version.txt\" 2>/dev/null || echo missing\n"
"echo RDP_script:\n"
+2 -2
View File
@@ -2,8 +2,8 @@
set -e
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
git clone --depth 1 -q https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git "$TMP/rdp"
git clone --depth 1 -q https://git.papatramp.ru/PapaTramp/ssh-monitor.git "$TMP/ssh"
git clone --depth 1 -q https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git "$TMP/rdp"
git clone --depth 1 -q https://git.kalinamall.ru/PapaTramp/ssh-monitor.git "$TMP/ssh"
echo RDP_version_txt:
cat "$TMP/rdp/version.txt" 2>/dev/null || echo missing
echo RDP_script: