Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"name": "s1-secops-skills",
"source": "./plugins/s1-secops-skills",
"description": "SentinelOne SecOps skills for Claude: PowerQuery threat hunting and STAR/Custom Detection rules; Management Console API; Singularity Data Lake API; SDL dashboards; log parsing (OCSF); Hyperautomation SOAR; z-score anomaly baselining; autonomous DFIR alert investigation (soc-investigator); and one-prompt SDL solutions: source onboarding, asset enrichment, UEBA, ingest health, detection exclusions, Risk-Based Alerting, alert noise reduction, and Detection as Code.",
"version": "1.2.14",
"version": "1.2.16",
"author": {
"name": "Prithvi Moses",
"email": "prithvi.moses@sentinelone.com"
Expand Down
10 changes: 5 additions & 5 deletions mcp/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ When bumping a pin, edit both. They are checked once via `grep` in CI; a mismatc

| What | Source | Current pin |
|---|---|---|
| Image version (`IMAGE_VERSION`) | this repo | `1.2.3` |
| `@pmoses-s1/sentinelone-mcp` | npm | `1.2.2` |
| Image version (`IMAGE_VERSION`) | this repo | `1.2.5` |
| `@pmoses-s1/sentinelone-mcp` | npm | `1.2.4` |
| `@burtthecoder/mcp-virustotal` | npm | `1.0.21` |
| `purple-mcp` | git | `07d4992` (Sentinel-One/purple-mcp `v0.7.0`, 2026-06-26) |

`purple-mcp` is pinned to the `v0.7.0` release commit rather than a floating `main`, per upstream security guidance. When a newer release ships, repin here and in `mcp/docker/build.sh`.

`IMAGE_VERSION` is the tag the image is published under (`ghcr.io/pmoses-s1/s1-mcps:1.2.3`). It is independent of the underlying MCP versions: bump it when the Dockerfile, dispatcher, or bundled CLAUDE.md changes, even if all three MCP pins stay the same.
`IMAGE_VERSION` is the tag the image is published under (`ghcr.io/pmoses-s1/s1-mcps:1.2.5`). It is independent of the underlying MCP versions: bump it when the Dockerfile, dispatcher, or bundled CLAUDE.md changes, even if all three MCP pins stay the same.

## Build locally

Expand All @@ -44,9 +44,9 @@ When bumping a pin, edit both. They are checked once via `grep` in CI; a mismatc
docker/build.sh

# Smoke test
docker run -i --rm s1-mcps:1.2.3 help
docker run -i --rm s1-mcps:1.2.5 help
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.1"}}}' \
| docker run -i --rm s1-mcps:1.2.3 s1-secops-mcp
| docker run -i --rm s1-mcps:1.2.5 s1-secops-mcp
```

The dispatcher accepts `sentinelone-mcp`, `purple-mcp`, `virustotal-mcp`, or `help`.
Expand Down
6 changes: 3 additions & 3 deletions mcp/docker/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# docker/build.sh # local single-arch build, tag s1-mcps:<version>
# PUSH=true docker/build.sh # multi-arch build + push to ghcr.io
# TAG=dev docker/build.sh # override tag
# S1_MCP_VERSION=1.2.2 docker/build.sh # override a pin
# S1_MCP_VERSION=1.2.3 docker/build.sh # override a pin
#
# All version pins live in the "Pinned versions" block below. Bump them
# there and the GHA workflow at .github/workflows/docker-publish.yml,
Expand All @@ -18,10 +18,10 @@ set -euo pipefail
# The version of THIS image. Independent of the underlying MCP versions
# below, bump this when the image content (Dockerfile, dispatcher, bundled
# CLAUDE.md) changes, even if all three MCP pins stay the same.
IMAGE_VERSION="${IMAGE_VERSION:-1.2.3}"
IMAGE_VERSION="${IMAGE_VERSION:-1.2.5}"

# ── Pinned MCP versions ──────────────────────────────────────────────────────
S1_MCP_VERSION="${S1_MCP_VERSION:-1.2.2}"
S1_MCP_VERSION="${S1_MCP_VERSION:-1.2.4}"
VT_MCP_PACKAGE="${VT_MCP_PACKAGE:-@burtthecoder/mcp-virustotal}"
VT_MCP_VERSION="${VT_MCP_VERSION:-1.0.21}"
# purple-mcp v0.7.0 (2026-06-26). Pinned to the release commit, not a floating
Expand Down
11 changes: 8 additions & 3 deletions mcp/docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@
# JSON-RPC over stdio per the MCP spec.
set -e

# Extra arguments after the server name are passed through to the server
# binary (e.g. `s1-secops-mcp --transport http --port 8765`).
case "${1:-help}" in
s1-secops-mcp|sentinelone-mcp|s1)
exec sentinelone-mcp
shift
exec sentinelone-mcp "$@"
;;
purple-mcp|purple)
exec purple-mcp-bin --mode stdio
shift
exec purple-mcp-bin --mode stdio "$@"
;;
virustotal-mcp|virustotal|vt)
exec mcp-virustotal
shift
exec mcp-virustotal "$@"
;;
help|--help|-h|"")
cat <<'EOF'
Expand Down
2 changes: 1 addition & 1 deletion mcp/s1-secops-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Add this to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):
"mcpServers": {
"s1-secops-mcp": {
"command": "npx",
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.2.3"],
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.2.4"],
"env": {
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
"S1_CONSOLE_API_TOKEN": "eyJ...",
Expand Down
28 changes: 18 additions & 10 deletions mcp/s1-secops-mcp/deploy/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Deployment guide

The canonical team-VM walkthrough for most users is **[docs/vm-deployment.md](../../docs/vm-deployment.md)** (one-line install, per-user bearer tokens, Caddy TLS, client config, day-2 ops). This file is the full deployment reference behind it: all three topologies below, plus the AWS-specific gotchas and internals the walkthrough links to. Credential keys are in [docs/credentials.md](../../docs/credentials.md).

Three supported topologies, in order of complexity.

| Topology | Who runs it | Transport | Auth | Use this when |
Expand All @@ -10,7 +12,13 @@ Three supported topologies, in order of complexity.

## A. Single user, local (stdio)

`curl -fsSL https://raw.githubusercontent.com/Sentinel-One/ai-siem/main/mcp/s1-secops-mcp/deploy/install.sh | bash`
Download the installer, review it, then run it (avoid piping a remote script straight into a shell). For production, pin the URL to a tagged release commit instead of `main`:

```bash
curl -fsSL -o /tmp/s1-mcp-install.sh https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/s1-secops-mcp/deploy/install.sh
# review /tmp/s1-mcp-install.sh, then:
bash /tmp/s1-mcp-install.sh --user
```

That runs `install.sh --user`, which:
1. Confirms Node 18+ is present (errors out with install hints if not).
Expand Down Expand Up @@ -52,7 +60,7 @@ Or, equivalently, by package name without the install:
"mcpServers": {
"s1-secops-mcp": {
"command": "npx",
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.2.2"]
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.2.4"]
}
}
}
Expand Down Expand Up @@ -126,7 +134,7 @@ Team members connect from their Claude clients with their own bearer token. Audi

