rapid7-insightidr 1.14.2: query a log set by name; Static widens UBA windows

query_log_set accepts log_set_name and resolves the id itself (exact name, or
the one set whose name contains it; otherwise refused with the existing
names): on a real run the model mangled a 36-character id it copied. Static
queries by name, retries a 0-event keyword search case-insensitively, takes
the 24 hours before a behaviour alert (they aggregate hours of activity), and
always looks for the user's other investigations.

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-11 22:32:16 +00:00
parent c3b15d1faf
commit 4bd468e671
4 changed files with 60 additions and 19 deletions
@@ -38,22 +38,30 @@ instructions: |
3. Alerts with evidence_api true: get_alert_actors, then get_alert_evidence.
4. Alerts with evidence_api false, and any alert whose evidence did not
answer: the logs.
- list_log_sets (1 call). Pick by the alert type:
- list_log_sets (1 call) to see the names. Then query_log_set with
log_set_name — the NAME, e.g. "Asset Authentication" — never a copied
log_set_id: a 36-character id gets mangled in the copy.
- Pick by the alert type:
* authentication / lateral movement / "new assets" / "Domain
Credentials": the Asset Authentication log set first, then the Active
Directory ones. Never File Access or File Modification for these.
* process / endpoint alerts: the Endpoint Agent log sets.
- query_log_set with a keyword search, no field names:
where("<user, host or IP>"), e.g. where("jdoe"). Always parentheses,
always quotes; no groupby or calculate.
- Window: from 30 minutes before the alert's first seen time to 30
minutes after its last seen time (start_time / end_time, ISO-8601).
The two can be inverted in the data: take the earlier as the start.
Credentials": Asset Authentication first. An Active Directory name
shared by several sets is refused as ambiguous: give the full name of
one domain controller's set.
* process / endpoint alerts: the Endpoint Agent log set.
* Never File Access or File Modification for an authentication alert.
- Keyword search, no field names: where("<user, host or IP>"), e.g.
where("jdoe"). If it returns 0 events, try once more
case-insensitively: where(/jdoe/i). Always parentheses; no groupby or
calculate.
- Window. A behaviour (UBA) alert aggregates activity over hours — its
count of "new assets" keeps growing after it is raised — so take the 24
hours before its first seen time, up to 30 minutes after its last seen
time. Other alerts: 30 minutes on each side. First and last seen can be
inverted in the data: take the earlier as the start.
- "Invalid Query Syntax": report the exact query and error, no variants.
- Keep at most 6 calls for logs.
5. Keep 1 call for search_investigation(actor_user_name=<user>): other
investigations of the same user. It takes start_time / end_time, not
time_range; empty, it covers the last 28 days.
5. ALWAYS call search_investigation(actor_user_name=<user>) once, whatever
the logs gave: other investigations of the same user. It takes
start_time / end_time, not time_range; empty, it covers 28 days.
In your answer, list the hosts and IPs the logs show for the user — that is
what an analyst needs from a "new assets" alert — and say which log set and
File diff suppressed because one or more lines are too long
@@ -65,8 +65,9 @@ def _annotate(a):
else:
a["next"] = ("Behaviour (UBA) alert with no RRN: get_alert, get_alert_actors, get_alert_evidence and "
"get_alert_process_trees answer 404 for it — do not call them. Its evidence is in the "
"authentication logs: list_log_sets, then query_log_set on the Asset Authentication log set "
"(or each Active Directory one) with where(\"<user>\") over the alert's window.")
"authentication logs: query_log_set with log_set_name \"Asset Authentication\" and "
"where(\"<user>\"), over the 24 hours before the alert's first seen time — a behaviour "
"alert aggregates hours of activity.")
try:
@@ -125,6 +125,31 @@ def _identify(events, fallback_log_id, detection_name):
return out
def _log_sets():
"""Every log set of the account, as (id, name)."""
out = request("GET", "log_search/management/logsets")
sets = out.get("logsets", []) if isinstance(out, dict) else []
return [(x.get("id", ""), x.get("name", "")) for x in sets if isinstance(x, dict)]
def _resolve_name(name):
"""The id of the log set called name: exact match first, ignoring case, then
the one log set whose name contains it. Anything else is refused with the
names that do exist, so the next call can pick one rather than guess."""
sets = _log_sets()
want = name.lower()
exact = [i for i, n in sets if n.lower() == want]
if len(exact) == 1:
return exact[0]
partial = [(i, n) for i, n in sets if want in n.lower()]
if len(partial) == 1:
return partial[0][0]
names = sorted(n for _, n in sets)
if partial:
raise ValueError("log_set_name %r matches several log sets: %s" % (name, ", ".join(sorted(n for _, n in partial))))
raise ValueError("no log set is called %r. Log sets: %s" % (name, ", ".join(names)))
def run():
inp = _inputs()
start_time = _to_ms(inp.get("start_time"))
@@ -152,8 +177,14 @@ def run():
# A pasted ID often drags a quote or a space along; quoted into the path it
# names no log set, and the API answers a bare 404.
log_set_id = str(inp.get("log_set_id") or "").strip().strip("\"'").strip()
name = str(inp.get("log_set_name") or "").strip()
if name:
# A model copying a 36-character id from one answer into the next call
# drops or swaps characters — seen on a real run, where the "Asset
# Authentication" query went to a mangled id. A name it copies right.
log_set_id = _resolve_name(name)
if not log_set_id:
raise ValueError("log_set_id is required (list_log_sets gives it)")
raise ValueError("log_set_id or log_set_name is required (list_log_sets gives both)")
first = request("GET", "log_search/query/logsets/" + urllib.parse.quote(log_set_id, safe=""), params=params)
# No fallback log id: a log set spans several logs and every event names its own.
events = _identify(_collect(first, limit), "", inp.get("detection_name"))