fix(sentinelone): map the unified alert as a real one is shaped (v1.14.0)

Checked against a live STAR alert, three of the details mappings were wrong
about where the data actually sits.

Its alert-level observables list is empty while all twelve observables sit
under the indicators, so the mapper saw none. The two lists are appended now
rather than reading the alert level alone.

The same alert has process.username null while its indicators name the user
(src.process.user), and names its parent process without hashing it anywhere
else. actor.user.name and the parent process path and hashes are read from the
indicator observables, with the alert's own field winning where it is filled
in, so nothing regresses on an alert that does populate it.

attacks and relatedAlerts are guarded on $count rather than $exists: that
alert carries "attacks": null, and null exists in JSONata — the entry would
have written an empty array onto every alert without an ATT&CK mapping.

Verified by running the whole mapper through the scheduler's own jsonata-go
build on that payload, with and without with_details: 95 entries, no empty
arrays, and the plain list fetch still falls back to the fileName/fileHash
observable.

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:38:21 +00:00
parent 31e596d770
commit da2fcbba6c
2 changed files with 18 additions and 6 deletions
@@ -117,13 +117,25 @@ ocsf:
# 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" }
# An indicator's observables carry what the alert body leaves null — on a
# real STAR alert process.username is null while src.process.user is set,
# and the parent process is named but never hashed outside this list.
# [0] takes the first indicator to name each one; they agree in practice.
- { source_path: '(indicators.observables[name="src.process.user"].value)[0]', ocsf_field: "actor.user.name" }
- { source_path: '(indicators.observables[name="src.parent.process.file.path"].value)[0]', ocsf_field: "actor.process.parent_process.file.path" }
- { source_path: '(indicators.observables[name="src.parent.process.file.sha256"].value)[0]', ocsf_field: "actor.process.parent_process.file.hashes.sha256" }
- { source_path: '(indicators.observables[name="src.parent.process.file.sha1"].value)[0]', ocsf_field: "actor.process.parent_process.file.hashes.sha1" }
- { source_path: '(indicators.observables[name="src.parent.process.file.md5"].value)[0]', ocsf_field: "actor.process.parent_process.file.hashes.md5" }
# Last, so the alert's own field wins when it is filled in.
- { source_path: "process.username", ocsf_field: "actor.user.name" }
# [ ] 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" }
- { source_path: '$count(indicators.attacks) > 0 ? [indicators.attacks.{"tactic": {"name": tactic.name, "uid": tactic.uid}, "technique": {"name": technique.name, "uid": technique.uid}}]', ocsf_field: "finding_info.attacks" }
- { source_path: '$count(relatedAlerts) > 0 ? [relatedAlerts.{"uid": id, "title": name}]', ocsf_field: "finding_info.related_events" }
# The alert-level list is empty on a STAR alert while every observable sits
# under the indicators, so the two are appended rather than one preferred.
- { source_path: '$exists(observables) or $exists(indicators.observables) ? [($append($boolean(observables) ? observables : [], $boolean(indicators.observables) ? indicators.observables : [])).{"name": name, "value": value, "type": type}]', ocsf_field: "observables" }