3. **Run the installer in server mode:**
```bash
curl -fsSL https://raw.githubusercontent.com/Sentinel-One/ai-siem/main/mcp/s1-secops-mcp/deploy/install.sh | sudo bash -s -- --server
curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/s1-secops-mcp/deploy/install.sh | sudo bash -s -- --server
```
It creates the `mcp` user, drops `/etc/s1-secops-mcp/credentials.json` (placeholder) and `/etc/s1-secops-mcp/bearer-tokens.json` (one freshly-generated admin token, printed once to stdout), installs the systemd unit, and starts the service.

Expand Down Expand Up @@ -257,7 +265,7 @@ curl -s https://mcp.s1.internal/healthz # in front of the proxy

## Connecting Claude Desktop to a remote MCP

Claude Desktop's `claude_desktop_config.json` only accepts stdio-based MCP servers in current stable builds; the `type: "http"` form gets rejected with "not valid MCP server configuration" on load. To connect Claude Desktop to your VM's HTTPS endpoint, use the bridge script shipped in this repo at [`bridge/s1-secops-mcp-bridge.mjs`](./bridge/s1-secops-mcp-bridge.mjs) a 40-line zero-dependency Node script that translates Claude Desktop's stdio into POST requests against the MCP HTTP endpoint.
Claude Desktop's `claude_desktop_config.json` only accepts stdio-based MCP servers in current stable builds; the `type: "http"` form gets rejected with "not valid MCP server configuration" on load. To connect Claude Desktop to your VM's HTTPS endpoint, use the bridge script shipped in this repo at [`bridge/s1-secops-mcp-bridge.mjs`](./bridge/s1-secops-mcp-bridge.mjs), a 40-line zero-dependency Node script that translates Claude Desktop's stdio into POST requests against the MCP HTTP endpoint.

Each team member drops the script anywhere on their machine (typically `~/.local/bin/s1-secops-mcp-bridge.mjs`) and points Claude Desktop at it:

