@pmoses-s1/sentinelone-mcp 1.0.0 → 1.2.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,264 @@
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_CONFIG_READ_KEY": "",
145
+ "SDL_CONFIG_WRITE_KEY": ""
146
+ }
147
+ EOF
148
+ chmod 600 "$CRED_PATH"
149
+ ok "wrote $CRED_PATH (placeholder, edit before starting)"
150
+ else
151
+ ok "$CRED_PATH already exists, leaving untouched"
152
+ fi
153
+
154
+ if [[ "$MODE" == "server" ]]; then
155
+ if ! id "$OWNER" >/dev/null 2>&1; then
156
+ step "Creating system user '$OWNER'"
157
+ useradd --system --no-create-home --shell /usr/sbin/nologin "$OWNER"
158
+ ok "created"
159
+ else
160
+ ok "user '$OWNER' already exists"
161
+ fi
162
+ chown -R "$OWNER":"$OWNER" "$CONF_DIR"
163
+ chmod 600 "$CRED_PATH"
164
+
165
+ if [[ ! -f "$TOKEN_PATH" ]]; then
166
+ step "Generating initial bearer token"
167
+ if command -v openssl >/dev/null 2>&1; then
168
+ TOKEN_ADMIN="$(openssl rand -hex 32)"
169
+ else
170
+ TOKEN_ADMIN="$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')"
171
+ fi
172
+ cat > "$TOKEN_PATH" <<EOF
173
+ {
174
+ "admin": "$TOKEN_ADMIN"
175
+ }
176
+ EOF
177
+ chmod 600 "$TOKEN_PATH"
178
+ chown "$OWNER":"$OWNER" "$TOKEN_PATH"
179
+ ok "wrote $TOKEN_PATH (one initial admin token)"
180
+ c_yellow " INITIAL ADMIN BEARER TOKEN:"
181
+ c_yellow " $TOKEN_ADMIN"
182
+ c_yellow " Save this value now; it is also stored in $TOKEN_PATH."
183
+ else
184
+ ok "$TOKEN_PATH already exists"
185
+ fi
186
+
187
+ if [[ ! -f "$ENV_PATH" ]]; then
188
+ cat > "$ENV_PATH" <<EOF
189
+ # Environment file for sentinelone-mcp.service.
190
+ # Adjust LOG_LEVEL or override anything here; reload with: systemctl reload sentinelone-mcp
191
+ EOF
192
+ chmod 600 "$ENV_PATH"
193
+ chown "$OWNER":"$OWNER" "$ENV_PATH"
194
+ ok "wrote $ENV_PATH"
195
+ else
196
+ ok "$ENV_PATH already exists"
197
+ fi
198
+
199
+ step "Installing systemd unit"
200
+ SVC_PATH="/etc/systemd/system/sentinelone-mcp.service"
201
+ GLOBAL_NODE_MODULES="$(npm root -g)"
202
+ SCRIPT_DIR="$GLOBAL_NODE_MODULES/$PKG"
203
+ # Rewrite the ExecStart path to point at the resolved global install,
204
+ # since the bundled unit uses %h which assumes per-user install.
205
+ sed "s|%h/.npm-global/lib/node_modules/@pmoses-s1/sentinelone-mcp|$SCRIPT_DIR|g" \
206
+ "$SCRIPT_DIR/deploy/systemd/sentinelone-mcp.service" > "$SVC_PATH"
207
+ systemctl daemon-reload
208
+ systemctl enable sentinelone-mcp >/dev/null 2>&1
209
+ ok "wrote $SVC_PATH and enabled the service"
210
+
211
+ step "Starting the service"
212
+ if systemctl is-active sentinelone-mcp >/dev/null 2>&1; then
213
+ systemctl restart sentinelone-mcp
214
+ ok "restarted"
215
+ else
216
+ systemctl start sentinelone-mcp
217
+ ok "started"
218
+ fi
219
+ sleep 1
220
+ if systemctl is-active --quiet sentinelone-mcp; then
221
+ ok "service is active"
222
+ else
223
+ c_red "service failed to start. Recent log lines:"
224
+ journalctl -u sentinelone-mcp -n 30 --no-pager | sed 's/^/ /'
225
+ exit 1
226
+ fi
227
+ fi
228
+
229
+ # ─── final notes ─────────────────────────────────────────────────────────────
230
+
231
+ step "Next steps"
232
+ if [[ "$MODE" == "user" ]]; then
233
+ cat <<EOF
234
+
235
+ 1. Edit $CRED_PATH with your real SentinelOne values.
236
+ 2. Try the server:
237
+ sentinelone-mcp --version
238
+ sentinelone-mcp --help
239
+ 3. Wire it into Claude Cowork / Claude Desktop / Claude Code via stdio
240
+ (no HTTP needed for single-user local). See deploy/README.md for the
241
+ exact config block.
242
+
243
+ EOF
244
+ elif [[ "$MODE" == "server" ]]; then
245
+ cat <<EOF
246
+
247
+ 1. Edit $CRED_PATH with your real SentinelOne values, then reload:
248
+ sudo systemctl reload sentinelone-mcp
249
+ 2. Verify the server is up:
250
+ curl -s http://127.0.0.1:8765/healthz
251
+ 3. Put TLS in front (Caddy template at $SCRIPT_DIR/deploy/caddy/Caddyfile.example).
252
+ 4. Add team members by editing $TOKEN_PATH and reloading:
253
+ echo '{"admin":"...", "alice":"...", "bob":"..."}' > $TOKEN_PATH
254
+ sudo systemctl reload sentinelone-mcp
255
+ (Reload sends SIGHUP; no connection drops.)
256
+ 5. Tail the audit log:
257
+ sudo journalctl -u sentinelone-mcp -f | grep '\[audit\]'
258
+
259
+ See deploy/README.md for the full Linux VM walkthrough.
260
+
261
+ EOF
262
+ fi
263
+
264
+ c_green "Done."
@@ -0,0 +1,58 @@
1
+ [Unit]
2
+ Description=SentinelOne MCP server (Streamable HTTP, team-shared)
3
+ Documentation=https://github.com/pmoses-s1/claude-skills/tree/main/sentinelone-mcp
4
+ After=network-online.target
5
+ Wants=network-online.target
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=mcp
10
+ Group=mcp
11
+
12
+ # Server credentials (the S1 service-user token + SDL keys) and bearer tokens.
13
+ # Both are written by deploy/install.sh; rotate by editing then `systemctl reload`
14
+ # (which sends SIGHUP and reloads bearer tokens without dropping connections).
15
+ EnvironmentFile=/etc/sentinelone-mcp/server.env
16
+ Environment=MCP_BEARER_TOKENS_FILE=/etc/sentinelone-mcp/bearer-tokens.json
17
+ Environment=S1_CREDS_FILE=/etc/sentinelone-mcp/credentials.json
18
+
19
+ ExecStart=/usr/bin/env node %h/.npm-global/lib/node_modules/@pmoses-s1/sentinelone-mcp/index.js \
20
+ --transport http \
21
+ --host 127.0.0.1 \
22
+ --port 8765 \
23
+ --path /mcp
24
+
25
+ # SIGHUP reloads the bearer token file without restart.
26
+ ExecReload=/bin/kill -HUP $MAINPID
27
+
28
+ Restart=on-failure
29
+ RestartSec=5
30
+
31
+ # Hardening
32
+ # NOTE: MemoryDenyWriteExecute=true and LockPersonality=true are incompatible
33
+ # with Node.js V8 JIT (W+X mappings). Including them causes a silent SIGTRAP
34
+ # at startup with no useful log output. Leave them off for Node services.
35
+ NoNewPrivileges=true
36
+ PrivateTmp=true
37
+ ProtectSystem=strict
38
+ ProtectHome=true
39
+ ProtectKernelTunables=true
40
+ ProtectKernelModules=true
41
+ ProtectControlGroups=true
42
+ RestrictNamespaces=true
43
+ RestrictRealtime=true
44
+ RestrictSUIDSGID=true
45
+ SystemCallArchitectures=native
46
+ ReadWritePaths=
47
+
48
+ # Resource limits
49
+ LimitNOFILE=4096
50
+ TasksMax=64
51
+
52
+ # Logging
53
+ StandardOutput=journal
54
+ StandardError=journal
55
+ SyslogIdentifier=sentinelone-mcp
56
+
57
+ [Install]
58
+ WantedBy=multi-user.target