AgentStack
SKILL verified MIT Self-run

Sentinelone Sdl Dashboard

skill-pmoses-s1-claude-skills-sentinelone-sdl-dashboard · by pmoses-s1

>

No reviews yet
0 installs
9 views
0.0% view→install

Install

$ agentstack add skill-pmoses-s1-claude-skills-sentinelone-sdl-dashboard

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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 No
  • Filesystem access Used
  • 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.

Are you the author of Sentinelone Sdl Dashboard? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

SentinelOne SDL Dashboard Skill

This skill helps you design, author, and deploy Singularity Data Lake (SDL) dashboards — from a single panel to a full multi-tab SOC dashboard. Dashboards live as configuration files in SDL and are authored as JSON (or a relaxed JavaScript-literal superset of it). You deploy them via the sentinelone-sdl-api skill's put_file method.

> Sandbox proxy blocked? If put_file or SDL API calls to *.sentinelone.net 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). Use the sdl_put_file tool to deploy dashboards and sdl_get_file / sdl_list_files to inspect what's already deployed.

Workflow

This workflow is mandatory for every new or modified dashboard. Steps 0, 1, and 7 are non-negotiable: pre-flight discovery, the safety pre-flight check, and the post-deploy log-evidence report. Skipping any of them produces dashboards that look fine in isolation but mislead, hang, or silently drop data.

  1. Discovery (MANDATORY for every session) — Re-enumerate connected data sources (| group UniqueDataSourceNames = array_agg_distinct(dataSource.name) | limit 1000), run V1-query schema discovery on every source the dashboard will touch, and validate the discriminator field for any event.type you intend to count. See Pre-authoring discovery below. Never start a panel from a remembered schema.

Execution path for schema discovery via sentinelone-mcp:

  1. PowerQuery enumeration (array_agg_distinct(dataSource.name)) — run via mcp__sentinelone-mcp__powerquery_run directly. The sentinelone-mcp server runs locally and makes direct HTTPS calls without sandbox interference.
  2. V1 query schema discovery (full event JSON per source) — use mcp__sentinelone-mcp__powerquery_schema_discover to fetch sample events from each data source and inspect their field names and types. The MCP server runs on your local machine and bypasses the sandbox proxy entirely.
  3. Understand the ask — What data should the dashboard show? Who is the audience (SOC analyst, manager, customer POC)? What time range makes sense? What posture should each panel reflect (a panel that legitimately returns 0 needs a markdown header explaining the SOC-positive interpretation, see Empty results are valid evidence).
  4. Design the structure — Choose tabs (if multi-topic), then panels per tab. Match panel type to the data shape using the guide in references/panel-type-cheatsheet.md. Key decisions: flows/kill-chains → sankey; KPI vs SLA target → bullet; SOC queue health → gauge; 3D outlier detection → scattered_bubble; time-based density → heatmap; multiple queries in one panel → tabbed table. Where one event.type covers multiple semantic populations (delivery-time vs click-time, scheduled vs on-demand, inbound vs outbound), build separate sections per population, not a mixed section.
  5. Write the JSON — Use the panel type reference below and real examples in references/community-examples.md. Compute explicit x/y/w/h for every panel. Apply the naming-hygiene rule from Panel naming hygiene so titles read as SLA-grade claims.
  6. Validate queries — Sample 3-5 events per source/event-ID to confirm field semantics. Test each panel query via the sentinelone-powerquery skill. Run the parallel load test (see Pre-deploy validation), acceptance thresholds: slowest panel ≤ 2s, wall-clock ≤ 5s. Run scripts/panel_safety_check.py against the dashboard JSON; resolve every flag before deploy.
  7. Deploy — Use the sentinelone-sdl-api skill to put_file to a path like /dashboards/my-dashboard with expected_version set from a prior get_file (CAS guard). Save a backup of the prior JSON first. Sleep 3s, then get_file to verify the version bumped AND grep the returned content for a canary string from your change.
  8. Iterate — Show the user what was built, explain each panel, offer to tweak. If the dashboard hangs, follow the escalation ladder in Pre-deploy validation.
  9. Log-evidence report (MANDATORY) — Run scripts/validate_dashboard.py against the deployed dashboard JSON to replay every panel, persist per-panel evidence (sample rows, row count, matchCount, elapsed, errors) to a JSON, and emit a markdown evidence file. Then run scripts/render_validation_pdf.py to render the PDF report (cover, per-tab sections, sample-data tables, empty-result appendix with SOC-meaningful interpretations). Deliver both alongside the dashboard. A dashboard delivered without an evidence report is incomplete.

