@pmoses-s1/sentinelone-mcp 1.2.0 → 1.2.2

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 CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## 1.2.0 2026-06-11
3
+ ## 1.2.2 - 2026-06-13
4
+
5
+ ### Changed
6
+ - **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.
7
+ - **`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 `sentinelone-powerquery/references/datasource-command.md`.
8
+
9
+ ### Notes
10
+ - Tool count unchanged at 26 (the Hyperautomation tool was renamed, not added or removed).
11
+ - `SERVER_INFO.version` bumped in lockstep with `package.json` (the drift that forced the 1.2.0 -> 1.2.1 re-release).
12
+
13
+ ## 1.2.1 - 2026-06-11
14
+
15
+ 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.
4
16
 
5
17
  ### Added
6
18
  - **`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).
package/README.md CHANGED
@@ -32,7 +32,7 @@ See **[deploy/README.md](./deploy/README.md)** for the full deployment walkthrou
32
32
  | SDL API | `sdl_list_files` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
33
33
  | SDL API | `sdl_put_file` | sentinelone-sdl-api / sdl-dashboard / sdl-log-parser |
34
34
  | SDL API | `hec_ingest` | sentinelone-sdl-api / sdl-log-parser |
35
- | Hyperautomation | `ha_archive_workflow` | sentinelone-hyperautomation |
35
+ | Hyperautomation | `ha_delete_workflow` | sentinelone-hyperautomation |
36
36
  | Hyperautomation | `ha_export_workflow` | sentinelone-hyperautomation |
37
37
  | Hyperautomation | `ha_get_workflow` | sentinelone-hyperautomation |
38
38
  | Hyperautomation | `ha_import_workflow` | sentinelone-hyperautomation |
