69e3a3e81cb4f04b728ee52f4bea2e1217443df3
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69e3a3e81c |
fix(rapid7-insightidr): do not send a bare UUID to /alerts/ops/rrns (v1.4.1)
The run died with HTTP 400 and eight "The provided RRN: [...] is invalid", every one a plain UUID. Two mistakes compounded. The alert list under an investigation is not consistent about where the RRN sits. On the ABA alert this work was built against, `id` held the RRN. On others it holds a bare UUID. The code read `id` first and `rrn` only as a fallback, so it picked the UUID whenever both were present. Worse, the failure was total rather than partial. /alerts/ops/rrns validates the whole list and rejects the entire request when one entry is malformed, so a single bad identifier costs every alert in that batch — and with one batch, the whole run. An investigation feed that stops dead because one alert is shaped differently is not an acceptable failure mode. So: read `rrn` first, and drop anything that is not RRN-shaped before the batch is sent, counting it as alert_ids_without_rrn in the run metadata. The investigation still arrives, its retrievable alerts still arrive, and what could not be fetched is a number an operator can see rather than an absence nobody would notice. Both commands that walk this list are fixed; the selector is shared and was checked against the two real shapes plus the empty and null cases. This turns a total failure into a partial one. It does NOT establish that those eight alerts are reachable at all: if they carry no RRN anywhere, they are not in the Alert Triage system and no field_ids or batch call will produce them. That needs the raw shape of one, which the next run of list_investigation_alerts on the failing investigation will show. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
841ae1199f |
feat(rapid7-insightidr): the investigation, with its alerts inlined (v1.4.0)
The previous command returned the alerts as its result rows, which quietly changed the unit: an investigation holding three alerts would have become three incidents. The investigation is what an analyst opens and what the SOC tracks — it stays the incident, and the alerts are its content. get_investigations_full walks the same three hops and inverts the shape: investigations under `data`, each carrying its alerts under `alerts` plus an alert_count. results_path=data and dedup_key=rrn, so it is a drop-in replacement for list_investigations in an existing ingestion rule — only the command name and the mapper change. A few first-alert values are also lifted to the top level, and that is not cosmetic. MapFields resolves dotted paths only and cannot index an array, so `alerts[0].ai_suggested_disposition` in a field_mappings entry reads as nothing — silently, leaving the custom field empty with no error anywhere. Three custom fields were empty for exactly that reason until an end-to-end run showed it. The ocsf section has no such limit: it is evaluated by full JSONata and indexes freely, which is why the host and process mappings worked while the custom fields did not. The mapper maps the investigation first and the first alert after, so the alert wins wherever the container says nothing. It also lands every alert — not just alerts[0] — as OCSF-shaped objects under `evidences`, because the extractor walks arrays and classifies on the path suffix: an investigation aggregating two alerts on two machines now indexes both hosts, where reading alerts[0] alone would have indexed one and silently dropped the other. Verified end to end against the investigation that started this, with a second alert added on another host: field mapping, OCSF mapping and entity extraction. Both hosts are indexed. With the incident type's extraction policy set to selected/host,ip,user, the entity set is exactly those two — the alert RRN's 32-hex suffix, which the IOC regex cannot tell from an MD5, is filtered out there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
220fa75b99 |
feat(rapid7-insightidr): ingest only the alerts that belong to an investigation (v1.3.0)
Ingesting from search_alerts brings back every alert the tenant produces, including the operational noise — two of the first three sampled were an agent reporting it lacked Full Disk Access. What is worth an incident is what Rapid7 or the analyst folded into an investigation. Filtering search_alerts on investigation_rrn does not work. The term is accepted — no validation error, HTTP 200 — and matches nothing: zero alerts over fifteen days, on a tenant where an alert carrying an investigation_rrn provably exists in that window. The field is not searchable, and the API says so by returning an empty page rather than by refusing the term. That failure mode is the reason not to build an ingestion feed on it: a filter that silently stops matching drains the feed to zero with no error anywhere, and nothing would ever say why. search_investigation_alerts is guaranteed by construction instead: 1. the investigations in the window (incremental on their created_time) 2. their alert RRNs, one call per investigation — there is no bulk form 3. the alerts themselves via /alerts/ops/rrns, batched 100 at a time So 1+N+1 calls, and an alert with no investigation is never even named. Its failure modes are loud: an unreadable investigation is skipped and counted, and HTTP errors surface as themselves. Step 2 is the one that grows with the window, so it runs under a time budget (60s, against the server's 90s ceiling) and reports truncated=true rather than being killed with the whole run's alerts lost. A partial fetch that says it is partial beats a silent zero — the same reasoning that rules out the filter. The output shape matches search_alerts exactly, so the mapper committed in 1.2.0 serves both. It gains five entries for the parent investigation, which resolve to nothing on the search_alerts feed and are skipped there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7e231c7c78 |
fix(rapid7-insightidr): name the input when terms_json will not parse (v1.2.1)
json.loads answers a bad value with "Expecting value: line 1 column 1 (char 0)",
which names neither the input at fault nor what it actually received. The
command has three string inputs; nothing in that message says which one broke,
so the operator is left guessing.
The mistake it hides is the obvious one. Documentation shows the input name and
the value together, so `terms_json: [{...}]` gets pasted whole into the value
field, and the parser then chokes on the field name it was handed. Echoing the
first 80 characters back makes that visible immediately.
Also carry a worked example in the manifest — the term that keeps only alerts
attached to an investigation, which is the filter this input mainly exists for —
and say explicitly to paste the array alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b6903d4590 |
feat(rapid7-insightidr): map alerts to OCSF, with real observables (v1.2.0)
An incident built from a Rapid7 investigation indexed zero entities. Nothing to
pivot on, nothing to correlate, and an AI analysis with nothing to read. The
reason turned out not to be the mapper but the feed: an investigation is an
envelope, and neither it nor its /alerts list carries a host, an account, a
process or a MITRE code.
The Alert Triage API does, in rule_keys_of_interest. On the alert behind the
investigation that started this, that field holds:
hostname NBSGIT01-26
process.name bcdedit.exe
process.cmd_line bcdedit /set {default} recoveryenabled No
process.exe_path C:\WINDOWS\System32\bcdedit.exe
parent_process.name cmd.exe
parent_process.cmd_line "C:\WINDOWS\system32\cmd.exe"
parent_process.exe_file.hashes.md5 e463e36104d22a2479046a7d4f620816
with rule.mitre_tcodes ["TA0040", "T1490"] — Impact / Inhibit System Recovery,
which is what disabling Windows recovery actually is. None of it was reachable
before.
It is a list of {key, values} pairs rather than an object, so every entry reads
through a JSONata predicate — rule_keys_of_interest[key='hostname'].values[0].
A key a rule does not emit resolves to nothing and is skipped, so one file
covers every detection rule.
Two details worth keeping:
- mitre_tcodes mixes tactics and techniques in one array ("TA0040" beside
"T1490"). They are split on the "TA" prefix so a tactic never lands in the
technique field, where it would name a technique that does not exist.
- rule.name is documented but absent from live ABA alerts, so the analytic
name falls back to `type`, which repeats the rule's display name.
Rapid7's own status, disposition and AI-suggested disposition ride as custom
fields. They cannot reach the incident's status and verdict columns — MapFields
accepts only title, severity, source, description and custom:* — but an analyst
can at least see that the source already closed the alert and what it decided.
The AI suggestion is exposed as a fact, never as a verdict.
Validated end to end against a live alert: field mapping, OCSF mapping and
entity extraction, which now yields the host and the parent process hash where
it previously yielded nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
157af87c17 |
fix(rapid7-insightidr): make list_alert_fields callable with no arguments (v1.1.1)
The bare call answered HTTP 400: root path with path depth greater than 1 is not supported for V2 without a search filter Sending neither path nor path_depth asks the V2 fields endpoint to walk the whole hierarchy from the root, which it refuses. That is exactly the call an operator makes first — open the command, run it, see what the tenant exposes — so the one entry point into this API failed on its most obvious use. Default path_depth to 1 when neither path nor search is given: the top-level nodes come back, and `path` drills down from there. Also expose the `search` query parameter, which was missing from the command entirely and which lifts the depth restriction — it is the way to look through the whole tree at once for a field whose branch you do not know yet (search=hostname). Found by running the command against a live tenant. The 400 also settles the open question from v1.1.0: the API answered a validation error rather than 403, so the Alert Triage API is reachable with this key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
da44d41555 |
feat(rapid7-insightidr): add the Alert Triage API commands (v1.1.0)
An ingested investigation carried almost nothing: title, priority, status,
assignee and timestamps. Not a host, not an account, not a command line — so
no entity was ever indexed, and neither the pivot graph nor the AI analysis
had anything to read. Fetching the investigation's alerts did not help: that
endpoint answers with the rule name (already the investigation's title), the
alert source and an alert RRN, and nothing else.
The detection content lives in a different API. The Alert Triage API is served
under /idr/at — its own base, next to /idr/v1 and /idr/v2 — and it is where
the evidence, the actors, the MITRE T-codes and the process trees actually
are. It was missing from this package entirely.
Six read commands:
search_alerts POST /alerts/ops/search
get_alert GET /alerts/{rrn}
get_alert_evidence GET /alerts/{rrn}/evidences
get_alert_actors GET /alerts/{rrn}/actors
get_alert_process_trees POST /alerts/{rrn}/process_trees/latest
list_alert_fields GET /alerts/2/fields
search_alerts is the one that changes the shape of the feed. It returns full
alert objects — rule with its mitre_tcodes, rule_keys_of_interest, the
AI-suggested disposition, the parent investigation RRN, the status transition
timings and the log entries behind the alert — and its start_time is required
by the API, which makes it a natural incremental watermark. So the alerts can
be read directly, paginated, with no per-investigation fan-out.
Two things worth knowing before using them:
- field_ids is not optional in practice. The API returns no additional
fields when it is empty, and those additional fields are where the
hostnames, accounts and command lines are. Their ids are per-tenant dotted
paths (process.cmd_line, …), which is what list_alert_fields is for.
/alerts/fields is deprecated, hence /alerts/2/fields.
- the whole API is gated on Managed Detection and Response with access to
the Alerts experience. A key that works on every investigation endpoint
can still be refused here, so 401 and 403 are reported with that named
explanation rather than as a bare status code — a licence boundary should
not read as a bug.
Written against the published OpenAPI spec (SIEM Alerts API 39.10.0); not yet
exercised against a live tenant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
729c339e2f |
feat(rapid7-insightidr): InsightIDR integration (19 commands + OCSF ingestion)
REST API integration for Rapid7 InsightIDR. Investigation ingestion (list_investigations) with an exhaustive OCSF mapper and a bundled default incident type, plus 18 commands across investigations (list/get/search/ create/update/assign/set-status/bulk-close), investigation alerts and Rapid7 product alerts, custom threat indicators (add/replace), log management and LEQL log/log-set queries with downloads, and user directory search. API v1/v2 selectable per instance (is_v2) and per command (api_version); multi-customer query parameter supported on v2 calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |