@pmoses-s1/s1-secops-mcp 1.3.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 +119 -0
- package/README.md +519 -0
- package/deploy/README.md +370 -0
- package/deploy/bridge/README.md +93 -0
- package/deploy/bridge/sentinelone-mcp-bridge.mjs +122 -0
- package/deploy/caddy/Caddyfile.example +110 -0
- package/deploy/install.sh +280 -0
- package/deploy/systemd/s1-secops-mcp.service +59 -0
- package/index.js +171 -0
- package/lib/auth.js +161 -0
- package/lib/credentials.js +124 -0
- package/lib/hec.js +144 -0
- package/lib/http-transport.js +289 -0
- package/lib/s1.js +610 -0
- package/lib/sdl.js +130 -0
- package/lib/server-core.js +263 -0
- package/lib/stdio-transport.js +77 -0
- package/lib/uam-ingest.js +444 -0
- package/package.json +50 -0
- package/scripts/regen-readme-tools-table.mjs +142 -0
- package/scripts/smoke-test-http.sh +125 -0
- package/scripts/test-mac.sh +187 -0
- package/tools/hyperautomation.js +284 -0
- package/tools/mgmt-console.js +344 -0
- package/tools/powerquery.js +129 -0
- package/tools/sdl-api.js +125 -0
- package/tools/uam-ingest.js +128 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Caddyfile for s1-secops-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
|
+
# AWS users: Let's Encrypt refuses to issue for *.amazonaws.com hostnames by
|
|
13
|
+
# policy. When you use the `tls <email>` shorthand, Caddy automatically falls
|
|
14
|
+
# back to ZeroSSL (also free, also publicly trusted, no policy block). The
|
|
15
|
+
# end-state cert should have `issuer=ZeroSSL ECC DV SSL CA 2`. Port 80 must
|
|
16
|
+
# be open in the SG for HTTP-01 to complete.
|
|
17
|
+
#
|
|
18
|
+
# DO NOT mix `tls <email>` with an `issuer acme { ... }` block: Caddy errors
|
|
19
|
+
# with "cannot mix issuer subdirective with other issuer-specific subdirectives".
|
|
20
|
+
# Pick one form: the email shorthand on its own line, OR a full `tls { ... }`
|
|
21
|
+
# block with the email moved inside the `issuer acme` stanza. Not both.
|
|
22
|
+
#
|
|
23
|
+
# Why Caddy and not nginx? Caddy auto-renews certs, has zero config for SSE
|
|
24
|
+
# streaming (flush_interval handled), and is one binary with no system Python
|
|
25
|
+
# or Lua dependency. Equally valid alternative: nginx with the snippet at the
|
|
26
|
+
# bottom of this file.
|
|
27
|
+
|
|
28
|
+
mcp.s1.internal {
|
|
29
|
+
tls internal
|
|
30
|
+
|
|
31
|
+
# Strict bearer-token enforcement. The MCP server also enforces tokens
|
|
32
|
+
# when MCP_BEARER_TOKENS_FILE is set, but checking here too means we never
|
|
33
|
+
# forward unauthenticated traffic to the backend.
|
|
34
|
+
#
|
|
35
|
+
# To bypass Caddy auth (and rely solely on the MCP server's enforcement),
|
|
36
|
+
# delete the @authorized matcher and the handle blocks below, leaving only
|
|
37
|
+
# the reverse_proxy.
|
|
38
|
+
@anyAuth header_regexp Authorization "^Bearer\s+\S+$"
|
|
39
|
+
|
|
40
|
+
handle @anyAuth {
|
|
41
|
+
reverse_proxy 127.0.0.1:8765 {
|
|
42
|
+
# MCP responses are streamed; flush immediately so partial replies
|
|
43
|
+
# reach the client without sitting in a buffer.
|
|
44
|
+
flush_interval -1
|
|
45
|
+
|
|
46
|
+
# Reasonable timeouts for long-running PowerQueries (LRQ can take
|
|
47
|
+
# ~30s on heavy queries). Increase if you see 504s on big queries.
|
|
48
|
+
transport http {
|
|
49
|
+
read_timeout 90s
|
|
50
|
+
write_timeout 90s
|
|
51
|
+
response_header_timeout 30s
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
handle {
|
|
57
|
+
respond "unauthorized" 401 {
|
|
58
|
+
close
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Standard security headers.
|
|
63
|
+
header {
|
|
64
|
+
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
65
|
+
X-Content-Type-Options "nosniff"
|
|
66
|
+
Referrer-Policy "no-referrer"
|
|
67
|
+
-Server
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Healthcheck pass-through (no auth needed on /healthz).
|
|
71
|
+
@health path /healthz /health
|
|
72
|
+
handle @health {
|
|
73
|
+
reverse_proxy 127.0.0.1:8765
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Access log to journald via systemd-journald, structured JSON.
|
|
77
|
+
log {
|
|
78
|
+
output stdout
|
|
79
|
+
format json
|
|
80
|
+
level INFO
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ─── nginx equivalent (commented; copy if you prefer nginx) ──────────────────
|
|
85
|
+
# upstream mcp_backend {
|
|
86
|
+
# server 127.0.0.1:8765;
|
|
87
|
+
# keepalive 16;
|
|
88
|
+
# }
|
|
89
|
+
# server {
|
|
90
|
+
# listen 443 ssl http2;
|
|
91
|
+
# server_name mcp.s1.internal;
|
|
92
|
+
# ssl_certificate /etc/ssl/certs/mcp.s1.internal.crt;
|
|
93
|
+
# ssl_certificate_key /etc/ssl/private/mcp.s1.internal.key;
|
|
94
|
+
#
|
|
95
|
+
# location = /healthz {
|
|
96
|
+
# proxy_pass http://mcp_backend;
|
|
97
|
+
# }
|
|
98
|
+
#
|
|
99
|
+
# location / {
|
|
100
|
+
# if ($http_authorization !~ "^Bearer\s+\S+$") {
|
|
101
|
+
# return 401;
|
|
102
|
+
# }
|
|
103
|
+
# proxy_pass http://mcp_backend;
|
|
104
|
+
# proxy_http_version 1.1;
|
|
105
|
+
# proxy_set_header Connection "";
|
|
106
|
+
# proxy_buffering off;
|
|
107
|
+
# proxy_read_timeout 90s;
|
|
108
|
+
# proxy_send_timeout 90s;
|
|
109
|
+
# }
|
|
110
|
+
# }
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# s1-secops-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/s1-secops-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
|
+
# Token and credential files must never be world-readable, even for the
|
|
22
|
+
# instant between creation and the explicit chmod 600 below.
|
|
23
|
+
umask 077
|
|
24
|
+
|
|
25
|
+
# ─── helpers ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
|
28
|
+
c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
|
29
|
+
c_yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
|
|
30
|
+
c_bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
31
|
+
|
|
32
|
+
step() { c_bold ">> $*"; }
|
|
33
|
+
ok() { c_green " ok: $*"; }
|
|
34
|
+
warn() { c_yellow " warn: $*"; }
|
|
35
|
+
die() {
|
|
36
|
+
c_red " error: $*"
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
PKG="@pmoses-s1/s1-secops-mcp"
|
|
41
|
+
MODE="user"
|
|
42
|
+
|
|
43
|
+
while [[ $# -gt 0 ]]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
--user)
|
|
46
|
+
MODE="user"
|
|
47
|
+
shift
|
|
48
|
+
;;
|
|
49
|
+
--server)
|
|
50
|
+
MODE="server"
|
|
51
|
+
shift
|
|
52
|
+
;;
|
|
53
|
+
-h | --help)
|
|
54
|
+
cat <<EOF
|
|
55
|
+
Usage: $0 [--user|--server]
|
|
56
|
+
|
|
57
|
+
--user Install for current user (default).
|
|
58
|
+
Default install path on macOS: ~/.config/sentinelone/
|
|
59
|
+
Default install path on Linux: ~/.config/sentinelone/
|
|
60
|
+
|
|
61
|
+
--server Install on a Linux VM as a shared service.
|
|
62
|
+
System path: /etc/s1-secops-mcp/
|
|
63
|
+
systemd unit: s1-secops-mcp.service
|
|
64
|
+
Requires sudo.
|
|
65
|
+
|
|
66
|
+
EOF
|
|
67
|
+
exit 0
|
|
68
|
+
;;
|
|
69
|
+
*) die "Unknown flag: $1 (try --help)" ;;
|
|
70
|
+
esac
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
OS="$(uname -s)"
|
|
74
|
+
case "$OS" in
|
|
75
|
+
Darwin) PLATFORM="mac" ;;
|
|
76
|
+
Linux) PLATFORM="linux" ;;
|
|
77
|
+
*)
|
|
78
|
+
c_red "Unsupported platform: $OS"
|
|
79
|
+
exit 2
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
|
|
83
|
+
if [[ "$MODE" == "server" && "$PLATFORM" != "linux" ]]; then
|
|
84
|
+
die "--server mode is Linux only (got $PLATFORM)"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# ─── prereqs ─────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
step "Checking prerequisites"
|
|
90
|
+
|
|
91
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
92
|
+
c_red "Node.js is required but not found on PATH."
|
|
93
|
+
c_red "Install Node 18+:"
|
|
94
|
+
c_red " macOS: brew install node@20"
|
|
95
|
+
c_red " Linux: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
|
|
96
|
+
exit 3
|
|
97
|
+
fi
|
|
98
|
+
NODE_MAJOR="$(node --version | sed 's/^v\([0-9]*\).*/\1/')"
|
|
99
|
+
if [[ "$NODE_MAJOR" -lt 18 ]]; then
|
|
100
|
+
die "Node $(node --version) is too old. Need Node 18+."
|
|
101
|
+
fi
|
|
102
|
+
ok "node $(node --version)"
|
|
103
|
+
|
|
104
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
105
|
+
die "npm not found alongside node; please install Node 18+ from nodejs.org or your package manager."
|
|
106
|
+
fi
|
|
107
|
+
ok "npm $(npm --version)"
|
|
108
|
+
|
|
109
|
+
if [[ "$MODE" == "server" ]]; then
|
|
110
|
+
if [[ "$EUID" -ne 0 ]]; then
|
|
111
|
+
die "--server mode must be run with sudo (need to create /etc/s1-secops-mcp/, system user, and systemd unit)."
|
|
112
|
+
fi
|
|
113
|
+
command -v systemctl >/dev/null 2>&1 || die "systemctl not found; this script targets systemd-based Linux."
|
|
114
|
+
ok "running as root, systemd present"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# ─── install package ─────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
step "Installing $PKG globally"
|
|
120
|
+
if [[ "$MODE" == "server" ]]; then
|
|
121
|
+
npm install -g "$PKG" >/dev/null
|
|
122
|
+
else
|
|
123
|
+
# Avoid sudo on Mac/personal Linux: use a per-user npm prefix if not already.
|
|
124
|
+
if ! npm config get prefix --location=user 2>/dev/null | grep -qE '^/'; then
|
|
125
|
+
npm config set prefix "$HOME/.npm-global"
|
|
126
|
+
case ":$PATH:" in
|
|
127
|
+
*":$HOME/.npm-global/bin:"*) ;;
|
|
128
|
+
*) warn "Add $HOME/.npm-global/bin to your PATH (currently missing)." ;;
|
|
129
|
+
esac
|
|
130
|
+
fi
|
|
131
|
+
npm install -g "$PKG" >/dev/null
|
|
132
|
+
fi
|
|
133
|
+
ok "$(npm ls -g --depth=0 "$PKG" 2>/dev/null | grep "$PKG" | head -1 | sed 's/.*-> //' || echo installed)"
|
|
134
|
+
|
|
135
|
+
# ─── credentials skeleton ────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
if [[ "$MODE" == "server" ]]; then
|
|
138
|
+
CONF_DIR="/etc/s1-secops-mcp"
|
|
139
|
+
CRED_PATH="$CONF_DIR/credentials.json"
|
|
140
|
+
TOKEN_PATH="$CONF_DIR/bearer-tokens.json"
|
|
141
|
+
ENV_PATH="$CONF_DIR/server.env"
|
|
142
|
+
OWNER="mcp"
|
|
143
|
+
else
|
|
144
|
+
CONF_DIR="$HOME/.config/sentinelone"
|
|
145
|
+
CRED_PATH="$CONF_DIR/credentials.json"
|
|
146
|
+
TOKEN_PATH=""
|
|
147
|
+
ENV_PATH=""
|
|
148
|
+
OWNER="$USER"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
step "Setting up $CONF_DIR"
|
|
152
|
+
mkdir -p "$CONF_DIR"
|
|
153
|
+
if [[ ! -f "$CRED_PATH" ]]; then
|
|
154
|
+
cat >"$CRED_PATH" <<'EOF'
|
|
155
|
+
{
|
|
156
|
+
"S1_CONSOLE_URL": "https://usea1-acme.sentinelone.net",
|
|
157
|
+
"S1_CONSOLE_API_TOKEN": "REPLACE_WITH_API_TOKEN",
|
|
158
|
+
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net"
|
|
159
|
+
}
|
|
160
|
+
EOF
|
|
161
|
+
chmod 600 "$CRED_PATH"
|
|
162
|
+
ok "wrote $CRED_PATH (placeholder, edit before starting)"
|
|
163
|
+
else
|
|
164
|
+
ok "$CRED_PATH already exists, leaving untouched"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
if [[ "$MODE" == "server" ]]; then
|
|
168
|
+
if ! id "$OWNER" >/dev/null 2>&1; then
|
|
169
|
+
step "Creating system user '$OWNER'"
|
|
170
|
+
useradd --system --no-create-home --shell /usr/sbin/nologin "$OWNER"
|
|
171
|
+
ok "created"
|
|
172
|
+
else
|
|
173
|
+
ok "user '$OWNER' already exists"
|
|
174
|
+
fi
|
|
175
|
+
chown -R "$OWNER":"$OWNER" "$CONF_DIR"
|
|
176
|
+
chmod 600 "$CRED_PATH"
|
|
177
|
+
|
|
178
|
+
if [[ ! -f "$TOKEN_PATH" ]]; then
|
|
179
|
+
step "Generating initial bearer token"
|
|
180
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
181
|
+
TOKEN_ADMIN="$(openssl rand -hex 32)"
|
|
182
|
+
else
|
|
183
|
+
TOKEN_ADMIN="$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')"
|
|
184
|
+
fi
|
|
185
|
+
cat >"$TOKEN_PATH" <<EOF
|
|
186
|
+
{
|
|
187
|
+
"admin": "$TOKEN_ADMIN"
|
|
188
|
+
}
|
|
189
|
+
EOF
|
|
190
|
+
chmod 600 "$TOKEN_PATH"
|
|
191
|
+
chown "$OWNER":"$OWNER" "$TOKEN_PATH"
|
|
192
|
+
ok "wrote $TOKEN_PATH (one initial admin token)"
|
|
193
|
+
c_yellow " INITIAL ADMIN BEARER TOKEN:"
|
|
194
|
+
c_yellow " $TOKEN_ADMIN"
|
|
195
|
+
c_yellow " Save this value now; it is also stored in $TOKEN_PATH."
|
|
196
|
+
else
|
|
197
|
+
ok "$TOKEN_PATH already exists"
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if [[ ! -f "$ENV_PATH" ]]; then
|
|
201
|
+
cat >"$ENV_PATH" <<EOF
|
|
202
|
+
# Environment file for s1-secops-mcp.service.
|
|
203
|
+
# Adjust LOG_LEVEL or override anything here; apply with: systemctl restart s1-secops-mcp
|
|
204
|
+
# (systemd only re-reads EnvironmentFile on restart; reload/SIGHUP re-reads bearer tokens only.)
|
|
205
|
+
EOF
|
|
206
|
+
chmod 600 "$ENV_PATH"
|
|
207
|
+
chown "$OWNER":"$OWNER" "$ENV_PATH"
|
|
208
|
+
ok "wrote $ENV_PATH"
|
|
209
|
+
else
|
|
210
|
+
ok "$ENV_PATH already exists"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
step "Installing systemd unit"
|
|
214
|
+
SVC_PATH="/etc/systemd/system/s1-secops-mcp.service"
|
|
215
|
+
GLOBAL_NODE_MODULES="$(npm root -g)"
|
|
216
|
+
SCRIPT_DIR="$GLOBAL_NODE_MODULES/$PKG"
|
|
217
|
+
# Rewrite the ExecStart path to point at the resolved global install,
|
|
218
|
+
# since the bundled unit uses %h which assumes per-user install.
|
|
219
|
+
sed "s|%h/.npm-global/lib/node_modules/@pmoses-s1/s1-secops-mcp|$SCRIPT_DIR|g" \
|
|
220
|
+
"$SCRIPT_DIR/deploy/systemd/s1-secops-mcp.service" >"$SVC_PATH"
|
|
221
|
+
systemctl daemon-reload
|
|
222
|
+
systemctl enable s1-secops-mcp >/dev/null 2>&1
|
|
223
|
+
ok "wrote $SVC_PATH and enabled the service"
|
|
224
|
+
|
|
225
|
+
step "Starting the service"
|
|
226
|
+
if systemctl is-active s1-secops-mcp >/dev/null 2>&1; then
|
|
227
|
+
systemctl restart s1-secops-mcp
|
|
228
|
+
ok "restarted"
|
|
229
|
+
else
|
|
230
|
+
systemctl start s1-secops-mcp
|
|
231
|
+
ok "started"
|
|
232
|
+
fi
|
|
233
|
+
sleep 1
|
|
234
|
+
if systemctl is-active --quiet s1-secops-mcp; then
|
|
235
|
+
ok "service is active"
|
|
236
|
+
else
|
|
237
|
+
c_red "service failed to start. Recent log lines:"
|
|
238
|
+
journalctl -u s1-secops-mcp -n 30 --no-pager | sed 's/^/ /'
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# ─── final notes ─────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
step "Next steps"
|
|
246
|
+
if [[ "$MODE" == "user" ]]; then
|
|
247
|
+
cat <<EOF
|
|
248
|
+
|
|
249
|
+
1. Edit $CRED_PATH with your real SentinelOne values.
|
|
250
|
+
2. Try the server:
|
|
251
|
+
s1-secops-mcp --version
|
|
252
|
+
s1-secops-mcp --help
|
|
253
|
+
3. Wire it into Claude Cowork / Claude Desktop / Claude Code via stdio
|
|
254
|
+
(no HTTP needed for single-user local). See deploy/README.md for the
|
|
255
|
+
exact config block.
|
|
256
|
+
|
|
257
|
+
EOF
|
|
258
|
+
elif [[ "$MODE" == "server" ]]; then
|
|
259
|
+
cat <<EOF
|
|
260
|
+
|
|
261
|
+
1. Edit $CRED_PATH with your real SentinelOne values, then restart:
|
|
262
|
+
sudo systemctl restart s1-secops-mcp
|
|
263
|
+
(credentials.json is read once at startup; reload/SIGHUP only re-reads
|
|
264
|
+
bearer tokens, so credential changes need a full restart.)
|
|
265
|
+
2. Verify the server is up:
|
|
266
|
+
curl -s http://127.0.0.1:8765/healthz
|
|
267
|
+
3. Put TLS in front (Caddy template at $SCRIPT_DIR/deploy/caddy/Caddyfile.example).
|
|
268
|
+
4. Add team members by editing $TOKEN_PATH and reloading:
|
|
269
|
+
echo '{"admin":"...", "alice":"...", "bob":"..."}' > $TOKEN_PATH
|
|
270
|
+
sudo systemctl reload s1-secops-mcp
|
|
271
|
+
(Reload sends SIGHUP; no connection drops.)
|
|
272
|
+
5. Tail the audit log:
|
|
273
|
+
sudo journalctl -u s1-secops-mcp -f | grep '\[audit\]'
|
|
274
|
+
|
|
275
|
+
See deploy/README.md for the full Linux VM walkthrough.
|
|
276
|
+
|
|
277
|
+
EOF
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
c_green "Done."
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=SentinelOne MCP server (Streamable HTTP, team-shared)
|
|
3
|
+
Documentation=https://github.com/pmoses-s1/claude-skills/tree/main/s1-secops-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. Bearer tokens rotate via
|
|
14
|
+
# `systemctl reload` (SIGHUP, no dropped connections). credentials.json is
|
|
15
|
+
# read once at startup, so changes to it require `systemctl restart`.
|
|
16
|
+
EnvironmentFile=/etc/s1-secops-mcp/server.env
|
|
17
|
+
Environment=MCP_BEARER_TOKENS_FILE=/etc/s1-secops-mcp/bearer-tokens.json
|
|
18
|
+
Environment=S1_CREDS_FILE=/etc/s1-secops-mcp/credentials.json
|
|
19
|
+
|
|
20
|
+
ExecStart=/usr/bin/env node %h/.npm-global/lib/node_modules/@pmoses-s1/s1-secops-mcp/index.js \
|
|
21
|
+
--transport http \
|
|
22
|
+
--host 127.0.0.1 \
|
|
23
|
+
--port 8765 \
|
|
24
|
+
--path /mcp
|
|
25
|
+
|
|
26
|
+
# SIGHUP reloads the bearer token file without restart.
|
|
27
|
+
ExecReload=/bin/kill -HUP $MAINPID
|
|
28
|
+
|
|
29
|
+
Restart=on-failure
|
|
30
|
+
RestartSec=5
|
|
31
|
+
|
|
32
|
+
# Hardening
|
|
33
|
+
# NOTE: MemoryDenyWriteExecute=true and LockPersonality=true are incompatible
|
|
34
|
+
# with Node.js V8 JIT (W+X mappings). Including them causes a silent SIGTRAP
|
|
35
|
+
# at startup with no useful log output. Leave them off for Node services.
|
|
36
|
+
NoNewPrivileges=true
|
|
37
|
+
PrivateTmp=true
|
|
38
|
+
ProtectSystem=strict
|
|
39
|
+
ProtectHome=true
|
|
40
|
+
ProtectKernelTunables=true
|
|
41
|
+
ProtectKernelModules=true
|
|
42
|
+
ProtectControlGroups=true
|
|
43
|
+
RestrictNamespaces=true
|
|
44
|
+
RestrictRealtime=true
|
|
45
|
+
RestrictSUIDSGID=true
|
|
46
|
+
SystemCallArchitectures=native
|
|
47
|
+
ReadWritePaths=
|
|
48
|
+
|
|
49
|
+
# Resource limits
|
|
50
|
+
LimitNOFILE=4096
|
|
51
|
+
TasksMax=64
|
|
52
|
+
|
|
53
|
+
# Logging
|
|
54
|
+
StandardOutput=journal
|
|
55
|
+
StandardError=journal
|
|
56
|
+
SyslogIdentifier=s1-secops-mcp
|
|
57
|
+
|
|
58
|
+
[Install]
|
|
59
|
+
WantedBy=multi-user.target
|
package/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SentinelOne MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Implements the Model Context Protocol over either stdio (default) or
|
|
6
|
+
* Streamable HTTP, using raw JSON-RPC 2.0 throughout. No external runtime
|
|
7
|
+
* dependencies. Pure Node.js 18+.
|
|
8
|
+
*
|
|
9
|
+
* Exposes 26 tools across PowerQuery, Mgmt Console REST, UAM, SDL API,
|
|
10
|
+
* Hyperautomation, and UAM Ingest; plus 2 resources and 2 prompts.
|
|
11
|
+
*
|
|
12
|
+
* Quick start:
|
|
13
|
+
* stdio (default, used by Claude Desktop / Claude Code / Cowork):
|
|
14
|
+
* node index.js
|
|
15
|
+
*
|
|
16
|
+
* Streamable HTTP, bound to localhost, no auth (single-user local):
|
|
17
|
+
* node index.js --transport http
|
|
18
|
+
*
|
|
19
|
+
* Streamable HTTP, bound to 0.0.0.0 with team bearer tokens:
|
|
20
|
+
* MCP_BEARER_TOKENS_FILE=/etc/s1-secops-mcp/bearer-tokens.json \
|
|
21
|
+
* node index.js --transport http --host 0.0.0.0 --port 8765
|
|
22
|
+
*
|
|
23
|
+
* Full configuration reference: README.md and deploy/README.md.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { dispatch, SERVER_INFO, ALL_TOOLS } from './lib/server-core.js';
|
|
27
|
+
import { getCreds, hasS1Creds, hasSdlCreds } from './lib/credentials.js';
|
|
28
|
+
import { hasHecCreds } from './lib/uam-ingest.js';
|
|
29
|
+
import { loadTokens, installSighupReload } from './lib/auth.js';
|
|
30
|
+
|
|
31
|
+
function log(...args) {
|
|
32
|
+
process.stderr.write('[s1-secops-mcp] ' + args.join(' ') + '\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── CLI flag parser ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const out = {
|
|
39
|
+
transport: process.env.MCP_TRANSPORT || 'stdio',
|
|
40
|
+
host: process.env.MCP_HTTP_HOST || '127.0.0.1',
|
|
41
|
+
port: Number(process.env.MCP_HTTP_PORT) || 8765,
|
|
42
|
+
path: process.env.MCP_HTTP_PATH || '/mcp',
|
|
43
|
+
help: false,
|
|
44
|
+
version: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const a = argv[i];
|
|
49
|
+
const next = () => argv[++i];
|
|
50
|
+
switch (a) {
|
|
51
|
+
case '-h': case '--help': out.help = true; break;
|
|
52
|
+
case '-v': case '--version': out.version = true; break;
|
|
53
|
+
case '--transport': out.transport = next(); break;
|
|
54
|
+
case '--host': out.host = next(); break;
|
|
55
|
+
case '--port': out.port = Number(next()); break;
|
|
56
|
+
case '--path': out.path = next(); break;
|
|
57
|
+
default:
|
|
58
|
+
if (a.startsWith('--')) {
|
|
59
|
+
process.stderr.write(`Unknown flag: ${a}\nRun with --help for usage.\n`);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!['stdio', 'http'].includes(out.transport)) {
|
|
66
|
+
process.stderr.write(`Invalid --transport: ${out.transport} (expected: stdio, http)\n`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
if (out.transport === 'http' && (!out.port || out.port <= 0 || out.port > 65535)) {
|
|
70
|
+
process.stderr.write(`Invalid --port: ${out.port}\n`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
if (!out.path.startsWith('/')) {
|
|
74
|
+
process.stderr.write(`Invalid --path: ${out.path} (must start with /)\n`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function printHelp() {
|
|
82
|
+
process.stdout.write(`\
|
|
83
|
+
s1-secops-mcp ${SERVER_INFO.version}
|
|
84
|
+
|
|
85
|
+
USAGE
|
|
86
|
+
s1-secops-mcp [options]
|
|
87
|
+
|
|
88
|
+
OPTIONS
|
|
89
|
+
--transport <stdio|http> Transport to use. Default: stdio.
|
|
90
|
+
--host <host> HTTP bind address. Default: 127.0.0.1.
|
|
91
|
+
--port <port> HTTP port. Default: 8765.
|
|
92
|
+
--path <path> HTTP MCP endpoint path. Default: /mcp.
|
|
93
|
+
-h, --help Show this help.
|
|
94
|
+
-v, --version Show server version.
|
|
95
|
+
|
|
96
|
+
ENVIRONMENT
|
|
97
|
+
MCP_TRANSPORT Same as --transport.
|
|
98
|
+
MCP_HTTP_HOST Same as --host.
|
|
99
|
+
MCP_HTTP_PORT Same as --port.
|
|
100
|
+
MCP_HTTP_PATH Same as --path.
|
|
101
|
+
|
|
102
|
+
MCP_BEARER_TOKENS_FILE Path to a JSON file mapping {name: token} for
|
|
103
|
+
per-user authenticated HTTP access. Recommended
|
|
104
|
+
for teams. SIGHUP reloads without restart.
|
|
105
|
+
MCP_BEARER_TOKENS Comma-separated raw tokens (no per-user names).
|
|
106
|
+
Fallback when MCP_BEARER_TOKENS_FILE is not set.
|
|
107
|
+
|
|
108
|
+
S1_CONSOLE_URL Console URL, e.g. https://usea1-acme.sentinelone.net
|
|
109
|
+
S1_CONSOLE_API_TOKEN Mgmt Console API token. Required for most tools.
|
|
110
|
+
S1_HEC_INGEST_URL HEC ingest host. Required for uam_ingest_alert,
|
|
111
|
+
uam_post_indicators, uam_post_alert.
|
|
112
|
+
S1_CREDS_FILE Explicit path to a credentials.json file.
|
|
113
|
+
Highest priority for credential resolution.
|
|
114
|
+
S1_CLAUDE_MD_PATH Absolute path to CLAUDE.md for the soc_analyst
|
|
115
|
+
prompt and sentinelone://soc-context resource.
|
|
116
|
+
|
|
117
|
+
EXAMPLES
|
|
118
|
+
Run as a local MCP server for Claude Desktop / Cowork:
|
|
119
|
+
s1-secops-mcp
|
|
120
|
+
|
|
121
|
+
Run as an HTTP service for personal use:
|
|
122
|
+
s1-secops-mcp --transport http
|
|
123
|
+
# then: curl -s http://127.0.0.1:8765/healthz
|
|
124
|
+
|
|
125
|
+
Run as a shared team service with token auth:
|
|
126
|
+
MCP_BEARER_TOKENS_FILE=/etc/s1-secops-mcp/bearer-tokens.json \\
|
|
127
|
+
s1-secops-mcp --transport http --host 0.0.0.0 --port 8765
|
|
128
|
+
# See deploy/README.md for the full Linux VM walkthrough.
|
|
129
|
+
`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
136
|
+
|
|
137
|
+
if (opts.help) { printHelp(); process.exit(0); }
|
|
138
|
+
if (opts.version) { process.stdout.write(`${SERVER_INFO.version}\n`); process.exit(0); }
|
|
139
|
+
|
|
140
|
+
log(`Starting ${SERVER_INFO.name} v${SERVER_INFO.version} (node ${process.version})`);
|
|
141
|
+
|
|
142
|
+
const creds = getCreds();
|
|
143
|
+
log(`S1 Mgmt API: ${hasS1Creds() ? 'configured (' + creds.S1_CONSOLE_URL + ')' : 'NOT configured'}`);
|
|
144
|
+
log(`SDL API: ${hasSdlCreds() ? 'configured (' + creds.S1_CONSOLE_URL + '/sdl)' : 'NOT configured'}`);
|
|
145
|
+
log(`UAM Ingest: ${hasHecCreds() ? 'configured (' + creds.S1_HEC_INGEST_URL + ')' : 'NOT configured (add S1_HEC_INGEST_URL)'}`);
|
|
146
|
+
log(`Tools: ${ALL_TOOLS.length} registered`);
|
|
147
|
+
|
|
148
|
+
if (opts.transport === 'http') {
|
|
149
|
+
try {
|
|
150
|
+
loadTokens();
|
|
151
|
+
} catch (e) {
|
|
152
|
+
process.stderr.write(`[auth] FATAL: ${e.message}\n`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
installSighupReload();
|
|
156
|
+
|
|
157
|
+
const { startHttp } = await import('./lib/http-transport.js');
|
|
158
|
+
await startHttp(dispatch, { port: opts.port, host: opts.host, path: opts.path });
|
|
159
|
+
log('HTTP transport ready. Press Ctrl+C to stop.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Default: stdio
|
|
164
|
+
const { startStdio } = await import('./lib/stdio-transport.js');
|
|
165
|
+
await startStdio(dispatch);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch(e => {
|
|
169
|
+
process.stderr.write(`Fatal: ${e.message}\n${e.stack}\n`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|