@@ -65,7 +65,7 @@ Add this to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):
65
65
  "mcpServers": {
66
66
  "sentinelone-mcp": {
67
67
  "command": "npx",
68
- "args": ["-y", "@pmoses-s1/sentinelone-mcp@1.1.0"],
68
+ "args": ["-y", "@pmoses-s1/sentinelone-mcp@1.2.2"],
69
69
  "env": {
70
70
  "S1_CONSOLE_URL": "https://usea1-yourorg.sentinelone.net",
71
71
  "S1_CONSOLE_API_TOKEN": "eyJ...",
@@ -463,7 +463,7 @@ sentinelone-mcp/
463
463
  powerquery.js PowerQuery enumerate/run/schema-discover
464
464
  mgmt-console.js S1 REST verbs + Purple AI summary + UAM
465
465
  sdl-api.js SDL config file + log ingestion tools
466
- hyperautomation.js Hyperautomation list/get/import/export/archive
466
+ hyperautomation.js Hyperautomation list/get/import/export/delete
467
467
  uam-ingest.js UAM Alert Interface ingestion tools
468
468
  deploy/
469
469
  install.sh One-shot installer (Mac and Linux)
package/deploy/README.md CHANGED
@@ -52,7 +52,7 @@ Or, equivalently, by package name without the install:
52
52
  "mcpServers": {
53
53
  "sentinelone-mcp": {
54
54
  "command": "npx",
55
- "args": ["-y", "@pmoses-s1/sentinelone-mcp@1.1.0"]
55
+ "args": ["-y", "@pmoses-s1/sentinelone-mcp@1.2.2"]
56
56
  }
57
57
  }
58
58
  }
@@ -359,7 +359,7 @@ Both block the W+X memory mappings V8 needs to JIT JavaScript. Adding them cause
359
359
 
360
360
  These are supported but not first-class:
361
361
 
362
- - **Docker / docker-compose.** Not shipped in this version. The single-file Node binary doesn't need it. If you want a container, the install is `FROM node:20-alpine` + `RUN npm install -g @pmoses-s1/sentinelone-mcp@1.1.0` + `CMD ["sentinelone-mcp", "--transport", "http", "--host", "0.0.0.0"]`. Mount creds at `/etc/sentinelone-mcp/credentials.json` and tokens at `/etc/sentinelone-mcp/bearer-tokens.json`.
362
+ - **Docker / docker-compose.** Not shipped in this version. The single-file Node binary doesn't need it. If you want a container, the install is `FROM node:20-alpine` + `RUN npm install -g @pmoses-s1/sentinelone-mcp@1.2.2` + `CMD ["sentinelone-mcp", "--transport", "http", "--host", "0.0.0.0"]`. Mount creds at `/etc/sentinelone-mcp/credentials.json` and tokens at `/etc/sentinelone-mcp/bearer-tokens.json`.
363
363
 
364
364
  - **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.
365
365
 
@@ -94,7 +94,7 @@ const PROMPTS = [
94
94
 
95
95
  export const SERVER_INFO = {
96
96
  name: 'sentinelone-mcp-server',
97
- version: '1.1.0',
97
+ version: '1.2.2',
98
98
  };
99
99
 
100
100
  export const PROTOCOL_VERSION = '2024-11-05';
@@ -203,9 +203,9 @@ export async function dispatch(method, params, id) {
203
203
  text: `Begin a new SOC analyst session. Follow this initialization sequence:
204
204
 
205
205
  1. Call \`powerquery_enumerate_sources\` to discover active SDL data sources (MANDATORY, never assume sources from prior sessions).
206
- 2. In parallel, call \`uam_list_alerts\` with filter="status=OPEN" to pull active alerts.
206
+ 2. In parallel, call \`uam_list_alerts\` with status="NEW" to pull untriaged active alerts. Valid status values are "NEW", "IN_PROGRESS", or "RESOLVED" (there is no "OPEN" status; it silently returns 0 results). Omit the status argument to pull the most recent alerts across all states.
207
207
  3. For each discovered data source not already in the schema registry, plan schema discovery via \`powerquery_schema_discover\`.
208
- 4. Report: (a) active data sources list, (b) open alert count and top 5 by severity, (c) which sources need schema discovery.
208
+ 4. Report: (a) active data sources list, (b) untriaged (NEW) alert count and top 5 by severity, (c) which sources need schema discovery.
209
209
 
210
210
  Apply the SOC analyst context from the soc_analyst prompt throughout.`,
211
211
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmoses-s1/sentinelone-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "MCP server orchestrating SentinelOne skills, APIs, and SOC analyst context. Stdio or Streamable HTTP transport with per-user bearer auth for team deployments.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -48,7 +48,7 @@ const TOOL_SKILL = {
48
48
  // Hyperautomation
49
49
  ha_list_workflows: 'sentinelone-hyperautomation',
50
50
  ha_get_workflow: 'sentinelone-hyperautomation',
51
- ha_archive_workflow: 'sentinelone-hyperautomation',
51
+ ha_delete_workflow: 'sentinelone-hyperautomation',
52
52
  ha_import_workflow: 'sentinelone-hyperautomation',
53
53
  ha_export_workflow: 'sentinelone-hyperautomation',
54
54
  // UAM Ingest
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # Mac validation script for sentinelone-mcp v1.1.0.
3
+ # Mac validation script for sentinelone-mcp v1.2.2.
4
4
  #
5
5
  # Runs the same test matrix as Linux:
6
6
  # - syntax-check every .js/.mjs file
@@ -100,12 +100,12 @@ fi
100
100
  # ─── 5. CLI flag sanity ───────────────────────────────────────────────────────
101
101
 
102
102
  step "5. CLI flag sanity"
103
- if [[ "$(node index.js --version 2>/dev/null)" == "1.1.0" ]]; then
104
- pass "--version returns 1.1.0"
103
+ if [[ "$(node index.js --version 2>/dev/null)" == "1.2.2" ]]; then
104
+ pass "--version returns 1.2.2"
105
105
  else
106
- fail "--version did not return 1.1.0" "Got: $(node index.js --version 2>&1)"
106
+ fail "--version did not return 1.2.2" "Got: $(node index.js --version 2>&1)"
107
107
  fi
108
- if node index.js --help 2>&1 | grep -q "sentinelone-mcp 1.1.0"; then
108
+ if node index.js --help 2>&1 | grep -q "sentinelone-mcp 1.2.2"; then
109
109
  pass "--help renders"
110
110
  else
111
111
  fail "--help broken" "$(node index.js --help 2>&1 | head -5)"
@@ -4,8 +4,8 @@
4
4
  * Tools:
5
5
  * ha_list_workflows List Hyperautomation workflows (with scope/state/sort filters)
6
6
  * ha_get_workflow Get a single workflow by ID (+ optional revisionId)
7
- * ha_archive_workflow Archive (soft-delete) a workflow
8
- * ha_import_workflow Import (create) a workflow from JSON
7
+ * ha_delete_workflow Delete (soft, recoverable) a workflow via REST DELETE
8
+ * ha_import_workflow Import (create) a workflow from JSON (account- or site-scoped)
9
9
  * ha_export_workflow Export all workflows as a ZIP archive
10
10
  *
11
11
  * API root (confirmed via live network capture 2026-05-03):
@@ -15,14 +15,15 @@
15
15
  * GET /workflows/single/{workflowId}/{revisionId}
16
16
  * The revisionId is workflow.version_id in the list response.
17
17
  *
18
- * Deletion is a soft-archive, not HTTP DELETE:
19
- * POST /workflows/archive { workflowIds: [<uuid>, ...] }
18
+ * Deletion is a soft, recoverable HTTP DELETE (validated 2026-06-13):
19
+ * DELETE /workflows/{id}?accountIds=<acct> (or ?siteIds=<site>)
20
+ * The older POST /workflows/archive returns 500 on this tenant — do not use it.
20
21
  *
21
- * Export/import paths were NOT captured in the network trace kept at their
22
- * previously confirmed /public paths until the /v1 equivalents are verified.
22
+ * Import scope: the public import endpoint accepts ?accountIds=<acct> or
23
+ * ?siteIds=<site>; with no scope it returns a misleading 403 on a scoped tenant.
23
24
  */
24
25
 
25
- import { apiGet, apiPost } from '../lib/s1.js';
26
+ import { apiGet, apiPost, apiDelete } from '../lib/s1.js';
26
27
 
27
28
  // Confirmed base path from live network monitor (2026-05-03).
28
29
  const HA_BASE = '/web/api/v2.1/hyper-automate/api/v1';
@@ -156,36 +157,57 @@ export const tools = [
156
157
  },
157
158
  },
158
159
 
159
- // ─── ha_archive_workflow ──────────────────────────────────────────────────
160
+ // ─── ha_delete_workflow ───────────────────────────────────────────────────
160
161
  {
161
- name: 'ha_archive_workflow',
162
- description: `Archive (soft-delete) one or more Hyperautomation workflows. Archive is the console's delete operation the workflow is removed from the active list but the underlying data is retained. Equivalent to clicking Delete in the Hyperautomation UI. Requires Hyper Automate.write permission. Returns the API response (200 OK on success). This action is NOT easily reversibleconfirm with the user before calling.`,
162
+ name: 'ha_delete_workflow',
163
+ description: `Delete one or more Hyperautomation workflows. Uses the REST DELETE /hyper-automate/api/v1/workflows/{id} endpoint (validated 2026-06-13) a soft, recoverable delete (the console offers a "Restore workflow" action), equivalent to clicking Delete in the Hyperautomation UI. Scope the call to where the workflow lives with accountIds (account-scoped workflow) or siteIds (site-scoped); a 404 "Object not found" means the id is not under that scope or is already deleted. Requires Hyper Automate.write permission. NOTE: do NOT use the older POST /workflows/archive path it returns 500 on this tenant.`,
163
164
  inputSchema: {
164
165
  type: 'object',
165
166
  properties: {
166
167
  workflowIds: {
167
168
  type: 'array',
168
169
  items: { type: 'string' },
169
- description: 'One or more workflow UUIDs to archive (from ha_list_workflows).',
170
+ description: 'One or more workflow UUIDs to delete (from ha_list_workflows).',
171
+ },
172
+ accountIds: {
173
+ type: 'string',
174
+ description: 'Account scope for an account-level workflow (e.g. "2046190533732727925"). Provide this OR siteIds.',
175
+ },
176
+ siteIds: {
177
+ type: 'string',
178
+ description: 'Site scope for a site-level workflow. Provide this OR accountIds.',
170
179
  },
171
180
  },
172
181
  required: ['workflowIds'],
173
182
  },
174
- async handler({ workflowIds }) {
183
+ async handler({ workflowIds, accountIds, siteIds } = {}) {
175
184
  if (!Array.isArray(workflowIds) || workflowIds.length === 0) {
176
185
  return JSON.stringify({ error: 'workflowIds must be a non-empty array of UUIDs.' });
177
186
  }
178
- // Confirmed: POST /workflows/archive with IDs in request body.
179
- // This is the backend call behind the UI Delete button.
180
- const result = await apiPost(`${HA_BASE}/workflows/archive`, { workflowIds });
181
- return JSON.stringify(result, null, 2);
187
+ if (!accountIds && !siteIds) {
188
+ return JSON.stringify({ error: 'Provide accountIds or siteIds to scope the delete to where the workflow lives (a workflow created at a site must be deleted with siteIds; an account-scoped one with accountIds).' });
189
+ }
190
+ const scope = accountIds
191
+ ? `accountIds=${encodeURIComponent(accountIds)}`
192
+ : `siteIds=${encodeURIComponent(siteIds)}`;
193
+ const results = [];
194
+ for (const id of workflowIds) {
195
+ try {
196
+ // DELETE returns 204 No Content on success.
197
+ await apiDelete(`${HA_BASE}/workflows/${encodeURIComponent(id)}?${scope}`);
198
+ results.push({ id, status: 'deleted' });
199
+ } catch (e) {
200
+ results.push({ id, status: 'error', error: e.message });
201
+ }
202
+ }
203
+ return JSON.stringify({ deleted: results }, null, 2);
182
204
  },
183
205
  },
184
206
 
185
207
  // ─── ha_import_workflow ───────────────────────────────────────────────────
186
208
  {
187
209
  name: 'ha_import_workflow',
188
- description: `Import a Hyperautomation workflow JSON into the SentinelOne console. The workflow JSON must follow the Hyperautomation schema (use the sentinelone-hyperautomation skill to generate valid JSON). Integration-backed actions (type=http_request with an integration_id) require pre-configured connections in Hyperautomation > Integrations before the workflow will run. The import API validates schema but cannot check if integrations are configured. Returns the created workflow ID on success. Requires Hyper Automate.write permission.`,
210
+ description: `Import a Hyperautomation workflow JSON into the SentinelOne console. Scope the import with accountIds (account-level) or siteIds (site-level); on a scoped tenant a bare import with no scope returns a misleading 403 "Insufficient permissions". The workflow JSON must follow the Hyperautomation schema (use the sentinelone-hyperautomation skill to generate valid JSON). Integration-backed actions (type=http_request with an integration_id) require pre-configured connections in Hyperautomation > Integrations before the workflow will run, and an imported flow lands as a private draft until published (publish endpoint) or activated. Returns the created workflow ID on success. Requires Hyper Automate.write permission.`,
189
211
  inputSchema: {
190
212
  type: 'object',
191
213
  properties: {
@@ -193,19 +215,31 @@ export const tools = [
193
215
  type: 'string',
194
216
  description: 'Full Hyperautomation workflow JSON as a string. Must be valid Hyperautomation schema. Generate this using the sentinelone-hyperautomation skill.',
195
217
  },
218
+ accountIds: {
219
+ type: 'string',
220
+ description: 'Account scope for an account-level import (e.g. "2046190533732727925"). Provide this OR siteIds.',
221
+ },
222
+ siteIds: {
223
+ type: 'string',
224
+ description: 'Site scope for a site-level import. Provide this OR accountIds.',
225
+ },
196
226
  },
197
227
  required: ['workflowJson'],
198
228
  },
199
- async handler({ workflowJson }) {
229
+ async handler({ workflowJson, accountIds, siteIds }) {
200
230
  let parsed;
201
231
  try {
202
232
  parsed = JSON.parse(workflowJson);
203
233
  } catch (e) {
204
234
  return JSON.stringify({ error: `Invalid JSON: ${e.message}` });
205
235
  }
206
- // Path confirmed working during backtest (returns 403 without write permission).
207
- // /v1 equivalent not yet captured keeping /public path until verified.
208
- const result = await apiPost(`${HA_PUBLIC}/workflow-import-export/import`, { data: parsed });
236
+ // Public import endpoint. Append the scope query param: account-level imports use
237
+ // ?accountIds=, site-level use ?siteIds=. A bare import (no scope) returns a misleading
238
+ // 403 on a scoped tenant (validated 2026-06-13).
239
+ let qs = '';
240
+ if (accountIds) qs = `?accountIds=${encodeURIComponent(accountIds)}`;
241
+ else if (siteIds) qs = `?siteIds=${encodeURIComponent(siteIds)}`;
242
+ const result = await apiPost(`${HA_PUBLIC}/workflow-import-export/import${qs}`, { data: parsed });
209
243
  return JSON.stringify(result, null, 2);
210
244
  },
211
245
  },
@@ -45,7 +45,7 @@ export const tools = [
45
45
  properties: {
46
46
  query: {
47
47
  type: 'string',
48
- description: 'The PowerQuery string. Use pipe-separated commands: | filter | group | sort | limit | columns. Three distinct wildcard idioms — use the right one: (1) FIELD PRESENCE / ATTRIBUTE WILDCARD: field=* means "field is present/non-null", e.g. dataSource.name=* | group count=count() by dataSource.name — use this as a query-opener or whenever you need "all events that have this field". (2) ALL-COLUMN TEXT SEARCH: * contains \'value\' or * matches \'regex\' in the initial filter (before the first |) searches ALL indexed fields — use when the user asks to find text anywhere in the event, e.g. dataSource.name=\'MySource\' * contains \'evil.com\'. Dramatically faster than message contains. (3) EMPTY FILTER (all events): start with | and no initial predicate, e.g. | group ct=count() by event.type. Do NOT use bare * alone as the initial filter — that causes HTTP 500 ("Don\'t understand [*]").',
48
+ description: 'The PowerQuery string. Use pipe-separated commands: | filter | group | sort | limit | columns. Three distinct wildcard idioms — use the right one: (1) FIELD PRESENCE / ATTRIBUTE WILDCARD: field=* means "field is present/non-null", e.g. dataSource.name=* | group count=count() by dataSource.name — use this as a query-opener or whenever you need "all events that have this field". (2) ALL-COLUMN TEXT SEARCH: * contains \'value\' or * matches \'regex\' in the initial filter (before the first |) searches ALL indexed fields — use when the user asks to find text anywhere in the event, e.g. dataSource.name=\'MySource\' * contains \'evil.com\'. Dramatically faster than message contains. (3) EMPTY FILTER (all events): start with | and no initial predicate, e.g. | group ct=count() by event.type. Do NOT use bare * alone as the initial filter — that causes HTTP 500 ("Don\'t understand [*]"). Beyond filtering, | datasource <name> [from <dataset>] reads SentinelOne-managed inventory (assets, alerts, vulnerabilities, misconfigurations, metering; e.g. | datasource assets from \'surface/identity\') and | savelookup \'<name>\' persists the result as a reusable lookup table (see references/datasource-command.md in sentinelone-powerquery).',
49
49
  },
50
50
  startTime: {
51
51
  type: 'string',