fix(cortex-xdr): put the actor where the other EDR mappers put it

HarfangLab and SentinelOne both send the process that acted to actor.* —
process.username and sourceProcessInfo.user to actor.user.name, the process
itself to actor.process.* — and keep user.name for the person the event
happened to: HarfangLab's top_impacted_users, SentinelOne's login account.

Cortex XDR filled neither slot correctly. XDR labels the acting process
actor_process_* and its account actor_effective_username; the mappers sent
the process to process.* and the account to user.name, which the OCSF
catalogue calls Target User. A rule written on actor.user.name or
actor.process.name matched HarfangLab and SentinelOne and skipped Cortex,
and a rule reading user.name as the victim read the attacker.

process.* is not free-for-all either: in the SentinelOne (targetProcessInfo)
and Carbon Black (childproc_*) mappers it holds the process acted upon, so
Cortex's actor was sitting in the target's seat.

get_incidents_full had the collision in the open: the incident's impacted
users[] and the first alert's user_name both wrote user.name, and last
non-empty wins, so the actor overwrote the impacted user on every incident
carrying both.

Tenants using the shipped templates pick this up on the next sync; a forked
mapper, or a column or rule pointed at the old Cortex paths, needs repointing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:03:06 +02:00
parent bf273b959a
commit 9f9ee5344d
3 changed files with 33 additions and 20 deletions
+18 -11
View File
@@ -57,14 +57,21 @@ ocsf:
# the incident feed (which maps hosts there) match alerts unchanged.
- { source_path: "agent_hostname", ocsf_field: "src_endpoint.hostname" }
- { source_path: "agent_ip_addresses[0]", ocsf_field: "src_endpoint.ip" }
- { source_path: "actor_effective_username", ocsf_field: "user.name" }
# ── What actually happened ────────────────────────────────────────
# ── What actually happened: the actor ─────────────────────────────
# The process XDR itself calls `actor_*` is the one that acted, so it lands in
# the Actor group — the same place HarfangLab puts process.* and SentinelOne
# sourceProcessInfo.*. A correlation or pre-processing rule written on
# actor.process.name / actor.user.name therefore matches all three feeds.
# `process.*` is left free: across the EDR mappers it holds the process acted
# UPON (SentinelOne targetProcessInfo, Carbon Black childproc_*), which the
# XDR alerts grid does not carry.
# Grid columns first, then the same detail read off the first event, which is
# where a tenant that does not flatten these columns puts them.
- { source_path: "actor_process_image_name", ocsf_field: "process.name" }
- { source_path: "actor_process_command_line", ocsf_field: "process.cmd_line" }
- { source_path: "actor_process_image_sha256", ocsf_field: "process.file.hashes.sha256" }
- { source_path: "causality_actor_process_command_line", ocsf_field: "process.parent_process.cmd_line" }
- { source_path: "actor_effective_username", ocsf_field: "actor.user.name" }
- { source_path: "actor_process_image_name", ocsf_field: "actor.process.name" }
- { source_path: "actor_process_command_line", ocsf_field: "actor.process.cmd_line" }
- { source_path: "actor_process_image_sha256", ocsf_field: "actor.process.file.hashes.sha256" }
- { source_path: "causality_actor_process_command_line", ocsf_field: "actor.process.parent_process.cmd_line" }
- { source_path: "action_file_path", ocsf_field: "file.path" }
- { source_path: "action_file_sha256", ocsf_field: "file.hashes.sha256" }
- { source_path: "action_file_md5", ocsf_field: "file.hashes.md5" }
@@ -75,11 +82,11 @@ ocsf:
- { source_path: "action_remote_ip", ocsf_field: "dst_endpoint.ip" }
- { source_path: "action_remote_port", ocsf_field: "dst_endpoint.port" }
- { source_path: "dst_action_external_hostname", ocsf_field: "dst_endpoint.hostname" }
- { source_path: "events[0].actor_process_image_name", ocsf_field: "process.name" }
- { source_path: "events[0].actor_process_command_line", ocsf_field: "process.cmd_line" }
- { source_path: "events[0].actor_process_image_path", ocsf_field: "process.path" }
- { source_path: "events[0].actor_process_image_sha256", ocsf_field: "process.file.hashes.sha256" }
- { source_path: "events[0].causality_actor_process_image_name", ocsf_field: "process.parent_process.name" }
- { source_path: "events[0].actor_process_image_name", ocsf_field: "actor.process.name" }
- { source_path: "events[0].actor_process_command_line", ocsf_field: "actor.process.cmd_line" }
- { source_path: "events[0].actor_process_image_path", ocsf_field: "actor.process.file.path" }
- { source_path: "events[0].actor_process_image_sha256", ocsf_field: "actor.process.file.hashes.sha256" }
- { source_path: "events[0].causality_actor_process_image_name", ocsf_field: "actor.process.parent_process.name" }
- { source_path: "events[0].action_file_path", ocsf_field: "file.path" }
- { source_path: "events[0].action_file_sha256", ocsf_field: "file.hashes.sha256" }
- { source_path: "events[0].action_remote_ip", ocsf_field: "dst_endpoint.ip" }
@@ -58,14 +58,20 @@ ocsf:
- { source_path: "alerts[0].host_ip[0]", ocsf_field: "src_endpoint.ip" }
- { source_path: "alerts[0].endpoint_id", ocsf_field: "device.uid" }
- { source_path: "alerts[0].agent_os_type", ocsf_field: "device.os.type" }
# users[] is who the incident happened TO; alerts[0].user_name is the account
# the acting process ran under. Both used to write user.name, so the alert's
# actor silently overwrote the impacted user (last non-empty wins).
- { source_path: "users[0]", ocsf_field: "user.name" }
- { source_path: "alerts[0].user_name", ocsf_field: "user.name" }
# ── What the first alert actually saw ─────────────────────────────
- { source_path: "alerts[0].actor_process_image_name", ocsf_field: "process.name" }
- { source_path: "alerts[0].actor_process_command_line", ocsf_field: "process.cmd_line" }
- { source_path: "alerts[0].actor_process_image_path", ocsf_field: "process.path" }
- { source_path: "alerts[0].actor_process_image_sha256", ocsf_field: "process.file.hashes.sha256" }
- { source_path: "alerts[0].causality_actor_process_image_name", ocsf_field: "process.parent_process.name" }
- { source_path: "alerts[0].user_name", ocsf_field: "actor.user.name" }
# ── What the first alert actually saw: the actor ──────────────────
# XDR's own `actor_*` naming is taken at its word and lands in the Actor
# group, as it does in the alerts mapper and in the HarfangLab and
# SentinelOne feeds; `process.*` stays reserved for the process acted upon.
- { source_path: "alerts[0].actor_process_image_name", ocsf_field: "actor.process.name" }
- { source_path: "alerts[0].actor_process_command_line", ocsf_field: "actor.process.cmd_line" }
- { source_path: "alerts[0].actor_process_image_path", ocsf_field: "actor.process.file.path" }
- { source_path: "alerts[0].actor_process_image_sha256", ocsf_field: "actor.process.file.hashes.sha256" }
- { source_path: "alerts[0].causality_actor_process_image_name", ocsf_field: "actor.process.parent_process.name" }
- { source_path: "alerts[0].action_file_path", ocsf_field: "file.path" }
- { source_path: "alerts[0].action_file_name", ocsf_field: "file.name" }
- { source_path: "alerts[0].action_file_sha256", ocsf_field: "file.hashes.sha256" }