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 +6 -3
- package/README.md +2 -0
- package/SKILL-API.md +19 -3
- package/SKILL-CLI.md +15 -5
- package/cli/automation.js +29 -1
- package/cli/firewall.js +5 -4
- package/cli/rateLimits.js +245 -0
- package/cli/security.js +33 -23
- package/cli.js +50 -6
- package/firewall.js +59 -31
- package/mcp/catalog.js +45 -13
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
327
|
-
`riskScore
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
['
|
|
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
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (!flags.force && !flags.yes) {
|
|
400
|
-
const ok = await ui.confirm('
|
|
401
|
-
if (!ok) { ui.info('Cancelled'); return; }
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const s = ui.spinner('
|
|
405
|
-
try {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
s.
|
|
410
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
933
|
-
|
|
934
|
-
|
|
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: '
|
|
1044
|
-
description: '
|
|
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: '
|
|
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
|
-
`
|
|
1432
|
-
'Use one canonical product score for automation decisions: riskScore.
|
|
1433
|
-
'
|
|
1434
|
-
'- Auto-
|
|
1435
|
-
'-
|
|
1436
|
-
'-
|
|
1437
|
-
'Start with
|
|
1438
|
-
'
|
|
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.
|
|
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