From c3b15d1faff96dd4a53f5e31bb094c63de62796c Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sun, 11 Oct 2026 22:20:58 +0000 Subject: [PATCH] rapid7-insightidr 1.14.1: mark UBA alerts in the data, give Static a budget list_investigation_alerts now marks each alert with alert_rrn, evidence_api and next: a behaviour alert with no RRN says, in the output the agent reads, that the Alert Triage commands answer 404 for it and that its evidence is in the authentication logs. Static still called them when only its instructions said so. Its sheet gets 14 calls spent in a stated order, authentication log sets for lateral-movement alerts (never file access), and a window built from first and last seen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ --- .../rapid7-insightidr/agents/operator.yaml | 59 ++++++++++--------- integrations/rapid7-insightidr/manifest.yaml | 2 +- .../scripts/list_investigation_alerts.py | 28 +++++++++ 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/integrations/rapid7-insightidr/agents/operator.yaml b/integrations/rapid7-insightidr/agents/operator.yaml index b140402..4604e3f 100644 --- a/integrations/rapid7-insightidr/agents/operator.yaml +++ b/integrations/rapid7-insightidr/agents/operator.yaml @@ -6,7 +6,7 @@ handle: Static title: Rapid7 InsightIDR operator role: analyst_l2 # needs integrations.execute — viewer would hide every command approval_threshold: safe_write -max_tool_calls: 10 +max_tool_calls: 14 when_to_delegate: | Anything an InsightIDR investigation can answer: the alerts it holds, the @@ -25,34 +25,39 @@ returns: | No verdict. instructions: | - ALWAYS start here, whatever you were asked: - 1. Read the Riposte alert with get_incident_context. Its source_ref is the + RULE ONE. list_investigation_alerts marks every alert with evidence_api and + next. When evidence_api is false (a behaviour/UBA alert, plain UUID id), NEVER + call get_alert, get_alert_actors, get_alert_evidence or + get_alert_process_trees: they answer 404 and waste your budget. Follow its + "next" instead. + + Your budget is 14 calls. Spend it in this order: + 1. get_incident_context (1 call): the Riposte alert. Its source_ref is the investigation RRN (rrn:investigation:...), never an alert RRN. - 2. list_investigation_alerts(investigation_id=) gives the - investigation's alerts. - 3. An alert whose id or rrn starts with rrn:alerts: -> get_alert_actors, then - get_alert_evidence, with that RRN. get_alert_process_trees only for - endpoint (process) alerts. - 4. An alert with a plain UUID id has no RRN: it is a behaviour (UBA) alert - such as "attempted to remotely access N new assets". The Alert Triage - commands answer 404 for it — do not call them with a UUID or with the - investigation RRN. Go to the logs instead (below). - Only then answer the question you were asked. + 2. list_investigation_alerts(investigation_id=) (1 call). + 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: + * 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(""), 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. + - "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=): other + investigations of the same user. It takes start_time / end_time, not + time_range; empty, it covers the last 28 days. - Log searches, only when the alert's own evidence does not answer: - - Take the log set id from list_log_sets; never invent one. - - LEQL keyword search, no field names: where(""), - e.g. where("jdoe") - Always parentheses, always quotes. "where x = y" without parentheses is - rejected as Invalid Query Syntax. No groupby or calculate. - - Window: the alert's first and last seen times, widened by 30 minutes on - each side (start_time / end_time, ISO-8601). - - If a query returns "Invalid Query Syntax", do not try variants: report the - exact query and the error. - - Same user or host elsewhere: search_investigation with actor_user_name or - actor_asset_hostname. It takes start_time / end_time, not time_range; left - empty it covers the last 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 + query produced them. commands: - list_investigation_alerts diff --git a/integrations/rapid7-insightidr/manifest.yaml b/integrations/rapid7-insightidr/manifest.yaml index 8fd5415..7d441ae 100644 --- a/integrations/rapid7-insightidr/manifest.yaml +++ b/integrations/rapid7-insightidr/manifest.yaml @@ -1,6 +1,6 @@ id: rapid7_insightidr name: Rapid7 InsightIDR -version: 1.14.0 +version: 1.14.1 description: "Rapid7 InsightIDR (REST API) — investigation ingestion and full lifecycle (list/get/search/create/update/assign/set-status/bulk-close), investigation alerts and Rapid7 product alerts, investigation comments (add/list), custom threat indicators (add/replace), log management and LEQL log/log-set queries with downloads, and user directory search." changelog: "1.13.2 — query_log and query_log_set: trim the quotes and spaces a pasted log or log set ID drags along, and explain the 404. A trailing quote was URL-encoded into the path and the API answered a bare \"HTTP 404 Not Found\" that named nothing. Also send both time bounds on every run: a scheduled run passes only the watermark, and the first run nothing — the window now ends now and, without a start, opens 24 hours back. | 1.13.1 — test_connection: accept an InsightIDR Essential key. Essential answers the Investigations API with a 403 that names the package, so a valid key failed its test; the test now falls back to Log Search and says which commands that package leaves out — the LEQL feeds (query_log, query_log_set) are its way to ingest detections. | 1.13.0 — Add add_comment and list_comments: the investigation comment thread is where an analyst’s reasoning lives, and a playbook had no way to write into it or read it back. The Comments API is v1-only and addresses its target by RRN — the short investigation ID the other v1 endpoints accept is rejected with a 400 that names nothing, so both commands refuse it up front. | 1.12.0 — Make the LEQL queries ingestible: query_log and query_log_set gain an ingest block, a stable event_id for dedup, a bounded page walk and a detection_name stamp, and they now read start_time as ISO-8601, UNIX seconds or milliseconds alike. A tenant whose basic detection rules only notify (email, Slack) creates no investigation and no alert an API can read, so replaying the rule's own query on a schedule is the only way its detections reach a SOAR. | 1.11.1 — Strip the whitespace out of the comma-separated filter params (statuses, priorities, sources, tags) before the URL. \"HIGH, CRITICAL\" as typed sent \" CRITICAL\" to the API, which rejects the whole call — so a priority filter written naturally fetched nothing. | 1.11.0 —Build one evidence row per alert evidence into custom:rapid7_evidence, for the table field: sortable and filterable where the Markdown summary only reads. | 1.10.0 — Render every detection key as Markdown into custom:rapid7_summary, so a layout can show what the OCSF catalogue has no field for. Map both ends of an account action, read source_json top-level scalars, drop placeholder values, and force evidences to stay an array. | 1.9.2 — Read the evidence data in both of its shapes. A parsed source states its event flat, with no source_data/custom_data wrapper, and the parser returned {} for all of them. | 1.9.1 — get_alert_process_trees: explain the 404, which the API returns with an empty body for three different situations. | 1.9.0 — Fetch each alert process tree and flatten it into ctx.process_tree, the shape the layout Process Tree block reads. Gated on the event type by default, since an authentication alert never has one. | 1.8.0 — Fetch each alert evidence at ingestion and parse it: the log line behind the alert lives in a JSON-encoded string no mapper can reach, and it is where the source and destination addresses, the account and the device actually are. | 1.7.0 — Map source/destination assets and accounts to src_endpoint, dst_endpoint, actor.user and user. A connection rule names both ends and neither is \"the host\", so an RDP-to-DC alert produced no entity at all and nothing a layout could show. | 1.6.0 — Surface region_failures: the Alert Triage API answers HTTP 200 with a partial result when it cannot reach a region, and that list was being dropped. Also report the regions seen in the RRNs against the configured one. | 1.5.0 — Flatten the whole of rule_keys_of_interest into the context. It is an open vocabulary — a process rule and an authentication rule share no keys — so the hardcoded list only covered rules already seen and left every other alert empty. | 1.4.1 — Read the alert RRN from `rrn` before `id` and drop anything not RRN-shaped: the investigation alert list also returns bare UUIDs, and one of those rejects the entire /alerts/ops/rrns batch (HTTP 400), losing every alert in the run. | 1.4.0 — Add get_investigations_full: the investigation with its alerts inlined, so it stays the unit that becomes an incident. Drop-in for list_investigations (results_path data, dedup rrn). | 1.3.0 — Add search_investigation_alerts: ingest only the alerts that belong to an investigation, with their full detection content, in 1+N+1 calls. Filtering search_alerts on investigation_rrn does not work — the term is accepted and silently matches nothing. | 1.2.1 — search_alerts: report a malformed terms_json with the input named and echoed, instead of json's bare \"Expecting value: line 1 column 1\". | 1.2.0 — Add the search_alerts OCSF mapper: rule_keys_of_interest carries the host, the process, its command line, its parent and the file hashes, so an alert-based feed indexes entities where the investigation feed indexed none. Validated against a live ABA alert. | 1.1.1 — list_alert_fields: default path_depth to 1 and expose the search filter. The V2 fields endpoint refuses an unbounded walk of the tree (\"root path with path depth greater than 1 is not supported for V2 without a search filter\"), so the bare call returned HTTP 400. | 1.1.0 — Add the Alert Triage API (/idr/at): search alerts, get an alert, its evidence, its actors and its process trees, plus the tenant's alert field catalogue. These carry the detection content the investigation endpoints do not — MITRE T-codes, matched rule keys, assets and accounts, command lines. MDR-gated. | 1.0.0 — Initial release: investigation ingestion (list_investigations) with an exhaustive OCSF mapper, 18 commands across investigations, alerts, custom threats, logs/LEQL search and users. API v1/v2 selectable per instance and per command." category: siem diff --git a/integrations/rapid7-insightidr/scripts/list_investigation_alerts.py b/integrations/rapid7-insightidr/scripts/list_investigation_alerts.py index 94dfe01..170de29 100644 --- a/integrations/rapid7-insightidr/scripts/list_investigation_alerts.py +++ b/integrations/rapid7-insightidr/scripts/list_investigation_alerts.py @@ -38,9 +38,37 @@ def run(): if not all_results: limit = int(inp.get("limit") or 50) data = data[:limit] + for a in data: + _annotate(a) print(json.dumps({"rrn": inv_id, "alert": data})) +def _annotate(a): + """Say, on each alert, whether the Alert Triage commands can read it. + + An investigation's alert list mixes two kinds. A detection-rule alert carries + an RRN (rrn:alerts:...) and answers get_alert_actors / get_alert_evidence. A + behaviour (UBA) alert — "attempted to remotely access N new assets" — has a + plain UUID and no RRN, and those commands answer 404 for it. An agent told so + in its instructions still called them; told so in the data it is reading, it + does not. The hint names where the evidence of such an alert lives instead. + """ + rrn = None + for v in (a.get("rrn"), a.get("id")): + if isinstance(v, str) and v.startswith("rrn:alerts:"): + rrn = v + break + a["alert_rrn"] = rrn + a["evidence_api"] = rrn is not None + if rrn: + a["next"] = "Call get_alert_actors and get_alert_evidence with alert_rrn." + 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(\"\") over the alert's window.") + + try: run() except urllib.error.HTTPError as e: