@pmoses-s1/s1-secops-mcp 1.3.2 → 1.3.4
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 +166 -2
- package/README.md +16 -8
- package/deploy/README.md +18 -5
- package/deploy/bridge/README.md +2 -2
- package/deploy/bridge/s1-secops-mcp-bridge.mjs +1 -1
- package/index.js +1 -1
- package/lib/credentials.js +6 -0
- package/lib/sdl.js +406 -26
- package/lib/server-core.js +1 -1
- package/package.json +1 -1
- package/scripts/regen-readme-tools-table.mjs +7 -0
- package/scripts/smoke-test-http.sh +2 -2
- package/scripts/test-mac.sh +4 -4
- package/tools/sdl-api.js +188 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,152 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.4 - 2026-08-17
|
|
4
|
+
|
|
5
|
+
Adds site-level dashboard lifecycle. Two gaps closed: SDL GraphQL calls never
|
|
6
|
+
sent an `S1-Scope` header, and the dashboard operations the console itself uses
|
|
7
|
+
were not wrapped at all. **26 tools → 32.**
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`S1-Scope` on every SDL GraphQL and V1-query call.** Optional `scope`
|
|
12
|
+
argument on `configFiles`, `configFile`, `putConfigFile`, `deleteConfigFile`
|
|
13
|
+
and `v1Query`, and on the four `sdl_*_file` tools. Falls back to a new
|
|
14
|
+
`S1_SCOPE` credential; `scope: null` suppresses that default and sends no
|
|
15
|
+
header. Format `"<accountId>"` or `"<accountId>:<siteId>"`, validated before
|
|
16
|
+
the request so a typo cannot silently widen the read.
|
|
17
|
+
- **Six dashboard-lifecycle tools** on the `dashboardsV2` surface:
|
|
18
|
+
`sdl_list_dashboards`, `sdl_get_dashboard`, `sdl_create_dashboard`,
|
|
19
|
+
`sdl_share_dashboard`, `sdl_save_dashboard_layout`, `sdl_delete_dashboard`.
|
|
20
|
+
- **`sdl_create_dashboard`** takes the whole dashboard document as one `config`
|
|
21
|
+
string, the path the console uses. It parses the JSON first, so the UI
|
|
22
|
+
stub-append failure (`{graphs: []}{...}` → "Content is invalid json /
|
|
23
|
+
Additional text after JSON object", leaving an empty dashboard behind) is
|
|
24
|
+
reported as a caller error instead of filing a broken shell.
|
|
25
|
+
- **`sdl_share_dashboard`** wraps `shareResource`, the only SDL operation that
|
|
26
|
+
takes an explicit scope target. This is how an account-scoped dashboard is
|
|
27
|
+
pushed to a site without recreating it. Scope targets are validated up front,
|
|
28
|
+
because the server accepts a malformed entry, shares nothing, and reports
|
|
29
|
+
success.
|
|
30
|
+
- **`S1_SCOPE`** added to `getCreds()`. It was absent, so any credentials-file
|
|
31
|
+
default would have been read and then dropped.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Scope-sensitive call sites now scope consistently.** Absence
|
|
36
|
+
disambiguation re-lists at the scope of the failed lookup, the `/dashboards/`
|
|
37
|
+
duplicate guard lists at the scope of the write, and delete verification
|
|
38
|
+
re-reads at the scope of the delete. Mixing scopes across these steps reports
|
|
39
|
+
a live site-scoped file as deleted, which is the same false-negative class
|
|
40
|
+
1.3.3 fixed for error text.
|
|
41
|
+
|
|
42
|
+
### Corrected documentation
|
|
43
|
+
|
|
44
|
+
- `sdl-api/references/config-file-graphql.md` claimed the `s1-scope` header was
|
|
45
|
+
"ignored, not rejected" on `/sdl/v2/graphql`. **That was wrong.** Measured on
|
|
46
|
+
`usea1-purple`, same token and query: `configFiles` returned 113 files at
|
|
47
|
+
account scope and 4 at a site scope. Config listings and dashboard reads are
|
|
48
|
+
scope-FILTERED, so a dropped header changes which objects appear to exist.
|
|
49
|
+
`auth_and_limits.md` corrected to match.
|
|
50
|
+
|
|
51
|
+
### Dashboard skill
|
|
52
|
+
|
|
53
|
+
- **`site.id`, not `site.name`, is the scoping predicate.** For one site over
|
|
54
|
+
24h, `site.id='<id>'` matched 60,410 events of which 510 carried the site id
|
|
55
|
+
with a null `site.name`: `ActivityFeed` 172, `asset` 111, unattributed 99,
|
|
56
|
+
`SentinelOne` 70, `Windows Event Logs` 48, `alert` 10. A `site.name` filter
|
|
57
|
+
silently drops alert and asset records. `site.id` is also the same value as
|
|
58
|
+
the `S1-Scope` `siteId` and survives a site rename.
|
|
59
|
+
- **New scope doctrine in `sdl-dashboard/SKILL.md`:** deployment scope and query
|
|
60
|
+
scope are separate decisions, and a site-deployed dashboard scopes its panels
|
|
61
|
+
to that site unless the user explicitly asks for account-wide queries.
|
|
62
|
+
- **`panel_safety_check.py --site-id <id>`** adds rule **S01** (query panel with
|
|
63
|
+
no, or wrong, `site.id` predicate on a site-targeted dashboard; opt out with
|
|
64
|
+
`--allow-account-scope-queries`) and rule **S02** (`site.name` used as a
|
|
65
|
+
scoping filter; never suppressed, the substitution is wrong at any scope).
|
|
66
|
+
- Recorded that the console's XDR selector injects
|
|
67
|
+
`preFilter: "dataSource.category = 'security'"` into every panel query.
|
|
68
|
+
|
|
69
|
+
### Python client (`sdl-api/scripts/sdl_client.py`)
|
|
70
|
+
|
|
71
|
+
- Per-call `scope` on `config_files`, `config_file`, `put_config_file`,
|
|
72
|
+
`delete_config_file`, with the same `_UNSET`-vs-`None` distinction.
|
|
73
|
+
- Six new methods mirroring the JS layer: `list_dashboards`, `get_dashboard`,
|
|
74
|
+
`create_dashboard`, `share_dashboard`, `save_dashboard_layout`,
|
|
75
|
+
`delete_dashboard`.
|
|
76
|
+
- `get_dashboard` treats both a null result and a GraphQL error as absence,
|
|
77
|
+
disambiguated against the listing. Assuming only the null form is what broke
|
|
78
|
+
every delete in 1.3.2; the confirming re-read threw on the success path.
|
|
79
|
+
|
|
80
|
+
### Tests
|
|
81
|
+
|
|
82
|
+
- 24 new cases in `tests/sdl-graphql.test.mjs`: header presence and absence,
|
|
83
|
+
credentials fallback, `scope: null` suppression, malformed-scope rejection
|
|
84
|
+
before any request, scope consistency across the guard / disambiguation /
|
|
85
|
+
delete-verify paths, the six dashboard operations, stub-append rejection, and
|
|
86
|
+
the `getDashboard` absence matrix.
|
|
87
|
+
- New `sdl-dashboard/tests/test_panel_safety_check.py`: 19 cases over S01 and
|
|
88
|
+
S02 including the wrong-site case, the opt-out flag, exempt panel types, and
|
|
89
|
+
a regression guard that existing rules still fire.
|
|
90
|
+
- Tool-count assertions updated 26 → 32 across the smoke, stdio, HTTP and
|
|
91
|
+
origin-guard suites.
|
|
92
|
+
|
|
93
|
+
## 1.3.3 - 2026-08-07
|
|
94
|
+
|
|
95
|
+
Fixes a user-facing regression in 1.3.2 found by running the live MCP tools
|
|
96
|
+
against a tenant for the first time. **Upgrade from 1.3.2 is recommended.**
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **Every successful `sdl_delete_file` reported an error in 1.3.2.** The delete
|
|
101
|
+
verification added in 1.3.2 re-reads the file to confirm removal, but the server
|
|
102
|
+
reports absence as a GraphQL error rather than a null result, so the confirming
|
|
103
|
+
read threw on exactly the success path. The delete itself always worked; only the
|
|
104
|
+
reported outcome was wrong. Verified live: three deletes across `/dashboards/`,
|
|
105
|
+
`/datatables/` and `/logParsers/` all removed their file and all three surfaced
|
|
106
|
+
as errors.
|
|
107
|
+
- **`sdl_get_file` on a missing path returned a raw GraphQL error** instead of the
|
|
108
|
+
actionable "this may be udoId-addressed, list it and retry" hint. The hint branch
|
|
109
|
+
was unreachable because the library threw before returning.
|
|
110
|
+
- **Absence is now detected reliably on both address forms.** The error text differs:
|
|
111
|
+
a missing name gives "Config file with name X not found.", a missing `udoId` gives
|
|
112
|
+
the generic "Something went wrong. Please try again...", which is also what a
|
|
113
|
+
version conflict returns. `configFile` normalises the explicit form and
|
|
114
|
+
disambiguates the generic one against the file listing, so a deleted dashboard
|
|
115
|
+
reads as absent while a genuine server error still propagates.
|
|
116
|
+
- **An out-of-range numeric `udoId` is no longer swallowed by the absence path.**
|
|
117
|
+
Validation now runs before the lookup, so a caller bug surfaces as a caller bug.
|
|
118
|
+
- **A transport error is never read as "file absent".** Absence detection now requires a
|
|
119
|
+
GraphQL-layer error, so a 404 page or WAF block whose body contains the words "not found"
|
|
120
|
+
no longer satisfies it. Without this a delete could confirm itself against a file it
|
|
121
|
+
never checked.
|
|
122
|
+
- **A failing listing during absence disambiguation keeps the original error** instead of
|
|
123
|
+
replacing it with the listing failure.
|
|
124
|
+
- **The duplicate guard is no longer bypassed by case.** Its namespace test was
|
|
125
|
+
case-sensitive while its name comparison was not, so `/Dashboards/AI Usage` skipped the
|
|
126
|
+
guard entirely. Both now share one normaliser.
|
|
127
|
+
- **`v1Query` keeps its backoff.** Restricting status retry to idempotent methods removed it
|
|
128
|
+
from this read-only POST, which schema discovery iterates once per data source.
|
|
129
|
+
|
|
130
|
+
### Python client (`sdl-api/scripts/sdl_client.py`)
|
|
131
|
+
|
|
132
|
+
Brought to parity with the JS client:
|
|
133
|
+
|
|
134
|
+
- Status retry is restricted to idempotent methods. The Python client was retrying POST
|
|
135
|
+
mutations, which is the mechanism that duplicates a dashboard on a re-sent write.
|
|
136
|
+
- `Retry-After` is capped at 30s; an unbounded value parked the process.
|
|
137
|
+
- The duplicate guard fails closed on an empty listing.
|
|
138
|
+
- Absence detection, name normalisation and delete verification match the JS behaviour.
|
|
139
|
+
|
|
140
|
+
### Tests
|
|
141
|
+
|
|
142
|
+
- `tests/sdl-graphql.test.mjs`: 86 cases, adding the transport-error-is-not-absence case,
|
|
143
|
+
listing-failure error preservation, the case-variant guard bypass, and `v1Query` retry.
|
|
144
|
+
- `sdl-api/tests/test_client.py`: new. 19 cases over a stubbed session, so the Python client
|
|
145
|
+
is no longer invisible to CI. Runs in ~0.01s with no network.
|
|
146
|
+
- Live regression through the real MCP stdio protocol: handshake, 26 tools, and full
|
|
147
|
+
create/read/update/stale-reject/delete/confirm-absent cycles for `/datatables/` and
|
|
148
|
+
`/dashboards/`, plus the duplicate guard, the notFound hint, and `/automaticLookups`.
|
|
149
|
+
|
|
3
150
|
## 1.3.2 - 2026-08-07
|
|
4
151
|
|
|
5
152
|
Config-file operations move from the legacy REST endpoints to GraphQL. Tool count unchanged at
|
|
@@ -38,7 +185,8 @@ Config-file operations move from the legacy REST endpoints to GraphQL. Tool coun
|
|
|
38
185
|
|
|
39
186
|
- `udoId` is assigned by namespace, verified live: only `/dashboards/` files get one. `/lookups/`,
|
|
40
187
|
`/datatables/`, `/logParsers/` and `/automaticLookups` are name-addressed with `udoId` null.
|
|
41
|
-
- `expectedVersion` is
|
|
188
|
+
- `expectedVersion` is enforced on both address forms. A stale value is rejected with
|
|
189
|
+
"There are conflicting changes in the file." and the stored content is left untouched.
|
|
42
190
|
- A `deleteConfigFile` returning `null` with no `errors` array is success, not failure.
|
|
43
191
|
- The scoped SDL keys (`SDL_CONFIG_READ_KEY` and friends) are retired; the console API token
|
|
44
192
|
covers every SDL operation.
|
|
@@ -48,6 +196,7 @@ Config-file operations move from the legacy REST endpoints to GraphQL. Tool coun
|
|
|
48
196
|
Hardening release from the 2026-07-31 code review. Tool count unchanged at 26.
|
|
49
197
|
|
|
50
198
|
### Fixed
|
|
199
|
+
|
|
51
200
|
- **Missing `Retry-After` header no longer sleeps 0ms before retrying.** `Number(null)` is 0, so `lib/s1.js`, `lib/hec.js`, and `lib/uam-ingest.js` treated an absent header as "wait 0ms" and hammered the backend. All three now use the validated pattern from `lib/sdl.js`: honor the header only when present and parseable as a finite number of seconds (capped at 30s), otherwise fall back to the exponential delay. Numeric headers behave exactly as before.
|
|
52
201
|
- **`uam_set_status` no longer reports silent success.** The `alertTriggerActions` mutation selected only `__typename`, so a skipped or failed action still returned as if it worked (observed live: status unchanged after a "successful" call). The selection now mirrors the full `actions { success failure skip }` shape and the client throws when the backend reports a failure entry, skips the action without a success, or returns an empty actions list (nothing applied, e.g. the filter matched no alert). `uam_add_note` was audited for the same pattern; it already verifies via the returned note list.
|
|
53
202
|
- **LRQ polling tolerates transient poll errors.** A single 429/5xx poll response used to throw and cancel the running query. Transient statuses now keep polling (interval doubles up to 5s) until the existing 5-minute deadline; other 4xx responses remain fatal.
|
|
@@ -59,6 +208,7 @@ Hardening release from the 2026-07-31 code review. Tool count unchanged at 26.
|
|
|
59
208
|
- **SDL `config_read` key chain reordered to least-privilege first** (config_read, config_write, console JWT), matching the Python `SDLClient` the header claims to mirror.
|
|
60
209
|
|
|
61
210
|
### Changed
|
|
211
|
+
|
|
62
212
|
- **Removed dead exports `purpleAiQuery` and `purpleAiInvestigate`** from `lib/s1.js`; their MCP tools were removed 2026-05-03 (browser-session teamToken requirement) and nothing referenced the library functions. Corrected stale doc text that pointed at a nonexistent `uam_set_analyst_verdict` tool: the analyst verdict is set via a raw `alertTriggerActions` mutation with the `analystVerdictUpdate` action through `s1_api_post`.
|
|
63
213
|
- **Deploy docs: credentials.json changes require `systemctl restart`.** SIGHUP reload only re-reads bearer tokens; the installer output and systemd unit comment said reload was enough. `deploy/install.sh` also sets `umask 077` so token/credential files are never world-readable at creation (the explicit `chmod 600` lines remain).
|
|
64
214
|
- **Claude Desktop bridge hardening:** 120s fetch timeout (`AbortSignal.timeout`), notification responses are drained so keep-alive sockets are released, and the URL constant no longer shadows the global `URL` constructor.
|
|
@@ -66,6 +216,7 @@ Hardening release from the 2026-07-31 code review. Tool count unchanged at 26.
|
|
|
66
216
|
- **`const status = response.error ? 200 : 200`** simplified; JSON-RPC errors still return HTTP 200 with an error envelope.
|
|
67
217
|
|
|
68
218
|
### Tests
|
|
219
|
+
|
|
69
220
|
- New `tests/regressions-2026-07-31.test.mjs` (mocked fetch, no network): missing `Retry-After` uses the exponential delay, `uamSetStatus` throws on failure/skip results, inline `?isLegacy=` is honored. Both regression suites are now part of `npm test`.
|
|
70
221
|
- Transport and smoke tests read the expected version from `package.json` instead of a hardcoded string.
|
|
71
222
|
|
|
@@ -74,12 +225,14 @@ Hardening release from the 2026-07-31 code review. Tool count unchanged at 26.
|
|
|
74
225
|
Correctness release from the 2026-07-29 defect review. Fixes two bugs that produced plausible-but-wrong query results, hardens the SDL auth chain and HTTP retry paths, corrects the HEC `/event` content type, and adds a regression suite. Tool count unchanged at 26. (The Docker bundle image moved to 1.2.4 pinning `S1_MCP_VERSION=1.2.3`; the image tag stays ahead of the npm tag as it has since the split.)
|
|
75
226
|
|
|
76
227
|
### Fixed
|
|
228
|
+
|
|
77
229
|
- **`powerquery_run` no longer collapses a caller-supplied time window.** The old code overwrote BOTH `startTime` and `endTime` with the last-`hours` default whenever either was missing, so a startTime-only call silently ran over the last 24 hours. Each bound now defaults independently. Live A/B: a startTime-only 7.4-day query returned 73,755 events on the fixed server vs 12,911 (the 24h control) on the old one.
|
|
78
230
|
- **`powerquery_run` now reports `matchCount`.** It was read from the top level of the LRQ response and came back `null` on every call; it lives inside the `data` block on current engines.
|
|
79
231
|
- **SDL auth chain falls through on 401/403.** `lib/sdl.js` treated an auth failure on the first configured key as fatal even when a later key (e.g. the console JWT) would work. It now advances through the chain and raises only when exhausted.
|
|
80
232
|
- **HEC `/event` ingestion uses `application/json`,** so per-event `time` backdating is honored instead of the envelope being indexed as opaque text at receive time.
|
|
81
233
|
|
|
82
234
|
### Changed
|
|
235
|
+
|
|
83
236
|
- **Write requests no longer auto-retry on 5xx.** Retry is restricted to idempotent methods (GET/HEAD); read-only POSTs (GraphQL queries, Purple AI launches) opt back in via `allowRetry`. HEC raw ingest no longer retries 5xx (no idempotency key); UAM ingest still retries because `metadata.uid` dedupes.
|
|
84
237
|
- **`Retry-After` parsing hardened:** an HTTP-date value no longer collapses to `sleep(NaN)`; waits are validated and capped at 30s.
|
|
85
238
|
- **`uam_add_note` returns the correct note** (matches by text, tiebreaks on newest `createdAt`) instead of assuming newest-last ordering.
|
|
@@ -88,15 +241,18 @@ Correctness release from the 2026-07-29 defect review. Fixes two bugs that produ
|
|
|
88
241
|
- **`powerquery_schema_discover` escapes single quotes** in the data-source name before building the V1 filter.
|
|
89
242
|
|
|
90
243
|
### Tests
|
|
244
|
+
|
|
91
245
|
- New `tests/regressions-2026-07-29.test.mjs` (mocked fetch, no network): independent time-bound defaults, `matchCount` extraction, SDL 401/403 fall-through, HEC per-endpoint content type, write-vs-read retry semantics.
|
|
92
246
|
|
|
93
247
|
## 1.2.2 - 2026-06-13
|
|
94
248
|
|
|
95
249
|
### Changed
|
|
250
|
+
|
|
96
251
|
- **Renamed `ha_archive_workflow` to `ha_delete_workflow`.** The old tool hit `POST /hyper-automate/api/v1/workflows/archive`, which returns HTTP 500 on this tenant. The replacement uses the validated `DELETE /hyper-automate/api/v1/workflows/{id}` endpoint (a soft, recoverable delete equivalent to clicking Delete in the Hyperautomation UI). Scope the call with `accountIds` or `siteIds`; a 404 "Object not found" means the id is not under that scope or is already deleted. Updated `README.md`, the tools-table regenerator, and the smoke test in lockstep.
|
|
97
252
|
- **`powerquery_run` description now documents the `datasource` and `savelookup` capabilities** (querying SentinelOne-managed inventory such as assets/alerts/vulnerabilities/misconfigurations, and persisting a result as a reusable lookup table), pointing at the new `powerquery/references/datasource-command.md`.
|
|
98
253
|
|
|
99
254
|
### Notes
|
|
255
|
+
|
|
100
256
|
- Tool count unchanged at 26 (the Hyperautomation tool was renamed, not added or removed).
|
|
101
257
|
- `SERVER_INFO.version` bumped in lockstep with `package.json` (the drift that forced the 1.2.0 -> 1.2.1 re-release).
|
|
102
258
|
|
|
@@ -105,22 +261,26 @@ Correctness release from the 2026-07-29 defect review. Fixes two bugs that produ
|
|
|
105
261
|
Supersedes 1.2.0, which was deprecated on npm. The 1.2.0 build shipped with a stale internal `SERVER_INFO.version` of `1.1.0` despite a `1.2.0` package version, so the server announced the wrong version on `initialize`. 1.2.1 is identical in features and corrects the reported runtime version. The content below is unchanged from the 1.2.0 work.
|
|
106
262
|
|
|
107
263
|
### Added
|
|
264
|
+
|
|
108
265
|
- **`hec_ingest` tool**: raw-log/event ingestion into the Singularity Data Lake via the HEC (HTTP Event Collector) endpoint (`/services/collector/raw` and `/services/collector/event`). Supports `parser` (-> `?sourcetype=`), custom `fields` (query params), **required** `scope` (S1-Scope header), gzip compression, and `isParsed` (-> `?isParsed=true`, indexes already-structured JSON with no SDL parser). Replaces the removed `sdl_upload_logs`. Validated live across the full HEC matrix (both endpoints, gzip on/off, parser field extraction, multi-line, batched, reserved-field handling, scope enforcement, isParsed). Grounded in the S-26.1 HEC docs (p.4723-4726).
|
|
109
266
|
|
|
110
267
|
### Removed
|
|
268
|
+
|
|
111
269
|
- **`sdl_upload_logs` tool** plus the underlying SDL `uploadLogs`/`addEvents` library functions and `SDL_LOG_WRITE_KEY` plumbing. SDL raw-log ingestion moves to the HEC path (`hec_ingest`). The `sdl-api` skill is now query + configuration only; the `sdl-log-parser` validation loop uses HEC ingest.
|
|
112
270
|
|
|
113
271
|
### Changed
|
|
272
|
+
|
|
114
273
|
- Tool count unchanged at 26 (removed `sdl_upload_logs`, added `hec_ingest`).
|
|
115
274
|
- Skill docs corrected: scheduled detection rules bind the Target Asset via `entityMappings` ("Entity column mapping"); the full scheduled-rule option set (UI <-> API) is catalogued in `powerquery/references/detection-rules.md`.
|
|
116
275
|
|
|
117
|
-
|
|
118
276
|
## 1.1.0 - 2026-05-28 (rebuilt 2026-05-31)
|
|
119
277
|
|
|
120
278
|
### Fixed (rebuild)
|
|
279
|
+
|
|
121
280
|
- **`s1_api_get` now auto-injects `isLegacy=false` for `/cloud-detection/rules` listings.** Without `isLegacy=false` the S1 API silently omits `queryType="scheduled"` PowerQuery rules from the response; no error, no warning, the response just lies by omission. The handler now guards against this when the caller forgets, and the tool description loudly flags the requirement. This eliminates the "I see zero scheduled detections" failure mode that was producing wrong verdicts when listing Custom Detection rules. Same `1.1.0` version per the rebuild request.
|
|
122
281
|
|
|
123
282
|
### Added
|
|
283
|
+
|
|
124
284
|
- **Streamable HTTP transport.** New `--transport http` mode (default stays `stdio`). Single-endpoint POST `/mcp` per the MCP 2024-11-05 spec, plus `/healthz` for load balancer probes. Implementation is pure `node:http`, no new dependencies.
|
|
125
285
|
- **Per-user bearer token auth.** New `MCP_BEARER_TOKENS_FILE` env var pointing at a `{ "<name>": "<token>" }` JSON file gives each team member a stable name in audit logs and supports rotation. SIGHUP reloads tokens without dropping connections. `MCP_BEARER_TOKENS` env var (comma-separated raw tokens) is a fallback for small or quick-test setups.
|
|
126
286
|
- **Audit logging.** Every authenticated HTTP request emits `[audit] <ts> | <name> | <method> | <param-summary> | <status>` to stderr; systemd captures it via journald.
|
|
@@ -137,12 +297,14 @@ Supersedes 1.2.0, which was deprecated on npm. The 1.2.0 build shipped with a st
|
|
|
137
297
|
- **README auto-regenerator** at `scripts/regen-readme-tools-table.mjs`. `npm run regen:readme` keeps the README table in sync with `ALL_TOOLS`. `npm run regen:readme -- --check` fails when stale (suitable for CI).
|
|
138
298
|
|
|
139
299
|
### Fixed
|
|
300
|
+
|
|
140
301
|
- **README tool table.** Previous count was 19; actual is 26. Auto-generated now.
|
|
141
302
|
- **Header comment in `index.js`.** Previously said 21; updated to 26.
|
|
142
303
|
- **`purple_ai_query`** removed from the documentation. The tool itself was removed 2026-05-03 because the underlying API requires a browser-session `teamToken` that service-account API tokens never obtain. The README, `index.js`, and `docs/mcp-tools.md` no longer reference it.
|
|
143
304
|
- **`uam_set_status` documentation.** Doc previously said valid status values include `CLOSED`. The source enum is `NEW`, `IN_PROGRESS`, `RESOLVED`; doc now matches.
|
|
144
305
|
|
|
145
306
|
### Changed
|
|
307
|
+
|
|
146
308
|
- **Refactored** dispatch out of `index.js` into `lib/server-core.js` so both transports use one code path. `lib/stdio-transport.js` is the extracted stdio loop; `lib/http-transport.js` is new.
|
|
147
309
|
- **package.json**:
|
|
148
310
|
- `version` 1.0.0 → 1.1.0
|
|
@@ -150,6 +312,7 @@ Supersedes 1.2.0, which was deprecated on npm. The 1.2.0 build shipped with a st
|
|
|
150
312
|
- new files included in the npm tarball: `deploy/`, `scripts/`, `CHANGELOG.md`
|
|
151
313
|
|
|
152
314
|
### Compatibility
|
|
315
|
+
|
|
153
316
|
- Default invocation is unchanged: `npx -y @pmoses-s1/s1-secops-mcp` still produces a stdio MCP server with identical behaviour to 1.0.0.
|
|
154
317
|
- Existing `claude_desktop_config.json` and `.mcp.json` configs work without modification.
|
|
155
318
|
- The 26 tools, 2 resources, and 2 prompts are unchanged from the late-1.0.0 line; only the documentation now matches reality.
|
|
@@ -157,6 +320,7 @@ Supersedes 1.2.0, which was deprecated on npm. The 1.2.0 build shipped with a st
|
|
|
157
320
|
## 1.0.0 - 2026-05-07
|
|
158
321
|
|
|
159
322
|
Initial public release.
|
|
323
|
+
|
|
160
324
|
- 19 tools across PowerQuery, S1 Mgmt REST, UAM, SDL API, Hyperautomation.
|
|
161
325
|
- stdio transport only.
|
|
162
326
|
- Credentials via env vars or auto-discovered `credentials.json`.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ See **[deploy/README.md](./deploy/README.md)** for the full deployment walkthrou
|
|
|
10
10
|
## What this exposes
|
|
11
11
|
|
|
12
12
|
<!-- BEGIN AUTO-GENERATED TOOLS TABLE -->
|
|
13
|
-
**
|
|
13
|
+
**32 tools** across PowerQuery, Mgmt Console, SDL API, Hyperautomation, and UAM Ingest:
|
|
14
14
|
|
|
15
15
|
| Group | Tool | Skill |
|
|
16
16
|
|-------|------|-------|
|
|
@@ -28,10 +28,16 @@ See **[deploy/README.md](./deploy/README.md)** for the full deployment walkthrou
|
|
|
28
28
|
| Mgmt Console | `uam_list_alerts` | mgmt-console-api |
|
|
29
29
|
| Mgmt Console | `uam_set_status` | mgmt-console-api |
|
|
30
30
|
| SDL API | `hec_ingest` | sdl-api / sdl-log-parser |
|
|
31
|
+
| SDL API | `sdl_create_dashboard` | sdl-api / sdl-dashboard |
|
|
32
|
+
| SDL API | `sdl_delete_dashboard` | sdl-api / sdl-dashboard |
|
|
31
33
|
| SDL API | `sdl_delete_file` | sdl-api |
|
|
34
|
+
| SDL API | `sdl_get_dashboard` | sdl-api / sdl-dashboard |
|
|
32
35
|
| SDL API | `sdl_get_file` | sdl-api / sdl-dashboard / sdl-log-parser |
|
|
36
|
+
| SDL API | `sdl_list_dashboards` | sdl-api / sdl-dashboard |
|
|
33
37
|
| SDL API | `sdl_list_files` | sdl-api / sdl-dashboard / sdl-log-parser |
|
|
34
38
|
| SDL API | `sdl_put_file` | sdl-api / sdl-dashboard / sdl-log-parser |
|
|
39
|
+
| SDL API | `sdl_save_dashboard_layout` | sdl-api / sdl-dashboard |
|
|
40
|
+
| SDL API | `sdl_share_dashboard` | sdl-api / sdl-dashboard |
|
|
35
41
|
| Hyperautomation | `ha_delete_workflow` | hyperautomation |
|
|
36
42
|
| Hyperautomation | `ha_export_workflow` | hyperautomation |
|
|
37
43
|
| Hyperautomation | `ha_get_workflow` | hyperautomation |
|
|
@@ -43,10 +49,12 @@ See **[deploy/README.md](./deploy/README.md)** for the full deployment walkthrou
|
|
|
43
49
|
<!-- END AUTO-GENERATED TOOLS TABLE -->
|
|
44
50
|
|
|
45
51
|
**2 resources:**
|
|
52
|
+
|
|
46
53
|
- `sentinelone://soc-context`: `CLAUDE.md`, the Principal SOC Analyst operating instructions.
|
|
47
54
|
- `sentinelone://credentials-status`: which credentials are configured and which API surfaces are available.
|
|
48
55
|
|
|
49
56
|
**2 prompts:**
|
|
57
|
+
|
|
50
58
|
- `soc_analyst`: embeds `CLAUDE.md` as a system prompt; call at session start.
|
|
51
59
|
- `session_init`: structured init: enumerate sources + triage alerts in parallel.
|
|
52
60
|
|
|
@@ -65,11 +73,11 @@ Add this to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):
|
|
|
65
73
|
"mcpServers": {
|
|
66
74
|
"s1-secops-mcp": {
|
|
67
75
|
"command": "npx",
|
|
68
|
-
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.3.
|
|
76
|
+
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.3.4"],
|
|
69
77
|
"env": {
|
|
70
78
|
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
71
79
|
"S1_CONSOLE_API_TOKEN": "eyJ...",
|
|
72
|
-
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net"
|
|
80
|
+
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net"
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
}
|
|
@@ -204,7 +212,7 @@ If neither env var is set, HTTP transport runs **without** authentication and th
|
|
|
204
212
|
|
|
205
213
|
Every authenticated HTTP request emits a structured stderr line that systemd captures via journald:
|
|
206
214
|
|
|
207
|
-
```
|
|
215
|
+
```json
|
|
208
216
|
[audit] 2026-05-28T15:01:22.413Z | alice | tools/call | name=powerquery_run | 200 ok
|
|
209
217
|
[audit] 2026-05-28T15:01:34.221Z | bob | tools/list | - | 200 ok
|
|
210
218
|
[audit] 2026-05-28T17:03:11.221Z | - | - | - | 401 unauthorized
|
|
@@ -434,7 +442,7 @@ The `maxRows` (`powerquery_run`) and `first` (`uam_list_alerts`) parameters are
|
|
|
434
442
|
|
|
435
443
|
## CLI reference
|
|
436
444
|
|
|
437
|
-
```
|
|
445
|
+
```text
|
|
438
446
|
s1-secops-mcp [options]
|
|
439
447
|
|
|
440
448
|
OPTIONS
|
|
@@ -448,7 +456,7 @@ OPTIONS
|
|
|
448
456
|
|
|
449
457
|
## Architecture
|
|
450
458
|
|
|
451
|
-
```
|
|
459
|
+
```text
|
|
452
460
|
s1-secops-mcp/
|
|
453
461
|
index.js Entry: flag parsing + transport selection
|
|
454
462
|
lib/
|
|
@@ -485,7 +493,7 @@ s1-secops-mcp/
|
|
|
485
493
|
| Purple AI GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
486
494
|
| UAM GraphQL | `Authorization: ApiToken <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
487
495
|
| UAM HEC ingest | `Authorization: Bearer <jwt>` | `S1_CONSOLE_API_TOKEN` |
|
|
488
|
-
| SDL config files (`POST /sdl/v2/graphql`) | `Authorization: Bearer <jwt>`,
|
|
496
|
+
| SDL config files (`POST /sdl/v2/graphql`) | `Authorization: Bearer <jwt>`, plus an `s1-scope` header that IS honoured: listings and reads are scope-filtered (measured 113 files at account scope vs 4 at a site scope) | `S1_CONSOLE_API_TOKEN`, optional `S1_SCOPE` |
|
|
489
497
|
|
|
490
498
|
## Testing
|
|
491
499
|
|
|
@@ -495,7 +503,7 @@ npm test
|
|
|
495
503
|
|
|
496
504
|
Three test suites under `tests/`:
|
|
497
505
|
|
|
498
|
-
- `smoke.test.mjs`: introspects `ALL_TOOLS` directly, no spawning. Asserts
|
|
506
|
+
- `smoke.test.mjs`: introspects `ALL_TOOLS` directly, no spawning. Asserts 32 tools by name; catches any drift between code and the README regenerator.
|
|
499
507
|
- `stdio-transport.test.mjs`: spawns the server in stdio mode, exercises `initialize`, `tools/list`, `resources/list`, `prompts/list`, and error handling.
|
|
500
508
|
- `http-transport.test.mjs`: spawns in HTTP mode on a random ephemeral port, exercises `/healthz`, `POST /mcp`, both auth-required and auth-optional flows, and the env-var token fallback.
|
|
501
509
|
|
package/deploy/README.md
CHANGED
|
@@ -21,6 +21,7 @@ bash /tmp/s1-mcp-install.sh --user
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
That runs `install.sh --user`, which:
|
|
24
|
+
|
|
24
25
|
1. Confirms Node 18+ is present (errors out with install hints if not).
|
|
25
26
|
2. Sets up a per-user npm prefix at `~/.npm-global` if one isn't configured.
|
|
26
27
|
3. Installs `@pmoses-s1/s1-secops-mcp` globally for your user.
|
|
@@ -33,7 +34,7 @@ Then edit `~/.config/sentinelone/credentials.json` with your real values:
|
|
|
33
34
|
{
|
|
34
35
|
"S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
|
|
35
36
|
"S1_CONSOLE_API_TOKEN": "eyJ...",
|
|
36
|
-
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net"
|
|
37
|
+
"S1_HEC_INGEST_URL": "https://ingest.us1.sentinelone.net"
|
|
37
38
|
}
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -56,7 +57,7 @@ Or, equivalently, by package name without the install:
|
|
|
56
57
|
"mcpServers": {
|
|
57
58
|
"s1-secops-mcp": {
|
|
58
59
|
"command": "npx",
|
|
59
|
-
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.3.
|
|
60
|
+
"args": ["-y", "@pmoses-s1/s1-secops-mcp@1.3.4"]
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -117,11 +118,13 @@ Team members connect from their Claude clients with their own bearer token. Audi
|
|
|
117
118
|
1. **Provision the VM.** Anything that runs systemd is fine: Ubuntu 22.04 LTS, Debian 12, Rocky/Alma 9, etc.
|
|
118
119
|
|
|
119
120
|
2. **Install Node 18+.** Pick one:
|
|
121
|
+
|
|
120
122
|
```bash
|
|
121
123
|
# Ubuntu / Debian
|
|
122
124
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
123
125
|
sudo apt install -y nodejs
|
|
124
126
|
```
|
|
127
|
+
|
|
125
128
|
```bash
|
|
126
129
|
# Rocky / Alma
|
|
127
130
|
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
|
|
@@ -129,12 +132,15 @@ Team members connect from their Claude clients with their own bearer token. Audi
|
|
|
129
132
|
```
|
|
130
133
|
|
|
131
134
|
3. **Run the installer in server mode:**
|
|
135
|
+
|
|
132
136
|
```bash
|
|
133
137
|
curl -fsSL https://raw.githubusercontent.com/pmoses-s1/claude-skills/main/s1-secops-mcp/deploy/install.sh | sudo bash -s -- --server
|
|
134
138
|
```
|
|
139
|
+
|
|
135
140
|
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.
|
|
136
141
|
|
|
137
142
|
4. **Fill in real SentinelOne credentials:**
|
|
143
|
+
|
|
138
144
|
```bash
|
|
139
145
|
sudo vim /etc/s1-secops-mcp/credentials.json
|
|
140
146
|
sudo systemctl reload s1-secops-mcp
|
|
@@ -142,15 +148,18 @@ Team members connect from their Claude clients with their own bearer token. Audi
|
|
|
142
148
|
```
|
|
143
149
|
|
|
144
150
|
5. **Put TLS in front with Caddy** (the recommended option):
|
|
151
|
+
|
|
145
152
|
```bash
|
|
146
153
|
sudo apt install -y caddy
|
|
147
154
|
sudo cp /usr/lib/node_modules/@pmoses-s1/s1-secops-mcp/deploy/caddy/Caddyfile.example /etc/caddy/Caddyfile
|
|
148
155
|
sudo vim /etc/caddy/Caddyfile # change mcp.s1.internal to your DNS name
|
|
149
156
|
sudo systemctl reload caddy
|
|
150
157
|
```
|
|
158
|
+
|
|
151
159
|
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.
|
|
152
160
|
|
|
153
161
|
6. **Add team members.** Generate a token per person and append to the file:
|
|
162
|
+
|
|
154
163
|
```bash
|
|
155
164
|
sudo bash -c 'cat > /etc/s1-secops-mcp/bearer-tokens.json' <<EOF
|
|
156
165
|
{
|
|
@@ -164,9 +173,11 @@ Team members connect from their Claude clients with their own bearer token. Audi
|
|
|
164
173
|
sudo chown mcp:mcp /etc/s1-secops-mcp/bearer-tokens.json
|
|
165
174
|
sudo systemctl reload s1-secops-mcp # SIGHUP, no downtime
|
|
166
175
|
```
|
|
176
|
+
|
|
167
177
|
Hand each person their token over a secure channel (1Password, Signal, etc.).
|
|
168
178
|
|
|
169
179
|
7. **Connect from a Claude client.** Each user adds the server to their config with their personal token:
|
|
180
|
+
|
|
170
181
|
```json
|
|
171
182
|
{
|
|
172
183
|
"mcpServers": {
|
|
@@ -182,6 +193,7 @@ Team members connect from their Claude clients with their own bearer token. Audi
|
|
|
182
193
|
```
|
|
183
194
|
|
|
184
195
|
8. **Verify end-to-end.** From a team member's machine:
|
|
196
|
+
|
|
185
197
|
```bash
|
|
186
198
|
curl -s -X POST https://mcp.s1.internal/mcp \
|
|
187
199
|
-H "Authorization: Bearer $TOKEN" \
|
|
@@ -191,6 +203,7 @@ Team members connect from their Claude clients with their own bearer token. Audi
|
|
|
191
203
|
```
|
|
192
204
|
|
|
193
205
|
9. **Watch the audit log.** Every authenticated request is logged with the bearer name, method, and param summary:
|
|
206
|
+
|
|
194
207
|
```bash
|
|
195
208
|
sudo journalctl -u s1-secops-mcp -f | grep '\[audit\]'
|
|
196
209
|
# [audit] 2026-05-28T15:01:22.413Z | alice | tools/call | name=powerquery_run | 200 ok
|
|
@@ -231,7 +244,7 @@ sudo systemctl restart s1-secops-mcp
|
|
|
231
244
|
|
|
232
245
|
The structured audit lines look like:
|
|
233
246
|
|
|
234
|
-
```
|
|
247
|
+
```json
|
|
235
248
|
[audit] 2026-05-28T15:01:22.413Z | alice | tools/call | name=powerquery_run | 200 ok
|
|
236
249
|
[audit] 2026-05-28T16:42:55.108Z | bob | tools/list | - | 200 ok
|
|
237
250
|
[audit] 2026-05-28T17:03:11.221Z | - | - | - | 401 unauthorized
|
|
@@ -296,7 +309,7 @@ The instance's `*.compute.internal` DNS name (e.g. `ip-172-31-7-227.ap-southeast
|
|
|
296
309
|
|
|
297
310
|
If you try to issue a cert for the EC2 public DNS, LE returns:
|
|
298
311
|
|
|
299
|
-
```
|
|
312
|
+
```text
|
|
300
313
|
HTTP 400 urn:ietf:params:acme:error:rejectedIdentifier
|
|
301
314
|
The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy
|
|
302
315
|
```
|
|
@@ -363,7 +376,7 @@ Both block the W+X memory mappings V8 needs to JIT JavaScript. Adding them cause
|
|
|
363
376
|
|
|
364
377
|
These are supported but not first-class:
|
|
365
378
|
|
|
366
|
-
- **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.3.
|
|
379
|
+
- **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.3.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`.
|
|
367
380
|
|
|
368
381
|
- **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.
|
|
369
382
|
|
package/deploy/bridge/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS,
|
|
|
46
46
|
|
|
47
47
|
`MCP_URL` is whatever URL your team's admin gave you (it should end in `/mcp`). `MCP_BEARER` is your personal bearer token from `/etc/s1-secops-mcp/bearer-tokens.json` on the VM.
|
|
48
48
|
|
|
49
|
-
Quit Claude Desktop fully (Cmd+Q on macOS, not just close the window) and reopen. The
|
|
49
|
+
Quit Claude Desktop fully (Cmd+Q on macOS, not just close the window) and reopen. The 32 tools should appear in the tools list.
|
|
50
50
|
|
|
51
51
|
## Smoke test (without Claude Desktop)
|
|
52
52
|
|
|
@@ -76,7 +76,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
|
|
|
76
76
|
|
|
77
77
|
## How it works
|
|
78
78
|
|
|
79
|
-
```
|
|
79
|
+
```text
|
|
80
80
|
+----------------+ stdio JSON-RPC +--------+ HTTPS POST /mcp +------+
|
|
81
81
|
| Claude Desktop | <------------------------------> | bridge | <---------------------------> | VM |
|
|
82
82
|
+----------------+ +--------+ Bearer auth, JSON in/out +------+
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* MCP_URL=... MCP_BEARER=... bash -c '
|
|
34
34
|
* echo "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}" \
|
|
35
35
|
* | node s1-secops-mcp-bridge.mjs'
|
|
36
|
-
* # -> JSON-RPC response with
|
|
36
|
+
* # -> JSON-RPC response with 32 tools in result.tools[]
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { createInterface } from 'node:readline';
|
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Streamable HTTP, using raw JSON-RPC 2.0 throughout. No external runtime
|
|
7
7
|
* dependencies. Pure Node.js 18+.
|
|
8
8
|
*
|
|
9
|
-
* Exposes
|
|
9
|
+
* Exposes 32 tools across PowerQuery, Mgmt Console REST, UAM, SDL API,
|
|
10
10
|
* Hyperautomation, and UAM Ingest; plus 2 resources and 2 prompts.
|
|
11
11
|
*
|
|
12
12
|
* Quick start:
|
package/lib/credentials.js
CHANGED
|
@@ -106,6 +106,12 @@ export function getCreds() {
|
|
|
106
106
|
S1_CONSOLE_URL: e('S1_CONSOLE_URL'),
|
|
107
107
|
S1_CONSOLE_API_TOKEN: e('S1_CONSOLE_API_TOKEN') || e('S1_API_TOKEN'),
|
|
108
108
|
S1_HEC_INGEST_URL: e('S1_HEC_INGEST_URL'),
|
|
109
|
+
// Default S1-Scope for SDL requests: "<accountId>" for account scope or
|
|
110
|
+
// "<accountId>:<siteId>" for site scope. Optional. Per-call scope arguments
|
|
111
|
+
// override it; passing scope:null suppresses it entirely. SDL reads are
|
|
112
|
+
// scope-FILTERED, so this value decides which config files and dashboards a
|
|
113
|
+
// session can see at all, not merely where new ones are filed.
|
|
114
|
+
S1_SCOPE: e('S1_SCOPE'),
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
|