rapid7-insightidr 1.14.9: a user_authentications answer that fits

Riposte gives a tool result 8000 characters; two full summaries ran to 11903
and were cut at 3903, the asset list half gone. The answer now keeps only the
fields an authentication question is answered from (destination and source
asset and address, domain, result, logon type, service) and lists up to 40
new destination assets in full: about 2.5 KB for 1000 events.

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 08:14:10 +00:00
parent fb560cc937
commit 80de5399a6
2 changed files with 36 additions and 3 deletions
File diff suppressed because one or more lines are too long
@@ -268,6 +268,39 @@ def _norm_account(v):
return v
# The fields an authentication question is answered from, matched by name so a
# source that prefixes or nests them still lands here. Everything else — event
# ids, timestamps, SIDs, the vendor payload — is left out: Riposte gives one
# tool result 8000 characters, and the first version of this answer, two full
# summaries, was cut at 3903 of 11903 with the asset list half gone.
_KEY_FIELDS = ("destination_asset", "source_asset", "destination_domain", "result", "logon_type", "service")
def _compact(events, top_assets):
"""Counts for the key fields only; destination assets listed in full."""
counts = {}
first_ts, last_ts = None, None
for e in events:
ts = e.get("timestamp")
if isinstance(ts, (int, float)):
first_ts = ts if first_ts is None or ts < first_ts else first_ts
last_ts = ts if last_ts is None or ts > last_ts else last_ts
for k, v in _fields(e).items():
kl = k.lower()
if any(f in kl for f in _KEY_FIELDS) and "sid" not in kl:
counts.setdefault(k, {})
key = str(v)[:120]
counts[k][key] = counts[k].get(key, 0) + 1
out = {"count": len(events),
"first_event": _iso(first_ts) if first_ts is not None else None,
"last_event": _iso(last_ts) if last_ts is not None else None}
for k, vals in sorted(counts.items()):
ranked = sorted(vals.items(), key=lambda x: -x[1])
n = top_assets if "destination_asset" in k.lower() and "address" not in k.lower() else 5
out[k] = {"distinct": len(vals), "top": [[v, c] for v, c in ranked[:n]]}
return out
def _is_users(fields, user):
"""The event is this user's: an account or user field holds the account.
@@ -331,8 +364,8 @@ def run():
"user_matches": len(events),
"truncated": len(matched) >= limit,
"window": "%s to %s" % (_iso(start_time), _iso(end_time)),
"all": _summarize(events, limit),
"new_authentications": _summarize(new, limit),
"all": _compact(events, 10),
"new_authentications": _compact(new, 40),
"note": ("all and new_authentications hold only this user's events (an account field equals "
"the user): keyword_matches counts every line the text appeared in, other accounts "
"included. new_authentications holds the events Rapid7 marked as a first "