Files
riposte-marketplace/integrations/rapid7-insightidr/mappers/get_investigations_full.yaml
T
f3nris 306450a4b6 fix(rapid7-insightidr): rule_keys_of_interest is an open vocabulary (v1.5.0)
Both mappers read that field through a hardcoded list of keys — hostname,
process.cmd_line, parent_process.*, user — taken from the one alert this work
was built against, a process_start_event detection. Every entry is a JSONata
predicate on an exact key, so a rule emitting anything else matches none of
them.

A real alert from a second tenant showed what that costs. It is a custom
ingress_auth rule, and its keys are:

  account             locuz_hma@totogroup.net
  source_ip           183.82.103.101
  geoip_country_code  IN
  result              SUCCESS

Not one appears in the hardcoded list. The alert was fetched, inlined and
mapped, and produced an incident as empty as the ones this integration set out
to fix — with the account and the source IP sitting in the payload, unread.

Two changes:

  - The whole list is flattened verbatim into `detection`, and rule_matching_keys
    into `detection_match`. Keys nobody anticipated now arrive intact, and they
    reach the AI analysis, which reads the mapped context and never `raw`.

  - account, source_ip, destination_ip, geoip_country_code and result gain
    explicit OCSF entries. The explicit mappings are not redundant with the
    flattening: a value under `detection.` is only ever seen by the IOC regex,
    so an account there would be indexed as an email, or not at all, rather than
    as a user. Typing an entity correctly still requires naming its field.

evidences[] carries the flattened keys too, so the same holds for the second and
third alert of an investigation, not only the first.

Verified against both rule shapes end to end. The process rule yields the two
hosts it did before; the authentication rule now yields the source IP and the
account, where it previously yielded nothing.

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

149 lines
12 KiB
YAML