validate_dashboard.py MUST be run as a background process — at ~10s per panel, a 30-panel dashboard takes 5 minutes; a 60-panel dashboard takes 10-30 minutes. Both exceed the MCP timeout. Start it with python3 scripts/validate_dashboard.py ... > /tmp/validate_out.txt 2>&1 &, confirm the PID, then poll len(json.load(open(evidence_json))) vs the expected panel count in short separate calls. The script persists results after every panel (idempotent), so a cancelled poll never loses work. When a stacked_bar or line panel using | transpose returns 0 rows in validation, cross-check whether the corresponding number panel for the same source shows data — if it does, the empty result is a V1-API artefact, not a broken query. Document it in the Appendix as confirmed false-empty and do not remove the panel.

  1. Screenshot review with the user (MANDATORY). API validation proves each panel's query returns rows; it does NOT prove the panel RENDERS. Render-only failures happen in the browser, not the API, so validate_dashboard.py cannot see them: a panel showing "Couldn't load content", a markdown tile showing "Untitled", a number reading "34 principals" under a title that already says principals, an empty chart, or a broken legend. After EVERY deploy, ALWAYS ask the user to open the dashboard and send screenshots of each tab, then read them, diagnose each visual defect, fix the JSON, and re-deploy, without waiting to be asked. Prompt explicitly, e.g.: "The dashboard is deployed at /dashboards/. Please open it and send screenshots of each tab so I can catch any render-only issues and fix them automatically." Treat this as part of deployment, not optional polish. Fixes for the common render-only defects are in the Quick triage table.

Pre-authoring discovery

Different tenants connect different data sources, and even the same tenant drifts between sessions as parsers are updated. Authoring a panel from a remembered schema is the single most common cause of empty-or-misleading dashboards.

1. Enumerate connected data sources every session

| group UniqueDataSourceNames = array_agg_distinct(dataSource.name)
| limit 1000

If the source the dashboard is meant to cover does not appear, the dashboard cannot work. Stop and surface this to the user. Do not silently switch to a different source.

2. PowerQuery cannot discover a source's schema by itself

| limit N against a parser-emitted source returns only timestamp + message. PowerQuery has no | columns * or wildcard projection. Use the V1 query endpoint (/api/query, returns full event JSON) via the SDL client. Force-clear the scoped keys so auth falls through to the console JWT (which has query permission):

from sdl_client import SDLClient
c = SDLClient()
c.keys["log_read_key"]    = ""
c.keys["config_read_key"] = ""
c.keys["config_write_key"] = ""

res = c.query(filter=f"dataSource.name=='{source}'", max_count=50, start_time="7d")
attrs = sorted({k for m in res["matches"] for k in (m.get("attributes") or {}).keys()})

Persist attrs to a per-session JSON and reference it during panel authoring. Do this for every source the dashboard will query.

SDL operations via sentinelone-mcp tools.

All SDL operations should use the sentinelone-mcp MCP tools, which run locally and bypass the sandbox proxy:

| Operation | sentinelone-mcp tool | |---|---| | PowerQuery (enumeration, hunts, panel queries) | mcp__sentinelone-mcp__powerquery_run | | V1 query (full event JSON for schema discovery) | mcp__sentinelone-mcp__powerquery_schema_discover | | put_file / get_file / list_files (dashboard deploy) | mcp__sentinelone-mcp__sdl_put_file, mcp__sentinelone-mcp__sdl_get_file, mcp__sentinelone-mcp__sdl_list_files |

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.

3. A field visible in raw_data may NOT be queryable

Parsers vary in what they extract to top-level OCSF / unmapped.* columns. A field plainly visible inside the raw_data JSON envelope may not exist as a queryable structured column. Always probe a single sample event with the V1 query to confirm a field is queryable before authoring a panel around it. Both the schema dump and a raw event are ground truth, neither alone is sufficient.

If a field is only present in raw_data, it can still be filtered via a full-text predicate but cannot be grouped or aggregated efficiently. See Full-text predicate cost below.

4. Identify the discriminator before counting

A single event.type value frequently bundles multiple distinct event kinds (delivery-time vs click-time, scheduled vs on-demand, inbound vs outbound, policy-event vs detection-event). The discriminator field, often named creationMethod, messageType, triggerType, disposition, etc., may or may not be promoted to the top level. Run an exploration query before authoring count panels:

dataSource.name='' event.type=''
| group hits=count() by 
| sort -hits
| limit 50

If the same event.type row repeats with different discriminator values, that secondary field is part of the partition key. Panels must filter on both, or split into separate sections per population. Counting "events of type X" without splitting by discriminator gives a number that conflates two semantically different things, which is the highest-cost class of dashboard bug because it looks correct.

4b. Null-check every grouping column before including it in a table panel

Before including any field as a grouping column in a table panel, confirm it is non-null for that specific event.type. A column that is null for all rows produces an empty column in the rendered table with no error. The check is one query:

dataSource.name='' event.type='' =*
| group count=count()
| limit 1

