From ea04ea586c8768c7305541da049af31f9efa78c4 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Wed, 7 Oct 2026 09:13:03 +0000 Subject: [PATCH] fix(sekoia-xdr): mapper reviewed on a real alert (1.2.0) - incident source is 'Sekoia XDR'; Sekoia's `source` (the alert's source address) goes to src_endpoint.ip/.hostname, `target` to dst_endpoint; - description: the alert's sentence with {rule:} replaced by the rule name; the rule documentation (`details`) moves to analytic.desc; - every TTP mapped into attacks; kill_chain_short_id no longer written as an ATT&CK tactic uid; - OCSF times in epoch ms; finding_info.types, analytic.category, metadata.product added. Checked with Riposte's mapper preview on the alert and a variant (IPv4 source, named target, one TTP): no expression error. Co-Authored-By: Claude Opus 5.5 --- integrations/sekoia-xdr/manifest.yaml | 4 +- .../sekoia-xdr/mappers/list_alerts.yaml | 52 +++++++++++++------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/integrations/sekoia-xdr/manifest.yaml b/integrations/sekoia-xdr/manifest.yaml index ffe68e3..62a7be7 100644 --- a/integrations/sekoia-xdr/manifest.yaml +++ b/integrations/sekoia-xdr/manifest.yaml @@ -1,8 +1,8 @@ id: sekoia_xdr name: Sekoia XDR -version: 1.1.0 +version: 1.2.0 description: "Sekoia Defend (XDR) REST API — alert ingestion and lifecycle (list/get/search, status workflow, comments), event search jobs (create/status/results + one-shot search), cases, asset management (get/list, attributes/keys add/remove), users, kill chains and a generic authenticated HTTP passthrough." -changelog: "1.1.0 — Declare the risk tier on the 1 command that carried none, all of them destructive. An unset risk was already gated as destructive, so the approval prompt does not move; what changes is that the tier is a stated decision rather than a default, and a command added later cannot inherit it by omission. | 1.0.0 — Initial release: alert ingestion (list_alerts) with an exhaustive OCSF mapper, 20 commands across alerts, event search, cases, assets, users and kill chains." +changelog: "1.2.0 — Mapper reviewed on a real Entra ID alert. The incident source is now 'Sekoia XDR': it carried Sekoia's own `source` field, the alert's source address, which now goes to src_endpoint.ip (or .hostname for a host), and `target` to dst_endpoint instead of src_endpoint. The description is the alert's sentence with its {rule:} placeholder replaced by the rule name, rather than the rule documentation (now finding_info.analytic.desc). Every TTP is mapped into attacks, not only the first; kill_chain_short_id, Sekoia's own kill chain, no longer poses as an ATT&CK tactic uid. Times are epoch milliseconds as OCSF expects (created_at/updated_at came in seconds, first/last seen as text); alert category in finding_info.types, detection type in analytic.category, product in metadata. | 1.1.0 — Declare the risk tier on the 1 command that carried none, all of them destructive. An unset risk was already gated as destructive, so the approval prompt does not move; what changes is that the tier is a stated decision rather than a default, and a command added later cannot inherit it by omission. | 1.0.0 — Initial release: alert ingestion (list_alerts) with an exhaustive OCSF mapper, 20 commands across alerts, event search, cases, assets, users and kill chains." category: siem # Per-instance configuration. The API host defaults to the EU region; an diff --git a/integrations/sekoia-xdr/mappers/list_alerts.yaml b/integrations/sekoia-xdr/mappers/list_alerts.yaml index 3d2fd45..c7f8acd 100644 --- a/integrations/sekoia-xdr/mappers/list_alerts.yaml +++ b/integrations/sekoia-xdr/mappers/list_alerts.yaml @@ -1,43 +1,63 @@ name: "Sekoia XDR Alerts → OCSF" -description: "Maps a Sekoia Defend (XDR) alert (/v1/sic/alerts, results_path = items) to OCSF Detection Finding fields. Alerts aggregate the matching events; use search_events for per-event detail." +description: "Maps a Sekoia Defend (XDR) alert (/v1/sic/alerts, results_path = items) to OCSF Detection Finding fields. Alerts aggregate the matching events; use search_events for per-event detail. Sekoia's `source` and `target` are the alert's main source and target (an IP address or a host), not the producing system." field_mappings: title: "title" - description: "details" + # The alert's own sentence, with the rule placeholder Sekoia leaves in it + # ('{rule:}') replaced by the rule's name. `details` is the rule's + # documentation — the same for every alert of the rule, MITRE prose and + # vendor links included — and goes to finding_info.analytic.desc instead. + description: "jsonata: description ? $replace(description, '{rule:' & rule.uuid & '}', rule.name) : title" # urgency is a 0-100 score; bucket it onto the 1-5 incident scale. No parentheses (the engine chains # right-associatively) and the lowest band uses '>= 0 ? 1' so the literal 1 sits in a then-branch — # a bare trailing else is read as a field path, not an integer, and would fall back to the default. severity: "urgency.value >= 80 ? 5 : urgency.value >= 60 ? 4 : urgency.value >= 40 ? 3 : urgency.value >= 20 ? 2 : urgency.value >= 0 ? 1 : 1" - # How the alert was generated. - source: "source" + # The producing system. Sekoia's own `source` field is the alert's source + # address or host — it goes to src_endpoint below. + source: "jsonata: 'Sekoia XDR'" # results_path = items; source_path is JSONata over ONE alert object. # Paths absent from a given alert return nothing and are skipped. ocsf: # ── Finding ─────────────────────────────────────────────────────── - { source_path: "uuid", ocsf_field: "finding_info.uid" } - { source_path: "title", ocsf_field: "finding_info.title" } - - { source_path: "rule.description", ocsf_field: "finding_info.desc" } - - { source_path: "created_at", ocsf_field: "finding_info.created_time" } - - { source_path: "updated_at", ocsf_field: "finding_info.modified_time" } - - { source_path: "first_seen_at", ocsf_field: "finding_info.first_seen_time" } - - { source_path: "last_seen_at", ocsf_field: "finding_info.last_seen_time" } + - { source_path: "description ? $replace(description, '{rule:' & rule.uuid & '}', rule.name)", ocsf_field: "finding_info.desc" } + # OCSF times are epoch milliseconds: created_at/updated_at come in seconds, + # first/last seen as ISO 8601. + - { source_path: "created_at * 1000", ocsf_field: "finding_info.created_time" } + - { source_path: "updated_at * 1000", ocsf_field: "finding_info.modified_time" } + - { source_path: "$toMillis(first_seen_at)", ocsf_field: "finding_info.first_seen_time" } + - { source_path: "$toMillis(last_seen_at)", ocsf_field: "finding_info.last_seen_time" } + - { source_path: "alert_type.category ? [alert_type.category]", ocsf_field: "finding_info.types" } # ── Detection rule (analytic) ───────────────────────────────────── - { source_path: "rule.name", ocsf_field: "finding_info.analytic.name" } - { source_path: "rule.uuid", ocsf_field: "finding_info.analytic.uid" } - - { source_path: "rule.description", ocsf_field: "finding_info.analytic.desc" } - { source_path: "rule.type", ocsf_field: "finding_info.analytic.type" } + - { source_path: "detection_type", ocsf_field: "finding_info.analytic.category" } + # The rule's documentation (Sekoia's `details`); rule objects carry no + # description of their own. + - { source_path: "details", ocsf_field: "finding_info.analytic.desc" } # ── Incident state ──────────────────────────────────────────────── - { source_path: "status.name", ocsf_field: "status" } - { source_path: "alert_type.value", ocsf_field: "activity_name" } - { source_path: "similar", ocsf_field: "count" } - { source_path: "urgency.value", ocsf_field: "risk_score" } - # ── MITRE ATT&CK (first TTP / kill-chain step) ──────────────────── - - { source_path: "ttps[0].name", ocsf_field: "attacks.technique.name" } - - { source_path: "ttps[0].id", ocsf_field: "attacks.technique.uid" } - - { source_path: "kill_chain_short_id", ocsf_field: "attacks.tactic.uid" } - # ── Target host ─────────────────────────────────────────────────── - - { source_path: "target", ocsf_field: "src_endpoint.hostname" } + # ── MITRE ATT&CK — every TTP, not only the first ────────────────── + # Sekoia names techniques by their STIX id (attack-pattern--…), not their + # ATT&CK id (T1583), and gives no tactic: kill_chain_short_id is Sekoia's + # own kill chain, not an ATT&CK tactic, so it is no longer written there. + # The array is forced with [...] so a one-TTP alert still yields a list. + - { source_path: "ttps ? [ttps[type = 'attack-pattern'].{'technique': {'name': name, 'uid': id}}]", ocsf_field: "attacks" } + # ── Source and target ───────────────────────────────────────────── + # An address goes to .ip, anything else (a host name) to .hostname. The + # target is where the activity landed: dst_endpoint, not src_endpoint. + - { source_path: "source and ($contains(source, ':') or $contains(source, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? source", ocsf_field: "src_endpoint.ip" } + - { source_path: "source and $not($contains(source, ':') or $contains(source, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? source", ocsf_field: "src_endpoint.hostname" } + - { source_path: "target and ($contains(target, ':') or $contains(target, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? target", ocsf_field: "dst_endpoint.ip" } + - { source_path: "target and $not($contains(target, ':') or $contains(target, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? target", ocsf_field: "dst_endpoint.hostname" } # ── Owning entity / tenant ──────────────────────────────────────── - { source_path: "entity.name", ocsf_field: "cloud.org.name" } - { source_path: "entity.uuid", ocsf_field: "cloud.org.uid" } - { source_path: "short_id", ocsf_field: "metadata.uid" } - { source_path: "community_uuid", ocsf_field: "metadata.tenant_uid" } + - { source_path: "'Sekoia XDR'", ocsf_field: "metadata.product.name" } + - { source_path: "'Sekoia'", ocsf_field: "metadata.product.vendor_name" }