diff --git a/integrations/cortex-xdr/manifest.yaml b/integrations/cortex-xdr/manifest.yaml index d0f2433..42a1c22 100644 --- a/integrations/cortex-xdr/manifest.yaml +++ b/integrations/cortex-xdr/manifest.yaml @@ -1,8 +1,8 @@ id: cortex_xdr name: Cortex XDR -version: 1.3.1 +version: 1.3.2 description: "Palo Alto Cortex XDR (public API v1) — incident and alert ingestion + write-back, endpoint isolation/scan/delete/tagging, RTR scripts, hash block/allow lists, file quarantine/restore/retrieval, alert exclusions, external alert push (parsed/CEF), device-control violations, audits, distributions and RBAC/risk." -changelog: "1.3.1 — get_incidents_full can watermark on modification_time (modified_after), which is what ingestion wants: an XDR incident keeps growing after creation, and a creation_time watermark fetches it once and never looks again, so every alert that joins it afterwards is lost. The full-incident fetch command is named cortex-xdr-get-incidents-full (id get_incidents_full), not cortex-xdr-fetch-incidents: sitting next to cortex-xdr-get-incidents in the command list, it now reads as what it is — the same call, everything included. A rule created against the old id must be pointed at the new one. 1.3.0 — Richer incident ingestion (get_incidents_full, on get_multiple_incidents_extra_data): incidents now arrive with their alerts in full and their file/network artifacts, where get_incidents only ever answered a 21-field summary carrying neither hosts, users, MITRE nor a single alert. Alert ingestion, and the alerts endpoint answers at last: get_alerts was sending the incidents dialect ({filters, search_from, search_to, sort}) to a grid endpoint that speaks request_data.filter_data (SEARCH_FIELD/SEARCH_TYPE/SEARCH_VALUE blocks, paging.from/to, sort as a list), and every call came back HTTP 500. Body rebuilt from the reference client, rows unwrapped out of their alert_fields envelope, severity code and the dotted status.progress key derived into readable fields. Alert ingestion: get_alerts is now a fetch command (results path reply.alerts, dedup on alert_id, incremental on source_insert_ts) with a bundled OCSF mapper and a Cortex XDR Alert incident type, so detections can be ingested alongside — or instead of — incidents. The incident mapper is fixed on the way past: its severity expression compared strings, which the mapping engine cannot do, so every ingested incident silently took the rule's default severity; it also now carries a detection anchor so MTTD is measurable. It pages past the API's 100-results-per-call ceiling, and sorts oldest-first on an incremental fetch so a truncated window drops the alerts the next poll can still see. 1.2.1 — Connection troubleshooting: the URL is normalised to the tenant host (a pasted /public_api/v1 or console path no longer breaks the call), a non-JSON reply reports the status, content type and body instead of a bare JSON parse error, missing key/key ID is caught up front, nonce and timestamp are sent in both auth modes as the reference client does, and test_connection now probes get_incidents. 1.2.0 — Incident write-back (update_incident: status/severity/assignment/resolve comment) and external alert push (insert_parsed_alerts, insert_cef_alerts). 1.1.0 — Full command coverage: added delete/alias/tag endpoints, abort scan, original alerts, script metadata/code/snippet/exec-status, file retrieval (+details), alert exclusions, device-control violations, audits, distribution url/status/create and RBAC (users, roles, groups, risk score, risky users/hosts). 1.0.0 — Initial release: incident ingestion (get_incidents) with OCSF mapper, endpoints, isolate/unisolate, scan, hash blocklist/allowlist, quarantine/restore, run script + results, alerts retrieval, distributions and action status. Standard or Advanced API authentication." +changelog: "1.3.2 — The mappers fill the Actor group, as the HarfangLab and SentinelOne mappers already did. XDR names the process that acted actor_process_* and its account actor_effective_username, but both landed outside the actor: the process in process.*, the account in user.name — the OCSF slot for the user an event was done TO. So a Cortex tenant answered nothing to a correlation or pre-processing rule written on actor.user.name / actor.process.name, and any rule reading user.name as the victim read the attacker instead. actor_process_* now maps to actor.process.*, causality_actor_process_* to actor.process.parent_process.*, actor_effective_username to actor.user.name; process.* is left free for the process acted upon, which is what it holds in the SentinelOne (targetProcessInfo) and Carbon Black (childproc_*) mappers. On get_incidents_full the incident's impacted users[] and the first alert's user_name both wrote user.name, so the alert's actor overwrote the impacted user; they are now two fields. Rules and columns written against the old Cortex paths need repointing. 1.3.1 — get_incidents_full can watermark on modification_time (modified_after), which is what ingestion wants: an XDR incident keeps growing after creation, and a creation_time watermark fetches it once and never looks again, so every alert that joins it afterwards is lost. The full-incident fetch command is named cortex-xdr-get-incidents-full (id get_incidents_full), not cortex-xdr-fetch-incidents: sitting next to cortex-xdr-get-incidents in the command list, it now reads as what it is — the same call, everything included. A rule created against the old id must be pointed at the new one. 1.3.0 — Richer incident ingestion (get_incidents_full, on get_multiple_incidents_extra_data): incidents now arrive with their alerts in full and their file/network artifacts, where get_incidents only ever answered a 21-field summary carrying neither hosts, users, MITRE nor a single alert. Alert ingestion, and the alerts endpoint answers at last: get_alerts was sending the incidents dialect ({filters, search_from, search_to, sort}) to a grid endpoint that speaks request_data.filter_data (SEARCH_FIELD/SEARCH_TYPE/SEARCH_VALUE blocks, paging.from/to, sort as a list), and every call came back HTTP 500. Body rebuilt from the reference client, rows unwrapped out of their alert_fields envelope, severity code and the dotted status.progress key derived into readable fields. Alert ingestion: get_alerts is now a fetch command (results path reply.alerts, dedup on alert_id, incremental on source_insert_ts) with a bundled OCSF mapper and a Cortex XDR Alert incident type, so detections can be ingested alongside — or instead of — incidents. The incident mapper is fixed on the way past: its severity expression compared strings, which the mapping engine cannot do, so every ingested incident silently took the rule's default severity; it also now carries a detection anchor so MTTD is measurable. It pages past the API's 100-results-per-call ceiling, and sorts oldest-first on an incremental fetch so a truncated window drops the alerts the next poll can still see. 1.2.1 — Connection troubleshooting: the URL is normalised to the tenant host (a pasted /public_api/v1 or console path no longer breaks the call), a non-JSON reply reports the status, content type and body instead of a bare JSON parse error, missing key/key ID is caught up front, nonce and timestamp are sent in both auth modes as the reference client does, and test_connection now probes get_incidents. 1.2.0 — Incident write-back (update_incident: status/severity/assignment/resolve comment) and external alert push (insert_parsed_alerts, insert_cef_alerts). 1.1.0 — Full command coverage: added delete/alias/tag endpoints, abort scan, original alerts, script metadata/code/snippet/exec-status, file retrieval (+details), alert exclusions, device-control violations, audits, distribution url/status/create and RBAC (users, roles, groups, risk score, risky users/hosts). 1.0.0 — Initial release: incident ingestion (get_incidents) with OCSF mapper, endpoints, isolate/unisolate, scan, hash blocklist/allowlist, quarantine/restore, run script + results, alerts retrieval, distributions and action status. Standard or Advanced API authentication." category: endpoint # Per-instance configuration. The base URL is the tenant API root, e.g. diff --git a/integrations/cortex-xdr/mappers/get_alerts.yaml b/integrations/cortex-xdr/mappers/get_alerts.yaml index b7d5d00..907dcaf 100644 --- a/integrations/cortex-xdr/mappers/get_alerts.yaml +++ b/integrations/cortex-xdr/mappers/get_alerts.yaml @@ -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" } diff --git a/integrations/cortex-xdr/mappers/get_incidents_full.yaml b/integrations/cortex-xdr/mappers/get_incidents_full.yaml index 4e85ef6..2a6d81f 100644 --- a/integrations/cortex-xdr/mappers/get_incidents_full.yaml +++ b/integrations/cortex-xdr/mappers/get_incidents_full.yaml @@ -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" }