feat: daily report agent version line and fix active users layout
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,8 @@ SSH_BAN_TYPES = frozenset({"ssh.ip.banned"})
|
|||||||
|
|
||||||
_SERVER_LINE_RE = re.compile(r"(?m)^🖥️\s*Сервер\s*:")
|
_SERVER_LINE_RE = re.compile(r"(?m)^🖥️\s*Сервер\s*:")
|
||||||
_ACTIVE_USERS_HEADER_RE = re.compile(r"^👥\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"^[📈🧾👥🖥️🕐]")
|
_SECTION_HEADER_RE = re.compile(r"^[📈🧾👥🖥️🕐]")
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +78,39 @@ def normalize_active_users_list(users: list[Any] | None) -> list[str]:
|
|||||||
return out
|
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:
|
def ensure_server_line(body: str, host: Host | None) -> str:
|
||||||
server = host_server_line(host)
|
server = host_server_line(host)
|
||||||
if not server or server == "unknown":
|
if not server or server == "unknown":
|
||||||
@@ -85,7 +120,10 @@ def ensure_server_line(body: str, host: Host | None) -> str:
|
|||||||
lines = body.replace("\r\n", "\n").split("\n")
|
lines = body.replace("\r\n", "\n").split("\n")
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if "ЕЖЕДНЕВНЫЙ ОТЧЕТ" in line:
|
if "ЕЖЕДНЕВНЫЙ ОТЧЕТ" in line:
|
||||||
lines.insert(i + 1, f"🖥️ Сервер: {server}")
|
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 "\n".join(lines)
|
||||||
return f"🖥️ Сервер: {server}\n{body}"
|
return f"🖥️ Сервер: {server}\n{body}"
|
||||||
|
|
||||||
@@ -116,6 +154,7 @@ def normalize_active_users_in_body(body: str) -> str:
|
|||||||
user_lines.extend(split_active_user_tokens(cur))
|
user_lines.extend(split_active_user_tokens(cur))
|
||||||
i += 1
|
i += 1
|
||||||
if user_lines:
|
if user_lines:
|
||||||
|
out[-1] = _fix_active_users_header_count(out[-1], len(user_lines))
|
||||||
out.extend(user_lines)
|
out.extend(user_lines)
|
||||||
continue
|
continue
|
||||||
out.append(line)
|
out.append(line)
|
||||||
@@ -126,6 +165,8 @@ def normalize_active_users_in_body(body: str) -> str:
|
|||||||
def normalize_report_body(body: str, host: Host | None, platform: Platform) -> str:
|
def normalize_report_body(body: str, host: Host | None, platform: Platform) -> str:
|
||||||
"""Приводит текст отчёта (агент/SAC) к единому компактному виду."""
|
"""Приводит текст отчёта (агент/SAC) к единому компактному виду."""
|
||||||
text = collapse_blank_lines(body.replace("\r\n", "\n"))
|
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 = ensure_server_line(text, host)
|
||||||
text = normalize_active_users_in_body(text)
|
text = normalize_active_users_in_body(text)
|
||||||
return collapse_blank_lines(text)
|
return collapse_blank_lines(text)
|
||||||
@@ -173,6 +214,11 @@ def build_report_body(
|
|||||||
time_str = when_local.strftime("%d.%m.%Y %H:%M:%S")
|
time_str = when_local.strftime("%d.%m.%Y %H:%M:%S")
|
||||||
top = list(stats.get("top_failed_ips") or [])[:5]
|
top = list(stats.get("top_failed_ips") or [])[:5]
|
||||||
active_users = normalize_active_users_list(list(stats.get("active_users") or []))
|
active_users = normalize_active_users_list(list(stats.get("active_users") or []))
|
||||||
|
agent_version = (
|
||||||
|
str(stats.get("agent_version") or stats.get("product_version") or host.product_version or "")
|
||||||
|
).strip()
|
||||||
|
if sac_generated and not agent_version:
|
||||||
|
agent_version = "sac"
|
||||||
|
|
||||||
if platform == "ssh":
|
if platform == "ssh":
|
||||||
title = "📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА"
|
title = "📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА"
|
||||||
@@ -180,8 +226,10 @@ def build_report_body(
|
|||||||
fail = int(stats.get("failed_logins", stats.get("failed_ssh", 0)))
|
fail = int(stats.get("failed_logins", stats.get("failed_ssh", 0)))
|
||||||
sudo = int(stats.get("sudo_commands", 0))
|
sudo = int(stats.get("sudo_commands", 0))
|
||||||
bans = int(stats.get("active_bans", 0))
|
bans = int(stats.get("active_bans", 0))
|
||||||
lines = [
|
lines = [title]
|
||||||
title,
|
if agent_version:
|
||||||
|
lines.append(format_agent_version_line(agent_version))
|
||||||
|
lines.extend([
|
||||||
f"🖥️ Сервер: {server}",
|
f"🖥️ Сервер: {server}",
|
||||||
f"🕐 Время отчета: {time_str}",
|
f"🕐 Время отчета: {time_str}",
|
||||||
"",
|
"",
|
||||||
@@ -190,14 +238,16 @@ def build_report_body(
|
|||||||
f" ❌ Неудачных попыток SSH: {fail}",
|
f" ❌ Неудачных попыток SSH: {fail}",
|
||||||
f" ⚠️ Команд через sudo: {sudo}",
|
f" ⚠️ Команд через sudo: {sudo}",
|
||||||
f" 🚫 Активных банов (ipset): {bans}",
|
f" 🚫 Активных банов (ipset): {bans}",
|
||||||
]
|
])
|
||||||
else:
|
else:
|
||||||
title = "📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ МОНИТОРИНГА WINDOWS"
|
title = "📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ МОНИТОРИНГА WINDOWS"
|
||||||
ok = int(stats.get("successful_logins", stats.get("rdp_success", 0)))
|
ok = int(stats.get("successful_logins", stats.get("rdp_success", 0)))
|
||||||
fail = int(stats.get("failed_logins", stats.get("rdp_failed", 0)))
|
fail = int(stats.get("failed_logins", stats.get("rdp_failed", 0)))
|
||||||
bans = int(stats.get("active_bans", 0))
|
bans = int(stats.get("active_bans", 0))
|
||||||
lines = [
|
lines = [title]
|
||||||
title,
|
if agent_version:
|
||||||
|
lines.append(format_agent_version_line(agent_version))
|
||||||
|
lines.extend([
|
||||||
f"🖥️ Сервер: {server}",
|
f"🖥️ Сервер: {server}",
|
||||||
f"🕐 Время отчета: {time_str}",
|
f"🕐 Время отчета: {time_str}",
|
||||||
"",
|
"",
|
||||||
@@ -205,7 +255,7 @@ def build_report_body(
|
|||||||
f" ✅ Успешных RDP подключений: {ok}",
|
f" ✅ Успешных RDP подключений: {ok}",
|
||||||
f" ❌ Неудачных попыток RDP: {fail}",
|
f" ❌ Неудачных попыток RDP: {fail}",
|
||||||
f" 🚫 Активных банов: {bans}",
|
f" 🚫 Активных банов: {bans}",
|
||||||
]
|
])
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.extend(_section_top_ips(top))
|
lines.extend(_section_top_ips(top))
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ def test_build_report_body_ssh_matches_agent_layout():
|
|||||||
)
|
)
|
||||||
body = build_report_body("ssh", host, stats, when, sac_generated=True)
|
body = build_report_body("ssh", host, stats, when, sac_generated=True)
|
||||||
assert "ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА" in body
|
assert "ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА" in body
|
||||||
|
assert "Agent version sac" in body
|
||||||
assert "🖥️ Сервер: HaProxy Kalina (192.168.160.117)" in body
|
assert "🖥️ Сервер: HaProxy Kalina (192.168.160.117)" in body
|
||||||
assert " 📈 СТАТИСТИКА" in body
|
assert " 📈 СТАТИСТИКА" in body
|
||||||
assert "Команд через sudo: 5" in body
|
assert "Команд через sudo: 5" in body
|
||||||
@@ -64,6 +65,7 @@ def test_build_report_body_windows_layout():
|
|||||||
)
|
)
|
||||||
body = build_report_body("windows", host, stats, when, sac_generated=True)
|
body = build_report_body("windows", host, stats, when, sac_generated=True)
|
||||||
assert "ЕЖЕДНЕВНЫЙ ОТЧЕТ МОНИТОРИНГА WINDOWS" in body
|
assert "ЕЖЕДНЕВНЫЙ ОТЧЕТ МОНИТОРИНГА WINDOWS" in body
|
||||||
|
assert "Agent version sac" in body
|
||||||
assert "K6A-DC3 (10.0.0.10)" in body
|
assert "K6A-DC3 (10.0.0.10)" in body
|
||||||
assert "Успешных RDP подключений: 1" in body
|
assert "Успешных RDP подключений: 1" in body
|
||||||
assert " 👥 АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ (2)" in body
|
assert " 👥 АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ (2)" in body
|
||||||
@@ -80,7 +82,12 @@ def test_normalize_active_users_list_splits_combined_line():
|
|||||||
|
|
||||||
|
|
||||||
def test_normalize_report_body_adds_server_and_collapses_blanks():
|
def test_normalize_report_body_adds_server_and_collapses_blanks():
|
||||||
host = Host(hostname="srv", display_name="Unimus Kalina", ipv4="192.168.160.17")
|
host = Host(
|
||||||
|
hostname="srv",
|
||||||
|
display_name="Unimus Kalina",
|
||||||
|
ipv4="192.168.160.17",
|
||||||
|
product_version="1.2.10-SAC",
|
||||||
|
)
|
||||||
raw = "\n".join(
|
raw = "\n".join(
|
||||||
[
|
[
|
||||||
"📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА",
|
"📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА",
|
||||||
@@ -92,12 +99,14 @@ def test_normalize_report_body_adds_server_and_collapses_blanks():
|
|||||||
" 📈 СТАТИСТИКА ЗА ПОСЛЕДНИЕ 24 ЧАСА:",
|
" 📈 СТАТИСТИКА ЗА ПОСЛЕДНИЕ 24 ЧАСА:",
|
||||||
" ✅ Успешных SSH подключений: 0",
|
" ✅ Успешных SSH подключений: 0",
|
||||||
"",
|
"",
|
||||||
" 👥 АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ (2):",
|
" 👥 АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ (1):",
|
||||||
" 👤 u1 👤 u2",
|
" 👤 u1 👤 u2",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
body = normalize_report_body(raw, host, "ssh")
|
body = normalize_report_body(raw, host, "ssh")
|
||||||
|
assert "Agent version 1.2.10-SAC" in body
|
||||||
assert "🖥️ Сервер: Unimus Kalina (192.168.160.17)" in body
|
assert "🖥️ Сервер: Unimus Kalina (192.168.160.17)" in body
|
||||||
assert "\n\n\n" not in body
|
assert "\n\n\n" not in body
|
||||||
user_lines = [ln for ln in body.split("\n") if ln.strip().startswith("👤")]
|
user_lines = [ln for ln in body.split("\n") if ln.strip().startswith("👤")]
|
||||||
assert len(user_lines) == 2
|
assert len(user_lines) == 2
|
||||||
|
assert " 👥 АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ (2)" in body
|
||||||
|
|||||||
Reference in New Issue
Block a user