fix(rapid7-insightidr): map both ends of a connection (v1.7.0)

An "RDP connection to DC" investigation arrived with device {}, process {} and
two indexed entities, both of them false — the 32-hex tails of its own alert
RRNs, which the IOC regex cannot tell from an MD5. Its four real values were
sitting in the payload, unmapped:

  source_account             vbalbakakis
  destination_account        vassilisadmin
  source_asset_address       sgazswvdip-01.toto.priv
  destination_asset_address  scazsvpdc01.toto.priv

The mappings assumed an alert has one host and one user, which holds for a
process_start_event and not for an auth rule: a connection names both ends and
neither is "the host". So source and destination now land on src_endpoint and
dst_endpoint, the acting account on actor.user and the account acted upon on
user — the distinction that lets a lateral move be read in one line.

That choice is also what makes the fields placeable. The layout builder's field
picker is a closed <select> over the OCSF catalogue in web/src/lib/ocsf.ts;
a path absent from it cannot be put on a layout at all. All four targets are in
that catalogue. `detection`, the open-vocabulary catch-all, is not and cannot
be — it keeps its value in the Context tab, which renders the whole tree, but a
layout cannot reference it, so anything meant for a layout has to be mapped to a
real OCSF field first.

evidences[] is flattened at the same time. It emitted "device": {}, "process":
{} and "attacks": {"technique": {}} whenever those keys were absent — three
empty objects per alert, filling the context and hiding what was populated.

Verified against the RDP investigation: four entities where there were none,
two hosts and two accounts, and no false hash once the incident type's
extraction policy is set to selected/host,ip,user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 10:21:27 +02:00
parent eeebbfed3a
commit c7569fd4d0
3 changed files with 42 additions and 8 deletions
@@ -101,6 +101,22 @@ ocsf:
# 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 <select> over
# the OCSF catalogue. A path that is not in that catalogue cannot be placed on
# a layout at all, which is why `detection` below, useful as it is, only ever
# shows up in the Context tab.
- { source_path: "alerts[0].rule_keys_of_interest[key='source_asset_address'].values[0]", ocsf_field: "src_endpoint.hostname" }
- { source_path: "alerts[0].rule_keys_of_interest[key='destination_asset_address'].values[0]", ocsf_field: "dst_endpoint.hostname" }
- { source_path: "alerts[0].rule_keys_of_interest[key='destination_asset'].values[0]", ocsf_field: "dst_endpoint.hostname" }
# actor.user is who acted, user is who it was done TO. Keeping them apart is
# what lets an analyst read a lateral move in one line.
- { source_path: "alerts[0].rule_keys_of_interest[key='source_account'].values[0]", ocsf_field: "actor.user.name" }
- { source_path: "alerts[0].rule_keys_of_interest[key='destination_account'].values[0]", ocsf_field: "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 /
@@ -137,12 +153,14 @@ ocsf:
"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] } },
"hostname": rule_keys_of_interest[key='hostname'].values[0],
"src_hostname": rule_keys_of_interest[key='source_asset_address'].values[0],
"dst_hostname": rule_keys_of_interest[key='destination_asset_address'].values[0],
"actor_user": rule_keys_of_interest[key='source_account'].values[0],
"target_user": rule_keys_of_interest[key='destination_account'].values[0],
"process": rule_keys_of_interest[key='process.name'].values[0],
"cmd_line": rule_keys_of_interest[key='process.cmd_line'].values[0],
"technique": rule.mitre_tcodes[$substring($, 0, 2) != 'TA'][0],
"detection": rule_keys_of_interest{key: values[0]}
}
ocsf_field: "evidences"
@@ -124,6 +124,22 @@ ocsf:
- { source_path: "investigation.priority", ocsf_field: "finding_info.related_events.priority" }
- { source_path: "investigation.disposition", ocsf_field: "finding_info.related_events.disposition" }
- { source_path: "investigation.rrn", ocsf_field: "finding_info.related_events.uid" }
# ── 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 <select> over
# the OCSF catalogue. A path that is not in that catalogue cannot be placed on
# a layout at all, which is why `detection` below, useful as it is, only ever
# shows up in the Context tab.
- { source_path: "rule_keys_of_interest[key='source_asset_address'].values[0]", ocsf_field: "src_endpoint.hostname" }
- { source_path: "rule_keys_of_interest[key='destination_asset_address'].values[0]", ocsf_field: "dst_endpoint.hostname" }
- { source_path: "rule_keys_of_interest[key='destination_asset'].values[0]", ocsf_field: "dst_endpoint.hostname" }
# actor.user is who acted, user is who it was done TO. Keeping them apart is
# what lets an analyst read a lateral move in one line.
- { source_path: "rule_keys_of_interest[key='source_account'].values[0]", ocsf_field: "actor.user.name" }
- { source_path: "rule_keys_of_interest[key='destination_account'].values[0]", ocsf_field: "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 /