securenow 5.10.2 → 5.11.1
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/CONSUMING-APPS-GUIDE.md +30 -0
- package/NPM_README.md +65 -0
- package/README.md +13 -0
- package/cidr.js +83 -0
- package/cli/auth.js +208 -208
- package/cli/config.js +117 -117
- package/cli/firewall.js +81 -0
- package/cli/fp.js +638 -0
- package/cli/security.js +4 -8
- package/cli.js +28 -1
- package/console-instrumentation.js +147 -147
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +40 -1
- package/docs/API-KEYS-GUIDE.md +215 -0
- package/docs/ENVIRONMENT-VARIABLES.md +880 -697
- package/docs/FIREWALL-GUIDE.md +388 -0
- package/docs/INDEX.md +8 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-tcp.js +58 -0
- package/firewall.js +235 -0
- package/free-trial-banner.js +174 -174
- package/nextjs-auto-capture.js +199 -199
- package/nextjs-middleware.js +186 -186
- package/nextjs-wrapper.js +158 -158
- package/nextjs.js +22 -2
- package/nuxt-server-plugin.mjs +400 -400
- package/package.json +30 -3
- package/resolve-ip.js +77 -0
- package/tracing.js +31 -56
- package/web-vite.mjs +239 -239
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# SecureNow Firewall — Automatic IP Blocking for Node.js
|
|
2
|
+
|
|
3
|
+
Block malicious IPs at your application layer with zero code changes. The firewall syncs your SecureNow blocklist and enforces it across up to four network layers — from HTTP 403 responses all the way down to kernel-level packet drops and cloud-edge WAF rules.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Internet Traffic
|
|
11
|
+
│
|
|
12
|
+
▼
|
|
13
|
+
┌──────────────────────────┐
|
|
14
|
+
│ Layer 4: Cloud/Edge WAF │ Blocked at CDN (Cloudflare, AWS WAF, GCP Cloud Armor)
|
|
15
|
+
└──────────┬───────────────┘
|
|
16
|
+
▼
|
|
17
|
+
┌──────────────────────────┐
|
|
18
|
+
│ Layer 3: OS Firewall │ Dropped at kernel (iptables/nftables)
|
|
19
|
+
└──────────┬───────────────┘
|
|
20
|
+
▼
|
|
21
|
+
┌──────────────────────────┐
|
|
22
|
+
│ Layer 2: TCP Socket │ socket.destroy() — zero bytes sent back
|
|
23
|
+
└──────────┬───────────────┘
|
|
24
|
+
▼
|
|
25
|
+
┌──────────────────────────┐
|
|
26
|
+
│ Layer 1: HTTP Handler │ 403 Forbidden JSON response
|
|
27
|
+
└──────────┬───────────────┘
|
|
28
|
+
▼
|
|
29
|
+
Your App
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
All layers share the same in-memory blocklist, synced every 60 seconds from the SecureNow API. Layer 1 (HTTP) is always active. Layers 2–4 are opt-in via environment variables.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### 1. Get an API Key
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Log in to SecureNow
|
|
42
|
+
npx securenow login
|
|
43
|
+
|
|
44
|
+
# View your firewall status and API key
|
|
45
|
+
npx securenow firewall status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or create an API key from the dashboard: **Settings → API Keys → Create Key** with the `firewall:read` scope.
|
|
49
|
+
|
|
50
|
+
### 2. Add the Key to Your Environment
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# .env
|
|
54
|
+
SECURENOW_API_KEY=snk_live_abc123...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Start Your App
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
node -r securenow/register app.js
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That's it. The firewall auto-activates when `SECURENOW_API_KEY` is present. You'll see:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
[securenow] Firewall: ENABLED
|
|
67
|
+
[securenow] Firewall: Layer 1 (HTTP 403) active
|
|
68
|
+
[securenow] Firewall: Layer 2 (TCP drop) disabled (set SECURENOW_FIREWALL_TCP=1)
|
|
69
|
+
[securenow] Firewall: Layer 3 (iptables) disabled (set SECURENOW_FIREWALL_IPTABLES=1)
|
|
70
|
+
[securenow] Firewall: Layer 4 (Cloud WAF) disabled (set SECURENOW_FIREWALL_CLOUD=cloudflare|aws|gcp)
|
|
71
|
+
[securenow] Firewall: synced 142 blocked IPs (138 exact + 4 CIDR ranges)
|
|
72
|
+
[securenow] Firewall: next sync in 60s
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Layers In Detail
|
|
78
|
+
|
|
79
|
+
### Layer 1: HTTP Handler (default — always on)
|
|
80
|
+
|
|
81
|
+
Intercepts every incoming HTTP/HTTPS request. Resolves the real client IP (respects `X-Forwarded-For` from trusted proxies), checks against the blocklist, and returns a 403 JSON response if blocked.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{ "error": "Forbidden" }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Works with every framework: Express, Fastify, Next.js, NestJS, Koa, Hapi, raw `http.createServer`, etc.
|
|
88
|
+
|
|
89
|
+
**Customize the status code:**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
SECURENOW_FIREWALL_STATUS_CODE=429
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Layer 2: TCP Socket (`SECURENOW_FIREWALL_TCP=1`)
|
|
96
|
+
|
|
97
|
+
Destroys the TCP connection before HTTP parsing starts. Zero bytes sent back to the attacker — they see a connection reset, not a response.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
SECURENOW_FIREWALL_TCP=1
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Caveat:** TCP-level only sees the direct connection IP (no proxy headers). Connections from known proxy IPs are let through to Layer 1 for proper header-based resolution. Most effective for direct-to-server deployments.
|
|
104
|
+
|
|
105
|
+
### Layer 3: OS Firewall (`SECURENOW_FIREWALL_IPTABLES=1`)
|
|
106
|
+
|
|
107
|
+
Manages a dedicated `SECURENOW_BLOCK` iptables/nftables chain. True kernel-level `DROP` — packets never reach Node.js.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
SECURENOW_FIREWALL_IPTABLES=1
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Requirements:**
|
|
114
|
+
- Linux only (skips gracefully on macOS/Windows)
|
|
115
|
+
- Requires `root` or `CAP_NET_ADMIN` capability
|
|
116
|
+
- Auto-detects nftables vs iptables
|
|
117
|
+
- Dedicated chain — never touches your existing rules
|
|
118
|
+
- Max 10,000 rules (configurable)
|
|
119
|
+
- Full cleanup on process shutdown (SIGINT/SIGTERM)
|
|
120
|
+
|
|
121
|
+
### Layer 4: Cloud/Edge WAF (`SECURENOW_FIREWALL_CLOUD=<provider>`)
|
|
122
|
+
|
|
123
|
+
Pushes the blocklist to your cloud WAF. Traffic is blocked at the CDN edge before it reaches your server.
|
|
124
|
+
|
|
125
|
+
#### Cloudflare
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
SECURENOW_FIREWALL_CLOUD=cloudflare
|
|
129
|
+
CLOUDFLARE_API_TOKEN=your-token
|
|
130
|
+
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Creates/updates an IP List named `securenow-blocklist` with a WAF custom rule.
|
|
134
|
+
|
|
135
|
+
#### AWS WAF
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
SECURENOW_FIREWALL_CLOUD=aws
|
|
139
|
+
AWS_WAF_IP_SET_ID=your-ip-set-id
|
|
140
|
+
AWS_WAF_IP_SET_NAME=securenow-blocklist # optional, default
|
|
141
|
+
AWS_WAF_SCOPE=REGIONAL # or CLOUDFRONT
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Requires `@aws-sdk/client-wafv2` installed as a peer dependency:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm install @aws-sdk/client-wafv2
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### GCP Cloud Armor
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
SECURENOW_FIREWALL_CLOUD=gcp
|
|
154
|
+
GCP_PROJECT_ID=your-project
|
|
155
|
+
GCP_SECURITY_POLICY=your-policy-name
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Requires `@google-cloud/compute` installed as a peer dependency:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm install @google-cloud/compute
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### Dry-Run Mode
|
|
165
|
+
|
|
166
|
+
Test cloud pushes without applying changes:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
SECURENOW_FIREWALL_CLOUD_DRY_RUN=1
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Environment Variables Reference
|
|
175
|
+
|
|
176
|
+
| Variable | Default | Description |
|
|
177
|
+
|----------|---------|-------------|
|
|
178
|
+
| `SECURENOW_API_KEY` | *(required)* | API key with `firewall:read` scope |
|
|
179
|
+
| `SECURENOW_API_URL` | `https://api.securenow.ai` | API base URL |
|
|
180
|
+
| `SECURENOW_FIREWALL_ENABLED` | `1` | Master kill-switch (`0` to disable) |
|
|
181
|
+
| `SECURENOW_FIREWALL_SYNC_INTERVAL` | `60` | Seconds between blocklist refreshes |
|
|
182
|
+
| `SECURENOW_FIREWALL_FAIL_MODE` | `open` | `open` = allow when list unavailable; `closed` = block all |
|
|
183
|
+
| `SECURENOW_FIREWALL_STATUS_CODE` | `403` | HTTP status code for blocked requests (Layer 1) |
|
|
184
|
+
| `SECURENOW_FIREWALL_LOG` | `1` | Log blocked requests to console |
|
|
185
|
+
| `SECURENOW_FIREWALL_TCP` | `0` | Enable Layer 2 TCP blocking |
|
|
186
|
+
| `SECURENOW_FIREWALL_IPTABLES` | `0` | Enable Layer 3 iptables/nftables blocking |
|
|
187
|
+
| `SECURENOW_FIREWALL_CLOUD` | *(none)* | Cloud WAF provider: `cloudflare`, `aws`, or `gcp` |
|
|
188
|
+
| `SECURENOW_FIREWALL_CLOUD_DRY_RUN` | `0` | Log cloud pushes without applying |
|
|
189
|
+
| `SECURENOW_TRUSTED_PROXIES` | *(none)* | Comma-separated trusted proxy IPs |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Framework Examples
|
|
194
|
+
|
|
195
|
+
### Express.js
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# .env
|
|
199
|
+
SECURENOW_APPID=my-express-app
|
|
200
|
+
SECURENOW_INSTANCE=https://freetrial.securenow.ai:4318
|
|
201
|
+
SECURENOW_API_KEY=snk_live_abc123...
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
node -r securenow/register app.js
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
No code changes needed. The firewall patches `http.createServer` before Express starts.
|
|
209
|
+
|
|
210
|
+
### Next.js
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# .env.local
|
|
214
|
+
SECURENOW_APPID=my-nextjs-app
|
|
215
|
+
SECURENOW_INSTANCE=https://freetrial.securenow.ai:4318
|
|
216
|
+
SECURENOW_API_KEY=snk_live_abc123...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Works automatically with `instrumentation.ts`:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
export async function register() {
|
|
223
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
224
|
+
await import('securenow/register');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### PM2 Cluster
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
// ecosystem.config.cjs
|
|
233
|
+
module.exports = {
|
|
234
|
+
apps: [{
|
|
235
|
+
name: 'my-app',
|
|
236
|
+
script: './app.js',
|
|
237
|
+
instances: 4,
|
|
238
|
+
node_args: '-r securenow/register',
|
|
239
|
+
env: {
|
|
240
|
+
SECURENOW_APPID: 'my-app',
|
|
241
|
+
SECURENOW_INSTANCE: 'https://freetrial.securenow.ai:4318',
|
|
242
|
+
SECURENOW_API_KEY: 'snk_live_abc123...',
|
|
243
|
+
SECURENOW_NO_UUID: '1',
|
|
244
|
+
SECURENOW_FIREWALL_TCP: '1',
|
|
245
|
+
}
|
|
246
|
+
}]
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Docker
|
|
251
|
+
|
|
252
|
+
```dockerfile
|
|
253
|
+
ENV SECURENOW_APPID=my-app
|
|
254
|
+
ENV SECURENOW_INSTANCE=https://freetrial.securenow.ai:4318
|
|
255
|
+
ENV SECURENOW_API_KEY=snk_live_abc123...
|
|
256
|
+
ENV SECURENOW_FIREWALL_TCP=1
|
|
257
|
+
|
|
258
|
+
CMD ["node", "-r", "securenow/register", "app.js"]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## CLI Commands
|
|
264
|
+
|
|
265
|
+
### Check Firewall Status
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npx securenow firewall status
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Shows: enabled/disabled, active layers, last sync time, blocked IP count, and API key info.
|
|
272
|
+
|
|
273
|
+
### Test an IP
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npx securenow firewall test-ip 203.0.113.42
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Check whether a specific IP would be blocked by the current blocklist.
|
|
280
|
+
|
|
281
|
+
### Manage the Blocklist
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# List blocked IPs
|
|
285
|
+
npx securenow blocklist
|
|
286
|
+
|
|
287
|
+
# Block an IP
|
|
288
|
+
npx securenow blocklist add 203.0.113.42 --reason "Brute force"
|
|
289
|
+
|
|
290
|
+
# Unblock
|
|
291
|
+
npx securenow blocklist remove <id>
|
|
292
|
+
|
|
293
|
+
# Statistics
|
|
294
|
+
npx securenow blocklist stats
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Changes take effect on the next sync interval (default 60 seconds).
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Security Considerations
|
|
302
|
+
|
|
303
|
+
### Fail-Open vs Fail-Closed
|
|
304
|
+
|
|
305
|
+
By default, the firewall operates in **fail-open** mode: if the API is unreachable, all traffic is allowed. This prevents the firewall from accidentally blocking legitimate traffic due to a network issue.
|
|
306
|
+
|
|
307
|
+
For high-security environments, set `SECURENOW_FIREWALL_FAIL_MODE=closed` to block all traffic when the blocklist is unavailable.
|
|
308
|
+
|
|
309
|
+
### IP Resolution
|
|
310
|
+
|
|
311
|
+
The firewall uses the same trusted-proxy-aware IP resolution as SecureNow tracing:
|
|
312
|
+
|
|
313
|
+
- Only reads `X-Forwarded-For` / `X-Real-IP` when the direct connection comes from a private/trusted IP
|
|
314
|
+
- Walks the header chain from right to left to find the first non-proxy IP
|
|
315
|
+
- Configure additional trusted proxies via `SECURENOW_TRUSTED_PROXIES`
|
|
316
|
+
|
|
317
|
+
### API Key Security
|
|
318
|
+
|
|
319
|
+
- The firewall API key only needs the `firewall:read` scope
|
|
320
|
+
- Store it in environment variables or `.env` — never commit it to source control
|
|
321
|
+
- The key is hashed (SHA-256) on the server — SecureNow never stores the plaintext
|
|
322
|
+
|
|
323
|
+
### Cleanup on Shutdown
|
|
324
|
+
|
|
325
|
+
All layers clean up on process exit (SIGINT/SIGTERM):
|
|
326
|
+
- Layer 1: restores original `http.createServer`
|
|
327
|
+
- Layer 2: restores original `net.Server.prototype.listen`
|
|
328
|
+
- Layer 3: removes the `SECURENOW_BLOCK` iptables/nftables chain
|
|
329
|
+
- Layer 4: no cleanup needed (cloud rules persist intentionally)
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Troubleshooting
|
|
334
|
+
|
|
335
|
+
### Firewall Not Activating
|
|
336
|
+
|
|
337
|
+
**Check 1:** Is `SECURENOW_API_KEY` set?
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
echo $SECURENOW_API_KEY
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Check 2:** Is the firewall disabled?
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# Must NOT be set to 0
|
|
347
|
+
echo $SECURENOW_FIREWALL_ENABLED
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Check 3:** Check the startup log for sync errors:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
[securenow] Firewall: initial sync failed: API returned 401
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
This usually means the API key is invalid or missing the `firewall:read` scope.
|
|
357
|
+
|
|
358
|
+
### IPs Not Being Blocked
|
|
359
|
+
|
|
360
|
+
**Check 1:** Is the IP actually in your blocklist?
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
npx securenow blocklist
|
|
364
|
+
npx securenow firewall test-ip 1.2.3.4
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Check 2:** Are you behind a proxy? The firewall needs to see the real client IP. Set `SECURENOW_TRUSTED_PROXIES` to your proxy's IP.
|
|
368
|
+
|
|
369
|
+
**Check 3:** Is the sync interval too long? Reduce it:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
SECURENOW_FIREWALL_SYNC_INTERVAL=10
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### iptables Layer Not Working
|
|
376
|
+
|
|
377
|
+
- Must be on Linux
|
|
378
|
+
- Must run as root or with `CAP_NET_ADMIN`
|
|
379
|
+
- Check: `iptables -L SECURENOW_BLOCK` (should show the chain)
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Related Documentation
|
|
384
|
+
|
|
385
|
+
- [API Keys Guide](./API-KEYS-GUIDE.md) — Creating and managing API keys
|
|
386
|
+
- [Environment Variables Reference](./ENVIRONMENT-VARIABLES.md) — All configuration options
|
|
387
|
+
- [All Frameworks Quick Start](./ALL-FRAMEWORKS-QUICKSTART.md) — Framework setup guides
|
|
388
|
+
- [Automatic IP Capture](./AUTOMATIC-IP-CAPTURE.md) — How client IPs are resolved
|
package/docs/INDEX.md
CHANGED
|
@@ -78,8 +78,10 @@ Complete documentation for SecureNow - OpenTelemetry instrumentation for Node.js
|
|
|
78
78
|
- **[Next.js Body Capture Comparison](NEXTJS-BODY-CAPTURE-COMPARISON.md)** - Compare different approaches
|
|
79
79
|
- **[Next.js Wrapper Approach](NEXTJS-WRAPPER-APPROACH.md)** - Using wrapper functions
|
|
80
80
|
|
|
81
|
-
### 🛡️ Security &
|
|
81
|
+
### 🛡️ Security & Protection
|
|
82
82
|
|
|
83
|
+
- **[Firewall Guide](FIREWALL-GUIDE.md)** - Automatic IP blocking (multi-layer: HTTP, TCP, iptables, Cloud WAF)
|
|
84
|
+
- **[API Keys Guide](API-KEYS-GUIDE.md)** - Creating, managing, and securing API keys
|
|
83
85
|
- **[Redaction Examples](REDACTION-EXAMPLES.md)** - How sensitive data is redacted
|
|
84
86
|
- **[Automatic IP Capture](AUTOMATIC-IP-CAPTURE.md)** - IP address and metadata collection
|
|
85
87
|
|
|
@@ -136,6 +138,11 @@ Complete documentation for SecureNow - OpenTelemetry instrumentation for Node.js
|
|
|
136
138
|
1. Read [Express Body Capture](EXPRESS-BODY-CAPTURE.md)
|
|
137
139
|
2. Check [Request Body Capture](REQUEST-BODY-CAPTURE.md) for general info
|
|
138
140
|
|
|
141
|
+
### "I want to block malicious IPs automatically"
|
|
142
|
+
1. Start with [Firewall Guide](FIREWALL-GUIDE.md)
|
|
143
|
+
2. Create an API key: [API Keys Guide](API-KEYS-GUIDE.md)
|
|
144
|
+
3. Configure environment variables: [Environment Variables Reference](ENVIRONMENT-VARIABLES.md)
|
|
145
|
+
|
|
139
146
|
### "I need to capture IP addresses and headers"
|
|
140
147
|
1. Read [Automatic IP Capture](AUTOMATIC-IP-CAPTURE.md)
|
|
141
148
|
2. Understand redaction: [Redaction Examples](REDACTION-EXAMPLES.md)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layer 4: Cloud/Edge WAF integration.
|
|
5
|
+
* Pushes the blocklist to the customer's cloud WAF provider so traffic is
|
|
6
|
+
* blocked at the CDN/edge before it ever reaches the origin server.
|
|
7
|
+
*
|
|
8
|
+
* Supported providers: cloudflare, aws, gcp
|
|
9
|
+
* Cloud SDKs are optional peer dependencies — only required when enabled.
|
|
10
|
+
* Cloud credentials are never logged.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const https = require('https');
|
|
14
|
+
|
|
15
|
+
let _options = null;
|
|
16
|
+
let _active = false;
|
|
17
|
+
let _provider = null;
|
|
18
|
+
let _lastPushTime = 0;
|
|
19
|
+
|
|
20
|
+
const MIN_PUSH_INTERVAL_MS = 30000;
|
|
21
|
+
|
|
22
|
+
// ────── Cloudflare ──────
|
|
23
|
+
|
|
24
|
+
async function cloudflareSync(ips) {
|
|
25
|
+
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
26
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
27
|
+
if (!token || !accountId) throw new Error('Missing CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID');
|
|
28
|
+
|
|
29
|
+
const listName = 'securenow-blocklist';
|
|
30
|
+
|
|
31
|
+
const lists = await cfApi('GET', `/accounts/${accountId}/rules/lists`, token);
|
|
32
|
+
let list = lists.result?.find(l => l.name === listName);
|
|
33
|
+
|
|
34
|
+
if (!list) {
|
|
35
|
+
const created = await cfApi('POST', `/accounts/${accountId}/rules/lists`, token, {
|
|
36
|
+
name: listName,
|
|
37
|
+
kind: 'ip',
|
|
38
|
+
description: 'Managed by SecureNow firewall SDK',
|
|
39
|
+
});
|
|
40
|
+
list = created.result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const items = ips.map(ip => ({ ip: ip.includes('/') ? ip : ip + '/32' }));
|
|
44
|
+
|
|
45
|
+
await cfApi('PUT', `/accounts/${accountId}/rules/lists/${list.id}/items`, token, items);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cfApi(method, path, token, body) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const data = body ? JSON.stringify(body) : null;
|
|
51
|
+
const req = https.request({
|
|
52
|
+
hostname: 'api.cloudflare.com',
|
|
53
|
+
path: '/client/v4' + path,
|
|
54
|
+
method,
|
|
55
|
+
headers: {
|
|
56
|
+
'Authorization': `Bearer ${token}`,
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
59
|
+
},
|
|
60
|
+
timeout: 15000,
|
|
61
|
+
}, (res) => {
|
|
62
|
+
let chunks = '';
|
|
63
|
+
res.on('data', c => chunks += c);
|
|
64
|
+
res.on('end', () => {
|
|
65
|
+
try { resolve(JSON.parse(chunks)); } catch (e) { reject(new Error(`CF parse error: ${chunks.slice(0, 200)}`)); }
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Cloudflare API timeout')); });
|
|
70
|
+
if (data) req.write(data);
|
|
71
|
+
req.end();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ────── AWS WAF ──────
|
|
76
|
+
|
|
77
|
+
async function awsSync(ips) {
|
|
78
|
+
let WAFV2;
|
|
79
|
+
try {
|
|
80
|
+
WAFV2 = require('@aws-sdk/client-wafv2');
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error('AWS WAF requires @aws-sdk/client-wafv2 — install it: npm i @aws-sdk/client-wafv2');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ipSetId = process.env.AWS_WAF_IP_SET_ID;
|
|
86
|
+
const ipSetName = process.env.AWS_WAF_IP_SET_NAME || 'securenow-blocklist';
|
|
87
|
+
const scope = process.env.AWS_WAF_SCOPE || 'REGIONAL';
|
|
88
|
+
if (!ipSetId) throw new Error('Missing AWS_WAF_IP_SET_ID');
|
|
89
|
+
|
|
90
|
+
const client = new WAFV2.WAFV2Client({});
|
|
91
|
+
|
|
92
|
+
const { IPSet, LockToken } = await client.send(new WAFV2.GetIPSetCommand({
|
|
93
|
+
Name: ipSetName,
|
|
94
|
+
Scope: scope,
|
|
95
|
+
Id: ipSetId,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
const addresses = ips.map(ip => ip.includes('/') ? ip : ip + '/32');
|
|
99
|
+
|
|
100
|
+
await client.send(new WAFV2.UpdateIPSetCommand({
|
|
101
|
+
Name: ipSetName,
|
|
102
|
+
Scope: scope,
|
|
103
|
+
Id: ipSetId,
|
|
104
|
+
Addresses: addresses,
|
|
105
|
+
LockToken,
|
|
106
|
+
Description: 'Managed by SecureNow firewall SDK',
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ────── GCP Cloud Armor ──────
|
|
111
|
+
|
|
112
|
+
async function gcpSync(ips) {
|
|
113
|
+
let compute;
|
|
114
|
+
try {
|
|
115
|
+
compute = require('@google-cloud/compute');
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error('GCP Cloud Armor requires @google-cloud/compute — install it: npm i @google-cloud/compute');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const project = process.env.GCP_PROJECT_ID;
|
|
121
|
+
const policyName = process.env.GCP_SECURITY_POLICY;
|
|
122
|
+
if (!project || !policyName) throw new Error('Missing GCP_PROJECT_ID or GCP_SECURITY_POLICY');
|
|
123
|
+
|
|
124
|
+
const client = new compute.SecurityPoliciesClient();
|
|
125
|
+
const rulePriority = 2000;
|
|
126
|
+
|
|
127
|
+
const srcRanges = ips.map(ip => ip.includes('/') ? ip : ip + '/32');
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await client.patchRule({
|
|
131
|
+
project,
|
|
132
|
+
securityPolicy: policyName,
|
|
133
|
+
priority: String(rulePriority),
|
|
134
|
+
securityPolicyRuleResource: {
|
|
135
|
+
priority: rulePriority,
|
|
136
|
+
action: 'deny(403)',
|
|
137
|
+
match: { versionedExpr: 'SRC_IPS_V1', config: { srcIpRanges: srcRanges } },
|
|
138
|
+
description: 'Managed by SecureNow firewall SDK',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (e.code === 404 || e.message?.includes('not found')) {
|
|
143
|
+
await client.addRule({
|
|
144
|
+
project,
|
|
145
|
+
securityPolicy: policyName,
|
|
146
|
+
securityPolicyRuleResource: {
|
|
147
|
+
priority: rulePriority,
|
|
148
|
+
action: 'deny(403)',
|
|
149
|
+
match: { versionedExpr: 'SRC_IPS_V1', config: { srcIpRanges: srcRanges } },
|
|
150
|
+
description: 'Managed by SecureNow firewall SDK',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ────── Provider dispatch ──────
|
|
160
|
+
|
|
161
|
+
const PROVIDERS = {
|
|
162
|
+
cloudflare: cloudflareSync,
|
|
163
|
+
aws: awsSync,
|
|
164
|
+
gcp: gcpSync,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ────── Public API ──────
|
|
168
|
+
|
|
169
|
+
function init(options) {
|
|
170
|
+
_options = options;
|
|
171
|
+
const provider = (options.cloud || '').toLowerCase();
|
|
172
|
+
|
|
173
|
+
if (!PROVIDERS[provider]) {
|
|
174
|
+
if (_options.log) console.warn('[securenow] Firewall cloud: unknown provider "%s", expected cloudflare|aws|gcp', provider);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_provider = provider;
|
|
179
|
+
_active = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Called by the firewall core after each successful blocklist sync.
|
|
184
|
+
* Throttled to max 1 push per MIN_PUSH_INTERVAL_MS.
|
|
185
|
+
* @param {string[]} ips - Array of IPs and CIDRs to push
|
|
186
|
+
*/
|
|
187
|
+
async function sync(ips) {
|
|
188
|
+
if (!_active || !_provider) return;
|
|
189
|
+
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
if (now - _lastPushTime < MIN_PUSH_INTERVAL_MS) return;
|
|
192
|
+
_lastPushTime = now;
|
|
193
|
+
|
|
194
|
+
const dryRun = process.env.SECURENOW_FIREWALL_CLOUD_DRY_RUN === '1';
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
if (_options.log) console.log('[securenow] Firewall cloud: dry-run — would push %d IPs to %s', ips.length, _provider);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await PROVIDERS[_provider](ips);
|
|
202
|
+
if (_options.log) console.log('[securenow] Firewall cloud: pushed %d IPs to %s', ips.length, _provider);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
if (_options.log) console.warn('[securenow] Firewall cloud: push to %s failed:', _provider, e.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shutdown() {
|
|
209
|
+
_active = false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { init, sync, shutdown };
|