securenow 8.7.0 → 8.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/cli/security.js +219 -0
  2. package/cli.js +867 -865
  3. package/mcp/catalog.js +2233 -2187
  4. package/package.json +193 -193
package/mcp/catalog.js CHANGED
@@ -1,82 +1,82 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const TEXT = 'text/plain';
7
- const JSON_MIME = 'application/json';
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const TEXT = 'text/plain';
7
+ const JSON_MIME = 'application/json';
8
8
  const MARKDOWN = 'text/markdown';
9
9
 
10
10
  const UNIVERSAL_SECURENOW_SETUP_PROMPT = `You are working in an existing JavaScript or TypeScript app. Set up SecureNow end-to-end for the framework/runtime already used by this repo. Treat this as a real onboarding, not just a package install.
11
11
 
12
12
  Primary goals:
13
- - Use the latest published SecureNow npm package. Require securenow@8.0.0 or newer for split admin/runtime credentials and runtime-authenticated ingestion.
13
+ - Use the latest published SecureNow npm package. Require securenow@8.0.0 or newer for split admin/runtime credentials and runtime-authenticated ingestion.
14
14
  - By default, enable tracing, logs, POST request body capture, multipart metadata capture, and the SecureNow firewall.
15
15
  - If I explicitly ask for firewall-only mode, keep the same install/login/verification gates, but use firewall-only preload and do not add tracing, logging, or OTel instrumentation.
16
- - The firewall must protect the selected SecureNow app, use SecureNow's own blocklist/allowlist/IPDB data, and respect that app's SecureNow AI IPDB confidence threshold. Do not add custom IP reputation providers or custom auto-blocking.
17
- - Do not confuse IP Allowlist with Trusted IPs. IP Allowlist is restrictive deny-by-default: when any allowlist entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. Use Trusted IPs for known-safe monitors, office/VPN traffic, or false-positive suppression. Only use allowlist after explicit human approval to lock the app/environment to known IPs.
16
+ - The firewall must protect the selected SecureNow app, use SecureNow's own blocklist/allowlist/IPDB data, and respect that app's SecureNow AI IPDB confidence threshold. Do not add custom IP reputation providers or custom auto-blocking.
17
+ - Do not confuse IP Allowlist with Trusted IPs. IP Allowlist is restrictive deny-by-default: when any allowlist entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. Use Trusted IPs for known-safe monitors, office/VPN traffic, or false-positive suppression. Only use allowlist after explicit human approval to lock the app/environment to known IPs.
18
18
 
19
19
  Safety rules:
20
- - Do not print full API keys, JWTs, tokens, or local SecureNow credential files (.securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, or .securenow/credentials.*.json). Mask secrets.
21
- - Keep local SecureNow credential files secret (.securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and .securenow/credentials.*.json). Do not print them; mask secrets in summaries.
20
+ - Do not print full API keys, JWTs, tokens, or local SecureNow credential files (.securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, or .securenow/credentials.*.json). Mask secrets.
21
+ - Keep local SecureNow credential files secret (.securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and .securenow/credentials.*.json). Do not print them; mask secrets in summaries.
22
22
  - Do not manually browse to a SecureNow auth URL. Always start auth with npx securenow login so the CLI generates the required callback and state.
23
23
  - If the browser says "Missing callback parameter", you opened the wrong URL: rerun npx securenow login from the project root.
24
24
  - Do not skip login, app selection, firewall connection, or verification unless I explicitly say to.
25
25
  - Preserve existing middleware, proxy, instrumentation, Docker, PM2, and start scripts.
26
26
 
27
27
  Runbook:
28
- 1. Identify the project root, package manager, framework, start/build/test scripts, process manager files, Docker files, and existing middleware/proxy/instrumentation.
29
- - Use the repo's own scripts for verification. If the correct dev/start/build/test command is missing or ambiguous, ask the customer which command to use.
30
- - Ask before starting long-running dev/start servers. Build/test/lint commands can be run when they are standard project scripts and safe for the current task.
31
- - Do not invent HTTP URLs. Only probe a local or production URL when the customer provides one, the running app prints one, or project/deploy config clearly defines one.
28
+ 1. Identify the project root, package manager, framework, start/build/test scripts, process manager files, Docker files, and existing middleware/proxy/instrumentation.
29
+ - Use the repo's own scripts for verification. If the correct dev/start/build/test command is missing or ambiguous, ask the customer which command to use.
30
+ - Ask before starting long-running dev/start servers. Build/test/lint commands can be run when they are standard project scripts and safe for the current task.
31
+ - Do not invent HTTP URLs. Only probe a local or production URL when the customer provides one, the running app prints one, or project/deploy config clearly defines one.
32
32
  2. Install or upgrade SecureNow with the detected package manager, using securenow@latest. Verify the actual installed version with:
33
33
  node -p "require('./node_modules/securenow/package.json').version"
34
34
  npx securenow version
35
- Stop and fix the install if either is below 8.0.0 or npx still resolves an older local package.
36
- 3. Read the installed package surface before editing files: node_modules/securenow/package.json, README/NPM_README, SKILL-API, SKILL-CLI, npx securenow help, and relevant subcommand help for login/init/firewall/doctor/env/test-span/log/mcp.
37
- 4. Mandatory auth/runtime gate:
38
- - Run npx securenow whoami from the project root.
39
- - If admin auth is missing, run npx securenow admin login from the project root and wait for the browser flow.
40
- - If runtime app config is missing, run npx securenow app connect from the project root and wait for the browser flow.
41
- - After the CLI exits, rerun npx securenow whoami.
42
- - Do not proceed to app edits or verification until whoami shows the required lane(s). SDK setup needs runtime app config; admin/global MCP operations need admin auth.
43
- 5. Validate project-local credentials without exposing secrets:
44
- - Confirm .securenow/runtime.json exists for SDK runtime setup, or legacy .securenow/credentials.json exists for old installs.
45
- - Confirm the runtime file has SecureNow's default config/explanations block.
46
- - Confirm the runtime file has an app key/name/instance and a runtime API key after app selection. The app key only routes telemetry; the runtime API key is required to authenticate telemetry ingestion and firewall sync.
47
- - Confirm .securenow/admin.json exists only when admin CLI/MCP auth is needed.
48
- - Confirm .securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
49
- 6. Run npx securenow init. If it fails with ui.header is not a function or another CLI bug, upgrade to securenow@latest, verify >=8.0.0, and retry. Do not silently ignore init failures.
50
- 7. Configure the least invasive framework-specific integration:
51
- - Next.js: preserve instrumentation.js/ts. Register securenow/nextjs only when NEXT_RUNTIME is nodejs. In ESM files, use createRequire before require("securenow/nextjs"). Include require("securenow/nextjs-auto-capture") for body capture. For Next 15+, add securenow to serverExternalPackages. For older Next.js, use experimental.serverComponentsExternalPackages. Preserve proxy.js/middleware.js.
52
- - Nuxt/Nitro: use the documented securenow/nuxt module or Nitro server plugin.
53
- - Express/Fastify/NestJS/Koa/Hapi/Hono/raw Node: preload securenow/register through existing scripts, NODE_OPTIONS, PM2 node_args, Docker CMD, or the process manager already used.
54
- - Firewall-only: preload securenow/firewall-only or use the documented securenow run --firewall-only command. Do not add OTel/tracing/logging in this mode.
55
- - Vite/browser-only: use only documented browser integration and state that server firewall protection requires a server runtime.
56
- 8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/runtime.json, with legacy .securenow/credentials.json and generated runtime credential files still supported:
57
- - config.logging.enabled: true
58
- - config.capture.body: true
59
- - config.capture.multipart: true
60
- - config.firewall.enabled: true
61
- - config.firewall.failMode: "open"
62
- - config.capture.maxBodySize: 10240
63
- For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json or <app-root>/.securenow/credentials.production.json. Do not recommend env vars unless the user explicitly asks for legacy fallbacks.
64
- Do not proceed with telemetry verification if the runtime API key is missing; run npx securenow app connect or npx securenow api-key create first. The v8 ingestion gateway rejects app-key-only telemetry.
65
- Local credentials should use config.runtime.deploymentEnvironment="local". Production runtime credentials should use "production". The app key stays the same; traces, logs, firewall status, forensics, and CLI/MCP queries are scoped by environment.
66
- 9. Verify firewall and threshold:
35
+ Stop and fix the install if either is below 8.0.0 or npx still resolves an older local package.
36
+ 3. Read the installed package surface before editing files: node_modules/securenow/package.json, README/NPM_README, SKILL-API, SKILL-CLI, npx securenow help, and relevant subcommand help for login/init/firewall/doctor/env/test-span/log/mcp.
37
+ 4. Mandatory auth/runtime gate:
38
+ - Run npx securenow whoami from the project root.
39
+ - If admin auth is missing, run npx securenow admin login from the project root and wait for the browser flow.
40
+ - If runtime app config is missing, run npx securenow app connect from the project root and wait for the browser flow.
41
+ - After the CLI exits, rerun npx securenow whoami.
42
+ - Do not proceed to app edits or verification until whoami shows the required lane(s). SDK setup needs runtime app config; admin/global MCP operations need admin auth.
43
+ 5. Validate project-local credentials without exposing secrets:
44
+ - Confirm .securenow/runtime.json exists for SDK runtime setup, or legacy .securenow/credentials.json exists for old installs.
45
+ - Confirm the runtime file has SecureNow's default config/explanations block.
46
+ - Confirm the runtime file has an app key/name/instance and a runtime API key after app selection. The app key only routes telemetry; the runtime API key is required to authenticate telemetry ingestion and firewall sync.
47
+ - Confirm .securenow/admin.json exists only when admin CLI/MCP auth is needed.
48
+ - Confirm .securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
49
+ 6. Run npx securenow init. If it fails with ui.header is not a function or another CLI bug, upgrade to securenow@latest, verify >=8.0.0, and retry. Do not silently ignore init failures.
50
+ 7. Configure the least invasive framework-specific integration:
51
+ - Next.js: preserve instrumentation.js/ts. Register securenow/nextjs only when NEXT_RUNTIME is nodejs. In ESM files, use createRequire before require("securenow/nextjs"). Include require("securenow/nextjs-auto-capture") for body capture. For Next 15+, add securenow to serverExternalPackages. For older Next.js, use experimental.serverComponentsExternalPackages. Preserve proxy.js/middleware.js.
52
+ - Nuxt/Nitro: use the documented securenow/nuxt module or Nitro server plugin.
53
+ - Express/Fastify/NestJS/Koa/Hapi/Hono/raw Node: preload securenow/register through existing scripts, NODE_OPTIONS, PM2 node_args, Docker CMD, or the process manager already used.
54
+ - Firewall-only: preload securenow/firewall-only or use the documented securenow run --firewall-only command. Do not add OTel/tracing/logging in this mode.
55
+ - Vite/browser-only: use only documented browser integration and state that server firewall protection requires a server runtime.
56
+ 8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/runtime.json, with legacy .securenow/credentials.json and generated runtime credential files still supported:
57
+ - config.logging.enabled: true
58
+ - config.capture.body: true
59
+ - config.capture.multipart: true
60
+ - config.firewall.enabled: true
61
+ - config.firewall.failMode: "open"
62
+ - config.capture.maxBodySize: 10240
63
+ For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json or <app-root>/.securenow/credentials.production.json. Do not recommend env vars unless the user explicitly asks for legacy fallbacks.
64
+ Do not proceed with telemetry verification if the runtime API key is missing; run npx securenow app connect or npx securenow api-key create first. The v8 ingestion gateway rejects app-key-only telemetry.
65
+ Local credentials should use config.runtime.deploymentEnvironment="local". Production runtime credentials should use "production". The app key stays the same; traces, logs, firewall status, forensics, and CLI/MCP queries are scoped by environment.
66
+ 9. Verify firewall and threshold:
67
67
  - Run npx securenow firewall apps and npx securenow firewall status.
68
- - Confirm the selected app is present, firewallEnabled is true, and the SecureNow AI IPDB confidence threshold is visible.
68
+ - Confirm the selected app is present, firewallEnabled is true, and the SecureNow AI IPDB confidence threshold is visible.
69
69
  - If firewallEnabled is false, run the documented per-app enable command, for example npx securenow firewall enable --app <appKey>, then verify again.
70
- 10. End-to-end proof:
71
- - Run npx securenow doctor.
72
- - Run npx securenow env and confirm loggingEnabled, captureBody, captureMultipart, and firewallEnabled resolve to true or 1 from credentials/defaults, unless I explicitly requested firewall-only.
73
- - If available and not in firewall-only mode, send telemetry:
74
- npx securenow test-span securenow.onboarding
75
- npx securenow log send "SecureNow onboarding test" --level info
76
- - Run the repo build/test/lint command when available. If multiple scripts look plausible or no script exists, ask the customer for the intended command instead of guessing.
77
- - If a local app URL is available, make one simple GET request to a real route to generate request telemetry, then check npx securenow status and traces for the selected environment. If no local URL is available, say that URL smoke testing was skipped.
78
- - If a production URL is provided or clearly discoverable, make one safe GET request to production and check npx securenow status/traces with --env production. If no production URL is available, do not fabricate one; ask for it or skip production URL smoke testing.
79
- - For MCP-capable clients, optionally smoke-test npx securenow mcp with the securenow_auth_status tool.
70
+ 10. End-to-end proof:
71
+ - Run npx securenow doctor.
72
+ - Run npx securenow env and confirm loggingEnabled, captureBody, captureMultipart, and firewallEnabled resolve to true or 1 from credentials/defaults, unless I explicitly requested firewall-only.
73
+ - If available and not in firewall-only mode, send telemetry:
74
+ npx securenow test-span securenow.onboarding
75
+ npx securenow log send "SecureNow onboarding test" --level info
76
+ - Run the repo build/test/lint command when available. If multiple scripts look plausible or no script exists, ask the customer for the intended command instead of guessing.
77
+ - If a local app URL is available, make one simple GET request to a real route to generate request telemetry, then check npx securenow status and traces for the selected environment. If no local URL is available, say that URL smoke testing was skipped.
78
+ - If a production URL is provided or clearly discoverable, make one safe GET request to production and check npx securenow status/traces with --env production. If no production URL is available, do not fabricate one; ask for it or skip production URL smoke testing.
79
+ - For MCP-capable clients, optionally smoke-test npx securenow mcp with the securenow_auth_status tool.
80
80
 
81
81
  Final response:
82
82
  - List every changed file.
@@ -85,2131 +85,2177 @@ Final response:
85
85
  - Mention skipped checks and why.
86
86
  - Provide exact command(s) to start the protected app.`;
87
87
 
88
-
89
- function string(description) {
90
- return { type: 'string', description };
91
- }
92
-
93
- function number(description, extra = {}) {
94
- return { type: 'number', description, ...extra };
95
- }
96
-
97
- function boolean(description) {
98
- return { type: 'boolean', description };
99
- }
100
-
101
- function arrayOfStrings(description) {
102
- return { type: 'array', items: { type: 'string' }, description };
103
- }
104
-
105
- function objectSchema(properties, required = []) {
106
- return {
107
- type: 'object',
108
- additionalProperties: false,
109
- properties,
110
- required,
111
- };
112
- }
113
-
114
- function jsonText(value) {
115
- return JSON.stringify(value, null, 2);
116
- }
117
-
118
- function maskSecret(value) {
119
- if (!value) return null;
120
- const text = String(value);
121
- if (text.length <= 12) return '***';
122
- return `${text.slice(0, 8)}...${text.slice(-4)}`;
123
- }
124
-
125
- function normalizeAppKeys(value) {
126
- if (Array.isArray(value)) return value.filter(Boolean).join(',');
127
- return value;
128
- }
129
-
130
- function sanitizeArgs(args = {}) {
131
- const clone = { ...args };
132
- for (const key of Object.keys(clone)) {
133
- if (/token|apiKey|api_key|authorization|password|secret/i.test(key)) {
134
- clone[key] = maskSecret(clone[key]);
135
- }
136
- }
137
- return clone;
138
- }
139
-
140
- const confirmSchema = {
141
- confirm: boolean('Required for write actions. Must be true.'),
142
- reason: string('Short human-readable reason for the write action.'),
143
- };
144
-
145
- const appKeysInput = {
146
- appKeys: {
147
- oneOf: [
148
- { type: 'string' },
149
- { type: 'array', items: { type: 'string' } },
150
- ],
151
- description: 'Application key or comma-separated/list of application keys.',
152
- },
153
- };
154
-
155
- const pagingInput = {
156
- limit: number('Maximum results to return.', { minimum: 1, maximum: 500 }),
157
- page: number('Page number.', { minimum: 1 }),
158
- };
159
-
160
- const timeRangeInput = {
161
- from: string('Start time as ISO 8601, optional.'),
162
- to: string('End time as ISO 8601, optional.'),
163
- };
164
-
165
- const environmentInput = {
166
- environment: string('Deployment environment scope: production, staging, preview, local, test, or all. Default for investigations is production.'),
167
- };
168
-
169
- const decisionReportInput = {
170
- decisionReport: {
171
- type: 'object',
172
- additionalProperties: true,
173
- description: 'Structured audit report explaining the decision, evidence reviewed, trace IDs, missing proof, and recommendations.',
174
- },
175
- decisionSummary: string('Short decision summary to record on the IP/case history.'),
176
- outcome: string('Decision outcome: blocked, rate_limited, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, or other.'),
177
- evidence: arrayOfStrings('Evidence strings that support the decision.'),
178
- reviewedHistory: arrayOfStrings('History/proofs reviewed before deciding.'),
179
- traceIds: arrayOfStrings('Trace IDs reviewed for this decision.'),
180
- paths: arrayOfStrings('Paths/endpoints reviewed for this decision.'),
181
- ips: arrayOfStrings('IP addresses affected by this case-level decision report.'),
182
- methods: arrayOfStrings('HTTP methods reviewed for this decision.'),
183
- statusCodes: arrayOfStrings('HTTP status codes reviewed for this decision.'),
184
- userAgents: arrayOfStrings('User agents reviewed for this decision.'),
185
- missingProof: arrayOfStrings('Proof that was missing when the row is skipped or ambiguous.'),
186
- recommendations: arrayOfStrings('Follow-up recommendations to record with the decision.'),
187
- };
188
-
189
- const TOOLS = [
190
- {
191
- name: 'securenow_auth_status',
192
- title: 'SecureNow Auth Status',
193
- description: 'Show local SecureNow credential source, selected app, and masked runtime API key.',
194
- scope: null,
195
- localOnly: true,
196
- readOnly: true,
197
- inputSchema: objectSchema({}),
198
- },
199
- {
200
- name: 'securenow_apps_list',
201
- title: 'List Applications',
202
- description: 'List SecureNow applications for the authenticated user.',
203
- scope: 'applications:read',
204
- readOnly: true,
205
- method: 'GET',
206
- endpoint: '/applications',
207
- inputSchema: objectSchema({}),
208
- },
209
- {
210
- name: 'securenow_apps_create',
211
- title: 'Create Application',
212
- description: 'Create a SecureNow application. Write action; requires confirmation.',
213
- scope: 'applications:write',
214
- readOnly: false,
215
- confirm: true,
216
- method: 'POST',
217
- endpoint: '/applications',
218
- bodyFields: ['name', 'hosts', 'instanceId'],
219
- inputSchema: objectSchema({
220
- name: string('Application name.'),
221
- hosts: arrayOfStrings('Optional hostnames/domains for this application.'),
222
- instanceId: string('Optional ClickHouse instance id.'),
223
- ...confirmSchema,
224
- }, ['name', 'confirm', 'reason']),
225
- },
226
- {
227
- name: 'securenow_apps_info',
228
- title: 'Get Application By Key',
229
- description: 'Get details for an application by app key.',
230
- scope: 'applications:read',
231
- readOnly: true,
232
- method: 'GET',
233
- endpoint: '/applications/key/:key',
234
- pathParams: ['key'],
235
- inputSchema: objectSchema({
236
- key: string('Application key UUID.'),
237
- }, ['key']),
238
- },
239
- {
240
- name: 'securenow_firewall_apps',
241
- title: 'List Firewall Apps',
242
- description: 'List applications with firewall toggle and SecureNow AI IPDB threshold state.',
243
- scope: 'applications:read',
244
- readOnly: true,
245
- method: 'GET',
246
- endpoint: '/firewall/apps',
247
- inputSchema: objectSchema({}),
248
- },
249
- {
250
- name: 'securenow_firewall_status',
251
- title: 'Firewall Status',
252
- description: 'Show firewall blocklist/allowlist status for the authenticated account.',
253
- scope: 'firewall:read',
254
- readOnly: true,
255
- method: 'GET',
256
- endpoint: '/firewall/status',
257
- queryFields: ['environment', 'appKey'],
258
- inputSchema: objectSchema({
259
- appKey: string('Optional application key UUID to scope the status check.'),
260
- ...environmentInput,
261
- }),
262
- },
263
- {
264
- name: 'securenow_firewall_enable',
265
- title: 'Enable App Firewall',
266
- description: 'Turn the per-app firewall ON. Write action; requires confirmation.',
267
- scope: 'applications:write',
268
- readOnly: false,
269
- confirm: true,
270
- method: 'PATCH',
271
- endpoint: '/firewall/app/:appKey',
272
- pathParams: ['appKey'],
273
- bodyFields: ['environment'],
274
- fixedBody: { enabled: true },
275
- inputSchema: objectSchema({
276
- appKey: string('Application key UUID.'),
277
- ...environmentInput,
278
- ...confirmSchema,
279
- }, ['appKey', 'confirm', 'reason']),
280
- },
281
- {
282
- name: 'securenow_firewall_disable',
283
- title: 'Disable App Firewall',
284
- description: 'Turn the per-app firewall OFF. Write action; requires confirmation.',
285
- scope: 'applications:write',
286
- readOnly: false,
287
- destructive: true,
288
- confirm: true,
289
- method: 'PATCH',
290
- endpoint: '/firewall/app/:appKey',
291
- pathParams: ['appKey'],
292
- bodyFields: ['environment'],
293
- fixedBody: { enabled: false },
294
- inputSchema: objectSchema({
295
- appKey: string('Application key UUID.'),
296
- ...environmentInput,
297
- ...confirmSchema,
298
- }, ['appKey', 'confirm', 'reason']),
299
- },
300
- {
301
- name: 'securenow_firewall_set_threshold',
302
- title: 'Set SecureNow AI IPDB Threshold',
303
- description: 'Set the per-app SecureNow AI IPDB confidence threshold. Write action; requires confirmation.',
304
- scope: 'applications:write',
305
- readOnly: false,
306
- confirm: true,
307
- method: 'PATCH',
308
- endpoint: '/firewall/app/:appKey',
309
- pathParams: ['appKey'],
310
- bodyFields: ['confidenceMinimum', 'environment'],
311
- inputSchema: objectSchema({
312
- appKey: string('Application key UUID.'),
313
- confidenceMinimum: number('Minimum SecureNow AI IPDB confidence score.', { minimum: 0, maximum: 100 }),
314
- ...environmentInput,
315
- ...confirmSchema,
316
- }, ['appKey', 'confidenceMinimum', 'confirm', 'reason']),
317
- },
318
- {
319
- name: 'securenow_firewall_test_ip',
320
- title: 'Test Firewall IP Decision',
321
- description: 'Check whether an IP would be blocked by the firewall.',
322
- scope: 'firewall:read',
323
- readOnly: true,
324
- method: 'GET',
325
- endpoint: '/firewall/check/:ip',
326
- pathParams: ['ip'],
327
- queryFields: ['environment', 'appKey'],
328
- inputSchema: objectSchema({
329
- ip: string('IPv4 address to test.'),
330
- appKey: string('Optional application key UUID to test the app/environment toggle and scoped lists.'),
331
- ...environmentInput,
332
- }, ['ip']),
333
- },
334
- {
335
- name: 'securenow_traces_list',
336
- title: 'List Traces',
337
- description: 'List recent traces for one or more applications.',
338
- scope: 'traces:read',
339
- readOnly: true,
340
- method: 'GET',
341
- endpoint: '/traces',
342
- queryFields: ['appKeys', 'environment', 'from', 'to', 'limit'],
343
- normalize: { appKeys: normalizeAppKeys },
344
- inputSchema: objectSchema({
345
- ...appKeysInput,
346
- ...environmentInput,
347
- ...timeRangeInput,
348
- limit: number('Maximum traces to return.', { minimum: 1, maximum: 200 }),
349
- }, ['appKeys']),
350
- },
351
- {
352
- name: 'securenow_traces_show',
353
- title: 'Show Trace',
354
- description: 'Show spans for a trace id scoped to one or more app keys.',
355
- scope: 'traces:read',
356
- readOnly: true,
357
- method: 'GET',
358
- endpoint: '/traces/:traceId',
359
- pathParams: ['traceId'],
360
- queryFields: ['appKeys', 'environment'],
361
- normalize: { appKeys: normalizeAppKeys },
362
- inputSchema: objectSchema({
363
- traceId: string('Trace id.'),
364
- ...appKeysInput,
365
- ...environmentInput,
366
- }, ['traceId', 'appKeys']),
367
- },
368
- {
369
- name: 'securenow_logs_list',
370
- title: 'List Logs',
371
- description: 'List recent logs for one or more applications.',
372
- scope: 'logs:read',
373
- readOnly: true,
374
- method: 'GET',
375
- endpoint: '/logs',
376
- queryFields: ['appKeys', 'environment', 'from', 'to', 'severity', 'limit'],
377
- normalize: { appKeys: normalizeAppKeys },
378
- inputSchema: objectSchema({
379
- ...appKeysInput,
380
- ...environmentInput,
381
- ...timeRangeInput,
382
- severity: string('Optional severity filter.'),
383
- limit: number('Maximum logs to return.', { minimum: 1, maximum: 500 }),
384
- }, ['appKeys']),
385
- },
386
- {
387
- name: 'securenow_logs_for_trace',
388
- title: 'Logs For Trace',
389
- description: 'Show logs correlated to a trace id.',
390
- scope: 'logs:read',
391
- readOnly: true,
392
- method: 'GET',
393
- endpoint: '/logs/trace/:traceId',
394
- pathParams: ['traceId'],
395
- queryFields: ['appKeys', 'environment'],
396
- normalize: { appKeys: normalizeAppKeys },
397
- inputSchema: objectSchema({
398
- traceId: string('Trace id.'),
399
- ...appKeysInput,
400
- ...environmentInput,
401
- }, ['traceId', 'appKeys']),
402
- },
403
- {
404
- name: 'securenow_notifications_list',
405
- title: 'List Notifications',
406
- description: 'List security notifications.',
407
- scope: 'notifications:read',
408
- readOnly: true,
409
- method: 'GET',
410
- endpoint: '/notifications',
411
- queryFields: ['limit', 'page'],
412
- inputSchema: objectSchema({ ...pagingInput }),
413
- },
414
- {
415
- name: 'securenow_notifications_unread',
416
- title: 'Unread Notification Count',
417
- description: 'Return unread notification count.',
418
- scope: 'notifications:read',
419
- readOnly: true,
420
- method: 'GET',
421
- endpoint: '/notifications/unread-count',
422
- inputSchema: objectSchema({}),
423
- },
424
- {
425
- name: 'securenow_notifications_read',
426
- title: 'Mark Notification Read',
427
- description: 'Mark a notification as read. Write action; requires confirmation.',
428
- scope: 'notifications:write',
429
- readOnly: false,
430
- confirm: true,
431
- method: 'PUT',
432
- endpoint: '/notifications/:id/read',
433
- pathParams: ['id'],
434
- inputSchema: objectSchema({
435
- id: string('Notification id.'),
436
- ...confirmSchema,
437
- }, ['id', 'confirm', 'reason']),
438
- },
439
- {
440
- name: 'securenow_notifications_get',
441
- title: 'Get Notification',
442
- description: 'Get one notification/case with alert context, clusters, and IP investigations.',
443
- scope: 'notifications:read',
444
- readOnly: true,
445
- method: 'GET',
446
- endpoint: '/notifications/:id',
447
- pathParams: ['id'],
448
- inputSchema: objectSchema({
449
- id: string('Notification id.'),
450
- }, ['id']),
451
- },
452
- {
453
- name: 'securenow_notifications_batch_get',
454
- title: 'Get Notifications Batch',
455
- description: 'Fetch multiple notification/case records in one request.',
456
- scope: 'notifications:read',
457
- readOnly: true,
458
- method: 'POST',
459
- endpoint: '/notifications/batch',
460
- bodyFields: ['ids'],
461
- inputSchema: objectSchema({
462
- ids: arrayOfStrings('Notification ids to fetch, up to 50.'),
463
- }, ['ids']),
464
- },
465
- {
466
- name: 'securenow_human_actions_list',
467
- title: 'List Human Action Queue',
468
- description: 'List approval-gated human decisions prepared by SecureNow AI, ordered by urgency.',
469
- scope: 'notifications:read',
470
- readOnly: true,
471
- method: 'GET',
472
- endpoint: '/notifications/approval-tasks',
473
- queryFields: ['limit', 'page', 'search'],
474
- inputSchema: objectSchema({
475
- ...pagingInput,
476
- search: string('Optional search over IP, rule, path, or verdict.'),
477
- }),
478
- },
479
- {
480
- name: 'securenow_human_actions_grouped_list',
481
- title: 'List Grouped Human Actions',
482
- description: 'List Requires Human work as root-cause groups with batch-safety preview instead of one row per IP/action.',
483
- scope: 'notifications:read',
484
- readOnly: true,
485
- method: 'GET',
486
- endpoint: '/notifications/approval-task-groups',
487
- queryFields: ['limit', 'page', 'search'],
488
- inputSchema: objectSchema({
489
- ...pagingInput,
490
- search: string('Optional search over IP, rule, path, action, or verdict.'),
491
- }),
492
- },
493
- {
494
- name: 'securenow_alert_review_runs_list',
495
- title: 'List Alert Review Root Causes',
496
- description: 'List pending alert-review runs as root-cause groups with batch-action preview, rather than row-by-row noisy alert triage.',
497
- scope: 'notifications:read',
498
- readOnly: true,
499
- method: 'GET',
500
- endpoint: '/notifications/alert-review-runs',
501
- queryFields: ['limit', 'page', 'search', 'rootCauseKey', 'groupKey', 'includeRuns', 'maxScan'],
502
- inputSchema: objectSchema({
503
- ...pagingInput,
504
- search: string('Optional search over rule, route, IP, evidence, or proposed guard.'),
505
- rootCauseKey: string('Optional root-cause group key to filter to one group.'),
506
- groupKey: string('Compatibility alias for rootCauseKey.'),
507
- includeRuns: boolean('Whether to include compact matching run rows in the response.'),
508
- maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
509
- }),
510
- },
511
- {
512
- name: 'securenow_alert_review_runs_count',
513
- title: 'Count Alert Review Runs',
514
- description: 'Count pending alert-review rows and the root-cause groups they collapse into.',
515
- scope: 'notifications:read',
516
- readOnly: true,
517
- method: 'GET',
518
- endpoint: '/notifications/alert-review-runs/count',
519
- queryFields: ['search', 'maxScan'],
520
- inputSchema: objectSchema({
521
- search: string('Optional search over rule, route, IP, evidence, or proposed guard.'),
522
- maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
523
- }),
524
- },
525
- {
526
- name: 'securenow_alert_review_run_get',
527
- title: 'Get Alert Review Run',
528
- description: 'Fetch one pending-review alert run with its root-cause key, evidence-quality score, and similar open siblings.',
529
- scope: 'notifications:read',
530
- readOnly: true,
531
- method: 'GET',
532
- endpoint: '/notifications/alert-review-runs/:id',
533
- pathParams: ['id'],
534
- queryFields: ['maxScan'],
535
- inputSchema: objectSchema({
536
- id: string('Notification id for the alert review run.'),
537
- maxScan: number('Maximum pending-review rows to scan for similar siblings. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
538
- }, ['id']),
539
- },
540
- {
541
- name: 'securenow_alert_review_run_resolve',
542
- title: 'Resolve Alert Review Run',
543
- description: 'Close one pending alert-review row with an auditable no-write decision report.',
544
- scope: 'notifications:write',
545
- readOnly: false,
546
- confirm: true,
547
- method: 'POST',
548
- endpoint: '/notifications/alert-review-runs/:id/resolve',
549
- pathParams: ['id'],
550
- fixedBody: { reportSource: 'mcp' },
551
- bodyFields: ['confirm', 'reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
552
- inputSchema: objectSchema({
553
- id: string('Notification id for the alert review run.'),
554
- ...decisionReportInput,
555
- ...confirmSchema,
556
- }, ['id', 'confirm', 'reason']),
557
- },
558
- {
559
- name: 'securenow_alert_review_run_dismiss',
560
- title: 'Dismiss Alert Review Run',
561
- description: 'Dismiss one pending alert-review row with an audit report and no enforcement write.',
562
- scope: 'notifications:write',
563
- readOnly: false,
564
- confirm: true,
565
- method: 'POST',
566
- endpoint: '/notifications/alert-review-runs/:id/dismiss',
567
- pathParams: ['id'],
568
- fixedBody: { reportSource: 'mcp' },
569
- bodyFields: ['confirm', 'reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
570
- inputSchema: objectSchema({
571
- id: string('Notification id for the alert review run.'),
572
- ...decisionReportInput,
573
- ...confirmSchema,
574
- }, ['id', 'confirm', 'reason']),
575
- },
576
- {
577
- name: 'securenow_alert_review_run_keep_alive',
578
- title: 'Keep Alert Rule Active',
579
- description: 'Mark the noisy/paused alert rule as reviewed, keep it active, and close the pending alert-review row with an audit report.',
580
- scope: 'notifications:write',
581
- readOnly: false,
582
- confirm: true,
583
- method: 'POST',
584
- endpoint: '/notifications/alert-review-runs/:id/keep-alive',
585
- pathParams: ['id'],
586
- fixedBody: { reportSource: 'mcp' },
587
- bodyFields: ['confirm', 'reason', 'reviewNote', 'decisionReport', 'decisionSummary', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
588
- inputSchema: objectSchema({
589
- id: string('Notification id for the alert review run.'),
590
- reviewNote: string('Optional rule-review note. Defaults to reason.'),
591
- ...decisionReportInput,
592
- ...confirmSchema,
593
- }, ['id', 'confirm', 'reason']),
594
- },
595
- {
596
- name: 'securenow_alert_review_run_apply_group_decision',
597
- title: 'Apply Alert Review Group Decision',
598
- description: 'Apply one verified no-write decision report to every currently matching alert-review row in a root-cause group, leaving non-matching rows open.',
599
- scope: 'notifications:write',
600
- readOnly: false,
601
- confirm: true,
602
- method: 'POST',
603
- endpoint: '/notifications/alert-review-runs/apply-group-decision',
604
- fixedBody: { reportSource: 'mcp' },
605
- bodyFields: ['confirm', 'reason', 'rootCauseKey', 'groupKey', 'prototypeRunId', 'notificationIds', 'status', 'outcome', 'allowMixed', 'previewOnly', 'maxRows', 'maxScan', 'search', 'decisionReport', 'decisionSummary', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
606
- inputSchema: objectSchema({
607
- rootCauseKey: string('Root-cause group key from securenow_alert_review_runs_list.'),
608
- groupKey: string('Compatibility alias for rootCauseKey.'),
609
- prototypeRunId: string('Optional notification id whose root-cause key should be used.'),
610
- notificationIds: arrayOfStrings('Optional subset of eligible notification ids to close. Leave empty to apply to every eligible row in the group.'),
611
- status: string('Resolution status: resolved or dismissed. Defaults to resolved.'),
612
- allowMixed: boolean('Allow a mixed decision/attribution group only after every row was manually reviewed. Defaults to false.'),
613
- previewOnly: boolean('When true, returns the eligible rows without writing.'),
614
- maxRows: number('Maximum rows to close in this batch. Defaults to 100, capped at 250.', { minimum: 1, maximum: 250 }),
615
- maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
616
- search: string('Optional search filter applied before grouping.'),
617
- ...decisionReportInput,
618
- ...confirmSchema,
619
- }, ['confirm', 'reason']),
620
- },
621
- {
622
- name: 'securenow_alert_review_apply_group_decision',
623
- title: 'Apply Alert Review Group Decision',
624
- description: 'Compatibility alias for securenow_alert_review_run_apply_group_decision.',
625
- scope: 'notifications:write',
626
- readOnly: false,
627
- confirm: true,
628
- method: 'POST',
629
- endpoint: '/notifications/alert-review-runs/apply-group-decision',
630
- fixedBody: { reportSource: 'mcp' },
631
- bodyFields: ['confirm', 'reason', 'rootCauseKey', 'groupKey', 'prototypeRunId', 'notificationIds', 'status', 'outcome', 'allowMixed', 'previewOnly', 'maxRows', 'maxScan', 'search', 'decisionReport', 'decisionSummary', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
632
- inputSchema: objectSchema({
633
- rootCauseKey: string('Root-cause group key from securenow_alert_review_runs_list.'),
634
- groupKey: string('Compatibility alias for rootCauseKey.'),
635
- prototypeRunId: string('Optional notification id whose root-cause key should be used.'),
636
- notificationIds: arrayOfStrings('Optional subset of eligible notification ids to close. Leave empty to apply to every eligible row in the group.'),
637
- status: string('Resolution status: resolved or dismissed. Defaults to resolved.'),
638
- allowMixed: boolean('Allow a mixed decision/attribution group only after every row was manually reviewed. Defaults to false.'),
639
- previewOnly: boolean('When true, returns the eligible rows without writing.'),
640
- maxRows: number('Maximum rows to close in this batch. Defaults to 100, capped at 250.', { minimum: 1, maximum: 250 }),
641
- maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
642
- search: string('Optional search filter applied before grouping.'),
643
- ...decisionReportInput,
644
- ...confirmSchema,
645
- }, ['confirm', 'reason']),
646
- },
647
- {
648
- name: 'securenow_alert_review_group_similar',
649
- title: 'List Similar Alert Review Runs',
650
- description: 'Fetch the current rows in one alert-review root-cause group for preview before a batch decision.',
651
- scope: 'notifications:read',
652
- readOnly: true,
653
- method: 'GET',
654
- endpoint: '/notifications/alert-review-runs',
655
- queryFields: ['rootCauseKey', 'groupKey', 'includeRuns', 'maxScan'],
656
- fixedQuery: { includeRuns: true },
657
- inputSchema: objectSchema({
658
- rootCauseKey: string('Root-cause group key from securenow_alert_review_runs_list.'),
659
- groupKey: string('Compatibility alias for rootCauseKey.'),
660
- maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
661
- }),
662
- },
663
- {
664
- name: 'securenow_human_action_report',
665
- title: 'Get Human Action Report',
666
- description: 'Fetch the full IP investigation report, investigation steps, proofs, metadata, and AI decision for one human action row.',
667
- scope: 'notifications:read',
668
- readOnly: true,
669
- method: 'GET',
670
- endpoint: '/notifications/:notificationId/ips/:ip',
671
- pathParams: ['notificationId', 'ip'],
672
- inputSchema: objectSchema({
673
- notificationId: string('Notification id from the human action row.'),
674
- ip: string('IP address from the human action row.'),
675
- }, ['notificationId', 'ip']),
676
- },
677
- {
678
- name: 'securenow_human_action_block',
679
- title: 'Approve AI Block Recommendation',
680
- description: 'Approve the AI-prepared block decision for an IP and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
681
- scope: 'notifications:write',
682
- readOnly: false,
683
- destructive: true,
684
- confirm: true,
685
- method: 'PUT',
686
- endpoint: '/notifications/:notificationId/ips/:ip/status',
687
- pathParams: ['notificationId', 'ip'],
688
- fixedBody: { status: 'blocked', decisionSource: 'ai_dag', reportSource: 'mcp' },
689
- reasonAsNote: true,
690
- bodyFields: ['note', 'verdict', 'riskScore', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
691
- inputSchema: objectSchema({
692
- notificationId: string('Notification id from the human action row.'),
693
- ip: string('IP address to block.'),
694
- note: string('Audit note explaining why the AI block was approved.'),
695
- verdict: string('Optional final verdict text.'),
696
- riskScore: number('Optional risk score.', { minimum: 0, maximum: 100 }),
697
- ...decisionReportInput,
698
- ...confirmSchema,
699
- }, ['notificationId', 'ip', 'confirm', 'reason']),
700
- },
701
- {
702
- name: 'securenow_human_action_decision_report_add',
703
- title: 'Record Human Action Decision Report',
704
- description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for rate-limited, skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
705
- scope: 'notifications:write',
706
- readOnly: false,
707
- confirm: true,
708
- method: 'POST',
709
- endpoint: '/notifications/:notificationId/ips/:ip/decision-report',
710
- pathParams: ['notificationId', 'ip'],
711
- fixedBody: { reportSource: 'mcp' },
712
- bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
713
- inputSchema: objectSchema({
714
- notificationId: string('Notification id from the human action row.'),
715
- ip: string('IP address for the report.'),
716
- ...decisionReportInput,
717
- ...confirmSchema,
718
- }, ['notificationId', 'ip', 'confirm', 'reason']),
719
- },
720
- {
721
- name: 'securenow_human_action_close_no_write',
722
- title: 'Close Human Action Without Enforcement',
723
- description: 'Close one Requires Human IP row with an auditable no-write decision report. Use for ambiguous, skipped, rule-tuning-needed, new-rule-needed, rate-limited, or already-handled rows.',
724
- scope: 'notifications:write',
725
- readOnly: false,
726
- confirm: true,
727
- method: 'POST',
728
- endpoint: '/notifications/:notificationId/ips/:ip/close-no-write',
729
- pathParams: ['notificationId', 'ip'],
730
- fixedBody: { reportSource: 'mcp' },
731
- bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
732
- inputSchema: objectSchema({
733
- notificationId: string('Notification id from the human action row.'),
734
- ip: string('IP address for the row to close.'),
735
- ...decisionReportInput,
736
- ...confirmSchema,
737
- }, ['notificationId', 'ip', 'confirm', 'reason']),
738
- },
739
- {
740
- name: 'securenow_human_action_false_positive',
741
- title: 'Mark Human Action False Positive',
742
- description: 'Mark an AI-prepared human action as a scoped false positive and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
743
- scope: 'notifications:write',
744
- readOnly: false,
745
- confirm: true,
746
- method: 'POST',
747
- endpoint: '/notifications/:notificationId/ips/:ip/false-positive',
748
- pathParams: ['notificationId', 'ip'],
749
- fixedBody: { reportSource: 'mcp' },
750
- bodyFields: ['reason', 'conditions', 'matchMode', 'createExclusion', 'applyToExisting', 'ruleScope', 'targetRuleIds', 'aiConfidence', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
751
- inputSchema: objectSchema({
752
- notificationId: string('Notification id from the human action row.'),
753
- ip: string('IP address to mark as false positive.'),
754
- conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Restrictive false-positive conditions. Prefer app/rule/path/method/status/user-agent/body scope.' },
755
- matchMode: string('Condition match mode: all or any.'),
756
- createExclusion: boolean('Whether to create a restrictive exclusion rule.'),
757
- applyToExisting: boolean('Whether to apply to existing matching IP investigations.'),
758
- ruleScope: string('Scope: this_rule, specific_rules, all_existing, or any_rule.'),
759
- targetRuleIds: arrayOfStrings('Specific rule ids when ruleScope is specific_rules.'),
760
- aiConfidence: number('AI confidence for audit metadata.', { minimum: 0, maximum: 100 }),
761
- ...decisionReportInput,
762
- ...confirmSchema,
763
- }, ['notificationId', 'ip', 'confirm', 'reason']),
764
- },
765
- {
766
- name: 'securenow_human_case_action_update',
767
- title: 'Update Case-Level Human Action',
768
- description: 'Approve, reject, execute, or fail a case-level proposed action such as tune_rule or create_exclusion. Write action; requires confirmation.',
769
- scope: 'notifications:write',
770
- readOnly: false,
771
- confirm: true,
772
- method: 'PUT',
773
- endpoint: '/notifications/:notificationId/agent-case/actions/:actionKey',
774
- pathParams: ['notificationId', 'actionKey'],
775
- fixedBody: { reportSource: 'mcp' },
776
- bodyFields: ['status', 'result', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
777
- reasonInResult: true,
778
- inputSchema: objectSchema({
779
- notificationId: string('Notification id from the case-action row.'),
780
- actionKey: string('Proposed action key from the row, for example tune_rule:...'),
781
- status: string('New status: proposed, approved, rejected, executed, or failed.'),
782
- result: { type: 'object', additionalProperties: true, description: 'Optional structured result/audit details.' },
783
- ...decisionReportInput,
784
- ...confirmSchema,
785
- }, ['notificationId', 'actionKey', 'status', 'confirm', 'reason']),
786
- },
787
- {
788
- name: 'securenow_human_case_decision_report_add',
789
- title: 'Record Case Decision Report',
790
- description: 'Attach a structured analyst/MCP decision report to a notification/case without changing IP status or proposed action state. Write action; requires confirmation.',
791
- scope: 'notifications:write',
792
- readOnly: false,
793
- confirm: true,
794
- method: 'POST',
795
- endpoint: '/notifications/:notificationId/decision-report',
796
- pathParams: ['notificationId'],
797
- fixedBody: { reportSource: 'mcp' },
798
- bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
799
- inputSchema: objectSchema({
800
- notificationId: string('Notification id from the case-action row.'),
801
- ...decisionReportInput,
802
- ...confirmSchema,
803
- }, ['notificationId', 'confirm', 'reason']),
804
- },
805
- {
806
- name: 'securenow_ip_lookup',
807
- title: 'IP Intelligence Lookup',
808
- description: 'Look up SecureNow IP intelligence for an IP address.',
809
- scope: 'ip_intel:read',
810
- readOnly: true,
811
- method: 'GET',
812
- endpoint: '/ip/:ip',
813
- pathParams: ['ip'],
814
- inputSchema: objectSchema({
815
- ip: string('IPv4 address.'),
816
- }, ['ip']),
817
- },
818
- {
819
- name: 'securenow_ip_traces',
820
- title: 'IP Traces',
821
- description: 'Fetch traces associated with an IP address.',
822
- scope: 'ip_intel:read',
823
- readOnly: true,
824
- method: 'GET',
825
- endpoint: '/ip/:ip/traces',
826
- pathParams: ['ip'],
827
- queryFields: ['appKeys', 'environment'],
828
- normalize: { appKeys: normalizeAppKeys },
829
- inputSchema: objectSchema({
830
- ip: string('IPv4 address.'),
831
- ...appKeysInput,
832
- ...environmentInput,
833
- }, ['ip']),
834
- },
835
- {
836
- name: 'securenow_forensics_query',
837
- title: 'Run Forensics Query',
838
- description: 'Run a natural-language forensic query. Write scope because it creates an async query job.',
839
- scope: 'forensics:write',
840
- readOnly: false,
841
- confirm: true,
842
- method: 'POST',
843
- endpoint: '/forensics/query',
844
- bodyFields: ['query', 'applicationId', 'instanceId', 'environment'],
845
- inputSchema: objectSchema({
846
- query: string('Natural-language forensic query.'),
847
- applicationId: string('Optional application database id.'),
848
- instanceId: string('Optional ClickHouse instance id.'),
849
- ...environmentInput,
850
- ...confirmSchema,
851
- }, ['query', 'confirm', 'reason']),
852
- },
853
- {
854
- name: 'securenow_forensics_library',
855
- title: 'Forensics Query Library',
856
- description: 'List saved forensics queries.',
857
- scope: 'forensics:read',
858
- readOnly: true,
859
- method: 'GET',
860
- endpoint: '/forensics/query-library',
861
- inputSchema: objectSchema({}),
862
- },
863
- {
864
- name: 'securenow_analytics_summary',
865
- title: 'Analytics Summary',
866
- description: 'Fetch response analytics summary.',
867
- scope: 'analytics:read',
868
- readOnly: true,
869
- method: 'GET',
870
- endpoint: '/analytics/summary',
871
- queryFields: ['instanceId'],
872
- inputSchema: objectSchema({
873
- instanceId: string('Optional ClickHouse instance id.'),
874
- }),
875
- },
876
- {
877
- name: 'securenow_automation_rules_list',
878
- title: 'List Automation Rules',
879
- description: 'List blocklist automation rules with app/environment scope and stats.',
880
- scope: 'automation:read',
881
- readOnly: true,
882
- method: 'GET',
883
- endpoint: '/automation-rules',
884
- inputSchema: objectSchema({}),
885
- },
886
- {
887
- name: 'securenow_automation_defaults_ensure',
888
- title: 'Ensure Default Automation Rules',
889
- description: 'Create or refresh SecureNow default risk-score automation rules for the current account. Write action; requires confirmation.',
890
- scope: 'automation:write',
891
- readOnly: false,
892
- confirm: true,
893
- method: 'POST',
894
- endpoint: '/automation-rules/defaults/ensure',
895
- bodyFields: ['forceEnableExisting'],
896
- inputSchema: objectSchema({
897
- forceEnableExisting: boolean('Re-enable existing SecureNow default rules if the customer disabled them. Defaults to false.'),
898
- ...confirmSchema,
899
- }, ['confirm', 'reason']),
900
- },
901
- {
902
- name: 'securenow_automation_rule_get',
903
- title: 'Get Automation Rule',
904
- description: 'Fetch one automation rule.',
905
- scope: 'automation:read',
906
- readOnly: true,
907
- method: 'GET',
908
- endpoint: '/automation-rules/:id',
909
- pathParams: ['id'],
910
- inputSchema: objectSchema({
911
- id: string('Automation rule id.'),
912
- }, ['id']),
913
- },
914
- {
915
- name: 'securenow_automation_rule_create',
916
- title: 'Create Automation Rule',
917
- description: 'Create a blocklist automation rule. Write action; requires confirmation.',
918
- scope: 'automation:write',
919
- readOnly: false,
920
- confirm: true,
921
- method: 'POST',
922
- endpoint: '/automation-rules',
923
- bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'status', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
924
- inputSchema: objectSchema({
925
- name: string('Rule name.'),
926
- description: string('Optional rule description.'),
927
- conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array. Fields include abuseConfidenceScore, riskScore, alertName, alertTag, attackType, path, environment.' },
928
- conditionLogic: string('AND or OR.'),
929
- actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array, for example [{ "type":"addToBlocklist", "config":{ "reason":"...", "ttlHours":24 }}].' },
930
- status: string('Initial rule status: active or disabled. Defaults to active.'),
931
- applicationsAll: boolean('Apply to all applications.'),
932
- applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
933
- environmentsAll: boolean('Apply to all environments.'),
934
- environments: arrayOfStrings('Deployment environments when not applying to all environments.'),
935
- ...confirmSchema,
936
- }, ['name', 'conditions', 'actions', 'confirm', 'reason']),
937
- },
938
- {
939
- name: 'securenow_automation_rule_update',
940
- title: 'Update Automation Rule',
941
- description: 'Update a blocklist automation rule. Write action; requires confirmation.',
942
- scope: 'automation:write',
943
- readOnly: false,
944
- confirm: true,
945
- method: 'PUT',
946
- endpoint: '/automation-rules/:id',
947
- pathParams: ['id'],
948
- bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'status', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
949
- inputSchema: objectSchema({
950
- id: string('Automation rule id.'),
951
- name: string('Rule name.'),
952
- description: string('Optional rule description.'),
953
- status: string('active or disabled.'),
954
- conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array.' },
955
- conditionLogic: string('AND or OR.'),
956
- actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array.' },
957
- applicationsAll: boolean('Apply to all applications.'),
958
- applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
959
- environmentsAll: boolean('Apply to all environments.'),
960
- environments: arrayOfStrings('Deployment environments when not applying to all environments.'),
961
- ...confirmSchema,
962
- }, ['id', 'confirm', 'reason']),
963
- },
964
- {
965
- name: 'securenow_automation_rule_dry_run',
966
- title: 'Dry-Run Automation Rule',
967
- description: 'Preview automation matches without writing blocklist entries.',
968
- scope: 'automation:read',
969
- readOnly: true,
970
- method: 'POST',
971
- endpoint: '/automation-rules/:id/dry-run',
972
- pathParams: ['id'],
973
- bodyFields: ['limit', 'sampleLimit'],
974
- inputSchema: objectSchema({
975
- id: string('Automation rule id.'),
976
- limit: number('Maximum notifications to scan.', { minimum: 1, maximum: 2000 }),
977
- sampleLimit: number('Maximum sample matches to return.', { minimum: 1, maximum: 50 }),
978
- }, ['id']),
979
- },
980
- {
981
- name: 'securenow_automation_rule_execute',
982
- title: 'Execute Automation Rule',
983
- description: 'Execute an automation rule and add matching IPs to the blocklist. Write action; requires confirmation.',
984
- scope: 'automation:write',
985
- readOnly: false,
986
- destructive: true,
987
- confirm: true,
988
- method: 'POST',
989
- endpoint: '/automation-rules/:id/execute',
990
- pathParams: ['id'],
991
- inputSchema: objectSchema({
992
- id: string('Automation rule id.'),
993
- ...confirmSchema,
994
- }, ['id', 'confirm', 'reason']),
995
- },
996
- {
997
- name: 'securenow_automation_rule_delete',
998
- title: 'Delete Automation Rule',
999
- description: 'Delete an automation rule. Write action; requires confirmation.',
1000
- scope: 'automation:write',
1001
- readOnly: false,
1002
- destructive: true,
1003
- confirm: true,
1004
- method: 'DELETE',
1005
- endpoint: '/automation-rules/:id',
1006
- pathParams: ['id'],
1007
- inputSchema: objectSchema({
1008
- id: string('Automation rule id.'),
1009
- ...confirmSchema,
1010
- }, ['id', 'confirm', 'reason']),
1011
- },
1012
- {
1013
- name: 'securenow_rate_limit_list',
1014
- title: 'List Rate Limits',
1015
- description: 'List rate-limit remediation rules with app/environment scope.',
1016
- scope: 'rate_limits:read',
1017
- readOnly: true,
1018
- method: 'GET',
1019
- endpoint: '/rate-limits',
1020
- queryFields: ['status', 'search', 'page', 'limit', 'appKey', 'environment'],
1021
- inputSchema: objectSchema({
1022
- status: string('Rule status: active, disabled, removed, or all. Defaults to active.'),
1023
- search: string('Optional IP/path/name search.'),
1024
- ...pagingInput,
1025
- appKey: string('Optional application key scope.'),
1026
- ...environmentInput,
1027
- }),
1028
- },
1029
- {
1030
- name: 'securenow_rate_limit_get',
1031
- title: 'Get Rate Limit',
1032
- description: 'Fetch one rate-limit remediation rule.',
1033
- scope: 'rate_limits:read',
1034
- readOnly: true,
1035
- method: 'GET',
1036
- endpoint: '/rate-limits/:id',
1037
- pathParams: ['id'],
1038
- inputSchema: objectSchema({
1039
- id: string('Rate-limit rule id.'),
1040
- }, ['id']),
1041
- },
1042
- {
1043
- name: 'securenow_rate_limit_test',
1044
- title: 'Test Rate Limit Match',
1045
- description: 'Check whether an IP/path/method would match an active rate-limit rule.',
1046
- scope: 'rate_limits:read',
1047
- readOnly: true,
1048
- method: 'GET',
1049
- endpoint: '/rate-limits/check',
1050
- queryFields: ['ip', 'path', 'method', 'appKey', 'environment'],
1051
- inputSchema: objectSchema({
1052
- ip: string('IPv4 address to test.'),
1053
- path: string('Request path to test. Defaults to /.'),
1054
- method: string('HTTP method to test. Defaults to GET.'),
1055
- appKey: string('Optional application key scope.'),
1056
- ...environmentInput,
1057
- }, ['ip']),
1058
- },
1059
- {
1060
- name: 'securenow_rate_limit_remove',
1061
- title: 'Remove Rate Limit',
1062
- description: 'Remove a rate-limit remediation rule. Write action; requires confirmation.',
1063
- scope: 'rate_limits:write',
1064
- readOnly: false,
1065
- confirm: true,
1066
- method: 'DELETE',
1067
- endpoint: '/rate-limits/:id',
1068
- pathParams: ['id'],
1069
- bodyFields: ['reason'],
1070
- inputSchema: objectSchema({
1071
- id: string('Rate-limit rule id.'),
1072
- ...confirmSchema,
1073
- }, ['id', 'confirm', 'reason']),
1074
- },
1075
- {
1076
- name: 'securenow_rate_limit_parse',
1077
- title: 'Parse Rate Limit Text',
1078
- description: 'Use SecureNow AI to convert natural language into a rate-limit remediation draft without creating it.',
1079
- scope: 'rate_limits:read',
1080
- readOnly: true,
1081
- method: 'POST',
1082
- endpoint: '/rate-limits/parse',
1083
- bodyFields: ['text', 'draft'],
1084
- inputSchema: objectSchema({
1085
- text: string('Natural-language rate-limit request, e.g. "rate limit /api/login to 2 attempts per minute for this IP".'),
1086
- draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields to preserve or resolve phrases such as "this IP".' },
1087
- }, ['text']),
1088
- },
1089
- {
1090
- name: 'securenow_rate_limit_create_from_text',
1091
- title: 'Create Rate Limit From Text',
1092
- description: 'Create a rate-limit remediation rule from natural language. Write action; requires confirmation.',
1093
- scope: 'rate_limits:write',
1094
- readOnly: false,
1095
- confirm: true,
1096
- method: 'POST',
1097
- endpoint: '/rate-limits/from-text',
1098
- bodyFields: ['text', 'draft', 'reason'],
1099
- inputSchema: objectSchema({
1100
- text: string('Natural-language rate-limit request.'),
1101
- draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields or scope hints.' },
1102
- ...confirmSchema,
1103
- }, ['text', 'confirm', 'reason']),
1104
- },
1105
- {
1106
- name: 'securenow_alert_rules_list',
1107
- title: 'List Alert Rules',
1108
- description: 'List alert rules, including system rules and app scope.',
1109
- scope: 'alerts:read',
1110
- readOnly: true,
1111
- method: 'GET',
1112
- endpoint: '/alert-rules',
1113
- inputSchema: objectSchema({}),
1114
- },
1115
- {
1116
- name: 'securenow_alert_rule_get',
1117
- title: 'Get Alert Rule',
1118
- description: 'Fetch one alert rule.',
1119
- scope: 'alerts:read',
1120
- readOnly: true,
1121
- method: 'GET',
1122
- endpoint: '/alert-rules/:id',
1123
- pathParams: ['id'],
1124
- inputSchema: objectSchema({
1125
- id: string('Alert rule id.'),
1126
- }, ['id']),
1127
- },
1128
- {
1129
- name: 'securenow_alert_rule_update',
1130
- title: 'Update Alert Rule',
1131
- description: 'Update alert rule scope/status/schedule/throttle or record a review action. Write action; requires confirmation.',
1132
- scope: 'alerts:write',
1133
- readOnly: false,
1134
- confirm: true,
1135
- method: 'PUT',
1136
- endpoint: '/alert-rules/:id',
1137
- pathParams: ['id'],
1138
- bodyFields: ['name', 'description', 'status', 'applicationsAll', 'applications', 'schedule', 'throttle', 'alertChannelIds', 'reviewAction', 'reviewNote', 'reviewNotificationId'],
1139
- inputSchema: objectSchema({
1140
- id: string('Alert rule id.'),
1141
- name: string('Rule name for custom rules.'),
1142
- description: string('Rule description for custom rules.'),
1143
- status: string('Active, Disabled, or Paused.'),
1144
- applicationsAll: boolean('Scope rule to all applications.'),
1145
- applications: arrayOfStrings('Application keys when applicationsAll is false.'),
1146
- schedule: { type: 'object', additionalProperties: true, description: 'Schedule patch.' },
1147
- throttle: { type: 'object', additionalProperties: true, description: 'Throttle patch.' },
1148
- alertChannelIds: arrayOfStrings('Alert channel ids.'),
1149
- reviewAction: string('Rule review action, e.g. keep_active or saved_new_version.'),
1150
- reviewNote: string('Review note.'),
1151
- reviewNotificationId: string('Notification id tied to this review.'),
1152
- ...confirmSchema,
1153
- }, ['id', 'confirm', 'reason']),
1154
- },
1155
- {
1156
- name: 'securenow_alert_rule_query_update',
1157
- title: 'Update System Alert Rule Query',
1158
- description: 'Admin-only: update the shared public query mapping behind a system alert rule for all customer copies. Write action; requires confirmation.',
1159
- scope: 'alerts:write',
1160
- readOnly: false,
1161
- confirm: true,
1162
- method: 'PUT',
1163
- endpoint: '/alert-rules/:id/query-mapping',
1164
- pathParams: ['id'],
1165
- bodyFields: ['sqlQuery', 'reason', 'expectedCurrentSqlHash', 'applyGlobally', 'reactivatePausedCopies', 'reviewNotificationId', 'reviewNote', 'source'],
1166
- fixedBody: { applyGlobally: true, source: 'mcp' },
1167
- inputSchema: objectSchema({
1168
- id: string('Alert rule id. Must be a system rule.'),
1169
- sqlQuery: string('Full replacement SQL query. System SQL must keep __USER_APP_KEYS__ for tenant scoping.'),
1170
- expectedCurrentSqlHash: string('Optional SHA-256 hash of the current SQL to prevent stale overwrites.'),
1171
- applyGlobally: boolean('Must be true. Updates the shared public query mapping used by all system rule copies.'),
1172
- reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the query is tuned.'),
1173
- reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1174
- reviewNote: string('Optional review note. Defaults to reason.'),
1175
- ...confirmSchema,
1176
- }, ['id', 'sqlQuery', 'confirm', 'reason']),
1177
- },
1178
- {
1179
- name: 'securenow_alert_rule_instant_update',
1180
- title: 'Update Instant Alert Rule',
1181
- description: 'Patch instant rule conditions/config with version/hash guards and before/after seeded tests. System rules require applyGlobally:true and admin permission. Write action; requires confirmation.',
1182
- scope: 'alerts:write',
1183
- readOnly: false,
1184
- confirm: true,
1185
- method: 'PUT',
1186
- endpoint: '/alert-rules/:id/instant',
1187
- pathParams: ['id'],
1188
- bodyFields: [
1189
- 'operations',
1190
- 'conditions',
1191
- 'enabled',
1192
- 'source',
1193
- 'matchMode',
1194
- 'executionMode',
1195
- 'severity',
1196
- 'throttle',
1197
- 'expectedRuleVersion',
1198
- 'expectedCurrentInstantHash',
1199
- 'expectedCurrentRuleHash',
1200
- 'applyGlobally',
1201
- 'dryRun',
1202
- 'reactivatePausedCopies',
1203
- 'fixtures',
1204
- 'reviewNotificationId',
1205
- 'reviewNote',
1206
- 'reason',
1207
- ],
1208
- fixedBody: { auditSource: 'mcp' },
1209
- inputSchema: objectSchema({
1210
- id: string('Alert rule id. Fetch with securenow_alert_rule_get first to get ruleVersion and instantHash.'),
1211
- operations: {
1212
- type: 'array',
1213
- items: { type: 'object', additionalProperties: true },
1214
- description: 'Condition patch operations: add_condition, remove_condition, update_condition. Use zero-based index/conditionIndex for remove/update.',
1215
- },
1216
- conditions: {
1217
- type: 'array',
1218
- items: { type: 'object', additionalProperties: true },
1219
- description: 'Optional full replacement instant conditions array. Use operations for smaller patches.',
1220
- },
1221
- enabled: boolean('Whether instant detection is enabled.'),
1222
- source: string('Instant event source. Currently trace.'),
1223
- matchMode: string('Condition match mode: all or any.'),
1224
- executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1225
- severity: string('Optional severity override: critical, high, medium, or low.'),
1226
- throttle: { type: 'object', additionalProperties: true, description: 'Throttle patch, for example { enabled:true, minutes:15 }.' },
1227
- expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_get.', { minimum: 1 }),
1228
- expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_get.'),
1229
- expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_get.'),
1230
- applyGlobally: boolean('Required true for system rules. Updates every customer copy of the system rule.'),
1231
- dryRun: boolean('When true, validates and runs before/after seeded tests without saving.'),
1232
- reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the instant rule is tuned.'),
1233
- fixtures: {
1234
- type: 'array',
1235
- items: { type: 'object', additionalProperties: true },
1236
- description: 'Optional seeded fixtures. Each may include kind attack/benign, label, path, method, requestBody, requestHeaders, and expectMatch.',
1237
- },
1238
- reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1239
- reviewNote: string('Optional review note. Defaults to reason.'),
1240
- ...confirmSchema,
1241
- }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1242
- },
1243
- {
1244
- name: 'securenow_alert_rule_conditions_get',
1245
- title: 'Get Alert Rule Conditions',
1246
- description: 'Fetch instant rule conditions with rule version and hash guards for safe condition-level tuning.',
1247
- scope: 'alerts:read',
1248
- readOnly: true,
1249
- method: 'GET',
1250
- endpoint: '/alert-rules/:id/conditions',
1251
- pathParams: ['id'],
1252
- inputSchema: objectSchema({
1253
- id: string('Alert rule id.'),
1254
- }, ['id']),
1255
- },
1256
- {
1257
- name: 'securenow_alert_rule_condition_update',
1258
- title: 'Update Alert Rule Condition',
1259
- description: 'Patch instant alert-rule conditions with version/hash guards and seeded before/after tests. Compatibility alias for condition-level tuning.',
1260
- scope: 'alerts:write',
1261
- readOnly: false,
1262
- confirm: true,
1263
- method: 'PUT',
1264
- endpoint: '/alert-rules/:id/instant',
1265
- pathParams: ['id'],
1266
- bodyFields: [
1267
- 'confirm',
1268
- 'operations',
1269
- 'conditions',
1270
- 'enabled',
1271
- 'source',
1272
- 'matchMode',
1273
- 'executionMode',
1274
- 'expectedRuleVersion',
1275
- 'expectedCurrentInstantHash',
1276
- 'expectedCurrentRuleHash',
1277
- 'applyGlobally',
1278
- 'reactivatePausedCopies',
1279
- 'fixtures',
1280
- 'reviewNotificationId',
1281
- 'reviewNote',
1282
- 'reason',
1283
- ],
1284
- fixedBody: { auditSource: 'mcp' },
1285
- inputSchema: objectSchema({
1286
- id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1287
- operations: {
1288
- type: 'array',
1289
- items: { type: 'object', additionalProperties: true },
1290
- description: 'Condition patch operations: add_condition, remove_condition, update_condition. Use zero-based index/conditionIndex for remove/update.',
1291
- },
1292
- conditions: {
1293
- type: 'array',
1294
- items: { type: 'object', additionalProperties: true },
1295
- description: 'Optional full replacement instant conditions array. Use operations for smaller patches.',
1296
- },
1297
- enabled: boolean('Whether instant detection is enabled.'),
1298
- source: string('Instant event source. Currently trace.'),
1299
- matchMode: string('Condition match mode: all or any.'),
1300
- executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1301
- expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_conditions_get.', { minimum: 1 }),
1302
- expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_conditions_get.'),
1303
- expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_conditions_get.'),
1304
- applyGlobally: boolean('Required true for system rules. Updates every customer copy of the system rule.'),
1305
- reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the condition is tuned.'),
1306
- fixtures: {
1307
- type: 'array',
1308
- items: { type: 'object', additionalProperties: true },
1309
- description: 'Optional seeded fixtures. Each may include kind attack/benign, label, path, method, requestBody, requestHeaders, and expectMatch.',
1310
- },
1311
- reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1312
- reviewNote: string('Optional review note. Defaults to reason.'),
1313
- ...confirmSchema,
1314
- }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1315
- },
1316
- {
1317
- name: 'securenow_alert_rule_condition_candidate_test',
1318
- title: 'Test Alert Rule Condition Candidate',
1319
- description: 'Dry-run a condition-level instant-rule patch without saving it, including seeded benign/attack fixture checks.',
1320
- scope: 'alerts:write',
1321
- readOnly: false,
1322
- confirm: true,
1323
- method: 'PUT',
1324
- endpoint: '/alert-rules/:id/instant',
1325
- pathParams: ['id'],
1326
- bodyFields: [
1327
- 'confirm',
1328
- 'operations',
1329
- 'conditions',
1330
- 'enabled',
1331
- 'source',
1332
- 'matchMode',
1333
- 'executionMode',
1334
- 'expectedRuleVersion',
1335
- 'expectedCurrentInstantHash',
1336
- 'expectedCurrentRuleHash',
1337
- 'applyGlobally',
1338
- 'fixtures',
1339
- 'reviewNotificationId',
1340
- 'reviewNote',
1341
- 'reason',
1342
- ],
1343
- fixedBody: { auditSource: 'mcp', dryRun: true },
1344
- inputSchema: objectSchema({
1345
- id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1346
- operations: {
1347
- type: 'array',
1348
- items: { type: 'object', additionalProperties: true },
1349
- description: 'Candidate condition operations to test.',
1350
- },
1351
- conditions: {
1352
- type: 'array',
1353
- items: { type: 'object', additionalProperties: true },
1354
- description: 'Optional full replacement instant conditions array to test.',
1355
- },
1356
- enabled: boolean('Whether instant detection would be enabled.'),
1357
- source: string('Instant event source. Currently trace.'),
1358
- matchMode: string('Condition match mode: all or any.'),
1359
- executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1360
- expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_conditions_get.', { minimum: 1 }),
1361
- expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_conditions_get.'),
1362
- expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_conditions_get.'),
1363
- applyGlobally: boolean('Required true for system rules, even for admin dry-runs.'),
1364
- fixtures: {
1365
- type: 'array',
1366
- items: { type: 'object', additionalProperties: true },
1367
- description: 'Optional seeded fixtures. Each may include kind attack/benign, label, path, method, requestBody, requestHeaders, and expectMatch.',
1368
- },
1369
- reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1370
- reviewNote: string('Optional review note. Defaults to reason.'),
1371
- ...confirmSchema,
1372
- }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1373
- },
1374
- {
1375
- name: 'securenow_alert_rule_condition_diff',
1376
- title: 'Diff Alert Rule Condition Candidate',
1377
- description: 'Return the current instant conditions, candidate conditions, hashes, and before/after fixture behavior without saving.',
1378
- scope: 'alerts:write',
1379
- readOnly: false,
1380
- confirm: true,
1381
- method: 'PUT',
1382
- endpoint: '/alert-rules/:id/instant',
1383
- pathParams: ['id'],
1384
- bodyFields: [
1385
- 'confirm',
1386
- 'operations',
1387
- 'conditions',
1388
- 'enabled',
1389
- 'source',
1390
- 'matchMode',
1391
- 'executionMode',
1392
- 'expectedRuleVersion',
1393
- 'expectedCurrentInstantHash',
1394
- 'expectedCurrentRuleHash',
1395
- 'applyGlobally',
1396
- 'fixtures',
1397
- 'reason',
1398
- ],
1399
- fixedBody: { auditSource: 'mcp', dryRun: true },
1400
- inputSchema: objectSchema({
1401
- id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1402
- operations: {
1403
- type: 'array',
1404
- items: { type: 'object', additionalProperties: true },
1405
- description: 'Candidate condition operations to diff.',
1406
- },
1407
- conditions: {
1408
- type: 'array',
1409
- items: { type: 'object', additionalProperties: true },
1410
- description: 'Optional full replacement instant conditions array to diff.',
1411
- },
1412
- enabled: boolean('Whether instant detection would be enabled.'),
1413
- source: string('Instant event source. Currently trace.'),
1414
- matchMode: string('Condition match mode: all or any.'),
1415
- executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1416
- expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_conditions_get.', { minimum: 1 }),
1417
- expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_conditions_get.'),
1418
- expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_conditions_get.'),
1419
- applyGlobally: boolean('Required true for system rules, even for admin dry-runs.'),
1420
- fixtures: {
1421
- type: 'array',
1422
- items: { type: 'object', additionalProperties: true },
1423
- description: 'Optional seeded fixtures used to explain benign removals and attack coverage.',
1424
- },
1425
- ...confirmSchema,
1426
- }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1427
- },
1428
- {
1429
- name: 'securenow_alert_rule_test',
1430
- title: 'Test Alert Rule',
1431
- description: 'Start a live or dry-run alert rule test.',
1432
- scope: 'alerts:write',
1433
- readOnly: false,
1434
- confirm: true,
1435
- method: 'POST',
1436
- endpoint: '/alert-rules/:id/test',
1437
- pathParams: ['id'],
1438
- bodyFields: ['applicationKey', 'mode'],
1439
- inputSchema: objectSchema({
1440
- id: string('Alert rule id.'),
1441
- applicationKey: string('Application key to test.'),
1442
- mode: string('dry_run or live.'),
1443
- ...confirmSchema,
1444
- }, ['id', 'confirm', 'reason']),
1445
- },
1446
- {
1447
- name: 'securenow_alert_rule_candidate_test',
1448
- title: 'Dry-Run Candidate Alert Rule SQL',
1449
- description: 'Dry-run a candidate SQL/query guard for an alert rule without saving it.',
1450
- scope: 'alerts:write',
1451
- readOnly: false,
1452
- confirm: true,
1453
- method: 'POST',
1454
- endpoint: '/alert-rules/:id/test',
1455
- pathParams: ['id'],
1456
- bodyFields: ['applicationKey', 'candidateSqlQuery'],
1457
- fixedBody: { mode: 'dry_run' },
1458
- inputSchema: objectSchema({
1459
- id: string('Alert rule id.'),
1460
- applicationKey: string('Application key to test.'),
1461
- candidateSqlQuery: string('Full candidate SQL query to dry-run without saving.'),
1462
- ...confirmSchema,
1463
- }, ['id', 'candidateSqlQuery', 'confirm', 'reason']),
1464
- },
1465
- {
1466
- name: 'securenow_alert_rule_test_result',
1467
- title: 'Get Alert Rule Test Result',
1468
- description: 'Poll alert rule test status and results.',
1469
- scope: 'alerts:read',
1470
- readOnly: true,
1471
- method: 'GET',
1472
- endpoint: '/alert-rules/:id/test/:testId',
1473
- pathParams: ['id', 'testId'],
1474
- inputSchema: objectSchema({
1475
- id: string('Alert rule id.'),
1476
- testId: string('Alert rule test id.'),
1477
- }, ['id', 'testId']),
1478
- },
1479
- {
1480
- name: 'securenow_alert_rule_exclusions_list',
1481
- title: 'List Alert Rule Exclusions',
1482
- description: 'List exclusions embedded on one alert rule.',
1483
- scope: 'alerts:read',
1484
- readOnly: true,
1485
- method: 'GET',
1486
- endpoint: '/alert-rules/:id/exclusions',
1487
- pathParams: ['id'],
1488
- inputSchema: objectSchema({
1489
- id: string('Alert rule id.'),
1490
- }, ['id']),
1491
- },
1492
- {
1493
- name: 'securenow_alert_rule_exclusion_add',
1494
- title: 'Add Alert Rule Exclusion',
1495
- description: 'Add a restrictive exclusion to one alert rule. Write action; requires confirmation.',
1496
- scope: 'alerts:write',
1497
- readOnly: false,
1498
- confirm: true,
1499
- method: 'POST',
1500
- endpoint: '/alert-rules/:id/exclusions',
1501
- pathParams: ['id'],
1502
- bodyFields: ['conditions', 'matchMode', 'reason', 'pathPattern', 'isActive'],
1503
- inputSchema: objectSchema({
1504
- id: string('Alert rule id.'),
1505
- conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Restrictive exclusion conditions.' },
1506
- matchMode: string('all or any.'),
1507
- reason: string('Exclusion reason.'),
1508
- pathPattern: string('Optional path pattern.'),
1509
- isActive: boolean('Whether the exclusion is active.'),
1510
- ...confirmSchema,
1511
- }, ['id', 'confirm', 'reason']),
1512
- },
1513
- {
1514
- name: 'securenow_alert_rule_exclusion_remove',
1515
- title: 'Remove Alert Rule Exclusion',
1516
- description: 'Remove an alert rule exclusion. Write action; requires confirmation.',
1517
- scope: 'alerts:write',
1518
- readOnly: false,
1519
- confirm: true,
1520
- method: 'DELETE',
1521
- endpoint: '/alert-rules/:id/exclusions/:exclusionId',
1522
- pathParams: ['id', 'exclusionId'],
1523
- inputSchema: objectSchema({
1524
- id: string('Alert rule id.'),
1525
- exclusionId: string('Exclusion id.'),
1526
- ...confirmSchema,
1527
- }, ['id', 'exclusionId', 'confirm', 'reason']),
1528
- },
1529
- {
1530
- name: 'securenow_blocklist_list',
1531
- title: 'List Blocklist',
1532
- description: 'List blocked IPs.',
1533
- scope: 'blocklist:read',
1534
- readOnly: true,
1535
- method: 'GET',
1536
- endpoint: '/blocklist',
1537
- queryFields: ['page', 'limit', 'status', 'approvalStatus', 'search', 'view', 'appKey', 'environment'],
1538
- inputSchema: objectSchema({
1539
- ...pagingInput,
1540
- status: string('Block entry status: active or removed. Defaults to active.'),
1541
- approvalStatus: string('Optional approval filter: pending, approved, or rejected.'),
1542
- search: string('Optional IP prefix search.'),
1543
- view: string('List view: all or operational.'),
1544
- appKey: string('Optional application key scope.'),
1545
- ...environmentInput,
1546
- }),
1547
- },
1548
- {
1549
- name: 'securenow_blocklist_pending_list',
1550
- title: 'List Pending Block Approvals',
1551
- description: 'List legacy or AI-prepared blocklist entries with approvalStatus=pending. Pending blocks are not enforced until approved.',
1552
- scope: 'blocklist:read',
1553
- readOnly: true,
1554
- method: 'GET',
1555
- endpoint: '/blocklist/pending',
1556
- queryFields: ['page', 'limit', 'search', 'appKey', 'environment'],
1557
- inputSchema: objectSchema({
1558
- ...pagingInput,
1559
- search: string('Optional IP prefix search.'),
1560
- appKey: string('Optional application key scope.'),
1561
- ...environmentInput,
1562
- }),
1563
- },
1564
- {
1565
- name: 'securenow_blocklist_pending_approve',
1566
- title: 'Approve Pending Block',
1567
- description: 'Approve one pending blocklist entry so the firewall enforces it. Write action; requires confirmation.',
1568
- scope: 'blocklist:write',
1569
- readOnly: false,
1570
- destructive: true,
1571
- confirm: true,
1572
- method: 'POST',
1573
- endpoint: '/blocklist/:id/approve',
1574
- pathParams: ['id'],
1575
- bodyFields: ['reason'],
1576
- inputSchema: objectSchema({
1577
- id: string('Pending blocklist entry id.'),
1578
- ...confirmSchema,
1579
- }, ['id', 'confirm', 'reason']),
1580
- },
1581
- {
1582
- name: 'securenow_blocklist_pending_reject',
1583
- title: 'Reject Pending Block',
1584
- description: 'Reject one pending blocklist entry and remove it from enforcement consideration. Write action; requires confirmation.',
1585
- scope: 'blocklist:write',
1586
- readOnly: false,
1587
- confirm: true,
1588
- method: 'POST',
1589
- endpoint: '/blocklist/:id/reject',
1590
- pathParams: ['id'],
1591
- bodyFields: ['reason'],
1592
- inputSchema: objectSchema({
1593
- id: string('Pending blocklist entry id.'),
1594
- ...confirmSchema,
1595
- }, ['id', 'confirm', 'reason']),
1596
- },
1597
- {
1598
- name: 'securenow_blocklist_pending_bulk_approve',
1599
- title: 'Bulk Approve Pending Blocks',
1600
- description: 'Approve multiple pending blocklist entries after they have all been reviewed under the same safe policy. Write action; requires confirmation.',
1601
- scope: 'blocklist:write',
1602
- readOnly: false,
1603
- destructive: true,
1604
- confirm: true,
1605
- method: 'POST',
1606
- endpoint: '/blocklist/bulk-approve',
1607
- bodyFields: ['ids', 'reason'],
1608
- inputSchema: objectSchema({
1609
- ids: arrayOfStrings('Pending blocklist entry ids to approve.'),
1610
- ...confirmSchema,
1611
- }, ['ids', 'confirm', 'reason']),
1612
- },
1613
- {
1614
- name: 'securenow_blocklist_pending_bulk_reject',
1615
- title: 'Bulk Reject Pending Blocks',
1616
- description: 'Reject multiple pending blocklist entries after they have all been reviewed as false positives or stale/ambiguous. Write action; requires confirmation.',
1617
- scope: 'blocklist:write',
1618
- readOnly: false,
1619
- confirm: true,
1620
- method: 'POST',
1621
- endpoint: '/blocklist/bulk-reject',
1622
- bodyFields: ['ids', 'reason'],
1623
- inputSchema: objectSchema({
1624
- ids: arrayOfStrings('Pending blocklist entry ids to reject.'),
1625
- ...confirmSchema,
1626
- }, ['ids', 'confirm', 'reason']),
1627
- },
1628
- {
1629
- name: 'securenow_blocklist_add',
1630
- title: 'Add Blocked IP',
1631
- description: 'Add an IP/CIDR to the blocklist, optionally scoped to a route/method. Write action; requires confirmation.',
1632
- scope: 'blocklist:write',
1633
- readOnly: false,
1634
- confirm: true,
1635
- method: 'POST',
1636
- endpoint: '/blocklist',
1637
- bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment', 'pathPattern', 'pathMatchMode', 'method'],
1638
- inputSchema: objectSchema({
1639
- ip: string('IPv4 address or CIDR.'),
1640
- reason: string('Reason for blocking.'),
1641
- expiresAt: string('Optional expiry time as ISO 8601.'),
1642
- metadata: { type: 'object', additionalProperties: true, description: 'Optional metadata.' },
1643
- appKey: string('Optional application key to scope this block. Omit for all apps.'),
1644
- pathPattern: string('Optional route/path pattern, for example /admin*. Omit to block all routes.'),
1645
- pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'], description: 'Path matching mode. Defaults to prefix.' },
1646
- method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], description: 'HTTP method scope. Defaults to ALL.' },
1647
- ...environmentInput,
1648
- ...confirmSchema,
1649
- }, ['ip', 'confirm', 'reason']),
1650
- },
1651
- {
1652
- name: 'securenow_blocklist_remove',
1653
- title: 'Unblock IP (Compat Alias)',
1654
- description: 'Compatibility alias for unblocking a blocklist entry while retaining block report/history. Write action; requires confirmation.',
1655
- scope: 'blocklist:write',
1656
- readOnly: false,
1657
- confirm: true,
1658
- method: 'POST',
1659
- endpoint: '/blocklist/:id/unblock',
1660
- pathParams: ['id'],
1661
- bodyFields: ['reason'],
1662
- inputSchema: objectSchema({
1663
- id: string('Blocklist entry id.'),
1664
- ...confirmSchema,
1665
- }, ['id', 'confirm', 'reason']),
1666
- },
1667
- {
1668
- name: 'securenow_blocklist_unblock',
1669
- title: 'Unblock IP And Keep History',
1670
- description: 'Unblock a blocklist entry so firewall enforcement stops, while retaining the block report, history, and unblock audit trail. Write action; requires confirmation.',
1671
- scope: 'blocklist:write',
1672
- readOnly: false,
1673
- confirm: true,
1674
- method: 'POST',
1675
- endpoint: '/blocklist/:id/unblock',
1676
- pathParams: ['id'],
1677
- bodyFields: ['reason'],
1678
- inputSchema: objectSchema({
1679
- id: string('Blocklist entry id.'),
1680
- ...confirmSchema,
1681
- }, ['id', 'confirm', 'reason']),
1682
- },
1683
- {
1684
- name: 'securenow_blocklist_stats',
1685
- title: 'Blocklist Stats',
1686
- description: 'Return blocklist statistics.',
1687
- scope: 'blocklist:read',
1688
- readOnly: true,
1689
- method: 'GET',
1690
- endpoint: '/blocklist/stats',
1691
- inputSchema: objectSchema({}),
1692
- },
1693
- {
1694
- name: 'securenow_allowlist_list',
1695
- title: 'List Restrictive Allowlist',
1696
- description: 'List restrictive allowlist entries. If any active entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. This is not the Trusted IP list.',
1697
- scope: 'allowlist:read',
1698
- readOnly: true,
1699
- method: 'GET',
1700
- endpoint: '/allowlist',
1701
- queryFields: ['page', 'limit', 'appKey', 'environment'],
1702
- inputSchema: objectSchema({
1703
- ...pagingInput,
1704
- appKey: string('Optional application key scope.'),
1705
- ...environmentInput,
1706
- }),
1707
- },
1708
- {
1709
- name: 'securenow_allowlist_add',
1710
- title: 'Add Restrictive Allowlist IP',
1711
- description: 'Dangerous production action: add an IP/CIDR to the restrictive allowlist. Once active, allowlist mode blocks every IP except listed entries. Do not use this to mark an IP trusted or suppress false positives; use securenow_trusted_add instead. Requires explicit human approval confirming deny-all behavior.',
1712
- scope: 'allowlist:write',
1713
- readOnly: false,
1714
- confirm: true,
1715
- method: 'POST',
1716
- endpoint: '/allowlist',
1717
- fixedBody: { initiatedBy: 'mcp' },
1718
- bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys', 'environment', 'allowlistDenyAllApproved'],
1719
- inputSchema: objectSchema({
1720
- ip: string('IPv4 address or CIDR.'),
1721
- label: string('Human-readable label.'),
1722
- reason: string('Required human-approved reason. Must acknowledge that allowlist blocks all non-listed IPs and why Trusted IPs is not the correct action.'),
1723
- expiresAt: string('Optional expiry time as ISO 8601.'),
1724
- applicationsAll: boolean('Apply to all applications.'),
1725
- applicationKeys: arrayOfStrings('Application keys to scope this allowlist entry to.'),
1726
- allowlistDenyAllApproved: boolean('Must be true only after the user explicitly approves deny-by-default allowlist behavior. Do not set this for trusted IPs, monitors, false positives, or investigation cleanup.'),
1727
- ...environmentInput,
1728
- ...confirmSchema,
1729
- }, ['ip', 'confirm', 'reason', 'allowlistDenyAllApproved']),
1730
- },
1731
- {
1732
- name: 'securenow_allowlist_remove',
1733
- title: 'Remove Allowed IP',
1734
- description: 'Remove an allowlist entry. Write action; requires confirmation.',
1735
- scope: 'allowlist:write',
1736
- readOnly: false,
1737
- confirm: true,
1738
- method: 'DELETE',
1739
- endpoint: '/allowlist/:id',
1740
- pathParams: ['id'],
1741
- inputSchema: objectSchema({
1742
- id: string('Allowlist entry id.'),
1743
- ...confirmSchema,
1744
- }, ['id', 'confirm', 'reason']),
1745
- },
1746
- {
1747
- name: 'securenow_trusted_list',
1748
- title: 'List Trusted IPs',
1749
- description: 'List trusted IPs. Trusted IPs are for known-safe traffic and do not turn on deny-by-default allowlist mode.',
1750
- scope: 'trusted_ips:read',
1751
- readOnly: true,
1752
- method: 'GET',
1753
- endpoint: '/trusted-ips',
1754
- queryFields: ['appKey', 'environment'],
1755
- inputSchema: objectSchema({
1756
- appKey: string('Optional application key scope.'),
1757
- ...environmentInput,
1758
- }),
1759
- },
1760
- {
1761
- name: 'securenow_trusted_add',
1762
- title: 'Add Trusted IP',
1763
- description: 'Add a trusted IP/CIDR for known-safe infrastructure, monitors, office/VPN traffic, or scoped false-positive suppression. This does not enable deny-by-default allowlist mode and is the correct tool when an IP should be trusted without blocking other visitors. Write action; requires confirmation.',
1764
- scope: 'trusted_ips:write',
1765
- readOnly: false,
1766
- confirm: true,
1767
- method: 'POST',
1768
- endpoint: '/trusted-ips',
1769
- bodyFields: ['ip', 'label', 'note', 'applicationsAll', 'applicationKeys', 'environment'],
1770
- inputSchema: objectSchema({
1771
- ip: string('IPv4 address or CIDR.'),
1772
- label: string('Human-readable label.'),
1773
- note: string('Optional note.'),
1774
- applicationsAll: boolean('Apply to all applications.'),
1775
- applicationKeys: arrayOfStrings('Application keys to scope this trusted IP to.'),
1776
- ...environmentInput,
1777
- ...confirmSchema,
1778
- }, ['ip', 'confirm', 'reason']),
1779
- },
1780
- {
1781
- name: 'securenow_trusted_remove',
1782
- title: 'Remove Trusted IP',
1783
- description: 'Remove a trusted IP entry. Write action; requires confirmation.',
1784
- scope: 'trusted_ips:write',
1785
- readOnly: false,
1786
- confirm: true,
1787
- method: 'DELETE',
1788
- endpoint: '/trusted-ips/:id',
1789
- pathParams: ['id'],
1790
- inputSchema: objectSchema({
1791
- id: string('Trusted IP entry id.'),
1792
- ...confirmSchema,
1793
- }, ['id', 'confirm', 'reason']),
1794
- },
1795
- ];
1796
-
1797
- const RESOURCES = [
1798
- {
1799
- uri: 'securenow://docs/skill-api',
1800
- name: 'skill-api',
1801
- title: 'SecureNow SDK Skill',
1802
- description: 'Installed SDK skill instructions from SKILL-API.md.',
1803
- mimeType: MARKDOWN,
1804
- file: '../SKILL-API.md',
1805
- },
1806
- {
1807
- uri: 'securenow://docs/skill-cli',
1808
- name: 'skill-cli',
1809
- title: 'SecureNow CLI Skill',
1810
- description: 'Installed CLI skill instructions from SKILL-CLI.md.',
1811
- mimeType: MARKDOWN,
1812
- file: '../SKILL-CLI.md',
1813
- },
1814
- {
1815
- uri: 'securenow://docs/npm-readme',
1816
- name: 'npm-readme',
1817
- title: 'SecureNow npm README',
1818
- description: 'Installed npm README.',
1819
- mimeType: MARKDOWN,
1820
- file: '../NPM_README.md',
1821
- },
1822
- {
1823
- uri: 'securenow://project/config',
1824
- name: 'project-config',
1825
- title: 'SecureNow Project Config',
1826
- description: 'Resolved local SecureNow config and masked credentials.',
1827
- mimeType: JSON_MIME,
1828
- dynamic: 'projectConfig',
1829
- },
1830
- {
1831
- uri: 'securenow://tools/catalog',
1832
- name: 'tools-catalog',
1833
- title: 'SecureNow MCP Tool Catalog',
1834
- description: 'MCP tool names, scopes, and write/read classification.',
1835
- mimeType: JSON_MIME,
1836
- dynamic: 'toolsCatalog',
1837
- },
1838
- ];
1839
-
1840
- const PROMPTS = [
1841
- {
1842
- name: 'secure_project_setup',
1843
- title: 'Secure Project Setup',
1844
- description: 'Generic framework setup prompt for SecureNow tracing, logs, body capture, multipart, and firewall defaults.',
1845
- arguments: [
1846
- { name: 'projectRoot', description: 'Project root to configure.', required: false },
1847
- { name: 'startCommand', description: 'Optional customer-approved command to start the app.', required: false },
1848
- { name: 'buildCommand', description: 'Optional customer-approved build command.', required: false },
1849
- { name: 'testCommand', description: 'Optional customer-approved test command.', required: false },
1850
- { name: 'localUrl', description: 'Optional local URL to smoke-test after the app is running.', required: false },
1851
- { name: 'productionUrl', description: 'Optional production URL to smoke-test with --env production.', required: false },
1852
- ],
1853
- },
1854
- {
1855
- name: 'verify_firewall_default_on',
1856
- title: 'Verify Default-On Firewall',
1857
- description: 'Verify SecureNow CLI login, app firewall toggle, credentials, and SDK defaults.',
1858
- arguments: [
1859
- { name: 'appKey', description: 'Optional SecureNow app key.', required: false },
1860
- ],
1861
- },
1862
- {
1863
- name: 'investigate_ip',
1864
- title: 'Investigate IP',
1865
- description: 'Investigate an IP with SecureNow IP intelligence, traces, logs, and remediation options.',
1866
- arguments: [
1867
- { name: 'ip', description: 'IP address to investigate.', required: true },
1868
- { name: 'appKeys', description: 'Optional comma-separated app keys.', required: false },
1869
- { name: 'environment', description: 'Environment to investigate. Defaults to production; use all only when explicitly needed.', required: false },
1870
- ],
1871
- },
1872
- {
1873
- name: 'investigate_human_action_row',
1874
- title: 'Investigate Human Action Row',
1875
- description: 'Use MCP tools to deeply review one Requires Human row and reach a safe audited outcome.',
1876
- arguments: [
1877
- { name: 'rowNumber', description: '1-based row number from the Requires Human queue.', required: true },
1878
- { name: 'page', description: 'Queue page number. Defaults to 1.', required: false },
1879
- { name: 'limit', description: 'Rows to fetch. Defaults to 20.', required: false },
1880
- { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
1881
- ],
1882
- },
1883
- {
1884
- name: 'work_human_actions',
1885
- title: 'Work Human Actions',
1886
- description: 'Use MCP tools to work the approval queue from most urgent to least urgent, with trace verification and scoped decisions.',
1887
- arguments: [
1888
- { name: 'limit', description: 'Maximum rows to review this run. Defaults to 10.', required: false },
1889
- { name: 'search', description: 'Optional search filter for the queue.', required: false },
1890
- { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
1891
- ],
1892
- },
1893
- {
1894
- name: 'cleanup_legacy_pending_blocks',
1895
- title: 'Clean Legacy Pending Blocks',
1896
- description: 'Use MCP tools to review and approve/reject legacy pending blocklist rows so only current human work remains.',
1897
- arguments: [
1898
- { name: 'limit', description: 'Maximum pending block rows to review this run. Defaults to 50.', required: false },
1899
- { name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
1900
- { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute approvals/rejections.', required: false },
1901
- ],
1902
- },
1903
- {
1904
- name: 'configure_default_automation',
1905
- title: 'Configure Default Automation',
1906
- description: 'Use MCP tools to set conservative default SecureNow automation rules based on the canonical riskScore and supporting evidence.',
1907
- arguments: [
1908
- { name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
1909
- { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to create/update automation rules.', required: false },
1910
- ],
1911
- },
1912
- ];
1913
-
1914
- function promptMessages(name, args = {}) {
1915
- if (name === 'secure_project_setup') {
1916
- return [
1917
- {
1918
- role: 'user',
1919
- content: {
1920
- type: 'text',
1921
- text: [
1922
- args.projectRoot ? `Project root: ${args.projectRoot}` : null,
1923
- args.startCommand ? `Customer-approved start command: ${args.startCommand}` : null,
1924
- args.buildCommand ? `Customer-approved build command: ${args.buildCommand}` : null,
1925
- args.testCommand ? `Customer-approved test command: ${args.testCommand}` : null,
1926
- args.localUrl ? `Local URL for smoke testing: ${args.localUrl}` : null,
1927
- args.productionUrl ? `Production URL for smoke testing: ${args.productionUrl}` : null,
1928
- UNIVERSAL_SECURENOW_SETUP_PROMPT,
1929
- ].filter(Boolean).join('\n\n'),
1930
- },
1931
- },
1932
- ];
1933
- }
1934
-
1935
- if (name === 'verify_firewall_default_on') {
1936
- return [
1937
- {
1938
- role: 'user',
1939
- content: {
1940
- type: 'text',
1941
- text: [
1942
- 'Verify SecureNow default-on protection for this project.',
1943
- args.appKey ? `App key: ${args.appKey}` : null,
1944
- 'Check npx securenow whoami, npx securenow api-key show, npx securenow firewall apps, and npx securenow firewall status.',
1945
- 'Confirm traces, logs, capture.body, capture.multipart, and firewall.enabled are enabled by .securenow/runtime.json defaults unless explicitly set false. Legacy .securenow/credentials.json is still accepted.',
1946
- 'Do not print full tokens or API keys.',
1947
- ].filter(Boolean).join('\n'),
1948
- },
1949
- },
1950
- ];
1951
- }
1952
-
1953
- if (name === 'investigate_ip') {
1954
- return [
1955
- {
1956
- role: 'user',
1957
- content: {
1958
- type: 'text',
1959
- text: [
1960
- `Investigate IP ${args.ip || '<ip>'} with SecureNow.`,
1961
- args.appKeys ? `Scope to app keys: ${args.appKeys}` : null,
1962
- `Environment scope: ${args.environment || 'production'}.`,
1963
- 'Use IP intelligence first, then related traces/logs, then recommend remediation.',
1964
- 'Only block or trust the IP after explicit user confirmation. Never use IP Allowlist for false positives or trusted traffic; allowlist is deny-by-default and blocks all non-listed IPs.',
1965
- ].filter(Boolean).join('\n'),
1966
- },
1967
- },
1968
- ];
1969
- }
1970
-
1971
- if (name === 'investigate_human_action_row') {
1972
- const rowNumber = args.rowNumber || '<rowNumber>';
1973
- const page = args.page || 1;
1974
- const limit = args.limit || 20;
1975
- const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
1976
- return [
1977
- {
1978
- role: 'user',
1979
- content: {
1980
- type: 'text',
1981
- text: [
1982
- `Investigate Requires Human row ${rowNumber} using SecureNow MCP.`,
1983
- `Fetch page=${page}, limit=${limit} with securenow_human_actions_list, select row ${rowNumber}, then call securenow_notifications_get and securenow_human_action_report for that notificationId and IP.`,
1984
- 'Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
1985
- 'Open trace evidence with securenow_traces_show and correlated logs with securenow_logs_for_trace when trace IDs are available.',
1986
- 'Return one clear outcome: Block IP, Rate Limit, False Positive, Rule Tuning Applied/Needed, New Alert Rule Needed, Case Action Approved/Rejected, or Ambiguous/Skipped with exact missing proof.',
1987
- 'A row can be finished without an enforcement write when no write is safe. In that case record or prepare a structured no-write decision report explaining the missing proof, missing permission, or vague guard.',
1988
- 'If evidence is mixed, split the decision by IP/cluster. Block only proven malicious IPs, false-positive only exact benign shapes, rate-limit only route-specific volume abuse, and leave ambiguous members with missing proof.',
1989
- 'If the right fix is global/system rule tuning and the MCP tool returns access denied, do not create customer-specific false positives. Prepare the exact attack-preserving guard/SQL, dry-run it if possible, and record outcome=rule_tuning with missingProof naming the missing permission.',
1990
- 'If an AI proposed action says to use a shared benign shape but does not provide exact path/method/status/user-agent/body/header conditions, do not execute it. Record the missing guard fields instead.',
1991
- 'Use rate limiting only as temporary soft remediation for repeated route-specific abuse such as login brute force, credential stuffing, scraping/API bursts, enumeration, recon/probing, or repeated noisy payloads where risk/impact evidence is below the block threshold.',
1992
- 'Do not rate-limit instead of blocking when there is confirmed exploit success, token/data exposure, SSRF reachability, file read, RCE, persistence, malware/C2, or riskScore >= 85 with high-confidence malicious evidence. Do not rate-limit benign false positives, trusted monitors, app-server/proxy attribution problems, isolated one-off requests, or broad noisy rules that need alert tuning.',
1993
- 'Do not create or recommend IP Allowlist entries during investigations unless the user explicitly asks to lock the app/environment to a known set of IPs. For safe monitors, offices, VPNs, or false positives, use Trusted IPs or scoped false-positive exclusions instead.',
1994
- confirmWrites
1995
- ? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block, securenow_rate_limit_create_from_text plus securenow_human_action_decision_report_add(outcome=rate_limited), or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If no enforcement write is safe, call securenow_human_action_decision_report_add with outcome=rule_tuning, new_alert_rule, ambiguous, skipped, or other so the audit trail is complete without changing IP status.'
1996
- : 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
1997
- 'If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
1998
- 'False positives must be narrow: app + alert rule + path + method/status/user-agent/body evidence where possible. Never globally trust an IP by default.',
1999
- ].join('\n'),
2000
- },
2001
- },
2002
- ];
2003
- }
2004
-
2005
- if (name === 'work_human_actions') {
2006
- const limit = args.limit || 10;
2007
- const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
2008
- return [
2009
- {
2010
- role: 'user',
2011
- content: {
2012
- type: 'text',
2013
- text: [
2014
- 'Work my SecureNow Requires Human queue like a senior security analyst using the MCP tools.',
2015
- `Review up to ${limit} row(s), most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
2016
- 'Start with securenow_human_actions_list. For each row, call securenow_notifications_get and securenow_human_action_report, inspect the AI report/investigation steps/proofs/trace IDs, and fetch trace/log evidence where useful.',
2017
- 'For each row choose exactly one outcome: Block IP, Rate Limit, False Positive, Rule Tuning Applied/Needed, New Alert Rule Needed, Case Action Approved/Rejected, or Ambiguous/Skipped with exact missing proof.',
2018
- 'Finished does not always mean enforcement changed. A row is also finished when a structured no-write decision report records the exact proof gap, missing permission, or vague guard and no weaker substitute write is used.',
2019
- 'If a global/system rule update is the right fix but access is denied, do not mark customer-specific false positives just to clear the row. Prepare the exact attack-preserving guard/SQL, dry-run it when possible, and record outcome=rule_tuning with missingProof.',
2020
- 'If evidence is mixed, do partial resolution: block proven malicious IPs, false-positive exact benign shapes, rate-limit route-specific volume abuse, and leave ambiguous IPs with their missing proof.',
2021
- 'Reject vague AI proposed actions. If the prompt/action lacks concrete rule id, path, method, status, user-agent/body/header guard, benign samples, and attack examples that still match, record it as ambiguous or rule_tuning needed.',
2022
- 'Use Rate Limit only for repeated route-specific abuse where temporary friction is safer than blocking: login brute force/credential stuffing, password reset/account enumeration, scraping/API bursts, path or ID enumeration, recon/probing, or repeated noisy payloads without confirmed exploit success.',
2023
- 'Do not rate-limit confirmed high-risk attacks that should be blocked, benign traffic that should be false-positive scoped, broad noisy rules that need tuning, app-server/proxy attribution problems, or isolated one-off requests.',
2024
- 'Do not create or recommend IP Allowlist entries while working this queue unless the user explicitly approves deny-by-default allowlist mode for the whole app/environment. Use Trusted IPs for trusted/bypass cases.',
2025
- confirmWrites
2026
- ? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For skipped/ambiguous/rule-tuning-needed/new-rule-needed rows that should be auditable without changing enforcement status, use securenow_human_action_decision_report_add or securenow_human_case_decision_report_add.'
2027
- : 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
2028
- 'For block decisions, use securenow_human_action_block. For rate-limit decisions, use securenow_rate_limit_create_from_text and then securenow_human_action_decision_report_add with outcome=rate_limited. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update only when the action is safe to approve/reject.',
2029
- 'End with counts: handled, blocked/proposed block, rate limits created/proposed, false positives, rule tuning/admin permission blockers with exact guard, partial resolutions, skipped with missing proof, and still waiting.',
2030
- ].join('\n'),
2031
- },
2032
- },
2033
- ];
2034
- }
2035
-
2036
- if (name === 'cleanup_legacy_pending_blocks') {
2037
- const limit = args.limit || 50;
2038
- const environment = args.environment || 'production';
2039
- const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
2040
- return [
2041
- {
2042
- role: 'user',
2043
- content: {
2044
- type: 'text',
2045
- text: [
2046
- 'Clean my SecureNow legacy pending blocklist queue using MCP tools.',
2047
- `Review up to ${limit} pending block row(s). Environment scope: ${environment}.`,
2048
- 'Start with securenow_blocklist_stats, then securenow_blocklist_pending_list({ page: 1, limit, environment }).',
2049
- 'For each pending block, inspect id, IP, source, reason, metadata.riskScore, metadata.aiRiskScore, metadata.abuseConfidenceScore, automation rule, linked notification, age, app, and environment.',
2050
- 'When investigationNotificationId exists, fetch securenow_notifications_get for that case and use the linked IP report/history as evidence.',
2051
- 'Approve only when the row has clear malicious evidence, riskScore >= 90, or SecureNow AI IPDB evidence score >= 80 with no false-positive/test signal.',
2052
- 'Reject stale, ambiguous, synthetic/test, self-traffic, or false-positive rows. Prefer narrow rule/exclusion tuning when the same benign pattern repeats.',
2053
- confirmWrites
2054
- ? 'The user requested execution. Use securenow_blocklist_pending_approve or securenow_blocklist_pending_reject with confirm:true and a precise reason. Use bulk tools only after every selected row satisfies the same reviewed policy.'
2055
- : 'Do not execute writes yet. Produce exact approve/reject MCP calls grouped by safe policy for user approval.',
2056
- 'After each write or batch, re-fetch securenow_blocklist_stats and securenow_blocklist_pending_list until the legacy pending count is zero or only ambiguous rows remain.',
2057
- 'End with counts: approved, rejected, skipped ambiguous, legacyPendingBlockCount, and remaining proof gaps.',
2058
- ].join('\n'),
2059
- },
2060
- },
2061
- ];
2062
- }
2063
-
2064
- if (name === 'configure_default_automation') {
2065
- const environment = args.environment || 'production';
2066
- const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
2067
- return [
2068
- {
2069
- role: 'user',
2070
- content: {
2071
- type: 'text',
2072
- text: [
2073
- 'Configure SecureNow default automation using the MCP tools.',
2074
- `Review environment context: ${environment}. Built-in defaults apply to all apps and all environments unless the customer narrows them later.`,
2075
- 'Use one canonical product score for automation decisions: riskScore. SecureNow AI IPDB score and AI confidence remain supporting evidence.',
2076
- 'Built-in defaults are active by default:',
2077
- '- Critical Risk Auto-Block: riskScore>=95, TTL 168h.',
2078
- '- High Risk Auto-Block: riskScore>=90 AND riskScore<95, TTL 72h.',
2079
- '- Elevated Risk Auto-Block: riskScore>=85 AND riskScore<90, TTL 24h.',
2080
- 'Start with securenow_automation_defaults_ensure({ confirm:true, reason:"Provision SecureNow default risk-score automation" }) when writes are confirmed, otherwise start with securenow_automation_rules_list and report what would be ensured.',
2081
- 'After ensuring defaults, dry-run each default rule with securenow_automation_rule_dry_run to inspect sample matches. Disable or tune only if evidence shows customer-specific false positives.',
2082
- confirmWrites
2083
- ? 'The user requested execution. Ensure defaults with confirm:true, do not force-enable previously disabled defaults unless the user explicitly asked, and re-list rules after writes.'
2084
- : 'Do not execute writes yet. Return exact MCP update/create calls and dry-run calls for approval.',
2085
- 'End with active rules, disabled rules that should stay disabled, and any risk from automation scope.',
2086
- ].join('\n'),
2087
- },
2088
- },
2089
- ];
2090
- }
2091
-
2092
- throw new Error(`Unknown prompt: ${name}`);
2093
- }
2094
-
2095
- function getTool(name) {
2096
- return TOOLS.find((tool) => tool.name === name) || null;
2097
- }
2098
-
2099
- function mcpToolDefinition(tool) {
2100
- return {
2101
- name: tool.name,
2102
- title: tool.title,
2103
- description: tool.description,
2104
- inputSchema: tool.inputSchema || objectSchema({}),
2105
- annotations: {
2106
- readOnlyHint: !!tool.readOnly,
2107
- destructiveHint: !!tool.destructive,
2108
- idempotentHint: !!tool.readOnly,
2109
- },
2110
- };
2111
- }
2112
-
2113
- function listTools() {
2114
- return TOOLS.map(mcpToolDefinition);
2115
- }
2116
-
2117
- function listResources() {
2118
- return RESOURCES.map(({ file, dynamic, ...resource }) => resource);
2119
- }
2120
-
2121
- function listPrompts() {
2122
- return PROMPTS;
2123
- }
2124
-
2125
- function assertConfirmed(tool, args = {}) {
2126
- if (!tool.confirm) return;
2127
- if (args.confirm !== true) {
2128
- throw new Error(`${tool.name} is a write action. Pass confirm:true and a reason to proceed.`);
2129
- }
2130
- if (!args.reason || !String(args.reason).trim()) {
2131
- throw new Error(`${tool.name} requires a non-empty reason.`);
2132
- }
2133
- if (tool.name === 'securenow_allowlist_add' && args.allowlistDenyAllApproved !== true) {
2134
- throw new Error('securenow_allowlist_add is deny-by-default: it blocks every non-listed IP for the scoped app/environment. Pass allowlistDenyAllApproved:true only after explicit human approval. Use securenow_trusted_add for trusted IPs or false positives.');
2135
- }
2136
- }
2137
-
2138
- function buildApiRequest(tool, rawArgs = {}) {
2139
- const args = { ...rawArgs };
2140
- let endpoint = tool.endpoint;
2141
-
2142
- for (const key of tool.pathParams || []) {
2143
- if (args[key] == null || args[key] === '') throw new Error(`Missing required argument: ${key}`);
2144
- endpoint = endpoint.replace(`:${key}`, encodeURIComponent(String(args[key])));
2145
- }
2146
-
2147
- const query = { ...(tool.fixedQuery || {}) };
2148
- for (const key of tool.queryFields || []) {
2149
- let value = args[key];
2150
- if (tool.normalize && typeof tool.normalize[key] === 'function') {
2151
- value = tool.normalize[key](value);
2152
- }
2153
- if (value != null && value !== '') query[key] = value;
2154
- }
2155
-
2156
- const body = { ...(tool.fixedBody || {}) };
2157
- for (const key of tool.bodyFields || []) {
2158
- let value = args[key];
2159
- if (tool.normalize && typeof tool.normalize[key] === 'function') {
2160
- value = tool.normalize[key](value);
2161
- }
2162
- if (value != null && value !== '') body[key] = value;
2163
- }
2164
-
2165
- if (tool.reasonAsNote && !body.note && args.reason) {
2166
- body.note = args.reason;
2167
- }
2168
- if (tool.reasonInResult && args.reason) {
2169
- body.result = {
2170
- ...(body.result && typeof body.result === 'object' ? body.result : {}),
2171
- reason: args.reason,
2172
- };
2173
- }
2174
-
2175
- return {
2176
- method: tool.method,
2177
- endpoint,
2178
- query,
2179
- body: Object.keys(body).length > 0 ? body : null,
2180
- };
2181
- }
2182
-
2183
- function readStaticResource(resource) {
2184
- const filepath = path.resolve(__dirname, resource.file);
2185
- return fs.readFileSync(filepath, 'utf8');
2186
- }
2187
-
2188
- function resourceContent(resource, dynamicHandlers = {}) {
2189
- if (resource.file) return readStaticResource(resource);
2190
- if (resource.dynamic && dynamicHandlers[resource.dynamic]) {
2191
- return dynamicHandlers[resource.dynamic]();
2192
- }
2193
- throw new Error(`Resource is not readable: ${resource.uri}`);
2194
- }
2195
-
2196
- module.exports = {
2197
- TOOLS,
2198
- RESOURCES,
2199
- PROMPTS,
2200
- TEXT,
2201
- JSON_MIME,
2202
- MARKDOWN,
2203
- getTool,
2204
- mcpToolDefinition,
2205
- listTools,
2206
- listResources,
2207
- listPrompts,
2208
- promptMessages,
2209
- assertConfirmed,
2210
- buildApiRequest,
2211
- resourceContent,
2212
- jsonText,
2213
- maskSecret,
2214
- sanitizeArgs,
2215
- };
88
+
89
+ function string(description) {
90
+ return { type: 'string', description };
91
+ }
92
+
93
+ function number(description, extra = {}) {
94
+ return { type: 'number', description, ...extra };
95
+ }
96
+
97
+ function boolean(description) {
98
+ return { type: 'boolean', description };
99
+ }
100
+
101
+ function arrayOfStrings(description) {
102
+ return { type: 'array', items: { type: 'string' }, description };
103
+ }
104
+
105
+ function objectSchema(properties, required = []) {
106
+ return {
107
+ type: 'object',
108
+ additionalProperties: false,
109
+ properties,
110
+ required,
111
+ };
112
+ }
113
+
114
+ function jsonText(value) {
115
+ return JSON.stringify(value, null, 2);
116
+ }
117
+
118
+ function maskSecret(value) {
119
+ if (!value) return null;
120
+ const text = String(value);
121
+ if (text.length <= 12) return '***';
122
+ return `${text.slice(0, 8)}...${text.slice(-4)}`;
123
+ }
124
+
125
+ function normalizeAppKeys(value) {
126
+ if (Array.isArray(value)) return value.filter(Boolean).join(',');
127
+ return value;
128
+ }
129
+
130
+ function sanitizeArgs(args = {}) {
131
+ const clone = { ...args };
132
+ for (const key of Object.keys(clone)) {
133
+ if (/token|apiKey|api_key|authorization|password|secret/i.test(key)) {
134
+ clone[key] = maskSecret(clone[key]);
135
+ }
136
+ }
137
+ return clone;
138
+ }
139
+
140
+ const confirmSchema = {
141
+ confirm: boolean('Required for write actions. Must be true.'),
142
+ reason: string('Short human-readable reason for the write action.'),
143
+ };
144
+
145
+ const appKeysInput = {
146
+ appKeys: {
147
+ oneOf: [
148
+ { type: 'string' },
149
+ { type: 'array', items: { type: 'string' } },
150
+ ],
151
+ description: 'Application key or comma-separated/list of application keys.',
152
+ },
153
+ };
154
+
155
+ const pagingInput = {
156
+ limit: number('Maximum results to return.', { minimum: 1, maximum: 500 }),
157
+ page: number('Page number.', { minimum: 1 }),
158
+ };
159
+
160
+ const timeRangeInput = {
161
+ from: string('Start time as ISO 8601, optional.'),
162
+ to: string('End time as ISO 8601, optional.'),
163
+ };
164
+
165
+ const environmentInput = {
166
+ environment: string('Deployment environment scope: production, staging, preview, local, test, or all. Default for investigations is production.'),
167
+ };
168
+
169
+ const decisionReportInput = {
170
+ decisionReport: {
171
+ type: 'object',
172
+ additionalProperties: true,
173
+ description: 'Structured audit report explaining the decision, evidence reviewed, trace IDs, missing proof, and recommendations.',
174
+ },
175
+ decisionSummary: string('Short decision summary to record on the IP/case history.'),
176
+ outcome: string('Decision outcome: blocked, rate_limited, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, or other.'),
177
+ evidence: arrayOfStrings('Evidence strings that support the decision.'),
178
+ reviewedHistory: arrayOfStrings('History/proofs reviewed before deciding.'),
179
+ traceIds: arrayOfStrings('Trace IDs reviewed for this decision.'),
180
+ paths: arrayOfStrings('Paths/endpoints reviewed for this decision.'),
181
+ ips: arrayOfStrings('IP addresses affected by this case-level decision report.'),
182
+ methods: arrayOfStrings('HTTP methods reviewed for this decision.'),
183
+ statusCodes: arrayOfStrings('HTTP status codes reviewed for this decision.'),
184
+ userAgents: arrayOfStrings('User agents reviewed for this decision.'),
185
+ missingProof: arrayOfStrings('Proof that was missing when the row is skipped or ambiguous.'),
186
+ recommendations: arrayOfStrings('Follow-up recommendations to record with the decision.'),
187
+ };
188
+
189
+ const TOOLS = [
190
+ {
191
+ name: 'securenow_auth_status',
192
+ title: 'SecureNow Auth Status',
193
+ description: 'Show local SecureNow credential source, selected app, and masked runtime API key.',
194
+ scope: null,
195
+ localOnly: true,
196
+ readOnly: true,
197
+ inputSchema: objectSchema({}),
198
+ },
199
+ {
200
+ name: 'securenow_apps_list',
201
+ title: 'List Applications',
202
+ description: 'List SecureNow applications for the authenticated user.',
203
+ scope: 'applications:read',
204
+ readOnly: true,
205
+ method: 'GET',
206
+ endpoint: '/applications',
207
+ inputSchema: objectSchema({}),
208
+ },
209
+ {
210
+ name: 'securenow_apps_create',
211
+ title: 'Create Application',
212
+ description: 'Create a SecureNow application. Write action; requires confirmation.',
213
+ scope: 'applications:write',
214
+ readOnly: false,
215
+ confirm: true,
216
+ method: 'POST',
217
+ endpoint: '/applications',
218
+ bodyFields: ['name', 'hosts', 'instanceId'],
219
+ inputSchema: objectSchema({
220
+ name: string('Application name.'),
221
+ hosts: arrayOfStrings('Optional hostnames/domains for this application.'),
222
+ instanceId: string('Optional ClickHouse instance id.'),
223
+ ...confirmSchema,
224
+ }, ['name', 'confirm', 'reason']),
225
+ },
226
+ {
227
+ name: 'securenow_apps_info',
228
+ title: 'Get Application By Key',
229
+ description: 'Get details for an application by app key.',
230
+ scope: 'applications:read',
231
+ readOnly: true,
232
+ method: 'GET',
233
+ endpoint: '/applications/key/:key',
234
+ pathParams: ['key'],
235
+ inputSchema: objectSchema({
236
+ key: string('Application key UUID.'),
237
+ }, ['key']),
238
+ },
239
+ {
240
+ name: 'securenow_firewall_apps',
241
+ title: 'List Firewall Apps',
242
+ description: 'List applications with firewall toggle and SecureNow AI IPDB threshold state.',
243
+ scope: 'applications:read',
244
+ readOnly: true,
245
+ method: 'GET',
246
+ endpoint: '/firewall/apps',
247
+ inputSchema: objectSchema({}),
248
+ },
249
+ {
250
+ name: 'securenow_firewall_status',
251
+ title: 'Firewall Status',
252
+ description: 'Show firewall blocklist/allowlist status for the authenticated account.',
253
+ scope: 'firewall:read',
254
+ readOnly: true,
255
+ method: 'GET',
256
+ endpoint: '/firewall/status',
257
+ queryFields: ['environment', 'appKey'],
258
+ inputSchema: objectSchema({
259
+ appKey: string('Optional application key UUID to scope the status check.'),
260
+ ...environmentInput,
261
+ }),
262
+ },
263
+ {
264
+ name: 'securenow_firewall_enable',
265
+ title: 'Enable App Firewall',
266
+ description: 'Turn the per-app firewall ON. Write action; requires confirmation.',
267
+ scope: 'applications:write',
268
+ readOnly: false,
269
+ confirm: true,
270
+ method: 'PATCH',
271
+ endpoint: '/firewall/app/:appKey',
272
+ pathParams: ['appKey'],
273
+ bodyFields: ['environment'],
274
+ fixedBody: { enabled: true },
275
+ inputSchema: objectSchema({
276
+ appKey: string('Application key UUID.'),
277
+ ...environmentInput,
278
+ ...confirmSchema,
279
+ }, ['appKey', 'confirm', 'reason']),
280
+ },
281
+ {
282
+ name: 'securenow_firewall_disable',
283
+ title: 'Disable App Firewall',
284
+ description: 'Turn the per-app firewall OFF. Write action; requires confirmation.',
285
+ scope: 'applications:write',
286
+ readOnly: false,
287
+ destructive: true,
288
+ confirm: true,
289
+ method: 'PATCH',
290
+ endpoint: '/firewall/app/:appKey',
291
+ pathParams: ['appKey'],
292
+ bodyFields: ['environment'],
293
+ fixedBody: { enabled: false },
294
+ inputSchema: objectSchema({
295
+ appKey: string('Application key UUID.'),
296
+ ...environmentInput,
297
+ ...confirmSchema,
298
+ }, ['appKey', 'confirm', 'reason']),
299
+ },
300
+ {
301
+ name: 'securenow_firewall_set_threshold',
302
+ title: 'Set SecureNow AI IPDB Threshold',
303
+ description: 'Set the per-app SecureNow AI IPDB confidence threshold. Write action; requires confirmation.',
304
+ scope: 'applications:write',
305
+ readOnly: false,
306
+ confirm: true,
307
+ method: 'PATCH',
308
+ endpoint: '/firewall/app/:appKey',
309
+ pathParams: ['appKey'],
310
+ bodyFields: ['confidenceMinimum', 'environment'],
311
+ inputSchema: objectSchema({
312
+ appKey: string('Application key UUID.'),
313
+ confidenceMinimum: number('Minimum SecureNow AI IPDB confidence score.', { minimum: 0, maximum: 100 }),
314
+ ...environmentInput,
315
+ ...confirmSchema,
316
+ }, ['appKey', 'confidenceMinimum', 'confirm', 'reason']),
317
+ },
318
+ {
319
+ name: 'securenow_firewall_test_ip',
320
+ title: 'Test Firewall IP Decision',
321
+ description: 'Check whether an IP would be blocked by the firewall.',
322
+ scope: 'firewall:read',
323
+ readOnly: true,
324
+ method: 'GET',
325
+ endpoint: '/firewall/check/:ip',
326
+ pathParams: ['ip'],
327
+ queryFields: ['environment', 'appKey'],
328
+ inputSchema: objectSchema({
329
+ ip: string('IPv4 address to test.'),
330
+ appKey: string('Optional application key UUID to test the app/environment toggle and scoped lists.'),
331
+ ...environmentInput,
332
+ }, ['ip']),
333
+ },
334
+ {
335
+ name: 'securenow_traces_list',
336
+ title: 'List Traces',
337
+ description: 'List recent traces for one or more applications.',
338
+ scope: 'traces:read',
339
+ readOnly: true,
340
+ method: 'GET',
341
+ endpoint: '/traces',
342
+ queryFields: ['appKeys', 'environment', 'from', 'to', 'limit'],
343
+ normalize: { appKeys: normalizeAppKeys },
344
+ inputSchema: objectSchema({
345
+ ...appKeysInput,
346
+ ...environmentInput,
347
+ ...timeRangeInput,
348
+ limit: number('Maximum traces to return.', { minimum: 1, maximum: 200 }),
349
+ }, ['appKeys']),
350
+ },
351
+ {
352
+ name: 'securenow_traces_show',
353
+ title: 'Show Trace',
354
+ description: 'Show spans for a trace id scoped to one or more app keys.',
355
+ scope: 'traces:read',
356
+ readOnly: true,
357
+ method: 'GET',
358
+ endpoint: '/traces/:traceId',
359
+ pathParams: ['traceId'],
360
+ queryFields: ['appKeys', 'environment'],
361
+ normalize: { appKeys: normalizeAppKeys },
362
+ inputSchema: objectSchema({
363
+ traceId: string('Trace id.'),
364
+ ...appKeysInput,
365
+ ...environmentInput,
366
+ }, ['traceId', 'appKeys']),
367
+ },
368
+ {
369
+ name: 'securenow_logs_list',
370
+ title: 'List Logs',
371
+ description: 'List recent logs for one or more applications.',
372
+ scope: 'logs:read',
373
+ readOnly: true,
374
+ method: 'GET',
375
+ endpoint: '/logs',
376
+ queryFields: ['appKeys', 'environment', 'from', 'to', 'severity', 'limit'],
377
+ normalize: { appKeys: normalizeAppKeys },
378
+ inputSchema: objectSchema({
379
+ ...appKeysInput,
380
+ ...environmentInput,
381
+ ...timeRangeInput,
382
+ severity: string('Optional severity filter.'),
383
+ limit: number('Maximum logs to return.', { minimum: 1, maximum: 500 }),
384
+ }, ['appKeys']),
385
+ },
386
+ {
387
+ name: 'securenow_logs_for_trace',
388
+ title: 'Logs For Trace',
389
+ description: 'Show logs correlated to a trace id.',
390
+ scope: 'logs:read',
391
+ readOnly: true,
392
+ method: 'GET',
393
+ endpoint: '/logs/trace/:traceId',
394
+ pathParams: ['traceId'],
395
+ queryFields: ['appKeys', 'environment'],
396
+ normalize: { appKeys: normalizeAppKeys },
397
+ inputSchema: objectSchema({
398
+ traceId: string('Trace id.'),
399
+ ...appKeysInput,
400
+ ...environmentInput,
401
+ }, ['traceId', 'appKeys']),
402
+ },
403
+ {
404
+ name: 'securenow_notifications_list',
405
+ title: 'List Notifications',
406
+ description: 'List security notifications.',
407
+ scope: 'notifications:read',
408
+ readOnly: true,
409
+ method: 'GET',
410
+ endpoint: '/notifications',
411
+ queryFields: ['limit', 'page'],
412
+ inputSchema: objectSchema({ ...pagingInput }),
413
+ },
414
+ {
415
+ name: 'securenow_notifications_unread',
416
+ title: 'Unread Notification Count',
417
+ description: 'Return unread notification count.',
418
+ scope: 'notifications:read',
419
+ readOnly: true,
420
+ method: 'GET',
421
+ endpoint: '/notifications/unread-count',
422
+ inputSchema: objectSchema({}),
423
+ },
424
+ {
425
+ name: 'securenow_notifications_read',
426
+ title: 'Mark Notification Read',
427
+ description: 'Mark a notification as read. Write action; requires confirmation.',
428
+ scope: 'notifications:write',
429
+ readOnly: false,
430
+ confirm: true,
431
+ method: 'PUT',
432
+ endpoint: '/notifications/:id/read',
433
+ pathParams: ['id'],
434
+ inputSchema: objectSchema({
435
+ id: string('Notification id.'),
436
+ ...confirmSchema,
437
+ }, ['id', 'confirm', 'reason']),
438
+ },
439
+ {
440
+ name: 'securenow_notifications_get',
441
+ title: 'Get Notification',
442
+ description: 'Get one notification/case with alert context, clusters, and IP investigations.',
443
+ scope: 'notifications:read',
444
+ readOnly: true,
445
+ method: 'GET',
446
+ endpoint: '/notifications/:id',
447
+ pathParams: ['id'],
448
+ inputSchema: objectSchema({
449
+ id: string('Notification id.'),
450
+ }, ['id']),
451
+ },
452
+ {
453
+ name: 'securenow_notifications_batch_get',
454
+ title: 'Get Notifications Batch',
455
+ description: 'Fetch multiple notification/case records in one request.',
456
+ scope: 'notifications:read',
457
+ readOnly: true,
458
+ method: 'POST',
459
+ endpoint: '/notifications/batch',
460
+ bodyFields: ['ids'],
461
+ inputSchema: objectSchema({
462
+ ids: arrayOfStrings('Notification ids to fetch, up to 50.'),
463
+ }, ['ids']),
464
+ },
465
+ {
466
+ name: 'securenow_human_actions_list',
467
+ title: 'List Human Action Queue',
468
+ description: 'List approval-gated human decisions prepared by SecureNow AI, ordered by urgency.',
469
+ scope: 'notifications:read',
470
+ readOnly: true,
471
+ method: 'GET',
472
+ endpoint: '/notifications/approval-tasks',
473
+ queryFields: ['limit', 'page', 'search'],
474
+ inputSchema: objectSchema({
475
+ ...pagingInput,
476
+ search: string('Optional search over IP, rule, path, or verdict.'),
477
+ }),
478
+ },
479
+ {
480
+ name: 'securenow_human_actions_grouped_list',
481
+ title: 'List Grouped Human Actions',
482
+ description: 'List Requires Human work as root-cause groups with batch-safety preview instead of one row per IP/action.',
483
+ scope: 'notifications:read',
484
+ readOnly: true,
485
+ method: 'GET',
486
+ endpoint: '/notifications/approval-task-groups',
487
+ queryFields: ['limit', 'page', 'search'],
488
+ inputSchema: objectSchema({
489
+ ...pagingInput,
490
+ search: string('Optional search over IP, rule, path, action, or verdict.'),
491
+ }),
492
+ },
493
+ {
494
+ name: 'securenow_alert_review_runs_list',
495
+ title: 'List Alert Review Root Causes',
496
+ description: 'List pending alert-review runs as root-cause groups with batch-action preview, rather than row-by-row noisy alert triage.',
497
+ scope: 'notifications:read',
498
+ readOnly: true,
499
+ method: 'GET',
500
+ endpoint: '/notifications/alert-review-runs',
501
+ queryFields: ['limit', 'page', 'search', 'rootCauseKey', 'groupKey', 'includeRuns', 'maxScan'],
502
+ inputSchema: objectSchema({
503
+ ...pagingInput,
504
+ search: string('Optional search over rule, route, IP, evidence, or proposed guard.'),
505
+ rootCauseKey: string('Optional root-cause group key to filter to one group.'),
506
+ groupKey: string('Compatibility alias for rootCauseKey.'),
507
+ includeRuns: boolean('Whether to include compact matching run rows in the response.'),
508
+ maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
509
+ }),
510
+ },
511
+ {
512
+ name: 'securenow_alert_review_runs_count',
513
+ title: 'Count Alert Review Runs',
514
+ description: 'Count pending alert-review rows and the root-cause groups they collapse into.',
515
+ scope: 'notifications:read',
516
+ readOnly: true,
517
+ method: 'GET',
518
+ endpoint: '/notifications/alert-review-runs/count',
519
+ queryFields: ['search', 'maxScan'],
520
+ inputSchema: objectSchema({
521
+ search: string('Optional search over rule, route, IP, evidence, or proposed guard.'),
522
+ maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
523
+ }),
524
+ },
525
+ {
526
+ name: 'securenow_alert_review_run_get',
527
+ title: 'Get Alert Review Run',
528
+ description: 'Fetch one pending-review alert run with its root-cause key, evidence-quality score, and similar open siblings.',
529
+ scope: 'notifications:read',
530
+ readOnly: true,
531
+ method: 'GET',
532
+ endpoint: '/notifications/alert-review-runs/:id',
533
+ pathParams: ['id'],
534
+ queryFields: ['maxScan'],
535
+ inputSchema: objectSchema({
536
+ id: string('Notification id for the alert review run.'),
537
+ maxScan: number('Maximum pending-review rows to scan for similar siblings. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
538
+ }, ['id']),
539
+ },
540
+ {
541
+ name: 'securenow_alert_review_run_resolve',
542
+ title: 'Resolve Alert Review Run',
543
+ description: 'Close one pending alert-review row with an auditable no-write decision report.',
544
+ scope: 'notifications:write',
545
+ readOnly: false,
546
+ confirm: true,
547
+ method: 'POST',
548
+ endpoint: '/notifications/alert-review-runs/:id/resolve',
549
+ pathParams: ['id'],
550
+ fixedBody: { reportSource: 'mcp' },
551
+ bodyFields: ['confirm', 'reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
552
+ inputSchema: objectSchema({
553
+ id: string('Notification id for the alert review run.'),
554
+ ...decisionReportInput,
555
+ ...confirmSchema,
556
+ }, ['id', 'confirm', 'reason']),
557
+ },
558
+ {
559
+ name: 'securenow_alert_review_run_dismiss',
560
+ title: 'Dismiss Alert Review Run',
561
+ description: 'Dismiss one pending alert-review row with an audit report and no enforcement write.',
562
+ scope: 'notifications:write',
563
+ readOnly: false,
564
+ confirm: true,
565
+ method: 'POST',
566
+ endpoint: '/notifications/alert-review-runs/:id/dismiss',
567
+ pathParams: ['id'],
568
+ fixedBody: { reportSource: 'mcp' },
569
+ bodyFields: ['confirm', 'reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
570
+ inputSchema: objectSchema({
571
+ id: string('Notification id for the alert review run.'),
572
+ ...decisionReportInput,
573
+ ...confirmSchema,
574
+ }, ['id', 'confirm', 'reason']),
575
+ },
576
+ {
577
+ name: 'securenow_alert_review_run_keep_alive',
578
+ title: 'Keep Alert Rule Active',
579
+ description: 'Mark the noisy/paused alert rule as reviewed, keep it active, and close the pending alert-review row with an audit report.',
580
+ scope: 'notifications:write',
581
+ readOnly: false,
582
+ confirm: true,
583
+ method: 'POST',
584
+ endpoint: '/notifications/alert-review-runs/:id/keep-alive',
585
+ pathParams: ['id'],
586
+ fixedBody: { reportSource: 'mcp' },
587
+ bodyFields: ['confirm', 'reason', 'reviewNote', 'decisionReport', 'decisionSummary', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
588
+ inputSchema: objectSchema({
589
+ id: string('Notification id for the alert review run.'),
590
+ reviewNote: string('Optional rule-review note. Defaults to reason.'),
591
+ ...decisionReportInput,
592
+ ...confirmSchema,
593
+ }, ['id', 'confirm', 'reason']),
594
+ },
595
+ {
596
+ name: 'securenow_alert_review_run_apply_group_decision',
597
+ title: 'Apply Alert Review Group Decision',
598
+ description: 'Apply one verified no-write decision report to every currently matching alert-review row in a root-cause group, leaving non-matching rows open.',
599
+ scope: 'notifications:write',
600
+ readOnly: false,
601
+ confirm: true,
602
+ method: 'POST',
603
+ endpoint: '/notifications/alert-review-runs/apply-group-decision',
604
+ fixedBody: { reportSource: 'mcp' },
605
+ bodyFields: ['confirm', 'reason', 'rootCauseKey', 'groupKey', 'prototypeRunId', 'notificationIds', 'status', 'outcome', 'allowMixed', 'previewOnly', 'maxRows', 'maxScan', 'search', 'decisionReport', 'decisionSummary', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
606
+ inputSchema: objectSchema({
607
+ rootCauseKey: string('Root-cause group key from securenow_alert_review_runs_list.'),
608
+ groupKey: string('Compatibility alias for rootCauseKey.'),
609
+ prototypeRunId: string('Optional notification id whose root-cause key should be used.'),
610
+ notificationIds: arrayOfStrings('Optional subset of eligible notification ids to close. Leave empty to apply to every eligible row in the group.'),
611
+ status: string('Resolution status: resolved or dismissed. Defaults to resolved.'),
612
+ allowMixed: boolean('Allow a mixed decision/attribution group only after every row was manually reviewed. Defaults to false.'),
613
+ previewOnly: boolean('When true, returns the eligible rows without writing.'),
614
+ maxRows: number('Maximum rows to close in this batch. Defaults to 100, capped at 250.', { minimum: 1, maximum: 250 }),
615
+ maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
616
+ search: string('Optional search filter applied before grouping.'),
617
+ ...decisionReportInput,
618
+ ...confirmSchema,
619
+ }, ['confirm', 'reason']),
620
+ },
621
+ {
622
+ name: 'securenow_alert_review_apply_group_decision',
623
+ title: 'Apply Alert Review Group Decision',
624
+ description: 'Compatibility alias for securenow_alert_review_run_apply_group_decision.',
625
+ scope: 'notifications:write',
626
+ readOnly: false,
627
+ confirm: true,
628
+ method: 'POST',
629
+ endpoint: '/notifications/alert-review-runs/apply-group-decision',
630
+ fixedBody: { reportSource: 'mcp' },
631
+ bodyFields: ['confirm', 'reason', 'rootCauseKey', 'groupKey', 'prototypeRunId', 'notificationIds', 'status', 'outcome', 'allowMixed', 'previewOnly', 'maxRows', 'maxScan', 'search', 'decisionReport', 'decisionSummary', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
632
+ inputSchema: objectSchema({
633
+ rootCauseKey: string('Root-cause group key from securenow_alert_review_runs_list.'),
634
+ groupKey: string('Compatibility alias for rootCauseKey.'),
635
+ prototypeRunId: string('Optional notification id whose root-cause key should be used.'),
636
+ notificationIds: arrayOfStrings('Optional subset of eligible notification ids to close. Leave empty to apply to every eligible row in the group.'),
637
+ status: string('Resolution status: resolved or dismissed. Defaults to resolved.'),
638
+ allowMixed: boolean('Allow a mixed decision/attribution group only after every row was manually reviewed. Defaults to false.'),
639
+ previewOnly: boolean('When true, returns the eligible rows without writing.'),
640
+ maxRows: number('Maximum rows to close in this batch. Defaults to 100, capped at 250.', { minimum: 1, maximum: 250 }),
641
+ maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
642
+ search: string('Optional search filter applied before grouping.'),
643
+ ...decisionReportInput,
644
+ ...confirmSchema,
645
+ }, ['confirm', 'reason']),
646
+ },
647
+ {
648
+ name: 'securenow_alert_review_group_similar',
649
+ title: 'List Similar Alert Review Runs',
650
+ description: 'Fetch the current rows in one alert-review root-cause group for preview before a batch decision.',
651
+ scope: 'notifications:read',
652
+ readOnly: true,
653
+ method: 'GET',
654
+ endpoint: '/notifications/alert-review-runs',
655
+ queryFields: ['rootCauseKey', 'groupKey', 'includeRuns', 'maxScan'],
656
+ fixedQuery: { includeRuns: true },
657
+ inputSchema: objectSchema({
658
+ rootCauseKey: string('Root-cause group key from securenow_alert_review_runs_list.'),
659
+ groupKey: string('Compatibility alias for rootCauseKey.'),
660
+ maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
661
+ }),
662
+ },
663
+ {
664
+ name: 'securenow_human_action_report',
665
+ title: 'Get Human Action Report',
666
+ description: 'Fetch the full IP investigation report, investigation steps, proofs, metadata, and AI decision for one human action row.',
667
+ scope: 'notifications:read',
668
+ readOnly: true,
669
+ method: 'GET',
670
+ endpoint: '/notifications/:notificationId/ips/:ip',
671
+ pathParams: ['notificationId', 'ip'],
672
+ inputSchema: objectSchema({
673
+ notificationId: string('Notification id from the human action row.'),
674
+ ip: string('IP address from the human action row.'),
675
+ }, ['notificationId', 'ip']),
676
+ },
677
+ {
678
+ name: 'securenow_human_action_block',
679
+ title: 'Approve AI Block Recommendation',
680
+ description: 'Approve the AI-prepared block decision for an IP and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
681
+ scope: 'notifications:write',
682
+ readOnly: false,
683
+ destructive: true,
684
+ confirm: true,
685
+ method: 'PUT',
686
+ endpoint: '/notifications/:notificationId/ips/:ip/status',
687
+ pathParams: ['notificationId', 'ip'],
688
+ fixedBody: { status: 'blocked', decisionSource: 'ai_dag', reportSource: 'mcp' },
689
+ reasonAsNote: true,
690
+ bodyFields: ['note', 'verdict', 'riskScore', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
691
+ inputSchema: objectSchema({
692
+ notificationId: string('Notification id from the human action row.'),
693
+ ip: string('IP address to block.'),
694
+ note: string('Audit note explaining why the AI block was approved.'),
695
+ verdict: string('Optional final verdict text.'),
696
+ riskScore: number('Optional risk score.', { minimum: 0, maximum: 100 }),
697
+ ...decisionReportInput,
698
+ ...confirmSchema,
699
+ }, ['notificationId', 'ip', 'confirm', 'reason']),
700
+ },
701
+ {
702
+ name: 'securenow_human_action_decision_report_add',
703
+ title: 'Record Human Action Decision Report',
704
+ description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for rate-limited, skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
705
+ scope: 'notifications:write',
706
+ readOnly: false,
707
+ confirm: true,
708
+ method: 'POST',
709
+ endpoint: '/notifications/:notificationId/ips/:ip/decision-report',
710
+ pathParams: ['notificationId', 'ip'],
711
+ fixedBody: { reportSource: 'mcp' },
712
+ bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
713
+ inputSchema: objectSchema({
714
+ notificationId: string('Notification id from the human action row.'),
715
+ ip: string('IP address for the report.'),
716
+ ...decisionReportInput,
717
+ ...confirmSchema,
718
+ }, ['notificationId', 'ip', 'confirm', 'reason']),
719
+ },
720
+ {
721
+ name: 'securenow_human_action_close_no_write',
722
+ title: 'Close Human Action Without Enforcement',
723
+ description: 'Close one Requires Human IP row with an auditable no-write decision report. Use for ambiguous, skipped, rule-tuning-needed, new-rule-needed, rate-limited, or already-handled rows.',
724
+ scope: 'notifications:write',
725
+ readOnly: false,
726
+ confirm: true,
727
+ method: 'POST',
728
+ endpoint: '/notifications/:notificationId/ips/:ip/close-no-write',
729
+ pathParams: ['notificationId', 'ip'],
730
+ fixedBody: { reportSource: 'mcp' },
731
+ bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
732
+ inputSchema: objectSchema({
733
+ notificationId: string('Notification id from the human action row.'),
734
+ ip: string('IP address for the row to close.'),
735
+ ...decisionReportInput,
736
+ ...confirmSchema,
737
+ }, ['notificationId', 'ip', 'confirm', 'reason']),
738
+ },
739
+ {
740
+ name: 'securenow_human_action_false_positive',
741
+ title: 'Mark Human Action False Positive',
742
+ description: 'Mark an AI-prepared human action as a scoped false positive and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
743
+ scope: 'notifications:write',
744
+ readOnly: false,
745
+ confirm: true,
746
+ method: 'POST',
747
+ endpoint: '/notifications/:notificationId/ips/:ip/false-positive',
748
+ pathParams: ['notificationId', 'ip'],
749
+ fixedBody: { reportSource: 'mcp' },
750
+ bodyFields: ['reason', 'conditions', 'matchMode', 'createExclusion', 'applyToExisting', 'ruleScope', 'targetRuleIds', 'aiConfidence', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
751
+ inputSchema: objectSchema({
752
+ notificationId: string('Notification id from the human action row.'),
753
+ ip: string('IP address to mark as false positive.'),
754
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Restrictive false-positive conditions. Prefer app/rule/path/method/status/user-agent/body scope.' },
755
+ matchMode: string('Condition match mode: all or any.'),
756
+ createExclusion: boolean('Whether to create a restrictive exclusion rule.'),
757
+ applyToExisting: boolean('Whether to apply to existing matching IP investigations.'),
758
+ ruleScope: string('Scope: this_rule, specific_rules, all_existing, or any_rule.'),
759
+ targetRuleIds: arrayOfStrings('Specific rule ids when ruleScope is specific_rules.'),
760
+ aiConfidence: number('AI confidence for audit metadata.', { minimum: 0, maximum: 100 }),
761
+ ...decisionReportInput,
762
+ ...confirmSchema,
763
+ }, ['notificationId', 'ip', 'confirm', 'reason']),
764
+ },
765
+ {
766
+ name: 'securenow_human_case_action_update',
767
+ title: 'Update Case-Level Human Action',
768
+ description: 'Approve, reject, execute, or fail a case-level proposed action such as tune_rule or create_exclusion. Write action; requires confirmation.',
769
+ scope: 'notifications:write',
770
+ readOnly: false,
771
+ confirm: true,
772
+ method: 'PUT',
773
+ endpoint: '/notifications/:notificationId/agent-case/actions/:actionKey',
774
+ pathParams: ['notificationId', 'actionKey'],
775
+ fixedBody: { reportSource: 'mcp' },
776
+ bodyFields: ['status', 'result', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
777
+ reasonInResult: true,
778
+ inputSchema: objectSchema({
779
+ notificationId: string('Notification id from the case-action row.'),
780
+ actionKey: string('Proposed action key from the row, for example tune_rule:...'),
781
+ status: string('New status: proposed, approved, rejected, executed, or failed.'),
782
+ result: { type: 'object', additionalProperties: true, description: 'Optional structured result/audit details.' },
783
+ ...decisionReportInput,
784
+ ...confirmSchema,
785
+ }, ['notificationId', 'actionKey', 'status', 'confirm', 'reason']),
786
+ },
787
+ {
788
+ name: 'securenow_human_case_decision_report_add',
789
+ title: 'Record Case Decision Report',
790
+ description: 'Attach a structured analyst/MCP decision report to a notification/case without changing IP status or proposed action state. Write action; requires confirmation.',
791
+ scope: 'notifications:write',
792
+ readOnly: false,
793
+ confirm: true,
794
+ method: 'POST',
795
+ endpoint: '/notifications/:notificationId/decision-report',
796
+ pathParams: ['notificationId'],
797
+ fixedBody: { reportSource: 'mcp' },
798
+ bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
799
+ inputSchema: objectSchema({
800
+ notificationId: string('Notification id from the case-action row.'),
801
+ ...decisionReportInput,
802
+ ...confirmSchema,
803
+ }, ['notificationId', 'confirm', 'reason']),
804
+ },
805
+ {
806
+ name: 'securenow_ip_lookup',
807
+ title: 'IP Intelligence Lookup',
808
+ description: 'Look up SecureNow IP intelligence for an IP address.',
809
+ scope: 'ip_intel:read',
810
+ readOnly: true,
811
+ method: 'GET',
812
+ endpoint: '/ip/:ip',
813
+ pathParams: ['ip'],
814
+ inputSchema: objectSchema({
815
+ ip: string('IPv4 address.'),
816
+ }, ['ip']),
817
+ },
818
+ {
819
+ name: 'securenow_ip_traces',
820
+ title: 'IP Traces',
821
+ description: 'Fetch traces associated with an IP address.',
822
+ scope: 'ip_intel:read',
823
+ readOnly: true,
824
+ method: 'GET',
825
+ endpoint: '/ip/:ip/traces',
826
+ pathParams: ['ip'],
827
+ queryFields: ['appKeys', 'environment'],
828
+ normalize: { appKeys: normalizeAppKeys },
829
+ inputSchema: objectSchema({
830
+ ip: string('IPv4 address.'),
831
+ ...appKeysInput,
832
+ ...environmentInput,
833
+ }, ['ip']),
834
+ },
835
+ {
836
+ name: 'securenow_forensics_query',
837
+ title: 'Run Forensics Query',
838
+ description: 'Run a natural-language forensic query. Write scope because it creates an async query job.',
839
+ scope: 'forensics:write',
840
+ readOnly: false,
841
+ confirm: true,
842
+ method: 'POST',
843
+ endpoint: '/forensics/query',
844
+ bodyFields: ['query', 'applicationId', 'instanceId', 'environment'],
845
+ inputSchema: objectSchema({
846
+ query: string('Natural-language forensic query.'),
847
+ applicationId: string('Optional application database id.'),
848
+ instanceId: string('Optional ClickHouse instance id.'),
849
+ ...environmentInput,
850
+ ...confirmSchema,
851
+ }, ['query', 'confirm', 'reason']),
852
+ },
853
+ {
854
+ name: 'securenow_forensics_library',
855
+ title: 'Forensics Query Library',
856
+ description: 'List saved forensics queries.',
857
+ scope: 'forensics:read',
858
+ readOnly: true,
859
+ method: 'GET',
860
+ endpoint: '/forensics/query-library',
861
+ inputSchema: objectSchema({}),
862
+ },
863
+ {
864
+ name: 'securenow_analytics_summary',
865
+ title: 'Analytics Summary',
866
+ description: 'Fetch response analytics summary.',
867
+ scope: 'analytics:read',
868
+ readOnly: true,
869
+ method: 'GET',
870
+ endpoint: '/analytics/summary',
871
+ queryFields: ['instanceId'],
872
+ inputSchema: objectSchema({
873
+ instanceId: string('Optional ClickHouse instance id.'),
874
+ }),
875
+ },
876
+ {
877
+ name: 'securenow_automation_rules_list',
878
+ title: 'List Automation Rules',
879
+ description: 'List blocklist automation rules with app/environment scope and stats.',
880
+ scope: 'automation:read',
881
+ readOnly: true,
882
+ method: 'GET',
883
+ endpoint: '/automation-rules',
884
+ inputSchema: objectSchema({}),
885
+ },
886
+ {
887
+ name: 'securenow_automation_defaults_ensure',
888
+ title: 'Ensure Default Automation Rules',
889
+ description: 'Create or refresh SecureNow default risk-score automation rules for the current account. Write action; requires confirmation.',
890
+ scope: 'automation:write',
891
+ readOnly: false,
892
+ confirm: true,
893
+ method: 'POST',
894
+ endpoint: '/automation-rules/defaults/ensure',
895
+ bodyFields: ['forceEnableExisting'],
896
+ inputSchema: objectSchema({
897
+ forceEnableExisting: boolean('Re-enable existing SecureNow default rules if the customer disabled them. Defaults to false.'),
898
+ ...confirmSchema,
899
+ }, ['confirm', 'reason']),
900
+ },
901
+ {
902
+ name: 'securenow_automation_rule_get',
903
+ title: 'Get Automation Rule',
904
+ description: 'Fetch one automation rule.',
905
+ scope: 'automation:read',
906
+ readOnly: true,
907
+ method: 'GET',
908
+ endpoint: '/automation-rules/:id',
909
+ pathParams: ['id'],
910
+ inputSchema: objectSchema({
911
+ id: string('Automation rule id.'),
912
+ }, ['id']),
913
+ },
914
+ {
915
+ name: 'securenow_automation_rule_create',
916
+ title: 'Create Automation Rule',
917
+ description: 'Create a blocklist automation rule. Write action; requires confirmation.',
918
+ scope: 'automation:write',
919
+ readOnly: false,
920
+ confirm: true,
921
+ method: 'POST',
922
+ endpoint: '/automation-rules',
923
+ bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'status', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
924
+ inputSchema: objectSchema({
925
+ name: string('Rule name.'),
926
+ description: string('Optional rule description.'),
927
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array. Fields include abuseConfidenceScore, riskScore, alertName, alertTag, attackType, path, environment.' },
928
+ conditionLogic: string('AND or OR.'),
929
+ actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array, for example [{ "type":"addToBlocklist", "config":{ "reason":"...", "ttlHours":24 }}].' },
930
+ status: string('Initial rule status: active or disabled. Defaults to active.'),
931
+ applicationsAll: boolean('Apply to all applications.'),
932
+ applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
933
+ environmentsAll: boolean('Apply to all environments.'),
934
+ environments: arrayOfStrings('Deployment environments when not applying to all environments.'),
935
+ ...confirmSchema,
936
+ }, ['name', 'conditions', 'actions', 'confirm', 'reason']),
937
+ },
938
+ {
939
+ name: 'securenow_automation_rule_update',
940
+ title: 'Update Automation Rule',
941
+ description: 'Update a blocklist automation rule. Write action; requires confirmation.',
942
+ scope: 'automation:write',
943
+ readOnly: false,
944
+ confirm: true,
945
+ method: 'PUT',
946
+ endpoint: '/automation-rules/:id',
947
+ pathParams: ['id'],
948
+ bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'status', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
949
+ inputSchema: objectSchema({
950
+ id: string('Automation rule id.'),
951
+ name: string('Rule name.'),
952
+ description: string('Optional rule description.'),
953
+ status: string('active or disabled.'),
954
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array.' },
955
+ conditionLogic: string('AND or OR.'),
956
+ actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array.' },
957
+ applicationsAll: boolean('Apply to all applications.'),
958
+ applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
959
+ environmentsAll: boolean('Apply to all environments.'),
960
+ environments: arrayOfStrings('Deployment environments when not applying to all environments.'),
961
+ ...confirmSchema,
962
+ }, ['id', 'confirm', 'reason']),
963
+ },
964
+ {
965
+ name: 'securenow_automation_rule_dry_run',
966
+ title: 'Dry-Run Automation Rule',
967
+ description: 'Preview automation matches without writing blocklist entries.',
968
+ scope: 'automation:read',
969
+ readOnly: true,
970
+ method: 'POST',
971
+ endpoint: '/automation-rules/:id/dry-run',
972
+ pathParams: ['id'],
973
+ bodyFields: ['limit', 'sampleLimit'],
974
+ inputSchema: objectSchema({
975
+ id: string('Automation rule id.'),
976
+ limit: number('Maximum notifications to scan.', { minimum: 1, maximum: 2000 }),
977
+ sampleLimit: number('Maximum sample matches to return.', { minimum: 1, maximum: 50 }),
978
+ }, ['id']),
979
+ },
980
+ {
981
+ name: 'securenow_automation_rule_execute',
982
+ title: 'Execute Automation Rule',
983
+ description: 'Execute an automation rule and add matching IPs to the blocklist. Write action; requires confirmation.',
984
+ scope: 'automation:write',
985
+ readOnly: false,
986
+ destructive: true,
987
+ confirm: true,
988
+ method: 'POST',
989
+ endpoint: '/automation-rules/:id/execute',
990
+ pathParams: ['id'],
991
+ inputSchema: objectSchema({
992
+ id: string('Automation rule id.'),
993
+ ...confirmSchema,
994
+ }, ['id', 'confirm', 'reason']),
995
+ },
996
+ {
997
+ name: 'securenow_automation_rule_delete',
998
+ title: 'Delete Automation Rule',
999
+ description: 'Delete an automation rule. Write action; requires confirmation.',
1000
+ scope: 'automation:write',
1001
+ readOnly: false,
1002
+ destructive: true,
1003
+ confirm: true,
1004
+ method: 'DELETE',
1005
+ endpoint: '/automation-rules/:id',
1006
+ pathParams: ['id'],
1007
+ inputSchema: objectSchema({
1008
+ id: string('Automation rule id.'),
1009
+ ...confirmSchema,
1010
+ }, ['id', 'confirm', 'reason']),
1011
+ },
1012
+ {
1013
+ name: 'securenow_rate_limit_list',
1014
+ title: 'List Rate Limits',
1015
+ description: 'List rate-limit remediation rules with app/environment scope.',
1016
+ scope: 'rate_limits:read',
1017
+ readOnly: true,
1018
+ method: 'GET',
1019
+ endpoint: '/rate-limits',
1020
+ queryFields: ['status', 'search', 'page', 'limit', 'appKey', 'environment'],
1021
+ inputSchema: objectSchema({
1022
+ status: string('Rule status: active, disabled, removed, or all. Defaults to active.'),
1023
+ search: string('Optional IP/path/name search.'),
1024
+ ...pagingInput,
1025
+ appKey: string('Optional application key scope.'),
1026
+ ...environmentInput,
1027
+ }),
1028
+ },
1029
+ {
1030
+ name: 'securenow_rate_limit_get',
1031
+ title: 'Get Rate Limit',
1032
+ description: 'Fetch one rate-limit remediation rule.',
1033
+ scope: 'rate_limits:read',
1034
+ readOnly: true,
1035
+ method: 'GET',
1036
+ endpoint: '/rate-limits/:id',
1037
+ pathParams: ['id'],
1038
+ inputSchema: objectSchema({
1039
+ id: string('Rate-limit rule id.'),
1040
+ }, ['id']),
1041
+ },
1042
+ {
1043
+ name: 'securenow_rate_limit_test',
1044
+ title: 'Test Rate Limit Match',
1045
+ description: 'Check whether an IP/path/method would match an active rate-limit rule.',
1046
+ scope: 'rate_limits:read',
1047
+ readOnly: true,
1048
+ method: 'GET',
1049
+ endpoint: '/rate-limits/check',
1050
+ queryFields: ['ip', 'path', 'method', 'appKey', 'environment'],
1051
+ inputSchema: objectSchema({
1052
+ ip: string('IPv4 address to test.'),
1053
+ path: string('Request path to test. Defaults to /.'),
1054
+ method: string('HTTP method to test. Defaults to GET.'),
1055
+ appKey: string('Optional application key scope.'),
1056
+ ...environmentInput,
1057
+ }, ['ip']),
1058
+ },
1059
+ {
1060
+ name: 'securenow_rate_limit_remove',
1061
+ title: 'Remove Rate Limit',
1062
+ description: 'Remove a rate-limit remediation rule. Write action; requires confirmation.',
1063
+ scope: 'rate_limits:write',
1064
+ readOnly: false,
1065
+ confirm: true,
1066
+ method: 'DELETE',
1067
+ endpoint: '/rate-limits/:id',
1068
+ pathParams: ['id'],
1069
+ bodyFields: ['reason'],
1070
+ inputSchema: objectSchema({
1071
+ id: string('Rate-limit rule id.'),
1072
+ ...confirmSchema,
1073
+ }, ['id', 'confirm', 'reason']),
1074
+ },
1075
+ {
1076
+ name: 'securenow_rate_limit_parse',
1077
+ title: 'Parse Rate Limit Text',
1078
+ description: 'Use SecureNow AI to convert natural language into a rate-limit remediation draft without creating it.',
1079
+ scope: 'rate_limits:read',
1080
+ readOnly: true,
1081
+ method: 'POST',
1082
+ endpoint: '/rate-limits/parse',
1083
+ bodyFields: ['text', 'draft'],
1084
+ inputSchema: objectSchema({
1085
+ text: string('Natural-language rate-limit request, e.g. "rate limit /api/login to 2 attempts per minute for this IP".'),
1086
+ draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields to preserve or resolve phrases such as "this IP".' },
1087
+ }, ['text']),
1088
+ },
1089
+ {
1090
+ name: 'securenow_rate_limit_create_from_text',
1091
+ title: 'Create Rate Limit From Text',
1092
+ description: 'Create a rate-limit remediation rule from natural language. Write action; requires confirmation.',
1093
+ scope: 'rate_limits:write',
1094
+ readOnly: false,
1095
+ confirm: true,
1096
+ method: 'POST',
1097
+ endpoint: '/rate-limits/from-text',
1098
+ bodyFields: ['text', 'draft', 'reason'],
1099
+ inputSchema: objectSchema({
1100
+ text: string('Natural-language rate-limit request.'),
1101
+ draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields or scope hints.' },
1102
+ ...confirmSchema,
1103
+ }, ['text', 'confirm', 'reason']),
1104
+ },
1105
+ {
1106
+ name: 'securenow_alert_rules_list',
1107
+ title: 'List Alert Rules',
1108
+ description: 'List alert rules, including system rules and app scope.',
1109
+ scope: 'alerts:read',
1110
+ readOnly: true,
1111
+ method: 'GET',
1112
+ endpoint: '/alert-rules',
1113
+ inputSchema: objectSchema({}),
1114
+ },
1115
+ {
1116
+ name: 'securenow_alert_rule_get',
1117
+ title: 'Get Alert Rule',
1118
+ description: 'Fetch one alert rule.',
1119
+ scope: 'alerts:read',
1120
+ readOnly: true,
1121
+ method: 'GET',
1122
+ endpoint: '/alert-rules/:id',
1123
+ pathParams: ['id'],
1124
+ inputSchema: objectSchema({
1125
+ id: string('Alert rule id.'),
1126
+ }, ['id']),
1127
+ },
1128
+ {
1129
+ name: 'securenow_alert_rule_versions',
1130
+ title: 'List Alert Rule Versions',
1131
+ description: 'List a rule\'s configuration version history (newest first). Each material update (SQL/instant/settings), duplicate, and rollback is a version. Lazily seeds a v1 baseline for rules created before versioning.',
1132
+ scope: 'alerts:read',
1133
+ readOnly: true,
1134
+ method: 'GET',
1135
+ endpoint: '/alert-rules/:id/versions',
1136
+ pathParams: ['id'],
1137
+ queryFields: ['limit', 'page'],
1138
+ inputSchema: objectSchema({
1139
+ id: string('Alert rule id.'),
1140
+ ...pagingInput,
1141
+ }, ['id']),
1142
+ },
1143
+ {
1144
+ name: 'securenow_alert_rule_version_get',
1145
+ title: 'Get Alert Rule Version',
1146
+ description: 'Fetch one version\'s full material snapshot (name/severity/mode/schedule/throttle/instant/exclusions and the resolved detection SQL) for diffing before a rollback.',
1147
+ scope: 'alerts:read',
1148
+ readOnly: true,
1149
+ method: 'GET',
1150
+ endpoint: '/alert-rules/:id/versions/:version',
1151
+ pathParams: ['id', 'version'],
1152
+ inputSchema: objectSchema({
1153
+ id: string('Alert rule id.'),
1154
+ version: number('Version number to fetch.', { minimum: 1 }),
1155
+ }, ['id', 'version']),
1156
+ },
1157
+ {
1158
+ name: 'securenow_alert_rule_rollback',
1159
+ title: 'Roll Back Alert Rule',
1160
+ description: 'Restore a prior version\'s full configuration onto a custom alert rule (re-validates the detection SQL first) and record it as a new version. Append-only history — you can roll back a rollback. System (read-only template) rules cannot be rolled back. Write action; requires confirmation.',
1161
+ scope: 'alerts:write',
1162
+ readOnly: false,
1163
+ confirm: true,
1164
+ method: 'POST',
1165
+ endpoint: '/alert-rules/:id/rollback',
1166
+ pathParams: ['id'],
1167
+ bodyFields: ['version', 'reason'],
1168
+ inputSchema: objectSchema({
1169
+ id: string('Alert rule id (must be a custom, non-system rule).'),
1170
+ version: number('Version number to restore. List options with securenow_alert_rule_versions.', { minimum: 1 }),
1171
+ ...confirmSchema,
1172
+ }, ['id', 'version', 'confirm', 'reason']),
1173
+ },
1174
+ {
1175
+ name: 'securenow_alert_rule_update',
1176
+ title: 'Update Alert Rule',
1177
+ description: 'Update alert rule scope/status/schedule/throttle or record a review action. Write action; requires confirmation.',
1178
+ scope: 'alerts:write',
1179
+ readOnly: false,
1180
+ confirm: true,
1181
+ method: 'PUT',
1182
+ endpoint: '/alert-rules/:id',
1183
+ pathParams: ['id'],
1184
+ bodyFields: ['name', 'description', 'status', 'applicationsAll', 'applications', 'schedule', 'throttle', 'alertChannelIds', 'reviewAction', 'reviewNote', 'reviewNotificationId'],
1185
+ inputSchema: objectSchema({
1186
+ id: string('Alert rule id.'),
1187
+ name: string('Rule name for custom rules.'),
1188
+ description: string('Rule description for custom rules.'),
1189
+ status: string('Active, Disabled, or Paused.'),
1190
+ applicationsAll: boolean('Scope rule to all applications.'),
1191
+ applications: arrayOfStrings('Application keys when applicationsAll is false.'),
1192
+ schedule: { type: 'object', additionalProperties: true, description: 'Schedule patch.' },
1193
+ throttle: { type: 'object', additionalProperties: true, description: 'Throttle patch.' },
1194
+ alertChannelIds: arrayOfStrings('Alert channel ids.'),
1195
+ reviewAction: string('Rule review action, e.g. keep_active or saved_new_version.'),
1196
+ reviewNote: string('Review note.'),
1197
+ reviewNotificationId: string('Notification id tied to this review.'),
1198
+ ...confirmSchema,
1199
+ }, ['id', 'confirm', 'reason']),
1200
+ },
1201
+ {
1202
+ name: 'securenow_alert_rule_query_update',
1203
+ title: 'Update System Alert Rule Query',
1204
+ description: 'Admin-only: update the shared public query mapping behind a system alert rule for all customer copies. Write action; requires confirmation.',
1205
+ scope: 'alerts:write',
1206
+ readOnly: false,
1207
+ confirm: true,
1208
+ method: 'PUT',
1209
+ endpoint: '/alert-rules/:id/query-mapping',
1210
+ pathParams: ['id'],
1211
+ bodyFields: ['sqlQuery', 'reason', 'expectedCurrentSqlHash', 'applyGlobally', 'reactivatePausedCopies', 'reviewNotificationId', 'reviewNote', 'source'],
1212
+ fixedBody: { applyGlobally: true, source: 'mcp' },
1213
+ inputSchema: objectSchema({
1214
+ id: string('Alert rule id. Must be a system rule.'),
1215
+ sqlQuery: string('Full replacement SQL query. System SQL must keep __USER_APP_KEYS__ for tenant scoping.'),
1216
+ expectedCurrentSqlHash: string('Optional SHA-256 hash of the current SQL to prevent stale overwrites.'),
1217
+ applyGlobally: boolean('Must be true. Updates the shared public query mapping used by all system rule copies.'),
1218
+ reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the query is tuned.'),
1219
+ reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1220
+ reviewNote: string('Optional review note. Defaults to reason.'),
1221
+ ...confirmSchema,
1222
+ }, ['id', 'sqlQuery', 'confirm', 'reason']),
1223
+ },
1224
+ {
1225
+ name: 'securenow_alert_rule_instant_update',
1226
+ title: 'Update Instant Alert Rule',
1227
+ description: 'Patch instant rule conditions/config with version/hash guards and before/after seeded tests. System rules require applyGlobally:true and admin permission. Write action; requires confirmation.',
1228
+ scope: 'alerts:write',
1229
+ readOnly: false,
1230
+ confirm: true,
1231
+ method: 'PUT',
1232
+ endpoint: '/alert-rules/:id/instant',
1233
+ pathParams: ['id'],
1234
+ bodyFields: [
1235
+ 'operations',
1236
+ 'conditions',
1237
+ 'enabled',
1238
+ 'source',
1239
+ 'matchMode',
1240
+ 'executionMode',
1241
+ 'severity',
1242
+ 'throttle',
1243
+ 'expectedRuleVersion',
1244
+ 'expectedCurrentInstantHash',
1245
+ 'expectedCurrentRuleHash',
1246
+ 'applyGlobally',
1247
+ 'dryRun',
1248
+ 'reactivatePausedCopies',
1249
+ 'fixtures',
1250
+ 'reviewNotificationId',
1251
+ 'reviewNote',
1252
+ 'reason',
1253
+ ],
1254
+ fixedBody: { auditSource: 'mcp' },
1255
+ inputSchema: objectSchema({
1256
+ id: string('Alert rule id. Fetch with securenow_alert_rule_get first to get ruleVersion and instantHash.'),
1257
+ operations: {
1258
+ type: 'array',
1259
+ items: { type: 'object', additionalProperties: true },
1260
+ description: 'Condition patch operations: add_condition, remove_condition, update_condition. Use zero-based index/conditionIndex for remove/update.',
1261
+ },
1262
+ conditions: {
1263
+ type: 'array',
1264
+ items: { type: 'object', additionalProperties: true },
1265
+ description: 'Optional full replacement instant conditions array. Use operations for smaller patches.',
1266
+ },
1267
+ enabled: boolean('Whether instant detection is enabled.'),
1268
+ source: string('Instant event source. Currently trace.'),
1269
+ matchMode: string('Condition match mode: all or any.'),
1270
+ executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1271
+ severity: string('Optional severity override: critical, high, medium, or low.'),
1272
+ throttle: { type: 'object', additionalProperties: true, description: 'Throttle patch, for example { enabled:true, minutes:15 }.' },
1273
+ expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_get.', { minimum: 1 }),
1274
+ expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_get.'),
1275
+ expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_get.'),
1276
+ applyGlobally: boolean('Required true for system rules. Updates every customer copy of the system rule.'),
1277
+ dryRun: boolean('When true, validates and runs before/after seeded tests without saving.'),
1278
+ reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the instant rule is tuned.'),
1279
+ fixtures: {
1280
+ type: 'array',
1281
+ items: { type: 'object', additionalProperties: true },
1282
+ description: 'Optional seeded fixtures. Each may include kind attack/benign, label, path, method, requestBody, requestHeaders, and expectMatch.',
1283
+ },
1284
+ reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1285
+ reviewNote: string('Optional review note. Defaults to reason.'),
1286
+ ...confirmSchema,
1287
+ }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1288
+ },
1289
+ {
1290
+ name: 'securenow_alert_rule_conditions_get',
1291
+ title: 'Get Alert Rule Conditions',
1292
+ description: 'Fetch instant rule conditions with rule version and hash guards for safe condition-level tuning.',
1293
+ scope: 'alerts:read',
1294
+ readOnly: true,
1295
+ method: 'GET',
1296
+ endpoint: '/alert-rules/:id/conditions',
1297
+ pathParams: ['id'],
1298
+ inputSchema: objectSchema({
1299
+ id: string('Alert rule id.'),
1300
+ }, ['id']),
1301
+ },
1302
+ {
1303
+ name: 'securenow_alert_rule_condition_update',
1304
+ title: 'Update Alert Rule Condition',
1305
+ description: 'Patch instant alert-rule conditions with version/hash guards and seeded before/after tests. Compatibility alias for condition-level tuning.',
1306
+ scope: 'alerts:write',
1307
+ readOnly: false,
1308
+ confirm: true,
1309
+ method: 'PUT',
1310
+ endpoint: '/alert-rules/:id/instant',
1311
+ pathParams: ['id'],
1312
+ bodyFields: [
1313
+ 'confirm',
1314
+ 'operations',
1315
+ 'conditions',
1316
+ 'enabled',
1317
+ 'source',
1318
+ 'matchMode',
1319
+ 'executionMode',
1320
+ 'expectedRuleVersion',
1321
+ 'expectedCurrentInstantHash',
1322
+ 'expectedCurrentRuleHash',
1323
+ 'applyGlobally',
1324
+ 'reactivatePausedCopies',
1325
+ 'fixtures',
1326
+ 'reviewNotificationId',
1327
+ 'reviewNote',
1328
+ 'reason',
1329
+ ],
1330
+ fixedBody: { auditSource: 'mcp' },
1331
+ inputSchema: objectSchema({
1332
+ id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1333
+ operations: {
1334
+ type: 'array',
1335
+ items: { type: 'object', additionalProperties: true },
1336
+ description: 'Condition patch operations: add_condition, remove_condition, update_condition. Use zero-based index/conditionIndex for remove/update.',
1337
+ },
1338
+ conditions: {
1339
+ type: 'array',
1340
+ items: { type: 'object', additionalProperties: true },
1341
+ description: 'Optional full replacement instant conditions array. Use operations for smaller patches.',
1342
+ },
1343
+ enabled: boolean('Whether instant detection is enabled.'),
1344
+ source: string('Instant event source. Currently trace.'),
1345
+ matchMode: string('Condition match mode: all or any.'),
1346
+ executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1347
+ expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_conditions_get.', { minimum: 1 }),
1348
+ expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_conditions_get.'),
1349
+ expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_conditions_get.'),
1350
+ applyGlobally: boolean('Required true for system rules. Updates every customer copy of the system rule.'),
1351
+ reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the condition is tuned.'),
1352
+ fixtures: {
1353
+ type: 'array',
1354
+ items: { type: 'object', additionalProperties: true },
1355
+ description: 'Optional seeded fixtures. Each may include kind attack/benign, label, path, method, requestBody, requestHeaders, and expectMatch.',
1356
+ },
1357
+ reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1358
+ reviewNote: string('Optional review note. Defaults to reason.'),
1359
+ ...confirmSchema,
1360
+ }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1361
+ },
1362
+ {
1363
+ name: 'securenow_alert_rule_condition_candidate_test',
1364
+ title: 'Test Alert Rule Condition Candidate',
1365
+ description: 'Dry-run a condition-level instant-rule patch without saving it, including seeded benign/attack fixture checks.',
1366
+ scope: 'alerts:write',
1367
+ readOnly: false,
1368
+ confirm: true,
1369
+ method: 'PUT',
1370
+ endpoint: '/alert-rules/:id/instant',
1371
+ pathParams: ['id'],
1372
+ bodyFields: [
1373
+ 'confirm',
1374
+ 'operations',
1375
+ 'conditions',
1376
+ 'enabled',
1377
+ 'source',
1378
+ 'matchMode',
1379
+ 'executionMode',
1380
+ 'expectedRuleVersion',
1381
+ 'expectedCurrentInstantHash',
1382
+ 'expectedCurrentRuleHash',
1383
+ 'applyGlobally',
1384
+ 'fixtures',
1385
+ 'reviewNotificationId',
1386
+ 'reviewNote',
1387
+ 'reason',
1388
+ ],
1389
+ fixedBody: { auditSource: 'mcp', dryRun: true },
1390
+ inputSchema: objectSchema({
1391
+ id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1392
+ operations: {
1393
+ type: 'array',
1394
+ items: { type: 'object', additionalProperties: true },
1395
+ description: 'Candidate condition operations to test.',
1396
+ },
1397
+ conditions: {
1398
+ type: 'array',
1399
+ items: { type: 'object', additionalProperties: true },
1400
+ description: 'Optional full replacement instant conditions array to test.',
1401
+ },
1402
+ enabled: boolean('Whether instant detection would be enabled.'),
1403
+ source: string('Instant event source. Currently trace.'),
1404
+ matchMode: string('Condition match mode: all or any.'),
1405
+ executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1406
+ expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_conditions_get.', { minimum: 1 }),
1407
+ expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_conditions_get.'),
1408
+ expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_conditions_get.'),
1409
+ applyGlobally: boolean('Required true for system rules, even for admin dry-runs.'),
1410
+ fixtures: {
1411
+ type: 'array',
1412
+ items: { type: 'object', additionalProperties: true },
1413
+ description: 'Optional seeded fixtures. Each may include kind attack/benign, label, path, method, requestBody, requestHeaders, and expectMatch.',
1414
+ },
1415
+ reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
1416
+ reviewNote: string('Optional review note. Defaults to reason.'),
1417
+ ...confirmSchema,
1418
+ }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1419
+ },
1420
+ {
1421
+ name: 'securenow_alert_rule_condition_diff',
1422
+ title: 'Diff Alert Rule Condition Candidate',
1423
+ description: 'Return the current instant conditions, candidate conditions, hashes, and before/after fixture behavior without saving.',
1424
+ scope: 'alerts:write',
1425
+ readOnly: false,
1426
+ confirm: true,
1427
+ method: 'PUT',
1428
+ endpoint: '/alert-rules/:id/instant',
1429
+ pathParams: ['id'],
1430
+ bodyFields: [
1431
+ 'confirm',
1432
+ 'operations',
1433
+ 'conditions',
1434
+ 'enabled',
1435
+ 'source',
1436
+ 'matchMode',
1437
+ 'executionMode',
1438
+ 'expectedRuleVersion',
1439
+ 'expectedCurrentInstantHash',
1440
+ 'expectedCurrentRuleHash',
1441
+ 'applyGlobally',
1442
+ 'fixtures',
1443
+ 'reason',
1444
+ ],
1445
+ fixedBody: { auditSource: 'mcp', dryRun: true },
1446
+ inputSchema: objectSchema({
1447
+ id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1448
+ operations: {
1449
+ type: 'array',
1450
+ items: { type: 'object', additionalProperties: true },
1451
+ description: 'Candidate condition operations to diff.',
1452
+ },
1453
+ conditions: {
1454
+ type: 'array',
1455
+ items: { type: 'object', additionalProperties: true },
1456
+ description: 'Optional full replacement instant conditions array to diff.',
1457
+ },
1458
+ enabled: boolean('Whether instant detection would be enabled.'),
1459
+ source: string('Instant event source. Currently trace.'),
1460
+ matchMode: string('Condition match mode: all or any.'),
1461
+ executionMode: string('Rule execution mode: instant or hybrid when instant detection is enabled.'),
1462
+ expectedRuleVersion: number('Required current reviewState.reviewVersion from securenow_alert_rule_conditions_get.', { minimum: 1 }),
1463
+ expectedCurrentInstantHash: string('Required SHA-256 hash of the current instant config from securenow_alert_rule_conditions_get.'),
1464
+ expectedCurrentRuleHash: string('Optional SHA-256 hash of the current instant/throttle/severity surface from securenow_alert_rule_conditions_get.'),
1465
+ applyGlobally: boolean('Required true for system rules, even for admin dry-runs.'),
1466
+ fixtures: {
1467
+ type: 'array',
1468
+ items: { type: 'object', additionalProperties: true },
1469
+ description: 'Optional seeded fixtures used to explain benign removals and attack coverage.',
1470
+ },
1471
+ ...confirmSchema,
1472
+ }, ['id', 'expectedRuleVersion', 'expectedCurrentInstantHash', 'confirm', 'reason']),
1473
+ },
1474
+ {
1475
+ name: 'securenow_alert_rule_test',
1476
+ title: 'Test Alert Rule',
1477
+ description: 'Start a live or dry-run alert rule test.',
1478
+ scope: 'alerts:write',
1479
+ readOnly: false,
1480
+ confirm: true,
1481
+ method: 'POST',
1482
+ endpoint: '/alert-rules/:id/test',
1483
+ pathParams: ['id'],
1484
+ bodyFields: ['applicationKey', 'mode'],
1485
+ inputSchema: objectSchema({
1486
+ id: string('Alert rule id.'),
1487
+ applicationKey: string('Application key to test.'),
1488
+ mode: string('dry_run or live.'),
1489
+ ...confirmSchema,
1490
+ }, ['id', 'confirm', 'reason']),
1491
+ },
1492
+ {
1493
+ name: 'securenow_alert_rule_candidate_test',
1494
+ title: 'Dry-Run Candidate Alert Rule SQL',
1495
+ description: 'Dry-run a candidate SQL/query guard for an alert rule without saving it.',
1496
+ scope: 'alerts:write',
1497
+ readOnly: false,
1498
+ confirm: true,
1499
+ method: 'POST',
1500
+ endpoint: '/alert-rules/:id/test',
1501
+ pathParams: ['id'],
1502
+ bodyFields: ['applicationKey', 'candidateSqlQuery'],
1503
+ fixedBody: { mode: 'dry_run' },
1504
+ inputSchema: objectSchema({
1505
+ id: string('Alert rule id.'),
1506
+ applicationKey: string('Application key to test.'),
1507
+ candidateSqlQuery: string('Full candidate SQL query to dry-run without saving.'),
1508
+ ...confirmSchema,
1509
+ }, ['id', 'candidateSqlQuery', 'confirm', 'reason']),
1510
+ },
1511
+ {
1512
+ name: 'securenow_alert_rule_test_result',
1513
+ title: 'Get Alert Rule Test Result',
1514
+ description: 'Poll alert rule test status and results.',
1515
+ scope: 'alerts:read',
1516
+ readOnly: true,
1517
+ method: 'GET',
1518
+ endpoint: '/alert-rules/:id/test/:testId',
1519
+ pathParams: ['id', 'testId'],
1520
+ inputSchema: objectSchema({
1521
+ id: string('Alert rule id.'),
1522
+ testId: string('Alert rule test id.'),
1523
+ }, ['id', 'testId']),
1524
+ },
1525
+ {
1526
+ name: 'securenow_alert_rule_exclusions_list',
1527
+ title: 'List Alert Rule Exclusions',
1528
+ description: 'List exclusions embedded on one alert rule.',
1529
+ scope: 'alerts:read',
1530
+ readOnly: true,
1531
+ method: 'GET',
1532
+ endpoint: '/alert-rules/:id/exclusions',
1533
+ pathParams: ['id'],
1534
+ inputSchema: objectSchema({
1535
+ id: string('Alert rule id.'),
1536
+ }, ['id']),
1537
+ },
1538
+ {
1539
+ name: 'securenow_alert_rule_exclusion_add',
1540
+ title: 'Add Alert Rule Exclusion',
1541
+ description: 'Add a restrictive exclusion to one alert rule. Write action; requires confirmation.',
1542
+ scope: 'alerts:write',
1543
+ readOnly: false,
1544
+ confirm: true,
1545
+ method: 'POST',
1546
+ endpoint: '/alert-rules/:id/exclusions',
1547
+ pathParams: ['id'],
1548
+ bodyFields: ['conditions', 'matchMode', 'reason', 'pathPattern', 'isActive'],
1549
+ inputSchema: objectSchema({
1550
+ id: string('Alert rule id.'),
1551
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Restrictive exclusion conditions.' },
1552
+ matchMode: string('all or any.'),
1553
+ reason: string('Exclusion reason.'),
1554
+ pathPattern: string('Optional path pattern.'),
1555
+ isActive: boolean('Whether the exclusion is active.'),
1556
+ ...confirmSchema,
1557
+ }, ['id', 'confirm', 'reason']),
1558
+ },
1559
+ {
1560
+ name: 'securenow_alert_rule_exclusion_remove',
1561
+ title: 'Remove Alert Rule Exclusion',
1562
+ description: 'Remove an alert rule exclusion. Write action; requires confirmation.',
1563
+ scope: 'alerts:write',
1564
+ readOnly: false,
1565
+ confirm: true,
1566
+ method: 'DELETE',
1567
+ endpoint: '/alert-rules/:id/exclusions/:exclusionId',
1568
+ pathParams: ['id', 'exclusionId'],
1569
+ inputSchema: objectSchema({
1570
+ id: string('Alert rule id.'),
1571
+ exclusionId: string('Exclusion id.'),
1572
+ ...confirmSchema,
1573
+ }, ['id', 'exclusionId', 'confirm', 'reason']),
1574
+ },
1575
+ {
1576
+ name: 'securenow_blocklist_list',
1577
+ title: 'List Blocklist',
1578
+ description: 'List blocked IPs.',
1579
+ scope: 'blocklist:read',
1580
+ readOnly: true,
1581
+ method: 'GET',
1582
+ endpoint: '/blocklist',
1583
+ queryFields: ['page', 'limit', 'status', 'approvalStatus', 'search', 'view', 'appKey', 'environment'],
1584
+ inputSchema: objectSchema({
1585
+ ...pagingInput,
1586
+ status: string('Block entry status: active or removed. Defaults to active.'),
1587
+ approvalStatus: string('Optional approval filter: pending, approved, or rejected.'),
1588
+ search: string('Optional IP prefix search.'),
1589
+ view: string('List view: all or operational.'),
1590
+ appKey: string('Optional application key scope.'),
1591
+ ...environmentInput,
1592
+ }),
1593
+ },
1594
+ {
1595
+ name: 'securenow_blocklist_pending_list',
1596
+ title: 'List Pending Block Approvals',
1597
+ description: 'List legacy or AI-prepared blocklist entries with approvalStatus=pending. Pending blocks are not enforced until approved.',
1598
+ scope: 'blocklist:read',
1599
+ readOnly: true,
1600
+ method: 'GET',
1601
+ endpoint: '/blocklist/pending',
1602
+ queryFields: ['page', 'limit', 'search', 'appKey', 'environment'],
1603
+ inputSchema: objectSchema({
1604
+ ...pagingInput,
1605
+ search: string('Optional IP prefix search.'),
1606
+ appKey: string('Optional application key scope.'),
1607
+ ...environmentInput,
1608
+ }),
1609
+ },
1610
+ {
1611
+ name: 'securenow_blocklist_pending_approve',
1612
+ title: 'Approve Pending Block',
1613
+ description: 'Approve one pending blocklist entry so the firewall enforces it. Write action; requires confirmation.',
1614
+ scope: 'blocklist:write',
1615
+ readOnly: false,
1616
+ destructive: true,
1617
+ confirm: true,
1618
+ method: 'POST',
1619
+ endpoint: '/blocklist/:id/approve',
1620
+ pathParams: ['id'],
1621
+ bodyFields: ['reason'],
1622
+ inputSchema: objectSchema({
1623
+ id: string('Pending blocklist entry id.'),
1624
+ ...confirmSchema,
1625
+ }, ['id', 'confirm', 'reason']),
1626
+ },
1627
+ {
1628
+ name: 'securenow_blocklist_pending_reject',
1629
+ title: 'Reject Pending Block',
1630
+ description: 'Reject one pending blocklist entry and remove it from enforcement consideration. Write action; requires confirmation.',
1631
+ scope: 'blocklist:write',
1632
+ readOnly: false,
1633
+ confirm: true,
1634
+ method: 'POST',
1635
+ endpoint: '/blocklist/:id/reject',
1636
+ pathParams: ['id'],
1637
+ bodyFields: ['reason'],
1638
+ inputSchema: objectSchema({
1639
+ id: string('Pending blocklist entry id.'),
1640
+ ...confirmSchema,
1641
+ }, ['id', 'confirm', 'reason']),
1642
+ },
1643
+ {
1644
+ name: 'securenow_blocklist_pending_bulk_approve',
1645
+ title: 'Bulk Approve Pending Blocks',
1646
+ description: 'Approve multiple pending blocklist entries after they have all been reviewed under the same safe policy. Write action; requires confirmation.',
1647
+ scope: 'blocklist:write',
1648
+ readOnly: false,
1649
+ destructive: true,
1650
+ confirm: true,
1651
+ method: 'POST',
1652
+ endpoint: '/blocklist/bulk-approve',
1653
+ bodyFields: ['ids', 'reason'],
1654
+ inputSchema: objectSchema({
1655
+ ids: arrayOfStrings('Pending blocklist entry ids to approve.'),
1656
+ ...confirmSchema,
1657
+ }, ['ids', 'confirm', 'reason']),
1658
+ },
1659
+ {
1660
+ name: 'securenow_blocklist_pending_bulk_reject',
1661
+ title: 'Bulk Reject Pending Blocks',
1662
+ description: 'Reject multiple pending blocklist entries after they have all been reviewed as false positives or stale/ambiguous. Write action; requires confirmation.',
1663
+ scope: 'blocklist:write',
1664
+ readOnly: false,
1665
+ confirm: true,
1666
+ method: 'POST',
1667
+ endpoint: '/blocklist/bulk-reject',
1668
+ bodyFields: ['ids', 'reason'],
1669
+ inputSchema: objectSchema({
1670
+ ids: arrayOfStrings('Pending blocklist entry ids to reject.'),
1671
+ ...confirmSchema,
1672
+ }, ['ids', 'confirm', 'reason']),
1673
+ },
1674
+ {
1675
+ name: 'securenow_blocklist_add',
1676
+ title: 'Add Blocked IP',
1677
+ description: 'Add an IP/CIDR to the blocklist, optionally scoped to a route/method. Write action; requires confirmation.',
1678
+ scope: 'blocklist:write',
1679
+ readOnly: false,
1680
+ confirm: true,
1681
+ method: 'POST',
1682
+ endpoint: '/blocklist',
1683
+ bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment', 'pathPattern', 'pathMatchMode', 'method'],
1684
+ inputSchema: objectSchema({
1685
+ ip: string('IPv4 address or CIDR.'),
1686
+ reason: string('Reason for blocking.'),
1687
+ expiresAt: string('Optional expiry time as ISO 8601.'),
1688
+ metadata: { type: 'object', additionalProperties: true, description: 'Optional metadata.' },
1689
+ appKey: string('Optional application key to scope this block. Omit for all apps.'),
1690
+ pathPattern: string('Optional route/path pattern, for example /admin*. Omit to block all routes.'),
1691
+ pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'], description: 'Path matching mode. Defaults to prefix.' },
1692
+ method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], description: 'HTTP method scope. Defaults to ALL.' },
1693
+ ...environmentInput,
1694
+ ...confirmSchema,
1695
+ }, ['ip', 'confirm', 'reason']),
1696
+ },
1697
+ {
1698
+ name: 'securenow_blocklist_remove',
1699
+ title: 'Unblock IP (Compat Alias)',
1700
+ description: 'Compatibility alias for unblocking a blocklist entry while retaining block report/history. Write action; requires confirmation.',
1701
+ scope: 'blocklist:write',
1702
+ readOnly: false,
1703
+ confirm: true,
1704
+ method: 'POST',
1705
+ endpoint: '/blocklist/:id/unblock',
1706
+ pathParams: ['id'],
1707
+ bodyFields: ['reason'],
1708
+ inputSchema: objectSchema({
1709
+ id: string('Blocklist entry id.'),
1710
+ ...confirmSchema,
1711
+ }, ['id', 'confirm', 'reason']),
1712
+ },
1713
+ {
1714
+ name: 'securenow_blocklist_unblock',
1715
+ title: 'Unblock IP And Keep History',
1716
+ description: 'Unblock a blocklist entry so firewall enforcement stops, while retaining the block report, history, and unblock audit trail. Write action; requires confirmation.',
1717
+ scope: 'blocklist:write',
1718
+ readOnly: false,
1719
+ confirm: true,
1720
+ method: 'POST',
1721
+ endpoint: '/blocklist/:id/unblock',
1722
+ pathParams: ['id'],
1723
+ bodyFields: ['reason'],
1724
+ inputSchema: objectSchema({
1725
+ id: string('Blocklist entry id.'),
1726
+ ...confirmSchema,
1727
+ }, ['id', 'confirm', 'reason']),
1728
+ },
1729
+ {
1730
+ name: 'securenow_blocklist_stats',
1731
+ title: 'Blocklist Stats',
1732
+ description: 'Return blocklist statistics.',
1733
+ scope: 'blocklist:read',
1734
+ readOnly: true,
1735
+ method: 'GET',
1736
+ endpoint: '/blocklist/stats',
1737
+ inputSchema: objectSchema({}),
1738
+ },
1739
+ {
1740
+ name: 'securenow_allowlist_list',
1741
+ title: 'List Restrictive Allowlist',
1742
+ description: 'List restrictive allowlist entries. If any active entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. This is not the Trusted IP list.',
1743
+ scope: 'allowlist:read',
1744
+ readOnly: true,
1745
+ method: 'GET',
1746
+ endpoint: '/allowlist',
1747
+ queryFields: ['page', 'limit', 'appKey', 'environment'],
1748
+ inputSchema: objectSchema({
1749
+ ...pagingInput,
1750
+ appKey: string('Optional application key scope.'),
1751
+ ...environmentInput,
1752
+ }),
1753
+ },
1754
+ {
1755
+ name: 'securenow_allowlist_add',
1756
+ title: 'Add Restrictive Allowlist IP',
1757
+ description: 'Dangerous production action: add an IP/CIDR to the restrictive allowlist. Once active, allowlist mode blocks every IP except listed entries. Do not use this to mark an IP trusted or suppress false positives; use securenow_trusted_add instead. Requires explicit human approval confirming deny-all behavior.',
1758
+ scope: 'allowlist:write',
1759
+ readOnly: false,
1760
+ confirm: true,
1761
+ method: 'POST',
1762
+ endpoint: '/allowlist',
1763
+ fixedBody: { initiatedBy: 'mcp' },
1764
+ bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys', 'environment', 'allowlistDenyAllApproved'],
1765
+ inputSchema: objectSchema({
1766
+ ip: string('IPv4 address or CIDR.'),
1767
+ label: string('Human-readable label.'),
1768
+ reason: string('Required human-approved reason. Must acknowledge that allowlist blocks all non-listed IPs and why Trusted IPs is not the correct action.'),
1769
+ expiresAt: string('Optional expiry time as ISO 8601.'),
1770
+ applicationsAll: boolean('Apply to all applications.'),
1771
+ applicationKeys: arrayOfStrings('Application keys to scope this allowlist entry to.'),
1772
+ allowlistDenyAllApproved: boolean('Must be true only after the user explicitly approves deny-by-default allowlist behavior. Do not set this for trusted IPs, monitors, false positives, or investigation cleanup.'),
1773
+ ...environmentInput,
1774
+ ...confirmSchema,
1775
+ }, ['ip', 'confirm', 'reason', 'allowlistDenyAllApproved']),
1776
+ },
1777
+ {
1778
+ name: 'securenow_allowlist_remove',
1779
+ title: 'Remove Allowed IP',
1780
+ description: 'Remove an allowlist entry. Write action; requires confirmation.',
1781
+ scope: 'allowlist:write',
1782
+ readOnly: false,
1783
+ confirm: true,
1784
+ method: 'DELETE',
1785
+ endpoint: '/allowlist/:id',
1786
+ pathParams: ['id'],
1787
+ inputSchema: objectSchema({
1788
+ id: string('Allowlist entry id.'),
1789
+ ...confirmSchema,
1790
+ }, ['id', 'confirm', 'reason']),
1791
+ },
1792
+ {
1793
+ name: 'securenow_trusted_list',
1794
+ title: 'List Trusted IPs',
1795
+ description: 'List trusted IPs. Trusted IPs are for known-safe traffic and do not turn on deny-by-default allowlist mode.',
1796
+ scope: 'trusted_ips:read',
1797
+ readOnly: true,
1798
+ method: 'GET',
1799
+ endpoint: '/trusted-ips',
1800
+ queryFields: ['appKey', 'environment'],
1801
+ inputSchema: objectSchema({
1802
+ appKey: string('Optional application key scope.'),
1803
+ ...environmentInput,
1804
+ }),
1805
+ },
1806
+ {
1807
+ name: 'securenow_trusted_add',
1808
+ title: 'Add Trusted IP',
1809
+ description: 'Add a trusted IP/CIDR for known-safe infrastructure, monitors, office/VPN traffic, or scoped false-positive suppression. This does not enable deny-by-default allowlist mode and is the correct tool when an IP should be trusted without blocking other visitors. Write action; requires confirmation.',
1810
+ scope: 'trusted_ips:write',
1811
+ readOnly: false,
1812
+ confirm: true,
1813
+ method: 'POST',
1814
+ endpoint: '/trusted-ips',
1815
+ bodyFields: ['ip', 'label', 'note', 'applicationsAll', 'applicationKeys', 'environment'],
1816
+ inputSchema: objectSchema({
1817
+ ip: string('IPv4 address or CIDR.'),
1818
+ label: string('Human-readable label.'),
1819
+ note: string('Optional note.'),
1820
+ applicationsAll: boolean('Apply to all applications.'),
1821
+ applicationKeys: arrayOfStrings('Application keys to scope this trusted IP to.'),
1822
+ ...environmentInput,
1823
+ ...confirmSchema,
1824
+ }, ['ip', 'confirm', 'reason']),
1825
+ },
1826
+ {
1827
+ name: 'securenow_trusted_remove',
1828
+ title: 'Remove Trusted IP',
1829
+ description: 'Remove a trusted IP entry. Write action; requires confirmation.',
1830
+ scope: 'trusted_ips:write',
1831
+ readOnly: false,
1832
+ confirm: true,
1833
+ method: 'DELETE',
1834
+ endpoint: '/trusted-ips/:id',
1835
+ pathParams: ['id'],
1836
+ inputSchema: objectSchema({
1837
+ id: string('Trusted IP entry id.'),
1838
+ ...confirmSchema,
1839
+ }, ['id', 'confirm', 'reason']),
1840
+ },
1841
+ ];
1842
+
1843
+ const RESOURCES = [
1844
+ {
1845
+ uri: 'securenow://docs/skill-api',
1846
+ name: 'skill-api',
1847
+ title: 'SecureNow SDK Skill',
1848
+ description: 'Installed SDK skill instructions from SKILL-API.md.',
1849
+ mimeType: MARKDOWN,
1850
+ file: '../SKILL-API.md',
1851
+ },
1852
+ {
1853
+ uri: 'securenow://docs/skill-cli',
1854
+ name: 'skill-cli',
1855
+ title: 'SecureNow CLI Skill',
1856
+ description: 'Installed CLI skill instructions from SKILL-CLI.md.',
1857
+ mimeType: MARKDOWN,
1858
+ file: '../SKILL-CLI.md',
1859
+ },
1860
+ {
1861
+ uri: 'securenow://docs/npm-readme',
1862
+ name: 'npm-readme',
1863
+ title: 'SecureNow npm README',
1864
+ description: 'Installed npm README.',
1865
+ mimeType: MARKDOWN,
1866
+ file: '../NPM_README.md',
1867
+ },
1868
+ {
1869
+ uri: 'securenow://project/config',
1870
+ name: 'project-config',
1871
+ title: 'SecureNow Project Config',
1872
+ description: 'Resolved local SecureNow config and masked credentials.',
1873
+ mimeType: JSON_MIME,
1874
+ dynamic: 'projectConfig',
1875
+ },
1876
+ {
1877
+ uri: 'securenow://tools/catalog',
1878
+ name: 'tools-catalog',
1879
+ title: 'SecureNow MCP Tool Catalog',
1880
+ description: 'MCP tool names, scopes, and write/read classification.',
1881
+ mimeType: JSON_MIME,
1882
+ dynamic: 'toolsCatalog',
1883
+ },
1884
+ ];
1885
+
1886
+ const PROMPTS = [
1887
+ {
1888
+ name: 'secure_project_setup',
1889
+ title: 'Secure Project Setup',
1890
+ description: 'Generic framework setup prompt for SecureNow tracing, logs, body capture, multipart, and firewall defaults.',
1891
+ arguments: [
1892
+ { name: 'projectRoot', description: 'Project root to configure.', required: false },
1893
+ { name: 'startCommand', description: 'Optional customer-approved command to start the app.', required: false },
1894
+ { name: 'buildCommand', description: 'Optional customer-approved build command.', required: false },
1895
+ { name: 'testCommand', description: 'Optional customer-approved test command.', required: false },
1896
+ { name: 'localUrl', description: 'Optional local URL to smoke-test after the app is running.', required: false },
1897
+ { name: 'productionUrl', description: 'Optional production URL to smoke-test with --env production.', required: false },
1898
+ ],
1899
+ },
1900
+ {
1901
+ name: 'verify_firewall_default_on',
1902
+ title: 'Verify Default-On Firewall',
1903
+ description: 'Verify SecureNow CLI login, app firewall toggle, credentials, and SDK defaults.',
1904
+ arguments: [
1905
+ { name: 'appKey', description: 'Optional SecureNow app key.', required: false },
1906
+ ],
1907
+ },
1908
+ {
1909
+ name: 'investigate_ip',
1910
+ title: 'Investigate IP',
1911
+ description: 'Investigate an IP with SecureNow IP intelligence, traces, logs, and remediation options.',
1912
+ arguments: [
1913
+ { name: 'ip', description: 'IP address to investigate.', required: true },
1914
+ { name: 'appKeys', description: 'Optional comma-separated app keys.', required: false },
1915
+ { name: 'environment', description: 'Environment to investigate. Defaults to production; use all only when explicitly needed.', required: false },
1916
+ ],
1917
+ },
1918
+ {
1919
+ name: 'investigate_human_action_row',
1920
+ title: 'Investigate Human Action Row',
1921
+ description: 'Use MCP tools to deeply review one Requires Human row and reach a safe audited outcome.',
1922
+ arguments: [
1923
+ { name: 'rowNumber', description: '1-based row number from the Requires Human queue.', required: true },
1924
+ { name: 'page', description: 'Queue page number. Defaults to 1.', required: false },
1925
+ { name: 'limit', description: 'Rows to fetch. Defaults to 20.', required: false },
1926
+ { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
1927
+ ],
1928
+ },
1929
+ {
1930
+ name: 'work_human_actions',
1931
+ title: 'Work Human Actions',
1932
+ description: 'Use MCP tools to work the approval queue from most urgent to least urgent, with trace verification and scoped decisions.',
1933
+ arguments: [
1934
+ { name: 'limit', description: 'Maximum rows to review this run. Defaults to 10.', required: false },
1935
+ { name: 'search', description: 'Optional search filter for the queue.', required: false },
1936
+ { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
1937
+ ],
1938
+ },
1939
+ {
1940
+ name: 'cleanup_legacy_pending_blocks',
1941
+ title: 'Clean Legacy Pending Blocks',
1942
+ description: 'Use MCP tools to review and approve/reject legacy pending blocklist rows so only current human work remains.',
1943
+ arguments: [
1944
+ { name: 'limit', description: 'Maximum pending block rows to review this run. Defaults to 50.', required: false },
1945
+ { name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
1946
+ { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute approvals/rejections.', required: false },
1947
+ ],
1948
+ },
1949
+ {
1950
+ name: 'configure_default_automation',
1951
+ title: 'Configure Default Automation',
1952
+ description: 'Use MCP tools to set conservative default SecureNow automation rules based on the canonical riskScore and supporting evidence.',
1953
+ arguments: [
1954
+ { name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
1955
+ { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to create/update automation rules.', required: false },
1956
+ ],
1957
+ },
1958
+ ];
1959
+
1960
+ function promptMessages(name, args = {}) {
1961
+ if (name === 'secure_project_setup') {
1962
+ return [
1963
+ {
1964
+ role: 'user',
1965
+ content: {
1966
+ type: 'text',
1967
+ text: [
1968
+ args.projectRoot ? `Project root: ${args.projectRoot}` : null,
1969
+ args.startCommand ? `Customer-approved start command: ${args.startCommand}` : null,
1970
+ args.buildCommand ? `Customer-approved build command: ${args.buildCommand}` : null,
1971
+ args.testCommand ? `Customer-approved test command: ${args.testCommand}` : null,
1972
+ args.localUrl ? `Local URL for smoke testing: ${args.localUrl}` : null,
1973
+ args.productionUrl ? `Production URL for smoke testing: ${args.productionUrl}` : null,
1974
+ UNIVERSAL_SECURENOW_SETUP_PROMPT,
1975
+ ].filter(Boolean).join('\n\n'),
1976
+ },
1977
+ },
1978
+ ];
1979
+ }
1980
+
1981
+ if (name === 'verify_firewall_default_on') {
1982
+ return [
1983
+ {
1984
+ role: 'user',
1985
+ content: {
1986
+ type: 'text',
1987
+ text: [
1988
+ 'Verify SecureNow default-on protection for this project.',
1989
+ args.appKey ? `App key: ${args.appKey}` : null,
1990
+ 'Check npx securenow whoami, npx securenow api-key show, npx securenow firewall apps, and npx securenow firewall status.',
1991
+ 'Confirm traces, logs, capture.body, capture.multipart, and firewall.enabled are enabled by .securenow/runtime.json defaults unless explicitly set false. Legacy .securenow/credentials.json is still accepted.',
1992
+ 'Do not print full tokens or API keys.',
1993
+ ].filter(Boolean).join('\n'),
1994
+ },
1995
+ },
1996
+ ];
1997
+ }
1998
+
1999
+ if (name === 'investigate_ip') {
2000
+ return [
2001
+ {
2002
+ role: 'user',
2003
+ content: {
2004
+ type: 'text',
2005
+ text: [
2006
+ `Investigate IP ${args.ip || '<ip>'} with SecureNow.`,
2007
+ args.appKeys ? `Scope to app keys: ${args.appKeys}` : null,
2008
+ `Environment scope: ${args.environment || 'production'}.`,
2009
+ 'Use IP intelligence first, then related traces/logs, then recommend remediation.',
2010
+ 'Only block or trust the IP after explicit user confirmation. Never use IP Allowlist for false positives or trusted traffic; allowlist is deny-by-default and blocks all non-listed IPs.',
2011
+ ].filter(Boolean).join('\n'),
2012
+ },
2013
+ },
2014
+ ];
2015
+ }
2016
+
2017
+ if (name === 'investigate_human_action_row') {
2018
+ const rowNumber = args.rowNumber || '<rowNumber>';
2019
+ const page = args.page || 1;
2020
+ const limit = args.limit || 20;
2021
+ const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
2022
+ return [
2023
+ {
2024
+ role: 'user',
2025
+ content: {
2026
+ type: 'text',
2027
+ text: [
2028
+ `Investigate Requires Human row ${rowNumber} using SecureNow MCP.`,
2029
+ `Fetch page=${page}, limit=${limit} with securenow_human_actions_list, select row ${rowNumber}, then call securenow_notifications_get and securenow_human_action_report for that notificationId and IP.`,
2030
+ 'Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
2031
+ 'Open trace evidence with securenow_traces_show and correlated logs with securenow_logs_for_trace when trace IDs are available.',
2032
+ 'Return one clear outcome: Block IP, Rate Limit, False Positive, Rule Tuning Applied/Needed, New Alert Rule Needed, Case Action Approved/Rejected, or Ambiguous/Skipped with exact missing proof.',
2033
+ 'A row can be finished without an enforcement write when no write is safe. In that case record or prepare a structured no-write decision report explaining the missing proof, missing permission, or vague guard.',
2034
+ 'If evidence is mixed, split the decision by IP/cluster. Block only proven malicious IPs, false-positive only exact benign shapes, rate-limit only route-specific volume abuse, and leave ambiguous members with missing proof.',
2035
+ 'If the right fix is global/system rule tuning and the MCP tool returns access denied, do not create customer-specific false positives. Prepare the exact attack-preserving guard/SQL, dry-run it if possible, and record outcome=rule_tuning with missingProof naming the missing permission.',
2036
+ 'If an AI proposed action says to use a shared benign shape but does not provide exact path/method/status/user-agent/body/header conditions, do not execute it. Record the missing guard fields instead.',
2037
+ 'Use rate limiting only as temporary soft remediation for repeated route-specific abuse such as login brute force, credential stuffing, scraping/API bursts, enumeration, recon/probing, or repeated noisy payloads where risk/impact evidence is below the block threshold.',
2038
+ 'Do not rate-limit instead of blocking when there is confirmed exploit success, token/data exposure, SSRF reachability, file read, RCE, persistence, malware/C2, or riskScore >= 85 with high-confidence malicious evidence. Do not rate-limit benign false positives, trusted monitors, app-server/proxy attribution problems, isolated one-off requests, or broad noisy rules that need alert tuning.',
2039
+ 'Do not create or recommend IP Allowlist entries during investigations unless the user explicitly asks to lock the app/environment to a known set of IPs. For safe monitors, offices, VPNs, or false positives, use Trusted IPs or scoped false-positive exclusions instead.',
2040
+ confirmWrites
2041
+ ? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block, securenow_rate_limit_create_from_text plus securenow_human_action_decision_report_add(outcome=rate_limited), or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If no enforcement write is safe, call securenow_human_action_decision_report_add with outcome=rule_tuning, new_alert_rule, ambiguous, skipped, or other so the audit trail is complete without changing IP status.'
2042
+ : 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
2043
+ 'If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
2044
+ 'False positives must be narrow: app + alert rule + path + method/status/user-agent/body evidence where possible. Never globally trust an IP by default.',
2045
+ ].join('\n'),
2046
+ },
2047
+ },
2048
+ ];
2049
+ }
2050
+
2051
+ if (name === 'work_human_actions') {
2052
+ const limit = args.limit || 10;
2053
+ const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
2054
+ return [
2055
+ {
2056
+ role: 'user',
2057
+ content: {
2058
+ type: 'text',
2059
+ text: [
2060
+ 'Work my SecureNow Requires Human queue like a senior security analyst using the MCP tools.',
2061
+ `Review up to ${limit} row(s), most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
2062
+ 'Start with securenow_human_actions_list. For each row, call securenow_notifications_get and securenow_human_action_report, inspect the AI report/investigation steps/proofs/trace IDs, and fetch trace/log evidence where useful.',
2063
+ 'For each row choose exactly one outcome: Block IP, Rate Limit, False Positive, Rule Tuning Applied/Needed, New Alert Rule Needed, Case Action Approved/Rejected, or Ambiguous/Skipped with exact missing proof.',
2064
+ 'Finished does not always mean enforcement changed. A row is also finished when a structured no-write decision report records the exact proof gap, missing permission, or vague guard and no weaker substitute write is used.',
2065
+ 'If a global/system rule update is the right fix but access is denied, do not mark customer-specific false positives just to clear the row. Prepare the exact attack-preserving guard/SQL, dry-run it when possible, and record outcome=rule_tuning with missingProof.',
2066
+ 'If evidence is mixed, do partial resolution: block proven malicious IPs, false-positive exact benign shapes, rate-limit route-specific volume abuse, and leave ambiguous IPs with their missing proof.',
2067
+ 'Reject vague AI proposed actions. If the prompt/action lacks concrete rule id, path, method, status, user-agent/body/header guard, benign samples, and attack examples that still match, record it as ambiguous or rule_tuning needed.',
2068
+ 'Use Rate Limit only for repeated route-specific abuse where temporary friction is safer than blocking: login brute force/credential stuffing, password reset/account enumeration, scraping/API bursts, path or ID enumeration, recon/probing, or repeated noisy payloads without confirmed exploit success.',
2069
+ 'Do not rate-limit confirmed high-risk attacks that should be blocked, benign traffic that should be false-positive scoped, broad noisy rules that need tuning, app-server/proxy attribution problems, or isolated one-off requests.',
2070
+ 'Do not create or recommend IP Allowlist entries while working this queue unless the user explicitly approves deny-by-default allowlist mode for the whole app/environment. Use Trusted IPs for trusted/bypass cases.',
2071
+ confirmWrites
2072
+ ? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For skipped/ambiguous/rule-tuning-needed/new-rule-needed rows that should be auditable without changing enforcement status, use securenow_human_action_decision_report_add or securenow_human_case_decision_report_add.'
2073
+ : 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
2074
+ 'For block decisions, use securenow_human_action_block. For rate-limit decisions, use securenow_rate_limit_create_from_text and then securenow_human_action_decision_report_add with outcome=rate_limited. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update only when the action is safe to approve/reject.',
2075
+ 'End with counts: handled, blocked/proposed block, rate limits created/proposed, false positives, rule tuning/admin permission blockers with exact guard, partial resolutions, skipped with missing proof, and still waiting.',
2076
+ ].join('\n'),
2077
+ },
2078
+ },
2079
+ ];
2080
+ }
2081
+
2082
+ if (name === 'cleanup_legacy_pending_blocks') {
2083
+ const limit = args.limit || 50;
2084
+ const environment = args.environment || 'production';
2085
+ const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
2086
+ return [
2087
+ {
2088
+ role: 'user',
2089
+ content: {
2090
+ type: 'text',
2091
+ text: [
2092
+ 'Clean my SecureNow legacy pending blocklist queue using MCP tools.',
2093
+ `Review up to ${limit} pending block row(s). Environment scope: ${environment}.`,
2094
+ 'Start with securenow_blocklist_stats, then securenow_blocklist_pending_list({ page: 1, limit, environment }).',
2095
+ 'For each pending block, inspect id, IP, source, reason, metadata.riskScore, metadata.aiRiskScore, metadata.abuseConfidenceScore, automation rule, linked notification, age, app, and environment.',
2096
+ 'When investigationNotificationId exists, fetch securenow_notifications_get for that case and use the linked IP report/history as evidence.',
2097
+ 'Approve only when the row has clear malicious evidence, riskScore >= 90, or SecureNow AI IPDB evidence score >= 80 with no false-positive/test signal.',
2098
+ 'Reject stale, ambiguous, synthetic/test, self-traffic, or false-positive rows. Prefer narrow rule/exclusion tuning when the same benign pattern repeats.',
2099
+ confirmWrites
2100
+ ? 'The user requested execution. Use securenow_blocklist_pending_approve or securenow_blocklist_pending_reject with confirm:true and a precise reason. Use bulk tools only after every selected row satisfies the same reviewed policy.'
2101
+ : 'Do not execute writes yet. Produce exact approve/reject MCP calls grouped by safe policy for user approval.',
2102
+ 'After each write or batch, re-fetch securenow_blocklist_stats and securenow_blocklist_pending_list until the legacy pending count is zero or only ambiguous rows remain.',
2103
+ 'End with counts: approved, rejected, skipped ambiguous, legacyPendingBlockCount, and remaining proof gaps.',
2104
+ ].join('\n'),
2105
+ },
2106
+ },
2107
+ ];
2108
+ }
2109
+
2110
+ if (name === 'configure_default_automation') {
2111
+ const environment = args.environment || 'production';
2112
+ const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
2113
+ return [
2114
+ {
2115
+ role: 'user',
2116
+ content: {
2117
+ type: 'text',
2118
+ text: [
2119
+ 'Configure SecureNow default automation using the MCP tools.',
2120
+ `Review environment context: ${environment}. Built-in defaults apply to all apps and all environments unless the customer narrows them later.`,
2121
+ 'Use one canonical product score for automation decisions: riskScore. SecureNow AI IPDB score and AI confidence remain supporting evidence.',
2122
+ 'Built-in defaults are active by default:',
2123
+ '- Critical Risk Auto-Block: riskScore>=95, TTL 168h.',
2124
+ '- High Risk Auto-Block: riskScore>=90 AND riskScore<95, TTL 72h.',
2125
+ '- Elevated Risk Auto-Block: riskScore>=85 AND riskScore<90, TTL 24h.',
2126
+ 'Start with securenow_automation_defaults_ensure({ confirm:true, reason:"Provision SecureNow default risk-score automation" }) when writes are confirmed, otherwise start with securenow_automation_rules_list and report what would be ensured.',
2127
+ 'After ensuring defaults, dry-run each default rule with securenow_automation_rule_dry_run to inspect sample matches. Disable or tune only if evidence shows customer-specific false positives.',
2128
+ confirmWrites
2129
+ ? 'The user requested execution. Ensure defaults with confirm:true, do not force-enable previously disabled defaults unless the user explicitly asked, and re-list rules after writes.'
2130
+ : 'Do not execute writes yet. Return exact MCP update/create calls and dry-run calls for approval.',
2131
+ 'End with active rules, disabled rules that should stay disabled, and any risk from automation scope.',
2132
+ ].join('\n'),
2133
+ },
2134
+ },
2135
+ ];
2136
+ }
2137
+
2138
+ throw new Error(`Unknown prompt: ${name}`);
2139
+ }
2140
+
2141
+ function getTool(name) {
2142
+ return TOOLS.find((tool) => tool.name === name) || null;
2143
+ }
2144
+
2145
+ function mcpToolDefinition(tool) {
2146
+ return {
2147
+ name: tool.name,
2148
+ title: tool.title,
2149
+ description: tool.description,
2150
+ inputSchema: tool.inputSchema || objectSchema({}),
2151
+ annotations: {
2152
+ readOnlyHint: !!tool.readOnly,
2153
+ destructiveHint: !!tool.destructive,
2154
+ idempotentHint: !!tool.readOnly,
2155
+ },
2156
+ };
2157
+ }
2158
+
2159
+ function listTools() {
2160
+ return TOOLS.map(mcpToolDefinition);
2161
+ }
2162
+
2163
+ function listResources() {
2164
+ return RESOURCES.map(({ file, dynamic, ...resource }) => resource);
2165
+ }
2166
+
2167
+ function listPrompts() {
2168
+ return PROMPTS;
2169
+ }
2170
+
2171
+ function assertConfirmed(tool, args = {}) {
2172
+ if (!tool.confirm) return;
2173
+ if (args.confirm !== true) {
2174
+ throw new Error(`${tool.name} is a write action. Pass confirm:true and a reason to proceed.`);
2175
+ }
2176
+ if (!args.reason || !String(args.reason).trim()) {
2177
+ throw new Error(`${tool.name} requires a non-empty reason.`);
2178
+ }
2179
+ if (tool.name === 'securenow_allowlist_add' && args.allowlistDenyAllApproved !== true) {
2180
+ throw new Error('securenow_allowlist_add is deny-by-default: it blocks every non-listed IP for the scoped app/environment. Pass allowlistDenyAllApproved:true only after explicit human approval. Use securenow_trusted_add for trusted IPs or false positives.');
2181
+ }
2182
+ }
2183
+
2184
+ function buildApiRequest(tool, rawArgs = {}) {
2185
+ const args = { ...rawArgs };
2186
+ let endpoint = tool.endpoint;
2187
+
2188
+ for (const key of tool.pathParams || []) {
2189
+ if (args[key] == null || args[key] === '') throw new Error(`Missing required argument: ${key}`);
2190
+ endpoint = endpoint.replace(`:${key}`, encodeURIComponent(String(args[key])));
2191
+ }
2192
+
2193
+ const query = { ...(tool.fixedQuery || {}) };
2194
+ for (const key of tool.queryFields || []) {
2195
+ let value = args[key];
2196
+ if (tool.normalize && typeof tool.normalize[key] === 'function') {
2197
+ value = tool.normalize[key](value);
2198
+ }
2199
+ if (value != null && value !== '') query[key] = value;
2200
+ }
2201
+
2202
+ const body = { ...(tool.fixedBody || {}) };
2203
+ for (const key of tool.bodyFields || []) {
2204
+ let value = args[key];
2205
+ if (tool.normalize && typeof tool.normalize[key] === 'function') {
2206
+ value = tool.normalize[key](value);
2207
+ }
2208
+ if (value != null && value !== '') body[key] = value;
2209
+ }
2210
+
2211
+ if (tool.reasonAsNote && !body.note && args.reason) {
2212
+ body.note = args.reason;
2213
+ }
2214
+ if (tool.reasonInResult && args.reason) {
2215
+ body.result = {
2216
+ ...(body.result && typeof body.result === 'object' ? body.result : {}),
2217
+ reason: args.reason,
2218
+ };
2219
+ }
2220
+
2221
+ return {
2222
+ method: tool.method,
2223
+ endpoint,
2224
+ query,
2225
+ body: Object.keys(body).length > 0 ? body : null,
2226
+ };
2227
+ }
2228
+
2229
+ function readStaticResource(resource) {
2230
+ const filepath = path.resolve(__dirname, resource.file);
2231
+ return fs.readFileSync(filepath, 'utf8');
2232
+ }
2233
+
2234
+ function resourceContent(resource, dynamicHandlers = {}) {
2235
+ if (resource.file) return readStaticResource(resource);
2236
+ if (resource.dynamic && dynamicHandlers[resource.dynamic]) {
2237
+ return dynamicHandlers[resource.dynamic]();
2238
+ }
2239
+ throw new Error(`Resource is not readable: ${resource.uri}`);
2240
+ }
2241
+
2242
+ module.exports = {
2243
+ TOOLS,
2244
+ RESOURCES,
2245
+ PROMPTS,
2246
+ TEXT,
2247
+ JSON_MIME,
2248
+ MARKDOWN,
2249
+ getTool,
2250
+ mcpToolDefinition,
2251
+ listTools,
2252
+ listResources,
2253
+ listPrompts,
2254
+ promptMessages,
2255
+ assertConfirmed,
2256
+ buildApiRequest,
2257
+ resourceContent,
2258
+ jsonText,
2259
+ maskSecret,
2260
+ sanitizeArgs,
2261
+ };