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.
@@ -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 & Privacy
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 };