securenow 7.7.6 → 7.7.8

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/NPM_README.md CHANGED
@@ -186,7 +186,7 @@ codex mcp add securenow -- npx securenow mcp
186
186
  npx -p securenow securenow-mcp
187
187
  ```
188
188
 
189
- The MCP surface exposes tools for applications, traces, logs, firewall, IP intelligence, forensics, notifications, blocklist, allowlist, trusted IPs, and docs-backed prompts/resources. Write actions require `confirm:true` and a reason.
189
+ The MCP surface exposes tools for applications, traces, logs, firewall, IP intelligence, forensics, notifications, blocklist, allowlist, trusted IPs, and docs-backed prompts/resources. Write actions require `confirm:true` and a reason. Use `securenow_blocklist_unblock` to stop firewall enforcement while keeping the block report/history; `securenow_blocklist_remove` is a compatibility alias.
190
190
 
191
191
  For hosted clients, SecureNow can expose the same surface at `https://api.securenow.ai/mcp`. The hosted endpoint uses the same API authentication and scope checks as the rest of SecureNow.
192
192
 
@@ -272,7 +272,9 @@ npx securenow ip traces 203.0.113.42
272
272
  # Manage the blocklist
273
273
  npx securenow blocklist
274
274
  npx securenow blocklist add 203.0.113.42 --reason "Brute force scanner"
275
- npx securenow blocklist remove <id>
275
+ npx securenow blocklist unblock <id> --reason "Reviewed as safe"
276
+ npx securenow blocklist remove <id> # compatibility alias
277
+ npx securenow blocklist list --status removed
276
278
  npx securenow blocklist stats
277
279
 
278
280
  # Manage trusted IPs
@@ -520,7 +522,8 @@ npx securenow logs --json --level error | jq '.logs'
520
522
  | | `automation dry-run <id>` | Preview automation matches without writing blocks |
521
523
  | | `automation execute <id> --yes` | Run an automation rule now |
522
524
  | | `blocklist add <ip>` | Block IP |
523
- | | `blocklist remove <id>` | Unblock IP |
525
+ | | `blocklist unblock <id>` | Unblock IP and retain report/history |
526
+ | | `blocklist remove <id>` | Compatibility alias for unblock |
524
527
  | | `blocklist stats` | Block stats |
525
528
  | | `allowlist` | Allowed IPs (restrict-mode) |
526
529
  | | `allowlist add <ip>` | Allow IP (`--label`, `--reason`) |
package/README.md CHANGED
@@ -191,6 +191,7 @@ npx securenow doctor # diagnose config + connectivity
191
191
  # Security
192
192
  npx securenow firewall status --env production
193
193
  npx securenow blocklist add 1.2.3.4 --reason "scanner"
194
+ npx securenow blocklist unblock <id> --reason "reviewed safe"
194
195
  npx securenow fp ai-fill --description "Stripe webhook POST /api/stripe/webhook"
195
196
 
196
197
  # Telemetry from shell (no SDK boot)
@@ -357,6 +358,7 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
357
358
  |---|---|
358
359
  | `securenow blocklist` | List blocked IPs |
359
360
  | `securenow blocklist add <ip> [--reason ...]` | Block an IP |
361
+ | `securenow blocklist unblock <id> [--reason ...]` | Stop enforcement and keep block history |
360
362
  | `securenow allowlist add <ip>` | Allow an IP (restrict-mode) |
361
363
  | `securenow trusted add <ip>` | Mark an IP as trusted |
362
364
 
package/SKILL-API.md CHANGED
@@ -66,9 +66,25 @@ npx securenow api-key set snk_live_abc123...
66
66
 
67
67
  Both paths write the key to `.securenow/credentials.json` (auto-gitignored) and the firewall activates on next start. For production, run `npx securenow credentials runtime --env production` and mount/copy the tokenless file as `.securenow/credentials.json`.
68
68
 
69
- The firewall syncs your blocklist and enforces it on every request — zero code changes.
70
-
71
- ---
69
+ The firewall syncs your blocklist and enforces it on every request — zero code changes.
70
+
71
+ Blocklist unblocks are audit-preserving: dashboard/API/CLI/MCP unblock actions
72
+ mark the active block as `removed`, invalidate firewall sync, clear expiry to
73
+ avoid TTL deletion, and retain block reports/history for future review or
74
+ reblock context.
75
+
76
+ For near-realtime propagation after a block/unblock, set
77
+ `SECURENOW_FIREWALL_VERSION_INTERVAL=1` or `2` in the protected app. The SDK
78
+ polls `/firewall/sync` with ETag/304, so unchanged checks are lightweight; keep
79
+ `SECURENOW_FIREWALL_SYNC_INTERVAL` high as a safety-net full refresh.
80
+
81
+ Default automation is active for new and existing customers. The API
82
+ idempotently provisions risk-score rules for all apps/environments:
83
+ `riskScore >= 95` blocks for 7 days, `90-94` for 72h, and `85-89` for 24h.
84
+ Run `securenow automation defaults --yes` or the API backfill script when an
85
+ operator needs to ensure those defaults immediately.
86
+
87
+ ---
72
88
 
73
89
  ## Import Map
74
90
 
package/SKILL-CLI.md CHANGED
@@ -300,13 +300,21 @@ securenow firewall test-ip <ip> --app <key> --env local # check if IP would be
300
300
  securenow blocklist # list blocked IPs
301
301
  securenow blocklist list
302
302
  securenow blocklist add <ip> --app <key> --env production --reason "Brute force"
