feat(sentinelone): with_details on the unified alerts fetch (v1.13.0)

The list nodes carry no indicator, no ATT&CK mapping, no observable and no
related alert — everything that says why the console fired, and the analyst
opens the alert in SentinelOne to find it. with_details reads the per-alert
view for every alert the fetch returns and folds it into the alert, so the
incident is created with that context instead of a title and a severity.

Read in aliased batches of ten, so a default-limit fetch costs ten extra
requests rather than a hundred. Merged under the list node — the list is the
authority on the fields both carry, the detail only fills in — so the richer
list asset/process survive the merge and gain process.username and file.size.
Best effort: an alert the console will not detail keeps its list fields and
the shortfall is reported in pagination.details, rather than failing a fetch
whose alerts are already in hand.

Off by default, and verified query-identical to 1.12.0 for a rule that does
not set it.

The mapper gains evidences, finding_info.attacks, finding_info.related_events
and the full observables list. Each is guarded with $exists and wrapped in [ ]
— checked against the jsonata-go build the scheduler uses: the brackets keep a
single indicator from collapsing into a bare object, and the guard leaves the
entry undefined on a plain list fetch so it cannot write an empty array over
the file summary those alerts do carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Rk9N6vNtp3q7YvLpjcc
This commit is contained in:
2026-09-09 12:32:49 +00:00
parent fe564cec2c
commit 31e596d770
3 changed files with 97 additions and 4 deletions
@@ -113,3 +113,17 @@ ocsf:
# ── The alert's own file summary, hash type unstated ──────────────
- { source_path: "fileName", ocsf_field: "observables.name" }
- { source_path: "fileHash", ocsf_field: "observables.value" }
# ── Only present when the fetch ran with_details ──────────────────
# Absent paths are skipped at ingestion, so these stay inert on a plain
# list fetch. observables comes last on purpose: with the detail read, the
# full list replaces the single file summary set just above.
- { source_path: "process.username", ocsf_field: "actor.user.name" }
- { source_path: "process.file.size", ocsf_field: "actor.process.file.size" }
- { source_path: "process.file.size", ocsf_field: "file.size" }
# [ ] forces a list of one to stay a list; the $exists guard keeps an empty
# or absent value undefined, so a plain list fetch skips these entries
# instead of writing an empty array over the file summary above.
- { source_path: '$exists(indicators) ? [indicators]', ocsf_field: "evidences" }
- { source_path: '$exists(indicators.attacks) ? [indicators.attacks.{"tactic": {"name": tactic.name, "uid": tactic.uid}, "technique": {"name": technique.name, "uid": technique.uid}}]', ocsf_field: "finding_info.attacks" }
- { source_path: '$exists(relatedAlerts) ? [relatedAlerts.{"uid": id, "title": name}]', ocsf_field: "finding_info.related_events" }
- { source_path: '$exists(observables) ? [observables.{"name": name, "value": value, "type": type}]', ocsf_field: "observables" }