# Sentinelone Sdl Dashboard

> >

- **Type:** Skill
- **Install:** `agentstack add skill-pmoses-s1-claude-skills-sentinelone-sdl-dashboard`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [pmoses-s1](https://agentstack.voostack.com/s/pmoses-s1)
- **Installs:** 0
- **Category:** [Data & Analytics](https://agentstack.voostack.com/c/data-and-analytics)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [pmoses-s1](https://github.com/pmoses-s1)
- **Source:** https://github.com/pmoses-s1/claude-skills/tree/main/sentinelone-sdl-dashboard

## Install

```sh
agentstack add skill-pmoses-s1-claude-skills-sentinelone-sdl-dashboard
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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.

0. **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.
1. **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**).
2. **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.
3. **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.
4. **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.
5. **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.
6. **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**.
7. **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.

8. **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):

```python
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.

- **Author:** [pmoses-s1](https://github.com/pmoses-s1)
- **Source:** [pmoses-s1/claude-skills](https://github.com/pmoses-s1/claude-skills)
- **License:** MIT

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

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-pmoses-s1-claude-skills-sentinelone-sdl-dashboard
- Seller: https://agentstack.voostack.com/s/pmoses-s1
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
