@pmoses-s1/sentinelone-mcp 1.0.0 → 1.1.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/CHANGELOG.md +44 -0
- package/README.md +187 -115
- package/deploy/README.md +277 -0
- package/deploy/caddy/Caddyfile.example +99 -0
- package/deploy/install.sh +265 -0
- package/deploy/systemd/sentinelone-mcp.service +57 -0
- package/index.js +136 -312
- package/lib/auth.js +161 -0
- package/lib/credentials.js +18 -6
- package/lib/http-transport.js +223 -0
- package/lib/s1.js +1 -1
- package/lib/server-core.js +265 -0
- package/lib/stdio-transport.js +77 -0
- package/lib/uam-ingest.js +1 -1
- package/package.json +10 -4
- package/scripts/regen-readme-tools-table.mjs +142 -0
- package/scripts/test-mac.sh +179 -0
- package/tools/hyperautomation.js +1 -1
package/deploy/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Deployment guide
|
|
2
|
+
|
|
3
|
+
Three supported topologies, in order of complexity.
|
|
4
|
+
|
|
5
|
+
| Topology | Who runs it | Transport | Auth | Use this when |
|
|
6
|
+
|---|---|---|---|---|
|
|
7
|
+
| **A. Single user, local** | One human | stdio | none | You use Claude Desktop / Claude Code / Claude Cowork on your own Mac or Linux laptop. |
|
|
8
|
+
| **B. Single user, HTTP** | One human | Streamable HTTP, `127.0.0.1` only | none | You want one server you can curl, or have a non-Claude client that speaks Streamable HTTP. |
|
|
9
|
+
| **C. Team, VM-hosted** | Many humans | Streamable HTTP, behind TLS | per-user bearer tokens | You want N team members to share one server with one set of SentinelOne credentials, with per-user audit and revocation. |
|
|
10
|
+
|
|
11
|
+
## A. Single user, local (stdio)
|
|
12
|
+
|
|
13
|
+
`curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/sentinelone-mcp/deploy/install.sh | bash`
|
|
14
|
+
|
|
15
|
+
That runs `install.sh --user`, which:
|
|
16
|
+
1. Confirms Node 18+ is present (errors out with install hints if not).
|
|
17
|
+
2. Sets up a per-user npm prefix at `~/.npm-global` if one isn't configured.
|
|
18
|
+
3. Installs `@pmoses-s1/sentinelone-mcp` globally for your user.
|
|
19
|
+
4. Writes a credentials skeleton to `~/.config/sentinelone/credentials.json` (mode 0600).
|
|
20
|
+
5. Prints the next steps.
|
|
21
|
+
|
|
22
|
+
Then edit `~/.config/sentinelone/credentials.json` with your real values:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
27
|
+
"S1_CONSOLE_API_TOKEN": "eyJ...",
|
|
28
|
+
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net",
|
|
29
|
+
"SDL_XDR_URL": "https://xdr.us1.sentinelone.net",
|
|
30
|
+
"SDL_LOG_READ_KEY": "...",
|
|
31
|
+
"SDL_LOG_WRITE_KEY": "...",
|
|
32
|
+
"SDL_CONFIG_READ_KEY": "...",
|
|
33
|
+
"SDL_CONFIG_WRITE_KEY": "..."
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Add the server to Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` on Mac, or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"sentinelone-mcp": {
|
|
43
|
+
"command": "sentinelone-mcp"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or, equivalently, by package name without the install:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"sentinelone-mcp": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "@pmoses-s1/sentinelone-mcp@1.1.0"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Restart Claude Desktop. The server picks credentials up from `~/.config/sentinelone/credentials.json` automatically.
|
|
63
|
+
|
|
64
|
+
## B. Single user, HTTP
|
|
65
|
+
|
|
66
|
+
Same `install.sh --user`, then start the server in HTTP mode:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
sentinelone-mcp --transport http
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It binds to `127.0.0.1:8765` and runs with no auth (which is fine when the bind address is loopback and you're the only user on the box). Hit it with curl:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -s http://127.0.0.1:8765/healthz
|
|
76
|
+
# -> ok
|
|
77
|
+
|
|
78
|
+
curl -s -X POST http://127.0.0.1:8765/mcp \
|
|
79
|
+
-H 'Content-Type: application/json' \
|
|
80
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length'
|
|
81
|
+
# -> 26
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
In Claude Cowork or any MCP client that supports remote HTTP servers, add it:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"sentinelone-mcp": {
|
|
90
|
+
"type": "http",
|
|
91
|
+
"url": "http://127.0.0.1:8765/mcp"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## C. Team, VM-hosted (recommended for shared deployments)
|
|
98
|
+
|
|
99
|
+
This is the topology to use when more than one person should have access to the same SentinelOne tenant through MCP, without distributing the underlying S1 service-user token.
|
|
100
|
+
|
|
101
|
+
### What you'll end up with
|
|
102
|
+
|
|
103
|
+
- One Linux VM, reachable on your private network (or via Tailscale, WireGuard, etc.).
|
|
104
|
+
- One `mcp` system user owning `/etc/sentinelone-mcp/`.
|
|
105
|
+
- One `credentials.json` containing the S1 service-user token + SDL keys. Mode 0600, never copied off the box.
|
|
106
|
+
- One `bearer-tokens.json` listing per-user tokens, one per team member: `{"alice": "...", "bob": "...", "claire": "..."}`. Mode 0600. SIGHUP-reloadable.
|
|
107
|
+
- One systemd service running the MCP on `127.0.0.1:8765` with auth enforced.
|
|
108
|
+
- Caddy in front terminating TLS and forwarding to the backend.
|
|
109
|
+
|
|
110
|
+
Team members connect from their Claude clients with their own bearer token. Audit log identifies them by name. Revocation is one file edit + `systemctl reload`.
|
|
111
|
+
|
|
112
|
+
### Step-by-step
|
|
113
|
+
|
|
114
|
+
1. **Provision the VM.** Anything that runs systemd is fine: Ubuntu 22.04 LTS, Debian 12, Rocky/Alma 9, etc.
|
|
115
|
+
|
|
116
|
+
2. **Install Node 18+.** Pick one:
|
|
117
|
+
```bash
|
|
118
|
+
# Ubuntu / Debian
|
|
119
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
120
|
+
sudo apt install -y nodejs
|
|
121
|
+
```
|
|
122
|
+
```bash
|
|
123
|
+
# Rocky / Alma
|
|
124
|
+
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
|
|
125
|
+
sudo dnf install -y nodejs
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
3. **Run the installer in server mode:**
|
|
129
|
+
```bash
|
|
130
|
+
curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/sentinelone-mcp/deploy/install.sh | sudo bash -s -- --server
|
|
131
|
+
```
|
|
132
|
+
It creates the `mcp` user, drops `/etc/sentinelone-mcp/credentials.json` (placeholder) and `/etc/sentinelone-mcp/bearer-tokens.json` (one freshly-generated admin token, printed once to stdout), installs the systemd unit, and starts the service.
|
|
133
|
+
|
|
134
|
+
4. **Fill in real SentinelOne credentials:**
|
|
135
|
+
```bash
|
|
136
|
+
sudo vim /etc/sentinelone-mcp/credentials.json
|
|
137
|
+
sudo systemctl reload sentinelone-mcp
|
|
138
|
+
curl -s http://127.0.0.1:8765/healthz # -> ok
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
5. **Put TLS in front with Caddy** (the recommended option):
|
|
142
|
+
```bash
|
|
143
|
+
sudo apt install -y caddy
|
|
144
|
+
sudo cp /usr/lib/node_modules/@pmoses-s1/sentinelone-mcp/deploy/caddy/Caddyfile.example /etc/caddy/Caddyfile
|
|
145
|
+
sudo vim /etc/caddy/Caddyfile # change mcp.s1.internal to your DNS name
|
|
146
|
+
sudo systemctl reload caddy
|
|
147
|
+
```
|
|
148
|
+
Default Caddyfile uses `tls internal` which signs with Caddy's own CA. Distribute `/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt` to your team for trust, or use `tls <your-email>` with a publicly resolvable hostname for Let's Encrypt.
|
|
149
|
+
|
|
150
|
+
6. **Add team members.** Generate a token per person and append to the file:
|
|
151
|
+
```bash
|
|
152
|
+
sudo bash -c 'cat > /etc/sentinelone-mcp/bearer-tokens.json' <<EOF
|
|
153
|
+
{
|
|
154
|
+
"admin": "$(openssl rand -hex 32)",
|
|
155
|
+
"alice": "$(openssl rand -hex 32)",
|
|
156
|
+
"bob": "$(openssl rand -hex 32)",
|
|
157
|
+
"claire":"$(openssl rand -hex 32)"
|
|
158
|
+
}
|
|
159
|
+
EOF
|
|
160
|
+
sudo chmod 600 /etc/sentinelone-mcp/bearer-tokens.json
|
|
161
|
+
sudo chown mcp:mcp /etc/sentinelone-mcp/bearer-tokens.json
|
|
162
|
+
sudo systemctl reload sentinelone-mcp # SIGHUP, no downtime
|
|
163
|
+
```
|
|
164
|
+
Hand each person their token over a secure channel (1Password, Signal, etc.).
|
|
165
|
+
|
|
166
|
+
7. **Connect from a Claude client.** Each user adds the server to their config with their personal token:
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"mcpServers": {
|
|
170
|
+
"sentinelone-mcp": {
|
|
171
|
+
"type": "http",
|
|
172
|
+
"url": "https://mcp.s1.internal/mcp",
|
|
173
|
+
"headers": {
|
|
174
|
+
"Authorization": "Bearer <THEIR_PERSONAL_TOKEN>"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
8. **Verify end-to-end.** From a team member's machine:
|
|
182
|
+
```bash
|
|
183
|
+
curl -s -X POST https://mcp.s1.internal/mcp \
|
|
184
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
185
|
+
-H 'Content-Type: application/json' \
|
|
186
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length'
|
|
187
|
+
# -> 26
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
9. **Watch the audit log.** Every authenticated request is logged with the bearer name, method, and param summary:
|
|
191
|
+
```bash
|
|
192
|
+
sudo journalctl -u sentinelone-mcp -f | grep '\[audit\]'
|
|
193
|
+
# [audit] 2026-05-28T15:01:22.413Z | alice | tools/call | name=powerquery_run | 200 ok
|
|
194
|
+
# [audit] 2026-05-28T15:01:34.221Z | bob | tools/list | - | 200 ok
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Day-2 operations
|
|
198
|
+
|
|
199
|
+
### Adding a team member
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
sudo vim /etc/sentinelone-mcp/bearer-tokens.json # add new {"name": "token"}
|
|
203
|
+
sudo systemctl reload sentinelone-mcp # SIGHUP, no downtime
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Revoking access
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
sudo vim /etc/sentinelone-mcp/bearer-tokens.json # remove the entry
|
|
210
|
+
sudo systemctl reload sentinelone-mcp
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Rotating the SentinelOne service-user token
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
sudo vim /etc/sentinelone-mcp/credentials.json # paste new S1_CONSOLE_API_TOKEN
|
|
217
|
+
sudo systemctl restart sentinelone-mcp # full restart needed for creds
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Upgrading the MCP server
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
sudo npm install -g @pmoses-s1/sentinelone-mcp@<new-version>
|
|
224
|
+
sudo systemctl restart sentinelone-mcp
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Reading the audit log
|
|
228
|
+
|
|
229
|
+
The structured audit lines look like:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
[audit] 2026-05-28T15:01:22.413Z | alice | tools/call | name=powerquery_run | 200 ok
|
|
233
|
+
[audit] 2026-05-28T16:42:55.108Z | bob | tools/list | - | 200 ok
|
|
234
|
+
[audit] 2026-05-28T17:03:11.221Z | - | - | - | 401 unauthorized
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Quick filters:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# everything alice did in the last hour
|
|
241
|
+
sudo journalctl -u sentinelone-mcp --since="1 hour ago" | grep '\[audit\].*| alice |'
|
|
242
|
+
|
|
243
|
+
# all unauthorized attempts today
|
|
244
|
+
sudo journalctl -u sentinelone-mcp --since=today | grep '\[audit\].*401'
|
|
245
|
+
|
|
246
|
+
# all tool calls (not just listings)
|
|
247
|
+
sudo journalctl -u sentinelone-mcp -f | grep 'tools/call'
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Health and readiness
|
|
251
|
+
|
|
252
|
+
`GET /healthz` returns `200 ok` whenever the server is accepting connections. Use it for load balancer probes and for `systemctl-aware` orchestrators:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
curl -s http://127.0.0.1:8765/healthz # behind the proxy
|
|
256
|
+
curl -s https://mcp.s1.internal/healthz # in front of the proxy
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Troubleshooting
|
|
260
|
+
|
|
261
|
+
| Symptom | Likely cause | Fix |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `Connection refused` on `127.0.0.1:8765` | Service not running | `sudo systemctl status sentinelone-mcp`; check `journalctl -u sentinelone-mcp -n 50`. |
|
|
264
|
+
| 401 on every request | No bearer token, or wrong one | Confirm `Authorization: Bearer <token>` is set; confirm the token is in `/etc/sentinelone-mcp/bearer-tokens.json`. |
|
|
265
|
+
| `tools/call` returns `Error: connect ECONNREFUSED` to `*.sentinelone.net` | S1 creds missing or VM has no outbound to console | `curl -v https://$YOUR_CONSOLE_URL`; check `/etc/sentinelone-mcp/credentials.json`. |
|
|
266
|
+
| Service starts but `Tools: 0 registered` | Code/import error | `journalctl -u sentinelone-mcp -n 100` for the import stack trace. |
|
|
267
|
+
| `502 Bad Gateway` from Caddy | Backend died between Caddy reload and proxy attempt | `systemctl status sentinelone-mcp`. |
|
|
268
|
+
|
|
269
|
+
## Alternative deployments
|
|
270
|
+
|
|
271
|
+
These are supported but not first-class:
|
|
272
|
+
|
|
273
|
+
- **Docker / docker-compose.** Not shipped in this version. The single-file Node binary doesn't need it. If you want a container, the install is `FROM node:20-alpine` + `RUN npm install -g @pmoses-s1/sentinelone-mcp@1.1.0` + `CMD ["sentinelone-mcp", "--transport", "http", "--host", "0.0.0.0"]`. Mount creds at `/etc/sentinelone-mcp/credentials.json` and tokens at `/etc/sentinelone-mcp/bearer-tokens.json`.
|
|
274
|
+
|
|
275
|
+
- **External bridge (`supergateway`, `mcp-proxy`).** Pre-1.1.0 deployments used these to wrap the stdio-only server. They still work; this server's native HTTP mode is functionally equivalent and removes the extra process. Prefer native unless you have a specific reason.
|
|
276
|
+
|
|
277
|
+
- **No-auth HTTP on a non-loopback bind.** Possible (set `--host 0.0.0.0` and omit `MCP_BEARER_TOKENS*`) but the server logs a loud warning at startup. Only use if the network itself is trusted (e.g. a Tailscale-only LAN where every node is authenticated upstream).
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Caddyfile for sentinelone-mcp behind TLS on a private network.
|
|
2
|
+
#
|
|
3
|
+
# Two adjustments before using:
|
|
4
|
+
# 1. Replace mcp.s1.internal with your DNS name (or Tailscale hostname).
|
|
5
|
+
# 2. Decide on a TLS strategy:
|
|
6
|
+
# tls internal Caddy's built-in CA (good for private networks;
|
|
7
|
+
# distribute the root cert to clients).
|
|
8
|
+
# tls <email> Let's Encrypt over HTTP-01 (needs public DNS + port 80).
|
|
9
|
+
# tls /path/to/cert.pem /path/to/key.pem
|
|
10
|
+
# Bring your own cert from an internal PKI.
|
|
11
|
+
#
|
|
12
|
+
# Why Caddy and not nginx? Caddy auto-renews certs, has zero config for SSE
|
|
13
|
+
# streaming (flush_interval handled), and is one binary with no system Python
|
|
14
|
+
# or Lua dependency. Equally valid alternative: nginx with the snippet at the
|
|
15
|
+
# bottom of this file.
|
|
16
|
+
|
|
17
|
+
mcp.s1.internal {
|
|
18
|
+
tls internal
|
|
19
|
+
|
|
20
|
+
# Strict bearer-token enforcement. The MCP server also enforces tokens
|
|
21
|
+
# when MCP_BEARER_TOKENS_FILE is set, but checking here too means we never
|
|
22
|
+
# forward unauthenticated traffic to the backend.
|
|
23
|
+
#
|
|
24
|
+
# To bypass Caddy auth (and rely solely on the MCP server's enforcement),
|
|
25
|
+
# delete the @authorized matcher and the handle blocks below, leaving only
|
|
26
|
+
# the reverse_proxy.
|
|
27
|
+
@anyAuth header_regexp Authorization "^Bearer\s+\S+$"
|
|
28
|
+
|
|
29
|
+
handle @anyAuth {
|
|
30
|
+
reverse_proxy 127.0.0.1:8765 {
|
|
31
|
+
# MCP responses are streamed; flush immediately so partial replies
|
|
32
|
+
# reach the client without sitting in a buffer.
|
|
33
|
+
flush_interval -1
|
|
34
|
+
|
|
35
|
+
# Reasonable timeouts for long-running PowerQueries (LRQ can take
|
|
36
|
+
# ~30s on heavy queries). Increase if you see 504s on big queries.
|
|
37
|
+
transport http {
|
|
38
|
+
read_timeout 90s
|
|
39
|
+
write_timeout 90s
|
|
40
|
+
response_header_timeout 30s
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handle {
|
|
46
|
+
respond "unauthorized" 401 {
|
|
47
|
+
close
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Standard security headers.
|
|
52
|
+
header {
|
|
53
|
+
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
54
|
+
X-Content-Type-Options "nosniff"
|
|
55
|
+
Referrer-Policy "no-referrer"
|
|
56
|
+
-Server
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Healthcheck pass-through (no auth needed on /healthz).
|
|
60
|
+
@health path /healthz /health
|
|
61
|
+
handle @health {
|
|
62
|
+
reverse_proxy 127.0.0.1:8765
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Access log to journald via systemd-journald, structured JSON.
|
|
66
|
+
log {
|
|
67
|
+
output stdout
|
|
68
|
+
format json
|
|
69
|
+
level INFO
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ─── nginx equivalent (commented; copy if you prefer nginx) ──────────────────
|
|
74
|
+
# upstream mcp_backend {
|
|
75
|
+
# server 127.0.0.1:8765;
|
|
76
|
+
# keepalive 16;
|
|
77
|
+
# }
|
|
78
|
+
# server {
|
|
79
|
+
# listen 443 ssl http2;
|
|
80
|
+
# server_name mcp.s1.internal;
|
|
81
|
+
# ssl_certificate /etc/ssl/certs/mcp.s1.internal.crt;
|
|
82
|
+
# ssl_certificate_key /etc/ssl/private/mcp.s1.internal.key;
|
|
83
|
+
#
|
|
84
|
+
# location = /healthz {
|
|
85
|
+
# proxy_pass http://mcp_backend;
|
|
86
|
+
# }
|
|
87
|
+
#
|
|
88
|
+
# location / {
|
|
89
|
+
# if ($http_authorization !~ "^Bearer\s+\S+$") {
|
|
90
|
+
# return 401;
|
|
91
|
+
# }
|
|
92
|
+
# proxy_pass http://mcp_backend;
|
|
93
|
+
# proxy_http_version 1.1;
|
|
94
|
+
# proxy_set_header Connection "";
|
|
95
|
+
# proxy_buffering off;
|
|
96
|
+
# proxy_read_timeout 90s;
|
|
97
|
+
# proxy_send_timeout 90s;
|
|
98
|
+
# }
|
|
99
|
+
# }
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# sentinelone-mcp installer for macOS and Linux.
|
|
4
|
+
#
|
|
5
|
+
# Modes:
|
|
6
|
+
# --user (default) Install for the current user only.
|
|
7
|
+
# Writes credentials to ~/.config/sentinelone/credentials.json,
|
|
8
|
+
# installs the npm package globally via the current Node toolchain.
|
|
9
|
+
#
|
|
10
|
+
# --server Linux VM deployment. Creates a system `mcp` user, installs the
|
|
11
|
+
# npm package globally, writes credentials and bearer tokens to
|
|
12
|
+
# /etc/sentinelone-mcp/, drops the systemd unit, enables and
|
|
13
|
+
# starts the service.
|
|
14
|
+
#
|
|
15
|
+
# Idempotent: rerunning is safe; it skips steps already completed.
|
|
16
|
+
#
|
|
17
|
+
# Exit codes: 0 ok, 1 generic failure, 2 unsupported platform, 3 missing prereq.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# ─── helpers ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
|
24
|
+
c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
|
25
|
+
c_yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
|
|
26
|
+
c_bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
27
|
+
|
|
28
|
+
step() { c_bold ">> $*"; }
|
|
29
|
+
ok() { c_green " ok: $*"; }
|
|
30
|
+
warn() { c_yellow " warn: $*"; }
|
|
31
|
+
die() { c_red " error: $*"; exit 1; }
|
|
32
|
+
|
|
33
|
+
PKG="@pmoses-s1/sentinelone-mcp"
|
|
34
|
+
MODE="user"
|
|
35
|
+
|
|
36
|
+
while [[ $# -gt 0 ]]; do
|
|
37
|
+
case "$1" in
|
|
38
|
+
--user) MODE="user"; shift ;;
|
|
39
|
+
--server) MODE="server"; shift ;;
|
|
40
|
+
-h|--help)
|
|
41
|
+
cat <<EOF
|
|
42
|
+
Usage: $0 [--user|--server]
|
|
43
|
+
|
|
44
|
+
--user Install for current user (default).
|
|
45
|
+
Default install path on macOS: ~/.config/sentinelone/
|
|
46
|
+
Default install path on Linux: ~/.config/sentinelone/
|
|
47
|
+
|
|
48
|
+
--server Install on a Linux VM as a shared service.
|
|
49
|
+
System path: /etc/sentinelone-mcp/
|
|
50
|
+
systemd unit: sentinelone-mcp.service
|
|
51
|
+
Requires sudo.
|
|
52
|
+
|
|
53
|
+
EOF
|
|
54
|
+
exit 0 ;;
|
|
55
|
+
*) die "Unknown flag: $1 (try --help)" ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
OS="$(uname -s)"
|
|
60
|
+
case "$OS" in
|
|
61
|
+
Darwin) PLATFORM="mac" ;;
|
|
62
|
+
Linux) PLATFORM="linux" ;;
|
|
63
|
+
*) c_red "Unsupported platform: $OS"; exit 2 ;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
if [[ "$MODE" == "server" && "$PLATFORM" != "linux" ]]; then
|
|
67
|
+
die "--server mode is Linux only (got $PLATFORM)"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ─── prereqs ─────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
step "Checking prerequisites"
|
|
73
|
+
|
|
74
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
75
|
+
c_red "Node.js is required but not found on PATH."
|
|
76
|
+
c_red "Install Node 18+:"
|
|
77
|
+
c_red " macOS: brew install node@20"
|
|
78
|
+
c_red " Linux: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
|
|
79
|
+
exit 3
|
|
80
|
+
fi
|
|
81
|
+
NODE_MAJOR="$(node --version | sed 's/^v\([0-9]*\).*/\1/')"
|
|
82
|
+
if [[ "$NODE_MAJOR" -lt 18 ]]; then
|
|
83
|
+
die "Node $(node --version) is too old. Need Node 18+."
|
|
84
|
+
fi
|
|
85
|
+
ok "node $(node --version)"
|
|
86
|
+
|
|
87
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
88
|
+
die "npm not found alongside node; please install Node 18+ from nodejs.org or your package manager."
|
|
89
|
+
fi
|
|
90
|
+
ok "npm $(npm --version)"
|
|
91
|
+
|
|
92
|
+
if [[ "$MODE" == "server" ]]; then
|
|
93
|
+
if [[ "$EUID" -ne 0 ]]; then
|
|
94
|
+
die "--server mode must be run with sudo (need to create /etc/sentinelone-mcp/, system user, and systemd unit)."
|
|
95
|
+
fi
|
|
96
|
+
command -v systemctl >/dev/null 2>&1 || die "systemctl not found; this script targets systemd-based Linux."
|
|
97
|
+
ok "running as root, systemd present"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ─── install package ─────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
step "Installing $PKG globally"
|
|
103
|
+
if [[ "$MODE" == "server" ]]; then
|
|
104
|
+
npm install -g "$PKG" >/dev/null
|
|
105
|
+
else
|
|
106
|
+
# Avoid sudo on Mac/personal Linux: use a per-user npm prefix if not already.
|
|
107
|
+
if ! npm config get prefix --location=user 2>/dev/null | grep -qE '^/'; then
|
|
108
|
+
npm config set prefix "$HOME/.npm-global"
|
|
109
|
+
case ":$PATH:" in
|
|
110
|
+
*":$HOME/.npm-global/bin:"*) ;;
|
|
111
|
+
*) warn "Add $HOME/.npm-global/bin to your PATH (currently missing)." ;;
|
|
112
|
+
esac
|
|
113
|
+
fi
|
|
114
|
+
npm install -g "$PKG" >/dev/null
|
|
115
|
+
fi
|
|
116
|
+
ok "$(npm ls -g --depth=0 "$PKG" 2>/dev/null | grep "$PKG" | head -1 | sed 's/.*-> //' || echo installed)"
|
|
117
|
+
|
|
118
|
+
# ─── credentials skeleton ────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
if [[ "$MODE" == "server" ]]; then
|
|
121
|
+
CONF_DIR="/etc/sentinelone-mcp"
|
|
122
|
+
CRED_PATH="$CONF_DIR/credentials.json"
|
|
123
|
+
TOKEN_PATH="$CONF_DIR/bearer-tokens.json"
|
|
124
|
+
ENV_PATH="$CONF_DIR/server.env"
|
|
125
|
+
OWNER="mcp"
|
|
126
|
+
else
|
|
127
|
+
CONF_DIR="$HOME/.config/sentinelone"
|
|
128
|
+
CRED_PATH="$CONF_DIR/credentials.json"
|
|
129
|
+
TOKEN_PATH=""
|
|
130
|
+
ENV_PATH=""
|
|
131
|
+
OWNER="$USER"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
step "Setting up $CONF_DIR"
|
|
135
|
+
mkdir -p "$CONF_DIR"
|
|
136
|
+
if [[ ! -f "$CRED_PATH" ]]; then
|
|
137
|
+
cat > "$CRED_PATH" <<'EOF'
|
|
138
|
+
{
|
|
139
|
+
"S1_CONSOLE_URL": "https://usea1-acme.sentinelone.net",
|
|
140
|
+
"S1_CONSOLE_API_TOKEN": "REPLACE_WITH_API_TOKEN",
|
|
141
|
+
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net",
|
|
142
|
+
"SDL_XDR_URL": "https://xdr.us1.sentinelone.net",
|
|
143
|
+
"SDL_LOG_READ_KEY": "",
|
|
144
|
+
"SDL_LOG_WRITE_KEY": "",
|
|
145
|
+
"SDL_CONFIG_READ_KEY": "",
|
|
146
|
+
"SDL_CONFIG_WRITE_KEY": ""
|
|
147
|
+
}
|
|
148
|
+
EOF
|
|
149
|
+
chmod 600 "$CRED_PATH"
|
|
150
|
+
ok "wrote $CRED_PATH (placeholder, edit before starting)"
|
|
151
|
+
else
|
|
152
|
+
ok "$CRED_PATH already exists, leaving untouched"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if [[ "$MODE" == "server" ]]; then
|
|
156
|
+
if ! id "$OWNER" >/dev/null 2>&1; then
|
|
157
|
+
step "Creating system user '$OWNER'"
|
|
158
|
+
useradd --system --no-create-home --shell /usr/sbin/nologin "$OWNER"
|
|
159
|
+
ok "created"
|
|
160
|
+
else
|
|
161
|
+
ok "user '$OWNER' already exists"
|
|
162
|
+
fi
|
|
163
|
+
chown -R "$OWNER":"$OWNER" "$CONF_DIR"
|
|
164
|
+
chmod 600 "$CRED_PATH"
|
|
165
|
+
|
|
166
|
+
if [[ ! -f "$TOKEN_PATH" ]]; then
|
|
167
|
+
step "Generating initial bearer token"
|
|
168
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
169
|
+
TOKEN_ADMIN="$(openssl rand -hex 32)"
|
|
170
|
+
else
|
|
171
|
+
TOKEN_ADMIN="$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')"
|
|
172
|
+
fi
|
|
173
|
+
cat > "$TOKEN_PATH" <<EOF
|
|
174
|
+
{
|
|
175
|
+
"admin": "$TOKEN_ADMIN"
|
|
176
|
+
}
|
|
177
|
+
EOF
|
|
178
|
+
chmod 600 "$TOKEN_PATH"
|
|
179
|
+
chown "$OWNER":"$OWNER" "$TOKEN_PATH"
|
|
180
|
+
ok "wrote $TOKEN_PATH (one initial admin token)"
|
|
181
|
+
c_yellow " INITIAL ADMIN BEARER TOKEN:"
|
|
182
|
+
c_yellow " $TOKEN_ADMIN"
|
|
183
|
+
c_yellow " Save this value now; it is also stored in $TOKEN_PATH."
|
|
184
|
+
else
|
|
185
|
+
ok "$TOKEN_PATH already exists"
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
if [[ ! -f "$ENV_PATH" ]]; then
|
|
189
|
+
cat > "$ENV_PATH" <<EOF
|
|
190
|
+
# Environment file for sentinelone-mcp.service.
|
|
191
|
+
# Adjust LOG_LEVEL or override anything here; reload with: systemctl reload sentinelone-mcp
|
|
192
|
+
EOF
|
|
193
|
+
chmod 600 "$ENV_PATH"
|
|
194
|
+
chown "$OWNER":"$OWNER" "$ENV_PATH"
|
|
195
|
+
ok "wrote $ENV_PATH"
|
|
196
|
+
else
|
|
197
|
+
ok "$ENV_PATH already exists"
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
step "Installing systemd unit"
|
|
201
|
+
SVC_PATH="/etc/systemd/system/sentinelone-mcp.service"
|
|
202
|
+
GLOBAL_NODE_MODULES="$(npm root -g)"
|
|
203
|
+
SCRIPT_DIR="$GLOBAL_NODE_MODULES/$PKG"
|
|
204
|
+
# Rewrite the ExecStart path to point at the resolved global install,
|
|
205
|
+
# since the bundled unit uses %h which assumes per-user install.
|
|
206
|
+
sed "s|%h/.npm-global/lib/node_modules/@pmoses-s1/sentinelone-mcp|$SCRIPT_DIR|g" \
|
|
207
|
+
"$SCRIPT_DIR/deploy/systemd/sentinelone-mcp.service" > "$SVC_PATH"
|
|
208
|
+
systemctl daemon-reload
|
|
209
|
+
systemctl enable sentinelone-mcp >/dev/null 2>&1
|
|
210
|
+
ok "wrote $SVC_PATH and enabled the service"
|
|
211
|
+
|
|
212
|
+
step "Starting the service"
|
|
213
|
+
if systemctl is-active sentinelone-mcp >/dev/null 2>&1; then
|
|
214
|
+
systemctl restart sentinelone-mcp
|
|
215
|
+
ok "restarted"
|
|
216
|
+
else
|
|
217
|
+
systemctl start sentinelone-mcp
|
|
218
|
+
ok "started"
|
|
219
|
+
fi
|
|
220
|
+
sleep 1
|
|
221
|
+
if systemctl is-active --quiet sentinelone-mcp; then
|
|
222
|
+
ok "service is active"
|
|
223
|
+
else
|
|
224
|
+
c_red "service failed to start. Recent log lines:"
|
|
225
|
+
journalctl -u sentinelone-mcp -n 30 --no-pager | sed 's/^/ /'
|
|
226
|
+
exit 1
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# ─── final notes ─────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
step "Next steps"
|
|
233
|
+
if [[ "$MODE" == "user" ]]; then
|
|
234
|
+
cat <<EOF
|
|
235
|
+
|
|
236
|
+
1. Edit $CRED_PATH with your real SentinelOne values.
|
|
237
|
+
2. Try the server:
|
|
238
|
+
sentinelone-mcp --version
|
|
239
|
+
sentinelone-mcp --help
|
|
240
|
+
3. Wire it into Claude Cowork / Claude Desktop / Claude Code via stdio
|
|
241
|
+
(no HTTP needed for single-user local). See deploy/README.md for the
|
|
242
|
+
exact config block.
|
|
243
|
+
|
|
244
|
+
EOF
|
|
245
|
+
elif [[ "$MODE" == "server" ]]; then
|
|
246
|
+
cat <<EOF
|
|
247
|
+
|
|
248
|
+
1. Edit $CRED_PATH with your real SentinelOne values, then reload:
|
|
249
|
+
sudo systemctl reload sentinelone-mcp
|
|
250
|
+
2. Verify the server is up:
|
|
251
|
+
curl -s http://127.0.0.1:8765/healthz
|
|
252
|
+
3. Put TLS in front (Caddy template at $SCRIPT_DIR/deploy/caddy/Caddyfile.example).
|
|
253
|
+
4. Add team members by editing $TOKEN_PATH and reloading:
|
|
254
|
+
echo '{"admin":"...", "alice":"...", "bob":"..."}' > $TOKEN_PATH
|
|
255
|
+
sudo systemctl reload sentinelone-mcp
|
|
256
|
+
(Reload sends SIGHUP; no connection drops.)
|
|
257
|
+
5. Tail the audit log:
|
|
258
|
+
sudo journalctl -u sentinelone-mcp -f | grep '\[audit\]'
|
|
259
|
+
|
|
260
|
+
See deploy/README.md for the full Linux VM walkthrough.
|
|
261
|
+
|
|
262
|
+
EOF
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
c_green "Done."
|