@pmoses-s1/sentinelone-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -0
- package/index.js +352 -0
- package/lib/credentials.js +116 -0
- package/lib/s1.js +767 -0
- package/lib/sdl.js +165 -0
- package/lib/uam-ingest.js +438 -0
- package/package.json +44 -0
- package/tools/hyperautomation.js +250 -0
- package/tools/mgmt-console.js +307 -0
- package/tools/powerquery.js +124 -0
- package/tools/sdl-api.js +133 -0
- package/tools/uam-ingest.js +128 -0
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# SentinelOne MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that orchestrates all six SentinelOne skills and their APIs. Built in pure Node.js 18+ with zero external dependencies.
|
|
4
|
+
|
|
5
|
+
## What this exposes
|
|
6
|
+
|
|
7
|
+
**19 tools** covering every skill in the plugin:
|
|
8
|
+
|
|
9
|
+
| Group | Tool | Skill |
|
|
10
|
+
|-------|------|-------|
|
|
11
|
+
| PowerQuery | `powerquery_enumerate_sources` | sentinelone-powerquery |
|
|
12
|
+
| PowerQuery | `powerquery_run` | sentinelone-powerquery |
|
|
13
|
+
| PowerQuery | `powerquery_schema_discover` | sentinelone-powerquery |
|
|
14
|
+
| Mgmt Console | `s1_api_get` | sentinelone-mgmt-console-api |
|
|
15
|
+
| Mgmt Console | `s1_api_post` | sentinelone-mgmt-console-api |
|
|
16
|
+
| Mgmt Console | `purple_ai_query` | sentinelone-mgmt-console-api |
|
|
17
|
+
| Mgmt Console | `uam_list_alerts` | sentinelone-mgmt-console-api |
|
|
18
|
+
| Mgmt Console | `uam_get_alert` | sentinelone-mgmt-console-api |
|
|
19
|
+
| Mgmt Console | `uam_add_note` | sentinelone-mgmt-console-api |
|
|
20
|
+
| Mgmt Console | `uam_set_status` | sentinelone-mgmt-console-api |
|
|
21
|
+
| SDL API | `sdl_list_files` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
22
|
+
| SDL API | `sdl_get_file` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
23
|
+
| SDL API | `sdl_put_file` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
|
|
24
|
+
| SDL API | `sdl_delete_file` | sentinelone-sdl-api |
|
|
25
|
+
| SDL API | `sdl_upload_logs` | sentinelone-sdl-api / sdl-log-parser |
|
|
26
|
+
| Hyperautomation | `ha_list_workflows` | sentinelone-hyperautomation |
|
|
27
|
+
| Hyperautomation | `ha_get_workflow` | sentinelone-hyperautomation |
|
|
28
|
+
| Hyperautomation | `ha_import_workflow` | sentinelone-hyperautomation |
|
|
29
|
+
| Hyperautomation | `ha_export_workflow` | sentinelone-hyperautomation |
|
|
30
|
+
|
|
31
|
+
**2 resources:**
|
|
32
|
+
- `sentinelone://soc-context`: CLAUDE.md (full SOC analyst operating instructions)
|
|
33
|
+
- `sentinelone://credentials-status`: Which credentials are configured
|
|
34
|
+
|
|
35
|
+
**2 prompts:**
|
|
36
|
+
- `soc_analyst`: Embeds CLAUDE.md as a system prompt; call at session start
|
|
37
|
+
- `session_init`: Structured initialization: enumerate sources + triage alerts in parallel
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
- Node.js 18 or later
|
|
42
|
+
- No `npm install` needed: zero external dependencies
|
|
43
|
+
|
|
44
|
+
## Credentials
|
|
45
|
+
|
|
46
|
+
Credentials are passed as environment variables in `claude_desktop_config.json` (see Installation below). The server also auto-discovers a `credentials.json` file by searching from the working directory upward as a backwards-compatible fallback for direct-skill users.
|
|
47
|
+
|
|
48
|
+
`S1_CONSOLE_URL` and `S1_CONSOLE_API_TOKEN` are sufficient for most tools. Add the SDL keys only if you need `sdl_upload_logs` (requires `SDL_LOG_WRITE_KEY`) or `sdl_put_file` (requires `SDL_CONFIG_WRITE_KEY`).
|
|
49
|
+
|
|
50
|
+
| Variable | Description |
|
|
51
|
+
|----------|-------------|
|
|
52
|
+
| `S1_CONSOLE_URL` | Your console URL, e.g. `https://usea1-acme.sentinelone.net` |
|
|
53
|
+
| `S1_CONSOLE_API_TOKEN` | Management Console API token (Settings → Users → Service Users) |
|
|
54
|
+
| `S1_HEC_INGEST_URL` | HEC ingest host, e.g. `https://ingest.us1.sentinelone.net` |
|
|
55
|
+
| `SDL_XDR_URL` | SDL tenant URL, e.g. `https://xdr.us1.sentinelone.net` |
|
|
56
|
+
| `SDL_LOG_WRITE_KEY` | SDL Log Write key (required for `sdl_upload_logs` only) |
|
|
57
|
+
| `SDL_LOG_READ_KEY` | SDL Log Read key (required for SDL query operations) |
|
|
58
|
+
| `SDL_CONFIG_WRITE_KEY` | SDL Config Write key (required for `sdl_put_file`) |
|
|
59
|
+
| `SDL_CONFIG_READ_KEY` | SDL Config Read key (required for `sdl_list_files`, `sdl_get_file`) |
|
|
60
|
+
|
|
61
|
+
## Run the server
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# From the published npm package (no clone, no install)
|
|
65
|
+
npx -y @pmoses-s1/sentinelone-mcp
|
|
66
|
+
|
|
67
|
+
# Or from a local clone (development)
|
|
68
|
+
node /path/to/claude-skills/sentinelone-mcp/index.js
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
### Why you need this (or the allowlist alternative)
|
|
74
|
+
|
|
75
|
+
The Claude sandbox proxy blocks outbound HTTPS to `*.sentinelone.net` by default. There are two ways to fix this:
|
|
76
|
+
|
|
77
|
+
**Option A: Install sentinelone-mcp (recommended).** The MCP server runs as a local process on your machine outside the sandbox. All API calls go directly from your machine to SentinelOne, bypassing the sandbox proxy entirely. No allowlist changes needed.
|
|
78
|
+
|
|
79
|
+
**Option B: Add `*.sentinelone.net` to the Claude sandbox allowlist.** In Claude Desktop go to Settings → Claude Code → Network Access and add `*.sentinelone.net` to the allowed domains. This lets the skills' Python scripts reach the API directly from inside the sandbox. Use this if you prefer to keep everything running in the sandbox rather than install a local server.
|
|
80
|
+
|
|
81
|
+
Most users should use Option A: it requires no admin changes and keeps credentials out of the sandbox environment.
|
|
82
|
+
|
|
83
|
+
### Option A: Add to Claude Desktop
|
|
84
|
+
|
|
85
|
+
In `~/Library/Application Support/Claude/claude_desktop_config.json`, add the `sentinelone-mcp` entry to your `mcpServers` block. The server runs via `npx` directly from npm, so there is no clone, no install, and no absolute path to manage. Credentials go in the `env` section:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"sentinelone-mcp": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["-y", "@pmoses-s1/sentinelone-mcp"],
|
|
93
|
+
"env": {
|
|
94
|
+
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
95
|
+
"S1_CONSOLE_API_TOKEN": "eyJ...your-api-token...",
|
|
96
|
+
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net",
|
|
97
|
+
"SDL_XDR_URL": "https://xdr.us1.sentinelone.net",
|
|
98
|
+
"SDL_LOG_WRITE_KEY": "0Z1Fy0...",
|
|
99
|
+
"SDL_LOG_READ_KEY": "0tzj...",
|
|
100
|
+
"SDL_CONFIG_WRITE_KEY": "0mXas6PD...",
|
|
101
|
+
"SDL_CONFIG_READ_KEY": "0MQTx..."
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`npx -y` answers "yes" to the install prompt on first launch, fetches the package, and caches it. Subsequent launches start instantly from the cache. Restart Claude Desktop after saving.
|
|
109
|
+
|
|
110
|
+
`S1_CONSOLE_URL` and `S1_CONSOLE_API_TOKEN` are the minimum required for most tools. Include the SDL keys only if you need log ingest or parser/dashboard deploy. Set `S1_CLAUDE_MD_PATH` if you keep CLAUDE.md outside your Cowork project folder.
|
|
111
|
+
|
|
112
|
+
### Option A: Add to Claude Code
|
|
113
|
+
|
|
114
|
+
In `.mcp.json` at your project root, or `~/.mcp.json` globally. Same npx invocation, same env block:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"sentinelone-mcp": {
|
|
120
|
+
"command": "npx",
|
|
121
|
+
"args": ["-y", "@pmoses-s1/sentinelone-mcp"],
|
|
122
|
+
"env": {
|
|
123
|
+
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
124
|
+
"S1_CONSOLE_API_TOKEN": "eyJ...your-api-token..."
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Option A: Run from a local clone (development only)
|
|
132
|
+
|
|
133
|
+
If you are developing the MCP server itself, replace the `npx` invocation with a path to your clone:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
"sentinelone-mcp": {
|
|
137
|
+
"command": "node",
|
|
138
|
+
"args": ["/absolute/path/to/claude-skills/sentinelone-mcp/index.js"],
|
|
139
|
+
"env": { "...": "..." }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Option B: Sandbox allowlist (no MCP server)
|
|
144
|
+
|
|
145
|
+
If you prefer to run API calls from inside the Claude sandbox rather than install a local server, add `*.sentinelone.net` to the network allowlist:
|
|
146
|
+
|
|
147
|
+
1. Open Claude Desktop → Settings → Claude Code → Network Access
|
|
148
|
+
2. Add `*.sentinelone.net` to the allowed domains
|
|
149
|
+
3. Restart Claude Desktop
|
|
150
|
+
|
|
151
|
+
The skills' Python scripts (`s1_client.py`, `sdl_client.py`, etc.) will then reach the API directly. No MCP server required for the skills to work.
|
|
152
|
+
|
|
153
|
+
## Workflow: session startup
|
|
154
|
+
|
|
155
|
+
When connecting to this MCP server, start every session with:
|
|
156
|
+
|
|
157
|
+
1. Read the `soc_analyst` prompt (or the `sentinelone://soc-context` resource) to load operating instructions from CLAUDE.md.
|
|
158
|
+
2. Call `powerquery_enumerate_sources` to discover active SDL data sources (mandatory: never assume sources from a prior session).
|
|
159
|
+
3. In parallel, call `uam_list_alerts` with `filter="status=OPEN"` to pull active alerts.
|
|
160
|
+
|
|
161
|
+
The `session_init` prompt automates steps 2-3 as a structured prompt.
|
|
162
|
+
|
|
163
|
+
## Architecture
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
sentinelone-mcp/
|
|
167
|
+
index.js Raw MCP JSON-RPC over stdio (no SDK dependency)
|
|
168
|
+
lib/
|
|
169
|
+
credentials.js Auto-discovers credentials.json (env vars > file > walk-up > ~/mnt/*)
|
|
170
|
+
s1.js S1 Mgmt REST API + LRQ PowerQuery + Purple AI + UAM GraphQL
|
|
171
|
+
sdl.js SDL config files (get/put/list) + V1 query + uploadLogs
|
|
172
|
+
tools/
|
|
173
|
+
powerquery.js PowerQuery enumerate/run/schema-discover tools
|
|
174
|
+
mgmt-console.js S1 REST + Purple AI + UAM tools
|
|
175
|
+
sdl-api.js SDL config file + log ingestion tools
|
|
176
|
+
hyperautomation.js Hyperautomation list/get/import/export tools
|
|
177
|
+
README.md
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Auth patterns (implemented)
|
|
181
|
+
|
|
182
|
+
| API surface | Auth header | Key |
|
|
183
|
+
|-------------|-------------|-----|
|
|
184
|
+
| S1 Mgmt REST API | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
185
|
+
| LRQ PowerQuery | `Authorization: Bearer <jwt>` | Same token, different prefix |
|
|
186
|
+
| Purple AI GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
187
|
+
| UAM GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
188
|
+
| SDL config ops | `Authorization: Bearer <key>` | `SDL_CONFIG_WRITE_KEY` or console JWT |
|
|
189
|
+
| SDL uploadLogs | `Authorization: Bearer <key>` | `SDL_LOG_WRITE_KEY` only (console JWT rejected) |
|
|
190
|
+
|
|
191
|
+
## Updating CLAUDE.md
|
|
192
|
+
|
|
193
|
+
The `sentinelone://soc-context` resource and `soc_analyst` prompt load CLAUDE.md at server startup. Resolution order (highest priority wins):
|
|
194
|
+
|
|
195
|
+
1. `S1_CLAUDE_MD_PATH` env var (explicit absolute path)
|
|
196
|
+
2. `<cwd>/CLAUDE.md` (your Cowork project folder when launched from a project)
|
|
197
|
+
3. Same-dir / parent / grandparent of the server's `index.js` (when running from a git clone)
|
|
198
|
+
|
|
199
|
+
For npx installs, drop a copy of CLAUDE.md into your Cowork project folder, or set `S1_CLAUDE_MD_PATH` in the `env` block of `claude_desktop_config.json`. Restart Claude Desktop to pick up changes.
|
package/index.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SentinelOne MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Implements the Model Context Protocol (MCP) over stdio using raw JSON-RPC 2.0.
|
|
6
|
+
* No external dependencies — pure Node.js 18+.
|
|
7
|
+
*
|
|
8
|
+
* Exposes:
|
|
9
|
+
* Resources : sentinelone://soc-context (CLAUDE.md SOC analyst operating instructions)
|
|
10
|
+
* Prompts : soc_analyst (system prompt embedding CLAUDE.md)
|
|
11
|
+
* Tools (21) : PowerQuery, Mgmt Console, SDL API, Hyperautomation, UAM Ingest
|
|
12
|
+
*
|
|
13
|
+
* Run as: node index.js
|
|
14
|
+
* Configure in claude_desktop_config.json or .mcp.json — see README.md.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createInterface } from 'readline';
|
|
18
|
+
import { readFileSync, existsSync } from 'fs';
|
|
19
|
+
import { join, dirname } from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
import { tools as pqTools } from './tools/powerquery.js';
|
|
23
|
+
import { tools as mgmtTools } from './tools/mgmt-console.js';
|
|
24
|
+
import { tools as sdlTools } from './tools/sdl-api.js';
|
|
25
|
+
import { tools as haTools } from './tools/hyperautomation.js';
|
|
26
|
+
import { tools as uamIngestTools } from './tools/uam-ingest.js';
|
|
27
|
+
import { getCreds, hasS1Creds, hasSdlCreds } from './lib/credentials.js';
|
|
28
|
+
import { hasHecCreds } from './lib/uam-ingest.js';
|
|
29
|
+
|
|
30
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
32
|
+
// ─── SOC context (CLAUDE.md) ──────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function loadSocContext() {
|
|
35
|
+
// Resolution order (highest priority wins):
|
|
36
|
+
// 1. S1_CLAUDE_MD_PATH env var (explicit override, e.g. set by claude_desktop_config.json)
|
|
37
|
+
// 2. <cwd>/CLAUDE.md (Cowork project folder when launched from a project)
|
|
38
|
+
// 3. relative to this server's install dir (works when running from a git clone)
|
|
39
|
+
const candidates = [
|
|
40
|
+
process.env.S1_CLAUDE_MD_PATH,
|
|
41
|
+
process.cwd() ? join(process.cwd(), 'CLAUDE.md') : null,
|
|
42
|
+
join(__dir, '..', 'CLAUDE.md'), // claude-skills/CLAUDE.md (git clone)
|
|
43
|
+
join(__dir, '..', '..', 'CLAUDE.md'), // project root (git clone)
|
|
44
|
+
join(__dir, 'CLAUDE.md'), // same dir
|
|
45
|
+
].filter(Boolean);
|
|
46
|
+
for (const p of candidates) {
|
|
47
|
+
if (existsSync(p)) {
|
|
48
|
+
try { return readFileSync(p, 'utf-8'); } catch { /* skip */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return '# SentinelOne SOC Analyst Context\n\n_CLAUDE.md not found. Place it in your Cowork project folder, or set S1_CLAUDE_MD_PATH to an absolute path._';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const SOC_CONTEXT = loadSocContext();
|
|
55
|
+
|
|
56
|
+
// ─── Tool registry ────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const ALL_TOOLS = [...pqTools, ...mgmtTools, ...sdlTools, ...haTools, ...uamIngestTools];
|
|
59
|
+
|
|
60
|
+
// MCP tool schema (inputSchema is JSON Schema)
|
|
61
|
+
const TOOL_DEFS = ALL_TOOLS.map(t => ({
|
|
62
|
+
name: t.name,
|
|
63
|
+
description: t.description,
|
|
64
|
+
inputSchema: t.inputSchema,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// Handler map
|
|
68
|
+
const HANDLERS = Object.fromEntries(ALL_TOOLS.map(t => [t.name, t.handler]));
|
|
69
|
+
|
|
70
|
+
// ─── Resources ────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const RESOURCES = [
|
|
73
|
+
{
|
|
74
|
+
uri: 'sentinelone://soc-context',
|
|
75
|
+
name: 'SOC Analyst Operating Instructions',
|
|
76
|
+
description: 'CLAUDE.md — Principal SOC Analyst operating instructions including investigation workflow, evidence discipline, anomaly detection playbook, MITRE ATT&CK mapping, and tool usage priorities.',
|
|
77
|
+
mimeType: 'text/markdown',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
uri: 'sentinelone://credentials-status',
|
|
81
|
+
name: 'Credential Configuration Status',
|
|
82
|
+
description: 'Reports which credentials are configured and which API surfaces are available.',
|
|
83
|
+
mimeType: 'application/json',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// ─── Prompts ──────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const PROMPTS = [
|
|
90
|
+
{
|
|
91
|
+
name: 'soc_analyst',
|
|
92
|
+
description: 'Load the Principal SOC Analyst system context from CLAUDE.md. Call at the start of every security investigation session to prime the operating instructions, evidence discipline rules, investigation workflow, and tool usage priorities.',
|
|
93
|
+
arguments: [],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'session_init',
|
|
97
|
+
description: 'Structured session initialization prompt. Triggers mandatory data-source enumeration, alert triage, and schema discovery in parallel — mirroring the standard engagement workflow from the SOC playbook.',
|
|
98
|
+
arguments: [],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// ─── MCP protocol ─────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const SERVER_INFO = {
|
|
105
|
+
name: 'sentinelone-mcp-server',
|
|
106
|
+
version: '1.0.0',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const PROTOCOL_VERSION = '2024-11-05';
|
|
110
|
+
|
|
111
|
+
function ok(id, result) {
|
|
112
|
+
return { jsonrpc: '2.0', id, result };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function err(id, code, message, data) {
|
|
116
|
+
return { jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function send(obj) {
|
|
120
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function log(...args) {
|
|
124
|
+
process.stderr.write('[sentinelone-mcp] ' + args.join(' ') + '\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Method handlers ──────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function dispatch(method, params, id) {
|
|
130
|
+
switch (method) {
|
|
131
|
+
|
|
132
|
+
case 'initialize': {
|
|
133
|
+
return ok(id, {
|
|
134
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
135
|
+
capabilities: {
|
|
136
|
+
resources: { subscribe: false, listChanged: false },
|
|
137
|
+
tools: { listChanged: false },
|
|
138
|
+
prompts: { listChanged: false },
|
|
139
|
+
},
|
|
140
|
+
serverInfo: SERVER_INFO,
|
|
141
|
+
instructions: 'SentinelOne MCP server providing PowerQuery, Mgmt Console API, SDL API, and Hyperautomation tools. Load the "soc_analyst" prompt at session start for full operating context.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'ping': {
|
|
146
|
+
return ok(id, {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case 'resources/list': {
|
|
150
|
+
return ok(id, { resources: RESOURCES });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'resources/read': {
|
|
154
|
+
const uri = params?.uri;
|
|
155
|
+
if (uri === 'sentinelone://soc-context') {
|
|
156
|
+
return ok(id, {
|
|
157
|
+
contents: [{ uri, mimeType: 'text/markdown', text: SOC_CONTEXT }],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (uri === 'sentinelone://credentials-status') {
|
|
161
|
+
const c = getCreds();
|
|
162
|
+
const status = {
|
|
163
|
+
s1MgmtApi: {
|
|
164
|
+
configured: hasS1Creds(),
|
|
165
|
+
consoleUrl: c.S1_CONSOLE_URL ? c.S1_CONSOLE_URL.replace(/https?:\/\//, '').split('.')[0] + '...' : 'NOT SET',
|
|
166
|
+
tokenPresent: !!c.S1_CONSOLE_API_TOKEN,
|
|
167
|
+
},
|
|
168
|
+
sdlApi: {
|
|
169
|
+
configured: hasSdlCreds(),
|
|
170
|
+
xdrUrl: c.SDL_XDR_URL || 'NOT SET',
|
|
171
|
+
configWriteKey: !!c.SDL_CONFIG_WRITE_KEY,
|
|
172
|
+
logWriteKey: !!c.SDL_LOG_WRITE_KEY,
|
|
173
|
+
},
|
|
174
|
+
uamIngestApi: {
|
|
175
|
+
configured: hasHecCreds(),
|
|
176
|
+
hecUrl: c.S1_HEC_INGEST_URL || 'NOT SET (add S1_HEC_INGEST_URL to credentials.json)',
|
|
177
|
+
tokenPresent: !!c.S1_CONSOLE_API_TOKEN,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
return ok(id, {
|
|
181
|
+
contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(status, null, 2) }],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return err(id, -32002, `Resource not found: ${uri}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case 'prompts/list': {
|
|
188
|
+
return ok(id, { prompts: PROMPTS });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'prompts/get': {
|
|
192
|
+
const name = params?.name;
|
|
193
|
+
|
|
194
|
+
if (name === 'soc_analyst') {
|
|
195
|
+
return ok(id, {
|
|
196
|
+
description: 'Principal SOC Analyst operating instructions from CLAUDE.md',
|
|
197
|
+
messages: [
|
|
198
|
+
{
|
|
199
|
+
role: 'user',
|
|
200
|
+
content: {
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: `You are operating as a Principal SOC Analyst. Load and follow the instructions below precisely.\n\n${SOC_CONTEXT}`,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (name === 'session_init') {
|
|
210
|
+
return ok(id, {
|
|
211
|
+
description: 'Structured session initialization',
|
|
212
|
+
messages: [
|
|
213
|
+
{
|
|
214
|
+
role: 'user',
|
|
215
|
+
content: {
|
|
216
|
+
type: 'text',
|
|
217
|
+
text: `Begin a new SOC analyst session. Follow this initialization sequence:
|
|
218
|
+
|
|
219
|
+
1. Call \`powerquery_enumerate_sources\` to discover active SDL data sources (MANDATORY — never assume sources from prior sessions).
|
|
220
|
+
2. In parallel, call \`uam_list_alerts\` with filter="status=OPEN" to pull active alerts.
|
|
221
|
+
3. For each discovered data source not already in the schema registry, plan schema discovery via \`powerquery_schema_discover\`.
|
|
222
|
+
4. Report: (a) active data sources list, (b) open alert count and top 5 by severity, (c) which sources need schema discovery.
|
|
223
|
+
|
|
224
|
+
Apply the SOC analyst context from the soc_analyst prompt throughout.`,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return err(id, -32002, `Prompt not found: ${name}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'tools/list': {
|
|
235
|
+
return ok(id, { tools: TOOL_DEFS });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'tools/call': {
|
|
239
|
+
const toolName = params?.name;
|
|
240
|
+
const args = params?.arguments || {};
|
|
241
|
+
|
|
242
|
+
if (!toolName) {
|
|
243
|
+
return err(id, -32602, 'Missing tool name');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const handler = HANDLERS[toolName];
|
|
247
|
+
if (!handler) {
|
|
248
|
+
return err(id, -32602, `Tool not found: ${toolName}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const output = await handler(args);
|
|
253
|
+
const text = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
|
|
254
|
+
return ok(id, {
|
|
255
|
+
content: [{ type: 'text', text }],
|
|
256
|
+
isError: false,
|
|
257
|
+
});
|
|
258
|
+
} catch (e) {
|
|
259
|
+
log(`Tool error [${toolName}]:`, e.message);
|
|
260
|
+
return ok(id, {
|
|
261
|
+
content: [{ type: 'text', text: `Error: ${e.message}` }],
|
|
262
|
+
isError: true,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Notifications have no id — just ignore
|
|
268
|
+
case 'notifications/initialized':
|
|
269
|
+
case 'initialized':
|
|
270
|
+
return null;
|
|
271
|
+
|
|
272
|
+
default: {
|
|
273
|
+
if (id !== undefined) {
|
|
274
|
+
return err(id, -32601, `Method not found: ${method}`);
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Main loop ────────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async function main() {
|
|
284
|
+
log(`Starting (node ${process.version})`);
|
|
285
|
+
|
|
286
|
+
const creds = getCreds();
|
|
287
|
+
log(`S1 Mgmt API: ${hasS1Creds() ? 'configured (' + creds.S1_CONSOLE_URL + ')' : 'NOT configured'}`);
|
|
288
|
+
log(`SDL API: ${hasSdlCreds() ? 'configured (' + creds.SDL_XDR_URL + ')' : 'NOT configured'}`);
|
|
289
|
+
log(`UAM Ingest: ${hasHecCreds() ? 'configured (' + creds.S1_HEC_INGEST_URL + ')' : 'NOT configured (add S1_HEC_INGEST_URL to credentials.json)'}`);
|
|
290
|
+
log(`Tools: ${ALL_TOOLS.length} registered`);
|
|
291
|
+
|
|
292
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
293
|
+
|
|
294
|
+
// Track in-flight async requests so we don't exit while one is still running
|
|
295
|
+
let inFlight = 0;
|
|
296
|
+
let stdinClosed = false;
|
|
297
|
+
|
|
298
|
+
function maybeExit() {
|
|
299
|
+
if (stdinClosed && inFlight === 0) {
|
|
300
|
+
log('All requests complete, exiting.');
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
rl.on('line', async (line) => {
|
|
306
|
+
const trimmed = line.trim();
|
|
307
|
+
if (!trimmed) return;
|
|
308
|
+
|
|
309
|
+
let msg;
|
|
310
|
+
try {
|
|
311
|
+
msg = JSON.parse(trimmed);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
send(err(null, -32700, `Parse error: ${e.message}`));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Notification (no id) — handle but don't respond
|
|
318
|
+
const isNotification = msg.id === undefined;
|
|
319
|
+
|
|
320
|
+
inFlight++;
|
|
321
|
+
try {
|
|
322
|
+
const response = await dispatch(msg.method, msg.params, msg.id);
|
|
323
|
+
if (response !== null && !isNotification) {
|
|
324
|
+
send(response);
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
log('Unhandled dispatch error:', e.message, e.stack);
|
|
328
|
+
if (!isNotification) {
|
|
329
|
+
send(err(msg.id ?? null, -32603, `Internal error: ${e.message}`));
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
inFlight--;
|
|
333
|
+
maybeExit();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
rl.on('close', () => {
|
|
338
|
+
log('stdin closed, waiting for in-flight requests...');
|
|
339
|
+
stdinClosed = true;
|
|
340
|
+
maybeExit();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
process.on('SIGINT', () => {
|
|
344
|
+
log('SIGINT received, exiting.');
|
|
345
|
+
process.exit(0);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main().catch(e => {
|
|
350
|
+
process.stderr.write(`Fatal: ${e.message}\n${e.stack}\n`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential loader — zero dependencies, synchronous.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order (highest wins):
|
|
5
|
+
* 1. Environment variables
|
|
6
|
+
* 2. COWORK_WORKSPACE/credentials.json
|
|
7
|
+
* 3. Walk-up from cwd looking for credentials.json
|
|
8
|
+
* 4. ~/mnt/<any-folder>/credentials.json (Cowork workspace mounts)
|
|
9
|
+
* 5. CLAUDE_CONFIG_DIR/sentinelone/credentials.json
|
|
10
|
+
* 6. ~/.config/sentinelone/credentials.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
|
|
17
|
+
const CRED_FILENAMES = [
|
|
18
|
+
'credentials.json',
|
|
19
|
+
'.sentinelone/credentials.json',
|
|
20
|
+
'.claude/sentinelone/credentials.json',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const MNT_SKIP = new Set(['.claude', '.auto-memory', '.remote-plugins', 'outputs', 'uploads']);
|
|
24
|
+
|
|
25
|
+
function tryLoad(dir) {
|
|
26
|
+
for (const rel of CRED_FILENAMES) {
|
|
27
|
+
const p = join(dir, rel);
|
|
28
|
+
if (existsSync(p)) {
|
|
29
|
+
try { return JSON.parse(readFileSync(p, 'utf-8')); } catch { /* bad JSON */ }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function discoverCredentials() {
|
|
36
|
+
// 1. COWORK_WORKSPACE env override
|
|
37
|
+
const ws = process.env.COWORK_WORKSPACE;
|
|
38
|
+
if (ws) {
|
|
39
|
+
const found = tryLoad(ws);
|
|
40
|
+
if (found) return found;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Walk up from cwd
|
|
44
|
+
let dir;
|
|
45
|
+
try { dir = process.cwd(); } catch { dir = '/'; }
|
|
46
|
+
for (let i = 0; i < 20; i++) {
|
|
47
|
+
const found = tryLoad(dir);
|
|
48
|
+
if (found) return found;
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir) break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. ~/mnt/* scan (Cowork workspace mounts)
|
|
55
|
+
const homeMnt = join(homedir(), 'mnt');
|
|
56
|
+
if (existsSync(homeMnt)) {
|
|
57
|
+
try {
|
|
58
|
+
const entries = readdirSync(homeMnt, { withFileTypes: true });
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
if (!e.isDirectory() || MNT_SKIP.has(e.name)) continue;
|
|
61
|
+
const found = tryLoad(join(homeMnt, e.name));
|
|
62
|
+
if (found) return found;
|
|
63
|
+
}
|
|
64
|
+
} catch { /* skip */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. CLAUDE_CONFIG_DIR plugin creds
|
|
68
|
+
const ccDir = process.env.CLAUDE_CONFIG_DIR;
|
|
69
|
+
if (ccDir) {
|
|
70
|
+
const p = join(ccDir, 'sentinelone', 'credentials.json');
|
|
71
|
+
if (existsSync(p)) {
|
|
72
|
+
try { return JSON.parse(readFileSync(p, 'utf-8')); } catch { /* bad JSON */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 5. ~/.config/sentinelone/credentials.json
|
|
77
|
+
const configPath = join(homedir(), '.config', 'sentinelone', 'credentials.json');
|
|
78
|
+
if (existsSync(configPath)) {
|
|
79
|
+
try { return JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* bad JSON */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Load once at module init
|
|
86
|
+
const _file = discoverCredentials();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns merged credentials. Environment variables take precedence over file values.
|
|
90
|
+
*/
|
|
91
|
+
export function getCreds() {
|
|
92
|
+
const e = (key) => process.env[key] || _file[key] || '';
|
|
93
|
+
return {
|
|
94
|
+
S1_CONSOLE_URL: e('S1_CONSOLE_URL'),
|
|
95
|
+
S1_CONSOLE_API_TOKEN: e('S1_CONSOLE_API_TOKEN') || e('S1_API_TOKEN'),
|
|
96
|
+
S1_HEC_INGEST_URL: e('S1_HEC_INGEST_URL'),
|
|
97
|
+
SDL_XDR_URL: e('SDL_XDR_URL') || e('SDL_BASE_URL'),
|
|
98
|
+
SDL_LOG_WRITE_KEY: e('SDL_LOG_WRITE_KEY'),
|
|
99
|
+
SDL_CONFIG_WRITE_KEY: e('SDL_CONFIG_WRITE_KEY'),
|
|
100
|
+
SDL_CONFIG_READ_KEY: e('SDL_CONFIG_READ_KEY'),
|
|
101
|
+
SDL_LOG_READ_KEY: e('SDL_LOG_READ_KEY'),
|
|
102
|
+
VT_API_KEY: e('VT_API_KEY'),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** True if minimum required credentials for S1 Mgmt API are present. */
|
|
107
|
+
export function hasS1Creds() {
|
|
108
|
+
const c = getCreds();
|
|
109
|
+
return !!(c.S1_CONSOLE_URL && c.S1_CONSOLE_API_TOKEN);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** True if minimum required credentials for SDL are present. */
|
|
113
|
+
export function hasSdlCreds() {
|
|
114
|
+
const c = getCreds();
|
|
115
|
+
return !!(c.SDL_XDR_URL && (c.SDL_CONFIG_WRITE_KEY || c.S1_CONSOLE_API_TOKEN));
|
|
116
|
+
}
|