@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.
@@ -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."