Files
riposte-marketplace/integrations/sentinelone/mappers/get_unified_alerts.yaml
T
f3nris a26e71f29e fix(sentinelone): make the process tree draw itself (v1.15.0)
The block reads actor.process.file.name and would have rendered the chain,
but mapperHasProcessChain — the test that places it when a layout is
generated from a mapper — wants actor.process.name alongside the parent's.
This mapper declared only the file name, so the block was never placed and
the chain went unseen on every SentinelOne incident.

process.file.name now feeds both, and certSubject feeds the node's signer
badge (the block reads file.company_name for it, and certSubject is the only
signing identity a unified alert carries).

Verified by running processTree.ts itself over this mapper's output on the
live ClickFix alert: placement true, and a two-node chain explorer.exe ->
powershell.exe carrying the host, the user, both paths and both sets of
hashes when the fetch ran with_details. Without it the chain still draws,
with the parent named but unhashed — which is all the list nodes say.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Rk9N6vNtp3q7YvLpjcc
2026-09-09 12:42:28 +00:00

150 lines
14 KiB
YAML

name: "SentinelOne Unified Alerts → OCSF"
description: "Map of a SentinelOne unified (UAM) alert node (data[]) to OCSF. The console-wide alert model, distinct from the cloud-detection alert of get_alerts: one alert per detection across endpoint, cloud, identity and third-party surfaces. The detected process is the actor (actor.*); the file it ran is the target (file.*)."
field_mappings:
title: "name"
description: "description"
source: "detectionSource.product"
# The last branch yields severity itself, not a number: an alert with no
# severity (or one SentinelOne adds later) then falls through to the rule's
# default rather than being asserted Informational on data that never said so.
severity: "severity = 'CRITICAL' ? 5 : (severity = 'HIGH' ? 4 : (severity = 'MEDIUM' ? 3 : (severity = 'LOW' ? 2 : (severity = 'INFO' ? 1 : severity))))"
# source_path is JSONata over ONE alert node (alert rule results_path = data).
# Paths absent from a given alert are skipped at ingestion, so extra entries are safe.
# A UAM alert carries a few fields with no honest OCSF home — ticketId,
# noteExists, asset.policy, asset.connectivityToConsole, kubernetes controllers.
# They are fetched and stay in the raw payload for playbooks rather than being
# forced into an unrelated field.
ocsf:
# ── Alert identity ────────────────────────────────────────────────
- { source_path: "id", ocsf_field: "finding_info.uid" }
- { source_path: "name", ocsf_field: "finding_info.title" }
- { source_path: "description", ocsf_field: "finding_info.desc" }
- { source_path: "externalId", ocsf_field: "finding_info.uid_alt" }
- { source_path: "classification", ocsf_field: "finding_info.types" }
- { source_path: "createdAt", ocsf_field: "finding_info.created_time" }
- { source_path: "updatedAt", ocsf_field: "finding_info.modified_time" }
- { source_path: "firstSeenAt", ocsf_field: "finding_info.first_seen_time" }
- { source_path: "lastSeenAt", ocsf_field: "finding_info.last_seen_time" }
- { source_path: "detectedAt", ocsf_field: "metadata.original_time" }
- { source_path: "storylineId", ocsf_field: "metadata.correlation_uid" }
- { source_path: "attackSurfaces", ocsf_field: "metadata.labels" }
- { source_path: "dataSources", ocsf_field: "metadata.log_source" }
# ── Triage state ──────────────────────────────────────────────────
- { source_path: "severity", ocsf_field: "severity" }
- { source_path: "status", ocsf_field: "status" }
- { source_path: "analystVerdict", ocsf_field: "status_detail" }
- { source_path: "analystVerdict", ocsf_field: "disposition" }
- { source_path: "classification", ocsf_field: "activity_name" }
- { source_path: "result", ocsf_field: "action" }
- { source_path: "confidenceLevel", ocsf_field: "confidence" }
# ── Detecting analytic / rule ─────────────────────────────────────
- { source_path: "analytics.uid", ocsf_field: "finding_info.analytic.uid" }
- { source_path: "analytics.name", ocsf_field: "finding_info.analytic.name" }
- { source_path: "analytics.type", ocsf_field: "finding_info.analytic.type" }
- { source_path: "analytics.category", ocsf_field: "finding_info.analytic.category" }
- { source_path: "analytics.name", ocsf_field: "rule.name" }
- { source_path: "analytics.uid", ocsf_field: "rule.uid" }
- { source_path: "analytics.typeValue", ocsf_field: "rule.type" }
# ── Detecting product ─────────────────────────────────────────────
- { source_path: "detectionSource.product", ocsf_field: "metadata.product.name" }
- { source_path: "detectionSource.vendor", ocsf_field: "metadata.product.vendor_name" }
- { source_path: "detectionSource.engine", ocsf_field: "metadata.product.feature.name" }
- { source_path: "asset.agentVersion", ocsf_field: "metadata.product.version" }
# ── Asset (current state) ─────────────────────────────────────────
- { source_path: "asset.name", ocsf_field: "src_endpoint.hostname" }
- { source_path: "asset.name", ocsf_field: "device.hostname" }
- { source_path: "asset.osType", ocsf_field: "device.os.type" }
- { source_path: "asset.agentVersion", ocsf_field: "device.agent.version" }
- { source_path: "asset.agentUuid", ocsf_field: "device.uid" }
- { source_path: "asset.id", ocsf_field: "device.uid_alt" }
- { source_path: "asset.type", ocsf_field: "device.type" }
- { source_path: "asset.assetTypeClassifier", ocsf_field: "src_endpoint.type" }
- { source_path: "asset.osType", ocsf_field: "src_endpoint.os.type" }
- { source_path: "asset.osVersion", ocsf_field: "src_endpoint.os.version" }
- { source_path: "asset.lastLoggedInUser", ocsf_field: "device.owner.name" }
# ── Asset (state at detection) ────────────────────────────────────
- { source_path: "detectionTime.asset.ipV4", ocsf_field: "src_endpoint.ip" }
- { source_path: "detectionTime.asset.ipV6", ocsf_field: "device.ip" }
- { source_path: "detectionTime.asset.domain", ocsf_field: "src_endpoint.domain" }
- { source_path: "detectionTime.asset.osName", ocsf_field: "src_endpoint.os.name" }
- { source_path: "detectionTime.asset.osRevision", ocsf_field: "device.os.build" }
- { source_path: "detectionTime.asset.subscriptionTime", ocsf_field: "device.first_seen_time" }
# ── Attacker (the other party) ────────────────────────────────────
- { source_path: "detectionTime.attacker.host", ocsf_field: "dst_endpoint.hostname" }
- { source_path: "detectionTime.attacker.ip", ocsf_field: "dst_endpoint.ip" }
# ── Targeted user ─────────────────────────────────────────────────
- { source_path: "detectionTime.targetUser.name", ocsf_field: "user.name" }
- { source_path: "detectionTime.targetUser.emailAddress", ocsf_field: "user.email_addr" }
- { source_path: "detectionTime.targetUser.domain", ocsf_field: "user.domain" }
# ── Cloud workload ────────────────────────────────────────────────
- { source_path: "detectionTime.cloud.accountId", ocsf_field: "cloud.account.uid" }
- { source_path: "detectionTime.cloud.cloudProvider", ocsf_field: "cloud.provider" }
- { source_path: "detectionTime.cloud.location", ocsf_field: "cloud.region" }
- { source_path: "detectionTime.cloud.instanceId", ocsf_field: "device.instance_uid" }
- { source_path: "detectionTime.cloud.network", ocsf_field: "device.vpc_uid" }
# ── SentinelOne tenancy (as scoped at detection) ──────────────────
- { source_path: "detectionTime.scope.accountName", ocsf_field: "cloud.org.name" }
- { source_path: "detectionTime.scope.accountId", ocsf_field: "cloud.org.uid" }
- { source_path: "detectionTime.scope.siteName", ocsf_field: "device.zone" }
- { source_path: "realTime.scope.site.id", ocsf_field: "metadata.tenant_uid" }
# ── Container ─────────────────────────────────────────────────────
- { source_path: "detectionTime.kubernetes.containerName", ocsf_field: "container.name" }
- { source_path: "detectionTime.kubernetes.containerId", ocsf_field: "container.uid" }
- { source_path: "detectionTime.kubernetes.containerImageName", ocsf_field: "container.image.name" }
- { source_path: "detectionTime.kubernetes.podName", ocsf_field: "container.pod_uuid" }
# ── Detected process — the actor ──────────────────────────────────
# actor.process.name alongside the file name: the process-tree block reads
# either, but places itself automatically only on a mapper that declares
# actor.process.name together with the parent's, and a chain of two named
# processes is exactly what this alert carries.
- { source_path: "process.file.name", ocsf_field: "actor.process.name" }
- { source_path: "process.cmdLine", ocsf_field: "actor.process.cmd_line" }
- { source_path: "process.parentName", ocsf_field: "actor.process.parent_process.name" }
- { source_path: "process.file.name", ocsf_field: "actor.process.file.name" }
- { source_path: "process.file.path", ocsf_field: "actor.process.file.path" }
- { source_path: "process.file.sha256", ocsf_field: "actor.process.file.hashes.sha256" }
- { source_path: "process.file.sha1", ocsf_field: "actor.process.file.hashes.sha1" }
- { source_path: "process.file.md5", ocsf_field: "actor.process.file.hashes.md5" }
# ── The file it ran — the target ──────────────────────────────────
- { source_path: "process.file.name", ocsf_field: "file.name" }
- { source_path: "process.file.path", ocsf_field: "file.path" }
- { source_path: "process.file.sha256", ocsf_field: "file.hashes.sha256" }
- { source_path: "process.file.sha1", ocsf_field: "file.hashes.sha1" }
- { source_path: "process.file.md5", ocsf_field: "file.hashes.md5" }
- { source_path: "process.file.certSubject", ocsf_field: "file.signature.value" }
# The signer badge on the process node reads company_name; certSubject is the
# only signing identity a unified alert carries.
- { source_path: "process.file.certSubject", ocsf_field: "actor.process.file.company_name" }
# ── Triage ownership ──────────────────────────────────────────────
- { source_path: "assignee.fullName", ocsf_field: "assignee.name" }
- { source_path: "assignee.email", ocsf_field: "assignee.email_addr" }
# ── 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.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: '$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" }