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:
@@ -38,22 +38,30 @@ instructions: |
|
|||||||
3. Alerts with evidence_api true: get_alert_actors, then get_alert_evidence.
|
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
|
4. Alerts with evidence_api false, and any alert whose evidence did not
|
||||||
answer: the logs.
|
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
|
* authentication / lateral movement / "new assets" / "Domain
|
||||||
Credentials": the Asset Authentication log set first, then the Active
|
Credentials": Asset Authentication first. An Active Directory name
|
||||||
Directory ones. Never File Access or File Modification for these.
|
shared by several sets is refused as ambiguous: give the full name of
|
||||||
* process / endpoint alerts: the Endpoint Agent log sets.
|
one domain controller's set.
|
||||||
- query_log_set with a keyword search, no field names:
|
* process / endpoint alerts: the Endpoint Agent log set.
|
||||||
where("<user, host or IP>"), e.g. where("jdoe"). Always parentheses,
|
* Never File Access or File Modification for an authentication alert.
|
||||||
always quotes; no groupby or calculate.
|
- Keyword search, no field names: where("<user, host or IP>"), e.g.
|
||||||
- Window: from 30 minutes before the alert's first seen time to 30
|
where("jdoe"). If it returns 0 events, try once more
|
||||||
minutes after its last seen time (start_time / end_time, ISO-8601).
|
case-insensitively: where(/jdoe/i). Always parentheses; no groupby or
|
||||||
The two can be inverted in the data: take the earlier as the start.
|
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.
|
- "Invalid Query Syntax": report the exact query and error, no variants.
|
||||||
- Keep at most 6 calls for logs.
|
- Keep at most 6 calls for logs.
|
||||||
5. Keep 1 call for search_investigation(actor_user_name=<user>): other
|
5. ALWAYS call search_investigation(actor_user_name=<user>) once, whatever
|
||||||
investigations of the same user. It takes start_time / end_time, not
|
the logs gave: other investigations of the same user. It takes
|
||||||
time_range; empty, it covers the last 28 days.
|
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
|
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
|
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:
|
else:
|
||||||
a["next"] = ("Behaviour (UBA) alert with no RRN: get_alert, get_alert_actors, get_alert_evidence and "
|
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 "
|
"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 "
|
"authentication logs: query_log_set with log_set_name \"Asset Authentication\" and "
|
||||||
"(or each Active Directory one) with where(\"<user>\") over the alert's window.")
|
"where(\"<user>\"), over the 24 hours before the alert's first seen time — a behaviour "
|
||||||
|
"alert aggregates hours of activity.")
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -125,6 +125,31 @@ def _identify(events, fallback_log_id, detection_name):
|
|||||||
return out
|
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():
|
def run():
|
||||||
inp = _inputs()
|
inp = _inputs()
|
||||||
start_time = _to_ms(inp.get("start_time"))
|
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
|
# 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.
|
# names no log set, and the API answers a bare 404.
|
||||||
log_set_id = str(inp.get("log_set_id") or "").strip().strip("\"'").strip()
|
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:
|
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)
|
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.
|
# 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"))
|
events = _identify(_collect(first, limit), "", inp.get("detection_name"))
|
||||||
|
|||||||
Reference in New Issue
Block a user