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" # The log line's own sentence beats a generic title when there is one. description: "first_evidence_message" # 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" # Every key the detection emitted, rendered as Markdown. Declare a custom field # named rapid7_summary of type markdown and place it on the layout: it shows # what no OCSF mapping can, because the layout's field picker only offers paths # from the OCSF catalogue and this vocabulary is open-ended. "custom:rapid7_summary": "summary_markdown" # One row per evidence, across every alert. Declare a custom field named # rapid7_evidence of type table: it reads an array of objects as it stands, so # nothing is reshaped here. It sits beside the Markdown summary rather than # replacing it — the summary reads as a page, this one sorts and filters. "custom:rapid7_evidence": "evidence_rows" 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" } # ── Event time (OCSF `time`) ─────────────────────────────────────── # Riposte no longer reads it to date detection: since migration 0256, MTTD # starts when Riposte records the alert. It is the alert's own record of when # the source saw the event. # 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" } # ── Lateral movement: source and destination ────────────────────── # An asset_auth rule describes a connection, so it names BOTH ends and neither # is "the host": source_asset_address / destination_asset_address, and # source_account / destination_account. Mapping them to src_endpoint and # dst_endpoint is not only the correct OCSF shape — it is also what makes them # appear in the layout builder, whose field picker is a closed