fix: reconcile daily report active users count and list (0.3.5)

Normalize empty-session placeholders; sync stats with report body after ingest.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 14:41:47 +10:00
parent 8b52c5fe3c
commit 64b942447b
5 changed files with 128 additions and 6 deletions
+68 -2
View File
@@ -16,6 +16,10 @@ SSH_BAN_TYPES = frozenset({"ssh.ip.banned"})
_SERVER_LINE_RE = re.compile(r"(?m)^🖥️\s*Сервер\s*:")
_ACTIVE_USERS_HEADER_RE = re.compile(r"^👥\s*АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ")
_ACTIVE_USERS_EMPTY_LINE_RE = re.compile(
r"^\(?нет данных|нет активных пользователей",
re.IGNORECASE,
)
_AGENT_VERSION_LINE_RE = re.compile(r"(?m)^Agent version\s+", re.IGNORECASE)
_NOTIFICATION_SOURCE_RE = re.compile(r"(?m)^📡\s*Оповещение:\s*")
_LEGACY_SAC_SOURCE_RE = re.compile(
@@ -130,10 +134,21 @@ def collapse_blank_lines(text: str, *, max_run: int = 1) -> str:
return "\n".join(out).strip()
def is_active_users_empty_line(text: str) -> bool:
"""Строка-заглушка вместо списка сессий (агент или SAC)."""
s = text.strip()
if not s:
return True
inner = s.lstrip("👤").strip()
if _ACTIVE_USERS_EMPTY_LINE_RE.search(inner):
return True
return False
def split_active_user_tokens(entry: str) -> list[str]:
"""Разбивает «👤 u1 👤 u2» на отдельные строки (как в отчёте агента)."""
entry = entry.strip()
if not entry:
if not entry or is_active_users_empty_line(entry):
return []
if entry.count("👤") <= 1:
line = entry if entry.startswith("👤") else f"👤 {entry}"
@@ -231,21 +246,71 @@ def normalize_active_users_in_body(body: str) -> str:
break
if _SECTION_HEADER_RE.match(stripped) and not stripped.startswith("👤"):
break
if stripped.startswith("(нет данных"):
if is_active_users_empty_line(stripped):
out[-1] = _fix_active_users_header_count(out[-1], 0)
out.append(cur)
i += 1
break
user_lines.extend(split_active_user_tokens(cur))
i += 1
user_lines = [ln for ln in user_lines if not is_active_users_empty_line(ln.strip())]
if user_lines:
out[-1] = _fix_active_users_header_count(out[-1], len(user_lines))
out.extend(user_lines)
else:
out[-1] = _fix_active_users_header_count(out[-1], 0)
continue
out.append(line)
i += 1
return "\n".join(out)
def extract_active_users_from_report_body(body: str) -> list[str]:
lines = body.replace("\r\n", "\n").split("\n")
in_section = False
users: list[str] = []
for line in lines:
stripped = line.strip()
if _ACTIVE_USERS_HEADER_RE.match(stripped):
in_section = True
continue
if not in_section:
continue
if not stripped:
break
if is_active_users_empty_line(stripped):
break
if stripped.startswith("Источник:") or stripped.startswith(NOTIFICATION_SOURCE_PREFIX):
break
if _SECTION_HEADER_RE.match(stripped) and not stripped.startswith("👤"):
break
users.extend(split_active_user_tokens(stripped))
return normalize_active_users_list(users)
def reconcile_stats_from_report_body(
stats: dict[str, Any],
body: str,
platform: Platform,
) -> dict[str, Any]:
"""Синхронизирует stats.active_users и счётчики с текстом отчёта после нормализации."""
out = dict(stats)
users = extract_active_users_from_report_body(body)
out["active_users"] = users
count = len(users)
if platform == "ssh":
out["active_sessions"] = count
else:
out["active_sessions_rdp"] = count
names: list[str] = []
for raw in users:
name = re.sub(r"^👤\s*", "", raw.strip()).strip()
if name and not is_active_users_empty_line(name):
names.append(name)
out["unique_users"] = sorted(set(names))
return out
def normalize_report_body(body: str, host: Host | None, platform: Platform) -> str:
"""Приводит текст отчёта (агент/SAC) к единому компактному виду."""
text = collapse_blank_lines(body.replace("\r\n", "\n"))
@@ -422,5 +487,6 @@ def normalize_daily_report_details(
stats = out.get("stats")
if isinstance(stats, dict):
stats = enrich_stats_for_storage(platform, dict(stats))
stats = reconcile_stats_from_report_body(stats, normalized_body, platform)
out["stats"] = stats
return out