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).
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ from app.models import Host
|
|||||||
|
|
||||||
SSH_MONITOR_UPDATE_SCRIPT = "/opt/scripts/update_ssh_monitor.sh"
|
SSH_MONITOR_UPDATE_SCRIPT = "/opt/scripts/update_ssh_monitor.sh"
|
||||||
SSH_OUTPUT_MAX_LEN = 12_000
|
SSH_OUTPUT_MAX_LEN = 12_000
|
||||||
|
_IPV4_RE = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$")
|
||||||
|
|
||||||
|
|
||||||
class LinuxAdminNotConfiguredError(Exception):
|
class LinuxAdminNotConfiguredError(Exception):
|
||||||
@@ -39,6 +41,32 @@ def is_linux_host(host: Host) -> bool:
|
|||||||
return (host.product or "").strip() == "ssh-monitor"
|
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]:
|
def iter_ssh_targets(host: Host) -> list[str]:
|
||||||
if not is_linux_host(host):
|
if not is_linux_host(host):
|
||||||
raise HostNotLinuxError("Host is not Linux")
|
raise HostNotLinuxError("Host is not Linux")
|
||||||
@@ -50,14 +78,26 @@ def iter_ssh_targets(host: Host) -> list[str]:
|
|||||||
text = (value or "").strip()
|
text = (value or "").strip()
|
||||||
if not text or text.casefold() in seen:
|
if not text or text.casefold() in seen:
|
||||||
return
|
return
|
||||||
|
if not _looks_like_ssh_hostname(text):
|
||||||
|
return
|
||||||
|
if not _ssh_name_resolves(text):
|
||||||
|
return
|
||||||
seen.add(text.casefold())
|
seen.add(text.casefold())
|
||||||
ordered.append(text)
|
ordered.append(text)
|
||||||
|
|
||||||
add(host.hostname)
|
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)
|
add(host.ipv4)
|
||||||
if not ordered:
|
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
|
return ordered
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||||
|
|
||||||
APP_NAME = "Security Alert Center"
|
APP_NAME = "Security Alert Center"
|
||||||
APP_VERSION = "0.11.1"
|
APP_VERSION = "0.11.2"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL
|
|||||||
|
|
||||||
|
|
||||||
def test_version_constants():
|
def test_version_constants():
|
||||||
assert APP_VERSION == "0.11.1"
|
assert APP_VERSION == "0.11.2"
|
||||||
assert APP_NAME == "Security Alert Center"
|
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"
|
||||||
|
|||||||
@@ -78,6 +78,35 @@ def test_probe_ssh_connection_non_root_does_not_use_sudo(monkeypatch):
|
|||||||
assert "sudo" not in captured[0]
|
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():
|
def test_iter_ssh_targets_rejects_windows():
|
||||||
host = Host(hostname="PC", os_family="windows", product="rdp-login-monitor", ipv4="1.2.3.4")
|
host = Host(hostname="PC", os_family="windows", product="rdp-login-monitor", ipv4="1.2.3.4")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||||
export const APP_NAME = "Security Alert Center";
|
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}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user