feat: show notification source (agent vs SAC) in alerts and reports
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,8 +17,92 @@ 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)
|
||||
_NOTIFICATION_SOURCE_RE = re.compile(r"(?m)^📡\s*Оповещение:\s*")
|
||||
_LEGACY_SAC_SOURCE_RE = re.compile(
|
||||
r"(?m)^Источник:\s*Security Alert Center\b.*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_DAILY_REPORT_TITLE_MARKER = "ЕЖЕДНЕВНЫЙ ОТЧЕТ"
|
||||
_SECTION_HEADER_RE = re.compile(r"^[📈🧾👥🖥️🕐]")
|
||||
NOTIFICATION_SOURCE_PREFIX = "📡 Оповещение:"
|
||||
|
||||
|
||||
def format_notification_source_plain(
|
||||
*,
|
||||
generated_by: str,
|
||||
product: str | None = None,
|
||||
product_version: str | None = None,
|
||||
) -> str:
|
||||
gb = (generated_by or "agent").strip().lower()
|
||||
if gb == "sac":
|
||||
return f"{NOTIFICATION_SOURCE_PREFIX} SAC (Security Alert Center)"
|
||||
label = (product or "агент").strip()
|
||||
version = (product_version or "").strip()
|
||||
if version:
|
||||
return f"{NOTIFICATION_SOURCE_PREFIX} агент ({label} {version})"
|
||||
return f"{NOTIFICATION_SOURCE_PREFIX} агент ({label})"
|
||||
|
||||
|
||||
def has_notification_source_line(text: str) -> bool:
|
||||
return bool(_NOTIFICATION_SOURCE_RE.search(text or ""))
|
||||
|
||||
|
||||
def strip_legacy_sac_source_line(text: str) -> str:
|
||||
return _LEGACY_SAC_SOURCE_RE.sub("", text or "").strip()
|
||||
|
||||
|
||||
def append_notification_source_plain(
|
||||
body: str,
|
||||
*,
|
||||
generated_by: str,
|
||||
product: str | None = None,
|
||||
product_version: str | None = None,
|
||||
) -> str:
|
||||
text = strip_legacy_sac_source_line(body or "")
|
||||
if has_notification_source_line(text):
|
||||
return text
|
||||
line = format_notification_source_plain(
|
||||
generated_by=generated_by,
|
||||
product=product,
|
||||
product_version=product_version,
|
||||
)
|
||||
if text:
|
||||
return f"{text.rstrip()}\n\n{line}"
|
||||
return line
|
||||
|
||||
|
||||
def append_notification_source_html(
|
||||
html_msg: str,
|
||||
*,
|
||||
generated_by: str,
|
||||
product: str | None = None,
|
||||
product_version: str | None = None,
|
||||
) -> str:
|
||||
plain_line = format_notification_source_plain(
|
||||
generated_by=generated_by,
|
||||
product=product,
|
||||
product_version=product_version,
|
||||
)
|
||||
if NOTIFICATION_SOURCE_PREFIX in (html_msg or ""):
|
||||
return (html_msg or "").rstrip()
|
||||
line = html.escape(plain_line)
|
||||
msg = (html_msg or "").rstrip()
|
||||
if msg:
|
||||
return f"{msg}\n{line}"
|
||||
return line
|
||||
|
||||
|
||||
def resolve_product_label(host: Host | None, platform: Platform | None = None) -> tuple[str | None, str | None]:
|
||||
if host is None:
|
||||
return None, None
|
||||
product = (host.product or "").strip()
|
||||
if not product:
|
||||
if platform == "ssh":
|
||||
product = "ssh-monitor"
|
||||
elif platform == "windows":
|
||||
product = "rdp-login-monitor"
|
||||
version = (host.product_version or "").strip() or None
|
||||
return (product or None), version
|
||||
|
||||
|
||||
def host_server_line(host: Host | None) -> str:
|
||||
@@ -101,7 +185,7 @@ def ensure_agent_version_line(body: str, version: str | None) -> str:
|
||||
|
||||
def _fix_active_users_header_count(header_line: str, user_count: int) -> str:
|
||||
stripped = header_line.rstrip()
|
||||
if _ACTIVE_USERS_COUNT_RE.match(stripped):
|
||||
if _ACTIVE_USERS_HEADER_RE.match(stripped.strip()):
|
||||
return re.sub(
|
||||
r"(\👥\s*АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ\s*\()[^)]+(\))",
|
||||
rf"\g<1>{user_count}\2",
|
||||
@@ -143,7 +227,7 @@ def normalize_active_users_in_body(body: str) -> str:
|
||||
stripped = cur.strip()
|
||||
if not stripped:
|
||||
break
|
||||
if stripped.startswith("Источник:"):
|
||||
if stripped.startswith("Источник:") or stripped.startswith(NOTIFICATION_SOURCE_PREFIX):
|
||||
break
|
||||
if _SECTION_HEADER_RE.match(stripped) and not stripped.startswith("👤"):
|
||||
break
|
||||
@@ -262,9 +346,18 @@ def build_report_body(
|
||||
lines.append("")
|
||||
lines.extend(_section_active_users(active_users, sac_generated=sac_generated))
|
||||
|
||||
if sac_generated:
|
||||
lines.append("")
|
||||
lines.append("Источник: Security Alert Center (агрегация ingest за 24 ч).")
|
||||
generated_by = "sac" if sac_generated else "agent"
|
||||
product, version = resolve_product_label(host, platform)
|
||||
if not sac_generated and agent_version:
|
||||
version = agent_version
|
||||
lines.append("")
|
||||
lines.append(
|
||||
format_notification_source_plain(
|
||||
generated_by=generated_by,
|
||||
product=product,
|
||||
product_version=version,
|
||||
)
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -307,6 +400,21 @@ def normalize_daily_report_details(
|
||||
return details
|
||||
|
||||
normalized_body = normalize_report_body(body, host, platform)
|
||||
gb = str(details.get("generated_by") or "agent").strip().lower()
|
||||
if gb not in ("agent", "sac"):
|
||||
gb = "agent"
|
||||
product, version = resolve_product_label(host, platform)
|
||||
stats_raw = details.get("stats")
|
||||
if isinstance(stats_raw, dict) and gb == "agent":
|
||||
av = stats_raw.get("agent_version") or stats_raw.get("product_version")
|
||||
if av:
|
||||
version = str(av).strip()
|
||||
normalized_body = append_notification_source_plain(
|
||||
normalized_body,
|
||||
generated_by=gb,
|
||||
product=product,
|
||||
product_version=version,
|
||||
)
|
||||
out = dict(details)
|
||||
out["report_body"] = normalized_body
|
||||
out["report_html"] = body_to_report_html(normalized_body)
|
||||
|
||||
Reference in New Issue
Block a user