@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/tools/sdl-api.js CHANGED
@@ -1,20 +1,34 @@
1
1
  /**
2
2
  * SDL API tools: sdl-api, sdl-dashboard, sdl-log-parser skills
3
3
  *
4
- * Tools:
5
- * sdl_list_files List every config file on the SDL tenant (GraphQL configFiles)
6
- * sdl_get_file Get file content and version, by path or udoId
7
- * sdl_put_file Deploy or update a config file (with optimistic locking)
8
- * sdl_delete_file Delete a config file
9
- * hec_ingest Ingest raw logs/events into SDL via the HEC endpoint (replaces uploadLogs)
4
+ * Config-file tools (raw SDL config layer):
5
+ * sdl_list_files List every config file visible at a scope
6
+ * sdl_get_file Get file content and version, by path or udoId
7
+ * sdl_put_file Deploy or update a config file (optimistic locking)
8
+ * sdl_delete_file Delete a config file
10
9
  *
11
- * All four config-file tools run on `POST /sdl/v2/graphql`. The legacy REST
10
+ * Dashboard lifecycle tools (dashboardsV2, the console's own surface):
11
+ * sdl_list_dashboards List dashboards with owner and sharing metadata
12
+ * sdl_get_dashboard Read one dashboard including its tabs
13
+ * sdl_create_dashboard Create from a full dashboard-JSON config
14
+ * sdl_share_dashboard Share to a site / account / global scope
15
+ * sdl_save_dashboard_layout Replace the panel layout of one tab
16
+ * sdl_delete_dashboard Delete a dashboard
17
+ *
18
+ * Ingest:
19
+ * hec_ingest Ingest raw logs/events into SDL via HEC
20
+ *
21
+ * Everything except hec_ingest runs on `POST /sdl/v2/graphql`. The legacy REST
12
22
  * `/sdl/api/*File` endpoints are NOT used: they silently omit every
13
23
  * udoId-addressed dashboard (1,914 vs 2,264 files on a live tenant) and return
14
24
  * `success/noSuchFile` for any of them.
15
25
  */
16
26
 
17
- import { configFiles, configFile, putConfigFile, deleteConfigFile } from '../lib/sdl.js';
27
+ import {
28
+ configFiles, configFile, putConfigFile, deleteConfigFile,
29
+ listDashboards, getDashboard, createDashboard, shareDashboard,
30
+ saveDashboardLayout, deleteDashboard,
31
+ } from '../lib/sdl.js';
18
32
  import { hecIngest } from '../lib/hec.js';
19
33
 
20
34
  const UDOID_NOTE =
@@ -24,11 +38,25 @@ const UDOID_NOTE =
24
38
  'udoId assignment is by namespace: only /dashboards/ files have one, /lookups/, /datatables/, ' +
25
39
  '/logParsers/ and /automaticLookups are all name-addressed with udoId null.';
26
40
 
41
+ const SCOPE_NOTE =
42
+ 'SDL config objects are filed against the scope of the request and reads are FILTERED by it, so this ' +
43
+ 'argument changes which objects exist as far as the caller can tell. Verified live: the same listing ' +
44
+ 'returned 113 files at account scope and 4 at a site scope. Format "<accountId>" for account scope or ' +
45
+ '"<accountId>:<siteId>" for site scope; ids come from GET /web/api/v2.1/accounts and /sites. Omit to ' +
46
+ 'use S1_SCOPE from credentials.json, or the token default when that is unset. If an object you expect ' +
47
+ 'is missing, re-check at the scope it was created in before concluding it is gone.';
48
+
49
+ /** Shared scope property for every tool schema. */
50
+ const scopeProp = {
51
+ type: 'string',
52
+ description: `Optional S1-Scope, e.g. "2046190533732727925:2547662415802335157". ${SCOPE_NOTE}`,
53
+ };
54
+
27
55
  export const tools = [
28
56
  // ─── sdl_list_files ───────────────────────────────────────────────────────
29
57
  {
30
58
  name: 'sdl_list_files',
31
- description: `List every configuration file on the SDL tenant via the GraphQL configFiles query: /logParsers/, /dashboards/, /alerts/, /lookups/, /datatables/, /automaticLookups. Returns {udoId, name, readOnly, version} per file. ${UDOID_NOTE} Use this to discover what is deployed, and to resolve a dashboard name to the udoId that sdl_get_file/sdl_put_file need. Never conclude a file is absent from a listing produced any other way; the legacy REST listing omits ~350 dashboards.`,
59
+ description: `List every configuration file visible at the given scope via the GraphQL configFiles query: /logParsers/, /dashboards/, /alerts/, /lookups/, /datatables/, /automaticLookups. Returns {udoId, name, readOnly, version} per file. ${UDOID_NOTE} Use this to discover what is deployed, and to resolve a dashboard name to the udoId that sdl_get_file/sdl_put_file need. THIS LISTING IS SCOPE-FILTERED: ${SCOPE_NOTE} Never conclude a file is absent from a listing produced any other way; the legacy REST listing omits ~350 dashboards.`,
32
60
  inputSchema: {
33
61
  type: 'object',
34
62
  properties: {
@@ -36,13 +64,14 @@ export const tools = [
36
64
  type: 'string',
37
65
  description: 'Optional filter, e.g. "/dashboards/" or "/logParsers/". Applied client-side to the full listing.',
38
66
  },
67
+ scope: scopeProp,
39
68
  },
40
69
  required: [],
41
70
  },
42
- async handler({ pathPrefix } = {}) {
43
- let files = await configFiles();
71
+ async handler({ pathPrefix, scope } = {}) {
72
+ let files = await configFiles({ scope });
44
73
  if (pathPrefix) files = files.filter(f => (f.name || '').startsWith(pathPrefix));
45
- return JSON.stringify({ count: files.length, files }, null, 2);
74
+ return JSON.stringify({ count: files.length, scope: scope ?? null, files }, null, 2);
46
75
  },
47
76
  },
48
77
 
@@ -61,15 +90,17 @@ export const tools = [
61
90
  type: 'string',
62
91
  description: 'Dashboard udoId, e.g. "3559330396332032". Get it from sdl_list_files. Required for /dashboards/ files.',
63
92
  },
93
+ scope: scopeProp,
64
94
  },
65
95
  required: [],
66
96
  },
67
- async handler({ path, udoId }) {
68
- const result = await configFile({ name: path, udoId });
97
+ async handler({ path, udoId, scope }) {
98
+ const result = await configFile({ name: path, udoId, scope });
69
99
  if (!result) {
70
100
  return JSON.stringify({
71
101
  status: 'notFound',
72
- hint: 'No file at that address. If this is a dashboard, it is udoId-addressed: run sdl_list_files with pathPrefix "/dashboards/" and retry with its udoId.',
102
+ scope: scope ?? null,
103
+ hint: 'No file at that address AT THIS SCOPE. Two things to check: (1) if this is a dashboard it is udoId-addressed, run sdl_list_files with pathPrefix "/dashboards/" and retry with its udoId; (2) the file may exist at a different scope, a site-scoped dashboard is invisible from account scope, so retry with the scope it was created in.',
73
104
  }, null, 2);
74
105
  }
75
106
  return JSON.stringify(result, null, 2);
@@ -97,13 +128,14 @@ export const tools = [
97
128
  },
98
129
  expectedVersion: {
99
130
  type: 'number',
100
- description: 'Current file version from sdl_get_file, for optimistic locking. Honoured on udoId-addressed writes. Omit when creating a new file.',
131
+ description: 'Current file version from sdl_get_file, for optimistic locking. Enforced on BOTH address forms, path and udoId: a stale value is rejected and nothing is written. Omit only when creating a new file.',
101
132
  },
133
+ scope: scopeProp,
102
134
  },
103
135
  required: ['content'],
104
136
  },
105
- async handler({ path, udoId, content, expectedVersion }) {
106
- const result = await putConfigFile({ name: path, udoId, content, expectedVersion });
137
+ async handler({ path, udoId, content, expectedVersion, scope }) {
138
+ const result = await putConfigFile({ name: path, udoId, content, expectedVersion, scope });
107
139
  return JSON.stringify(result, null, 2);
108
140
  },
109
141
  },
@@ -127,11 +159,147 @@ export const tools = [
127
159
  type: 'number',
128
160
  description: 'Current file version for optimistic locking (from sdl_get_file). Strongly recommended.',
129
161
  },
