securenow 7.7.9 → 7.7.11
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 +11 -3
- package/README.md +7 -1
- package/SKILL-API.md +7 -4
- package/SKILL-CLI.md +36 -8
- package/cli/apiKey.js +43 -1
- package/cli/config.js +9 -3
- package/cli/rateLimits.js +101 -1
- package/cli/security.js +116 -5
- package/cli.js +61 -17
- package/mcp/catalog.js +83 -7
- package/package.json +7 -1
- package/rate-limits.d.ts +86 -0
- package/rate-limits.js +88 -0
- package/resolve-ip.js +3 -2
package/NPM_README.md
CHANGED
|
@@ -157,7 +157,8 @@ npx securenow login --global
|
|
|
157
157
|
# Check who you're logged in as (shows auth source)
|
|
158
158
|
npx securenow whoami
|
|
159
159
|
|
|
160
|
-
#
|
|
160
|
+
# Need or already have a firewall key? Create or store it without re-running login:
|
|
161
|
+
npx securenow api-key create --name "CLI firewall"
|
|
161
162
|
npx securenow api-key set snk_live_abc123... # --global for ~/.securenow/
|
|
162
163
|
npx securenow api-key show # masked key + source
|
|
163
164
|
npx securenow api-key clear # remove just the key
|
|
@@ -186,7 +187,7 @@ codex mcp add securenow -- npx securenow mcp
|
|
|
186
187
|
npx -p securenow securenow-mcp
|
|
187
188
|
```
|
|
188
189
|
|
|
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
|
+
The MCP surface exposes tools for applications, traces, logs, firewall, IP intelligence, forensics, notifications, blocklist, allowlist, trusted IPs, rate-limit remediation, 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
191
|
|
|
191
192
|
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
193
|
|
|
@@ -256,6 +257,8 @@ npx securenow alerts rules
|
|
|
256
257
|
npx securenow alerts rules show <rule-id>
|
|
257
258
|
npx securenow alerts rules update <rule-id> --applications-all
|
|
258
259
|
npx securenow alerts rules update <rule-id> --apps key1,key2
|
|
260
|
+
npx securenow alerts rules dry-run-query <rule-id> --sql @candidate.sql --app <key> --wait
|
|
261
|
+
npx securenow alerts rules tune-query <rule-id> --sql @candidate.sql --reason "Preserve exploit detector, remove noisy broad match" --apply-globally --yes
|
|
259
262
|
npx securenow alerts channels
|
|
260
263
|
npx securenow alerts history --limit 20
|
|
261
264
|
```
|
|
@@ -489,7 +492,8 @@ npx securenow logs --json --level error | jq '.logs'
|
|
|
489
492
|
| | `apps info <id>` | Application details |
|
|
490
493
|
| | `apps delete <id>` | Delete application |
|
|
491
494
|
| | `apps default <key>` | Set default app |
|
|
492
|
-
| **API Key** | `api-key
|
|
495
|
+
| **API Key** | `api-key create [--name "CLI firewall"] [--global]` | Mint and save a firewall key with your logged-in session |
|
|
496
|
+
| | `api-key set <snk_live_...> [--global]` | Save firewall key to `.securenow/credentials.json` |
|
|
493
497
|
| | `api-key show` | Show masked key + source |
|
|
494
498
|
| | `api-key clear [--global]` | Remove stored key (leaves session/app) |
|
|
495
499
|
| **Observe** | `traces` | List traces |
|
|
@@ -525,6 +529,9 @@ npx securenow logs --json --level error | jq '.logs'
|
|
|
525
529
|
| | `blocklist unblock <id>` | Unblock IP and retain report/history |
|
|
526
530
|
| | `blocklist remove <id>` | Compatibility alias for unblock |
|
|
527
531
|
| | `blocklist stats` | Block stats |
|
|
532
|
+
| | `ratelimit parse "<request>"` | Fill a rate-limit draft from natural language |
|
|
533
|
+
| | `ratelimit from-text "<request>" --yes` | Create a soft rate-limit rule from natural language |
|
|
534
|
+
| | `ratelimit test <ip> --path /api/login` | Check whether a request would match rate-limit rules |
|
|
528
535
|
| | `allowlist` | Allowed IPs (restrict-mode) |
|
|
529
536
|
| | `allowlist add <ip>` | Allow IP (`--label`, `--reason`) |
|
|
530
537
|
| | `allowlist remove <id>` | Remove from allowlist |
|
|
@@ -1287,6 +1294,7 @@ SecureNow provides multiple entry points depending on your needs:
|
|
|
1287
1294
|
| `securenow/nuxt` | `modules: ['securenow/nuxt']` | Yes | Yes | Nuxt 3 module |
|
|
1288
1295
|
| `securenow/nextjs-webpack-config` | `withSecureNow(config)` | - | - | Next.js config wrapper |
|
|
1289
1296
|
| `securenow/firewall` | `require('securenow/firewall').init({...})` | No | Yes | Programmatic firewall API |
|
|
1297
|
+
| `securenow/rate-limits` | `require('securenow/rate-limits').parseRateLimitText(...)` | No | Yes | Rate-limit remediation API helper |
|
|
1290
1298
|
| `securenow/tracing` | `require('securenow/tracing')` | Yes | No | Programmatic tracing API |
|
|
1291
1299
|
|
|
1292
1300
|
---
|
package/README.md
CHANGED
|
@@ -175,6 +175,7 @@ npx securenow login --global # save to ~/.securenow/ instead
|
|
|
175
175
|
npx securenow login --token <TOKEN> # headless (CI)
|
|
176
176
|
npx securenow init --env local # scaffold framework files + local env scope
|
|
177
177
|
npx securenow credentials runtime --env production # write tokenless production credentials file
|
|
178
|
+
npx securenow api-key create --name "CLI firewall" # mint + store firewall key
|
|
178
179
|
npx securenow api-key set snk_live_... # store firewall key in .securenow/credentials.json
|
|
179
180
|
|
|
180
181
|
# Apps
|
|
@@ -214,7 +215,7 @@ codex mcp add securenow -- npx securenow mcp
|
|
|
214
215
|
npx -p securenow securenow-mcp
|
|
215
216
|
```
|
|
216
217
|
|
|
217
|
-
The MCP server reuses the same project-local `.securenow/credentials.json` as the CLI and SDK. It exposes tools for apps, traces, logs, firewall, IP intelligence, forensics, blocklist/allowlist/trusted IPs, plus resources for the bundled SecureNow docs and setup prompts.
|
|
218
|
+
The MCP server reuses the same project-local `.securenow/credentials.json` as the CLI and SDK. It exposes tools for apps, traces, logs, firewall, IP intelligence, forensics, blocklist/allowlist/trusted IPs, rate-limit remediation, plus resources for the bundled SecureNow docs and setup prompts.
|
|
218
219
|
|
|
219
220
|
---
|
|
220
221
|
|
|
@@ -301,6 +302,7 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
|
|
|
301
302
|
| `securenow logout` | Clear project-local credentials |
|
|
302
303
|
| `securenow logout --global` | Clear ~/.securenow/ instead |
|
|
303
304
|
| `securenow whoami` | Show current session (email, app, expiry) |
|
|
305
|
+
| `securenow api-key create [--name "CLI firewall"]` | Mint and store a firewall key using your session token |
|
|
304
306
|
| `securenow api-key set <snk_live_...>` | Store firewall key in `.securenow/credentials.json` (`--global` for `~/.securenow/`) |
|
|
305
307
|
| `securenow api-key show` | Print masked key + source file |
|
|
306
308
|
| `securenow api-key clear` | Remove stored key (`--global` for `~/.securenow/`) |
|
|
@@ -334,6 +336,8 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
|
|
|
334
336
|
| `securenow notifications` | List notifications |
|
|
335
337
|
| `securenow notifications unread` | Unread count |
|
|
336
338
|
| `securenow alerts rules` | List alert rules |
|
|
339
|
+
| `securenow alerts rules dry-run-query <id> --sql @candidate.sql --wait` | Dry-run candidate alert SQL without saving |
|
|
340
|
+
| `securenow alerts rules tune-query <id> --sql @candidate.sql --apply-globally --yes` | Admin: update a shared system rule query mapping |
|
|
337
341
|
| `securenow alerts history` | Alert history |
|
|
338
342
|
|
|
339
343
|
### Investigate
|
|
@@ -359,6 +363,8 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
|
|
|
359
363
|
| `securenow blocklist` | List blocked IPs |
|
|
360
364
|
| `securenow blocklist add <ip> [--reason ...]` | Block an IP |
|
|
361
365
|
| `securenow blocklist unblock <id> [--reason ...]` | Stop enforcement and keep block history |
|
|
366
|
+
| `securenow ratelimit parse "<request>"` | Fill a rate-limit rule draft from natural language |
|
|
367
|
+
| `securenow ratelimit from-text "<request>" --yes` | Create a soft rate-limit rule from natural language |
|
|
362
368
|
| `securenow allowlist add <ip>` | Allow an IP (restrict-mode) |
|
|
363
369
|
| `securenow trusted add <ip>` | Mark an IP as trusted |
|
|
364
370
|
|
package/SKILL-API.md
CHANGED
|
@@ -4,7 +4,9 @@ Instrument any Node.js application with OpenTelemetry tracing, structured loggin
|
|
|
4
4
|
|
|
5
5
|
**CLI parity:** every capability exposed below (redaction, CIDR matching, log/span emission, firewall preload, config inspection) has an equivalent `securenow` CLI command. See [SKILL-CLI.md](./SKILL-CLI.md) for the terminal surface.
|
|
6
6
|
|
|
7
|
-
**MCP parity (v7.5+):** `npx securenow mcp` starts a local stdio MCP server for Codex, Claude, and other MCP clients. It reuses the same `.securenow/credentials.json` file as the CLI/SDK and exposes SecureNow tools, bundled docs resources, and setup prompts to agents.
|
|
7
|
+
**MCP parity (v7.5+):** `npx securenow mcp` starts a local stdio MCP server for Codex, Claude, and other MCP clients. It reuses the same `.securenow/credentials.json` file as the CLI/SDK and exposes SecureNow tools, bundled docs resources, and setup prompts to agents. Alert-rule operators can inspect notifications, read exact `metadata.matchedSubdetectors`, dry-run candidate SQL with `securenow_alert_rule_candidate_test`, and apply global system-rule query fixes with `securenow_alert_rule_query_update` when evidence is clear.
|
|
8
|
+
|
|
9
|
+
**Noisy alert-rule reviews:** prefer fixing a generic system-rule detector over creating customer-specific false positives. Dry-run candidate SQL first, preserve tenant scoping with `__USER_APP_KEYS__`, keep exploit-specific indicators, then save the shared query mapping only with an audit reason and explicit confirmation.
|
|
8
10
|
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
@@ -97,9 +99,10 @@ operator needs to ensure those defaults immediately.
|
|
|
97
99
|
| `securenow/nextjs-middleware` | Edge middleware body capture; exports `middleware()`, `redactSensitiveData()`, `DEFAULT_SENSITIVE_FIELDS` | CJS |
|
|
98
100
|
| `securenow/nextjs-wrapper` | Route handler wrappers; exports `withSecureNow()`, `withSecureNowAsync()`, `captureRequestBody()`, `redactSensitiveData()` | CJS |
|
|
99
101
|
| `securenow/nextjs-auto-capture` | Auto-patch Next request for body capture; exports `patchNextRequest()`, `safeBodyCapture()`, `redactSensitiveData()`, `isBodyCaptureEnabled()` | CJS |
|
|
100
|
-
| `securenow/nuxt` | Nuxt 3 module (add to `modules` array) | ESM |
|
|
101
|
-
| `securenow/firewall` | Standalone firewall; exports `init()`, `shutdown()`, `getStats()`, `getMatcher()`, `getAllowlistMatcher()` | CJS |
|
|
102
|
-
| `securenow/
|
|
102
|
+
| `securenow/nuxt` | Nuxt 3 module (add to `modules` array) | ESM |
|
|
103
|
+
| `securenow/firewall` | Standalone firewall; exports `init()`, `shutdown()`, `getStats()`, `getMatcher()`, `getAllowlistMatcher()` | CJS |
|
|
104
|
+
| `securenow/rate-limits` | Rate-limit remediation API helper; exports `parseRateLimitText()`, `createRateLimitFromText()`, `createRateLimit()`, `listRateLimits()` | CJS |
|
|
105
|
+
| `securenow/firewall-only` | Preload: dotenv + firewall only, no tracing | Preload (`-r`) |
|
|
103
106
|
| `securenow/cidr` | CIDR utilities; exports `createMatcher()`, `ipToInt()`, `parseCidr()`, `matchesCidr()` | CJS |
|
|
104
107
|
| `securenow/resolve-ip` | IP resolution; exports `resolveClientIp()`, `resolveSocketIp()`, `isFromTrustedProxy()` | CJS |
|
|
105
108
|
| `securenow/console-instrumentation` | Console→OTLP bridge; exports `originalConsole`, `restoreConsole()` | CJS |
|
package/SKILL-CLI.md
CHANGED
|
@@ -24,7 +24,7 @@ codex mcp add securenow -- npx securenow mcp
|
|
|
24
24
|
npx -p securenow securenow-mcp
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The MCP server reuses `.securenow/credentials.json` and exposes apps, traces, logs, firewall, IP intelligence, forensics, notifications, remediation tools, bundled docs resources, and setup prompts. Write tools require `confirm:true` plus a reason.
|
|
27
|
+
The MCP server reuses `.securenow/credentials.json` and exposes apps, traces, logs, firewall, IP intelligence, forensics, notifications, remediation tools, alert-rule review/tuning tools, bundled docs resources, and setup prompts. Write tools require `confirm:true` plus a reason.
|
|
28
28
|
|
|
29
29
|
### Authenticate
|
|
30
30
|
|
|
@@ -153,16 +153,17 @@ securenow apps scan [--yes] # scan all app domains for new subd
|
|
|
153
153
|
|
|
154
154
|
Manage the firewall API key stored in the credentials file. Since v7.5.1 the login flow writes `snk_live_...` keys to `.securenow/credentials.json` by default, so no env var is required for local dev.
|
|
155
155
|
|
|
156
|
-
```bash
|
|
157
|
-
securenow api-key
|
|
158
|
-
securenow api-key set
|
|
156
|
+
```bash
|
|
157
|
+
securenow api-key create --name "CLI firewall" # mint + save a firewall key with your logged-in session
|
|
158
|
+
securenow api-key set snk_live_xxxxxxxxxx # save to project ./.securenow/ (default)
|
|
159
|
+
securenow api-key set snk_live_xxx --global # save to ~/.securenow/ instead
|
|
159
160
|
securenow api-key show # print the masked current key + its source
|
|
160
161
|
securenow api-key clear # remove just the API key (keeps session/app)
|
|
161
162
|
securenow api-key clear --global # same, but from the global file
|
|
162
163
|
securenow credentials runtime --env production # write .securenow/credentials.production.json for production secret-file deploys
|
|
163
164
|
```
|
|
164
165
|
|
|
165
|
-
The key must start with `snk_live_`. `securenow login` with firewall enabled writes the key automatically; use `api-key set` when you already have a key from the dashboard
|
|
166
|
+
The key must start with `snk_live_`. `securenow login` with firewall enabled writes the key automatically; use `api-key create` when you have an account session but no runtime key yet, or `api-key set` when you already have a key from the dashboard.
|
|
166
167
|
|
|
167
168
|
### Init — Project Setup
|
|
168
169
|
|
|
@@ -239,7 +240,7 @@ MCP parity:
|
|
|
239
240
|
- `securenow_human_case_action_update`
|
|
240
241
|
- prompts: `investigate_human_action_row`, `work_human_actions`
|
|
241
242
|
|
|
242
|
-
Write tools still require `confirm:true` plus a reason. False positives should stay restrictive to the app, alert rule, path, method/status, user-agent, body pattern, or other exact evidence the AI report supports.
|
|
243
|
+
Write tools still require `confirm:true` plus a reason. False positives should stay restrictive to the app, alert rule, path, method/status, user-agent, body pattern, or other exact evidence the AI report supports. Notification IP investigations can include `metadata.matchedSubdetectors`; prefer that exact field over inferring which subdetector fired from the title or sample body.
|
|
243
244
|
|
|
244
245
|
### Alerts
|
|
245
246
|
|
|
@@ -250,10 +251,22 @@ securenow alerts rules show <id> # one rule; JSON: --json
|
|
|
250
251
|
securenow alerts rules update <id> --applications-all # all current & future apps
|
|
251
252
|
securenow alerts rules update <id> --apps key1,key2 # explicit app keys only
|
|
252
253
|
securenow alerts rules test <id> --mode dry_run --wait # validate a rule query
|
|
254
|
+
securenow alerts rules dry-run-query <id> --sql @candidate.sql --app <key> --wait
|
|
255
|
+
securenow alerts rules tune-query <id> --sql @candidate.sql --reason "Preserve exploit detector, remove benign broad match" --apply-globally --yes
|
|
253
256
|
securenow alerts rules exclusions <id> list # embedded rule exclusions
|
|
254
257
|
securenow alerts channels # list alert channels (Slack, email, etc.)
|
|
255
|
-
securenow alerts history [--limit N] # past triggered alerts
|
|
256
|
-
```
|
|
258
|
+
securenow alerts history [--limit N] # past triggered alerts
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
MCP parity for noisy alert-rule reviews:
|
|
262
|
+
|
|
263
|
+
- `securenow_alert_rule_get`
|
|
264
|
+
- `securenow_alert_rule_candidate_test` dry-runs a full candidate SQL query without saving it.
|
|
265
|
+
- `securenow_alert_rule_test_result` polls the dry-run.
|
|
266
|
+
- `securenow_alert_rule_query_update` updates the shared public query mapping behind a system rule for all customer copies. It is admin-only, requires `confirm:true`, `applyGlobally:true`, `reason`, and SQL that keeps `__USER_APP_KEYS__` tenant scoping.
|
|
267
|
+
- `securenow_alert_rule_exclusion_add` remains the last-resort customer-specific path; it supports restrictive conditions plus `matchMode` and should not be used to hide a generic system-rule bug.
|
|
268
|
+
|
|
269
|
+
For system-rule tuning, dry-run the candidate SQL first, then save with `tune-query`/`securenow_alert_rule_query_update` only when the guard preserves attack detection. Good guards add exact exploit tokens, dangerous schemes, matched subdetectors, sensitive path/status evidence, malicious user agents, or repeat thresholds; bad guards simply suppress a noisy path.
|
|
257
270
|
|
|
258
271
|
---
|
|
259
272
|
|
|
@@ -322,6 +335,21 @@ work:
|
|
|
322
335
|
- `securenow_blocklist_pending_bulk_reject`
|
|
323
336
|
- prompt: `cleanup_legacy_pending_blocks`
|
|
324
337
|
|
|
338
|
+
### Rate Limits - Soft Remediation
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
securenow ratelimit list --env production
|
|
342
|
+
securenow ratelimit add 203.0.113.42 --route /api/login --limit 2 --window 1m --duration 24h
|
|
343
|
+
securenow ratelimit parse "rate limit 203.0.113.42 on POST /api/login to 2 attempts per minute"
|
|
344
|
+
securenow ratelimit from-text "rate limit /api/login for this IP to 2 attempts per minute for 24h" --yes
|
|
345
|
+
securenow ratelimit test 203.0.113.42 --path /api/login --method POST
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
MCP parity:
|
|
349
|
+
|
|
350
|
+
- `securenow_rate_limit_parse`
|
|
351
|
+
- `securenow_rate_limit_create_from_text`
|
|
352
|
+
|
|
325
353
|
### Automation Rules
|
|
326
354
|
|
|
327
355
|
```bash
|
package/cli/apiKey.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { api, requireAuth } = require('./client');
|
|
3
4
|
const config = require('./config');
|
|
4
5
|
const ui = require('./ui');
|
|
5
6
|
|
|
@@ -8,6 +9,47 @@ function maskKey(key) {
|
|
|
8
9
|
return `${key.slice(0, 12)}••••••${key.slice(-4)}`;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
async function create(args, flags) {
|
|
13
|
+
requireAuth();
|
|
14
|
+
|
|
15
|
+
const name = flags.name || args.join(' ').trim() || 'CLI firewall';
|
|
16
|
+
const preset = flags.preset || 'firewall';
|
|
17
|
+
const local = flags.global ? false : true;
|
|
18
|
+
|
|
19
|
+
const s = ui.spinner(`Creating ${preset} API key`);
|
|
20
|
+
try {
|
|
21
|
+
const result = await api.post('/api-keys', { name, preset });
|
|
22
|
+
const key = result && result.key;
|
|
23
|
+
if (!key || !key.startsWith('snk_live_')) {
|
|
24
|
+
throw new Error('API did not return a valid firewall key');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
config.setApiKey(key, { local });
|
|
28
|
+
if (local) config.ensureLocalGitignore();
|
|
29
|
+
s.stop('API key created and saved');
|
|
30
|
+
|
|
31
|
+
if (flags.json) {
|
|
32
|
+
ui.json({
|
|
33
|
+
id: result.apiKey?._id || result.apiKey?.id || null,
|
|
34
|
+
name: result.apiKey?.name || name,
|
|
35
|
+
preset,
|
|
36
|
+
key: maskKey(key),
|
|
37
|
+
savedTo: local ? 'project .securenow/credentials.json' : '~/.securenow/credentials.json',
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ui.success(`API key saved (${maskKey(key)})`);
|
|
43
|
+
ui.info(local
|
|
44
|
+
? 'Stored in project .securenow/credentials.json (local)'
|
|
45
|
+
: 'Stored in ~/.securenow/credentials.json (global)');
|
|
46
|
+
ui.info('The plaintext key is not printed. The SDK will read it from the credentials file.');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
s.fail('Failed to create API key');
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
11
53
|
async function set(args, flags) {
|
|
12
54
|
const key = args[0];
|
|
13
55
|
if (!key) {
|
|
@@ -52,4 +94,4 @@ async function show() {
|
|
|
52
94
|
ui.info(`Source: ${config.getAuthSource()}`);
|
|
53
95
|
}
|
|
54
96
|
|
|
55
|
-
module.exports = { set, clear, show };
|
|
97
|
+
module.exports = { create, set, clear, show };
|
package/cli/config.js
CHANGED
|
@@ -47,6 +47,12 @@ function saveJSON(filepath, data) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function credentialsForWrite(targetFile) {
|
|
51
|
+
const inherited = loadCredentials() || {};
|
|
52
|
+
const existing = loadJSON(targetFile);
|
|
53
|
+
return appConfig.mergeCredentials(inherited, existing) || {};
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
function credentialsFileForLocal(local) {
|
|
51
57
|
return local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
|
|
52
58
|
}
|
|
@@ -113,7 +119,7 @@ function withOnboardingFirewallEnabled(creds) {
|
|
|
113
119
|
function ensureCredentialDefaults({ local, enableFirewall = false } = {}) {
|
|
114
120
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
115
121
|
const targetFile = credentialsFileForLocal(useLocal);
|
|
116
|
-
const existing = loadJSON(targetFile);
|
|
122
|
+
const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
|
|
117
123
|
const payload = enableFirewall
|
|
118
124
|
? withOnboardingFirewallEnabled(existing || {})
|
|
119
125
|
: appConfig.withCredentialDefaults(existing || {}) || {};
|
|
@@ -168,7 +174,7 @@ function getApp() {
|
|
|
168
174
|
function setApiKey(apiKey, { local } = {}) {
|
|
169
175
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
170
176
|
const targetFile = credentialsFileForLocal(useLocal);
|
|
171
|
-
const existing = loadJSON(targetFile);
|
|
177
|
+
const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
|
|
172
178
|
saveJSON(targetFile, withOnboardingFirewallEnabled({ ...existing, apiKey }));
|
|
173
179
|
}
|
|
174
180
|
|
|
@@ -189,7 +195,7 @@ function getApiKey() {
|
|
|
189
195
|
function setApp(app, { local } = {}) {
|
|
190
196
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
191
197
|
const targetFile = credentialsFileForLocal(useLocal);
|
|
192
|
-
const existing = loadJSON(targetFile);
|
|
198
|
+
const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
|
|
193
199
|
saveJSON(targetFile, appConfig.withCredentialDefaults({
|
|
194
200
|
...existing,
|
|
195
201
|
app: {
|
package/cli/rateLimits.js
CHANGED
|
@@ -47,6 +47,54 @@ function describeTarget(rule) {
|
|
|
47
47
|
return parts.join(' ') || 'all traffic';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function naturalText(args, flags) {
|
|
51
|
+
return String(flags.text || flags.prompt || flags.query || args.join(' ') || '').trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function draftFromFlags(flags) {
|
|
55
|
+
const draft = {};
|
|
56
|
+
const appKey = resolveApp(flags);
|
|
57
|
+
const environment = resolveEnvironment(flags, null);
|
|
58
|
+
if (appKey) draft.appKey = appKey;
|
|
59
|
+
if (environment) draft.environment = environment;
|
|
60
|
+
if (flags.ip) draft.ip = flags.ip;
|
|
61
|
+
if (flags.route || flags.path || flags.pattern) draft.pathPattern = flags.route || flags.path || flags.pattern;
|
|
62
|
+
if (flags.mode || flags['path-mode']) draft.pathMatchMode = flags.mode || flags['path-mode'];
|
|
63
|
+
if (flags.method) draft.method = flags.method;
|
|
64
|
+
if (flags['key-by'] || flags.keyBy) draft.keyBy = flags['key-by'] || flags.keyBy;
|
|
65
|
+
if (flags.limit) draft.limit = Number(flags.limit);
|
|
66
|
+
if (flags.window || flags['window-seconds']) draft.windowSeconds = parseWindow(flags.window || flags['window-seconds']);
|
|
67
|
+
if (flags.duration) draft.duration = flags.duration;
|
|
68
|
+
if (flags.reason) draft.reason = flags.reason;
|
|
69
|
+
if (flags.name) draft.name = flags.name;
|
|
70
|
+
return draft;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printParsedDraft(data) {
|
|
74
|
+
const draft = data.draft || {};
|
|
75
|
+
ui.keyValue([
|
|
76
|
+
['Name', draft.name || '-'],
|
|
77
|
+
['Target', describeTarget(draft)],
|
|
78
|
+
['Limit', `${draft.limit || '-'} / ${draft.windowSeconds || '-'}s`],
|
|
79
|
+
['Key by', draft.keyBy || 'ip'],
|
|
80
|
+
['App', draft.appKey || 'all'],
|
|
81
|
+
['Environment', draft.environment || 'all'],
|
|
82
|
+
['Duration', draft.duration || 'none'],
|
|
83
|
+
['Status', draft.status || 'active'],
|
|
84
|
+
['Reason', draft.reason || '-'],
|
|
85
|
+
['Provider', data.provider || '-'],
|
|
86
|
+
['Confidence', data.confidence != null ? String(data.confidence) : '-'],
|
|
87
|
+
]);
|
|
88
|
+
if (Array.isArray(data.missingFields) && data.missingFields.length) {
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(` ${ui.c.yellow('Missing:')} ${data.missingFields.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(data.warnings) && data.warnings.length) {
|
|
93
|
+
console.log('');
|
|
94
|
+
for (const warning of data.warnings.slice(0, 5)) console.log(` ${ui.c.yellow('!')} ${warning}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
50
98
|
async function list(args, flags) {
|
|
51
99
|
requireAuth();
|
|
52
100
|
const s = ui.spinner('Fetching rate limits');
|
|
@@ -76,6 +124,58 @@ async function list(args, flags) {
|
|
|
76
124
|
}
|
|
77
125
|
}
|
|
78
126
|
|
|
127
|
+
async function parseText(args, flags) {
|
|
128
|
+
requireAuth();
|
|
129
|
+
const text = naturalText(args, flags);
|
|
130
|
+
if (!text) {
|
|
131
|
+
ui.error('Usage: securenow ratelimit parse "rate limit /api/login to 2 attempts per minute"');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const s = ui.spinner('Parsing natural-language rate limit');
|
|
136
|
+
try {
|
|
137
|
+
const data = await api.post('/rate-limits/parse', { text, draft: draftFromFlags(flags) });
|
|
138
|
+
s.stop('Rate-limit draft parsed');
|
|
139
|
+
if (flags.json) { ui.json(data); return; }
|
|
140
|
+
console.log('');
|
|
141
|
+
printParsedDraft(data);
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(` ${ui.c.dim('Create it with:')} securenow ratelimit from-text ${JSON.stringify(text)} --yes`);
|
|
144
|
+
console.log('');
|
|
145
|
+
} catch (err) {
|
|
146
|
+
s.fail('Failed to parse rate limit');
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function fromText(args, flags) {
|
|
152
|
+
requireAuth();
|
|
153
|
+
const text = naturalText(args, flags);
|
|
154
|
+
if (!text) {
|
|
155
|
+
ui.error('Usage: securenow ratelimit from-text "rate limit /api/login to 2 attempts per minute" --yes');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
if (!flags.yes && !flags.force) {
|
|
159
|
+
const ok = await ui.confirm('Create this rate-limit rule from natural language?');
|
|
160
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const s = ui.spinner('Creating rate limit from natural language');
|
|
164
|
+
try {
|
|
165
|
+
const data = await api.post('/rate-limits/from-text', { text, draft: draftFromFlags(flags) });
|
|
166
|
+
s.stop('Rate limit created');
|
|
167
|
+
if (flags.json) { ui.json(data); return; }
|
|
168
|
+
const r = data.rateLimit;
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log(` ${ui.c.green('ACTIVE')} ${describeTarget(r)} at ${r.limit}/${r.windowSeconds}s`);
|
|
171
|
+
console.log(` ${ui.c.dim('ID:')} ${r.id || r._id}`);
|
|
172
|
+
console.log('');
|
|
173
|
+
} catch (err) {
|
|
174
|
+
s.fail('Failed to create rate limit from text');
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
79
179
|
async function show(args, flags) {
|
|
80
180
|
requireAuth();
|
|
81
181
|
const id = args[0];
|
|
@@ -242,4 +342,4 @@ async function test(args, flags) {
|
|
|
242
342
|
}
|
|
243
343
|
}
|
|
244
344
|
|
|
245
|
-
module.exports = { list, show, add, enable, disable, remove, test };
|
|
345
|
+
module.exports = { list, show, add, parseText, fromText, enable, disable, remove, test };
|
package/cli/security.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { api, requireAuth } = require('./client');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const ui = require('./ui');
|
|
6
7
|
|
|
7
8
|
function resolveApp(flags) {
|
|
8
9
|
return flags.app || config.getDefaultApp();
|
|
@@ -44,6 +45,12 @@ async function alertRulesRoute(args, flags) {
|
|
|
44
45
|
if (sub === 'test') {
|
|
45
46
|
return alertRuleTest(args.slice(1), flags);
|
|
46
47
|
}
|
|
48
|
+
if (sub === 'dry-run-query' || sub === 'candidate-test') {
|
|
49
|
+
return alertRuleCandidateTest(args.slice(1), flags);
|
|
50
|
+
}
|
|
51
|
+
if (sub === 'tune-query' || sub === 'query-update') {
|
|
52
|
+
return alertRuleQueryUpdate(args.slice(1), flags);
|
|
53
|
+
}
|
|
47
54
|
if (sub === 'exclusions') {
|
|
48
55
|
return alertRuleExclusions(args.slice(1), flags);
|
|
49
56
|
}
|
|
@@ -180,6 +187,16 @@ async function alertRuleUpdate(args, flags) {
|
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
function readSqlArg(flags) {
|
|
191
|
+
const raw = flags.sql || flags.query || flags.file;
|
|
192
|
+
if (!raw) return null;
|
|
193
|
+
if (flags.file) return fs.readFileSync(flags.file, 'utf8');
|
|
194
|
+
const text = String(raw);
|
|
195
|
+
if (text === '-') return fs.readFileSync(0, 'utf8');
|
|
196
|
+
if (text.startsWith('@')) return fs.readFileSync(text.slice(1), 'utf8');
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
async function alertRuleTest(args, flags) {
|
|
184
201
|
requireAuth();
|
|
185
202
|
const id = args[0];
|
|
@@ -219,6 +236,100 @@ async function alertRuleTest(args, flags) {
|
|
|
219
236
|
}
|
|
220
237
|
}
|
|
221
238
|
|
|
239
|
+
async function alertRuleCandidateTest(args, flags) {
|
|
240
|
+
requireAuth();
|
|
241
|
+
const id = args[0];
|
|
242
|
+
const candidateSqlQuery = readSqlArg(flags);
|
|
243
|
+
if (!id || !candidateSqlQuery) {
|
|
244
|
+
ui.error('Usage: securenow alerts rules dry-run-query <rule-id> --sql <sql|@file|-> [--app <key>] [--wait]');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const body = { mode: 'dry_run', candidateSqlQuery };
|
|
249
|
+
if (flags.app) body.applicationKey = flags.app;
|
|
250
|
+
|
|
251
|
+
const s = ui.spinner('Starting candidate SQL dry-run');
|
|
252
|
+
try {
|
|
253
|
+
let data = await api.post(`/alert-rules/${id}/test`, body);
|
|
254
|
+
if (flags.wait && data.testId) {
|
|
255
|
+
s.update('Waiting for candidate SQL dry-run results');
|
|
256
|
+
for (let i = 0; i < 40; i++) {
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
258
|
+
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
259
|
+
if (['complete', 'failed'].includes(data.status)) break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
s.stop('Candidate dry-run ready');
|
|
263
|
+
if (flags.json) { ui.json(data); return; }
|
|
264
|
+
console.log('');
|
|
265
|
+
ui.keyValue([
|
|
266
|
+
['Test ID', data.testId || '-'],
|
|
267
|
+
['Status', data.status || '-'],
|
|
268
|
+
['Candidate', data.candidate ? 'yes' : 'no'],
|
|
269
|
+
['Current SQL hash', data.currentSqlHash || '-'],
|
|
270
|
+
['Candidate SQL hash', data.candidateSqlHash || '-'],
|
|
271
|
+
['Result count', String(data.resultCount ?? '-')],
|
|
272
|
+
['Error', data.error || '-'],
|
|
273
|
+
]);
|
|
274
|
+
console.log('');
|
|
275
|
+
} catch (err) {
|
|
276
|
+
s.fail('Failed to dry-run candidate SQL');
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function alertRuleQueryUpdate(args, flags) {
|
|
282
|
+
requireAuth();
|
|
283
|
+
const id = args[0];
|
|
284
|
+
const sqlQuery = readSqlArg(flags);
|
|
285
|
+
const reason = flags.reason;
|
|
286
|
+
if (!id || !sqlQuery || !reason) {
|
|
287
|
+
ui.error('Usage: securenow alerts rules tune-query <rule-id> --sql <sql|@file|-> --reason "..." --apply-globally --yes');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
if (!flags['apply-globally'] && flags.applyGlobally !== true) {
|
|
291
|
+
ui.error('System query tuning requires --apply-globally.');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!flags.force && !flags.yes) {
|
|
296
|
+
const ok = await ui.confirm('Update the shared system query mapping for all customer copies?');
|
|
297
|
+
if (!ok) {
|
|
298
|
+
ui.info('Cancelled');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const body = {
|
|
304
|
+
sqlQuery,
|
|
305
|
+
reason,
|
|
306
|
+
applyGlobally: true,
|
|
307
|
+
reactivatePausedCopies: !!(flags['reactivate-paused'] || flags.reactivatePausedCopies),
|
|
308
|
+
};
|
|
309
|
+
if (flags['expected-hash']) body.expectedCurrentSqlHash = flags['expected-hash'];
|
|
310
|
+
if (flags.notification) body.reviewNotificationId = flags.notification;
|
|
311
|
+
if (flags.note) body.reviewNote = flags.note;
|
|
312
|
+
|
|
313
|
+
const s = ui.spinner('Updating system query mapping');
|
|
314
|
+
try {
|
|
315
|
+
const data = await api.put(`/alert-rules/${id}/query-mapping`, body);
|
|
316
|
+
s.stop('System query mapping updated');
|
|
317
|
+
if (flags.json) { ui.json(data); return; }
|
|
318
|
+
console.log('');
|
|
319
|
+
ui.keyValue([
|
|
320
|
+
['Query mapping', data.queryMapping?.name || data.queryMapping?.id || '-'],
|
|
321
|
+
['Previous SQL hash', data.queryMapping?.previousSqlHash || '-'],
|
|
322
|
+
['New SQL hash', data.queryMapping?.newSqlHash || '-'],
|
|
323
|
+
['Affected system rules', String(data.affectedSystemRules ?? '-')],
|
|
324
|
+
['Reactivated paused copies', data.reactivatedPausedCopies ? 'yes' : 'no'],
|
|
325
|
+
]);
|
|
326
|
+
console.log('');
|
|
327
|
+
} catch (err) {
|
|
328
|
+
s.fail('Failed to update system query mapping');
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
222
333
|
async function alertRuleExclusions(args, flags) {
|
|
223
334
|
requireAuth();
|
|
224
335
|
const id = args[0];
|
package/cli.js
CHANGED
|
@@ -77,11 +77,21 @@ const COMMANDS = {
|
|
|
77
77
|
},
|
|
78
78
|
'api-key': {
|
|
79
79
|
desc: 'Manage the firewall API key stored in .securenow/credentials.json',
|
|
80
|
-
usage: 'securenow api-key <subcommand> [options]',
|
|
81
|
-
sub: {
|
|
82
|
-
|
|
83
|
-
desc: '
|
|
84
|
-
usage: 'securenow api-key
|
|
80
|
+
usage: 'securenow api-key <subcommand> [options]',
|
|
81
|
+
sub: {
|
|
82
|
+
create: {
|
|
83
|
+
desc: 'Create a firewall API key with your session token and save it',
|
|
84
|
+
usage: 'securenow api-key create [name] [--name <name>] [--preset firewall] [--global]',
|
|
85
|
+
flags: {
|
|
86
|
+
name: 'Human-readable key name',
|
|
87
|
+
preset: 'API key preset to create (default: firewall)',
|
|
88
|
+
global: 'Save to ~/.securenow/ instead of project-local',
|
|
89
|
+
},
|
|
90
|
+
run: (a, f) => require('./cli/apiKey').create(a, f),
|
|
91
|
+
},
|
|
92
|
+
set: {
|
|
93
|
+
desc: 'Save an API key (snk_live_...) to the credentials file',
|
|
94
|
+
usage: 'securenow api-key set <snk_live_...> [--global]',
|
|
85
95
|
flags: { global: 'Save to ~/.securenow/ instead of project-local' },
|
|
86
96
|
run: (a, f) => require('./cli/apiKey').set(a, f),
|
|
87
97
|
},
|
|
@@ -208,16 +218,30 @@ const COMMANDS = {
|
|
|
208
218
|
desc: 'Manage alerting',
|
|
209
219
|
usage: 'securenow alerts <subcommand> [options]',
|
|
210
220
|
sub: {
|
|
211
|
-
rules: {
|
|
212
|
-
desc: 'List, show, or
|
|
213
|
-
flags: {
|
|
214
|
-
json: 'Output as JSON',
|
|
215
|
-
'applications-all': 'With update: scope rule to all apps',
|
|
216
|
-
'no-applications-all': 'With update: scope to explicit --apps list',
|
|
217
|
-
apps: 'Comma-separated app keys (with update)',
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
rules: {
|
|
222
|
+
desc: 'List, show, update, test, or tune alert rules',
|
|
223
|
+
flags: {
|
|
224
|
+
json: 'Output as JSON',
|
|
225
|
+
'applications-all': 'With update: scope rule to all apps',
|
|
226
|
+
'no-applications-all': 'With update: scope to explicit --apps list',
|
|
227
|
+
apps: 'Comma-separated app keys (with update)',
|
|
228
|
+
app: 'Application key for rule tests',
|
|
229
|
+
mode: 'Rule test mode: dry_run or live',
|
|
230
|
+
wait: 'Wait for rule test completion',
|
|
231
|
+
sql: 'Candidate/replacement SQL, @file, or - for stdin',
|
|
232
|
+
query: 'Alias for --sql',
|
|
233
|
+
file: 'Read candidate/replacement SQL from a file',
|
|
234
|
+
reason: 'Audit reason for a write',
|
|
235
|
+
'apply-globally': 'Required for system query tuning',
|
|
236
|
+
'reactivate-paused': 'Reactivate paused system copies after tuning',
|
|
237
|
+
'expected-hash': 'Expected current SQL SHA-256 hash',
|
|
238
|
+
notification: 'Review notification id tied to tuning',
|
|
239
|
+
note: 'Review note tied to tuning',
|
|
240
|
+
yes: 'Confirm write prompts',
|
|
241
|
+
force: 'Confirm write prompts',
|
|
242
|
+
},
|
|
243
|
+
run: (a, f) => require('./cli/security').alertRulesRoute(a, f),
|
|
244
|
+
},
|
|
221
245
|
channels: { desc: 'List alert channels', run: (a, f) => require('./cli/security').alertChannelsList(a, f) },
|
|
222
246
|
history: { desc: 'View alert history', flags: { limit: 'Max results' }, run: (a, f) => require('./cli/security').alertHistoryList(a, f) },
|
|
223
247
|
},
|
|
@@ -255,7 +279,7 @@ const COMMANDS = {
|
|
|
255
279
|
},
|
|
256
280
|
ratelimit: {
|
|
257
281
|
desc: 'Manage soft rate-limit remediation rules',
|
|
258
|
-
usage: 'securenow ratelimit <list|add|show|test|enable|disable|remove> [options]',
|
|
282
|
+
usage: 'securenow ratelimit <list|add|parse|from-text|show|test|enable|disable|remove> [options]',
|
|
259
283
|
flags: {
|
|
260
284
|
app: 'Scope to app key (defaults to logged-in app)',
|
|
261
285
|
env: 'Scope to environment (default for create/test: production)',
|
|
@@ -269,10 +293,13 @@ const COMMANDS = {
|
|
|
269
293
|
mode: 'Path mode: exact, prefix, or regex',
|
|
270
294
|
method: 'HTTP method, or ALL',
|
|
271
295
|
reason: 'Audit reason',
|
|
296
|
+
text: 'Natural-language rate-limit request',
|
|
272
297
|
},
|
|
273
298
|
sub: {
|
|
274
299
|
list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
|
|
275
300
|
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) },
|
|
301
|
+
parse: { desc: 'Parse natural language into a rate-limit draft', usage: 'securenow ratelimit parse "rate limit /api/login to 2 attempts per minute"', run: (a, f) => require('./cli/rateLimits').parseText(a, f) },
|
|
302
|
+
'from-text': { desc: 'Create a rate-limit rule from natural language', usage: 'securenow ratelimit from-text "rate limit /api/login to 2 attempts per minute" --yes', run: (a, f) => require('./cli/rateLimits').fromText(a, f) },
|
|
276
303
|
show: { desc: 'Show one rate-limit rule', usage: 'securenow ratelimit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
|
|
277
304
|
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
305
|
enable: { desc: 'Enable a rate-limit rule', usage: 'securenow ratelimit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
|
|
@@ -283,10 +310,27 @@ const COMMANDS = {
|
|
|
283
310
|
},
|
|
284
311
|
'rate-limit': {
|
|
285
312
|
desc: 'Alias for ratelimit',
|
|
286
|
-
usage: 'securenow rate-limit <list|add|show|test|enable|disable|remove> [options]',
|
|
313
|
+
usage: 'securenow rate-limit <list|add|parse|from-text|show|test|enable|disable|remove> [options]',
|
|
314
|
+
flags: {
|
|
315
|
+
app: 'Scope to app key (defaults to logged-in app)',
|
|
316
|
+
env: 'Scope to environment (default for create/test: production)',
|
|
317
|
+
environment: 'Alias for --env',
|
|
318
|
+
json: 'Output as JSON',
|
|
319
|
+
limit: 'Allowed requests per window',
|
|
320
|
+
window: 'Window size, e.g. 30s, 1m, 1h',
|
|
321
|
+
duration: 'Expiry, e.g. 24h or 7d',
|
|
322
|
+
route: 'Path pattern such as /api/login',
|
|
323
|
+
path: 'Alias for --route',
|
|
324
|
+
mode: 'Path mode: exact, prefix, or regex',
|
|
325
|
+
method: 'HTTP method, or ALL',
|
|
326
|
+
reason: 'Audit reason',
|
|
327
|
+
text: 'Natural-language rate-limit request',
|
|
328
|
+
},
|
|
287
329
|
sub: {
|
|
288
330
|
list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
|
|
289
331
|
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) },
|
|
332
|
+
parse: { desc: 'Parse natural language into a rate-limit draft', usage: 'securenow rate-limit parse "rate limit /api/login to 2 attempts per minute"', run: (a, f) => require('./cli/rateLimits').parseText(a, f) },
|
|
333
|
+
'from-text': { desc: 'Create a rate-limit rule from natural language', usage: 'securenow rate-limit from-text "rate limit /api/login to 2 attempts per minute" --yes', run: (a, f) => require('./cli/rateLimits').fromText(a, f) },
|
|
290
334
|
show: { desc: 'Show one rate-limit rule', usage: 'securenow rate-limit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
|
|
291
335
|
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
336
|
enable: { desc: 'Enable a rate-limit rule', usage: 'securenow rate-limit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
|
package/mcp/catalog.js
CHANGED
|
@@ -169,7 +169,7 @@ const decisionReportInput = {
|
|
|
169
169
|
description: 'Structured audit report explaining the decision, evidence reviewed, trace IDs, missing proof, and recommendations.',
|
|
170
170
|
},
|
|
171
171
|
decisionSummary: string('Short decision summary to record on the IP/case history.'),
|
|
172
|
-
outcome: string('Decision outcome: blocked, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, or other.'),
|
|
172
|
+
outcome: string('Decision outcome: blocked, rate_limited, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, or other.'),
|
|
173
173
|
evidence: arrayOfStrings('Evidence strings that support the decision.'),
|
|
174
174
|
reviewedHistory: arrayOfStrings('History/proofs reviewed before deciding.'),
|
|
175
175
|
traceIds: arrayOfStrings('Trace IDs reviewed for this decision.'),
|
|
@@ -513,7 +513,7 @@ const TOOLS = [
|
|
|
513
513
|
{
|
|
514
514
|
name: 'securenow_human_action_decision_report_add',
|
|
515
515
|
title: 'Record Human Action Decision Report',
|
|
516
|
-
description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
|
|
516
|
+
description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for rate-limited, skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
|
|
517
517
|
scope: 'notifications:write',
|
|
518
518
|
readOnly: false,
|
|
519
519
|
confirm: true,
|
|
@@ -802,6 +802,36 @@ const TOOLS = [
|
|
|
802
802
|
...confirmSchema,
|
|
803
803
|
}, ['id', 'confirm', 'reason']),
|
|
804
804
|
},
|
|
805
|
+
{
|
|
806
|
+
name: 'securenow_rate_limit_parse',
|
|
807
|
+
title: 'Parse Rate Limit Text',
|
|
808
|
+
description: 'Use SecureNow AI to convert natural language into a rate-limit remediation draft without creating it.',
|
|
809
|
+
scope: 'rate_limits:read',
|
|
810
|
+
readOnly: true,
|
|
811
|
+
method: 'POST',
|
|
812
|
+
endpoint: '/rate-limits/parse',
|
|
813
|
+
bodyFields: ['text', 'draft'],
|
|
814
|
+
inputSchema: objectSchema({
|
|
815
|
+
text: string('Natural-language rate-limit request, e.g. "rate limit /api/login to 2 attempts per minute for this IP".'),
|
|
816
|
+
draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields to preserve or resolve phrases such as "this IP".' },
|
|
817
|
+
}, ['text']),
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: 'securenow_rate_limit_create_from_text',
|
|
821
|
+
title: 'Create Rate Limit From Text',
|
|
822
|
+
description: 'Create a rate-limit remediation rule from natural language. Write action; requires confirmation.',
|
|
823
|
+
scope: 'rate_limits:write',
|
|
824
|
+
readOnly: false,
|
|
825
|
+
confirm: true,
|
|
826
|
+
method: 'POST',
|
|
827
|
+
endpoint: '/rate-limits/from-text',
|
|
828
|
+
bodyFields: ['text', 'draft', 'reason'],
|
|
829
|
+
inputSchema: objectSchema({
|
|
830
|
+
text: string('Natural-language rate-limit request.'),
|
|
831
|
+
draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields or scope hints.' },
|
|
832
|
+
...confirmSchema,
|
|
833
|
+
}, ['text', 'confirm', 'reason']),
|
|
834
|
+
},
|
|
805
835
|
{
|
|
806
836
|
name: 'securenow_alert_rules_list',
|
|
807
837
|
title: 'List Alert Rules',
|
|
@@ -852,6 +882,29 @@ const TOOLS = [
|
|
|
852
882
|
...confirmSchema,
|
|
853
883
|
}, ['id', 'confirm', 'reason']),
|
|
854
884
|
},
|
|
885
|
+
{
|
|
886
|
+
name: 'securenow_alert_rule_query_update',
|
|
887
|
+
title: 'Update System Alert Rule Query',
|
|
888
|
+
description: 'Admin-only: update the shared public query mapping behind a system alert rule for all customer copies. Write action; requires confirmation.',
|
|
889
|
+
scope: 'alerts:write',
|
|
890
|
+
readOnly: false,
|
|
891
|
+
confirm: true,
|
|
892
|
+
method: 'PUT',
|
|
893
|
+
endpoint: '/alert-rules/:id/query-mapping',
|
|
894
|
+
pathParams: ['id'],
|
|
895
|
+
bodyFields: ['sqlQuery', 'reason', 'expectedCurrentSqlHash', 'applyGlobally', 'reactivatePausedCopies', 'reviewNotificationId', 'reviewNote', 'source'],
|
|
896
|
+
fixedBody: { applyGlobally: true, source: 'mcp' },
|
|
897
|
+
inputSchema: objectSchema({
|
|
898
|
+
id: string('Alert rule id. Must be a system rule.'),
|
|
899
|
+
sqlQuery: string('Full replacement SQL query. System SQL must keep __USER_APP_KEYS__ for tenant scoping.'),
|
|
900
|
+
expectedCurrentSqlHash: string('Optional SHA-256 hash of the current SQL to prevent stale overwrites.'),
|
|
901
|
+
applyGlobally: boolean('Must be true. Updates the shared public query mapping used by all system rule copies.'),
|
|
902
|
+
reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the query is tuned.'),
|
|
903
|
+
reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
|
|
904
|
+
reviewNote: string('Optional review note. Defaults to reason.'),
|
|
905
|
+
...confirmSchema,
|
|
906
|
+
}, ['id', 'sqlQuery', 'confirm', 'reason']),
|
|
907
|
+
},
|
|
855
908
|
{
|
|
856
909
|
name: 'securenow_alert_rule_test',
|
|
857
910
|
title: 'Test Alert Rule',
|
|
@@ -870,6 +923,25 @@ const TOOLS = [
|
|
|
870
923
|
...confirmSchema,
|
|
871
924
|
}, ['id', 'confirm', 'reason']),
|
|
872
925
|
},
|
|
926
|
+
{
|
|
927
|
+
name: 'securenow_alert_rule_candidate_test',
|
|
928
|
+
title: 'Dry-Run Candidate Alert Rule SQL',
|
|
929
|
+
description: 'Dry-run a candidate SQL/query guard for an alert rule without saving it.',
|
|
930
|
+
scope: 'alerts:write',
|
|
931
|
+
readOnly: false,
|
|
932
|
+
confirm: true,
|
|
933
|
+
method: 'POST',
|
|
934
|
+
endpoint: '/alert-rules/:id/test',
|
|
935
|
+
pathParams: ['id'],
|
|
936
|
+
bodyFields: ['applicationKey', 'candidateSqlQuery'],
|
|
937
|
+
fixedBody: { mode: 'dry_run' },
|
|
938
|
+
inputSchema: objectSchema({
|
|
939
|
+
id: string('Alert rule id.'),
|
|
940
|
+
applicationKey: string('Application key to test.'),
|
|
941
|
+
candidateSqlQuery: string('Full candidate SQL query to dry-run without saving.'),
|
|
942
|
+
...confirmSchema,
|
|
943
|
+
}, ['id', 'candidateSqlQuery', 'confirm', 'reason']),
|
|
944
|
+
},
|
|
873
945
|
{
|
|
874
946
|
name: 'securenow_alert_rule_test_result',
|
|
875
947
|
title: 'Get Alert Rule Test Result',
|
|
@@ -1386,9 +1458,11 @@ function promptMessages(name, args = {}) {
|
|
|
1386
1458
|
`Fetch page=${page}, limit=${limit} with securenow_human_actions_list, select row ${rowNumber}, then call securenow_notifications_get and securenow_human_action_report for that notificationId and IP.`,
|
|
1387
1459
|
'Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
|
|
1388
1460
|
'Open trace evidence with securenow_traces_show and correlated logs with securenow_logs_for_trace when trace IDs are available.',
|
|
1389
|
-
'Return one clear outcome: Block IP, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
|
|
1461
|
+
'Return one clear outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
|
|
1462
|
+
'Use rate limiting only as temporary soft remediation for repeated route-specific abuse such as login brute force, credential stuffing, scraping/API bursts, enumeration, recon/probing, or repeated noisy payloads where risk/impact evidence is below the block threshold.',
|
|
1463
|
+
'Do not rate-limit instead of blocking when there is confirmed exploit success, token/data exposure, SSRF reachability, file read, RCE, persistence, malware/C2, or riskScore >= 85 with high-confidence malicious evidence. Do not rate-limit benign false positives, trusted monitors, app-server/proxy attribution problems, isolated one-off requests, or broad noisy rules that need alert tuning.',
|
|
1390
1464
|
confirmWrites
|
|
1391
|
-
? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If you skip/mark ambiguous but still need to record the audit trail, call securenow_human_action_decision_report_add.'
|
|
1465
|
+
? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block, securenow_rate_limit_create_from_text plus securenow_human_action_decision_report_add(outcome=rate_limited), or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If you skip/mark ambiguous but still need to record the audit trail, call securenow_human_action_decision_report_add.'
|
|
1392
1466
|
: 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
|
|
1393
1467
|
'If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
|
|
1394
1468
|
'False positives must be narrow: app + alert rule + path + method/status/user-agent/body evidence where possible. Never globally trust an IP by default.',
|
|
@@ -1410,12 +1484,14 @@ function promptMessages(name, args = {}) {
|
|
|
1410
1484
|
'Work my SecureNow Requires Human queue like a senior security analyst using the MCP tools.',
|
|
1411
1485
|
`Review up to ${limit} row(s), most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
|
|
1412
1486
|
'Start with securenow_human_actions_list. For each row, call securenow_notifications_get and securenow_human_action_report, inspect the AI report/investigation steps/proofs/trace IDs, and fetch trace/log evidence where useful.',
|
|
1413
|
-
'For each row choose exactly one outcome: Block IP, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
|
|
1487
|
+
'For each row choose exactly one outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
|
|
1488
|
+
'Use Rate Limit only for repeated route-specific abuse where temporary friction is safer than blocking: login brute force/credential stuffing, password reset/account enumeration, scraping/API bursts, path or ID enumeration, recon/probing, or repeated noisy payloads without confirmed exploit success.',
|
|
1489
|
+
'Do not rate-limit confirmed high-risk attacks that should be blocked, benign traffic that should be false-positive scoped, broad noisy rules that need tuning, app-server/proxy attribution problems, or isolated one-off requests.',
|
|
1414
1490
|
confirmWrites
|
|
1415
1491
|
? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For skipped/ambiguous/rule-tuning-needed rows that should be auditable without changing IP status, use securenow_human_action_decision_report_add.'
|
|
1416
1492
|
: 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
|
|
1417
|
-
'For block decisions, use securenow_human_action_block. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update only when the action is safe to approve/reject.',
|
|
1418
|
-
'End with counts: handled, proposed block, proposed false positive, rule tuning needed, skipped, still waiting.',
|
|
1493
|
+
'For block decisions, use securenow_human_action_block. For rate-limit decisions, use securenow_rate_limit_create_from_text and then securenow_human_action_decision_report_add with outcome=rate_limited. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update only when the action is safe to approve/reject.',
|
|
1494
|
+
'End with counts: handled, proposed block, rate limits created/proposed, proposed false positive, rule tuning needed, skipped, still waiting.',
|
|
1419
1495
|
].join('\n'),
|
|
1420
1496
|
},
|
|
1421
1497
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.11",
|
|
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",
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
"./firewall": {
|
|
80
80
|
"default": "./firewall.js"
|
|
81
81
|
},
|
|
82
|
+
"./rate-limits": {
|
|
83
|
+
"types": "./rate-limits.d.ts",
|
|
84
|
+
"default": "./rate-limits.js"
|
|
85
|
+
},
|
|
82
86
|
"./firewall-only": {
|
|
83
87
|
"default": "./firewall-only.js"
|
|
84
88
|
},
|
|
@@ -128,6 +132,8 @@
|
|
|
128
132
|
"resolve-ip.js",
|
|
129
133
|
"cidr.js",
|
|
130
134
|
"firewall.js",
|
|
135
|
+
"rate-limits.js",
|
|
136
|
+
"rate-limits.d.ts",
|
|
131
137
|
"firewall-only.js",
|
|
132
138
|
"firewall-tcp.js",
|
|
133
139
|
"firewall-iptables.js",
|
package/rate-limits.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export interface RateLimitDraft {
|
|
2
|
+
name?: string;
|
|
3
|
+
appKey?: string;
|
|
4
|
+
applicationKey?: string;
|
|
5
|
+
environment?: string;
|
|
6
|
+
ip?: string;
|
|
7
|
+
pathPattern?: string;
|
|
8
|
+
path?: string;
|
|
9
|
+
route?: string;
|
|
10
|
+
pathMatchMode?: 'exact' | 'prefix' | 'regex';
|
|
11
|
+
method?: 'ALL' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
12
|
+
keyBy?: 'ip' | 'global';
|
|
13
|
+
limit?: number;
|
|
14
|
+
windowSeconds?: number;
|
|
15
|
+
duration?: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
status?: 'active' | 'disabled';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RateLimitAuthOptions {
|
|
21
|
+
token?: string;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
authToken?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RateLimitTextOptions extends RateLimitAuthOptions {
|
|
27
|
+
draft?: RateLimitDraft;
|
|
28
|
+
reason?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ParsedRateLimitText {
|
|
32
|
+
success: boolean;
|
|
33
|
+
draft: RateLimitDraft;
|
|
34
|
+
payload: Record<string, unknown>;
|
|
35
|
+
confidence: number;
|
|
36
|
+
missingFields: string[];
|
|
37
|
+
warnings: string[];
|
|
38
|
+
explanation?: string;
|
|
39
|
+
provider: 'anthropic' | 'local-fallback' | string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RateLimitRule {
|
|
43
|
+
id?: string;
|
|
44
|
+
_id?: string;
|
|
45
|
+
applicationKey?: string | null;
|
|
46
|
+
environment?: string | null;
|
|
47
|
+
name?: string;
|
|
48
|
+
ip?: string;
|
|
49
|
+
method?: string;
|
|
50
|
+
pathPattern?: string;
|
|
51
|
+
pathMatchMode?: string;
|
|
52
|
+
keyBy?: string;
|
|
53
|
+
limit: number;
|
|
54
|
+
windowSeconds: number;
|
|
55
|
+
reason?: string;
|
|
56
|
+
source?: string;
|
|
57
|
+
status?: string;
|
|
58
|
+
expiresAt?: string | null;
|
|
59
|
+
metadata?: Record<string, unknown>;
|
|
60
|
+
createdAt?: string;
|
|
61
|
+
updatedAt?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RateLimitResponse {
|
|
65
|
+
success: boolean;
|
|
66
|
+
rateLimit: RateLimitRule;
|
|
67
|
+
parsed?: ParsedRateLimitText;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseRateLimitText(text: string, options?: RateLimitTextOptions): Promise<ParsedRateLimitText>;
|
|
71
|
+
export function createRateLimitFromText(text: string, options?: RateLimitTextOptions): Promise<RateLimitResponse>;
|
|
72
|
+
export function createRateLimit(payload: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<RateLimitResponse>;
|
|
73
|
+
export function listRateLimits(query?: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<{
|
|
74
|
+
success: boolean;
|
|
75
|
+
rateLimits: RateLimitRule[];
|
|
76
|
+
total: number;
|
|
77
|
+
page: number;
|
|
78
|
+
limit: number;
|
|
79
|
+
}>;
|
|
80
|
+
export function testRateLimit(query?: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<{
|
|
81
|
+
success: boolean;
|
|
82
|
+
limited: boolean;
|
|
83
|
+
matches: RateLimitRule[];
|
|
84
|
+
}>;
|
|
85
|
+
export function updateRateLimit(id: string, payload?: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<RateLimitResponse>;
|
|
86
|
+
export function removeRateLimit(id: string, options?: RateLimitAuthOptions & { reason?: string }): Promise<RateLimitResponse>;
|
package/rate-limits.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { api } = require('./cli/client');
|
|
4
|
+
const config = require('./cli/config');
|
|
5
|
+
|
|
6
|
+
function cleanText(value) {
|
|
7
|
+
return String(value || '').trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function authToken(options = {}) {
|
|
11
|
+
return (
|
|
12
|
+
options.token ||
|
|
13
|
+
options.apiKey ||
|
|
14
|
+
options.authToken ||
|
|
15
|
+
process.env.SECURENOW_TOKEN ||
|
|
16
|
+
process.env.SECURENOW_API_KEY ||
|
|
17
|
+
config.getToken() ||
|
|
18
|
+
config.getApiKey() ||
|
|
19
|
+
undefined
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requestOptions(options = {}) {
|
|
24
|
+
const token = authToken(options);
|
|
25
|
+
return token ? { token } : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireText(text) {
|
|
29
|
+
const value = cleanText(text);
|
|
30
|
+
if (!value) throw new Error('text is required');
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function parseRateLimitText(text, options = {}) {
|
|
35
|
+
return api.post(
|
|
36
|
+
'/rate-limits/parse',
|
|
37
|
+
{
|
|
38
|
+
text: requireText(text),
|
|
39
|
+
draft: options.draft && typeof options.draft === 'object' ? options.draft : {},
|
|
40
|
+
},
|
|
41
|
+
requestOptions(options)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function createRateLimitFromText(text, options = {}) {
|
|
46
|
+
const body = {
|
|
47
|
+
text: requireText(text),
|
|
48
|
+
draft: options.draft && typeof options.draft === 'object' ? options.draft : {},
|
|
49
|
+
};
|
|
50
|
+
if (options.reason) body.reason = String(options.reason);
|
|
51
|
+
return api.post('/rate-limits/from-text', body, requestOptions(options));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function createRateLimit(payload, options = {}) {
|
|
55
|
+
return api.post('/rate-limits', payload || {}, requestOptions(options));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function listRateLimits(query = {}, options = {}) {
|
|
59
|
+
return api.get('/rate-limits', { query, ...(requestOptions(options) || {}) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function testRateLimit(query = {}, options = {}) {
|
|
63
|
+
return api.get('/rate-limits/check', { query, ...(requestOptions(options) || {}) });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function updateRateLimit(id, payload = {}, options = {}) {
|
|
67
|
+
if (!id) throw new Error('id is required');
|
|
68
|
+
return api.put(`/rate-limits/${encodeURIComponent(id)}`, payload, requestOptions(options));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function removeRateLimit(id, options = {}) {
|
|
72
|
+
if (!id) throw new Error('id is required');
|
|
73
|
+
const query = options.reason ? { reason: options.reason } : undefined;
|
|
74
|
+
return api.delete(`/rate-limits/${encodeURIComponent(id)}`, {
|
|
75
|
+
query,
|
|
76
|
+
...(requestOptions(options) || {}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
parseRateLimitText,
|
|
82
|
+
createRateLimitFromText,
|
|
83
|
+
createRateLimit,
|
|
84
|
+
listRateLimits,
|
|
85
|
+
testRateLimit,
|
|
86
|
+
updateRateLimit,
|
|
87
|
+
removeRateLimit,
|
|
88
|
+
};
|
package/resolve-ip.js
CHANGED
|
@@ -110,8 +110,9 @@ function resolveClientIpWithDetails(request) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
if (LOOPBACK_RE.test(socketIp)) {
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// A localhost request without proxy headers is app/server self-traffic.
|
|
114
|
+
// Rewriting it to the host's public IP can cause the app to block itself.
|
|
115
|
+
return { ...details, source: 'loopback' };
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
if (trustedProxy && getHostIps().has(socketIp)) {
|