diff --git a/backend/app/services/ssh_connect.py b/backend/app/services/ssh_connect.py index 76c4524..3ee892d 100644 --- a/backend/app/services/ssh_connect.py +++ b/backend/app/services/ssh_connect.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import socket from dataclasses import dataclass @@ -9,6 +10,7 @@ from app.models import Host SSH_MONITOR_UPDATE_SCRIPT = "/opt/scripts/update_ssh_monitor.sh" SSH_OUTPUT_MAX_LEN = 12_000 +_IPV4_RE = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$") class LinuxAdminNotConfiguredError(Exception): @@ -39,6 +41,32 @@ def is_linux_host(host: Host) -> bool: return (host.product or "").strip() == "ssh-monitor" +def _is_ipv4(value: str) -> bool: + return bool(_IPV4_RE.match(value.strip())) + + +def _looks_like_ssh_hostname(value: str) -> bool: + """Короткое имя или FQDN; не человекочитаемый display_name вроде «Ubabuba Kalina (10.10.36.9)».""" + text = value.strip() + if not text or " " in text or "(" in text or ")" in text: + return False + if _is_ipv4(text): + return True + if len(text) > 253: + return False + return all(ch.isalnum() or ch in ".-_" for ch in text) + + +def _ssh_name_resolves(name: str) -> bool: + if _is_ipv4(name): + return True + try: + socket.getaddrinfo(name.strip(), 22, type=socket.SOCK_STREAM) + return True + except OSError: + return False + + def iter_ssh_targets(host: Host) -> list[str]: if not is_linux_host(host): raise HostNotLinuxError("Host is not Linux") @@ -50,14 +78,26 @@ def iter_ssh_targets(host: Host) -> list[str]: text = (value or "").strip() if not text or text.casefold() in seen: return + if not _looks_like_ssh_hostname(text): + return + if not _ssh_name_resolves(text): + return seen.add(text.casefold()) ordered.append(text) add(host.hostname) - add(host.display_name) + + inventory = host.inventory if isinstance(host.inventory, dict) else {} + computer_name = inventory.get("computer_name") + if isinstance(computer_name, str): + add(computer_name) + + if host.display_name and _looks_like_ssh_hostname(host.display_name): + add(host.display_name) + add(host.ipv4) if not ordered: - raise HostTargetMissingError("Host has no hostname or IPv4 for SSH") + raise HostTargetMissingError("Host has no resolvable hostname or IPv4 for SSH") return ordered diff --git a/backend/app/version.py b/backend/app/version.py index b6ac1de..a10cd42 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.11.1" +APP_VERSION = "0.11.2" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py index f39377d..20321e7 100644 --- a/backend/tests/test_health.py +++ b/backend/tests/test_health.py @@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL def test_version_constants(): - assert APP_VERSION == "0.11.1" + assert APP_VERSION == "0.11.2" assert APP_NAME == "Security Alert Center" - assert APP_VERSION_LABEL == "Security Alert Center v.0.11.1" + assert APP_VERSION_LABEL == "Security Alert Center v.0.11.2" diff --git a/backend/tests/test_ssh_connect.py b/backend/tests/test_ssh_connect.py index fb52705..f7a88f3 100644 --- a/backend/tests/test_ssh_connect.py +++ b/backend/tests/test_ssh_connect.py @@ -78,6 +78,35 @@ def test_probe_ssh_connection_non_root_does_not_use_sudo(monkeypatch): assert "sudo" not in captured[0] +def test_iter_ssh_targets_skips_human_display_name(monkeypatch): + monkeypatch.setattr(ssh_connect, "_ssh_name_resolves", lambda name: True) + host = Host( + hostname="ubabuba", + display_name="Ubabuba Kalina (10.10.36.9)", + os_family="linux", + product="ssh-monitor", + ipv4="10.10.36.9", + ) + targets = iter_ssh_targets(host) + assert "Ubabuba Kalina (10.10.36.9)" not in targets + assert "10.10.36.9" in targets + + +def test_iter_ssh_targets_skips_unresolvable_hostname(monkeypatch): + def resolves(name: str) -> bool: + return ssh_connect._is_ipv4(name) + + monkeypatch.setattr(ssh_connect, "_ssh_name_resolves", resolves) + host = Host( + hostname="ubabuba", + os_family="linux", + product="ssh-monitor", + ipv4="10.10.36.9", + ) + targets = iter_ssh_targets(host) + assert targets == ["10.10.36.9"] + + def test_iter_ssh_targets_rejects_windows(): host = Host(hostname="PC", os_family="windows", product="rdp-login-monitor", ipv4="1.2.3.4") try: diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 4b181c6..865ea5c 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.11.1"; +export const APP_VERSION = "0.11.2"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;