securenow 8.12.4 → 8.14.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.
package/mcp/catalog.js CHANGED
@@ -190,7 +190,7 @@ const TOOLS = [
190
190
  {
191
191
  name: 'securenow_auth_status',
192
192
  title: 'SecureNow Auth Status',
193
- description: 'Show local SecureNow credential source, selected app, and masked runtime API key.',
193
+ description: 'Show who is signed in and how: over the hosted connector, the OAuth client, access level (investigate or investigate_and_respond) and the organization with the role of the person signed in; locally, the credential source, selected app and masked runtime API key.',
194
194
  scope: null,
195
195
  localOnly: true,
196
196
  readOnly: true,
@@ -209,7 +209,7 @@ const TOOLS = [
209
209
  {
210
210
  name: 'securenow_apps_create',
211
211
  title: 'Create Application',
212
- description: 'Create a SecureNow application. Write action; requires confirmation. Each app is billed $99/month on subscribed accounts (free during the 14-day trial). Confirm with the user before creating apps.',
212
+ description: 'Create a SecureNow application. Write action; requires confirmation. Each app is billed $99/month on subscribed accounts (free during the 14-day trial). Creating one changes the bill.',
213
213
  scope: 'applications:write',
214
214
  readOnly: false,
215
215
  confirm: true,
@@ -318,16 +318,18 @@ const TOOLS = [
318
318
  {
319
319
  name: 'securenow_firewall_test_ip',
320
320
  title: 'Test Firewall IP Decision',
321
- description: 'Check whether an IP would be blocked by the firewall.',
321
+ description: 'Check whether an IP would be blocked by the firewall. Pass path (and method) to evaluate route-scoped block rules the way the running app does, including case and trailing-slash aliases.',
322
322
  scope: 'firewall:read',
323
323
  readOnly: true,
324
324
  method: 'GET',
325
325
  endpoint: '/firewall/check/:ip',
326
326
  pathParams: ['ip'],
327
- queryFields: ['environment', 'appKey'],
327
+ queryFields: ['environment', 'appKey', 'path', 'method'],
328
328
  inputSchema: objectSchema({
329
329
  ip: string('IPv4 address to test.'),
330
330
  appKey: string('Optional application key UUID to test the app/environment toggle and scoped lists.'),
331
+ path: string('Optional request path, for example /api/auth/login. Without it, route-scoped rules are not evaluated.'),
332
+ method: string('Optional HTTP method for the path, default GET.'),
331
333
  ...environmentInput,
332
334
  }, ['ip']),
333
335
  },
@@ -379,7 +381,7 @@ const TOOLS = [
379
381
  ...appKeysInput,
380
382
  ...environmentInput,
381
383
  ...timeRangeInput,
382
- severity: string('Optional severity filter.'),
384
+ severity: string('Optional severity filter: debug, info, warn, error or fatal (case-insensitive). error also returns fatal crash records.'),
383
385
  limit: number('Maximum logs to return.', { minimum: 1, maximum: 500 }),
384
386
  }, ['appKeys']),
385
387
  },
@@ -403,13 +405,19 @@ const TOOLS = [
403
405
  {
404
406
  name: 'securenow_notifications_list',
405
407
  title: 'List Notifications',
406
- description: 'List security notifications.',
408
+ description: 'List security alerts (notifications), newest first, as compact summaries: rule, severity, status, test mode, IPs with AI verdicts, agent status and proposed actions. Open alerts by default; securenow_notifications_get returns one alert in full.',
407
409
  scope: 'notifications:read',
408
410
  readOnly: true,
409
411
  method: 'GET',
410
412
  endpoint: '/notifications',
411
- queryFields: ['limit', 'page'],
412
- inputSchema: objectSchema({ ...pagingInput }),
413
+ queryFields: ['limit', 'page', 'status', 'severity', 'search', 'archived'],
414
+ inputSchema: objectSchema({
415
+ ...pagingInput,
416
+ status: string('Optional status filter, for example open, pending_review or resolved.'),
417
+ severity: string('Optional severity filter: critical, high, medium or low.'),
418
+ search: string('Optional text search over the alert title, IPs and rule.'),
419
+ archived: boolean('true lists archived alerts instead of open ones.'),
420
+ }),
413
421
  },
414
422
  {
415
423
  name: 'securenow_notifications_unread',
@@ -465,7 +473,7 @@ const TOOLS = [
465
473
  {
466
474
  name: 'securenow_human_actions_list',
467
475
  title: 'List Human Action Queue',
468
- description: 'List approval-gated human decisions prepared by SecureNow AI, ordered by urgency.',
476
+ description: 'List approval-gated decisions prepared by SecureNow AI, most urgent first, as compact summaries: IP rows (kind ip) with the AI recommendation, risk score, confidence and proofs, and rule-level case actions (kind case_action, no IP) such as tune_rule.',
469
477
  scope: 'notifications:read',
470
478
  readOnly: true,
471
479
  method: 'GET',
@@ -609,7 +617,7 @@ const TOOLS = [
609
617
  prototypeRunId: string('Optional notification id whose root-cause key should be used.'),
610
618
  notificationIds: arrayOfStrings('Optional subset of eligible notification ids to close. Leave empty to apply to every eligible row in the group.'),
611
619
  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.'),
620
+ allowMixed: boolean('Allows a mixed decision/attribution group, meant for groups whose rows were all reviewed manually. Defaults to false.'),
613
621
  previewOnly: boolean('When true, returns the eligible rows without writing.'),
614
622
  maxRows: number('Maximum rows to close in this batch. Defaults to 100, capped at 250.', { minimum: 1, maximum: 250 }),
615
623
  maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
@@ -635,7 +643,7 @@ const TOOLS = [
635
643
  prototypeRunId: string('Optional notification id whose root-cause key should be used.'),
636
644
  notificationIds: arrayOfStrings('Optional subset of eligible notification ids to close. Leave empty to apply to every eligible row in the group.'),
637
645
  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.'),
646
+ allowMixed: boolean('Allows a mixed decision/attribution group, meant for groups whose rows were all reviewed manually. Defaults to false.'),
639
647
  previewOnly: boolean('When true, returns the eligible rows without writing.'),
640
648
  maxRows: number('Maximum rows to close in this batch. Defaults to 100, capped at 250.', { minimum: 1, maximum: 250 }),
641
649
  maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 500, capped at 1000.', { minimum: 1, maximum: 1000 }),
@@ -751,7 +759,7 @@ const TOOLS = [
751
759
  inputSchema: objectSchema({
752
760
  notificationId: string('Notification id from the human action row.'),
753
761
  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.' },
762
+ conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Restrictive false-positive conditions, scoped by app, rule, path, method, status, user agent or body.' },
755
763
  matchMode: string('Condition match mode: all or any.'),
756
764
  createExclusion: boolean('Whether to create a restrictive exclusion rule.'),
757
765
  applyToExisting: boolean('Whether to apply to existing matching IP investigations.'),
@@ -818,30 +826,39 @@ const TOOLS = [
818
826
  {
819
827
  name: 'securenow_ip_traces',
820
828
  title: 'IP Traces',
821
- description: 'Fetch traces associated with an IP address.',
829
+ description: 'Fetch the requests an IP made: time, app key, route, method, status code, duration and user agent, newest first. Covers the last 24 hours unless from/to are given, up to 500 requests per telemetry store, across every app unless appKeys are given; the response states the window.',
822
830
  scope: 'ip_intel:read',
823
831
  readOnly: true,
824
832
  method: 'GET',
825
833
  endpoint: '/ip/:ip/traces',
826
834
  pathParams: ['ip'],
827
- queryFields: ['appKeys', 'environment'],
835
+ queryFields: ['appKeys', 'environment', 'from', 'to'],
828
836
  normalize: { appKeys: normalizeAppKeys },
829
837
  inputSchema: objectSchema({
830
838
  ip: string('IPv4 address.'),
831
839
  ...appKeysInput,
832
840
  ...environmentInput,
841
+ from: string('Start time as ISO 8601. Defaults to 24 hours before to.'),
842
+ to: string('End time as ISO 8601. Defaults to now.'),
833
843
  }, ['ip']),
834
844
  },
835
845
  {
836
846
  name: 'securenow_forensics_query',
837
847
  title: 'Run Forensics Query',
838
- description: 'Run a natural-language forensic query. Write scope because it creates an async query job.',
848
+ description: 'Run a natural-language forensic query over your traffic and return the result rows. Waits up to 30 seconds; a query still running after that returns its jobId with status processing. Write scope because it creates an async query job.',
839
849
  scope: 'forensics:write',
840
850
  readOnly: false,
841
851
  confirm: true,
842
852
  method: 'POST',
843
853
  endpoint: '/forensics/query',
844
854
  bodyFields: ['query', 'applicationId', 'instanceId', 'environment'],
855
+ poll: {
856
+ tool: 'securenow_forensics_query_status',
857
+ idField: 'jobId',
858
+ pending: ['processing'],
859
+ timeoutMs: 30000,
860
+ intervalMs: 2000,
861
+ },
845
862
  inputSchema: objectSchema({
846
863
  query: string('Natural-language forensic query.'),
847
864
  applicationId: string('Optional application database id.'),
@@ -850,6 +867,19 @@ const TOOLS = [
850
867
  ...confirmSchema,
851
868
  }, ['query', 'confirm', 'reason']),
852
869
  },
870
+ {
871
+ name: 'securenow_forensics_query_status',
872
+ title: 'Forensics Query Result',
873
+ description: 'Get the status (processing, completed or failed), generated SQL and result rows of a forensics query job.',
874
+ scope: 'forensics:read',
875
+ readOnly: true,
876
+ method: 'GET',
877
+ endpoint: '/forensics/query/status/:jobId',
878
+ pathParams: ['jobId'],
879
+ inputSchema: objectSchema({
880
+ jobId: string('Forensics query job id.'),
881
+ }, ['jobId']),
882
+ },
853
883
  {
854
884
  name: 'securenow_forensics_library',
855
885
  title: 'Forensics Query Library',
@@ -1072,6 +1102,165 @@ const TOOLS = [
1072
1102
  ...confirmSchema,
1073
1103
  }, ['id', 'confirm', 'reason']),
1074
1104
  },
1105
+ {
1106
+ name: 'securenow_rate_limit_create',
1107
+ title: 'Create Rate Limit',
1108
+ description: 'Create a rate-limit rule from structured fields. The running app enforces it within about 10 seconds and answers 429 with Retry-After once the limit is reached. keyBy picks what each bucket counts: ip (default), global (one shared bucket), header (one bucket per value of keyName, e.g. x-api-key) or cookie (one bucket per value of keyName, e.g. session). Several rules can stack on one route (for example a burst limit and an hourly cap). Write action; requires confirmation.',
1109
+ scope: 'rate_limits:write',
1110
+ readOnly: false,
1111
+ confirm: true,
1112
+ method: 'POST',
1113
+ endpoint: '/rate-limits',
1114
+ bodyFields: ['name', 'appKey', 'environment', 'ip', 'pathPattern', 'pathMatchMode', 'method', 'keyBy', 'keyName', 'limit', 'windowSeconds', 'duration', 'expiresAt', 'status', 'reason'],
1115
+ inputSchema: objectSchema({
1116
+ name: string('Optional rule name.'),
1117
+ appKey: string('Optional application key. Omit for all apps.'),
1118
+ ip: string('Optional IPv4 address or CIDR the rule applies to. Omit for any IP.'),
1119
+ pathPattern: string('Route pattern, for example /api/auth/login or /api/. A rule needs a route, an IP, or both.'),
1120
+ pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'], description: 'Path matching mode. Defaults to prefix.' },
1121
+ method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], description: 'HTTP method scope. Defaults to ALL.' },
1122
+ keyBy: { type: 'string', enum: ['ip', 'global', 'header', 'cookie'], description: 'What each bucket counts. Defaults to ip.' },
1123
+ keyName: string('Header or cookie name when keyBy is header or cookie, for example x-api-key or session.'),
1124
+ limit: number('Requests allowed per window.', { minimum: 1, maximum: 1000000 }),
1125
+ windowSeconds: number('Window length in seconds (1 to 86400).', { minimum: 1, maximum: 86400 }),
1126
+ duration: string('Optional rule lifetime, for example 2h or 7d. Omit for no expiry.'),
1127
+ expiresAt: string('Optional expiry time as ISO 8601 instead of duration.'),
1128
+ status: { type: 'string', enum: ['active', 'disabled'], description: 'Create it disabled to stage it. Defaults to active.' },
1129
+ ...environmentInput,
1130
+ ...confirmSchema,
1131
+ }, ['limit', 'windowSeconds', 'confirm', 'reason']),
1132
+ },
1133
+ {
1134
+ name: 'securenow_rate_limit_update',
1135
+ title: 'Update Rate Limit',
1136
+ description: 'Change a rate-limit rule: enable or disable it (status), or change its limit, window, route, method, key or expiry. Only the fields you pass change. Write action; requires confirmation.',
1137
+ scope: 'rate_limits:write',
1138
+ readOnly: false,
1139
+ confirm: true,
1140
+ method: 'PUT',
1141
+ endpoint: '/rate-limits/:id',
1142
+ pathParams: ['id'],
1143
+ bodyFields: ['name', 'status', 'limit', 'windowSeconds', 'pathPattern', 'pathMatchMode', 'method', 'keyBy', 'keyName', 'ip', 'duration', 'expiresAt', 'reason'],
1144
+ inputSchema: objectSchema({
1145
+ id: string('Rate-limit rule id.'),
1146
+ name: string('Optional new name.'),
1147
+ status: { type: 'string', enum: ['active', 'disabled'], description: 'active enables the rule, disabled stops enforcing it.' },
1148
+ limit: number('New requests-per-window limit.', { minimum: 1, maximum: 1000000 }),
1149
+ windowSeconds: number('New window length in seconds.', { minimum: 1, maximum: 86400 }),
1150
+ pathPattern: string('New route pattern.'),
1151
+ pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'] },
1152
+ method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] },
1153
+ keyBy: { type: 'string', enum: ['ip', 'global', 'header', 'cookie'] },
1154
+ keyName: string('Header or cookie name for keyBy header/cookie.'),
1155
+ ip: string('New IPv4 address or CIDR filter ("" for any IP).'),
1156
+ duration: string('New lifetime from now, for example 2h.'),
1157
+ expiresAt: string('New expiry as ISO 8601.'),
1158
+ ...confirmSchema,
1159
+ }, ['id', 'confirm', 'reason']),
1160
+ },
1161
+ {
1162
+ name: 'securenow_firewall_hits',
1163
+ title: 'Firewall Hits',
1164
+ description: 'Who the firewall actually stopped: blocks (403), rate limits (429) and challenges, totalled per rule and per IP, plus the most recent hits. Use it to check that a new block or rate limit is working.',
1165
+ scope: 'firewall:read',
1166
+ readOnly: true,
1167
+ method: 'GET',
1168
+ endpoint: '/firewall/hits',
1169
+ queryFields: ['appKey', 'environment', 'action', 'source', 'ruleId', 'ip', 'since', 'from', 'to', 'limit'],
1170
+ inputSchema: objectSchema({
1171
+ appKey: string('Optional application key.'),
1172
+ action: { type: 'string', enum: ['blocked', 'rate_limited', 'captcha_challenged', 'captcha_passed', 'captcha_failed', 'allowed'], description: 'Optional action filter.' },
1173
+ source: { type: 'string', enum: ['blocklist', 'allowlist', 'rate_limit', 'challenge'], description: 'Optional source filter.' },
1174
+ ruleId: string('Optional rule id (rate limit, scoped block or challenge).'),
1175
+ ip: string('Optional client IP.'),
1176
+ since: string('Look-back window, for example 30m, 6h or 7d (default 24h, max 30d).'),
1177
+ from: string('Optional start time as ISO 8601 (instead of since).'),
1178
+ to: string('Optional end time as ISO 8601.'),
1179
+ limit: number('Recent hits to return.', { minimum: 1, maximum: 200 }),
1180
+ ...environmentInput,
1181
+ }),
1182
+ },
1183
+ {
1184
+ name: 'securenow_challenge_list',
1185
+ title: 'List Challenges',
1186
+ description: 'List proof-of-work challenge rules (an invisible check browsers pass and simple bots do not).',
1187
+ scope: 'challenges:read',
1188
+ readOnly: true,
1189
+ method: 'GET',
1190
+ endpoint: '/challenges',
1191
+ queryFields: ['status', 'appKey', 'environment'],
1192
+ inputSchema: objectSchema({
1193
+ status: string('Optional status filter: active or disabled.'),
1194
+ appKey: string('Optional application key.'),
1195
+ ...environmentInput,
1196
+ }),
1197
+ },
1198
+ {
1199
+ name: 'securenow_challenge_create',
1200
+ title: 'Create Challenge',
1201
+ description: 'Serve a proof-of-work challenge on a route (optionally for one IP/CIDR and method). Browsers solve it invisibly and keep a clearance cookie; clients that cannot solve it never reach the route. Write action; requires confirmation.',
1202
+ scope: 'challenges:write',
1203
+ readOnly: false,
1204
+ confirm: true,
1205
+ method: 'POST',
1206
+ endpoint: '/challenges',
1207
+ bodyFields: ['name', 'appKey', 'environment', 'ip', 'pathPattern', 'pathMatchMode', 'method', 'difficulty', 'clearanceTtlSeconds', 'duration', 'status', 'reason'],
1208
+ inputSchema: objectSchema({
1209
+ name: string('Optional rule name.'),
1210
+ appKey: string('Optional application key. Omit for all apps.'),
1211
+ ip: string('Optional IPv4 address or CIDR. Omit for any IP.'),
1212
+ pathPattern: string('Route pattern to protect, for example /api/signup.'),
1213
+ pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'] },
1214
+ method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] },
1215
+ difficulty: number('Proof-of-work difficulty bits (4 to 28, default 14).', { minimum: 4, maximum: 28 }),
1216
+ clearanceTtlSeconds: number('How long a solved challenge stays valid, in seconds (default 1800).', { minimum: 60 }),
1217
+ duration: string('Optional rule lifetime, for example 24h.'),
1218
+ status: { type: 'string', enum: ['active', 'disabled'] },
1219
+ ...environmentInput,
1220
+ ...confirmSchema,
1221
+ }, ['confirm', 'reason']),
1222
+ },
1223
+ {
1224
+ name: 'securenow_challenge_update',
1225
+ title: 'Update Challenge',
1226
+ description: 'Enable/disable a challenge rule or change its route, difficulty or expiry. Write action; requires confirmation.',
1227
+ scope: 'challenges:write',
1228
+ readOnly: false,
1229
+ confirm: true,
1230
+ method: 'PUT',
1231
+ endpoint: '/challenges/:id',
1232
+ pathParams: ['id'],
1233
+ bodyFields: ['name', 'status', 'pathPattern', 'pathMatchMode', 'method', 'ip', 'difficulty', 'clearanceTtlSeconds', 'duration', 'reason'],
1234
+ inputSchema: objectSchema({
1235
+ id: string('Challenge rule id.'),
1236
+ name: string('Optional new name.'),
1237
+ status: { type: 'string', enum: ['active', 'disabled'] },
1238
+ pathPattern: string('New route pattern.'),
1239
+ pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'] },
1240
+ method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] },
1241
+ ip: string('New IPv4 address or CIDR filter.'),
1242
+ difficulty: number('New difficulty bits.', { minimum: 4, maximum: 28 }),
1243
+ clearanceTtlSeconds: number('New clearance lifetime in seconds.', { minimum: 60 }),
1244
+ duration: string('New lifetime from now, for example 24h.'),
1245
+ ...confirmSchema,
1246
+ }, ['id', 'confirm', 'reason']),
1247
+ },
1248
+ {
1249
+ name: 'securenow_challenge_remove',
1250
+ title: 'Remove Challenge',
1251
+ description: 'Remove a proof-of-work challenge rule. Write action; requires confirmation.',
1252
+ scope: 'challenges:write',
1253
+ readOnly: false,
1254
+ confirm: true,
1255
+ method: 'DELETE',
1256
+ endpoint: '/challenges/:id',
1257
+ pathParams: ['id'],
1258
+ bodyFields: ['reason'],
1259
+ inputSchema: objectSchema({
1260
+ id: string('Challenge rule id.'),
1261
+ ...confirmSchema,
1262
+ }, ['id', 'confirm', 'reason']),
1263
+ },
1075
1264
  {
1076
1265
  name: 'securenow_rate_limit_parse',
1077
1266
  title: 'Parse Rate Limit Text',
@@ -1105,12 +1294,18 @@ const TOOLS = [
1105
1294
  {
1106
1295
  name: 'securenow_alert_rules_list',
1107
1296
  title: 'List Alert Rules',
1108
- description: 'List alert rules, including system rules and app scope.',
1297
+ description: 'List detection rules as compact summaries: name, mode (test or prod), status, severity, schedule, throttle, app scope and trigger stats. The detection SQL is left out; securenow_alert_rule_get returns one rule in full.',
1109
1298
  scope: 'alerts:read',
1110
1299
  readOnly: true,
1111
1300
  method: 'GET',
1112
1301
  endpoint: '/alert-rules',
1113
- inputSchema: objectSchema({}),
1302
+ queryFields: ['status', 'mode', 'isSystem', 'active'],
1303
+ inputSchema: objectSchema({
1304
+ status: string('Optional status filter: Active, Disabled or Paused.'),
1305
+ mode: string('Optional mode filter: test or prod.'),
1306
+ isSystem: boolean('true lists only template rules, false only your own.'),
1307
+ active: boolean('true lists only rules with status Active.'),
1308
+ }),
1114
1309
  },
1115
1310
  {
1116
1311
  name: 'securenow_alert_rule_get',
@@ -1167,7 +1362,7 @@ const TOOLS = [
1167
1362
  bodyFields: ['version', 'reason'],
1168
1363
  inputSchema: objectSchema({
1169
1364
  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 }),
1365
+ version: number('Version number to restore, as listed by securenow_alert_rule_versions.', { minimum: 1 }),
1171
1366
  ...confirmSchema,
1172
1367
  }, ['id', 'version', 'confirm', 'reason']),
1173
1368
  },
@@ -1257,7 +1452,7 @@ const TOOLS = [
1257
1452
  ],
1258
1453
  fixedBody: { auditSource: 'mcp' },
1259
1454
  inputSchema: objectSchema({
1260
- id: string('Alert rule id. Fetch with securenow_alert_rule_get first to get ruleVersion and instantHash.'),
1455
+ id: string('Alert rule id.'),
1261
1456
  operations: {
1262
1457
  type: 'array',
1263
1458
  items: { type: 'object', additionalProperties: true },
@@ -1341,7 +1536,7 @@ const TOOLS = [
1341
1536
  ],
1342
1537
  fixedBody: { auditSource: 'mcp' },
1343
1538
  inputSchema: objectSchema({
1344
- id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1539
+ id: string('Alert rule id.'),
1345
1540
  operations: {
1346
1541
  type: 'array',
1347
1542
  items: { type: 'object', additionalProperties: true },
@@ -1400,7 +1595,7 @@ const TOOLS = [
1400
1595
  ],
1401
1596
  fixedBody: { auditSource: 'mcp', dryRun: true },
1402
1597
  inputSchema: objectSchema({
1403
- id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1598
+ id: string('Alert rule id.'),
1404
1599
  operations: {
1405
1600
  type: 'array',
1406
1601
  items: { type: 'object', additionalProperties: true },
@@ -1456,7 +1651,7 @@ const TOOLS = [
1456
1651
  ],
1457
1652
  fixedBody: { auditSource: 'mcp', dryRun: true },
1458
1653
  inputSchema: objectSchema({
1459
- id: string('Alert rule id. Fetch with securenow_alert_rule_conditions_get first to get ruleVersion and instantHash.'),
1654
+ id: string('Alert rule id.'),
1460
1655
  operations: {
1461
1656
  type: 'array',
1462
1657
  items: { type: 'object', additionalProperties: true },
@@ -1692,11 +1887,12 @@ const TOOLS = [
1692
1887
  confirm: true,
1693
1888
  method: 'POST',
1694
1889
  endpoint: '/blocklist',
1695
- bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment', 'pathPattern', 'pathMatchMode', 'method'],
1890
+ bodyFields: ['ip', 'reason', 'expiresAt', 'duration', 'metadata', 'appKey', 'environment', 'pathPattern', 'pathMatchMode', 'method'],
1696
1891
  inputSchema: objectSchema({
1697
1892
  ip: string('IPv4 address or CIDR.'),
1698
1893
  reason: string('Reason for blocking.'),
1699
1894
  expiresAt: string('Optional expiry time as ISO 8601.'),
1895
+ duration: string('Optional block lifetime instead of expiresAt, for example 15m, 2h or 7d.'),
1700
1896
  metadata: { type: 'object', additionalProperties: true, description: 'Optional metadata.' },
1701
1897
  appKey: string('Optional application key to scope this block. Omit for all apps.'),
1702
1898
  pathPattern: string('Optional route/path pattern, for example /admin*. Omit to block all routes.'),
@@ -1766,7 +1962,7 @@ const TOOLS = [
1766
1962
  {
1767
1963
  name: 'securenow_allowlist_add',
1768
1964
  title: 'Add Restrictive Allowlist IP',
1769
- 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.',
1965
+ description: 'Dangerous production action: add an IP/CIDR to the restrictive allowlist. Once active, allowlist mode blocks every IP except listed entries, so it is not a way to mark an IP trusted or suppress false positives (trusted IPs are a separate list). The server rejects the call unless allowlistDenyAllApproved is true.',
1770
1966
  scope: 'allowlist:write',
1771
1967
  readOnly: false,
1772
1968
  confirm: true,
@@ -1781,7 +1977,7 @@ const TOOLS = [
1781
1977
  expiresAt: string('Optional expiry time as ISO 8601.'),
1782
1978
  applicationsAll: boolean('Apply to all applications.'),
1783
1979
  applicationKeys: arrayOfStrings('Application keys to scope this allowlist entry to.'),
1784
- 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.'),
1980
+ allowlistDenyAllApproved: boolean('Confirms the deny-by-default behavior: once the allowlist has entries, every unlisted IP is blocked. Must be true.'),
1785
1981
  ...environmentInput,
1786
1982
  ...confirmSchema,
1787
1983
  }, ['ip', 'confirm', 'reason', 'allowlistDenyAllApproved']),
@@ -1871,6 +2067,11 @@ const DESTRUCTIVE_TOOLS = new Set([
1871
2067
  'securenow_automation_rule_delete',
1872
2068
  'securenow_rate_limit_remove',
1873
2069
  'securenow_rate_limit_create_from_text',
2070
+ 'securenow_rate_limit_create',
2071
+ 'securenow_rate_limit_update',
2072
+ 'securenow_challenge_create',
2073
+ 'securenow_challenge_update',
2074
+ 'securenow_challenge_remove',
1874
2075
  'securenow_alert_rule_rollback',
1875
2076
  'securenow_alert_rule_update',
1876
2077
  'securenow_alert_rule_query_update',
@@ -2294,6 +2495,8 @@ function mcpToolDefinition(tool) {
2294
2495
  description: tool.description,
2295
2496
  inputSchema: tool.inputSchema || objectSchema({}),
2296
2497
  annotations: {
2498
+ // Directory listings (Claude's connector review) read the title here too.
2499
+ title: tool.title,
2297
2500
  readOnlyHint: !!tool.readOnly,
2298
2501
  destructiveHint: !tool.readOnly && !!tool.destructive,
2299
2502
  idempotentHint: !!tool.readOnly,
@@ -2372,6 +2575,209 @@ function buildApiRequest(tool, rawArgs = {}) {
2372
2575
  };
2373
2576
  }
2374
2577
 
2578
+ // Async tools (tool.poll) return a job id; follow it with the status tool until
2579
+ // the job settles or the wait runs out. fetchStatus(request) performs one API call.
2580
+ async function pollToolResult(tool, first, fetchStatus) {
2581
+ const spec = tool.poll;
2582
+ const id = spec && first && first[spec.idField];
2583
+ if (!id || !spec.pending.includes(first.status)) return first;
2584
+ const statusTool = getTool(spec.tool);
2585
+ const deadline = Date.now() + spec.timeoutMs;
2586
+ let last = first;
2587
+ while (Date.now() < deadline) {
2588
+ await new Promise((resolve) => setTimeout(resolve, spec.intervalMs));
2589
+ last = await fetchStatus(buildApiRequest(statusTool, { [spec.idField]: id }));
2590
+ if (!last || !spec.pending.includes(last.status)) return last;
2591
+ }
2592
+ if (last && last.error) {
2593
+ return { ...last, message: `Not finished after ${Math.round(spec.timeoutMs / 1000)} seconds, and the last attempt failed: ${last.error}. It may be retrying; ${statusTool.name} with ${spec.idField} ${id} shows its state. A narrower question (one app, a shorter window) usually avoids this.` };
2594
+ }
2595
+ return { ...last, message: `Still running. Call ${statusTool.name} with ${spec.idField} ${id} to get the result.` };
2596
+ }
2597
+
2598
+ // Tool results land in a chat context, so very large API responses (a 500-IP
2599
+ // investigation, every saved rule) are shrunk: long arrays keep their first
2600
+ // items and long strings are cut, with a note saying how to narrow the call.
2601
+ const MAX_RESULT_CHARS = 60000;
2602
+ const TRIM_STEPS = [[50, 4000], [20, 2000], [10, 1000], [5, 400], [3, 200], [1, 100]];
2603
+ const TRUNCATED_NOTE = 'This result was shortened to fit the conversation. Use filters, a smaller limit or a get tool for the full details.';
2604
+
2605
+ function trimValue(value, maxItems, maxString) {
2606
+ if (typeof value === 'string') {
2607
+ return value.length > maxString ? `${value.slice(0, maxString)}… [${value.length - maxString} more characters]` : value;
2608
+ }
2609
+ if (Array.isArray(value)) {
2610
+ const kept = value.slice(0, maxItems).map((item) => trimValue(item, maxItems, maxString));
2611
+ if (value.length > maxItems) kept.push(`… ${value.length - maxItems} more items not shown`);
2612
+ return kept;
2613
+ }
2614
+ if (value && typeof value === 'object') {
2615
+ const out = {};
2616
+ for (const [key, item] of Object.entries(value)) out[key] = trimValue(item, maxItems, maxString);
2617
+ return out;
2618
+ }
2619
+ return value;
2620
+ }
2621
+
2622
+ // Tool results are sent as compact JSON, so sizes are measured that way.
2623
+ const compactJson = (value) => JSON.stringify(value);
2624
+
2625
+ function fitToolResult(value, maxChars = MAX_RESULT_CHARS) {
2626
+ if (value == null || typeof value !== 'object') return value;
2627
+ if (compactJson(value).length <= maxChars) return value;
2628
+ for (const [maxItems, maxString] of TRIM_STEPS) {
2629
+ const trimmed = trimValue(value, maxItems, maxString);
2630
+ const wrapped = Array.isArray(trimmed) ? { _truncated: TRUNCATED_NOTE, result: trimmed } : { _truncated: TRUNCATED_NOTE, ...trimmed };
2631
+ if (compactJson(wrapped).length <= maxChars) return wrapped;
2632
+ }
2633
+ return { _truncated: TRUNCATED_NOTE, preview: compactJson(value).slice(0, maxChars) };
2634
+ }
2635
+
2636
+ // List tools return compact rows so a page of results fits in a chat context;
2637
+ // the matching get tools return one record in full. Each summarizer keeps the
2638
+ // response envelope (pagination, summary) and only reshapes the rows.
2639
+ const clip = (value, max = 300) => (typeof value === 'string' && value.length > max ? `${value.slice(0, max)}…` : value);
2640
+ const firstN = (list, n) => (Array.isArray(list) ? list.slice(0, n) : []);
2641
+
2642
+ function summarizeNotification(n) {
2643
+ const investigations = Array.isArray(n.ipInvestigations) ? n.ipInvestigations : [];
2644
+ return {
2645
+ id: n._id || n.id,
2646
+ title: n.title,
2647
+ detectionName: n.alertContext?.detectionName || null,
2648
+ alertRuleId: n.alertRuleId,
2649
+ severity: n.severity,
2650
+ status: n.status,
2651
+ testMode: n.testMode === true,
2652
+ applications: n.applications,
2653
+ occurrences: n.occurrences,
2654
+ firstOccurredAt: n.firstOccurredAt,
2655
+ lastOccurredAt: n.lastOccurredAt,
2656
+ createdAt: n.createdAt,
2657
+ readAt: n.readAt || null,
2658
+ primaryIp: n.primaryIp || null,
2659
+ ipCount: Math.max(n.ipSummary?.totalCount || 0, investigations.length, (n.relatedIps || []).length + (n.primaryIp ? 1 : 0)),
2660
+ stormSignal: n.ipSummary?.stormSignal === true,
2661
+ resultCount: n.resultCount,
2662
+ agent: n.agentState ? { status: n.agentState.status, summary: clip(n.agentState.summary, 200), confidence: n.agentState.confidence } : null,
2663
+ proposedActions: firstN(n.proposedActions, 3).map((a) => ({
2664
+ actionKey: a.actionKey, type: a.type, status: a.status, confidence: a.confidence, ips: firstN(a.scope?.ips, 5),
2665
+ })),
2666
+ ips: firstN(investigations, 5).map((i) => ({ ip: i.ip, status: i.status, riskScore: i.riskScore, hitCount: i.hitCount })),
2667
+ };
2668
+ }
2669
+
2670
+ function summarizeRule(r) {
2671
+ return {
2672
+ id: r.id || r._id,
2673
+ name: r.name,
2674
+ detection: clip(r.queryMappingId?.nlpQuery || r.description, 140) || null,
2675
+ mode: r.mode,
2676
+ status: r.status,
2677
+ severity: r.severity,
2678
+ isSystem: r.isSystem === true,
2679
+ executionMode: r.executionMode,
2680
+ applicationsAll: r.applicationsAll,
2681
+ applications: r.applications,
2682
+ schedule: r.schedule?.enabled === false ? null : r.schedule?.cronExpression || null,
2683
+ throttleMinutes: r.throttle?.enabled === false ? 0 : r.throttle?.minutes ?? null,
2684
+ instant: r.instant ? { enabled: r.instant.enabled === true, block: r.instant.block === true } : null,
2685
+ exclusions: Array.isArray(r.exclusions) ? r.exclusions.length : 0,
2686
+ stats: r.stats ? {
2687
+ triggerCount: r.stats.triggerCount, executionCount: r.stats.executionCount,
2688
+ lastTriggeredAt: r.stats.lastTriggeredAt || null, lastExecutedAt: r.stats.lastExecutedAt || null,
2689
+ } : null,
2690
+ currentVersion: r.currentVersion,
2691
+ };
2692
+ }
2693
+
2694
+ function summarizeTask(t) {
2695
+ const proofs = t.proofs || {};
2696
+ return {
2697
+ id: t.id,
2698
+ kind: t.kind || 'ip',
2699
+ actionKey: t.actionKey,
2700
+ actionType: t.actionType,
2701
+ notificationId: t.notificationId,
2702
+ ip: t.ip,
2703
+ alertRuleId: t.alertRuleId,
2704
+ title: t.title,
2705
+ detectionName: t.detectionName,
2706
+ severity: t.severity,
2707
+ applications: t.applications,
2708
+ hitCount: t.hitCount,
2709
+ firstSeenAt: t.firstSeenAt,
2710
+ lastSeenAt: t.lastSeenAt,
2711
+ urgencyScore: t.urgencyScore,
2712
+ cluster: t.cluster ? { clusterId: t.cluster.clusterId, label: t.cluster.label, ipCount: t.cluster.ipCount, riskScore: t.cluster.riskScore } : null,
2713
+ aiDecision: t.aiDecision ? {
2714
+ recommendation: t.aiDecision.recommendation, label: t.aiDecision.label, riskScore: t.aiDecision.riskScore,
2715
+ confidence: t.aiDecision.confidence, completedAt: t.aiDecision.completedAt, reason: clip(t.aiDecision.reason || t.aiDecision.summary, 300),
2716
+ } : null,
2717
+ proofs: {
2718
+ paths: firstN(proofs.paths, 5), methods: firstN(proofs.methods, 5), statusCodes: firstN(proofs.statusCodes, 5),
2719
+ userAgents: firstN(proofs.userAgents, 3).map((ua) => clip(ua, 120)), traceIds: firstN(proofs.traceIds, 5),
2720
+ rootCauseKey: proofs.rootCauseKey, evidenceShape: proofs.evidenceShape,
2721
+ },
2722
+ };
2723
+ }
2724
+
2725
+ function summarizeApp(a) {
2726
+ return {
2727
+ id: a._id || a.id,
2728
+ key: a.key,
2729
+ name: a.name,
2730
+ status: a.status,
2731
+ hosts: a.hosts,
2732
+ firewallEnabled: a.firewallEnabled,
2733
+ tracingEnabled: a.tracingEnabled,
2734
+ loggingEnabled: a.loggingEnabled,
2735
+ lastTelemetryAt: a.lastTelemetryAt || null,
2736
+ lastTelemetryEnvironment: a.lastTelemetryEnvironment || null,
2737
+ lastTraceAt: a.lastTraceAt || null,
2738
+ lastTraceEnvironment: a.lastTraceEnvironment || null,
2739
+ lastLogAt: a.lastLogAt || null,
2740
+ lastLogEnvironment: a.lastLogEnvironment || null,
2741
+ };
2742
+ }
2743
+
2744
+ function summarizeRateLimit(r) {
2745
+ const expired = !!r.expiresAt && new Date(r.expiresAt).getTime() < Date.now();
2746
+ return { ...r, metadata: undefined, effectiveStatus: r.status === 'active' && expired ? 'expired' : r.status };
2747
+ }
2748
+
2749
+ const SUMMARIZERS = {
2750
+ securenow_notifications_list: (res) => (Array.isArray(res?.notifications) ? { ...res, notifications: res.notifications.map(summarizeNotification) } : res),
2751
+ securenow_alert_rules_list: (res) => {
2752
+ if (!Array.isArray(res?.alertRules)) return res;
2753
+ const rules = res.alertRules;
2754
+ const count = (fn) => rules.filter(fn).length;
2755
+ const counts = {
2756
+ total: rules.length,
2757
+ active: count((r) => r.status === 'Active'),
2758
+ prodActive: count((r) => r.status === 'Active' && r.mode === 'prod'),
2759
+ testActive: count((r) => r.status === 'Active' && r.mode === 'test'),
2760
+ paused: count((r) => r.status === 'Paused'),
2761
+ disabled: count((r) => r.status === 'Disabled'),
2762
+ neverTriggered: count((r) => !r.stats?.triggerCount),
2763
+ };
2764
+ return { counts, ...res, alertRules: rules.map(summarizeRule) };
2765
+ },
2766
+ securenow_human_actions_list: (res) => (Array.isArray(res?.tasks) ? { ...res, tasks: res.tasks.map(summarizeTask) } : res),
2767
+ securenow_apps_list: (res) => (Array.isArray(res?.applications) ? { ...res, applications: res.applications.map(summarizeApp) } : res),
2768
+ securenow_rate_limit_list: (res) => (Array.isArray(res?.rateLimits) ? { ...res, rateLimits: res.rateLimits.map(summarizeRateLimit) } : res),
2769
+ };
2770
+
2771
+ function summarizeToolResult(tool, result) {
2772
+ const summarize = tool && SUMMARIZERS[tool.name];
2773
+ if (!summarize || result == null || typeof result !== 'object') return result;
2774
+ try {
2775
+ return summarize(result);
2776
+ } catch (_) {
2777
+ return result;
2778
+ }
2779
+ }
2780
+
2375
2781
  function readStaticResource(resource) {
2376
2782
  const filepath = path.resolve(__dirname, resource.file);
2377
2783
  return fs.readFileSync(filepath, 'utf8');
@@ -2400,6 +2806,9 @@ module.exports = {
2400
2806
  promptMessages,
2401
2807
  assertConfirmed,
2402
2808
  buildApiRequest,
2809
+ pollToolResult,
2810
+ fitToolResult,
2811
+ summarizeToolResult,
2403
2812
  resourceContent,
2404
2813
  jsonText,
2405
2814
  maskSecret,