name: "Rapid7 InsightIDR Investigations (full) → OCSF"
description: |
Maps one Rapid7 investigation fetched with its alerts inlined
(get_investigations_full, results_path = data) to OCSF finding fields.
The plain investigation feed cannot carry a detection: idr/v2/investigations answers
with a title, a priority, a status, an assignee and timestamps, and nothing else. An
incident built from it indexes zero entities. This mapper reads the same investigation
plus the alerts the command inlined under `alerts`, where the detection content
actually lives.
Investigation values are mapped first and the first alert's equivalents after, so the
alert wins wherever the container says nothing. `alerts` is empty on an investigation
whose alerts could not be read (the command sets alerts_error) — every alert path then
resolves to nothing and is skipped, and the incident degrades to what the investigation
alone says rather than failing.
field_mappings:
title: "title"
# toSeverity maps CRITICAL→5, HIGH→4, MEDIUM→3, LOW→2; anything else falls back to the
# alert rule's default severity.
severity: "priority"
source: "source"
# Rapid7's own triage state. It cannot reach the incident's status and verdict columns
# — MapFields accepts only title, severity, source, description and custom:* — so it
# rides as custom fields, where an analyst at least sees that the source already closed
# this and what it concluded.
"custom:rapid7_status": "status"
"custom:rapid7_disposition": "disposition"
# Rapid7's AI suggestion, exposed as a fact and never as a verdict.
# first_alert_* are lifted to the top level by the command on purpose: this
# mapper section resolves dotted paths only and cannot index an array, so
# `alerts[0].x` here would read as nothing — silently, leaving the custom field
# empty with no error. The ocsf section below is full JSONata and indexes freely.
"custom:rapid7_ai_disposition": "first_alert_ai_disposition"
"custom:rapid7_alert_count": "alert_count"
"custom:rapid7_log_id": "first_alert_log_id"
"custom:rapid7_logset_id": "first_alert_logset_id"
"custom:rapid7_rule_rrn": "first_alert_rule_rrn"
"custom:rapid7_event_type": "first_alert_event_type"
ocsf:
# ── Finding: the investigation ────────────────────────────────────
- { source_path: "rrn", ocsf_field: "finding_info.uid" }
- { source_path: "title", ocsf_field: "finding_info.title" }
- { source_path: "created_time", ocsf_field: "finding_info.created_time" }
- { source_path: "last_accessed", ocsf_field: "finding_info.modified_time" }
- { source_path: "first_alert_time", ocsf_field: "finding_info.first_seen_time" }
- { source_path: "latest_alert_time", ocsf_field: "finding_info.last_seen_time" }
- { source_path: "tags", ocsf_field: "metadata.labels" }
- { source_path: "alert_count", ocsf_field: "count" }
# ── Incident state ────────────────────────────────────────────────
- { source_path: "status", ocsf_field: "status" }
- { source_path: "priority", ocsf_field: "priority" }
- { source_path: "disposition", ocsf_field: "disposition" }
- { source_path: "source", ocsf_field: "activity_name" }
- { source_path: "organization_id", ocsf_field: "cloud.org.uid" }
- { source_path: "assignee.name", ocsf_field: "assignee.name" }
- { source_path: "assignee.email", ocsf_field: "assignee.email_addr" }
# ── Detection time: the MTTD anchor ───────────────────────────────
# Weakest first, strongest last. The investigation is created after the alert that
# opened it; the alert's own alerted_at is when the event happened in the source system.
- { source_path: "created_time", ocsf_field: "time" }
- { source_path: "first_alert_time", ocsf_field: "time" }
- { source_path: "alerts[0].alerted_at", ocsf_field: "time" }
# ── The detection rule, from the first alert ──────────────────────
# `rule.name` is documented but absent from live ABA alerts, so `type` — which repeats
# the rule's display name — is the honest fallback.
- { source_path: "alerts[0].type", ocsf_field: "finding_info.analytic.name" }
- { source_path: "alerts[0].rule.rrn", ocsf_field: "finding_info.analytic.uid" }
- { source_path: "alerts[0].rule.version_rrn", ocsf_field: "finding_info.analytic.version" }
- { source_path: "alerts[0].rrn", ocsf_field: "finding_info.uid_alt" }
# ── Product identity ──────────────────────────────────────────────
- { source_path: "'Rapid7 InsightIDR'", ocsf_field: "metadata.product.name" }
- { source_path: "'Rapid7'", ocsf_field: "metadata.product.vendor_name" }
- { source_path: "alerts[0].external_source", ocsf_field: "metadata.log_source" }
- { source_path: "alerts[0].triggering_event_type", ocsf_field: "metadata.event_code" }
- { source_path: "alerts[0].log_details[0].log_entry_id", ocsf_field: "metadata.uid" }
# ── MITRE ATT&CK ──────────────────────────────────────────────────
# mitre_tcodes mixes both kinds in one array: "TA0040" is a tactic, "T1490" a technique.
# Splitting on the "TA" prefix keeps a tactic out of the technique field, where it would
# name a technique that does not exist.
- { source_path: "alerts[0].rule.mitre_tcodes[$substring($, 0, 2) = 'TA'][0]", ocsf_field: "attacks.tactic.uid" }
- { source_path: "alerts[0].rule.mitre_tcodes[$substring($, 0, 2) != 'TA'][0]", ocsf_field: "attacks.technique.uid" }
# ── The affected host ─────────────────────────────────────────────
- { source_path: "alerts[0].rule_keys_of_interest[key='hostname'].values[0]", ocsf_field: "device.hostname" }
- { source_path: "alerts[0].rule_keys_of_interest[key='hostname'].values[0]", ocsf_field: "src_endpoint.hostname" }
# ── The process the alert is about ────────────────────────────────
- { source_path: "alerts[0].rule_keys_of_interest[key='process.name'].values[0]", ocsf_field: "process.name" }
- { source_path: "alerts[0].rule_keys_of_interest[key='process.name'].values[0]", ocsf_field: "process.file.name" }
- { source_path: "alerts[0].rule_keys_of_interest[key='process.cmd_line'].values[0]", ocsf_field: "process.cmd_line" }
- { source_path: "alerts[0].rule_keys_of_interest[key='process.exe_path'].values[0]", ocsf_field: "process.file.path" }
- { source_path: "alerts[0].rule_keys_of_interest[key='process.exe_file.hashes.md5'].values[0]", ocsf_field: "process.file.hashes.md5" }
- { source_path: "alerts[0].rule_keys_of_interest[key='process.exe_file.hashes.sha256'].values[0]", ocsf_field: "process.file.hashes.sha256" }
# ── Its parent, which says how it was launched ────────────────────
- { source_path: "alerts[0].rule_keys_of_interest[key='parent_process.name'].values[0]", ocsf_field: "actor.process.parent_process.name" }
- { source_path: "alerts[0].rule_keys_of_interest[key='parent_process.cmd_line'].values[0]", ocsf_field: "actor.process.parent_process.cmd_line" }
- { source_path: "alerts[0].rule_keys_of_interest[key='parent_process.exe_path'].values[0]", ocsf_field: "actor.process.parent_process.file.path" }
- { source_path: "alerts[0].rule_keys_of_interest[key='parent_process.exe_file.hashes.md5'].values[0]", ocsf_field: "actor.process.parent_process.file.hashes.md5" }
# ── The acting account ────────────────────────────────────────────
# This tenant's field catalogue has no canonical user namespace, so an account only
# appears when a rule names it among its keys of interest. Both spellings are mapped
# because rules are not consistent about which they emit.
- { source_path: "alerts[0].rule_keys_of_interest[key='user'].values[0]", ocsf_field: "actor.user.name" }
- { source_path: "alerts[0].rule_keys_of_interest[key='username'].values[0]", ocsf_field: "actor.user.name" }
# ── Every key the rule found interesting ──────────────────────────
# rule_keys_of_interest is an OPEN vocabulary: each detection rule names its own
# keys. A process_start_event rule emits hostname / process.cmd_line /
# parent_process.*; an ingress_auth rule emits account / source_ip /
# geoip_country_code / result and none of the above. The explicit entries below
# can therefore only ever cover the rules someone has already seen — on any
# other rule they all resolve to nothing and the incident is empty again, which
# is the exact failure this integration started from.
#
# So the whole list is also flattened, verbatim, into one object. Keys nobody
# anticipated arrive intact and are visible to the analyst and to the AI
# analysis, which reads the mapped context but never `raw`. The explicit
# mappings stay because they are what types an entity correctly — a value under
# `detection.` is only ever matched by the IOC regex, so an account there would
# never be indexed as a user.
- { source_path: "alerts[0].rule_keys_of_interest{key: values[0]}", ocsf_field: "detection" }
- { source_path: "alerts[0].rule_matching_keys{key: values[0]}", ocsf_field: "detection_match" }
# ── Keys seen on non-process rules ────────────────────────────────
- { source_path: "alerts[0].rule_keys_of_interest[key='account'].values[0]", ocsf_field: "actor.user.account.name" }
- { source_path: "alerts[0].rule_keys_of_interest[key='source_ip'].values[0]", ocsf_field: "src_endpoint.ip" }
- { source_path: "alerts[0].rule_keys_of_interest[key='destination_ip'].values[0]", ocsf_field: "dst_endpoint.ip" }
- { source_path: "alerts[0].rule_keys_of_interest[key='geoip_country_code'].values[0]", ocsf_field: "src_endpoint.location.country" }
- { source_path: "alerts[0].rule_keys_of_interest[key='result'].values[0]", ocsf_field: "status_detail" }
# ── Every alert, not just the first ───────────────────────────────
# An investigation aggregates alerts, and the fields above only ever read alerts[0].
# This entry lands the whole set as OCSF-shaped objects, so the host and the process of
# the SECOND and THIRD alert are indexed as entities too — the extractor walks arrays
# and classifies on the path's suffix, so `.hostname` under here counts exactly as
# `device.hostname` does above.
- source_path: >
alerts.{
"uid": rrn,
"title": title,
"time": alerted_at,
"status": status,
"disposition": disposition,
"device": { "hostname": rule_keys_of_interest[key='hostname'].values[0] },
"process": {
"name": rule_keys_of_interest[key='process.name'].values[0],
"cmd_line": rule_keys_of_interest[key='process.cmd_line'].values[0]
},
"attacks": { "technique": { "uid": rule.mitre_tcodes[$substring($, 0, 2) != 'TA'][0] } },
"detection": rule_keys_of_interest{key: values[0]}
}
ocsf_field: "evidences"