da2fcbba6c
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
142 lines
13 KiB
YAML
142 lines
13 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 ──────────────────────────────────
|
|
- { 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" }
|
|
# ── 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" }
|