162
+ scope: scopeProp,
163
+ },
164
+ required: [],
165
+ },
166
+ async handler({ path, udoId, expectedVersion, scope }) {
167
+ const result = await deleteConfigFile({ name: path, udoId, expectedVersion, scope });
168
+ return JSON.stringify(result, null, 2);
169
+ },
170
+ },
171
+
172
+ // ─── sdl_list_dashboards ──────────────────────────────────────────────────
173
+ {
174
+ name: 'sdl_list_dashboards',
175
+ description: `List dashboards visible at the given scope via the GraphQL dashboardsV2 query, returning {id, name, description, configType, access:{public, users, owner}} each. Prefer this over sdl_list_files when you need the owner or the sharing state; use sdl_list_files when you need the config-file version for optimistic locking. The "id" here IS the "udoId" in sdl_list_files, they address the same object. ${SCOPE_NOTE}`,
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: { scope: scopeProp },
179
+ required: [],
180
+ },
181
+ async handler({ scope } = {}) {
182
+ const dashboards = await listDashboards({ scope });
183
+ return JSON.stringify({ count: dashboards.length, scope: scope ?? null, dashboards }, null, 2);
184
+ },
185
+ },
186
+
187
+ // ─── sdl_get_dashboard ────────────────────────────────────────────────────
188
+ {
189
+ name: 'sdl_get_dashboard',
190
+ description: `Read one dashboard including its tabs, description, duration, sharing and authorship, via getDashboardV2. Address by id (preferred) or name. NOTE tabs[].graphs, .parameters, .filters and .options come back as JSON STRINGS, not objects; parse each one to inspect panels. The "version" field here is a display string and is usually empty; it is NOT the optimistic-locking token, use sdl_get_file for the numeric version. Returns status notFound when the dashboard does not exist at this scope. ${SCOPE_NOTE}`,
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ id: { type: 'string', description: 'Dashboard id, e.g. "6994516145065984". Same value as udoId in sdl_list_files.' },
195
+ name: { type: 'string', description: 'Dashboard display name, e.g. "Metacortex Site". Use when you do not have the id; ambiguous if duplicates exist.' },
196
+ scope: scopeProp,
197
+ },
198
+ required: [],
199
+ },
200
+ async handler({ id, name, scope }) {
201
+ const result = await getDashboard({ id, name, scope });
202
+ if (!result) {
203
+ return JSON.stringify({
204
+ status: 'notFound',
205
+ scope: scope ?? null,
206
+ hint: 'No dashboard at that address AT THIS SCOPE. A site-scoped dashboard is invisible from account scope and vice versa; retry with the scope it was created in, or run sdl_list_dashboards at that scope to confirm.',
207
+ }, null, 2);
208
+ }
209
+ return JSON.stringify(result, null, 2);
210
+ },
211
+ },
212
+
213
+ // ─── sdl_create_dashboard ─────────────────────────────────────────────────
214
+ {
215
+ name: 'sdl_create_dashboard',
216
+ description: `Create a dashboard from a complete dashboard-JSON document via createDashboardV2, filed at the given scope. THIS IS THE PREFERRED WAY TO DEPLOY A NEW DASHBOARD: it accepts the whole document (configType, duration, description, tabs[]) in one call, unlike sdl_put_file which writes the raw config file. It also avoids the console's stub-append trap, where creating an empty dashboard in the UI and pasting JSON after the existing "{graphs: []}" stub yields "Content is invalid json / Additional text after JSON object" and leaves an empty dashboard behind. To deploy to a SITE, either pass scope as "<accountId>:<siteId>" here, or create at account scope and then use sdl_share_dashboard. Duplicate names ARE allowed (the console itself makes "<name> - Copy" siblings); set failIfNameExists to refuse instead. ${SCOPE_NOTE}`,
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties: {
220
+ name: { type: 'string', description: 'Dashboard display name, e.g. "Metacortex Site Replica".' },
221
+ config: { type: 'string', description: 'The full dashboard JSON document as a string: {"configType":"TABBED","duration":"24h","description":"...","tabs":[...]}. Validated as JSON before the mutation is sent.' },
222
+ isPublic: { type: 'boolean', description: 'Share with all users in scope (the console\'s "Public" badge). Default false.' },
223
+ failIfNameExists: { type: 'boolean', description: 'Refuse if a dashboard of this name already exists at this scope. Default false, which permits siblings. Costs one extra listing call.' },
224
+ scope: scopeProp,
225
+ },
226
+ required: ['name', 'config'],
227
+ },
228
+ async handler({ name, config, isPublic, failIfNameExists, scope }) {
229
+ const result = await createDashboard({ name, config, isPublic, failIfNameExists, scope });
230
+ return JSON.stringify({ status: 'created', scope: scope ?? null, dashboard: result }, null, 2);
231
+ },
232
+ },
233
+
234
+ // ─── sdl_share_dashboard ──────────────────────────────────────────────────
235
+ {
236
+ name: 'sdl_share_dashboard',
237
+ description: 'Share (or unshare) a dashboard to one or more scopes and/or users via the shareResource mutation. THIS IS THE ONLY SDL OPERATION THAT TAKES AN EXPLICIT SCOPE TARGET; every other operation infers scope from the request header. Use it to push an account-scoped dashboard down to a specific site without recreating it, which is how site-level deployment is done when the calling token sits at account scope. Note the two different scope arguments: the "scopes" array is WHERE THE DASHBOARD GOES, while "scope" is the header for this call, i.e. where you are standing when you share.',
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ id: { type: 'string', description: 'Dashboard id from sdl_list_dashboards or sdl_create_dashboard.' },
242
+ scopes: {
243
+ type: 'array',
244
+ description: 'Share targets. Each entry is {scopeType, scopeId, operation}: scopeType is "site" | "account" | "global"; scopeId is the numeric id from GET /web/api/v2.1/sites or /accounts (not required for global); operation is "ADD" or "REMOVE" (default ADD). Example: [{"scopeType":"site","scopeId":"2547662415802335157","operation":"ADD"}].',
245
+ items: {
246
+ type: 'object',
247
+ properties: {
248
+ scopeType: { type: 'string', enum: ['site', 'account', 'global'] },
249
+ scopeId: { type: 'string' },
250
+ operation: { type: 'string', enum: ['ADD', 'REMOVE'] },
251
+ },
252
+ required: ['scopeType'],
253
+ },
254
+ },
255
+ users: { type: 'array', description: 'Optional user share targets, same command shape as the console sends. Pass [] when sharing only to scopes.', items: { type: 'object' } },
256
+ scope: scopeProp,
257
+ },
258
+ required: ['id'],
259
+ },
260
+ async handler({ id, scopes, users, scope }) {
261
+ const result = await shareDashboard({ id, scopes, users, scope });
262
+ return JSON.stringify(result, null, 2);
263
+ },
264
+ },
265
+
266
+ // ─── sdl_save_dashboard_layout ────────────────────────────────────────────
267
+ {
268
+ name: 'sdl_save_dashboard_layout',
269
+ description: 'Replace the panel layout of ONE tab of a dashboard via saveDashboardLayout. Use for incremental panel edits (repositioning, adding or removing a panel on a single tab); use sdl_create_dashboard for a whole new document, or sdl_put_file with expectedVersion to rewrite an existing dashboard\'s full config. The graphs argument is a JSON string shaped {"graphs":[...]} including the wrapper key, even though the response echoes a bare array.',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ id: { type: 'string', description: 'Dashboard id.' },
274
+ name: { type: 'string', description: 'Dashboard display name, as an alternative to id.' },
275
+ tabName: { type: 'string', description: 'Exact tab name to replace, e.g. "2. Metacortex operations". Must match an existing tab.' },
276
+ graphs: { type: 'string', description: 'JSON string shaped {"graphs":[{panel},...]}. Validated before sending; the top-level "graphs" array is required.' },
277
+ options: { type: 'string', description: 'Optional tab options as a JSON string, e.g. "{}".' },
278
+ scope: scopeProp,
279
+ },
280
+ required: ['graphs', 'tabName'],
281
+ },
282
+ async handler({ id, name, tabName, graphs, options, scope }) {
283
+ const result = await saveDashboardLayout({ id, name, tabName, graphs, options, scope });
284
+ return JSON.stringify(result, null, 2);
285
+ },
286
+ },
287
+
288
+ // ─── sdl_delete_dashboard ─────────────────────────────────────────────────
289
+ {
290
+ name: 'sdl_delete_dashboard',
291
+ description: `Delete a dashboard by id or name via the deleteDashboard mutation. Deletion is permanent. The mutation returns a bare boolean, so this tool re-reads the dashboard afterwards and only reports success once it is confirmed gone. Equivalent to sdl_delete_file by udoId; prefer this one when you are working through the dashboard surface. ${SCOPE_NOTE}`,
292
+ inputSchema: {
293
+ type: 'object',
294
+ properties: {
295
+ id: { type: 'string', description: 'Dashboard id to delete.' },
296
+ name: { type: 'string', description: 'Dashboard name to delete, as an alternative to id. Ambiguous if duplicates exist; prefer id.' },
297
+ scope: scopeProp,
130
298
  },
131
299
  required: [],
132
300
  },
133
- async handler({ path, udoId, expectedVersion }) {
134
- const result = await deleteConfigFile({ name: path, udoId, expectedVersion });
301
+ async handler({ id, name, scope }) {
302
+ const result = await deleteDashboard({ id, name, scope });
135
303
  return JSON.stringify(result, null, 2);
136
304
  },
137
305
  },