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.
This commit is contained in:
PTah
2026-06-20 17:01:06 +10:00
parent 07b97bd111
commit c21043d845
5 changed files with 146 additions and 26 deletions
+8 -1
View File
@@ -4,6 +4,7 @@ from sqlalchemy import func, select
from sqlalchemy.orm import Session
from datetime import datetime, timezone
from functools import partial
from app.auth.jwt_auth import get_current_user, require_admin
from app.config import get_settings
@@ -412,7 +413,13 @@ def update_host_agent_via_ssh(
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
response = _run_ssh_action_on_host(host, cfg, action=run_ssh_monitor_update)
agent_cfg = get_effective_agent_update_config(db)
repo_url = (agent_cfg.ssh_git_repo_url or "").strip() or None
response = _run_ssh_action_on_host(
host,
cfg,
action=partial(run_ssh_monitor_update, repo_url=repo_url),
)
_set_ssh_admin_status(host, response.ok)
if response.ok and response.product_version:
host.product_version = response.product_version
+16 -3
View File
@@ -141,7 +141,12 @@ def process_agent_update_ingest(db: Session, host: Host, event_type: str, detail
_clear_pending(host)
def run_linux_agent_update_fallback(host: Host, cfg_linux) -> tuple[bool, str, str | None]:
def run_linux_agent_update_fallback(
host: Host,
cfg_linux,
*,
repo_url: str | None = None,
) -> tuple[bool, str, str | None]:
if not is_linux_host(host):
return False, "Host is not Linux", None
if not cfg_linux.configured:
@@ -154,8 +159,14 @@ def run_linux_agent_update_fallback(host: Host, cfg_linux) -> tuple[bool, str, s
except (HostNotLinuxError, HostTargetMissingError) as exc:
return False, str(exc), None
effective_repo = (repo_url or "").strip() or None
for target in targets:
result = run_ssh_monitor_update(target=target, user=cfg_linux.user, password=cfg_linux.password)
result = run_ssh_monitor_update(
target=target,
user=cfg_linux.user,
password=cfg_linux.password,
repo_url=effective_repo,
)
last_message = result.message
if result.ok:
version = result.agent_version
@@ -204,7 +215,9 @@ def execute_agent_update_fallback(db: Session, host: Host) -> tuple[bool, str, s
if product == PRODUCT_SSH or is_linux_host(host):
linux_cfg = get_effective_linux_admin_config(db)
ok, message, version = run_linux_agent_update_fallback(host, linux_cfg)
agent_cfg = get_effective_agent_update_config(db)
repo_url = (agent_cfg.ssh_git_repo_url or "").strip() or None
ok, message, version = run_linux_agent_update_fallback(host, linux_cfg, repo_url=repo_url)
elif product == PRODUCT_RDP or is_windows_host(host):
win_cfg = get_effective_win_admin_config(db)
ok, message, version = run_windows_agent_update_fallback(host, win_cfg, cfg.win_agent_update_script)
+63 -12
View File
@@ -9,6 +9,9 @@ from dataclasses import dataclass
from app.models import Host
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.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 "
@@ -271,13 +274,30 @@ def probe_ssh_monitor_version(
return None
def _ssh_monitor_bootstrap_command(repo_url: str) -> str:
"""Clone ssh-monitor repo and install updater when /opt/scripts/update_ssh_monitor.sh is absent."""
safe_repo = repo_url.replace("\\", "\\\\").replace('"', '\\"')
return (
f'UPDATE_DIR="{SSH_MONITOR_UPDATE_DIR}";'
f'REPO_URL="{safe_repo}";'
f'SCRIPT_NAME="{SSH_MONITOR_REPO_NAME}";'
f'INSTALL="{SSH_MONITOR_UPDATE_SCRIPT}";'
f'mkdir -p "$UPDATE_DIR" && cd "$UPDATE_DIR" && '
f'([ -d "$SCRIPT_NAME" ] || git clone "$REPO_URL" "$SCRIPT_NAME") && '
f'cp "$UPDATE_DIR/$SCRIPT_NAME/update_ssh_monitor.sh" "$INSTALL" && '
f'chmod 750 "$INSTALL" && "$INSTALL"'
)
def run_ssh_monitor_update(
*,
target: str,
user: str,
password: str,
repo_url: str | None = None,
) -> SshCommandResult:
script = SSH_MONITOR_UPDATE_SCRIPT
effective_repo = (repo_url or SSH_MONITOR_UPDATE_REPO_URL).strip()
check_cmd = f"test -x {script} && echo ready || echo missing"
probe = run_ssh_command(
target=target,
@@ -286,32 +306,54 @@ def run_ssh_monitor_update(
remote_cmd=check_cmd,
command_timeout_sec=30,
)
if not probe.ok or "missing" in probe.stdout:
if not probe.ok:
return SshCommandResult(
ok=False,
message=f"Update script not found or not executable: {script} on {target}",
message=f"SSH probe failed on {target}: {probe.message}",
target=target,
stdout=probe.stdout,
stderr=probe.stderr,
exit_code=probe.exit_code,
)
updated = run_ssh_command(
target=target,
user=user,
password=password,
remote_cmd=script,
command_timeout_sec=900,
need_root=True,
)
bootstrapped = False
if "missing" in probe.stdout:
bootstrap_cmd = _ssh_monitor_bootstrap_command(effective_repo)
updated = run_ssh_command(
target=target,
user=user,
password=password,
remote_cmd=bootstrap_cmd,
command_timeout_sec=900,
need_root=True,
)
bootstrapped = True
else:
updated = run_ssh_command(
target=target,
user=user,
password=password,
remote_cmd=script,
command_timeout_sec=900,
need_root=True,
)
if not updated.ok:
return updated
prefix = "Updater bootstrap failed" if bootstrapped else "SSH update failed"
return SshCommandResult(
ok=False,
message=f"{prefix} ({target}): {updated.message}",
target=updated.target,
stdout=updated.stdout,
stderr=updated.stderr,
exit_code=updated.exit_code,
)
version = probe_ssh_monitor_version(target=target, user=user, password=password)
if not version:
version = parse_ssh_monitor_version_text(updated.stdout)
if version:
message = f"{updated.message}\nagent version: {version}"
prefix = "Bootstrap + update OK" if bootstrapped else updated.message
message = f"{prefix}\nagent version: {version}"
return SshCommandResult(
ok=True,
message=message,
@@ -321,4 +363,13 @@ def run_ssh_monitor_update(
exit_code=updated.exit_code,
agent_version=version,
)
if bootstrapped:
return SshCommandResult(
ok=True,
message=f"Bootstrap + update OK ({target}), exit 0",
target=updated.target,
stdout=updated.stdout,
stderr=updated.stderr,
exit_code=updated.exit_code,
)
return updated
+1 -1
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center"
APP_VERSION = "0.20.1"
APP_VERSION = "0.20.2"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
+58 -9
View File
@@ -152,21 +152,70 @@ def test_run_ssh_command_auth_failure(monkeypatch):
client.close.assert_called_once()
def test_run_ssh_monitor_update_missing_script(monkeypatch):
monkeypatch.setattr(
ssh_connect,
"run_ssh_command",
lambda **kwargs: ssh_connect.SshCommandResult(
def test_run_ssh_monitor_update_missing_script_bootstraps(monkeypatch):
calls = 0
def fake_run(**kwargs):
nonlocal calls
calls += 1
if calls == 1:
return ssh_connect.SshCommandResult(
ok=True,
message="ok",
target="h",
stdout="missing\n",
)
if calls == 2:
cmd = kwargs["remote_cmd"]
assert "git clone" in cmd
assert "update_ssh_monitor.sh" in cmd
assert kwargs.get("need_root") is True
return ssh_connect.SshCommandResult(
ok=True,
message="SSH OK",
target="185.87.149.9",
stdout="SUMMARY updated 2.1.0-SAC\n",
exit_code=0,
)
return ssh_connect.SshCommandResult(
ok=True,
message="ok",
target="185.87.149.9",
stdout="2.1.0-SAC\n",
exit_code=0,
)
monkeypatch.setattr(ssh_connect, "run_ssh_command", fake_run)
result = run_ssh_monitor_update(target="185.87.149.9", user="root", password="pw")
assert result.ok is True
assert result.agent_version == "2.1.0-SAC"
assert "Bootstrap" in result.message
assert calls == 3
def test_run_ssh_monitor_update_missing_script(monkeypatch):
def fake_run(**kwargs):
if kwargs["remote_cmd"].startswith("test -x"):
return ssh_connect.SshCommandResult(
ok=True,
message="ok",
target="h",
stdout="missing\n",
)
return ssh_connect.SshCommandResult(
ok=False,
message="git clone failed",
target="h",
stdout="missing\n",
),
)
stderr="fatal: could not read",
exit_code=128,
)
monkeypatch.setattr(ssh_connect, "run_ssh_command", fake_run)
result = run_ssh_monitor_update(target="ubabuba", user="root", password="pw")
assert result.ok is False
assert "not found" in result.message.lower()
assert "bootstrap failed" in result.message.lower()
def test_run_ssh_monitor_update_runs_script(monkeypatch):