Compare commits

...

2 Commits

Author SHA1 Message Date
f3nris df27876a8f feat(sentinelone): declare a risk tier on every command (v1.9.0)
An unset risk is treated as destructive by the agent approval gate. Only
test_connection carried one, so the other 83 commands — get-threats,
get-alerts, count-agents, every lookup in the package — sat behind the same
gate as isolate-agent and run-remote-script. A gate that stops everything
stops being read as a signal.

The package was the marketplace's worst on this: 2 of 86 commands annotated,
where all 180 packages declare some and 57 declare them throughout. The next
weakest is panos at 2 of 10.

39 read, 22 safe_write, 24 destructive. The line drawn for destructive is
impact on the estate, not reversibility — isolate-agent is reversible and is
the canonical destructive example:

- what removes detection or protection: creating an exclusion, disabling or
  editing a STAR rule (an edit can silently narrow a rule to match nothing)
- what adds blocking: the hash blocklist, a firewall rule
- what changes containment in either direction: isolate and reconnect both,
  since undoing containment mid-incident is its own decision
- what reboots or shuts a host down: shutdown, uninstall, enable-agent (it
  takes an optional reboot), move-agent (it changes the applied policy)
- every delete, including removals from the blocklist and the exclusions

Authoring detection content in a customer console is a change to that
console, so the whole STAR rule lifecycle is gated: create as well as update,
disable and delete. Draft status is one click from live and does not earn a
lower tier.

Additive threat intel stays safe_write: creating an IOC or enabling an
existing STAR rule widens what the console sees, which is not the direction
that needs a gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 16:26:05 +02:00
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
4 changed files with 403 additions and 2 deletions
@@ -0,0 +1,3 @@
name: "SentinelOne Unified Alert"
color: "#f28c28"
icon: "shield-alert"
+111 -2
View File
@@ -1,8 +1,8 @@
id: sentinelone
name: SentinelOne
version: 1.7.1
version: 1.9.0
description: "SentinelOne Singularity (API v2.1) — endpoint detection & response: triage threats, enrich, isolate/reconnect hosts, mitigate, scan."
changelog: "1.7.1 — count_agents' isActive filter is a text field again: declared as a boolean it rendered as a checkbox, which has no empty state, so the count could never be asked for both the active and the inactive agents. Left empty the filter is not sent. 1.7.0 — count_agents takes an accountIds filter, so a multi-tenant console can be counted one account at a time. 1.6.0 — count_agents takes an isActive filter, to count the active agents apart from the inactive ones; left unset, the count still covers both. 1.5.0 — count_agents takes four more filters: agentVersion__gt to count the fleet still below a target build, networkStatuses to count only the endpoints in a given connection state, operationalStatesNin to leave states out of the count, and machineTypes to count servers apart from laptops. 1.4.0 — get_threats and get_alerts accept an optional account_ids filter (accountIds), so a multi-tenant console can be scoped to one or more accounts at fetch time. 1.3.0 — Exhaustive OCSF mappers: get_threats (38 fields) and get_alerts (58 fields) now cover device, finding, malware, actor/target process, file, registry, network, indicators and container. 1.2.4 — Re-modelled the OCSF mappers to OCSF actor/target semantics: the initiating process maps to actor.* (actor.process, actor.user), and the process/file acted upon maps to the target (process.*, file.*, user.*). 1.2.3 — Expanded the OCSF mappers (get_threats, get_alerts) with more fields: file hashes (sha1/md5), file ext/size, finding/rule ids, confidence, status, parent process and device/user identity. 1.2.2 — Added pre-built OCSF mappers (get_threats, get_alerts) for the mapper library. 1.2.1 — Added test_connection for the instance Test button. 1.2.0 — Added 13 commands: threat-analysis, threat-download-from-cloud, abort-endpoint-scan, endpoint-fetch-logs, fetch-file, get-remote-script-task-status/results, get-service-users, list-installed-singularity-marketplace-applications, update-uam-alert-status/verdict, run-powerquery and create-tag-rule (83 commands total). 1.1.0 — Command names prefixed with 'sentinelone-' (e.g. sentinelone-isolate-agent) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: 70 commands covering agents, threats, alerts, blocklist/exclusions, IOCs, STAR rules, Deep Visibility, remote scripts, tags, firewall and network discovery based on the SentinelOne API v2.1."
changelog: "1.9.0 — Every command now declares its risk tier, so the agent approval gate can tell a lookup from an action. Only test_connection and get_unified_alerts did; the other 83 were unset, and an unset risk is treated as destructive, which put get-threats and get-alerts behind the same gate as isolate-agent and run-remote-script. 39 read, 22 safe_write, 24 destructive. Destructive covers what removes detection or protection (exclusions, disabling or editing a STAR rule), what adds blocking (blocklist, firewall rule), what changes containment in either direction (isolate and reconnect both), what reboots or shuts a host down, and every delete. 1.8.0 — get_unified_alerts fetches the unified (UAM) alerts: the console-wide alert model served by the unifiedalerts GraphQL endpoint, which get_alerts (cloud-detection) does not cover. Walks the cursor to the requested limit, sorts ascending so a truncated run leaves the watermark on the oldest unseen alert, and flattens edges/node to a plain data[] so the alert rule dedups on a top-level id. Ships with an OCSF mapper (74 fields) and a 'SentinelOne Unified Alert' incident type. 1.7.1 — count_agents' isActive filter is a text field again: declared as a boolean it rendered as a checkbox, which has no empty state, so the count could never be asked for both the active and the inactive agents. Left empty the filter is not sent. 1.7.0 — count_agents takes an accountIds filter, so a multi-tenant console can be counted one account at a time. 1.6.0 — count_agents takes an isActive filter, to count the active agents apart from the inactive ones; left unset, the count still covers both. 1.5.0 — count_agents takes four more filters: agentVersion__gt to count the fleet still below a target build, networkStatuses to count only the endpoints in a given connection state, operationalStatesNin to leave states out of the count, and machineTypes to count servers apart from laptops. 1.4.0 — get_threats and get_alerts accept an optional account_ids filter (accountIds), so a multi-tenant console can be scoped to one or more accounts at fetch time. 1.3.0 — Exhaustive OCSF mappers: get_threats (38 fields) and get_alerts (58 fields) now cover device, finding, malware, actor/target process, file, registry, network, indicators and container. 1.2.4 — Re-modelled the OCSF mappers to OCSF actor/target semantics: the initiating process maps to actor.* (actor.process, actor.user), and the process/file acted upon maps to the target (process.*, file.*, user.*). 1.2.3 — Expanded the OCSF mappers (get_threats, get_alerts) with more fields: file hashes (sha1/md5), file ext/size, finding/rule ids, confidence, status, parent process and device/user identity. 1.2.2 — Added pre-built OCSF mappers (get_threats, get_alerts) for the mapper library. 1.2.1 — Added test_connection for the instance Test button. 1.2.0 — Added 13 commands: threat-analysis, threat-download-from-cloud, abort-endpoint-scan, endpoint-fetch-logs, fetch-file, get-remote-script-task-status/results, get-service-users, list-installed-singularity-marketplace-applications, update-uam-alert-status/verdict, run-powerquery and create-tag-rule (83 commands total). 1.1.0 — Command names prefixed with 'sentinelone-' (e.g. sentinelone-isolate-agent) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: 70 commands covering agents, threats, alerts, blocklist/exclusions, IOCs, STAR rules, Deep Visibility, remote scripts, tags, firewall and network discovery based on the SentinelOne API v2.1."
category: endpoint
# Per-instance configuration. The scripts build the API base as <url>/web/api/v2.1.
@@ -34,6 +34,7 @@ commands:
- id: get_threats
name: sentinelone-get-threats
description: List threats/detections matching filters.
risk: read
inputs_schema:
properties:
limit: { type: number, description: "Max results (default 20)" }
@@ -52,6 +53,7 @@ commands:
- id: list_agents
name: sentinelone-list-agents
description: List endpoints (agents) matching filters.
risk: read
inputs_schema:
properties:
computer_name: { type: string, description: "Substring match on computer name" }
@@ -64,6 +66,7 @@ commands:
- id: get_agent
name: sentinelone-get-agent
description: Get details for one or more agents by ID.
risk: read
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs" }
@@ -73,6 +76,7 @@ commands:
- id: get_hash_verdict
name: sentinelone-get-hash-verdict
description: Reputation verdict for a SHA1 hash.
risk: read
inputs_schema:
properties:
hash: { type: string, description: "SHA1 hash" }
@@ -83,6 +87,7 @@ commands:
- id: isolate_agent
name: sentinelone-isolate-agent
description: Disconnect agents from the network.
risk: destructive
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs" }
@@ -92,6 +97,7 @@ commands:
- id: reconnect_agent
name: sentinelone-reconnect-agent
description: Reconnect agents to the network.
risk: destructive
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs" }
@@ -101,6 +107,7 @@ commands:
- id: mitigate_threat
name: sentinelone-mitigate-threat
description: Apply a mitigation action to threats (kill, quarantine, remediate, rollback).
risk: destructive
inputs_schema:
properties:
action: { type: string, description: "kill | quarantine | un-quarantine | remediate | rollback-remediation" }
@@ -111,6 +118,7 @@ commands:
- id: initiate_scan
name: sentinelone-initiate-scan
description: Start a full disk scan on agents.
risk: safe_write
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs" }
@@ -120,6 +128,7 @@ commands:
- id: write_threat_note
name: sentinelone-write-threat-note
description: Add a note to one or more threats.
risk: safe_write
inputs_schema:
properties:
threat_ids: { type: string, description: "Comma-separated threat IDs" }
@@ -131,6 +140,7 @@ commands:
- id: shutdown_agent
name: sentinelone-shutdown-agent
description: "Send a shutdown command to agents matching the filter."
risk: destructive
inputs_schema:
properties:
agent_id: { type: string, description: "Comma-separated agent IDs" }
@@ -141,6 +151,7 @@ commands:
- id: uninstall_agent
name: sentinelone-uninstall-agent
description: "Send an uninstall command to agents matching the filter."
risk: destructive
inputs_schema:
properties:
agent_id: { type: string, description: "Comma-separated agent IDs" }
@@ -151,6 +162,7 @@ commands:
- id: broadcast_message
name: sentinelone-broadcast-message
description: "Broadcast a message to agents matching the filter."
risk: safe_write
inputs_schema:
properties:
message: { type: string, description: "Message to broadcast to agents" }
@@ -162,6 +174,7 @@ commands:
- id: enable_agent
name: sentinelone-enable-agent
description: "Enable agents matching the given IDs, optionally rebooting them."
risk: destructive
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs" }
@@ -171,6 +184,7 @@ commands:
- id: move_agent
name: sentinelone-move-agent
description: "Move agents into the specified group."
risk: destructive
inputs_schema:
properties:
group_id: { type: string, description: "Target group ID" }
@@ -180,6 +194,7 @@ commands:
- id: count_agents
name: sentinelone-count-agents
description: "Count agents matching the given filters."
risk: read
inputs_schema:
properties:
computer_name: { type: string, description: "Filter by computer name" }
@@ -198,6 +213,7 @@ commands:
- id: get_agent_mac
name: sentinelone-get-agent-mac
description: "Retrieve network interfaces (IP and MAC) for a single agent."
risk: read
inputs_schema:
properties:
agent_id: { type: string, description: "Agent ID" }
@@ -206,6 +222,7 @@ commands:
- id: get_installed_applications
name: sentinelone-get-installed-applications
description: "Retrieve the applications installed on the given agents."
risk: read
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs" }
@@ -214,6 +231,7 @@ commands:
- id: manage_tags
name: sentinelone-manage-tags
description: "Add, override or remove a tag on the given agents."
risk: safe_write
inputs_schema:
properties:
tagId: { type: string, description: "Tag ID to apply" }
@@ -226,6 +244,7 @@ commands:
- id: resolve_threat
name: sentinelone-resolve-threat
description: "Mark one or more threats as resolved."
risk: safe_write
inputs_schema:
properties:
threat_ids: { type: string, description: "Comma-separated threat IDs to resolve." }
@@ -234,6 +253,7 @@ commands:
- id: mark_as_threat
name: sentinelone-mark-as-threat
description: "Mark detections as a true threat, scoped to a site or the whole tenant."
risk: safe_write
inputs_schema:
properties:
threat_ids: { type: string, description: "Comma-separated threat IDs to mark." }
@@ -243,6 +263,7 @@ commands:
- id: update_threats_verdict
name: sentinelone-update-threats-verdict
description: "Set the analyst verdict on one or more threats."
risk: safe_write
inputs_schema:
properties:
verdict: { type: string, description: "undefined, true_positive, false_positive, or suspicious." }
@@ -252,6 +273,7 @@ commands:
- id: update_threats_status
name: sentinelone-update-threats-status
description: "Set the incident status on one or more threats."
risk: safe_write
inputs_schema:
properties:
status: { type: string, description: "in_progress, resolved, or unresolved." }
@@ -261,6 +283,7 @@ commands:
- id: get_threat_notes
name: sentinelone-get-threat-notes
description: "Retrieve the notes attached to a single threat."
risk: read
inputs_schema:
properties:
threat_id: { type: string, description: "ID of the threat whose notes to retrieve." }
@@ -269,6 +292,7 @@ commands:
- id: get_threat_summary
name: sentinelone-get-threat-summary
description: "Retrieve a dashboard threat summary, optionally filtered by site and group."
risk: read
inputs_schema:
properties:
site_ids: { type: string, description: "Comma-separated site IDs." }
@@ -278,6 +302,7 @@ commands:
- id: fetch_threat_file
name: sentinelone-fetch-threat-file
description: "Request the threat file for download, protected by a ZIP password."
risk: safe_write
inputs_schema:
properties:
threat_id: { type: string, description: "ID of the threat whose file to fetch." }
@@ -287,6 +312,7 @@ commands:
- id: get_alerts
name: sentinelone-get-alerts
description: "Retrieve cloud-detection alerts filtered by creation window and optional criteria."
risk: read
inputs_schema:
properties:
created_from: { type: string, description: "Return alerts created at or after this timestamp." }
@@ -307,6 +333,7 @@ commands:
- id: update_alerts_verdict
name: sentinelone-update-alerts-verdict
description: "Set the analyst verdict on one or more cloud-detection alerts."
risk: safe_write
inputs_schema:
properties:
verdict: { type: string, description: "undefined, true_positive, false_positive, or suspicious." }
@@ -316,6 +343,7 @@ commands:
- id: update_alerts_status
name: sentinelone-update-alerts-status
description: "Set the incident status on one or more cloud-detection alerts."
risk: safe_write
inputs_schema:
properties:
status: { type: string, description: "in_progress, resolved, or unresolved." }
@@ -327,6 +355,7 @@ commands:
- id: get_blocklist
name: sentinelone-get-blocklist
description: "Retrieve hash blocklist (restriction) entries, optionally filtered by hash, scope, and pagination."
risk: read
inputs_schema:
properties:
global: { type: boolean, description: "Query the global (tenant) scope. Defaults to true." }
@@ -341,6 +370,7 @@ commands:
- id: add_hash_to_blocklist
name: sentinelone-add-hash-to-blocklist
description: "Add a SHA1 or SHA256 hash to the blocklist for the given scope."
risk: destructive
inputs_schema:
properties:
os_type: { type: string, description: "Target OS type: windows, linux, or macos." }
@@ -356,6 +386,7 @@ commands:
- id: remove_hash_from_blocklist
name: sentinelone-remove-hash-from-blocklist
description: "Find blocklist entries matching a SHA1 hash and delete them."
risk: destructive
inputs_schema:
properties:
sha1: { type: string, description: "SHA1 hash to remove." }
@@ -365,6 +396,7 @@ commands:
- id: get_white_list
name: sentinelone-get-white-list
description: "Retrieve exclusion items, optionally filtered by IDs, type, OS, and scope."
risk: read
inputs_schema:
properties:
item_ids: { type: string, description: "Comma-separated exclusion item IDs." }
@@ -379,6 +411,7 @@ commands:
- id: create_white_list_item
name: sentinelone-create-white-list-item
description: "Create an exclusion (whitelist) item of the given type and value."
risk: destructive
inputs_schema:
properties:
exclusion_type: { type: string, description: "Exclusion type (path, white_hash, certificate, browser, file_type)." }
@@ -394,6 +427,7 @@ commands:
- id: remove_item_from_whitelist
name: sentinelone-remove-item-from-whitelist
description: "Find exclusion items matching a value and delete them."
risk: destructive
inputs_schema:
properties:
item: { type: string, description: "The exclusion value to find and remove." }
@@ -404,6 +438,7 @@ commands:
- id: delete_exclusion
name: sentinelone-delete-exclusion
description: "Delete exclusion items by their IDs and type."
risk: destructive
inputs_schema:
properties:
ids: { type: string, description: "Comma-separated exclusion item IDs to delete." }
@@ -413,6 +448,7 @@ commands:
- id: create_ioc
name: sentinelone-create-ioc
description: "Create a threat-intelligence IOC for the given account scope."
risk: safe_write
inputs_schema:
properties:
name: { type: string, description: "Name of the IOC." }
@@ -429,6 +465,7 @@ commands:
- id: delete_ioc
name: sentinelone-delete-ioc
description: "Delete threat-intelligence IOCs by UUID within the given account scope."
risk: destructive
inputs_schema:
properties:
account_ids: { type: string, description: "Comma-separated account IDs." }
@@ -438,6 +475,7 @@ commands:
- id: get_iocs
name: sentinelone-get-iocs
description: "Retrieve threat-intelligence IOCs, optionally filtered by type, value, source, and name."
risk: read
inputs_schema:
properties:
account_ids: { type: string, description: "Comma-separated account IDs." }
@@ -453,6 +491,7 @@ commands:
- id: get_activities
name: sentinelone-get-activities
description: "Retrieve activities, optionally filtered by date, agents, or threats."
risk: read
inputs_schema:
properties:
created_after: { type: string, description: "Activities created after this timestamp." }
@@ -464,6 +503,7 @@ commands:
- id: get_groups
name: sentinelone-get-groups
description: "List groups with optional filters."
risk: read
inputs_schema:
properties:
group_type: { type: string, description: "Group type (static, dynamic)." }
@@ -477,6 +517,7 @@ commands:
- id: delete_group
name: sentinelone-delete-group
description: "Delete a group by its ID."
risk: destructive
inputs_schema:
properties:
group_id: { type: string, description: "ID of the group to delete." }
@@ -485,6 +526,7 @@ commands:
- id: get_sites
name: sentinelone-get-sites
description: "List sites with optional filters."
risk: read
inputs_schema:
properties:
query: { type: string, description: "Free-text search." }
@@ -498,6 +540,7 @@ commands:
- id: get_site
name: sentinelone-get-site
description: "Retrieve a single site by its ID."
risk: read
inputs_schema:
properties:
site_id: { type: string, description: "ID of the site." }
@@ -506,6 +549,7 @@ commands:
- id: reactivate_site
name: sentinelone-reactivate-site
description: "Reactivate an expired site."
risk: safe_write
inputs_schema:
properties:
site_id: { type: string, description: "ID of the site to reactivate." }
@@ -516,6 +560,7 @@ commands:
- id: expire_site
name: sentinelone-expire-site
description: "Expire a site immediately."
risk: destructive
inputs_schema:
properties:
site_id: { type: string, description: "ID of the site to expire." }
@@ -524,6 +569,7 @@ commands:
- id: get_accounts
name: sentinelone-get-accounts
description: "List all accounts, or retrieve a single account when an ID is provided."
risk: read
inputs_schema:
properties:
account_id: { type: string, description: "Optional account ID." }
@@ -532,6 +578,7 @@ commands:
- id: list_users
name: sentinelone-list-users
description: "List users, optionally filtered by account IDs."
risk: read
inputs_schema:
properties:
account_ids: { type: string, description: "Comma-separated account IDs." }
@@ -541,6 +588,7 @@ commands:
- id: delete_user
name: sentinelone-delete-user
description: "Delete a user by their ID."
risk: destructive
inputs_schema:
properties:
user_id: { type: string, description: "ID of the user to delete." }
@@ -549,6 +597,7 @@ commands:
- id: create_query
name: sentinelone-create-query
description: "Initialize a Deep Visibility query over a given time range."
risk: safe_write
inputs_schema:
properties:
query: { type: string, description: "Deep Visibility query string." }
@@ -559,6 +608,7 @@ commands:
- id: get_dv_query_status
name: sentinelone-get-dv-query-status
description: "Get the status of a Deep Visibility query by ID."
risk: read
inputs_schema:
properties:
query_id: { type: string, description: "ID of the Deep Visibility query." }
@@ -567,6 +617,7 @@ commands:
- id: get_events
name: sentinelone-get-events
description: "Retrieve events for a completed Deep Visibility query."
risk: read
inputs_schema:
properties:
query_id: { type: string, description: "ID of the Deep Visibility query." }
@@ -577,6 +628,7 @@ commands:
- id: get_processes
name: sentinelone-get-processes
description: "Retrieve process events for a completed Deep Visibility query."
risk: read
inputs_schema:
properties:
query_id: { type: string, description: "ID of the Deep Visibility query." }
@@ -588,6 +640,7 @@ commands:
- id: create_star_rule
name: sentinelone-create-star-rule
description: "Create a STAR custom detection rule in Draft status."
risk: destructive
inputs_schema:
properties:
name: { type: string, description: "Rule name." }
@@ -607,6 +660,7 @@ commands:
- id: get_star_rules
name: sentinelone-get-star-rules
description: "List STAR custom detection rules."
risk: read
inputs_schema:
properties:
status: { type: string, description: "Filter by rule status." }
@@ -621,6 +675,7 @@ commands:
- id: update_star_rule
name: sentinelone-update-star-rule
description: "Update an existing STAR custom detection rule by ID."
risk: destructive
inputs_schema:
properties:
rule_id: { type: string, description: "ID of the rule to update." }
@@ -642,6 +697,7 @@ commands:
- id: enable_star_rules
name: sentinelone-enable-star-rules
description: "Enable one or more STAR custom detection rules by ID."
risk: safe_write
inputs_schema:
properties:
rule_ids: { type: string, description: "Comma-separated rule IDs to enable." }
@@ -650,6 +706,7 @@ commands:
- id: disable_star_rules
name: sentinelone-disable-star-rules
description: "Disable one or more STAR custom detection rules by ID."
risk: destructive
inputs_schema:
properties:
rule_ids: { type: string, description: "Comma-separated rule IDs to disable." }
@@ -658,6 +715,7 @@ commands:
- id: delete_star_rule
name: sentinelone-delete-star-rule
description: "Delete one or more STAR custom detection rules by ID."
risk: destructive
inputs_schema:
properties:
rule_ids: { type: string, description: "Comma-separated rule IDs to delete." }
@@ -666,6 +724,7 @@ commands:
- id: run_remote_script
name: sentinelone-run-remote-script
description: "Execute a remote script (RSO) on the targeted agents."
risk: destructive
inputs_schema:
properties:
account_ids: { type: string, description: "Comma-separated account IDs." }
@@ -682,6 +741,7 @@ commands:
- id: create_endpoint_tag
name: sentinelone-create-endpoint-tag
description: "Create an endpoint tag in the tag manager."
risk: safe_write
inputs_schema:
properties:
type: { type: string, description: "Tag type." }
@@ -697,6 +757,7 @@ commands:
- id: delete_endpoint_tag
name: sentinelone-delete-endpoint-tag
description: "Delete endpoint tags matching the given scope filter."
risk: destructive
inputs_schema:
properties:
siteIds: { type: string, description: "Comma-separated site IDs." }
@@ -708,6 +769,7 @@ commands:
- id: get_endpoint_tags
name: sentinelone-get-endpoint-tags
description: "List endpoint tags, optionally filtered by key or value substring."
risk: read
inputs_schema:
properties:
key__contains: { type: string, description: "Filter by key substring." }
@@ -717,6 +779,7 @@ commands:
- id: create_firewall_rule
name: sentinelone-create-firewall-rule
description: "Create a firewall control rule scoped to an account or site."
risk: destructive
inputs_schema:
properties:
action: { type: string, description: "Allow | Block." }
@@ -732,6 +795,7 @@ commands:
- id: get_network_discovery_table
name: sentinelone-get-network-discovery-table
description: "Retrieve the Ranger network discovery table view."
risk: read
inputs_schema:
properties:
hostnames__contains: { type: string, description: "Filter by hostname substring." }
@@ -742,6 +806,7 @@ commands:
- id: get_network_discovery_assets
name: sentinelone-get-network-discovery-assets
description: "Retrieve XDR network discovery surface assets."
risk: read
inputs_schema:
properties:
limit: { type: number, description: "Max results (default 50)." }
@@ -754,6 +819,7 @@ commands:
- id: get_shadow_assets
name: sentinelone-get-shadow-assets
description: "Retrieve XDR shadow assets with optional coverage filters."
risk: read
inputs_schema:
properties:
limit: { type: number, description: "Max results (default 50)." }
@@ -767,6 +833,7 @@ commands:
- id: get_gateways
name: sentinelone-get-gateways
description: "List Ranger gateways."
risk: read
inputs_schema:
properties: {}
required: []
@@ -774,6 +841,7 @@ commands:
- id: purge_crash_dumps
name: sentinelone-purge-crash-dumps
description: "Purge agent crash dumps for the specified agents."
risk: destructive
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs." }
@@ -782,6 +850,7 @@ commands:
- id: list_endpoints_with_application
name: sentinelone-list-endpoints-with-application
description: "List the application inventory across endpoints."
risk: read
inputs_schema:
properties:
groupIds: { type: string, description: "Comma-separated group IDs." }
@@ -795,6 +864,7 @@ commands:
- id: get_app_inventory_endpoints
name: sentinelone-get-app-inventory-endpoints
description: "List endpoints that have a given application installed."
risk: read
inputs_schema:
properties:
applicationName: { type: string, description: "Application name filter." }
@@ -811,6 +881,7 @@ commands:
- id: threat_analysis
name: sentinelone-threat-analysis
description: "Return the detailed analysis for a threat (agent, detection and threat info)."
risk: read
inputs_schema:
properties:
threat_id: { type: string, description: "Threat ID." }
@@ -820,6 +891,7 @@ commands:
- id: threat_download_from_cloud
name: sentinelone-threat-download-from-cloud
description: "Get a download URL for a threat's sample from the cloud (BinaryVault)."
risk: read
inputs_schema:
properties:
threat_id: { type: string, description: "Threat ID." }
@@ -830,6 +902,7 @@ commands:
- id: abort_endpoint_scan
name: sentinelone-abort-endpoint-scan
description: "Abort the disk scan on the given agents."
risk: safe_write
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs." }
@@ -839,6 +912,7 @@ commands:
- id: endpoint_fetch_logs
name: sentinelone-endpoint-fetch-logs
description: "Fetch agent/endpoint logs from the given agents."
risk: safe_write
inputs_schema:
properties:
agent_ids: { type: string, description: "Comma-separated agent IDs." }
@@ -851,6 +925,7 @@ commands:
- id: fetch_file
name: sentinelone-fetch-file
description: "Request a file fetch from an agent endpoint (collected as a password-protected zip)."
risk: safe_write
inputs_schema:
properties:
agent_id: { type: string, description: "Agent ID." }
@@ -863,6 +938,7 @@ commands:
- id: get_remote_script_task_status
name: sentinelone-get-remote-script-task-status
description: "Get the status of remote-script tasks for a parent task."
risk: read
inputs_schema:
properties:
parent_task_id: { type: string, description: "Parent task ID." }
@@ -878,6 +954,7 @@ commands:
- id: get_remote_script_task_results
name: sentinelone-get-remote-script-task-results
description: "Get download links for the results of remote-script tasks."
risk: read
inputs_schema:
properties:
task_ids: { type: string, description: "Comma-separated task IDs." }
@@ -889,6 +966,7 @@ commands:
- id: get_service_users
name: sentinelone-get-service-users
description: "List service users (API token accounts)."
risk: read
inputs_schema:
properties:
account_ids: { type: string, description: "Comma-separated account IDs." }
@@ -902,6 +980,7 @@ commands:
- id: list_installed_singularity_marketplace_applications
name: sentinelone-list-installed-singularity-marketplace-applications
description: "List installed Singularity Marketplace applications."
risk: read
inputs_schema:
properties:
account_ids: { type: string, description: "Comma-separated account IDs." }
@@ -915,9 +994,36 @@ commands:
outputs_schema: { properties: {} }
# ── Unified (UAM) alerts ──────────────────────────────────────────────────
# Console-wide alerts, served by the unifiedalerts GraphQL endpoint rather than
# the REST v2.1 tree. A different object from the cloud-detection alert of
# get_alerts: one alert per detection across every surface SentinelOne watches.
- id: get_unified_alerts
name: sentinelone-get-unified-alerts
description: "Retrieve unified (UAM) alerts across endpoint, cloud, identity and third-party surfaces. Walks the cursor until the limit is reached."
risk: read
inputs_schema:
properties:
created_after: { type: string, description: "Return alerts created at or after this time (ISO8601 or epoch). The incremental field: leave empty and let the alert rule set it." }
created_until: { type: string, description: "Return alerts created at or before this time (ISO8601 or epoch)." }
updated_after: { type: string, description: "Return alerts updated at or after this time. Set, it sorts on updatedAt instead of createdAt — use it to pick up alerts retriaged in the console, not to create incidents." }
view_type: { type: string, description: "Surface to read: ALL (default) | CLOUD | CUSTOM_ALERTS | ENDPOINT | IDENTITY | STAR | THIRD_PARTY" }
status: { type: string, description: "Filter on alert status, e.g. NEW | IN_PROGRESS | RESOLVED." }
severity: { type: string, description: "Filter on severity: CRITICAL | HIGH | MEDIUM | LOW | INFO." }
analyst_verdict: { type: string, description: "Filter on analyst verdict, e.g. TRUE_POSITIVE_MALWARE, FALSE_POSITIVE_BENIGN, UNDEFINED." }
alert_id: { type: string, description: "Fetch one alert by its UUID." }
limit: { type: number, description: "Max alerts to return across pages (default 100)." }
cursor: { type: string, description: "Resume from a previous run's pagination.endCursor." }
required: []
outputs_schema: { properties: {} }
ingest:
results_path: data
dedup_key: id
incremental_field: created_after
- id: update_uam_alert_status
name: sentinelone-update-uam-alert-status
description: "Update the status of a unified (UAM) alert."
risk: safe_write
inputs_schema:
properties:
alert_id: { type: string, description: "UAM alert ID (UUID)." }
@@ -928,6 +1034,7 @@ commands:
- id: update_uam_alert_verdict
name: sentinelone-update-uam-alert-verdict
description: "Update the analyst verdict of a unified (UAM) alert."
risk: safe_write
inputs_schema:
properties:
alert_id: { type: string, description: "UAM alert ID (UUID)." }
@@ -939,6 +1046,7 @@ commands:
- id: run_powerquery
name: sentinelone-run-powerquery
description: "Run a PowerQuery against the Singularity Data Lake (separate XDR URL + Log Read API key)."
risk: read
inputs_schema:
properties:
singularity_xdr_url: { type: string, description: "Singularity Data Lake XDR URL (https://...)." }
@@ -956,6 +1064,7 @@ commands:
- id: create_tag_rule
name: sentinelone-create-tag-rule
description: "Create a dynamic asset tag rule (XDR) that tags assets matching a property filter."
risk: safe_write
inputs_schema:
properties:
name: { type: string, description: "Rule name." }
@@ -0,0 +1,105 @@
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" }
@@ -0,0 +1,184 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
# Unified (UAM) alerts live behind a GraphQL endpoint, not the REST v2.1 tree the
# rest of this package uses. The console-wide alert model is a different object
# from /cloud-detection/alerts (see get_alerts): one alert per detection across
# endpoint, cloud, identity and third-party surfaces.
VIEW_TYPES = ("ALL", "CLOUD", "CUSTOM_ALERTS", "ENDPOINT", "IDENTITY", "STAR", "THIRD_PARTY")
# Fields requested per alert node. Kept in one place so the mapper and this
# query cannot drift apart.
NODE_FIELDS = """
id name description severity status result analystVerdict attackSurfaces
classification confidenceLevel externalId createdAt updatedAt firstSeenAt
lastSeenAt detectedAt noteExists dataSources storylineId ticketId
fileName fileHash
analytics { uid name type typeValue category }
assignee { userId fullName email }
detectionSource { product vendor engine }
asset {
id name agentUuid agentVersion assetTypeClassifier category subcategory type
connectivityToConsole osType osVersion pendingReboot lastLoggedInUser policy
}
detectionTime {
asset {
agentVersion consoleIpAddress domain ipV4 ipV6 lastLoggedInUser
osName osRevision osType policy subscriptionTime
}
attacker { host ip }
cloud { accountId cloudProvider image instanceId instanceSize location network tags }
kubernetes {
clusterName namespaceName nodeName podName containerId containerImageName
containerName containerNetworkStatus controllerName controllerType
}
scope { accountId accountName groupName siteName }
targetUser { name emailAddress domain }
}
process { cmdLine parentName file { certSubject md5 name path sha1 sha256 } }
realTime { scope { account { id name } group { id name } site { id name } } }
"""
def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read()
return json.loads(raw) if raw else {}
def epoch_ms(v):
"""Normalise a watermark to epoch milliseconds.
The alert-rule scheduler injects the watermark raw, in whichever format the
operator picked for the rule (iso8601 | unix | unix_ms), so accept all three
rather than forcing one and failing silently on the others.
"""
s = str(v or "").strip()
if not s:
return None
if s.isdigit():
n = int(s)
return n * 1000 if n < 100000000000 else n # seconds vs milliseconds
import datetime
t = s.replace("Z", "+00:00")
try:
dt = datetime.datetime.fromisoformat(t)
except ValueError:
raise ValueError("cannot read timestamp %r: expected ISO8601 or an epoch" % s)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=datetime.timezone.utc)
return int(dt.timestamp() * 1000)
def gql_string(v):
"""Quote a value for inline use in the query (json.dumps escapes to GraphQL's rules)."""
return json.dumps(str(v))
def build_filters(inputs):
out = []
start = epoch_ms(inputs.get("created_after"))
end = epoch_ms(inputs.get("created_until"))
if start is not None or end is not None:
parts = []
if start is not None:
parts.append("start: %d" % start)
if end is not None:
parts.append("end: %d" % end)
out.append('{ fieldId: "createdAt" dateTimeRange: { %s } }' % " ".join(parts))
updated = epoch_ms(inputs.get("updated_after"))
if updated is not None:
out.append('{ fieldId: "updatedAt" dateTimeRange: { start: %d } }' % updated)
for field, key in (("status", "status"), ("severity", "severity"),
("analystVerdict", "analyst_verdict"), ("id", "alert_id")):
v = str(inputs.get(key) or "").strip()
if v:
out.append('{ fieldId: "%s" stringEqual: { value: %s } }' % (field, gql_string(v)))
return out
def main():
secrets = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
base = secrets.get("url", "").rstrip("/") + "/web/api/v2.1"
headers = {
"Authorization": "ApiToken " + secrets.get("api_token", ""),
"Accept": "application/json",
"Content-Type": "application/json",
}
view_type = str(inputs.get("view_type") or "ALL").strip().upper()
if view_type not in VIEW_TYPES:
raise ValueError("view_type must be one of %s" % ", ".join(VIEW_TYPES))
limit = int(inputs.get("limit") or 100)
sort_by = "updatedAt" if str(inputs.get("updated_after") or "").strip() else "createdAt"
filters = build_filters(inputs)
filter_clause = "filters: [ %s ]" % " ".join(filters) if filters else ""
# Ascending, so a truncated run leaves the watermark on the oldest unseen
# alert rather than skipping everything between it and the newest.
nodes, cursor, page_info = [], str(inputs.get("cursor") or "").strip(), {}
total = None
while len(nodes) < limit:
page = min(limit - len(nodes), 100)
query = """query Alerts {
alerts(
first: %d
viewType: %s
after: %s
sort: { by: "%s", order: ASC }
%s
) {
totalCount
edges { node { %s } }
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
}
}""" % (page, view_type, gql_string(cursor) if cursor else "null", sort_by, filter_clause, NODE_FIELDS)
resp = request("POST", base + "/unifiedalerts/graphql", headers, {"query": query})
# GraphQL answers 200 with an errors array; without this an unusable
# query would look like an endless run of empty, successful fetches.
if resp.get("errors"):
print(json.dumps({"error": "GraphQL error", "detail": resp["errors"]}))
sys.exit(1)
alerts = (resp.get("data") or {}).get("alerts") or {}
if total is None:
total = alerts.get("totalCount")
edges = alerts.get("edges") or []
for edge in edges:
node = edge.get("node")
if node:
nodes.append(node)
page_info = alerts.get("pageInfo") or {}
cursor = page_info.get("endCursor")
# An empty page ends the walk even when the console still claims a next
# one: without this, a console answering hasNextPage on no edges would
# keep the fetch spinning until the run is killed.
if not edges or not page_info.get("hasNextPage") or not cursor:
break
# Flattened to {data: [...]} so the alert rule reads a plain array of alerts
# (results_path: data) and dedups on a top-level id, matching the REST
# commands in this package. The edges/node envelope carries no id of its own.
print(json.dumps({
"data": nodes,
"pagination": {
"totalCount": total,
"returned": len(nodes),
"hasNextPage": bool(page_info.get("hasNextPage")),
"endCursor": page_info.get("endCursor"),
},
}))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)