Files
riposte-marketplace/integrations/sentinelone/mappers/get_unified_alerts.yaml
T
f3nris fd4b1b18f7 feat(sentinelone): fetch the unified (UAM) alerts (v1.8.0)
The package could already write to a unified alert — update its status and
its analyst verdict — but never read one. The only fetch was get_alerts,
which reads /cloud-detection/alerts: a different object, covering only the
detections the STAR and custom rules raise. Everything SentinelOne surfaces
through the console-wide alert model — cloud, identity, third-party — had
no way into an incident.

get_unified_alerts reads the unifiedalerts GraphQL endpoint, alongside the
two mutations already pointed at it. Notes on the shape it returns:

- edges/node is flattened to a plain data[]. The envelope carries no id of
  its own, so a rule dedup'ing on a top-level id would key every alert on
  nothing; flattened, the ingest block matches the REST commands in this
  package (results_path: data, dedup_key: id).
- Sorted ascending, so a run truncated by the limit leaves the watermark on
  the oldest unseen alert instead of skipping everything behind the newest.
- created_after is normalised from ISO8601, epoch seconds or epoch
  milliseconds: the scheduler injects the watermark raw, in whichever of the
  three formats the rule was configured with. The script builds the GraphQL
  filter itself, so the incremental input stays a plain timestamp.
- A GraphQL error arrives as HTTP 200 with an errors array. Left unread, an
  unusable query would look like an endless run of empty, successful fetches.

The mapper covers 74 fields. Its severity branch ends on the raw value
rather than a number, so an alert with no severity — or a level SentinelOne
adds later — falls through to the rule's default instead of being asserted
Informational on data that never said so. A UAM alert also carries fields
with no honest OCSF home (assignee, ticketId, the kubernetes controllers);
they are fetched and left in the raw payload rather than forced into an
unrelated field.

The package default ingestion still points at get_threats: the new command
is opt-in from the Collect editor, so no existing instance changes what it
collects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 16:16:46 +02:00

106 lines
9.5 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 — assignee, 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: "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.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" }
# ── 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 alert's own file summary, hash type unstated ──────────────
- { source_path: "fileName", ocsf_field: "observables.name" }
- { source_path: "fileHash", ocsf_field: "observables.value" }