"""Единый текст и stats для report.daily.ssh / report.daily.rdp (агент и SAC).""" from __future__ import annotations import html import re from datetime import datetime from typing import Any, Literal from app.models import Host Platform = Literal["ssh", "windows"] RDP_BAN_TYPES = frozenset({"rdp.ip.banned", "rdp.ip.ban"}) SSH_BAN_TYPES = frozenset({"ssh.ip.banned"}) _SERVER_LINE_RE = re.compile(r"(?m)^🖥️\s*Сервер\s*:") _ACTIVE_USERS_HEADER_RE = re.compile(r"^👥\s*АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ") _AGENT_VERSION_LINE_RE = re.compile(r"(?m)^Agent version\s+", re.IGNORECASE) _DAILY_REPORT_TITLE_MARKER = "ЕЖЕДНЕВНЫЙ ОТЧЕТ" _SECTION_HEADER_RE = re.compile(r"^[📈🧾👥🖥️🕐]") def host_server_line(host: Host | None) -> str: if host is None: return "unknown" label = (host.display_name or host.hostname or "unknown").strip() ip = (host.ipv4 or "").strip() if ip: return f"{label} ({ip})" return label def collapse_blank_lines(text: str, *, max_run: int = 1) -> str: lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n") out: list[str] = [] blank_run = 0 for line in lines: if not line.strip(): blank_run += 1 if blank_run <= max_run: out.append("") continue blank_run = 0 out.append(line.rstrip()) return "\n".join(out).strip() def split_active_user_tokens(entry: str) -> list[str]: """Разбивает «👤 u1 👤 u2» на отдельные строки (как в отчёте агента).""" entry = entry.strip() if not entry: return [] if entry.count("👤") <= 1: line = entry if entry.startswith("👤") else f"👤 {entry}" return [f" {line}"] parts = re.split(r"(?=👤)", entry) result: list[str] = [] for part in parts: part = part.strip() if not part: continue if not part.startswith("👤"): part = f"👤 {part}" result.append(f" {part}") return result def normalize_active_users_list(users: list[Any] | None) -> list[str]: out: list[str] = [] seen: set[str] = set() for raw in users or []: for line in split_active_user_tokens(str(raw)): key = line.strip().lower() if key and key not in seen: seen.add(key) out.append(line) return out def format_agent_version_line(version: str) -> str: v = (version or "").strip() if not v: return "" return f"Agent version {v}" def ensure_agent_version_line(body: str, version: str | None) -> str: line = format_agent_version_line(version or "") if not line: return body if _AGENT_VERSION_LINE_RE.search(body): return body lines = body.replace("\r\n", "\n").split("\n") for i, ln in enumerate(lines): if _DAILY_REPORT_TITLE_MARKER in ln: lines.insert(i + 1, line) return "\n".join(lines) return body def _fix_active_users_header_count(header_line: str, user_count: int) -> str: stripped = header_line.rstrip() if _ACTIVE_USERS_COUNT_RE.match(stripped): return re.sub( r"(\👥\s*АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ\s*\()[^)]+(\))", rf"\g<1>{user_count}\2", stripped, count=1, ) return stripped def ensure_server_line(body: str, host: Host | None) -> str: server = host_server_line(host) if not server or server == "unknown": return body if _SERVER_LINE_RE.search(body): return body lines = body.replace("\r\n", "\n").split("\n") for i, line in enumerate(lines): if "ЕЖЕДНЕВНЫЙ ОТЧЕТ" in line: insert_at = i + 1 if insert_at < len(lines) and _AGENT_VERSION_LINE_RE.match(lines[insert_at].strip()): insert_at += 1 lines.insert(insert_at, f"🖥️ Сервер: {server}") return "\n".join(lines) return f"🖥️ Сервер: {server}\n{body}" def normalize_active_users_in_body(body: str) -> str: lines = body.replace("\r\n", "\n").split("\n") out: list[str] = [] i = 0 while i < len(lines): line = lines[i] if _ACTIVE_USERS_HEADER_RE.match(line.strip()): out.append(line) i += 1 user_lines: list[str] = [] while i < len(lines): cur = lines[i] stripped = cur.strip() if not stripped: break if stripped.startswith("Источник:"): break if _SECTION_HEADER_RE.match(stripped) and not stripped.startswith("👤"): break if stripped.startswith("(нет данных"): out.append(cur) i += 1 break user_lines.extend(split_active_user_tokens(cur)) i += 1 if user_lines: out[-1] = _fix_active_users_header_count(out[-1], len(user_lines)) out.extend(user_lines) continue out.append(line) i += 1 return "\n".join(out) def normalize_report_body(body: str, host: Host | None, platform: Platform) -> str: """Приводит текст отчёта (агент/SAC) к единому компактному виду.""" text = collapse_blank_lines(body.replace("\r\n", "\n")) agent_version = host.product_version if host else None text = ensure_agent_version_line(text, agent_version) text = ensure_server_line(text, host) text = normalize_active_users_in_body(text) return collapse_blank_lines(text) def body_to_report_html(body: str) -> str: escaped = html.escape(body) return f'