feat: host inventory (agent.inventory), host detail page, UI columns

Store hardware/software snapshots on hosts; warn on hardware changes. Host card from Hosts list. Add agent version column to Events/Overview; rename Hosts table columns and sort by status.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-05 09:53:00 +10:00
parent 5d3cda2ab6
commit 54337a5917
23 changed files with 826 additions and 17 deletions
@@ -333,6 +333,50 @@ def format_smb_admin_share_html(event: Event) -> str:
return msg.rstrip()
def _format_inventory_change_line(change: dict[str, Any]) -> str:
field = html_escape(str(change.get("field") or "?"))
old = change.get("old")
new = change.get("new")
return f"• <b>{field}</b>: {html_escape(old)}{html_escape(new)}"
def format_agent_inventory_html(event: Event) -> str:
details = _details_dict(event)
changes = details.get("hardware_changes")
if isinstance(changes, list) and changes:
msg = "<b>⚠️ Изменилось железо хоста</b>\n"
msg += _line("🏢", "Хост", host_label(event.host))
msg += _line("🕐", "Время", format_time(event.occurred_at))
msg += "\n"
for item in changes:
if isinstance(item, dict):
msg += _format_inventory_change_line(item) + "\n"
if event.summary:
msg += f"\n{html_escape(event.summary)}"
return msg.rstrip()
inv = details.get("inventory")
if not isinstance(inv, dict):
inv = {}
msg = "<b>🖥️ Инвентаризация хоста</b>\n"
msg += _line("🏢", "Хост", host_label(event.host))
windows = inv.get("windows") if isinstance(inv.get("windows"), dict) else {}
os_name = windows.get("product_name") or event.host.os_version if event.host else None
if os_name:
msg += _line("💿", "ОС", html_escape(os_name))
mem = inv.get("memory_gb")
if mem is not None:
msg += _line("🧠", "RAM", f"{html_escape(mem)} GB")
procs = inv.get("processor")
if isinstance(procs, list) and procs:
names = [str(p.get("name") or "").strip() for p in procs if isinstance(p, dict)]
names = [n for n in names if n]
if names:
msg += _line("⚙️", "CPU", html_escape("; ".join(names)))
msg += _line("🕐", "Время", format_time(event.occurred_at))
return msg.rstrip()
def format_generic_event_html(event: Event) -> str:
sev = event.severity.upper()
msg = f"<b>🚨 SAC: {html_escape(event.title)}</b>\n"
@@ -453,6 +497,8 @@ def _format_event_body_html(event: Event) -> str:
return format_winrm_session_html(event)
if event.type.startswith("smb."):
return format_smb_admin_share_html(event)
if event.type == "agent.inventory":
return format_agent_inventory_html(event)
return format_generic_event_html(event)