303
- securenow blocklist remove <id>
303
+ securenow blocklist unblock <id> --reason "False alarm after review"
304
+ securenow blocklist remove <id> # compatibility alias for unblock
305
+ securenow blocklist list --status removed # audit retained unblocks
304
306
  securenow blocklist stats # block counts, top reasons
305
307
  ```
306
308
 
309
+ Unblock stops firewall enforcement but preserves the block report, history, and
310
+ unblock audit fields. Reblocking the same IP later creates a fresh active block
311
+ without erasing the previous investigation trail.
312
+
307
313
  MCP exposes legacy pending-block cleanup separately from current Requires Human
308
314
  work:
309
315
 
316
+ - `securenow_blocklist_unblock`
317
+ - `securenow_blocklist_remove` (compatibility alias for unblock)
310
318
  - `securenow_blocklist_pending_list`
311
319
  - `securenow_blocklist_pending_approve`
312
320
  - `securenow_blocklist_pending_reject`
@@ -318,15 +326,17 @@ work:
318
326
 
319
327
  ```bash
320
328
  securenow automation # list blocklist automation rules
329
+ securenow automation defaults --yes # ensure built-in default risk-score rules
321
330
  securenow automation show <id>
322
331
  securenow automation dry-run <id> --limit 500
323
332
  securenow automation execute <id> --yes
