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:<uuid>} 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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:<uuid>} 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
|
||||
|
||||
@@ -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:<uuid>}') 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" }
|
||||
|
||||
Reference in New Issue
Block a user