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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user