rapid7-insightidr 1.14.4: keep UBA alerts in the full investigation feed

get_investigations_full dropped every alert without an RRN, so a behaviour
(UBA) investigation arrived with alerts: [] and alert_count: 0 and its
reading worked from a title alone. Those alerts are now kept under
alerts_without_rrn as the investigation endpoint returns them, counted in
alert_count, rendered in summary_markdown with where their evidence lives,
and lift first_alert_type / first_alert_source / first_alert_id when they are
all the investigation has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
This commit is contained in:
2026-10-12 07:02:12 +00:00
parent 9605e1f0dc
commit 3e39f17bfe
2 changed files with 56 additions and 3 deletions
@@ -399,6 +399,37 @@ def _evidence_rows(alerts):
return rows, max(dropped, 0)
def _without_rrn(a):
"""An investigation alert the Alert Triage API cannot read, as returned,
with what an agent or analyst needs to know about it."""
out = dict(a)
out["evidence_api"] = False
out["evidence_hint"] = ("Behaviour (UBA) alert: InsightIDR exposes no actors or evidence for it through the "
"API. Its activity is in the authentication logs (Asset Authentication log set), "
"around its event times.")
return out
def _without_rrn_summary(alerts):
"""Markdown for the alerts that have no RRN, so a layout or a reading shows
them instead of an empty investigation."""
lines = []
for a in alerts:
lines.append("### " + str(a.get("title") or a.get("alert_type") or "Behaviour alert"))
meta = []
for k, label in (("alert_type", "type"), ("alert_source", "source"),
("first_event_time", "first event"), ("latest_event_time", "latest event"),
("created_time", "created")):
if a.get(k):
meta.append("%s %s" % (label, a[k]))
if meta:
lines.append(" · ".join(meta))
lines.append("")
lines.append("_" + a["evidence_hint"] + "_")
lines.append("")
return "\n".join(lines).strip()
def _markdown_summary(inv, alerts):
"""A readable rendering of what the detection actually says.
@@ -495,6 +526,12 @@ def run():
# output with an alerts_error: losing the whole run over one of them, or
# dropping it silently, would both be worse.
rrns_by_inv, order, truncated, skipped = {}, [], False, 0
# Alerts with no RRN — behaviour (UBA) alerts such as "attempted to remotely
# access N new assets". The Alert Triage API cannot fetch them, and they
# used to be dropped here: the investigation arrived with alerts: [] and
# alert_count: 0, and every reading of it worked from a title alone. They
# are kept as the investigation endpoint returned them.
no_rrn_by_inv = {}
region_failures, regions_seen = [], set()
for inv in investigations:
inv_rrn = inv.get("rrn") or ""
@@ -514,6 +551,7 @@ def run():
rrn = _alert_rrn(a)
if rrn is None:
skipped += 1
no_rrn_by_inv.setdefault(inv_rrn, []).append(_without_rrn(a))
continue
got.append(rrn)
r = _rrn_region(rrn)
@@ -598,7 +636,10 @@ def run():
rrns = rrns_by_inv.get(inv.get("rrn") or "", [])
alerts = [by_rrn[r] for r in rrns if r in by_rrn]
inv["alerts"] = alerts
inv["alert_count"] = len(rrns)
no_rrn = no_rrn_by_inv.get(inv.get("rrn") or "", [])
inv["alerts_without_rrn"] = no_rrn
# What the investigation holds, not what this run could fetch.
inv["alert_count"] = len(rrns) + len(no_rrn)
# The block reads ctx.process_tree at the top level and draws one chain
# per alert, so every alert's chains are gathered here rather than left
# nested where no mapper expression could reach past alerts[0].
@@ -645,6 +686,18 @@ def run():
if ef:
inv["first_evidence_message"] = ef.get("msg") or ef.get("logdesc") or ef.get("message")
inv["first_evidence_raw_line"] = a.get("evidence_raw_line")
if no_rrn:
# Readable beside the detection alerts, or alone when they are all
# the investigation has — a UBA investigation usually is.
uba_md = _without_rrn_summary(no_rrn)
inv["summary_markdown"] = (inv["summary_markdown"] + "\n\n" + uba_md) if inv.get("summary_markdown") else uba_md
if not alerts:
u = no_rrn[0]
inv.update({
"first_alert_type": u.get("alert_type") or u.get("title"),
"first_alert_source": u.get("alert_source"),
"first_alert_id": u.get("id"),
})
print(json.dumps({
"data": investigations,