324
333
  ```
325
334
 
326
- For default automation, prefer production-scoped `riskScore` policies:
327
- `riskScore >= 90` for autonomous AI malicious blocking, `riskScore >= 80`
328
- plus `alertTag in xss` for short-lived XSS blocks, and keep raw SecureNow IPDB
329
- confidence as supporting reputation evidence.
335
+ Built-in default automation is enabled by default for all apps/environments and
336
+ uses the canonical `riskScore`: 95-100 blocks for 7 days, 90-94 blocks for 72h,
337
+ 85-89 blocks for 24h, and scores below 85 stay in investigation/review unless
338
+ the customer adds a stricter custom rule. Raw SecureNow IPDB confidence remains
339
+ supporting evidence, not the primary automation score.
330
340
 
331
341
  ### Allowlist — Restrict to Known IPs
332
342
 
package/cli/automation.js CHANGED
@@ -249,6 +249,34 @@ async function execute(args, flags) {
249
249
  }
250
250
  }
251
251
 
252
+ async function defaults(args, flags) {
253
+ requireAuth();
254
+ if ((flags['force-enable'] || flags.force) && !flags.yes) {
255
+ const ok = await ui.confirm('Re-enable existing SecureNow default automation rules that were disabled?');
256
+ if (!ok) { ui.info('Cancelled'); return; }
257
+ }
258
+
259
+ const s = ui.spinner('Ensuring default automation rules');
260
+ try {
261
+ const data = await api.post('/automation-rules/defaults/ensure', {
262
+ forceEnableExisting: !!(flags['force-enable'] || flags.force),
263
+ });
264
+ s.stop('Default automation rules ensured');
265
+ if (flags.json) { ui.json(data); return; }
266
+
267
+ const result = data.result || {};
268
+ ui.keyValue([
269
+ ['Version', String(result.version ?? '-')],
270
+ ['Created', String(result.created ?? 0)],
271
+ ['Updated', String(result.updated ?? 0)],
272
+ ['Already present', String(result.alreadyProvisioned ?? 0)],
273
+ ]);
274
+ } catch (err) {
275
+ s.fail('Failed to ensure default automation rules');
276
+ throw err;
277
+ }
278
+ }
279
+
252
280
  async function remove(args, flags) {
253
281
  requireAuth();
254
282
  const id = args[0];
@@ -272,4 +300,4 @@ async function remove(args, flags) {
272
300
  }
273
301
  }
274
302
 
275
- module.exports = { list, show, create, update, dryRun, execute, remove };
303
+ module.exports = { list, show, create, update, dryRun, execute, defaults, remove };
package/cli/firewall.js CHANGED
@@ -36,10 +36,11 @@ async function status(args, flags) {
36
36
  ['App scope', appKey || 'all apps'],
37
37
  ['Environment', data.environment || environment],
38
38
  ['Last updated', data.updatedAt || 'unknown'],
39
- ['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
40
- ['Allowlist updated', data.allowlistUpdatedAt || 'never'],
41
- ['Sync TTL', `${data.ttl || 60}s`],
42
- ]);
39
+ ['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
40
+ ['Allowlist updated', data.allowlistUpdatedAt || 'never'],
41
+ ['Rate limit rules', String(data.rateLimitCount ?? 0)],
42
+ ['Sync TTL', `${data.ttl || 60}s`],
43
+ ]);
43
44
  if (data.allowlistCount > 0) {
44
45
  console.log('');
45
46
  console.log(` ${ui.c.yellow('!')} Allowlist is active — only ${data.allowlistCount} IP(s) can reach your app`);
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ const { api, requireAuth } = require('./client');
4
+ const config = require('./config');
5
+ const ui = require('./ui');
6
+
7
+ function resolveApp(flags) {
8
+ return flags.app || config.getDefaultApp();
9
+ }
10
+
11
+ function resolveEnvironment(flags, fallback = null) {
12
+ return flags.env || flags.environment || fallback;
13
+ }
14
+
15
+ function parseWindow(value) {
16
+ if (value == null || value === '') return 60;
17
+ if (/^\d+$/.test(String(value))) return Number(value);
18
+ const raw = String(value).trim().toLowerCase();
19
+ const match = raw.match(/^(\d+)\s*(s|sec|second|seconds|m|min|minute|minutes|h|hr|hour|hours)$/);
20
+ if (!match) {
21
+ ui.error('Window must be seconds or look like 30s, 1m, or 1h.');
22
+ process.exit(1);
23
+ }
24
+ const n = Number(match[1]);
25
+ const unit = match[2][0];
26
+ return unit === 'h' ? n * 3600 : unit === 'm' ? n * 60 : n;
27
+ }
28
+
29
+ function buildQuery(flags) {
30
+ const query = {};
31
+ if (flags.page) query.page = flags.page;
32
+ if (flags.limit) query.limit = flags.limit;
33
+ if (flags.status) query.status = flags.status;
34
+ if (flags.search) query.search = flags.search;
35
+ const appKey = resolveApp(flags);
36
+ if (appKey) query.appKey = appKey;
37
+ const environment = resolveEnvironment(flags, null);
38
+ if (environment) query.environment = environment;
39
+ return query;
40
+ }
41
+
42
+ function describeTarget(rule) {
43
+ const parts = [];
44
+ if (rule.ip) parts.push(rule.ip);
45
+ if (rule.method && rule.method !== 'ALL') parts.push(rule.method);
46
+ if (rule.pathPattern) parts.push(`${rule.pathMatchMode || 'prefix'}:${rule.pathPattern}`);
47
+ return parts.join(' ') || 'all traffic';
48
+ }
49
+
50
+ async function list(args, flags) {
51
+ requireAuth();
52
+ const s = ui.spinner('Fetching rate limits');
53
+ try {
54
+ const data = await api.get('/rate-limits', { query: buildQuery(flags) });
55
+ const rules = data.rateLimits || [];
56
+ s.stop(`Found ${rules.length} rate-limit rule${rules.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
57
+
58
+ if (flags.json) { ui.json(data); return; }
59
+
60
+ console.log('');
61
+ const rows = rules.map((r) => [
62
+ ui.c.dim(ui.truncate(r.id || r._id, 12)),
63
+ r.status === 'active' ? ui.statusBadge('active') : ui.statusBadge(r.status || 'unknown'),
64
+ describeTarget(r),
65
+ `${r.limit}/${r.windowSeconds}s`,
66
+ r.applicationKey || ui.c.dim('all apps'),
67
+ r.environment || ui.c.dim('all envs'),
68
+ r.expiresAt ? new Date(r.expiresAt).toLocaleString() : ui.c.dim('permanent'),
69
+ ui.timeAgo(r.createdAt),
70
+ ]);
71
+ ui.table(['ID', 'Status', 'Target', 'Limit', 'App', 'Env', 'Expires', 'Created'], rows);
72
+ console.log('');
73
+ } catch (err) {
74
+ s.fail('Failed to fetch rate limits');
75
+ throw err;
76
+ }
77
+ }
78
+
79
+ async function show(args, flags) {
80
+ requireAuth();
81
+ const id = args[0];
82
+ if (!id) {
83
+ ui.error('Usage: securenow ratelimit show <id>');
84
+ process.exit(1);
85
+ }
86
+ const s = ui.spinner('Fetching rate limit');
87
+ try {
88
+ const data = await api.get(`/rate-limits/${id}`);
89
+ const r = data.rateLimit;
90
+ s.stop('Rate limit loaded');
91
+ if (flags.json) { ui.json(r); return; }
92
+ console.log('');
93
+ ui.heading(r.name || 'Rate limit');
94
+ console.log('');
95
+ ui.keyValue([
96
+ ['ID', r.id || r._id || id],
97
+ ['Status', r.status || '-'],
98
+ ['Target', describeTarget(r)],
99
+ ['Limit', `${r.limit}/${r.windowSeconds}s`],
100
+ ['Key by', r.keyBy || 'ip'],
101
+ ['App', r.applicationKey || 'all apps'],
102
+ ['Environment', r.environment || 'all envs'],
103
+ ['Reason', r.reason || '-'],
104
+ ['Expires', r.expiresAt || 'permanent'],
105
+ ]);
106
+ console.log('');
107
+ } catch (err) {
108
+ s.fail('Failed to fetch rate limit');
109
+ throw err;
110
+ }
111
+ }
112
+
113
+ async function add(args, flags) {
114
+ requireAuth();
115
+ const ip = args[0] || flags.ip || '';
116
+ const pathPattern = flags.route || flags.path || flags.pattern || '';
117
+
118
+ if (!ip && !pathPattern) {
119
+ ui.error('Provide an IP/CIDR, --route, or both.');
120
+ ui.info('Example: securenow ratelimit add 203.0.113.10 --limit 30 --window 1m --duration 24h');
121
+ process.exit(1);
122
+ }
123
+ if (!flags.limit) {
124
+ ui.error('Pass --limit <count>.');
125
+ process.exit(1);
126
+ }
127
+
128
+ const appKey = resolveApp(flags);
129
+ const environment = resolveEnvironment(flags, 'production');
130
+ const body = {
131
+ ip,
132
+ pathPattern,
133
+ pathMatchMode: flags.mode || flags['path-mode'] || (pathPattern ? 'prefix' : 'prefix'),
134
+ method: flags.method || 'ALL',
135
+ keyBy: flags['key-by'] || flags.keyBy || 'ip',
136
+ limit: Number(flags.limit),
137
+ windowSeconds: parseWindow(flags.window || flags['window-seconds']),
138
+ reason: flags.reason || '',
139
+ name: flags.name || '',
140
+ duration: flags.duration || '',
141
+ expiresAt: flags.expiresAt || flags.expires || '',
142
+ };
143
+ if (appKey) body.appKey = appKey;
144
+ if (environment) body.environment = environment;
145
+
146
+ const s = ui.spinner('Creating rate limit');
147
+ try {
148
+ const data = await api.post('/rate-limits', body);
149
+ s.stop('Rate limit created');
150
+ if (flags.json) { ui.json(data); return; }
151
+ const r = data.rateLimit;
152
+ console.log('');
153
+ console.log(` ${ui.c.green('ACTIVE')} ${describeTarget(r)} at ${r.limit}/${r.windowSeconds}s`);
154
+ console.log(` ${ui.c.dim('ID:')} ${r.id || r._id}`);
155
+ console.log('');
156
+ } catch (err) {
157
+ s.fail('Failed to create rate limit');
158
+ throw err;
159
+ }
160
+ }
161
+
162
+ async function setStatus(args, flags, status) {
163
+ requireAuth();
164
+ const id = args[0];
165
+ if (!id) {
166
+ ui.error(`Usage: securenow ratelimit ${status === 'active' ? 'enable' : 'disable'} <id>`);
167
+ process.exit(1);
168
+ }
169
+ const s = ui.spinner(`${status === 'active' ? 'Enabling' : 'Disabling'} rate limit`);
170
+ try {
171
+ const data = await api.put(`/rate-limits/${id}`, { status });
172
+ s.stop(status === 'active' ? 'Rate limit enabled' : 'Rate limit disabled');
173
+ if (flags.json) ui.json(data);
174
+ } catch (err) {
175
+ s.fail('Failed to update rate limit');
176
+ throw err;
177
+ }
178
+ }
179
+
180
+ async function enable(args, flags) { return setStatus(args, flags, 'active'); }
181
+ async function disable(args, flags) { return setStatus(args, flags, 'disabled'); }
182
+
183
+ async function remove(args, flags) {
184
+ requireAuth();
185
+ const id = args[0];
186
+ if (!id) {
187
+ ui.error('Usage: securenow ratelimit remove <id> [--reason "..."]');
188
+ process.exit(1);
189
+ }
190
+ if (!flags.force && !flags.yes) {
191
+ const ok = await ui.confirm('Remove this rate-limit rule?');
192
+ if (!ok) { ui.info('Cancelled'); return; }
193
+ }
194
+ const s = ui.spinner('Removing rate limit');
195
+ try {
196
+ const opts = flags.reason ? { query: { reason: flags.reason } } : undefined;
197
+ const data = await api.delete(`/rate-limits/${id}`, opts);
198
+ s.stop('Rate limit removed');
199
+ if (flags.json) ui.json(data);
200
+ } catch (err) {
201
+ s.fail('Failed to remove rate limit');
202
+ throw err;
203
+ }
204
+ }
205
+
206
+ async function test(args, flags) {
207
+ requireAuth();
208
+ const ip = args[0] || flags.ip;
209
+ if (!ip) {
210
+ ui.error('Usage: securenow ratelimit test <ip> --path /api/login [--method POST]');
211
+ process.exit(1);
212
+ }
213
+ const query = {
214
+ ip,
215
+ path: flags.path || flags.route || '/',
216
+ method: flags.method || 'GET',
217
+ };
218
+ const appKey = resolveApp(flags);
219
+ if (appKey) query.appKey = appKey;
220
+ const environment = resolveEnvironment(flags, 'production');
221
+ if (environment) query.environment = environment;
222
+
223
+ const s = ui.spinner('Testing rate-limit match');
224
+ try {
225
+ const data = await api.get('/rate-limits/check', { query });
226
+ s.stop('Rate-limit check complete');
227
+ if (flags.json) { ui.json(data); return; }
228
+ console.log('');
229
+ if (data.limited) {
230
+ console.log(` ${ui.c.bold(ui.c.yellow('RATE LIMITED'))} - ${data.matches.length} matching rule${data.matches.length !== 1 ? 's' : ''}`);
231
+ for (const r of data.matches.slice(0, 5)) {
232
+ console.log(` ${ui.c.dim(r.id || r._id)} ${describeTarget(r)} ${r.limit}/${r.windowSeconds}s`);
233
+ }
234
+ } else {
235
+ console.log(` ${ui.c.bold(ui.c.green('NOT LIMITED'))} - no matching rate-limit rules`);
236
+ }
237
+ console.log(` ${ui.c.dim(`App scope: ${appKey || 'all apps'} - Environment: ${data.environment || environment}`)}`);
238
+ console.log('');
239
+ } catch (err) {
240
+ s.fail('Failed to test rate limit');
241
+ throw err;
242
+ }
243
+ }
244
+
245
+ module.exports = { list, show, add, enable, disable, remove, test };
package/cli/security.js CHANGED
@@ -339,6 +339,12 @@ async function blocklistList(args, flags) {
339
339
  if (flags.limit) query.limit = flags.limit;
340
340
  if (flags.app) query.appKey = flags.app;
341
341
  if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
342
+ if (flags.status) query.status = flags.status;
343
+ if (flags.search) query.search = flags.search;
344
+ if (flags.view) query.view = flags.view;
345
+ if (flags.approvalStatus || flags['approval-status']) {
346
+ query.approvalStatus = flags.approvalStatus || flags['approval-status'];
347
+ }
342
348
  const data = await api.get('/blocklist', { query });
343
349
  const items = data.blockedIps || [];
344
350
  s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
@@ -348,15 +354,17 @@ async function blocklistList(args, flags) {
348
354
  console.log('');
349
355
  const rows = items.map(b => [
350
356
  ui.c.dim(ui.truncate(b._id, 12)),
351
- ui.c.red(b.ip || b.cidr || '—'),
352
- ui.truncate(b.reason || '', 40),
357
+ ui.c.red(b.ip || b.cidr || '—'),
358
+ ui.truncate(b.reason || '', 40),
353
359
  b.source || '—',
360
+ b.status || 'active',
354
361
  b.applicationKey || ui.c.dim('all apps'),
355
362
  b.environment || ui.c.dim('all envs'),
356
363
  ui.timeAgo(b.createdAt),
364
+ b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
357
365
  b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
358
366
  ]);
359
- ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'App', 'Env', 'Added', 'Expires'], rows);
367
+ ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
360
368
  console.log('');
