Files
riposte-marketplace/integrations/rapid7-insightidr/scripts/get_alert_process_trees.py
T
f3nris 2d6e097742 fix(rapid7-insightidr): explain the process-tree 404 (v1.9.1)
Running the command on an alert reports `{"error": "HTTP 404", "detail": ""}`.
The API returns 404 with an empty body, so the operator is told a number and
nothing else — and cannot tell a normal answer from a fault.

It is usually normal, and the URL is not at fault: it is built exactly as
get_alert_evidence builds its own, which works. Rapid7 uses the same 404 for
three unrelated situations and does not distinguish them:

  - the alert is not an endpoint detection, so no tree exists. This is the
    common case on a feed of ingress_auth, asset_auth or firewall events;

  - the tree exists in principle but was never generated. The endpoint returns
    the CACHED tree unless force_refresh is set, so an ungenerated tree answers
    404 while its data is there. Retrying with force_refresh=true is the first
    thing to try on an endpoint detection;

  - the key lacks the privilege.

All three are now named in the error, and the force_refresh input says what it
is really for rather than describing it only as a staleness control.

The ingestion path already handled this correctly — it treats 404 as "no tree"
and counts only other statuses as errors — so nothing changes there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:56:23 +02:00

87 lines
3.9 KiB
Python

import json, os, sys, urllib.request, urllib.parse, urllib.error
# The Alert Triage API lives under /idr/at, a base of its own — not /idr/v1 or
# /idr/v2 like the investigation endpoints. Same host, same API key.
AT = "idr/at/"
def _cfg():
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
region = str(s.get("region") or "us").strip().lower()
base = "https://" + region + ".api.insight.rapid7.com/"
headers = {"X-Api-Key": s.get("api_key", ""), "Content-Type": "application/json", "Accept": "application/json"}
multi = str(s.get("is_multi_customer") or "").strip().lower() in ("1", "true", "yes")
return base, headers, multi
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _list(v):
if v in (None, ""):
return []
if isinstance(v, list):
return [str(x).strip() for x in v if str(x).strip()]
return [p.strip() for p in str(v).split(",") if p.strip()]
def request(method, path, params=None, body=None):
base, headers, _ = _cfg()
url = base + path.lstrip("/")
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean, doseq=True)
data = json.dumps(body).encode("utf-8") if body is not None else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def run():
inp = _inputs()
rrn = inp.get("alert_rrn", "")
limit = int(inp.get("limit") or 10)
# force_refresh is left off by default on purpose: the API documents
# regeneration as expensive and slow, and the command runs under the
# server's 90-second budget like every other one.
params = {"index": inp.get("index") or "0", "size": inp.get("page_size") or limit}
if str(inp.get("force_refresh") or "").strip().lower() in ("1", "true", "yes"):
params["force_refresh"] = "true"
if inp.get("branch") not in (None, ""):
params["branch"] = inp["branch"]
out = request("POST", AT + "alerts/" + urllib.parse.quote(rrn, safe="") + "/process_trees/latest",
params=params)
trees = out.get("process_trees") or []
print(json.dumps({"rrn": rrn, "process_trees": trees[:limit], "metadata": out.get("metadata") or {}}))
try:
run()
except urllib.error.HTTPError as e:
detail = e.read().decode("utf-8", "replace")
if e.code == 404:
# The API returns 404 with an EMPTY body here, so without this the
# operator gets `{"error": "HTTP 404", "detail": ""}` and no way to tell
# a normal answer from a fault. It is usually normal: only an endpoint
# detection has a tree at all.
detail = (detail or "no process tree for this alert. Rapid7 answers 404 for three "
"different situations and does not distinguish them: (1) the alert is not "
"an endpoint detection, so no tree exists — check triggering_event_type "
"with rapid7-insight-idr-get-alert, and expect no tree for ingress_auth, "
"asset_auth, unparsed or firewall events; (2) the tree was never generated "
"— this endpoint returns the CACHED tree unless force_refresh is set, so "
"retry with force_refresh=true, which the API documents as slow; (3) the "
"API key lacks the privilege for it.")
if e.code in (401, 403):
detail += (" — the Alert Triage API (/idr/at) is only available to Managed Detection and"
" Response customers with access to the Alerts experience; a key that works on"
" the investigation endpoints can still be refused here")
print(json.dumps({"error": "HTTP " + str(e.code), "detail": detail}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)