MSP Shadow AI Governance Console
Real-time detection of unauthorized local AI models — Ollama, LM Studio, llama.cpp, Whisper — rogue GPU usage, and unapproved AI tooling across all client endpoints. Powered by SentinelOne Deep Visibility v2.1 with per-site tenant isolation and six wired remediation actions routed through your backend proxy.
What Is This Console
Purpose, scope, threat model
The Shadow AI Governance Console is an MSP-facing security tool that detects unauthorized local AI model deployments on client endpoints. It surfaces processes, file paths, and network ports associated with Ollama, LM Studio, llama.cpp, Whisper, and other local AI runtimes — identifying which clients have active models, which endpoints are affected, and what data exposure risk those models create.
The console is built around the SentinelOne Deep Visibility API. Every detection, endpoint event, and process match is sourced from a real-time DV query sent to your S1 management instance via a backend proxy. Client data and event history rendered in the UI are populated from that query pipeline — not from independent EDR agents or a separate SIEM.
Local AI models running on employee workstations create several distinct risks: sensitive data (source code, financial records, PHI) is fed into models with no audit trail; Ollama's default bind to 0.0.0.0:11434 exposes the model API on the network; .gguf model files may be sourced from untrusted registries; and GPU abuse on shared or OT network segments can mask malicious compute usage. Standard EDR rules do not flag these behaviors by default — Deep Visibility queries are required.
What the Console Is Not
This is not a standalone EDR or a SIEM. It does not generate its own telemetry — it queries SentinelOne's existing telemetry pipeline. It requires Deep Visibility to be licensed and enabled at the Site level for each client. For clients using Microsoft Defender (the Mu Manufacturing case in the demo data), the console displays a fallback indicator but does not actively query Microsoft Graph Advanced Hunting — that integration must be built separately in your proxy.
Intended Audience
Who uses it and how
Layout & Sections
Page structure, navigation, responsive breakpoints
The page is a single scrollable document with a sticky topbar, a fixed right-side dot navigation, an API status banner, a KPI strip, and five distinct content sections. Navigation between sections happens by scrolling, clicking topbar nav links, or clicking the right-side dot nav — all use smooth scroll to the section's anchor ID.
runScan(). Shows dual spinning rings and step text as the proxy makes S1 API calls. Dismisses automatically when the final DV events response is received.alertsData array. In production, populate from DV events grouped by client.Responsive Breakpoints
| Viewport | Layout Change |
|---|---|
| > 900px | Full layout — right sidenav visible, full padding, 2-column analytics/deep-dive grids |
| < 900px | Sidenav hidden, padding collapsed to 20px, all 2-column grids collapse to 1 column, hero font size reduced |
API Audit Findings
Complete audit against official SentinelOne API v2.1 documentation
The console was audited against official SentinelOne API v2.1 documentation. Six issues were identified and all have been corrected in the shipped version. The console is proxy-ready — deploy your aggregator at the paths in Section 20 and it goes live with no further frontend changes.
Finding 1 — runScan Was Pure Theatre (Fixed ✓)
The original runScan() used a setTimeout loop to animate progress text with hardcoded step descriptions. It made zero actual API calls — the "69 total events" message was fabricated. The overlay gave the visual impression of real scanning while doing nothing. This has been replaced with a real async fetch chain that calls your proxy at five sequential endpoints: sites, agents, dv/init-query, dv/query-status (polled until FINISHED), and dv/events. Falls back gracefully if the proxy isn't deployed yet.
Finding 2 — runQuery Was a Fake setTimeout (Fixed ✓)
The original runQuery() waited 1.2 seconds then displayed a fabricated response with a random queryId, hardcoded 342ms latency, and a random estimatedResults count. No real query was sent. The function is now an async fetch that POSTs to /api/shadow-ai/edr/sentinelone/dv/init-query, extracts the real queryId from the response, and displays the actual server response in the terminal. When the proxy isn't deployed, it shows the ready-to-send payload as reference rather than a fake result.
Finding 3 — All 6 Remediation Actions Were Toast-Only (Fixed ✓)
Every remediation action in the Deep Dive panel called only toast('Action queued: ...') with a 2-second color reset — no API call of any kind. All six actions are now routed through ACTION_ROUTES to specific proxy endpoints. See Section 12 for the full routing table and per-action API constraints.
Finding 4 — Invalid DSL Field: GPUUsage (Fixed ✓)
GPUUsage is not a valid SentinelOne Deep Visibility DSL field. The DV query language exposes process, file, network, and registry telemetry fields — GPU utilization metrics are not part of the DV schema. A query using GPUUsage > 60 would silently return no results without an error, making it appear that no GPU anomalies exist. The preset has been replaced with a valid process-correlation query that identifies AI-related processes known to be GPU-intensive (Python with cuda/torch args, ollama, llama-server).
Finding 5 — Missing dv/query-status Poll Step (Fixed ✓)
The S1 Deep Visibility API is asynchronous — POST /web/api/v2.1/dv/init-query returns a queryId and status RUNNING, not results. You must then poll GET /web/api/v2.1/dv/query-status?queryId={id} until the status is FINISHED, then fetch results with GET /web/api/v2.1/dv/events?queryId={id}. The original scan flow skipped the status poll entirely. The API banner also did not show the dv/query-status or dv/events endpoints as distinct steps. Both have been added.
Finding 6 — Fake Live Simulation Interval (Fixed ✓)
A setInterval running every 6 seconds randomly incremented detection counts on clients that had detections, then called renderKPIs(). This had no connection to real data — it fabricated upward-trending metrics to simulate live activity. It has been removed. Detection count updates now only happen when a real scan completes.
Audit Summary
| Finding | Severity | Status |
|---|---|---|
| runScan() — setTimeout theatre, zero API calls | Critical | ✓ Fixed — real 5-step fetch chain |
| runQuery() — fake 342ms response, random queryId | Critical | ✓ Fixed — real async fetch + real queryId |
| 6 remediation actions — toast() only, no fetch | Critical | ✓ Fixed — ACTION_ROUTES with real endpoints |
| GPUUsage DSL field — not a valid S1 DV field | Critical | ✓ Fixed — replaced with valid process query |
| dv/query-status poll step missing from scan flow | High | ✓ Fixed — async poll loop added |
| Fake 6s detection increment interval | High | ✓ Fixed — interval removed |
| S1 API endpoints — all paths correct per v2.1 docs | Pass | ✓ No change needed |
| Auth scheme — ApiToken header correct per docs | Pass | ✓ No change needed |
| DV DSL operators — ContainsCIS, EndWith, IN, = all valid | Pass | ✓ No change needed (excluding GPUUsage) |
Proxy Architecture
Why a proxy is required and how it fits
SentinelOne's API requires an Authorization: ApiToken {token} header on every request. That token cannot live in browser-side JavaScript — it would be exposed to any user who opens DevTools. All S1 API calls are therefore routed through a backend proxy that holds the token server-side, validates dashboard requests, and forwards them to your S1 management instance.
Dashboard (browser) → POST /api/shadow-ai/edr/sentinelone/dv/init-query → Your Proxy (server) → POST https://usea1-api.sentinelone.net/web/api/v2.1/dv/init-query with Authorization: ApiToken {vault_token} → S1 Management API → Response back through proxy to dashboard.
The dashboard sends X-Dashboard: shadow-ai-gov on every request. Your proxy should validate this header as a lightweight shared-secret check before forwarding. For production deployments, add proper auth (JWT, session token, or mTLS) between the dashboard and proxy.
Multi-Tenant Site Isolation
Each client in the console maps to a SentinelOne Site — a tenant-isolated container within your S1 account. The siteId field (e.g. site_acme) is the S1 Site ID for that client. All DV queries use siteIds: [clientSiteId] to restrict results to that client's endpoints. Your proxy must map your internal client identifiers to real S1 Site IDs obtained from GET /web/api/v2.1/sites.
Token Scope
The S1 Service User token used by your proxy should be Account-scoped with Viewer role for read operations (sites, agents, DV queries). For remediation actions (disconnect, firewall-control), the Service User requires elevated permissions — either a separate token with the appropriate role, or a custom role with exactly the permissions listed in Section 07.
Auth — ApiToken
SentinelOne Service User token setup and scope requirements
SentinelOne uses Authorization: ApiToken {token} for all API requests. The token is generated from a Service User — not a console user account — and is scoped to Account or Site level. For an MSP, Account-level scope allows a single token to query all Sites within the account.
-
01
Create a Service UserIn the S1 Management Console: Settings → Users → Service Users → Actions → Create New Service User. Set an expiration date — SentinelOne recommends rotating tokens monthly or less. For MSP use, scope to Account level.
-
02
Assign RoleFor read-only DV operations (sites, agents, dv/init-query, dv/events): built-in Viewer role is sufficient. For remediation actions (disconnect, firewall-control): create a custom role with Endpoints → Disconnect from Network + Reconnect to Network + Firewall Control. Deep Visibility requires the Deep Visibility feature to be enabled in the Site policy.
-
03
Copy the Token OnceThe API token is shown only at creation time. Copy it immediately and store it in your vault (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager, or your proxy's environment variables). It cannot be retrieved again — you must delete and recreate the Service User to get a new token.
-
04
Configure Your ProxySet the token as an environment variable in your proxy runtime (
S1_API_TOKEN). The proxy reads it at startup and injects it as the Authorization header on every forwarded request. Never hardcode the token in source code or pass it to the browser.
// Every S1 API request from your proxy must include: Authorization: ApiToken {your_service_user_token} Content-Type: application/json // Example proxy injection (Node.js): const headers = { 'Authorization': `ApiToken ${process.env.S1_API_TOKEN}`, 'Content-Type': 'application/json' };
S1 Endpoint Reference
All SentinelOne API v2.1 endpoints used by this console — verified against official documentation
| Method + Path | Purpose | Required Role / Permission | Key Params |
|---|---|---|---|
| GET /web/api/v2.1/sites | Fetch all S1 Sites (clients) in the account | Viewer | limit, state=active |
| GET /web/api/v2.1/agents | Agent inventory — count online/offline per site | Viewer | siteIds, isActive, limit |
| POST /web/api/v2.1/dv/init-query | Initialize a Deep Visibility query — returns queryId | Viewer + Deep Visibility license | query, fromDate, toDate, siteIds, limit |
| GET /web/api/v2.1/dv/query-status | Poll query completion — returns RUNNING or FINISHED | Viewer + Deep Visibility license | queryId (query param) |
| GET /web/api/v2.1/dv/events | Fetch query results — returns matching events | Viewer + Deep Visibility license | queryId, limit, sortBy |
| POST /web/api/v2.1/agents/actions/disconnect | Network-isolate endpoint(s) from the network | Custom role: Endpoints → Disconnect from Network | filter.ids or filter.siteIds |
| POST /web/api/v2.1/agents/actions/connect | Re-connect isolated endpoint(s) to the network | Custom role: Endpoints → Reconnect to Network | filter.ids |
| POST /web/api/v2.1/firewall-control | Create a firewall rule to block a port or IP | Admin or custom Firewall Control role | data.name, data.rules[], data.siteIds |
| POST /web/api/v2.1/threats/mitigate/kill | Kill a threat-tagged process | SOC or IR Team role | filter.ids (threatIds — NOT processIds) |
| POST /web/api/v2.1/threats/mitigate/remediate | Delete threat-tagged files and revert changes | SOC or IR Team role | filter.ids (threatIds only) |
"Kill ollama/lm-studio processes remotely" and "Delete .gguf model files" cannot be performed via a direct S1 API call targeted by process name or file path. The S1 mitigation endpoints (threats/mitigate/kill, threats/mitigate/remediate) operate on threat-tagged objects that have an S1 threat ID — they cannot target arbitrary running processes or arbitrary file paths. The correct approach for these two actions is to use the S1 Remote Script Library: upload a PowerShell or Bash script to S1, then invoke it via POST /web/api/v2.1/remote-scripts/execute targeting the affected endpoint by agent ID.
Deep Visibility Query Flow — Step by Step
- 01POST dv/init-querySend query DSL string, date range, siteIds, and limit. Response:
{data: {queryId: "abc123", status: "RUNNING"}}. Store thequeryId. - 02Poll GET dv/query-status?queryId={id}Response returns
status: "RUNNING"orstatus: "FINISHED". Poll every 2 seconds, max ~12 times. Most queries finish in 3–10 seconds. If not finished after 24s, the query may still be running — increase poll count or increase poll interval. - 03GET dv/events?queryId={id}Once status is FINISHED, fetch the results. Response:
{data: [{processName, filePath, networkPort, computerName, ...}], pagination: {totalItems, nextCursor}}. Paginate usingcursoriftotalItemsexceeds your limit.
// Step 1 — POST /web/api/v2.1/dv/init-query { "siteIds": ["YOUR_SITE_ID"], "query": "ProcessName ContainsCIS \"ollama\" OR ProcessName ContainsCIS \"LM Studio\" OR ProcessName ContainsCIS \"llama-server\" OR ProcessName ContainsCIS \"whisper\" OR FilePath EndWith \".gguf\" OR NetworkPort IN (\"11434\",\"1234\",\"8080\")", "fromDate": "2026-02-01T00:00:00Z", "toDate": "2026-03-22T23:59:59Z", "limit": 500 } // Step 2 — GET /web/api/v2.1/dv/query-status?queryId=abc123 // Poll until: { "data": { "status": "FINISHED" } } // Step 3 — GET /web/api/v2.1/dv/events?queryId=abc123&limit=500 // Returns matched events with full process and file telemetry
API Status Banner
Connection nodes, dot colors, and what each displays
The API status banner at the top of the page shows nine nodes rendered by renderApiBanner(). Each node shows a dot (green = live, yellow = mock/pending, red = error), a current value, and the specific API endpoint it represents. In the shipped version these are static — wire your proxy health-check responses into this function to show real-time connection status.
| Node | Endpoint | Live Dot When |
|---|---|---|
| S1 Proxy | /api/shadow-ai/edr/sentinelone | Proxy health check returns 200 |
| Auth | Authorization: ApiToken {SERVICE_USER} | Never live — credential display only, always mock dot |
| Region | usea1-api.sentinelone.net | Sites endpoint reachable |
| Sites | GET /web/api/v2.1/sites | Sites data returned successfully |
| Agents | GET /web/api/v2.1/agents | Agents data returned successfully |
| DV Query | POST /web/api/v2.1/dv/init-query | Mock until a scan is actively running |
| DV Status | GET /web/api/v2.1/dv/query-status | Mock until a scan is actively polling |
| DV Events | GET /web/api/v2.1/dv/events | Mock until events have been fetched |
| Defender | Microsoft Graph Advanced Hunting | Always yellow fallback — not yet integrated |
KPI Strip
Seven tiles — what each counts and where the data comes from
| Tile | Computed As | Color |
|---|---|---|
| Total Detections | Sum of client.detections across all clients | Red (c-red) |
| Critical Clients | Count of clients where risk === 'critical' | Red (c-red) |
| Shadow AI Models | Static "12 unique" — update from DV events distinct model names | Orange (c-orange) |
| Endpoints Scanned | Sum of client.agents across all clients | Cyan (c-blue) |
| .gguf Files Found | Sum of client.ggufFiles across all clients | Purple (c-purple) |
| GPU Anomaly Flags | Count of clients where gpuFlag === true | Orange (c-orange) |
| Clean Sites | Count of clients where risk === 'low' | Green (c-green) |
KPI tiles have a 2px top color bar, lift-and-shadow on hover, and cursor:default — they are display-only and do not open drawers or drill-down panels. In production, renderKPIs() is called after each completed scan cycle rather than on a timer.
Detection Grid
Client cards, risk levels, model tags, and stat cells
The client grid renders one card per entry in the clients array, filtered by the active filter bar state. Each card is clickable and toggles the Deep Dive panel inline below the grid. Cards are colored by risk level — a 2px top border in red (critical), orange (high), yellow (medium), or green (low).
client.models array. In production, populate from distinct ProcessName values returned by the DV query for that site.agentsOnline/agents. Source from GET /web/api/v2.1/agents?siteIds={siteId}&isActive=true.Deep Dive Panel
Per-client event log, endpoint list, API payload preview, governance status
Clicking a client card opens the Deep Dive panel inline below the grid. The panel is a 2-column grid with four cards: Event Log, Affected Endpoints, API Call preview (terminal), and Governance Policy Status. Clicking the same card again or pressing the × button collapses it.
client.events[] — in production, populate from DV events filtered to this site's agentId values.client.endpoints[]. The FLAGGED status badge is static — in production, derive status from most recent DV event timestamp.POST /web/api/v2.1/dv/init-query · siteId: {id}.Remediation Actions
Six wired actions — API routing, S1 constraints, and what each proxy endpoint must do
Every remediation action now POSTs to a specific proxy endpoint with a 10-second timeout, passes the current client's siteId and clientName in the body, and falls back optimistically if the proxy isn't deployed. Each action POST includes X-Dashboard: shadow-ai-gov for proxy routing.
| Action | Proxy Endpoint | S1 API Target | Constraint |
|---|---|---|---|
| Isolate affected endpoints via S1 API | POST /api/shadow-ai/actions/isolate | POST /web/api/v2.1/agents/actions/disconnect | Requires agent IDs. Proxy resolves siteId → agentIds via GET /agents?siteIds=. |
| Kill ollama/lm-studio processes remotely | POST /api/shadow-ai/actions/kill-process | S1 Remote Script Library | No direct process-kill-by-PID in S1 API. Proxy must invoke Remote Script execution targeting the agent. |
| Block port 11434 via S1 Firewall Control | POST /api/shadow-ai/actions/block-port | POST /web/api/v2.1/firewall-control | Requires Admin or Firewall Control role. Rule must specify portRange, protocol, direction, action:"Block". |
| Delete .gguf model files (File Remediation) | POST /api/shadow-ai/actions/delete-files | S1 Remote Script Library | No arbitrary file delete in S1 API. threats/mitigate/remediate works only on threat-tagged files. Proxy must use Remote Script execution. |
| Send policy acknowledgment request to user | POST /api/shadow-ai/actions/policy-ack | PSA / Email workflow | Not a S1 API action. Proxy sends email or creates PSA task — no S1 call involved. |
| Generate incident report for client | POST /api/shadow-ai/actions/incident-report | PSA / Custom report | Not a S1 API action. Proxy assembles report from collected DV data and creates a PSA ticket or document. |
POST /api/shadow-ai/actions/{slug}
Content-Type: application/json
X-Dashboard: shadow-ai-gov
{
"action": "isolate",
"siteId": "site_acme", // null if no client is open in deep-dive
"clientName": "Acme Corp",
"timestamp": "2026-03-22T14:00:00.000Z"
}
// Proxy response — optional, shown in toast if present:
{ "message": "3 endpoints isolated · Acme Corp" }
The siteId in the action body is null if no client card is currently open in the Deep Dive panel. For actions that require S1 API calls (isolate, block-port), your proxy must validate that siteId is present before dispatching to S1 — and return a 400 error if it's missing. The dashboard will show that error in the toast. Isolate additionally requires agent IDs, which your proxy must resolve from the site by calling GET /web/api/v2.1/agents?siteIds={siteId} before dispatching the disconnect action.
Analytics Charts
Five Chart.js 4.4.1 canvases — data sources and production wiring
| Canvas ID | Type | Demo Data Source | Production Source |
|---|---|---|---|
| chartModels | Doughnut | Hardcoded: [14,5,4,2,3] for Ollama/LM Studio/llama.cpp/Whisper/Unknown | DV events — count distinct ProcessName values matching each model |
| chartTrend | Line | Math.random() for 30 days | DV events — group by createdAt date, count per day |
| chartRisk | Bar | client.detections sorted descending | DV events — count per siteId, sorted by total events |
| chartCoverage | Horizontal Bar | client.agentsOnline / client.agents | GET /web/api/v2.1/agents per site — count isActive=true vs total |
| chartGPU | Multi-line | Math.random() per client, 14 days | Process-correlation DV query for CUDA/torch processes per site per day |
All charts use a consistent tooltip theme: dark background (#0a0f18), JetBrains Mono title/body fonts, and subdued label colors to match the console aesthetic. Chart instances are stored in the chartInstances object. Charts are rendered once on init and not re-rendered on scan completion in the current version — add renderCharts() to the scan completion handler to refresh them with real data.
Query Builder
Preset selection, DSL textarea, execution, and payload preview
The Query Builder allows analysts to compose, preview, and execute SentinelOne Deep Visibility queries against any Site or the full portfolio. It is wired to POST real queries to your proxy — execute populates the terminal with the real queryId and response, while Preview shows the JSON payload without sending it.
showPayload() to refresh the terminal preview. All presets use confirmed valid DV DSL operators.["[all_site_ids]"] as a placeholder — your proxy must expand this to the list of all real site IDs from GET /sites. Selecting a specific site sends that site's ID directly.pagination.nextCursor is present in the response, more results are available./api/shadow-ai/edr/sentinelone/dv/init-query with the current payload. Returns real queryId and initial status. Terminal updates with the actual response.showPayload() which builds and displays the JSON payload in the terminal WITHOUT sending it. Use this to inspect and copy the exact request that will be sent.lastPayload (the most recently previewed or executed query body) to clipboard using navigator.clipboard.writeText().Policy Status Table
Per-client AI governance compliance — how it's currently computed and how to source it live
The Policy Status table shows seven columns per client: Client/Site, Overall Status, AI Policy (written policy on file), S1 Rule (behavioral detection rule deployed), User ACK (employee acknowledgment), Incident (open PSA ticket), and Events. The overall status is derived from client.risk.
In the current version, all columns except Events are derived from client.risk using simple boolean logic — for example, hasPolicy = client.risk === 'low'. For production compliance tracking, these fields should be sourced from your GRC platform, PSA custom fields, or a separate policy management database. The table structure supports direct replacement of the computed values with real data from those systems.
The "S1 Rule" column currently shows "Active" for non-critical S1-managed clients. In production, validate this by checking whether a custom STAR (Singularity Threat Analytics Rule) targeting AI process names exists for the site. Use GET /web/api/v2.1/cloud-detection/rules?siteIds={id} to query deployed detection rules. If no rule matching "ollama" or "llama" exists, mark as "Missing".
Alert Feed
Detection alerts — demo data structure and production population
The Alert Feed renders the alertsData array — currently seven static entries covering the highest-severity findings across clients. Each entry shows: severity icon, client name, message, endpoint detail (↳ processName · hostname · context), and source metadata (platform, query method, age).
In production, populate alertsData from your DV events response after each scan. Group events by siteId, rank by severity (processes listening on 0.0.0.0 = critical, HIPAA-relevant models on clinical machines = critical, .gguf files = high, policy-violation ports = high, single session clean exit = medium). The endpoint detail and meta strings should be built from the DV event's agentComputerName, processName, and createdAt fields.
Deep Visibility DSL
Valid operators, queryable fields, and constraints confirmed against S1 v2.1 API documentation
Valid Operators
| Operator | Description | Example |
|---|---|---|
| ContainsCIS | Case-insensitive substring match | ProcessName ContainsCIS "ollama" |
| EndWith | String ends with value (case-insensitive) | FilePath EndWith ".gguf" |
| IN | Value is in a set of strings | NetworkPort IN ("11434","1234") |
| = | Exact match | NetworkPort = "11434" |
| AND / OR | Boolean logic — OR has lower precedence than AND | Use parentheses to group OR clauses |
Queryable Fields (Process Events)
| Field | Description |
|---|---|
| ProcessName | Executable name of the process (e.g. "ollama.exe") |
| ProcessCmdLine | Full command line including arguments — good for catching model loading flags |
| FilePath | Full path of file accessed or created by the process |
| NetworkPort | Port number the process is listening on or connecting to |
| InitiatingProcessName | Parent process name — useful for detecting scripts that launch AI runtimes |
| InitiatingProcessFileName | Parent process executable filename |
| SrcIP / DstIP | Source/destination IP — useful for detecting external connections to local model APIs |
| SrcPort / DstPort | Source/destination port for network events |
| SHA1 / SHA256 | File hashes — for blocklist matching of known model executables |
GPUUsage does not exist in the SentinelOne Deep Visibility query schema. GPU utilization metrics are not exposed through the DV API. A query containing GPUUsage > 60 returns zero results without an error, silently appearing to show a clean environment. For GPU anomaly detection, use process-based correlation: identify processes known to use GPU acceleration (Python with torch/CUDA args, ollama, llama-server) rather than querying GPU metrics directly.
Query Limitations
pagination.nextCursor is present in the events response, additional results exist. Pass cursor={value} as a query param on subsequent requests to retrieve the next page.Preset Query Library
Seven validated queries — what each detects and why
| Preset Key | Query (DSL) | Detects |
|---|---|---|
| ollama | ProcessName ContainsCIS "ollama" OR ProcessCmdLine ContainsCIS "ollama" OR NetworkPort = "11434" | Ollama runtime — process running, started via script, or port 11434 active. Port match catches the API server even if the process name differs. |
| gguf | FilePath EndWith ".gguf" OR FilePath EndWith ".bin" AND FilePath ContainsCIS "models" | GGUF model files (Ollama, llama.cpp, LM Studio format) and .bin model files in model directories. |
| lmstudio | ProcessName ContainsCIS "lm studio" OR ProcessName ContainsCIS "LMStudio" OR NetworkPort = "1234" | LM Studio — process name (space variant and concatenated) or default API port 1234. |
| gpu | ProcessName ContainsCIS "python" AND (ProcessCmdLine ContainsCIS "torch" OR ProcessCmdLine ContainsCIS "cuda" OR ProcessCmdLine ContainsCIS "gpu") OR ProcessName ContainsCIS "ollama" OR ProcessName ContainsCIS "llama-server" | GPU-intensive AI processes — Python with ML framework args, or known AI runtimes. Replaces the invalid GPUUsage > 60 query. |
| llamacpp | ProcessName ContainsCIS "llama-server" OR ProcessName ContainsCIS "llama.cpp" OR ProcessCmdLine ContainsCIS "gguf" | llama.cpp server process, the llama.cpp binary, or any process whose command line references a .gguf file path. |
| whisper | ProcessName ContainsCIS "whisper" OR ProcessCmdLine ContainsCIS "whisper" OR FilePath ContainsCIS "openai/whisper" | OpenAI Whisper STT — process, command line, or model file path. Critical for HIPAA environments where PHI may be in audio. |
| localport | NetworkPort IN ("11434","1234","8080","5000","3000") AND InitiatingProcessName ContainsCIS "python" | Common local AI API ports bound by Python processes. Catches custom AI server deployments that don't use known binary names. |
Defender Fallback
What it means, what it does now, and how to wire it
One client in the demo data — Mu Manufacturing — uses edr: 'defender'. This represents clients whose endpoints run Microsoft Defender for Endpoint rather than SentinelOne. The console displays a yellow "● Defender · Fallback EDR" badge for these clients and shows their static detection data — but does not actively query Microsoft Graph Advanced Hunting.
The API status banner shows Defender: FALLBACK with a yellow dot and labels the endpoint as "Microsoft Graph Advanced Hunting" — but no actual Graph API calls are made anywhere in the current codebase. The Defender client's data is entirely static. To implement this, your proxy must call POST https://graph.microsoft.com/v1.0/security/runHuntingQuery with a KQL query equivalent to the S1 DV DSL query, using an app registration with the ThreatHunting.Read.All permission. The console filter already supports edr: 'defender' — wire the data population in your proxy and push the results into the client array via a scan completion callback.
Microsoft Graph Advanced Hunting — KQL Equivalents
| S1 DV DSL Query | Microsoft Graph / KQL Equivalent |
|---|---|
ProcessName ContainsCIS "ollama" | DeviceProcessEvents | where FileName contains "ollama" |
FilePath EndWith ".gguf" | DeviceFileEvents | where FileName endswith ".gguf" |
NetworkPort = "11434" | DeviceNetworkEvents | where LocalPort == 11434 or RemotePort == 11434 |
ProcessCmdLine ContainsCIS "whisper" | DeviceProcessEvents | where ProcessCommandLine contains "whisper" |
Proxy Endpoint Spec
Every path the dashboard calls — what your proxy must implement
GET /web/api/v2.1/sites?limit=1000&state=active. Return the full sites array. Used by runScan to count clients and build siteId list.GET /web/api/v2.1/agents?siteIds=all&limit=1000 (or per-site). Return agent count by site. Used by runScan for inventory.POST /web/api/v2.1/dv/init-query. Return {data:{queryId, status}}. Body fields: query, fromDate, toDate, siteIds, limit.GET /web/api/v2.1/dv/query-status?queryId={queryId}. Return {data:{status}} where status is RUNNING or FINISHED.GET /web/api/v2.1/dv/events?queryId={id}&limit={n}. Return events array and pagination object.{filter:{ids:[agentIds]}}. Returns count of isolated agents.{data:{name:"Block AI Ports",rules:[{portRange:"11434",protocol:"TCP",direction:"inbound",action:"Block"}],siteIds:[body.siteId]}}.Every call from the dashboard includes Content-Type: application/json and X-Dashboard: shadow-ai-gov. Your proxy should validate the X-Dashboard header as a routing signal and basic anti-CSRF measure. All action endpoints also receive an 10-second AbortSignal.timeout, so your proxy must respond within that window.
Go-Live Checklist
Ordered steps from demo mode to production
- 01Create S1 Service User and generate API tokenAccount-scoped Viewer role for read operations. Create a second Service User with a custom role that includes Endpoints → Disconnect from Network and Firewall Control for remediation actions. Store both tokens in your vault.
- 02Enable Deep Visibility on all client SitesIn S1 Management Console: Sentinels → Policy → Deep Visibility → Enabled. DV must be both licensed and enabled per-Site. Without this, dv/init-query will succeed but dv/events will return empty results.
- 03Deploy the backend proxyImplement all 10 endpoints from Section 20. Serve from the same origin as the dashboard HTML or configure a reverse proxy so relative paths resolve correctly. The token must live only in the proxy's environment — never in the browser.
- 04Map client names to real S1 Site IDsRun
GET /web/api/v2.1/sitesvia your proxy and map the returned site IDs to the client records in theclientsarray. Update thesiteIdvalues from the placeholder strings (site_acme, site_zeta, etc.) to your real numeric S1 Site IDs. Update the Query Builder's site dropdown options to match. - 05Wire renderClientGrid to DV event dataAfter a scan completes, transform DV events into the client array shape (detections, models[], endpoints[], events[], gpuFlag, port11434, ggufFiles). Call
renderClientGrid(clients)andrenderKPIs()with the updated data. The cards and KPI tiles will reflect live scan results automatically. - 06Deploy action proxy endpointsImplement the six /api/shadow-ai/actions/* routes. Test each one manually before enabling in the console. The dashboard falls back gracefully if the routes aren't yet live — no frontend change needed when you add them.
- 07Update API status banner to reflect real healthAdd a lightweight proxy health endpoint and call it from
renderApiBanner()on page load. Update node dot classes (dot-live / dot-mock / dot-error) based on the health response rather than static strings. - 08Wire Analytics Charts to DV events dataAfter a scan completes, aggregate DV event data into the chart shapes documented in Section 13 and call
renderCharts()with the live data. The chart canvases and Chart.js instances are already set up — you only need to provide real data arrays.
FAQ
Common questions about wiring, limitations, and edge cases
Your proxy is not deployed — the fetch('/api/shadow-ai/edr/sentinelone/sites') call on the first scan step fails immediately with a network error, triggering the fallback which closes the overlay and re-renders demo data. Deploy the proxy at /api/shadow-ai/edr/sentinelone/sites and the scan will proceed through all five steps.
Deep Visibility is not enabled for that Site. Verify in the S1 Management Console under Sentinels → Policy → Deep Visibility. The policy must explicitly have Deep Visibility enabled — it is not on by default. Also confirm the Service User has the Deep Visibility license scope at the Account level.
The original GPUUsage > 60 field is not valid in S1 DV DSL — it would always return zero. The corrected query uses process-name and command-line matching for known GPU-intensive processes. If you're still seeing zero results, verify that Deep Visibility is collecting process command line data for the relevant Site (check the DV event type configuration in the Site policy).
The disconnect action requires the Service User token to have the "Disconnect from Network" permission in its role. The built-in Viewer role does not include this. Create a custom role with Endpoints → Disconnect from Network checked, assign it to the Service User used for remediation, and use that token for action routes. Keep a separate read-only Viewer token for DV query routes.
Yes. Add a new entry to the presets object with a DSL query targeting the tool's process name or typical file paths. Add a corresponding button in the Preset Queries row HTML. No other changes needed — the query builder, terminal, and scan flow all work with arbitrary valid DV DSL strings. Verify any new query operators against the field list in Section 17 before adding.
SentinelOne DV queries are resource-intensive — running a full portfolio-wide scan against 8+ sites on every page load would be inappropriate. The console is designed for on-demand scanning (the "Run Deep Visibility Scan" button) rather than continuous polling. For automated periodic scans, run the query cycle from your proxy on a cron schedule (e.g. every 30 minutes), cache the results, and expose them at a lightweight GET /api/shadow-ai/scan-results endpoint that the dashboard polls instead of initiating a new DV query each time.