Expand All @@ -276,7 +284,7 @@ Each team member drops the script anywhere on their machine (typically `~/.local
}
```

Then Cmd+Q and reopen Claude Desktop. See [`bridge/README.md`](./bridge/README.md) for install, smoke-test, and troubleshooting steps. Claude Cowork users can keep using the native `type: "http"` config (it supports remote HTTP MCPs in current builds) only Claude Desktop needs the bridge.
Then Cmd+Q and reopen Claude Desktop. See [`bridge/README.md`](./bridge/README.md) for install, smoke-test, and troubleshooting steps. Claude Cowork users can keep using the native `type: "http"` config (it supports remote HTTP MCPs in current builds), only Claude Desktop needs the bridge.

## AWS-specific gotchas

Expand All @@ -297,19 +305,19 @@ HTTP 400 urn:ietf:params:acme:error:rejectedIdentifier
The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy
```

Caddy auto-falls back to **ZeroSSL** (also free, also publicly trusted, no policy block on `amazonaws.com`). Use the email-shorthand form `tls <email>` and Caddy handles the fallback transparently. The right end state is a cert with `issuer=ZeroSSL ECC DV SSL CA 2` verify with:
Caddy auto-falls back to **ZeroSSL** (also free, also publicly trusted, no policy block on `amazonaws.com`). Use the email-shorthand form `tls <email>` and Caddy handles the fallback transparently. The right end state is a cert with `issuer=ZeroSSL ECC DV SSL CA 2`, verify with:

```bash
echo | openssl s_client -connect $HOST:8764 -servername $HOST 2>/dev/null \
| grep -E "^(issuer=|verify return code)"
```

For long-term peace of mind, use a real domain instead (Route 53 A record pointing at the Elastic IP) both LE and ZeroSSL issue without restriction and the hostname survives instance replacement.
For long-term peace of mind, use a real domain instead (Route 53 A record pointing at the Elastic IP), both LE and ZeroSSL issue without restriction and the hostname survives instance replacement.

### Caddyfile: don't mix `tls` shorthand with `issuer acme` block

```caddyfile
# WRONG Caddy errors: "cannot mix issuer subdirective with other issuer-specific subdirectives"
# WRONG: Caddy errors: "cannot mix issuer subdirective with other issuer-specific subdirectives"
tls prithvi@example.com {
issuer acme {
disable_http_challenge
Expand Down Expand Up @@ -343,7 +351,7 @@ The hardened service file we ship omits two systemd directives that would otherw
- `MemoryDenyWriteExecute=true`
- `LockPersonality=true`

Both block the W+X memory mappings V8 needs to JIT JavaScript. Adding them causes the service to silently SIGTRAP at startup with `Result: core-dump` and ~5 MB peak memory no useful log output. If you customize the unit, leave both off.
Both block the W+X memory mappings V8 needs to JIT JavaScript. Adding them causes the service to silently SIGTRAP at startup with `Result: core-dump` and ~5 MB peak memory; no useful log output. If you customize the unit, leave both off.

## Troubleshooting

Expand All @@ -359,7 +367,7 @@ Both block the W+X memory mappings V8 needs to JIT JavaScript. Adding them cause

These are supported but not first-class:

- **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/s1-secops-mcp@1.2.2` + `CMD ["s1-secops-mcp", "--transport", "http", "--host", "0.0.0.0"]`. Mount creds at `/etc/s1-secops-mcp/credentials.json` and tokens at `/etc/s1-secops-mcp/bearer-tokens.json`.
- **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/s1-secops-mcp@1.2.4` + `CMD ["s1-secops-mcp", "--transport", "http", "--host", "0.0.0.0"]`. Mount creds at `/etc/s1-secops-mcp/credentials.json` and tokens at `/etc/s1-secops-mcp/bearer-tokens.json`.

- **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.

Expand Down
10 changes: 5 additions & 5 deletions mcp/s1-secops-mcp/deploy/bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ A small stdio↔HTTPS proxy so Claude Desktop can talk to a team-shared `s1-seco

Claude Desktop's `claude_desktop_config.json` only accepts stdio-based MCP servers in current stable builds. Adding a remote server via `type: "http"` gets rejected with "not valid MCP server configuration". The bridge wraps the remote HTTPS endpoint as a local stdio process, which Claude Desktop accepts.

Claude Cowork and Claude Code don't need this both support `type: "http"` natively.
Claude Cowork and Claude Code don't need this, both support `type: "http"` natively.

## What's in the box

- [`s1-secops-mcp-bridge.mjs`](./s1-secops-mcp-bridge.mjs) the script. 40 lines, zero external dependencies. Requires Node.js 18+ (uses the built-in `fetch`).
- [`s1-secops-mcp-bridge.mjs`](./s1-secops-mcp-bridge.mjs): the script. 40 lines, zero external dependencies. Requires Node.js 18+ (uses the built-in `fetch`).

## Install (per team member, one-time)

```bash
# Download the script
mkdir -p ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/Sentinel-One/ai-siem/main/mcp/s1-secops-mcp/deploy/bridge/s1-secops-mcp-bridge.mjs \
curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/s1-secops-mcp/deploy/bridge/s1-secops-mcp-bridge.mjs \
-o ~/.local/bin/s1-secops-mcp-bridge.mjs
chmod +x ~/.local/bin/s1-secops-mcp-bridge.mjs

Expand Down Expand Up @@ -72,7 +72,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| `fetch error: ... UNABLE_TO_GET_ISSUER_CERT_LOCALLY` | Server is using a private CA (e.g. `tls internal`); Node doesn't read the system keychain | Use a publicly-trusted cert on the server (Let's Encrypt or ZeroSSL). See [../README.md#aws-specific-gotchas](../README.md#aws-specific-gotchas). |
| `bridge fetch error: ... ENOTFOUND` | DNS doesn't resolve `MCP_URL` host | Verify with `nslookup` or `dig`. If using an AWS public DNS, it may have changed; re-check the EC2 console. |
| 401 from upstream in the log | Wrong / revoked bearer token | Ask the admin for a fresh token; replace `MCP_BEARER`. |
| Bridge starts but Claude Desktop times out | Node version too old | `node --version` need 18+. Built-in fetch was added in 18. |
| Bridge starts but Claude Desktop times out | Node version too old | `node --version`: need 18+. Built-in fetch was added in 18. |

## How it works

Expand All @@ -84,7 +84,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \

The bridge reads one JSON-RPC message per line from stdin, POSTs it to `MCP_URL` with the `Authorization: Bearer <token>` header, and writes the JSON-RPC reply to stdout. JSON-RPC notifications (messages with no `id`) get no reply, matching the spec. Errors get translated to a JSON-RPC error envelope so Claude Desktop sees something useful instead of a hung process.

There is no session state, no buffering, and no SDK dependency it's just stdin → fetch → stdout.
There is no session state, no buffering, and no SDK dependency; it's just stdin → fetch → stdout.

## Security notes

Expand Down
30 changes: 24 additions & 6 deletions mcp/s1-secops-mcp/deploy/bridge/s1-secops-mcp-bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@

import { createInterface } from 'node:readline';

const URL = process.env.MCP_URL || (() => { throw new Error('MCP_URL not set'); })();
const BEARER = process.env.MCP_BEARER || (() => { throw new Error('MCP_BEARER not set'); })();
// Named MCP_URL (not URL) so the global URL constructor is not shadowed.
const MCP_URL = process.env.MCP_URL || (() => { throw new Error('MCP_URL not set'); })();
const BEARER = process.env.MCP_BEARER || (() => { throw new Error('MCP_BEARER not set'); })();

const log = (...a) => process.stderr.write('[bridge] ' + a.join(' ') + '\n');

log('starting; target', URL);
log('starting; target', MCP_URL);

const rl = createInterface({ input: process.stdin, terminal: false });

Expand All @@ -68,20 +69,37 @@ rl.on('line', async (line) => {

inFlight++;
try {
const res = await fetch(URL, {
const res = await fetch(MCP_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${BEARER}`,
'Accept': 'application/json',
},
body: raw,
// A hung upstream must not wedge the bridge forever; 120s covers the
// slowest LRQ tool calls with headroom.
signal: AbortSignal.timeout(120000),
});

if (isNotification) return;
if (isNotification) {
// Drain the body so the keep-alive socket is released.
await res.text().catch(() => {});
return;
}

const text = await res.text();
if (!res.ok) log(`HTTP ${res.status} from upstream: ${text.slice(0, 200)}`);
if (!res.ok) {
// Non-OK upstream: emit a JSON-RPC error envelope (not raw text/HTML) so the
// client always receives valid JSON-RPC and a meaningful diagnostic.
log(`HTTP ${res.status} from upstream: ${text.slice(0, 200)}`);
process.stdout.write(JSON.stringify({
jsonrpc: '2.0',
id: msg.id ?? null,
error: { code: -32603, message: `upstream HTTP ${res.status}`, data: text.slice(0, 500) },
}) + '\n');
return;
}
process.stdout.write(text.trimEnd() + '\n');
} catch (e) {
const cause = e.cause ? ` cause=${e.cause.code || e.cause.message || JSON.stringify(e.cause)}` : '';
Expand Down
2 changes: 1 addition & 1 deletion mcp/s1-secops-mcp/deploy/caddy/Caddyfile.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# end-state cert should have `issuer=ZeroSSL ECC DV SSL CA 2`. Port 80 must
# be open in the SG for HTTP-01 to complete.
#
# DO NOT mix `tls <email>` with an `issuer acme { ... }` block Caddy errors
# DO NOT mix `tls <email>` with an `issuer acme { ... }` block: Caddy errors
# with "cannot mix issuer subdirective with other issuer-specific subdirectives".
# Pick one form: the email shorthand on its own line, OR a full `tls { ... }`
# block with the email moved inside the `issuer acme` stanza. Not both.
Expand Down
Loading
Loading