rapid7-insightidr 1.14.3: the log window is computed from the alert's time

query_log_set takes anchor_time with hours_before / minutes_after and computes
the window itself, and returns it as text: on a real run the agent computed
"24 hours before 2026-10-11T00:04Z" as ending on the 10th and reported a
day-old window. Static passes the alert's first seen time instead of dates,
and lists domain controllers apart from the new assets.

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:38:42 +00:00
parent 4bd468e671
commit 2245d5500e
4 changed files with 39 additions and 11 deletions
@@ -52,11 +52,11 @@ instructions: |
where("jdoe"). If it returns 0 events, try once more where("jdoe"). If it returns 0 events, try once more
case-insensitively: where(/jdoe/i). Always parentheses; no groupby or case-insensitively: where(/jdoe/i). Always parentheses; no groupby or
calculate. calculate.
- Window. A behaviour (UBA) alert aggregates activity over hours — its - Window: never compute dates yourself. Pass anchor_time = the alert's
count of "new assets" keeps growing after it is raised — so take the 24 first seen time exactly as the data gives it, and the window is
hours before its first seen time, up to 30 minutes after its last seen computed for you. A behaviour (UBA) alert aggregates hours of activity:
time. Other alerts: 30 minutes on each side. First and last seen can be hours_before 24, minutes_after 30 (the defaults). Other alerts:
inverted in the data: take the earlier as the start. hours_before 0.5. Report the "window" the answer returns, not your own.
- "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. ALWAYS call search_investigation(actor_user_name=<user>) once, whatever 5. ALWAYS call search_investigation(actor_user_name=<user>) once, whatever
@@ -64,8 +64,10 @@ instructions: |
start_time / end_time, not time_range; empty, it covers 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,
query produced them. query and window produced them. Domain controllers (names with "dc") are
where every logon is checked: list them apart, they are not the "new
assets" the alert is about.
commands: commands:
- list_investigation_alerts - list_investigation_alerts
File diff suppressed because one or more lines are too long
@@ -66,8 +66,8 @@ def _annotate(a):
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: query_log_set with log_set_name \"Asset Authentication\" and " "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 " "where(\"<user>\") and anchor_time = this alert's first seen time: the 24 hours before it "
"alert aggregates hours of activity.") "are queried — a behaviour alert aggregates hours of activity.")
try: try:
@@ -55,6 +55,18 @@ def _to_ms(v):
return None return None
def _num(v, default):
try:
return float(v) if v not in (None, "") else default
except (TypeError, ValueError):
return default
def _iso(ms):
"""The window as it was queried, in the form the agent reports it."""
return datetime.datetime.fromtimestamp(ms / 1000, tz=datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def request(method, url, params=None): def request(method, url, params=None):
base, headers = _cfg() base, headers = _cfg()
full = url if url.startswith("http") else base + url.lstrip("/") full = url if url.startswith("http") else base + url.lstrip("/")
@@ -156,6 +168,16 @@ def run():
end_time = _to_ms(inp.get("end_time")) end_time = _to_ms(inp.get("end_time"))
if inp.get("time_range"): if inp.get("time_range"):
start_time, end_time = _rel_ms_range(inp.get("time_range")) start_time, end_time = _rel_ms_range(inp.get("time_range"))
# An agent copies a timestamp right and gets date arithmetic wrong: on a real
# run "24 hours before 2026-10-11T00:04Z" came out ending on the 10th, and
# the events it found were a day too early. Given the alert's own time, the
# window is computed here.
anchor = _to_ms(inp.get("anchor_time"))
if anchor is not None:
before = _num(inp.get("hours_before"), 24.0)
after = _num(inp.get("minutes_after"), 30.0)
start_time = int(anchor - before * 3600 * 1000)
end_time = int(anchor + after * 60 * 1000)
# Log Search wants both bounds. A scheduled run hands over start_time alone # Log Search wants both bounds. A scheduled run hands over start_time alone
# (the watermark), and the very first run may hand over nothing at all. # (the watermark), and the very first run may hand over nothing at all.
now_ms = int(time.time() * 1000) now_ms = int(time.time() * 1000)
@@ -188,7 +210,8 @@ def run():
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"))
print(json.dumps({"events": events, "count": len(events), "from": start_time, "to": end_time})) print(json.dumps({"events": events, "count": len(events), "from": start_time, "to": end_time,
"window": "%s to %s" % (_iso(start_time), _iso(end_time))}))
try: try: