Install
$ agentstack add skill-pmoses-s1-claude-skills-sentinelone-mgmt-console-api ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
SentinelOne Management Console API
Wraps the SentinelOne Management Console API (Swagger 2.0, spec version 2.1, 781 operations) with a pre-built Python client, a compact endpoint index, and per-tag reference files.
> Sandbox proxy blocked? If calls to *.sentinelone.net fail with a connection or proxy error inside the Claude sandbox, use the sentinelone-mcp server instead. It runs locally on your machine via node and bypasses the sandbox proxy entirely. Setup: add it to claude_desktop_config.json (see sentinelone-mcp/README.md). The MCP server exposes all the tools in this skill — s1_api_get, s1_api_post, purple_ai_alert_summary, uam_list_alerts, uam_get_alert, uam_add_note, uam_set_status — with schemas validated against the live API. For natural-language Purple AI queries and AI investigations, use the Purple MCP (mcp__purple-mcp__purple_ai) directly — those operations require a browser-session teamToken that API tokens never obtain.
Setup — configure credentials first
Drop a credentials.json file directly into your Cowork project folder with the required fields:
{
"S1_CONSOLE_URL": "https://usea1-acme.sentinelone.net",
"S1_CONSOLE_API_TOKEN": "eyJ...your-api-token...",
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net"
}
S1_CONSOLE_URL is the tenant console URL (no trailing slash, no /web/api/v2.1). S1_CONSOLE_API_TOKEN is an API User token from Settings → Users → Service Users in the S1 console. S1_HEC_INGEST_URL is the SentinelOne HEC ingest host (region-specific, look up yours in SentinelOne Endpoint URLs by Region) and is only required if you push alerts/indicators into UAM via the UAM Alert Interface.
The plugin's SessionStart hook auto-discovers the file at the start of every session, so all S1 scripts and CLIs find it without any preflight. To trigger a manual refresh:
bash scripts/bootstrap_creds.sh # idempotent, returns the destination path
Environment variables (S1_CONSOLE_URL, S1_CONSOLE_API_TOKEN, S1_HEC_INGEST_URL, S1_VERIFY_TLS) still override the credentials file if set.
Before running anything, confirm credentials resolved. If not, stop and ask the user to drop credentials.json into their Cowork project folder.
Workflow
When the user asks for something involving the S1 API, follow this pattern:
- Find the right endpoint.
- If the user's ask is verb-shaped ("list / count / isolate / hunt …") and you need orientation, read
references/CAPABILITY_MAP.mdfirst — it's a compact per-tag summary of what verbs each resource supports. - For a specific multi-step task ("threat triage", "endpoint isolation", "DV hunt"),
references/WORKFLOWS.mdhas ready-to-adapt recipes. - Otherwise go straight to
scripts/search_endpoints.pywith a keyword matching the user's intent. It now ranks results by relevance (path segment hits + verb intent + tag) and supports synonyms ("isolate" → "disconnect", "endpoint" → "agent"). Add--only-worksto restrict to endpoints confirmed reachable on this tenant by the most recent smoke test.
- Read the per-tag reference. Open
references/tags/.md(names match the table inreferences/TAG_INDEX.md) to see full parameter lists, descriptions, required permissions, and response codes for that group. Only read the tag file(s) relevant to the task — don't read them all. - Call the endpoint. Either use
scripts/call_endpoint.pyfor one-off calls, or importS1Clientfromscripts/s1_client.pyin a Python script for anything that needs loops, joins, or transforms. For independent GETs, preferc.get_many([(path, params), ...])— it fans out in parallel over the client's pooled connection and is ~3× faster than a sequential loop. - Paginate correctly. S1 list endpoints use cursor-based pagination. The client's
paginate()anditer_items()handle this automatically — prefer them over manualskip/limitmath, which caps at 1000 items. - Summarize the result for the user. Don't dump raw JSON unless asked. Prefer a short prose summary plus a table or CSV/XLSX if the volume warrants.
GET vs POST: the rule you must follow
The S1 API uses GET for every read operation — listing, counting, and exporting. POST is only for actions and mutations. Violating this produces HTTP 404, not a permissions error, because the path simply does not exist.
Read (always GET): | Intent | Correct endpoint | |---|---| | List agents | GET /web/api/v2.1/agents | | Count agents | GET /web/api/v2.1/agents/count → {"data":{"total":N}} | | List threats | GET /web/api/v2.1/threats | | Count threats | GET /web/api/v2.1/threats?countOnly=true or check pagination.totalItems from any list call | | Export threats to CSV | GET /web/api/v2.1/threats/export (no extra params required) | | List sites / accounts / groups | GET /web/api/v2.1/{sites,accounts,groups} | | Get system info | GET /web/api/v2.1/system/info |
Write / action (POST/PUT/DELETE):
- Agent actions (isolate, reconnect, uninstall, scan):
POST /web/api/v2.1/agents/actions/ - Threat actions (mitigate, verdict, fetch-file):
POST /web/api/v2.1/threats/ - Mutations (notes, exclusions, IOCs, custom rules): POST/PUT/DELETE on the relevant resource path
Paths that do not exist — never call these:
These paths return HTTP 404 and have never existed in the v2.1 spec. They are plausible-sounding guesses that are consistently wrong:
| Wrong path | Why you might guess it | Correct alternative | |---|---|---| | POST /web/api/v2.1/agents/ids | "query agents by IDs" | GET /web/api/v2.1/agents?ids=, | | POST /web/api/v2.1/threats/summary | "get a threat summary/count" | GET /web/api/v2.1/threats?countOnly=true | | POST /web/api/v2.1/export/threats | "export threats" | GET /web/api/v2.1/threats/export | | POST /web/api/v2.1/threats/count | "count threats via POST" | GET /web/api/v2.1/threats?countOnly=true | | POST /web/api/v2.1/agents/summary | "agent summary" | GET /web/api/v2.1/agents/count + GET /web/api/v2.1/agents |
If you are about to call s1_api_post for a read/count/export operation, stop and switch to s1_api_get. Before any s1_api_post call, confirm the path exists: python3 scripts/search_endpoints.py "" — if the path does not appear, it does not exist.
Schema gotchas — confirmed on live tenant
These are fields where the obvious guess is wrong. All verified against the live tenant.
Custom Detection Rules and STAR Rules — POST /cloud-detection/rules
STAR rules ("streaming threat assessment rules") are the product name for real-time detection rules that evaluate every matching event as it arrives. There is no separate /star-rules API path — that path returns 404. STAR rules are cloud-detection/rules with queryType=events, the same endpoint as all other Custom Detection Rules.
queryType enum (from swagger): events, scheduled, correlation, uebafirstseen.
⚠️ LISTING DETECTION RULES — read this BEFORE calling GET /cloud-detection/rules
The list endpoint hides queryType: "scheduled" rules by default. Without isLegacy=false in the query string, scheduled rules return 0 results even though they exist and are visible in the console UI. This is the single most common drift between API output and console reality. There is no error, no warning, no hint — the response simply omits scheduled rules. Re-confirmed against the live API, 2026-05.
Always pass isLegacy=false when listing. The only time you can omit it is if you are 100% sure you only care about events-type STAR rules and want to filter scheduled rules out.
| Goal | Correct query | Anti-pattern (silently wrong) | |---|---|---| | List all detection rules | GET /cloud-detection/rules?isLegacy=false&limit=200 | GET /cloud-detection/rules?limit=200 — drops scheduled | | List only scheduled detections | GET /cloud-detection/rules?isLegacy=false&queryType=scheduled&limit=200 | GET /cloud-detection/rules?queryType=scheduled — returns 0 | | List only events (STAR) rules | GET /cloud-detection/rules?queryType=events&limit=200 | (this one is safe; events rules are not gated by isLegacy) | | Search by name across all types | GET /cloud-detection/rules?isLegacy=false&name__contains=Foo | GET /cloud-detection/rules?name__contains=Foo — drops scheduled |
Verdict-language note for analysts: if you query without isLegacy=false and the response is empty, the correct statement is "no events rules came back; I haven't yet checked scheduled rules" — not "this tenant has zero scheduled detections". Promoting the absence of evidence to "confirmed zero" is the failure mode this gotcha exists to prevent.
queryType accepts a single value or an array (comma-separated): queryType=scheduled or queryType=events,scheduled both work. The combination queryType= + nameSubstring= returns HTTP 500; use one filter at a time, or use name__contains instead of nameSubstring (which works with both).
⚠️ Pick the right queryType for your rule body BEFORE composing the POST. Mismatches return HTTP 400.
| Rule body language | Correct queryType | Correct queryLang | Field carrying the query | |---|---|---|---| | PowerQuery (pipe syntax \|) | scheduled | "2.0" | data.scheduledParams.query | | S1QL log-search / event search | events | "1.0" (default, omit) | data.s1ql | | EUEBA first-seen | uebafirstseen | n/a | per swagger schema | | Correlation | correlation | n/a | per swagger schema |
Any time the rule body has the pipe character \| (i.e. PowerQuery), the only working path is queryType: "scheduled" + queryLang: "2.0". queryType: "events" rejects pipe syntax (HTTP 400 Don't understand [|]), and queryLang: "2.1" is not in the enum (HTTP 400 queryLang: "2.1" is not a valid choice). Confirmed against the live API, 2026-05.
See the PowerQuery Scheduled Detections subsection below for the full scheduled-rule body, gotchas, and the feature-flag fallback path if the tenant has not enabled Scheduled Detections.
expirationMode valid values: "Permanent" or "Temporary". "Never" is not valid and returns 400.
Events (STAR) rule — confirmed CREATE body (live API, 2026-05):
{
"data": {
"name": "my-star-rule",
"description": "optional",
"severity": "Low",
"expirationMode": "Permanent",
"queryType": "events",
"status": "Draft",
"s1ql": "EventType = \"Process Creation\" AND ProcessName = \"suspicious.exe\"",
"treatAsThreat": "UNDEFINED",
"networkQuarantine": false
},
"filter": {"siteIds": [""]}
}
Events rule gotchas confirmed against live API:
"activeResponse"in the body returns HTTP 400 "Unknown field" — omit it.queryLangdefaults to"1.0"for events rules; do not set it explicitly (unlike scheduled rules which require"2.0").treatAsThreat="UNDEFINED"is accepted on input but stored asnullin the response.status="Draft"creates a rule that never fires even if live telemetry matches.- GET
nameSubstring+queryTypecombined returns HTTP 500 — use one filter at a time. - DELETE body is top-level
{filter: {ids: [...], siteIds: [...]}}with no"data"wrapper. isLegacy=falseis NOT needed for events rules (only required for scheduled rules).
status valid values: "Draft", "Activating", "Active", "Disabling", "Disabled". Use "Disabled" to create a non-firing rule; the API returns "Draft" in the response for a rule that has never been activated.
PUT /cloud-detection/rules/{id} — status is required in the PUT body even though it is read-only in practice. Omitting it returns 400 "Missing data for required field."
Saved Filters — POST /filters
The body field for filter criteria is filterFields, not filters:
{
"data": {
"name": "my-filter",
"filterFields": {"infected": true, "networkStatuses": ["connected"]}
},
"filter": {"accountIds": [""]}
}
PUT /filters/{id} — does NOT accept a top-level filter scope wrapper. Pass only {"data": {...}}:
{"data": {"name": "updated-name", "filterFields": {"infected": false}}}
PowerQuery Scheduled Detections — POST/PUT/GET/DELETE /cloud-detection/rules
queryType: "scheduled" rules are PowerQuery-based detections. This is the only path that accepts pipe-syntax PowerQuery in a detection rule body. Events rules reject pipe syntax even with queryLang: "2.0". They use a different schema from events and correlation rules and have several non-obvious requirements confirmed against the live API (2026-05).
If the POST fails with a feature-not-enabled / not-licensed / unauthorized response on a tenant where the schema is otherwise correct, stop and tell the user to enable the Scheduled Detections feature on the tenant before retrying. Do not silently downgrade the rule body to S1QL or re-attempt with queryType: "events". The console path is typically Settings → Account → Detection / SDL Add-Ons → Scheduled Detections but varies by platform version, so phrase the ask in terms of capability ("please enable Scheduled Detections on this account") rather than the exact click path.
CREATE — POST /web/api/v2.1/cloud-detection/rules
All five data fields are required. queryLang: "2.0" is mandatory; omitting it returns HTTP 400 "query lang must be 2.0". filter accepts accountIds or siteIds (account-level rules cover all sites under that account):
{
"data": {
"name": "My Scheduled Detection",
"queryType": "scheduled",
"queryLang": "2.0",
"severity": "Medium",
"expirationMode": "Permanent",
"status": "Disabled",
"scheduledParams": {
"query": "dataSource.name='Proofpoint' event.type='Click' unmapped.classification='malware' | group hits=count(), first_seen=oldest(timestamp), last_seen=newest(timestamp) by clickIP | filter hits >= 1 | sort -hits | limit 100",
"runIntervalMinutes": 60,
"lookbackWindowMinutes": 60,
"threshold": {"value": 0, "operator": "Greater"}
}
},
"filter": {"accountIds": [""]}
}
Scheduled-rule gotchas confirmed against live API (2026-05):
disableAgentMitigationis not part of the scheduled schema. Including it returns HTTP 400Unknown field. Cloud-source PQ rules do not need it; mitigation actions are not supported on scheduled rules anyway.treatAsThreat: "Malicious"is for events rules with EDR telemetry. Scheduled rules accepttreatAsThreat: "UNDEFINED"(or omit) andnetworkQuarantine: false. The verdict surfaces via the rule'sseverity, not via mitigation.- New rules are created in
Draftstatus regardless of the requestedstatusin the POST. To enable, callPUT /web/api/v2.1/cloud-detection/rules/enablewith{"filter": {"ids": [...], "accountIds": [...]}}after creation. The response transitions toActivatingand thenActivewithin the hour. - The PowerQuery in
scheduledParams.querymust NOT usenolimit,compare, or subqueries. The 1,000-row intermediate cap fromreferences/detection-rules.mdapplies.
GET requires isLegacy=false — without this query parameter, the list endpoint returns 0 results for scheduled rules even though they exist and are visible in the console UI. Always include it. When listing all rules regardless of type (e.g. searching by name), always pass isLegacy=false — without it you will only see events-type rules and silently miss all scheduled rules:
GET /web/api/v2.1/cloud-detection/rules?isLegacy=false&limit=100
GET /web/api/v2.1/cloud-detection/rules?siteIds=&queryType=scheduled&isLegacy=false
GET /web/api/v2.1/cloud-detection/rules?ids=&siteIds=&isLegacy=false
Note: name__contains filter silently returns 0 for scheduled rules when isLegacy=false is omitted. Always pair name searches with `isLe
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: pmoses-s1
- Source: pmoses-s1/claude-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.