securenow 5.16.2 → 5.17.0
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/SKILL-API.md +3 -0
- package/SKILL-CLI.md +14 -1
- package/cli/auth.js +19 -4
- package/cli.js +1 -1
- package/docs/FIREWALL-GUIDE.md +15 -0
- package/firewall.js +372 -243
- package/package.json +1 -1
package/SKILL-API.md
CHANGED
|
@@ -460,6 +460,7 @@ Add custom fields via `SECURENOW_SENSITIVE_FIELDS=field1,field2`.
|
|
|
460
460
|
| `SECURENOW_API_KEY` | API key (`snk_live_...`); activates firewall when set | — |
|
|
461
461
|
| `SECURENOW_API_URL` | SecureNow API base URL | `https://api.securenow.ai` |
|
|
462
462
|
| `SECURENOW_FIREWALL_ENABLED` | Master kill-switch (`0` to disable) | `1` |
|
|
463
|
+
| `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between lightweight version checks | `10` |
|
|
463
464
|
| `SECURENOW_FIREWALL_SYNC_INTERVAL` | Full blocklist refresh interval in seconds | `300` |
|
|
464
465
|
| `SECURENOW_FIREWALL_FAIL_MODE` | `open` (allow all when unavailable) or `closed` | `open` |
|
|
465
466
|
| `SECURENOW_FIREWALL_STATUS_CODE` | HTTP status for blocked requests | `403` |
|
|
@@ -470,6 +471,8 @@ Add custom fields via `SECURENOW_SENSITIVE_FIELDS=field1,field2`.
|
|
|
470
471
|
| `SECURENOW_FIREWALL_CLOUD_DRY_RUN` | `1` to log cloud pushes without applying | `0` |
|
|
471
472
|
| `SECURENOW_TRUSTED_PROXIES` | Comma-separated trusted proxy IPs | — |
|
|
472
473
|
|
|
474
|
+
**Resilience:** The firewall SDK includes a circuit breaker (opens after 5 consecutive errors, 2-min cooldown), in-flight request guards (prevents overlapping requests), 429 Retry-After support, and exponential backoff on both version checks and initial sync retries.
|
|
475
|
+
|
|
473
476
|
### Cloud WAF Provider Variables
|
|
474
477
|
|
|
475
478
|
| Provider | Variables |
|
package/SKILL-CLI.md
CHANGED
|
@@ -336,7 +336,7 @@ securenow instances test <id> # test connection
|
|
|
336
336
|
|
|
337
337
|
```bash
|
|
338
338
|
securenow notifications list --limit 5 --json
|
|
339
|
-
|
|
339
|
+
# IMPORTANT: parse the ipInvestigations array for each notification — see statuses below
|
|
340
340
|
securenow ip <attacker-ip> --json
|
|
341
341
|
securenow ip traces <attacker-ip> --json
|
|
342
342
|
securenow traces show <trace-id> --json
|
|
@@ -345,6 +345,19 @@ securenow traces analyze <trace-id> --json
|
|
|
345
345
|
securenow blocklist add <attacker-ip> --reason "Automated: SQL injection from issue #<id>"
|
|
346
346
|
```
|
|
347
347
|
|
|
348
|
+
#### Notification IP Investigation Statuses
|
|
349
|
+
|
|
350
|
+
Each notification contains an `ipInvestigations` array. **Every IP has its own `status` field** that you MUST check before taking action or raising flags. Do not rely only on top-level notification fields — always cross-reference each IP's individual investigation status.
|
|
351
|
+
|
|
352
|
+
| Status | Meaning | Action |
|
|
353
|
+
|--------|---------|--------|
|
|
354
|
+
| `open` | Still needs review — no determination yet | Investigate: run `securenow ip <ip>`, check traces, decide to block or dismiss |
|
|
355
|
+
| `false_positive` | Trusted/dismissed by exclusion rule, verified bot, or trusted IP list | **Skip** — greyed out in UI, no action needed |
|
|
356
|
+
| `blocked` | Already on the user's blocklist | **Skip** — already handled |
|
|
357
|
+
| `clean` | Pipeline analyzed and cleared as benign | **Skip** — verified safe |
|
|
358
|
+
|
|
359
|
+
**Critical workflow rule:** When investigating notifications, iterate `ipInvestigations[]` and **only flag or act on IPs with `status: "open"`**. IPs marked `false_positive`, `blocked`, or `clean` have already been triaged — do not re-flag them.
|
|
360
|
+
|
|
348
361
|
### Triage and Suppress a False Positive
|
|
349
362
|
|
|
350
363
|
```bash
|
package/cli/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const http = require('http');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const { execFileSync } = require('child_process');
|
|
5
6
|
const config = require('./config');
|
|
6
7
|
const { api, CLIError } = require('./client');
|
|
@@ -39,16 +40,18 @@ function decodeJwtPayload(token) {
|
|
|
39
40
|
|
|
40
41
|
async function loginWithBrowser() {
|
|
41
42
|
const appUrl = config.getAppUrl();
|
|
43
|
+
const nonce = crypto.randomBytes(24).toString('base64url');
|
|
42
44
|
|
|
43
45
|
return new Promise((resolve, reject) => {
|
|
44
46
|
let pendingToken = null;
|
|
45
47
|
|
|
46
48
|
const server = http.createServer((req, res) => {
|
|
47
|
-
const url = new URL(req.url, `http://
|
|
49
|
+
const url = new URL(req.url, `http://127.0.0.1`);
|
|
48
50
|
|
|
49
51
|
if (url.pathname === '/callback') {
|
|
50
52
|
const token = url.searchParams.get('token');
|
|
51
53
|
const error = url.searchParams.get('error');
|
|
54
|
+
const returnedState = url.searchParams.get('state');
|
|
52
55
|
|
|
53
56
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
54
57
|
|
|
@@ -59,13 +62,20 @@ async function loginWithBrowser() {
|
|
|
59
62
|
return;
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
if (returnedState !== nonce) {
|
|
66
|
+
res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Security Error</h2><p>State mismatch — this request may not have originated from your CLI. Please try again.</p></body></html>');
|
|
67
|
+
server.close();
|
|
68
|
+
reject(new CLIError('State mismatch on callback — possible CSRF. Please retry `securenow login`.'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
if (token) {
|
|
63
73
|
pendingToken = token;
|
|
64
74
|
const payload = decodeJwtPayload(token);
|
|
65
75
|
const email = payload?.email || 'unknown account';
|
|
66
76
|
const safeEmail = email.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
67
77
|
const port = server.address().port;
|
|
68
|
-
const switchUrl = `${appUrl}/cli/auth?callback=http://
|
|
78
|
+
const switchUrl = `${appUrl}/cli/auth?callback=http://127.0.0.1:${port}/callback&state=${encodeURIComponent(nonce)}&force_login=1`;
|
|
69
79
|
|
|
70
80
|
res.end([
|
|
71
81
|
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>SecureNow CLI Login</title></head>',
|
|
@@ -91,7 +101,7 @@ async function loginWithBrowser() {
|
|
|
91
101
|
'<script>',
|
|
92
102
|
'document.getElementById("confirm-btn").addEventListener("click", function(){',
|
|
93
103
|
' this.disabled=true;this.textContent="Connecting\u2026";this.style.background="#86efac";this.style.cursor="default";',
|
|
94
|
-
` fetch("/confirm").then(function(){`,
|
|
104
|
+
` fetch("/confirm?nonce=${encodeURIComponent(nonce)}").then(function(){`,
|
|
95
105
|
' document.getElementById("confirm-btn").style.display="none";',
|
|
96
106
|
' document.getElementById("done-msg").style.display="block";',
|
|
97
107
|
' });',
|
|
@@ -109,6 +119,11 @@ async function loginWithBrowser() {
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
if (url.pathname === '/confirm' && pendingToken) {
|
|
122
|
+
if (url.searchParams.get('nonce') !== nonce) {
|
|
123
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
124
|
+
res.end('{"error":"invalid nonce"}');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
112
127
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
113
128
|
res.end('{"ok":true}');
|
|
114
129
|
const token = pendingToken;
|
|
@@ -124,7 +139,7 @@ async function loginWithBrowser() {
|
|
|
124
139
|
|
|
125
140
|
server.listen(0, '127.0.0.1', () => {
|
|
126
141
|
const port = server.address().port;
|
|
127
|
-
const authUrl = `${appUrl}/cli/auth?callback=http://
|
|
142
|
+
const authUrl = `${appUrl}/cli/auth?callback=http://127.0.0.1:${port}/callback&state=${encodeURIComponent(nonce)}`;
|
|
128
143
|
|
|
129
144
|
console.log('');
|
|
130
145
|
ui.info('Opening browser for authentication...');
|
package/cli.js
CHANGED
package/docs/FIREWALL-GUIDE.md
CHANGED
|
@@ -345,6 +345,21 @@ The firewall uses the same trusted-proxy-aware IP resolution as SecureNow tracin
|
|
|
345
345
|
- Store it in environment variables or `.env` — never commit it to source control
|
|
346
346
|
- The key is hashed (SHA-256) on the server — SecureNow never stores the plaintext
|
|
347
347
|
|
|
348
|
+
### Sync Architecture
|
|
349
|
+
|
|
350
|
+
The SDK uses a unified sync endpoint (`/firewall/sync`) that combines version checking and data fetching into a single request:
|
|
351
|
+
|
|
352
|
+
1. **One request per poll** — The SDK sends its current blocklist and allowlist versions. The API responds with version info and only includes full IP lists for lists that have changed.
|
|
353
|
+
2. **HTTP Keep-Alive** — TCP connections are reused across polls. TLS handshake happens once; subsequent requests reuse the socket (~5-10ms vs ~100-200ms per request).
|
|
354
|
+
3. **Automatic fallback** — If the unified endpoint is not available (older API), the SDK falls back to legacy separate endpoints.
|
|
355
|
+
|
|
356
|
+
### Circuit Breaker & Back-Pressure
|
|
357
|
+
|
|
358
|
+
- **Circuit breaker** — After 5 consecutive errors, the SDK pauses all polling for 2 minutes. After cooldown, a single probe is sent. If it succeeds, normal polling resumes.
|
|
359
|
+
- **In-flight guard** — Only one poll can be in-flight at a time. If the API is slow, the next scheduled poll is skipped.
|
|
360
|
+
- **429 Retry-After** — When the API returns HTTP 429, all polling pauses for the `Retry-After` duration.
|
|
361
|
+
- **Exponential backoff** — Poll interval doubles on each consecutive error (10s → 20s → 40s → 80s, capped at 120s).
|
|
362
|
+
|
|
348
363
|
### Cleanup on Shutdown
|
|
349
364
|
|
|
350
365
|
All layers clean up on process exit (SIGINT/SIGTERM):
|
package/firewall.js
CHANGED
|
@@ -8,7 +8,7 @@ const { resolveClientIp } = require('./resolve-ip');
|
|
|
8
8
|
let _options = null;
|
|
9
9
|
let _matcher = null;
|
|
10
10
|
let _syncTimer = null;
|
|
11
|
-
let
|
|
11
|
+
let _pollTimer = null;
|
|
12
12
|
let _lastModified = null;
|
|
13
13
|
let _lastVersion = null;
|
|
14
14
|
let _lastSyncEtag = null;
|
|
@@ -25,85 +25,89 @@ let _allowlistRawIps = [];
|
|
|
25
25
|
let _lastAllowlistModified = null;
|
|
26
26
|
let _lastAllowlistVersion = null;
|
|
27
27
|
let _lastAllowlistSyncEtag = null;
|
|
28
|
-
let _allowlistVersionTimer = null;
|
|
29
28
|
|
|
30
|
-
//
|
|
29
|
+
// Circuit breaker
|
|
30
|
+
const CIRCUIT_OPEN_THRESHOLD = 5;
|
|
31
|
+
const CIRCUIT_OPEN_COOLDOWN_MS = 120_000;
|
|
32
|
+
let _circuitState = 'closed';
|
|
33
|
+
let _circuitOpenedAt = 0;
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
// In-flight guard and 429 back-off
|
|
36
|
+
let _pollInflight = false;
|
|
37
|
+
let _retryAfterUntil = 0;
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const parsed = new URL(url);
|
|
39
|
+
// Keep-alive agents — reuse TCP connections across polls (TLS handshake once)
|
|
40
|
+
const _httpAgent = new http.Agent({ keepAlive: true, maxSockets: 2, keepAliveMsecs: 30_000 });
|
|
41
|
+
const _httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 2, keepAliveMsecs: 30_000 });
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
44
|
-
path: parsed.pathname + parsed.search,
|
|
45
|
-
method: 'GET',
|
|
46
|
-
headers: {
|
|
47
|
-
'Authorization': `Bearer ${_options.apiKey}`,
|
|
48
|
-
'User-Agent': 'securenow-firewall-sdk',
|
|
49
|
-
},
|
|
50
|
-
timeout: 10000,
|
|
51
|
-
};
|
|
43
|
+
// Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
|
|
44
|
+
let _useUnifiedSync = true;
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
reqOptions.headers['If-None-Match'] = _lastSyncEtag;
|
|
55
|
-
} else if (_lastModified) {
|
|
56
|
-
reqOptions.headers['If-Modified-Since'] = _lastModified;
|
|
57
|
-
}
|
|
46
|
+
// ────── Circuit Breaker ──────
|
|
58
47
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
function maybeOpenCircuit() {
|
|
49
|
+
if (_circuitState === 'open') return;
|
|
50
|
+
if (_consecutiveErrors >= CIRCUIT_OPEN_THRESHOLD) {
|
|
51
|
+
_circuitState = 'open';
|
|
52
|
+
_circuitOpenedAt = Date.now();
|
|
53
|
+
if (_options && _options.log) {
|
|
54
|
+
console.warn('[securenow] Firewall: circuit breaker OPEN — pausing polling for %ds', CIRCUIT_OPEN_COOLDOWN_MS / 1000);
|
|
63
55
|
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const body = JSON.parse(data);
|
|
74
|
-
const ips = body.ips || [];
|
|
75
|
-
_rawIps = ips;
|
|
76
|
-
_matcher = createMatcher(ips);
|
|
77
|
-
_lastModified = res.headers['last-modified'] || null;
|
|
78
|
-
if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
|
|
79
|
-
_stats.syncs++;
|
|
80
|
-
notifyLayers(ips);
|
|
81
|
-
callback(null, true, _matcher.stats());
|
|
82
|
-
} catch (e) {
|
|
83
|
-
callback(new Error(`Failed to parse blocklist response: ${e.message}`));
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
});
|
|
59
|
+
function resetCircuit() {
|
|
60
|
+
if (_circuitState !== 'closed') {
|
|
61
|
+
_circuitState = 'closed';
|
|
62
|
+
if (_options && _options.log) console.log('[securenow] Firewall: circuit breaker CLOSED — API healthy');
|
|
63
|
+
}
|
|
64
|
+
_consecutiveErrors = 0;
|
|
65
|
+
}
|
|
87
66
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
67
|
+
function shouldSkipRequest() {
|
|
68
|
+
if (Date.now() < _retryAfterUntil) return true;
|
|
69
|
+
if (_circuitState === 'closed') return false;
|
|
70
|
+
if (_circuitState === 'open') {
|
|
71
|
+
if (Date.now() - _circuitOpenedAt >= CIRCUIT_OPEN_COOLDOWN_MS) {
|
|
72
|
+
_circuitState = 'half-open';
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false; // half-open: allow one probe
|
|
91
78
|
}
|
|
92
79
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
80
|
+
function handleRetryAfter(res) {
|
|
81
|
+
const ra = res.headers['retry-after'];
|
|
82
|
+
if (!ra) return;
|
|
83
|
+
const secs = parseInt(ra, 10);
|
|
84
|
+
if (secs > 0 && secs <= 300) {
|
|
85
|
+
_retryAfterUntil = Date.now() + secs * 1000;
|
|
86
|
+
if (_options && _options.log) {
|
|
87
|
+
console.warn('[securenow] Firewall: API returned 429, backing off for %ds', secs);
|
|
88
|
+
}
|
|
96
89
|
}
|
|
97
90
|
}
|
|
98
91
|
|
|
99
|
-
// ──────
|
|
92
|
+
// ────── HTTP helpers ──────
|
|
100
93
|
|
|
101
|
-
function
|
|
102
|
-
|
|
94
|
+
function buildUrl(apiUrl, path) {
|
|
95
|
+
return apiUrl.replace(/\/+$/, '') + '/api/v1' + path;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function jitter(baseMs) {
|
|
99
|
+
return baseMs * (0.8 + Math.random() * 0.4);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function agentFor(url) {
|
|
103
|
+
return url.startsWith('https') ? _httpsAgent : _httpAgent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function httpGet(url, extraHeaders, timeout, callback) {
|
|
103
107
|
const mod = url.startsWith('https') ? https : http;
|
|
104
108
|
const parsed = new URL(url);
|
|
105
109
|
|
|
106
|
-
const
|
|
110
|
+
const req = mod.request({
|
|
107
111
|
hostname: parsed.hostname,
|
|
108
112
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
109
113
|
path: parsed.pathname + parsed.search,
|
|
@@ -111,211 +115,315 @@ function syncAllowlist(callback) {
|
|
|
111
115
|
headers: {
|
|
112
116
|
'Authorization': `Bearer ${_options.apiKey}`,
|
|
113
117
|
'User-Agent': 'securenow-firewall-sdk',
|
|
118
|
+
...extraHeaders,
|
|
114
119
|
},
|
|
115
|
-
timeout
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (_lastAllowlistSyncEtag) {
|
|
119
|
-
reqOptions.headers['If-None-Match'] = _lastAllowlistSyncEtag;
|
|
120
|
-
} else if (_lastAllowlistModified) {
|
|
121
|
-
reqOptions.headers['If-Modified-Since'] = _lastAllowlistModified;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const req = mod.request(reqOptions, (res) => {
|
|
125
|
-
if (res.statusCode === 304) {
|
|
126
|
-
callback(null, false);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
120
|
+
timeout,
|
|
121
|
+
agent: agentFor(url),
|
|
122
|
+
}, (res) => {
|
|
130
123
|
let data = '';
|
|
131
124
|
res.on('data', (chunk) => { data += chunk; });
|
|
132
|
-
res.on('end', () => {
|
|
133
|
-
if (res.statusCode !== 200) {
|
|
134
|
-
callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const body = JSON.parse(data);
|
|
139
|
-
const ips = body.ips || [];
|
|
140
|
-
_allowlistRawIps = ips;
|
|
141
|
-
_allowlistMatcher = createMatcher(ips);
|
|
142
|
-
_lastAllowlistModified = res.headers['last-modified'] || null;
|
|
143
|
-
if (res.headers['etag']) _lastAllowlistSyncEtag = res.headers['etag'];
|
|
144
|
-
callback(null, true, _allowlistMatcher.stats());
|
|
145
|
-
} catch (e) {
|
|
146
|
-
callback(new Error(`Failed to parse allowlist response: ${e.message}`));
|
|
147
|
-
}
|
|
148
|
-
});
|
|
125
|
+
res.on('end', () => { callback(null, res, data); });
|
|
149
126
|
});
|
|
150
127
|
|
|
151
128
|
req.on('error', (err) => callback(err));
|
|
152
|
-
req.on('timeout', () => { req.destroy(); callback(new Error('
|
|
129
|
+
req.on('timeout', () => { req.destroy(); callback(new Error('Request timed out')); });
|
|
153
130
|
req.end();
|
|
154
131
|
}
|
|
155
132
|
|
|
156
|
-
|
|
157
|
-
const url = buildUrl(_options.apiUrl, '/firewall/allowlist/version');
|
|
158
|
-
const mod = url.startsWith('https') ? https : http;
|
|
159
|
-
const parsed = new URL(url);
|
|
133
|
+
// ────── Unified Sync (v2 — single request for everything) ──────
|
|
160
134
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (_lastAllowlistVersion) headers['
|
|
135
|
+
function doUnifiedSync(callback) {
|
|
136
|
+
const url = buildUrl(_options.apiUrl, '/firewall/sync');
|
|
137
|
+
const headers = {};
|
|
138
|
+
if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
|
|
139
|
+
if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
|
|
140
|
+
|
|
141
|
+
httpGet(url, headers, 8000, (err, res, data) => {
|
|
142
|
+
if (err) return callback(err);
|
|
143
|
+
|
|
144
|
+
_stats.versionChecks++;
|
|
166
145
|
|
|
167
|
-
const req = mod.request({
|
|
168
|
-
hostname: parsed.hostname,
|
|
169
|
-
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
170
|
-
path: parsed.pathname + parsed.search,
|
|
171
|
-
method: 'GET',
|
|
172
|
-
headers,
|
|
173
|
-
timeout: 5000,
|
|
174
|
-
}, (res) => {
|
|
175
146
|
if (res.statusCode === 304) {
|
|
176
|
-
|
|
177
|
-
callback(null, false);
|
|
178
|
-
return;
|
|
147
|
+
return callback(null, { blChanged: false, alChanged: false });
|
|
179
148
|
}
|
|
180
149
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
150
|
+
if (res.statusCode === 404) {
|
|
151
|
+
_useUnifiedSync = false;
|
|
152
|
+
if (_options.log) console.log('[securenow] Firewall: /sync not available, using legacy endpoints');
|
|
153
|
+
return callback(null, { blChanged: false, alChanged: false, useLegacy: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (res.statusCode === 429) { handleRetryAfter(res); }
|
|
157
|
+
|
|
158
|
+
if (res.statusCode !== 200) {
|
|
159
|
+
return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const body = JSON.parse(data);
|
|
164
|
+
|
|
165
|
+
let blChanged = false;
|
|
166
|
+
let alChanged = false;
|
|
167
|
+
|
|
168
|
+
// Update blocklist version + data
|
|
169
|
+
if (body.blocklist) {
|
|
170
|
+
const newVer = body.blocklist.version;
|
|
171
|
+
if (newVer !== _lastVersion) {
|
|
172
|
+
_lastVersion = newVer;
|
|
173
|
+
blChanged = true;
|
|
174
|
+
}
|
|
187
175
|
}
|
|
188
|
-
try {
|
|
189
|
-
const body = JSON.parse(data);
|
|
190
|
-
const version = body.version || null;
|
|
191
|
-
const changed = version !== _lastAllowlistVersion;
|
|
192
|
-
if (changed) _lastAllowlistVersion = version;
|
|
193
|
-
callback(null, changed);
|
|
194
|
-
} catch (_e) { callback(null, false); }
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
176
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
177
|
+
if (body.blocklistIps) {
|
|
178
|
+
_rawIps = body.blocklistIps;
|
|
179
|
+
_matcher = createMatcher(body.blocklistIps);
|
|
180
|
+
_stats.syncs++;
|
|
181
|
+
notifyLayers(body.blocklistIps);
|
|
182
|
+
blChanged = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Update allowlist version + data
|
|
186
|
+
if (body.allowlist) {
|
|
187
|
+
const newVer = body.allowlist.version;
|
|
188
|
+
if (newVer !== _lastAllowlistVersion) {
|
|
189
|
+
_lastAllowlistVersion = newVer;
|
|
190
|
+
alChanged = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (body.allowlistIps) {
|
|
195
|
+
_allowlistRawIps = body.allowlistIps;
|
|
196
|
+
_allowlistMatcher = createMatcher(body.allowlistIps);
|
|
197
|
+
alChanged = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
callback(null, { blChanged, alChanged });
|
|
201
|
+
} catch (e) {
|
|
202
|
+
callback(new Error(`Failed to parse sync response: ${e.message}`));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
201
205
|
}
|
|
202
206
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
// ────── Legacy Sync (v1 — separate endpoints, kept for backward compat) ──────
|
|
208
|
+
|
|
209
|
+
function legacyBlocklistSync(callback) {
|
|
210
|
+
const url = buildUrl(_options.apiUrl, '/firewall/blocklist');
|
|
211
|
+
const headers = {};
|
|
212
|
+
if (_lastSyncEtag) headers['If-None-Match'] = _lastSyncEtag;
|
|
213
|
+
else if (_lastModified) headers['If-Modified-Since'] = _lastModified;
|
|
214
|
+
|
|
215
|
+
httpGet(url, headers, 10000, (err, res, data) => {
|
|
216
|
+
if (err) return callback(err);
|
|
217
|
+
if (res.statusCode === 304) return callback(null, false);
|
|
218
|
+
if (res.statusCode === 429) { handleRetryAfter(res); }
|
|
219
|
+
if (res.statusCode !== 200) return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const body = JSON.parse(data);
|
|
223
|
+
const ips = body.ips || [];
|
|
224
|
+
_rawIps = ips;
|
|
225
|
+
_matcher = createMatcher(ips);
|
|
226
|
+
_lastModified = res.headers['last-modified'] || null;
|
|
227
|
+
if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
|
|
228
|
+
_stats.syncs++;
|
|
229
|
+
notifyLayers(ips);
|
|
230
|
+
callback(null, true, _matcher.stats());
|
|
231
|
+
} catch (e) {
|
|
232
|
+
callback(new Error(`Failed to parse blocklist: ${e.message}`));
|
|
209
233
|
}
|
|
210
234
|
});
|
|
211
235
|
}
|
|
212
236
|
|
|
213
|
-
function
|
|
214
|
-
const
|
|
215
|
-
const
|
|
237
|
+
function legacyAllowlistSync(callback) {
|
|
238
|
+
const url = buildUrl(_options.apiUrl, '/firewall/allowlist');
|
|
239
|
+
const headers = {};
|
|
240
|
+
if (_lastAllowlistSyncEtag) headers['If-None-Match'] = _lastAllowlistSyncEtag;
|
|
241
|
+
else if (_lastAllowlistModified) headers['If-Modified-Since'] = _lastAllowlistModified;
|
|
216
242
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
243
|
+
httpGet(url, headers, 10000, (err, res, data) => {
|
|
244
|
+
if (err) return callback(err);
|
|
245
|
+
if (res.statusCode === 304) return callback(null, false);
|
|
246
|
+
if (res.statusCode === 429) { handleRetryAfter(res); }
|
|
247
|
+
if (res.statusCode !== 200) return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const body = JSON.parse(data);
|
|
251
|
+
const ips = body.ips || [];
|
|
252
|
+
_allowlistRawIps = ips;
|
|
253
|
+
_allowlistMatcher = createMatcher(ips);
|
|
254
|
+
_lastAllowlistModified = res.headers['last-modified'] || null;
|
|
255
|
+
if (res.headers['etag']) _lastAllowlistSyncEtag = res.headers['etag'];
|
|
256
|
+
callback(null, true, _allowlistMatcher.stats());
|
|
257
|
+
} catch (e) {
|
|
258
|
+
callback(new Error(`Failed to parse allowlist: ${e.message}`));
|
|
259
|
+
}
|
|
260
|
+
});
|
|
227
261
|
}
|
|
228
262
|
|
|
229
|
-
function
|
|
263
|
+
function legacyVersionCheck(callback) {
|
|
230
264
|
const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
|
|
231
|
-
const
|
|
232
|
-
const parsed = new URL(url);
|
|
233
|
-
|
|
234
|
-
const headers = {
|
|
235
|
-
'Authorization': `Bearer ${_options.apiKey}`,
|
|
236
|
-
'User-Agent': 'securenow-firewall-sdk',
|
|
237
|
-
};
|
|
265
|
+
const headers = {};
|
|
238
266
|
if (_lastVersion) headers['If-None-Match'] = _lastVersion;
|
|
239
267
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
243
|
-
path: parsed.pathname + parsed.search,
|
|
244
|
-
method: 'GET',
|
|
245
|
-
headers,
|
|
246
|
-
timeout: 5000,
|
|
247
|
-
}, (res) => {
|
|
268
|
+
httpGet(url, headers, 5000, (err, res, data) => {
|
|
269
|
+
if (err) return callback(err);
|
|
248
270
|
_stats.versionChecks++;
|
|
271
|
+
if (res.statusCode === 304) return callback(null, false, false);
|
|
272
|
+
if (res.statusCode === 429) { handleRetryAfter(res); }
|
|
273
|
+
if (res.statusCode !== 200) return callback(new Error(`API ${res.statusCode}`));
|
|
249
274
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
275
|
+
try {
|
|
276
|
+
const body = JSON.parse(data);
|
|
277
|
+
const version = body.version || null;
|
|
278
|
+
const changed = version !== _lastVersion;
|
|
279
|
+
if (changed) _lastVersion = version;
|
|
280
|
+
callback(null, changed, false);
|
|
281
|
+
} catch (_e) { callback(null, false, false); }
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function legacyAllowlistVersionCheck(callback) {
|
|
286
|
+
const url = buildUrl(_options.apiUrl, '/firewall/allowlist/version');
|
|
287
|
+
const headers = {};
|
|
288
|
+
if (_lastAllowlistVersion) headers['If-None-Match'] = _lastAllowlistVersion;
|
|
289
|
+
|
|
290
|
+
httpGet(url, headers, 5000, (err, res, data) => {
|
|
291
|
+
if (err) return callback(err);
|
|
292
|
+
if (res.statusCode === 304) return callback(null, false);
|
|
293
|
+
if (res.statusCode === 429) { handleRetryAfter(res); }
|
|
294
|
+
if (res.statusCode !== 200) return callback(new Error(`API ${res.statusCode}`));
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const body = JSON.parse(data);
|
|
298
|
+
const version = body.version || null;
|
|
299
|
+
const changed = version !== _lastAllowlistVersion;
|
|
300
|
+
if (changed) _lastAllowlistVersion = version;
|
|
301
|
+
callback(null, changed);
|
|
302
|
+
} catch (_e) { callback(null, false); }
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function doLegacyPoll(callback) {
|
|
307
|
+
let pending = 2;
|
|
308
|
+
let blChanged = false;
|
|
309
|
+
let alChanged = false;
|
|
310
|
+
let firstError = null;
|
|
311
|
+
|
|
312
|
+
function checkDone() {
|
|
313
|
+
if (--pending > 0) return;
|
|
314
|
+
if (firstError) return callback(firstError);
|
|
315
|
+
|
|
316
|
+
let syncsPending = 0;
|
|
317
|
+
if (blChanged) syncsPending++;
|
|
318
|
+
if (alChanged) syncsPending++;
|
|
319
|
+
if (syncsPending === 0) return callback(null, { blChanged: false, alChanged: false });
|
|
320
|
+
|
|
321
|
+
function syncDone() {
|
|
322
|
+
if (--syncsPending > 0) return;
|
|
323
|
+
callback(null, { blChanged, alChanged });
|
|
255
324
|
}
|
|
256
325
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
326
|
+
if (blChanged) {
|
|
327
|
+
legacyBlocklistSync((err, changed, stats) => {
|
|
328
|
+
if (err && _options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err.message);
|
|
329
|
+
else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
|
|
330
|
+
syncDone();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (alChanged) {
|
|
334
|
+
legacyAllowlistSync((err, changed, stats) => {
|
|
335
|
+
if (err && _options.log) console.warn('[securenow] Firewall: allowlist sync failed:', err.message);
|
|
336
|
+
else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
|
|
337
|
+
syncDone();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
legacyVersionCheck((err, changed) => {
|
|
343
|
+
if (err) firstError = firstError || err;
|
|
344
|
+
blChanged = changed;
|
|
345
|
+
checkDone();
|
|
275
346
|
});
|
|
276
347
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
348
|
+
legacyAllowlistVersionCheck((err, changed) => {
|
|
349
|
+
if (err) firstError = firstError || err;
|
|
350
|
+
alChanged = changed;
|
|
351
|
+
checkDone();
|
|
352
|
+
});
|
|
280
353
|
}
|
|
281
354
|
|
|
282
|
-
|
|
283
|
-
|
|
355
|
+
// ────── Unified poll loop ──────
|
|
356
|
+
|
|
357
|
+
function notifyLayers(ips) {
|
|
358
|
+
for (const layer of _layers) {
|
|
359
|
+
try { if (typeof layer.sync === 'function') layer.sync(ips); } catch (_) {}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function pollOnce(callback) {
|
|
364
|
+
if (_pollInflight || shouldSkipRequest()) return callback(null);
|
|
365
|
+
_pollInflight = true;
|
|
366
|
+
|
|
367
|
+
const done = (err, result) => {
|
|
368
|
+
_pollInflight = false;
|
|
284
369
|
if (err) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
370
|
+
_consecutiveErrors++;
|
|
371
|
+
_stats.errors++;
|
|
372
|
+
maybeOpenCircuit();
|
|
373
|
+
if (_options.log) console.warn('[securenow] Firewall: poll failed:', err.message);
|
|
374
|
+
return callback(err);
|
|
288
375
|
}
|
|
289
|
-
|
|
290
|
-
|
|
376
|
+
_consecutiveErrors = 0;
|
|
377
|
+
resetCircuit();
|
|
378
|
+
if (result) {
|
|
379
|
+
if (result.blChanged && _options.log && _matcher) {
|
|
380
|
+
const s = _matcher.stats();
|
|
381
|
+
console.log('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
382
|
+
}
|
|
383
|
+
if (result.alChanged && _options.log && _allowlistMatcher) {
|
|
384
|
+
const s = _allowlistMatcher.stats();
|
|
385
|
+
console.log('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
callback(null);
|
|
389
|
+
};
|
|
291
390
|
|
|
292
|
-
|
|
293
|
-
|
|
391
|
+
if (_useUnifiedSync) {
|
|
392
|
+
doUnifiedSync((err, result) => {
|
|
393
|
+
if (err) return done(err);
|
|
394
|
+
if (result && result.useLegacy) {
|
|
395
|
+
doLegacyPoll(done);
|
|
396
|
+
} else {
|
|
397
|
+
done(null, result);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
} else {
|
|
401
|
+
doLegacyPoll(done);
|
|
402
|
+
}
|
|
294
403
|
}
|
|
295
404
|
|
|
296
|
-
function
|
|
405
|
+
function scheduleNextPoll() {
|
|
297
406
|
const baseMs = (_options.versionCheckInterval || 10) * 1000;
|
|
298
407
|
const backoffMs = Math.min(baseMs * Math.pow(2, _consecutiveErrors), 120_000);
|
|
299
408
|
const delayMs = jitter(backoffMs);
|
|
300
409
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (changed) {
|
|
304
|
-
if (_options.log) console.log('[securenow] Firewall: blocklist version changed, syncing…');
|
|
305
|
-
doFullSync();
|
|
306
|
-
}
|
|
307
|
-
scheduleNextVersionCheck();
|
|
308
|
-
});
|
|
410
|
+
_pollTimer = setTimeout(() => {
|
|
411
|
+
pollOnce(() => { scheduleNextPoll(); });
|
|
309
412
|
}, delayMs);
|
|
310
|
-
if (
|
|
413
|
+
if (_pollTimer.unref) _pollTimer.unref();
|
|
311
414
|
}
|
|
312
415
|
|
|
313
416
|
function startSyncLoop() {
|
|
314
417
|
const fullSyncIntervalMs = (_options.syncInterval || 300) * 1000;
|
|
315
|
-
const
|
|
418
|
+
const BASE_RETRY = 5000;
|
|
419
|
+
const MAX_RETRY = 120_000;
|
|
420
|
+
let _initAttempt = 0;
|
|
316
421
|
|
|
317
422
|
function initialSync() {
|
|
318
|
-
|
|
423
|
+
// Use unified endpoint for initial sync too
|
|
424
|
+
const syncFn = _useUnifiedSync ? doUnifiedSync : (cb) => doLegacyPoll(cb);
|
|
425
|
+
|
|
426
|
+
syncFn((err, result) => {
|
|
319
427
|
if (err) {
|
|
320
428
|
const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
|
|
321
429
|
if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
|
|
@@ -327,42 +435,58 @@ function startSyncLoop() {
|
|
|
327
435
|
if (retryTimer.unref) retryTimer.unref();
|
|
328
436
|
return;
|
|
329
437
|
}
|
|
438
|
+
if (result && result.useLegacy) {
|
|
439
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
440
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
330
443
|
if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
|
|
331
444
|
if (_options.failMode === 'closed') {
|
|
332
445
|
_matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
|
|
333
446
|
}
|
|
334
|
-
|
|
447
|
+
_initAttempt++;
|
|
448
|
+
const delay = Math.min(BASE_RETRY * Math.pow(2, _initAttempt), MAX_RETRY);
|
|
449
|
+
const retryTimer = setTimeout(initialSync, jitter(delay));
|
|
335
450
|
if (retryTimer.unref) retryTimer.unref();
|
|
336
451
|
return;
|
|
337
452
|
}
|
|
338
|
-
if (changed && stats) {
|
|
339
|
-
if (_options.log) console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
|
|
340
|
-
}
|
|
341
|
-
_initialized = true;
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
453
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (err) {
|
|
348
|
-
if (_options.log) console.warn('[securenow] Firewall: initial allowlist sync failed:', err.message);
|
|
349
|
-
const retryTimer = setTimeout(initialAllowlistSync, RETRY_DELAY);
|
|
454
|
+
if (result && result.useLegacy) {
|
|
455
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
350
456
|
if (retryTimer.unref) retryTimer.unref();
|
|
351
457
|
return;
|
|
352
458
|
}
|
|
353
|
-
|
|
354
|
-
|
|
459
|
+
|
|
460
|
+
_initialized = true;
|
|
461
|
+
if (_options.log && _matcher) {
|
|
462
|
+
const s = _matcher.stats();
|
|
463
|
+
console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
464
|
+
}
|
|
465
|
+
if (_options.log && _allowlistMatcher) {
|
|
466
|
+
const s = _allowlistMatcher.stats();
|
|
467
|
+
if (s.total > 0) console.log('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
355
468
|
}
|
|
356
469
|
});
|
|
357
470
|
}
|
|
358
471
|
|
|
359
472
|
initialSync();
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
473
|
+
scheduleNextPoll();
|
|
474
|
+
|
|
475
|
+
// Safety-net full sync timer (less frequent, uses same path)
|
|
476
|
+
_syncTimer = setInterval(() => {
|
|
477
|
+
if (shouldSkipRequest()) return;
|
|
478
|
+
// Force a full re-fetch by clearing versions so unified endpoint returns full data
|
|
479
|
+
const savedBlVer = _lastVersion;
|
|
480
|
+
const savedAlVer = _lastAllowlistVersion;
|
|
481
|
+
_lastVersion = null;
|
|
482
|
+
_lastAllowlistVersion = null;
|
|
483
|
+
pollOnce((err) => {
|
|
484
|
+
if (err) {
|
|
485
|
+
_lastVersion = savedBlVer;
|
|
486
|
+
_lastAllowlistVersion = savedAlVer;
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}, fullSyncIntervalMs);
|
|
366
490
|
if (_syncTimer.unref) _syncTimer.unref();
|
|
367
491
|
}
|
|
368
492
|
|
|
@@ -445,7 +569,6 @@ function firewallRequestHandler(req, res) {
|
|
|
445
569
|
sendBlockResponse(req, res, ip);
|
|
446
570
|
return true;
|
|
447
571
|
}
|
|
448
|
-
// IP is on the allowlist — skip blocklist check, allow through
|
|
449
572
|
return false;
|
|
450
573
|
}
|
|
451
574
|
|
|
@@ -479,7 +602,6 @@ function patchHttpLayer() {
|
|
|
479
602
|
if (_httpPatched) return;
|
|
480
603
|
_httpPatched = true;
|
|
481
604
|
|
|
482
|
-
// Patch Server.prototype.emit to intercept requests on already-created servers
|
|
483
605
|
patchEmitLayer();
|
|
484
606
|
|
|
485
607
|
http.createServer = function(...args) {
|
|
@@ -506,11 +628,9 @@ function init(options) {
|
|
|
506
628
|
|
|
507
629
|
if (_options.log) console.log('[securenow] Firewall: ENABLED');
|
|
508
630
|
|
|
509
|
-
// Layer 1: HTTP (always on)
|
|
510
631
|
patchHttpLayer();
|
|
511
632
|
if (_options.log) console.log('[securenow] Firewall: Layer 1 (HTTP 403) active');
|
|
512
633
|
|
|
513
|
-
// Layer 2: TCP
|
|
514
634
|
if (_options.tcp) {
|
|
515
635
|
try {
|
|
516
636
|
const tcpLayer = require('./firewall-tcp');
|
|
@@ -524,7 +644,6 @@ function init(options) {
|
|
|
524
644
|
if (_options.log) console.log('[securenow] Firewall: Layer 2 (TCP drop) disabled (set SECURENOW_FIREWALL_TCP=1)');
|
|
525
645
|
}
|
|
526
646
|
|
|
527
|
-
// Layer 3: iptables
|
|
528
647
|
if (_options.iptables) {
|
|
529
648
|
try {
|
|
530
649
|
const iptablesLayer = require('./firewall-iptables');
|
|
@@ -538,7 +657,6 @@ function init(options) {
|
|
|
538
657
|
if (_options.log) console.log('[securenow] Firewall: Layer 3 (iptables) disabled (set SECURENOW_FIREWALL_IPTABLES=1)');
|
|
539
658
|
}
|
|
540
659
|
|
|
541
|
-
// Layer 4: Cloud WAF
|
|
542
660
|
if (_options.cloud) {
|
|
543
661
|
try {
|
|
544
662
|
const cloudLayer = require('./firewall-cloud');
|
|
@@ -556,10 +674,18 @@ function init(options) {
|
|
|
556
674
|
}
|
|
557
675
|
|
|
558
676
|
function shutdown() {
|
|
559
|
-
if (
|
|
560
|
-
if (_allowlistVersionTimer) { clearTimeout(_allowlistVersionTimer); _allowlistVersionTimer = null; }
|
|
677
|
+
if (_pollTimer) { clearTimeout(_pollTimer); _pollTimer = null; }
|
|
561
678
|
if (_syncTimer) { clearInterval(_syncTimer); _syncTimer = null; }
|
|
562
679
|
|
|
680
|
+
_circuitState = 'closed';
|
|
681
|
+
_circuitOpenedAt = 0;
|
|
682
|
+
_consecutiveErrors = 0;
|
|
683
|
+
_pollInflight = false;
|
|
684
|
+
_retryAfterUntil = 0;
|
|
685
|
+
|
|
686
|
+
_httpAgent.destroy();
|
|
687
|
+
_httpsAgent.destroy();
|
|
688
|
+
|
|
563
689
|
for (const layer of _layers) {
|
|
564
690
|
try { if (typeof layer.shutdown === 'function') layer.shutdown(); } catch (_) {}
|
|
565
691
|
}
|
|
@@ -582,6 +708,9 @@ function getStats() {
|
|
|
582
708
|
matcher: _matcher ? _matcher.stats() : null,
|
|
583
709
|
allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
|
|
584
710
|
initialized: _initialized,
|
|
711
|
+
circuitState: _circuitState,
|
|
712
|
+
consecutiveErrors: _consecutiveErrors,
|
|
713
|
+
unifiedSync: _useUnifiedSync,
|
|
585
714
|
};
|
|
586
715
|
}
|
|
587
716
|
|
package/package.json
CHANGED