361
369
  } catch (err) {
362
370
  s.fail('Failed to fetch blocklist');
@@ -390,26 +398,28 @@ async function blocklistAdd(args, flags) {
390
398
 
391
399
  async function blocklistRemove(args, flags) {
392
400
  requireAuth();
393
- const id = args[0];
394
- if (!id) {
395
- ui.error('Blocklist entry ID required. Usage: securenow blocklist remove <id>');
396
- process.exit(1);
397
- }
398
-
399
- if (!flags.force && !flags.yes) {
400
- const ok = await ui.confirm('Remove this IP from blocklist?');
401
- if (!ok) { ui.info('Cancelled'); return; }
402
- }
403
-
404
- const s = ui.spinner('Removing from blocklist');
405
- try {
406
- await api.delete(`/blocklist/${id}`);
407
- s.stop('Removed from blocklist');
408
- } catch (err) {
409
- s.fail('Failed to remove from blocklist');
410
- throw err;
411
- }
412
- }
401
+ const id = args[0];
402
+ if (!id) {
403
+ ui.error('Blocklist entry ID required. Usage: securenow blocklist unblock <id> [--reason <reason>]');
404
+ process.exit(1);
405
+ }
406
+
407
+ if (!flags.force && !flags.yes) {
408
+ const ok = await ui.confirm('Unblock this IP? Firewall enforcement stops, but block report and history stay saved.');
409
+ if (!ok) { ui.info('Cancelled'); return; }
410
+ }
411
+
412
+ const s = ui.spinner('Unblocking IP');
413
+ try {
414
+ const body = {};
415
+ if (flags.reason) body.reason = flags.reason;
416
+ await api.post(`/blocklist/${id}/unblock`, body);
417
+ s.stop('Unblocked; block report and history retained');
418
+ } catch (err) {
419
+ s.fail('Failed to unblock IP');
420
+ throw err;
421
+ }
422
+ }
413
423
 
414
424
  async function blocklistStats(args, flags) {
415
425
  requireAuth();
package/cli.js CHANGED
@@ -253,9 +253,51 @@ const COMMANDS = {
253
253
  },
254
254
  defaultSub: 'status',
255
255
  },
256
+ ratelimit: {
257
+ desc: 'Manage soft rate-limit remediation rules',
258
+ usage: 'securenow ratelimit <list|add|show|test|enable|disable|remove> [options]',
259
+ flags: {
260
+ app: 'Scope to app key (defaults to logged-in app)',
261
+ env: 'Scope to environment (default for create/test: production)',
262
+ environment: 'Alias for --env',
263
+ json: 'Output as JSON',
264
+ limit: 'Allowed requests per window',
265
+ window: 'Window size, e.g. 30s, 1m, 1h',
266
+ duration: 'Expiry, e.g. 24h or 7d',
267
+ route: 'Path pattern such as /api/login',
268
+ path: 'Alias for --route',
269
+ mode: 'Path mode: exact, prefix, or regex',
270
+ method: 'HTTP method, or ALL',
271
+ reason: 'Audit reason',
272
+ },
273
+ sub: {
274
+ list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
275
+ add: { desc: 'Create a rate-limit rule', usage: 'securenow ratelimit add [ip] --route /api/login --limit 5 --window 1m --duration 24h', run: (a, f) => require('./cli/rateLimits').add(a, f) },
276
+ show: { desc: 'Show one rate-limit rule', usage: 'securenow ratelimit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
277
+ test: { desc: 'Check whether a request would match rate-limit rules', usage: 'securenow ratelimit test <ip> --path /api/login --method POST', run: (a, f) => require('./cli/rateLimits').test(a, f) },
278
+ enable: { desc: 'Enable a rate-limit rule', usage: 'securenow ratelimit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
279
+ disable: { desc: 'Disable a rate-limit rule', usage: 'securenow ratelimit disable <id>', run: (a, f) => require('./cli/rateLimits').disable(a, f) },
280
+ remove: { desc: 'Remove a rate-limit rule', usage: 'securenow ratelimit remove <id> [--reason "..."]', run: (a, f) => require('./cli/rateLimits').remove(a, f) },
281
+ },
282
+ defaultSub: 'list',
283
+ },
284
+ 'rate-limit': {
285
+ desc: 'Alias for ratelimit',
286
+ usage: 'securenow rate-limit <list|add|show|test|enable|disable|remove> [options]',
287
+ sub: {
288
+ list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
289
+ add: { desc: 'Create a rate-limit rule', usage: 'securenow rate-limit add [ip] --route /api/login --limit 5 --window 1m --duration 24h', run: (a, f) => require('./cli/rateLimits').add(a, f) },
290
+ show: { desc: 'Show one rate-limit rule', usage: 'securenow rate-limit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
291
+ test: { desc: 'Check whether a request would match rate-limit rules', usage: 'securenow rate-limit test <ip> --path /api/login --method POST', run: (a, f) => require('./cli/rateLimits').test(a, f) },
292
+ enable: { desc: 'Enable a rate-limit rule', usage: 'securenow rate-limit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
293
+ disable: { desc: 'Disable a rate-limit rule', usage: 'securenow rate-limit disable <id>', run: (a, f) => require('./cli/rateLimits').disable(a, f) },
294
+ remove: { desc: 'Remove a rate-limit rule', usage: 'securenow rate-limit remove <id> [--reason "..."]', run: (a, f) => require('./cli/rateLimits').remove(a, f) },
295
+ },
296
+ defaultSub: 'list',
297
+ },
256
298
  automation: {
257
299
  desc: 'Manage automation rules for blocklist actions',
258
- usage: 'securenow automation <list|show|create|update|dry-run|execute|delete> [rule-id] [options]',
300
+ usage: 'securenow automation <list|defaults|show|create|update|dry-run|execute|delete> [rule-id] [options]',
259
301
  flags: {
260
302
  json: 'Output as JSON',
261
303
  body: 'Full JSON body for create/update',
@@ -280,6 +322,7 @@ const COMMANDS = {
280
322
  },
281
323
  sub: {
282
324
  list: { desc: 'List automation rules', run: (a, f) => require('./cli/automation').list(a, f) },
325
+ defaults: { desc: 'Ensure SecureNow default risk-score automation rules', usage: 'securenow automation defaults [--force-enable] [--yes]', run: (a, f) => require('./cli/automation').defaults(a, f) },
283
326
  show: { desc: 'Show automation rule details', usage: 'securenow automation show <rule-id>', run: (a, f) => require('./cli/automation').show(a, f) },
284
327
  create: { desc: 'Create automation rule', usage: 'securenow automation create --name "..." --conditions \'[...]\' --actions \'[...]\'', run: (a, f) => require('./cli/automation').create(a, f) },
285
328
  update: { desc: 'Update automation rule', usage: 'securenow automation update <rule-id> --body \'{...}\'', run: (a, f) => require('./cli/automation').update(a, f) },
@@ -292,13 +335,14 @@ const COMMANDS = {
292
335
  blocklist: {
293
336
  desc: 'Manage IP blocklist',
294
337
  usage: 'securenow blocklist <subcommand> [options]',
295
- flags: { app: 'Scope to app key', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', json: 'Output as JSON' },
338
+ flags: { app: 'Scope to app key', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', status: 'Entry status: active or removed', 'approval-status': 'Approval filter: pending, approved, or rejected', search: 'IP prefix search', view: 'List view: all or operational', reason: 'Audit reason for unblock', json: 'Output as JSON' },
296
339
  sub: {
297
340
  list: { desc: 'List blocked IPs', run: (a, f) => require('./cli/security').blocklistList(a, f) },
298
341
  add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--app <key>] [--env production] [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
299
- remove: { desc: 'Unblock an IP', usage: 'securenow blocklist remove <id>', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
300
- stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
301
- },
342
+ unblock: { desc: 'Unblock an IP and keep block history', usage: 'securenow blocklist unblock <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
343
+ remove: { desc: 'Unblock an IP (compatibility alias)', usage: 'securenow blocklist remove <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
344
+ stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
345
+ },
302
346
  defaultSub: 'list',
303
347
  },
304
348
  allowlist: {
@@ -553,7 +597,7 @@ function showHelp(commandName) {
553
597
  'Detect & Respond': ['human', 'notifications', 'alerts', 'fp'],
554
598
  'Investigate': ['ip', 'forensics'],
555
599
  'Firewall': ['firewall'],
556
- 'Remediation': ['automation', 'blocklist', 'allowlist', 'trusted'],
600
+ 'Remediation': ['automation', 'ratelimit', 'blocklist', 'allowlist', 'trusted'],
557
601
  'Telemetry': ['log', 'test-span'],
558
602
  'Utilities': ['redact', 'cidr', 'doctor', 'env', 'mcp'],
559
603
  'Settings': ['instances', 'config', 'version'],
package/firewall.js CHANGED
@@ -30,11 +30,16 @@ let _remoteEnabled = true;
30
30
  let _lastRemoteEnabled = null;
31
31
 
32
32
  // Allowlist state
33
- let _allowlistMatcher = null;
34
- let _allowlistRawIps = [];
35
- let _lastAllowlistModified = null;
36
- let _lastAllowlistVersion = null;
37
- let _lastAllowlistSyncEtag = null;
33
+ let _allowlistMatcher = null;
34
+ let _allowlistRawIps = [];
35
+ let _lastAllowlistModified = null;
36
+ let _lastAllowlistVersion = null;
37
+ let _lastAllowlistSyncEtag = null;
38
+
39
+ // Rate-limit policy state is synced in Phase 1 for forward compatibility.
40
+ // Enforcement is intentionally added in the next SDK phase.
41
+ let _rateLimitRules = [];
42
+ let _lastRateLimitVersion = null;
38
43
 
39
44
  // Circuit breaker
40
45
  const CIRCUIT_OPEN_THRESHOLD = 5;
@@ -295,6 +300,7 @@ function doUnifiedSync(callback) {
295
300
  if (_options.environment) headers['X-SecureNow-Environment'] = _options.environment;
296
301
  if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
297
302
  if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
303
+ if (_lastRateLimitVersion) headers['X-Rate-Limit-Version'] = _lastRateLimitVersion;
298
304
  if (_lastUnifiedEtag) headers['If-None-Match'] = _lastUnifiedEtag;
299
305
 
300
306
  httpGet(url, headers, 8000, (err, res, data) => {
@@ -364,13 +370,24 @@ function doUnifiedSync(callback) {
364
370
  }
365
371
  }
366
372
 
367
- if (body.allowlistIps) {
368
- _allowlistRawIps = body.allowlistIps;
369
- _allowlistMatcher = createMatcher(body.allowlistIps);
370
- alChanged = true;
371
- }
372
-
373
- callback(null, { blChanged, alChanged });
373
+ if (body.allowlistIps) {
374
+ _allowlistRawIps = body.allowlistIps;
375
+ _allowlistMatcher = createMatcher(body.allowlistIps);
376
+ alChanged = true;
377
+ }
378
+
379
+ if (body.rateLimits) {
380
+ const newVer = body.rateLimits.version;
381
+ if (newVer !== _lastRateLimitVersion) {
382
+ _lastRateLimitVersion = newVer;
383
+ }
384
+ }
385
+
386
+ if (Array.isArray(body.rateLimitRules)) {
387
+ _rateLimitRules = body.rateLimitRules;
388
+ }
389
+
390
+ callback(null, { blChanged, alChanged, rlChanged: Array.isArray(body.rateLimitRules) });
374
391
  } catch (e) {
375
392
  callback(new Error(`Failed to parse sync response: ${e.message}`));
376
393
  }
@@ -557,11 +574,14 @@ function pollOnce(callback) {
557
574
  const s = _matcher.stats();
558
575
  fwLog('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
559
576
  }
560
- if (result.alChanged && _options.log && _allowlistMatcher) {
561
- const s = _allowlistMatcher.stats();
562
- fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
563
- }
564
- }
577
+ if (result.alChanged && _options.log && _allowlistMatcher) {
578
+ const s = _allowlistMatcher.stats();
579
+ fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
580
+ }
581
+ if (result.rlChanged && _options.log) {
582
+ fwLog('[securenow] Firewall: re-synced %d rate-limit rules (enforcement pending SDK phase 2)', _rateLimitRules.length);
583
+ }
584
+ }
565
585
  callback(null);
566
586
  };
567
587
 
@@ -639,12 +659,15 @@ function startSyncLoop() {
639
659
  const s = _matcher.stats();
640
660
  fwLog('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
641
661
  }
642
- if (_options.log && _allowlistMatcher) {
643
- const s = _allowlistMatcher.stats();
644
- if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
645
- }
646
- });
647
- }
662
+ if (_options.log && _allowlistMatcher) {
663
+ const s = _allowlistMatcher.stats();
664
+ if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
665
+ }
666
+ if (_options.log && _rateLimitRules.length > 0) {
667
+ fwLog('[securenow] Firewall: synced %d rate-limit rules (enforcement pending SDK phase 2)', _rateLimitRules.length);
668
+ }
669
+ });
670
+ }
648
671
 
649
672
  initialSync();
650
673
  scheduleNextPoll();
@@ -655,14 +678,17 @@ function startSyncLoop() {
655
678
  // Force a full re-fetch by clearing versions so unified endpoint returns full data
656
679
  const savedBlVer = _lastVersion;
657
680
  const savedAlVer = _lastAllowlistVersion;
681
+ const savedRlVer = _lastRateLimitVersion;
658
682
  const savedUnifiedEtag = _lastUnifiedEtag;
659
683
  _lastVersion = null;
660
684
  _lastAllowlistVersion = null;
685
+ _lastRateLimitVersion = null;
661
686
  _lastUnifiedEtag = null;
662
687
  pollOnce((err) => {
663
688
  if (err) {
664
689
  _lastVersion = savedBlVer;
665
690
  _lastAllowlistVersion = savedAlVer;
691
+ _lastRateLimitVersion = savedRlVer;
666
692
  _lastUnifiedEtag = savedUnifiedEtag;
667
693
  }
668
694
  });
@@ -912,9 +938,10 @@ function shutdown() {
912
938
  function getStats() {
913
939
  return {
914
940
  ..._stats,
915
- matcher: _matcher ? _matcher.stats() : null,
916
- allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
917
- initialized: _initialized,
941
+ matcher: _matcher ? _matcher.stats() : null,
942
+ allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
943
+ rateLimitRules: _rateLimitRules.length,
944
+ initialized: _initialized,
918
945
  circuitState: _circuitState,
919
946
  consecutiveErrors: _consecutiveErrors,
920
947
  unifiedSync: _useUnifiedSync,
@@ -927,8 +954,9 @@ function getStats() {
927
954
  // Layers (TCP / iptables / cloud) read the matcher to populate kernel-level
928
955
  // rules. When the remote toggle is off, return null so they treat the policy
929
956
  // as "no IPs to block" without us mutating the cached matcher.
930
- function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
931
- function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
932
- function isRemoteEnabled() { return _remoteEnabled !== false; }
933
-
934
- module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, isRemoteEnabled };
957
+ function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
958
+ function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
959
+ function getRateLimitRules() { return _remoteEnabled === false ? [] : _rateLimitRules.slice(); }
960
+ function isRemoteEnabled() { return _remoteEnabled !== false; }
961
+
962
+ module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getRateLimitRules, isRemoteEnabled };
package/mcp/catalog.js CHANGED
@@ -676,6 +676,21 @@ const TOOLS = [
676
676
  endpoint: '/automation-rules',
677
677
  inputSchema: objectSchema({}),
678
678
  },
679
+ {
680
+ name: 'securenow_automation_defaults_ensure',
681
+ title: 'Ensure Default Automation Rules',
682
+ description: 'Create or refresh SecureNow default risk-score automation rules for the current account. Write action; requires confirmation.',
683
+ scope: 'automation:write',
684
+ readOnly: false,
685
+ confirm: true,
686
+ method: 'POST',
687
+ endpoint: '/automation-rules/defaults/ensure',
688
+ bodyFields: ['forceEnableExisting'],
689
+ inputSchema: objectSchema({
690
+ forceEnableExisting: boolean('Re-enable existing SecureNow default rules if the customer disabled them. Defaults to false.'),
691
+ ...confirmSchema,
692
+ }, ['confirm', 'reason']),
693
+ },
679
694
  {
680
695
  name: 'securenow_automation_rule_get',
681
696
  title: 'Get Automation Rule',
@@ -1040,14 +1055,31 @@ const TOOLS = [
1040
1055
  },
1041
1056
  {
1042
1057
  name: 'securenow_blocklist_remove',
1043
- title: 'Remove Blocked IP',
1044
- description: 'Remove a blocklist entry. Write action; requires confirmation.',
1058
+ title: 'Unblock IP (Compat Alias)',
1059
+ description: 'Compatibility alias for unblocking a blocklist entry while retaining block report/history. Write action; requires confirmation.',
1045
1060
  scope: 'blocklist:write',
1046
1061
  readOnly: false,
1047
1062
  confirm: true,
1048
- method: 'DELETE',
1049
- endpoint: '/blocklist/:id',
1063
+ method: 'POST',
1064
+ endpoint: '/blocklist/:id/unblock',
1050
1065
  pathParams: ['id'],
1066
+ bodyFields: ['reason'],
1067
+ inputSchema: objectSchema({
1068
+ id: string('Blocklist entry id.'),
1069
+ ...confirmSchema,
1070
+ }, ['id', 'confirm', 'reason']),
1071
+ },
1072
+ {
1073
+ name: 'securenow_blocklist_unblock',
1074
+ title: 'Unblock IP And Keep History',
1075
+ description: 'Unblock a blocklist entry so firewall enforcement stops, while retaining the block report, history, and unblock audit trail. Write action; requires confirmation.',
1076
+ scope: 'blocklist:write',
1077
+ readOnly: false,
1078
+ confirm: true,
1079
+ method: 'POST',
1080
+ endpoint: '/blocklist/:id/unblock',
1081
+ pathParams: ['id'],
1082
+ bodyFields: ['reason'],
1051
1083
  inputSchema: objectSchema({
1052
1084
  id: string('Blocklist entry id.'),
1053
1085
  ...confirmSchema,
@@ -1428,16 +1460,16 @@ function promptMessages(name, args = {}) {
1428
1460
  type: 'text',
1429
1461
  text: [
1430
1462
  'Configure SecureNow default automation using the MCP tools.',
1431
- `Default environment scope: ${environment}.`,
1432
- 'Use one canonical product score for automation decisions: riskScore. Treat SecureNow IPDB / AbuseIPDB score and AI confidence as supporting evidence, not the primary UI score.',
1433
- 'Desired defaults:',
1434
- '- Auto-block High-Confidence AI Malicious IPs: aiDecision=pending_approval AND isMalicious=true AND riskScore>=90, TTL 168h.',
1435
- '- Auto-block High-Confidence XSS IPs: alertTag in xss AND riskScore>=80, TTL 24h.',
1436
- '- High-reputation IPDB automation may remain enabled only as an explicit reputation policy: abuseConfidenceScore>=80.',
1437
- 'Start with securenow_automation_rules_list. Reuse/update existing rules where names/conditions match instead of creating duplicates.',
1438
- 'Create new rules with status=disabled when possible, dry-run each changed or created rule with securenow_automation_rule_dry_run, then enable only after the sample matches look safe.',
1463
+ `Review environment context: ${environment}. Built-in defaults apply to all apps and all environments unless the customer narrows them later.`,
1464
+ 'Use one canonical product score for automation decisions: riskScore. SecureNow IPDB / AbuseIPDB score and AI confidence remain supporting evidence.',
1465
+ 'Built-in defaults are active by default:',
1466
+ '- Critical Risk Auto-Block: riskScore>=95, TTL 168h.',
1467
+ '- High Risk Auto-Block: riskScore>=90 AND riskScore<95, TTL 72h.',
1468
+ '- Elevated Risk Auto-Block: riskScore>=85 AND riskScore<90, TTL 24h.',
1469
+ '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.',
1470
+ '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.',
1439
1471
  confirmWrites
1440
- ? 'The user requested execution. Create/update rules with confirm:true, production scope, clear names, and precise reasons. For new rules, prefer status=disabled, dry-run, then update status=active after review. Re-list rules after writes.'
1472
+ ? '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.'
1441
1473
  : 'Do not execute writes yet. Return exact MCP update/create calls and dry-run calls for approval.',
1442
1474
  'End with active rules, disabled rules that should stay disabled, and any risk from automation scope.',
1443
1475
  ].join('\n'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.6",
3
+ "version": "7.7.8",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",