Install
$ agentstack add skill-pmoses-s1-claude-skills-sentinelone-sdl-api Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ● Shell / process execution Used
- ✓ 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 SDL API
Wraps the Singularity Data Lake API (query and configuration-file methods) with a pre-built Python client, a CLI runner, and a per-method reference.
The SDL API is distinct from the Management Console API. It speaks JSON over Bearer tokens (not ApiToken) and is the canonical path for querying the data lake and editing parsers/dashboards/alerts/lookups directly. Raw-log ingestion is via HEC (see the sentinelone-mgmt-console-api skill).
> Sandbox proxy blocked? If calls to *.sentinelone.net (SDL host or console host) fail with a connection or proxy error inside the Claude sandbox, use the sentinelone-mcp server instead. It runs locally 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 sdl_list_files, sdl_get_file, sdl_put_file, and sdl_delete_file — running directly from your machine against the SDL API. Raw-log ingestion uses HEC (see sentinelone-mgmt-console-api), not this skill.
IMPORTANT: query methods are deprecated — and LRQ is NOT available here
The query methods on this skill (query, powerQuery, facetQuery, timeseriesQuery, numericQuery) wrap the V1 SDL endpoints (/api/query, /api/powerQuery, etc.) at the centralized host xdr.us1.sentinelone.net. Those endpoints are deprecated and sunset on 2027-02-15 (also applies to the Deep Visibility /web/api/v2.1/dv/events/pq endpoint).
The LRQ API is NOT a replacement available through this skill. LRQ runs at POST /sdl/v2/api/queries on the tenant's own Management Console host (e.g. your-tenant.sentinelone.net) — it is part of the Mgmt Console API surface, not the SDL API (xdr.us1.sentinelone.net). To run PowerQueries programmatically, use the sentinelone-mgmt-console-api skill which holds the LRQ runner, auth pattern, and slicing strategy.
SDL dashboard panels do not use LRQ either. Dashboard panel queries are executed by the SDL console's own built-in rendering engine when a user loads the dashboard in their browser. The panel JSON just stores the query string — no API call is needed. Do not attempt to test or run dashboard panel queries via LRQ.
| Task | Correct skill / path | |------|------| | PowerQuery programmatically (any range) | sentinelone-mgmt-console-api → LRQ at POST /sdl/v2/api/queries on console host | | Dashboard panel queries | SDL console renders them in-browser — no API needed | | Quick one-off stats under 24h (deprecated) | V1 methods on this skill still work until 2027-02-15 | | get_file / put_file / list_files (parsers, dashboards, lookups) | This skill |
Setup — configure credentials first
Drop a credentials.json file directly into your Cowork project folder with the keys you need:
{
"SDL_XDR_URL": "https://xdr.us1.sentinelone.net",
"S1_CONSOLE_API_TOKEN": "eyJ...your-token...",
"SDL_CONFIG_WRITE_KEY": "0mXas6PD1Zvg..."
}
The plugin's SessionStart hook auto-discovers the file at the start of every session, so the SDL client picks it up with no preflight. To trigger a manual refresh:
bash scripts/bootstrap_creds.sh # idempotent, returns the destination path
Each key type unlocks a specific set of methods (matrix below). The client picks the right key per method automatically; callers never hand-pick a token.
| Key | Methods unlocked | |-----|-----------------| | Log Read Access | query, numericQuery, facetQuery, timeseriesQuery, powerQuery | | Configuration Read | All Log Read methods, plus getFile, listFiles | | Configuration Write | All of the above, plus putFile | | S1_CONSOLE_API_TOKEN (mgmt-console JWT) | All query + config methods; set SDL_S1_SCOPE if multi-site/account. Same JWT used by S1Client. (Legacy alias SDL_CONSOLE_API_TOKEN still recognised.) |
Environment variables (SDL_XDR_URL, S1_CONSOLE_API_TOKEN, etc.) still override the credentials file if set.
Before running anything, confirm SDL_XDR_URL is set and at least one key for the operation chain is present. 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 SDL API:
- Pick the method. Check
references/methods.mdfor the right call. For configuration files (get_file,put_file,list_files), this skill is the right tool. Raw-log ingestion is via HEC (seesentinelone-mgmt-console-api). For queries, use the V1 methods on this skill only for quick one-off stats under 24h; for anything programmatic or multi-day, switch to thesentinelone-mgmt-console-apiskill and the LRQ API — LRQ is NOT available at the SDL API host (xdr.us1.sentinelone.net). - Use the client.
from sdl_client import SDLClientthen call the named method (query,power_query,facet_query,timeseries_query,numeric_query,list_files,get_file,put_file). The client picks the correct key, handles JSON encoding, retries 429/5xx/error/server/backoff, and returns parsed JSON. Note:queryandpower_queryhit the deprecated V1 endpoints — they work until 2027-02-15 for quick lookups but should not be used for production query pipelines. - For ad-hoc shots, use the CLI.
python scripts/sdl_cli.py [args]. The CLI mirrors the client. - Summarize for the user. Don't dump raw JSON unless asked. For query results, prefer a concise table or CSV; for ingestion, confirm
bytesChargedand the session ID; for config files, show path + version + (truncated) content.
Schema discovery — the right way
Every SDL session must run live schema discovery for every data source it will query — including the S1 internal sources alert, vulnerability, misconfiguration, asset, finding, ActivityFeed, Identity, indicator and every third-party source. Documented schemas drift between sessions due to parser edits, reserved-field rewrites, and ingestion changes.
asset and ActivityFeed — confirmed live schemas (126 and 41 fields respectively):
dataSource.name='asset'— 126 fields of rich device inventory (OCSF classuid 3004, categoryname = 'Discovery'). Key fields:device.agent.uuid,device.name,device.os.{name,version,type},device.agent.{network_status,network_status_title,network_quarantine_enabled,is_active,is_decommissioned,is_uninstalled,scan_status,version,last_logged_in_user_name},device.ip_external,device.hw_info.*,device.network_interfaces[N].*,severity_id,severity_,operation(= OPERATION_UPSERT),s1_metadata.{site_id,site_name,group_id,group_name}. Use this for endpoint inventory panels and asset state tracking. Fields that do not exist:entity.uid,entity_result.*,agent.health.online,agent.uuid(usedevice.agent.uuid).dataSource.name='ActivityFeed'— 41 fields of Hyperautomation/management activity audit log (sca:RetentionType = 'ACTIVITY_LOG'). Key fields:activity_type(numeric, NOT a string — e.g. 9207 = workflow execution event),activity_uuid,primary_description,secondary_description,data.workflow_{id,name,execution_url},data.{scope_id,scope_level,scope_name,site_name,user_id},created_at,updated_at,account.{id,name},site_id,context. Useful for Hyperautomation workflow audit and compliance tracking. Not useful for threat hunting.
The actual ingestion pipeline metrics source is finding (dataSource.category='metrics', tag='ingestionHealth', fields: batchCt, eventLatency.*, processor, etc.) — do not confuse it with asset or ActivityFeed.
Why PowerQuery is the wrong tool for this: PowerQuery's default projection returns timestamp + message only. Naive dataSource.name='alert' | limit 1 hides the actual fields. | columns * returns HTTP 500. You can probe specific fields with | columns f1, f2 but you have to already know what to ask for — which defeats the purpose of discovery.
Use the V1 query method instead. It returns each match as the full event JSON with every populated attribute keyed in an attributes dict. That's the only built-in way to see what fields a source actually carries.
Auth caveat: the V1 query method requires Log Read permission. The default credential chain is log_read_key → config_read_key → config_write_key → console_api_token. If your credentials.json has SDL_CONFIG_WRITE_KEY set but no SDL_LOG_READ_KEY, the chain picks the config write key first — which does NOT grant View Logs and returns HTTP 403: authorization token does not grant View logs permission.
Force-clear the scoped keys so the chain falls through to the console JWT:
from sdl_client import SDLClient
c = SDLClient()
c.keys["log_read_key"] = ""
c.keys["config_read_key"] = ""
c.keys["config_write_key"] = ""
schemas = {}
for source in all_sources_from_step1_enumeration:
res = c.query(filter=f"dataSource.name=='{source}'", max_count=2, start_time="24h")
matches = res.get("matches") or []
if not matches:
continue
attrs = matches[0].get("attributes") or {}
schemas[source] = sorted(attrs.keys())
import json, datetime
out = f"outputs/sdl_schemas_{datetime.date.today().isoformat()}.json"
json.dump(schemas, open(out, "w"), indent=2)
Direct MCP tools bypass sandbox proxy entirely.
The Cowork sandboxed shell blocks all outbound HTTPS to xdr.us1.sentinelone.net. Use the sentinelone-mcp MCP tools instead, which run locally and bypass the proxy:
| Operation | sentinelone-mcp tool | |---|---| | PowerQuery | mcp__sentinelone-mcp__powerquery_run or mcp__sentinelone-mcp__powerquery_schema_discover | | put_file / get_file / list_files | mcp__sentinelone-mcp__sdl_put_file, mcp__sentinelone-mcp__sdl_get_file, mcp__sentinelone-mcp__sdl_list_files |
All of these tools run on your local machine and make direct HTTPS calls to xdr.us1.sentinelone.net without sandbox proxy interference. No fallback or workaround needed.
# Example: use sdl_get_file MCP tool directly instead
import sys, subprocess, json
result = subprocess.run(["mdfind", "-name", "sdl_client.py"], capture_output=True, text=True)
sdk_dir = [p for p in result.stdout.strip().split("\n") if "claude-skills" in p][0].rsplit("/", 1)[0]
sys.path.insert(0, sdk_dir)
from sdl_client import SDLClient
c = SDLClient()
c.keys["log_read_key"] = c.keys["config_read_key"] = c.keys["config_write_key"] = ""
# SDLClient.query(...) / put_file(...) / get_file(...) here
A proxy error treated as an empty result produces a fabricated schema, causing every downstream panel to silently query non-existent fields.
The attributes dict exposes nested arrays as flattened keys like resources[0].name and vulnerabilities[0].cve.uid. Those flattened keys are display-only — they are NOT valid PowerQuery columns paths. PowerQuery returns HTTP 500 on bracket-array indexing. For analytics over array fields, either stay on V1 query or use array_get(arr, 0) inside a PowerQuery let.
Trailing-underscore reserved-field rule: Field names ending in _ (severity_, status_, classification_) are SDL's auto-rename when source data carries a field colliding with an SDL reserved name. The underscored form IS the canonical, queryable field. Numeric OCSF variants (severity_id 0-5, status_id, class_uid) live alongside the underscored string fields. Prefer numeric OCSF for filters; the string severity_ is case-mixed (Critical and CRITICAL co-exist) and will produce split columns in transpose.
Files in this skill
/credentials.json— credentials (setSDL_XDR_URLand the keys you need; see Setup above). Auto-discovered by the plugin's SessionStart hook.scripts/bootstrap_creds.sh— idempotent helper that copies workspace creds into the sandbox-local path. Wired to the plugin's SessionStart hook; safe to re-run manually.scripts/sdl_client.py— importable Python client (SDLClient). Picks the right key per method, retries with exponential backoff, exposes ergonomic method names.scripts/sdl_cli.py— CLI runner:python scripts/sdl_cli.py power-query "dataset='accesslog' | group count() by status" --start 1h.references/methods.md— single per-method reference (parameters, defaults, response shape, gotchas) for the SDL query and configuration-file endpoints.references/auth_and_limits.md— key matrix, console-token rules, S1-Scope, leaky-bucket CPU rate-limit model, retry guidance, daily caps.
Using the client
import sys
sys.path.insert(0, "scripts")
from sdl_client import SDLClient
c = SDLClient()
# ---- Log read ----
# PowerQuery — best general-purpose tool
res = c.power_query(
query="dataset='accesslog' status >= 400 | group count() by status",
start_time="1h",
)
# res = {"status": "success", "matchingEvents": ..., "columns": [...], "values": [[...], ...]}
# Raw event search
matches = list(c.iter_query(filter="error", start_time="15m", max_total=500))
# Top-N values
top_ips = c.facet_query(field="srcIp", filter="status >= 400", start_time="24h", max_count=20)
# Numeric / timeseries (1 query)
ts = c.timeseries_query(queries=[
{"filter": "serverHost contains 'frontend'", "function": "count", "startTime": "1h", "buckets": 60}
])
# ---- Configuration files ----
# Parsers live under /logParsers/ — the SDL API also accepts /parsers/
# but the Log Parsers UI only reads /logParsers/, so PUTs at /parsers/ are invisible
# in the console. Use /logParsers/ by default.
files = c.list_files() # {"status":"success","paths":["/foo", ...]}
parser = c.get_file("/logParsers/MyParser") # {"status":"success","content":"...","version":7,...}
c.put_file("/logParsers/MyParser", content="// new parser body")
c.put_file("/logParsers/Stale", delete=True)
Authentication
Every request sets Authorization: Bearer . The client picks the key per method using these chains (first non-empty wins):
log_read:log_read_key→config_read_key→config_write_key→console_api_tokenconfig_read:config_read_key→config_write_key→console_api_tokenconfig_write(putFile):config_write_key→console_api_token
If a console_api_token is used and the user has access to multiple sites or accounts, set s1_scope (e.g. ":" for site scope, "" for account scope). The client adds S1-Scope automatically when both conditions hold.
A 401 with error/client/noPermission means the token is wrong or expired. SDL keys do not expire by default, but console user tokens do.
Rate limits and retries
The client retries automatically on HTTP 429, 5xx, and SDL status: error/server/backoff (which can come back inside a 200), honouring Retry-After. Things to know up-front:
- Query budget is a leaky bucket of CPU seconds. When
cpuUsageSecondsToWaitshows in a 429, back off by that many seconds.priority: "low"(the default) gets a more generous bucket than"high". Seereferences/auth_and_limits.mdfor the bucket model. - From 19 March 2026, all query methods cap at 8 queries/sec per tenant.
- Concurrency cap: 12 simultaneous requests per API key (non-query). For loops, throttle in code.
For long-running ingest, use the binary truncated exponential backoff loop in references/integration_patterns.md rather than the client's default retries — it is designed to stop on discardBuffer and to slowly relax wait times after success.
Destructive actions — confirm first
put_file(delete=True) and put_file(content=...) overwriting an existing path can wipe a parser, dashboard, alert, or lookup table. Before any putFile write or delete:
- Run
get_filefirst to read currentversionand content. Pass that version asexpected_versionon the write to fail-fast on a concurrent edit (error/client/versionMismatch). - For deletes, summarise the pat
…
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.