If this returns 0, that field is null for that event type. Remove it from the panel or replace with the correct field. This check is mandatory for every column in every table panel, not just fields you suspect might be missing.

Common trap: src_endpoint.svc_name (service name), src_endpoint.ip, and app_name may be populated for traffic events but null for vpn or app-ctrl events from the same source. Schema discovery on traffic events does not transfer to other event types.

5. event.type is not always the right partition key

Some sources emit multiple log subtypes under the same event.type (header logs vs body logs, policy events vs detection events). Run the same exploration query above with event.type PLUS a secondary discriminator before assuming event.type partitions the source cleanly.


PowerQuery feature gaps to design around

The patterns below produce HTTP 500s or silent renderer failures on current SDL builds. They appear syntactically valid in language references and may even work in a developer's local PowerQuery preview, but they are not safe inside dashboard JSON. Treat them as red flags during code review. scripts/panel_safety_check.py scans for them automatically.

| Pattern | Failure mode | |---|---| | \| let x = if(predicate, then, else) then aggregating on x | 500 server error | | count_if(predicate) / countif(predicate) aggregate functions | 500 server error | | sum(if(predicate, value, 0)) inside \| group ... by ... | 500 server error | | concat(field_a, ' literal ', field_b) in \| let bindings | 500 server error | | \| union (subquery) to add synthetic rows or merge two pipelines | 500 server error | | let totals = (... \| group ...) named subquery before main pipeline | 500 server error | | \| parse // with named captures and grouping in same query | 500 server error | | \| matches '' with \\s / \\d escapes inside the regex literal | 500 server error | | Anything after \| transpose (terminal command) | "transpose can only be used as the last command" | | graphStyle: "area" panel with a query field (not plots: [...]) | Indefinite spinner, no error surfaced | | Hyphenated arithmetic in ANY let, including z-scores / ratios: (live-base)/sd, total-min without spaces | "Identifier is ambiguous", the WHOLE panel fails with "Couldn't load content". Always write (live - base) / sd | | markdown panel with content: field instead of markdown: | Renders blank tile, no error | | markdown panel with NO title key (S-26.1) | Header renders "Untitled". Set a short plain-text title; prose only in markdown | | Number panel suffix repeats the unit already in the title | Reads "34 principals" under title "Active principals". Put the unit in the title OR the suffix, not both | | graphStyle: "bar" / "line" / "area" with a categorical (non-time) first column | "The first column of power query output should have numeric value in epoch s/ms/us/ns" error. For a category bar chart use "stacked_bar" with "xAxis": "grouped_data" and a (category, value) query |

Patterns that DO work and should be preferred

| Pattern | Use case | |---|---| | \| group n=count() \| limit 1 | Number panel | | \| group n=estimate_distinct() \| limit 1 | Cardinality number panel (HyperLogLog, fast) | | \| group =count() by , \| sort - \| limit N | Top-N table | | \| group =count() by timestamp=timebucket(''), \| transpose on timestamp | Time-series stacked-bar / line | | \| group =count() by , \| transpose on | Cross-tab / per-category × action stacked-bar | | Long-format table: \| group hits=count() by , \| sort , -hits \| limit N | When you need both dims as columns and a wide table can't be produced | | Index-level filter (before the first pipe) | Narrow scan to relevant events; cheaper than a post-pipe \| filter | | \| filter matches '' for selective dim filtering | Works with simple character classes; avoid \\s / \\d escapes |

Two-pass parse for quoted KV values

Network device logs (FortiGate, Palo Alto, etc.) emit KV pairs where values are wrapped in double quotes: app="HTTPS.BROWSER" appcat="Web.Client". The | parse format string is itself double-quoted, so you cannot embed a " to match the wrappers. The workaround is two passes:

Pass 1 — capture the whole non-whitespace token including its surrounding quotes:

| parse "app=$raw_app{regex=\\S+}$" from message

This extracts "HTTPS.BROWSER" (with quotes) into raw_app.

Pass 2 — extract the clean value by matching the alphanumeric content, which skips the leading ":

| parse "$app_name{regex=[A-Z0-9./_-]+}$" from raw_app

This produces HTTPS.BROWSER (no quotes) in app_name.

Full example for an app-ctrl panel:

dataSource.name='FortiGate' event.type='app-ctrl'
| parse "app=$raw_app{regex=\\S+}$" from message
| parse "appcat=$raw_cat{regex=\\S+}$" from message
| parse "$app_name{regex=[A-Z0-9./_-]+}$" from raw_app
| parse "$app_cat{regex=[A-Za-z0-9./_-]+}$" from raw_cat
| filter app_name != ''
| group Events=count() by Application=app_name, Category=app_cat
| sort -Events
| limit 20

Escaping in dashboard JSON: \\S+ in the PowerQuery string (what the engine sees) must be written as `\

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.