feat(tickets): open, comment and resolve tickets from the UI and MCP; known issues on alerts and anomalies (#6351, #6352)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m37s
CI / integration (push) Successful in 1m51s
CI / publish (push) Successful in 1m16s

A ticket records a known problem: title, area, hosts, subject, description,
links to alert rules and anomalies, and a log of comments, edits, resolving
and reopening. Opening, resolving (with the resolution as the note) and
reopening go on the timeline; every step is audited, and the audit rows stay
off the timeline.

An alert or anomaly an open ticket covers (by link, by subject, or a
host-wide ticket on the host itself) shows a Known issue badge, and offers
"Open a ticket" prefilled from its rule or anomaly when none does. Hosts,
monitors and containers get a Known issues card.

MCP: list_tickets, get_ticket (viewer); open_ticket, update_ticket,
comment_ticket, resolve_ticket, reopen_ticket (operator). list_alerts and
list_anomalies carry known_issue; get_overview, get_host, get_monitor and
get_container carry open_tickets. Also update_change and delete_change for
changes recorded by hand (author or admin).

Tickets are left out of backups, like the timeline (2026-10-15 ruling).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:06:43 -04:00
co-authored by Claude Opus 5.5
parent 1ea86c0330
commit 2a561210c3
40 changed files with 1786 additions and 35 deletions
+10 -1
View File
@@ -222,7 +222,8 @@ async def active_rows(db, now: datetime) -> list[dict]:
"""The alerts page's active list, firing first, then no data, then
acknowledged, then pending; within each the longest-running first. Each
row: its state, rule, label, sentence, the link to what it watches, when
the breach began, and who acknowledged it."""
the breach began, who acknowledged it, and the open tickets that make it
a known issue."""
alerts = await active_alerts(db)
started = await breach_started(db, {rule.id for state, rule in alerts
if state.state.value in _BREACHING})
@@ -245,6 +246,14 @@ async def active_rows(db, now: datetime) -> list[dict]:
"says": alert_sentence(rule, label, value, since, now),
"url": await resource_url(db, rule),
})
# An open ticket on the rule (or its covering rule) or on what it
# watches makes it a known issue.
from steward.tickets.service import known_issues
known = await known_issues(db, {
r["rule"].id: ([("alert_rule", r["rule"].id), ("alert_rule", r["rule"].parent_id or "")],
r["rule"].resource_name) for r in rows})
for r in rows:
r["tickets"] = known.get(r["rule"].id, [])
epoch = datetime.max.replace(tzinfo=timezone.utc)
rows.sort(key=lambda r: (ACTIVE_ORDER.get(r["label"], 9), r["since"] or epoch))
return rows