securenow 6.0.2 → 6.1.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/CONSUMING-APPS-GUIDE.md +455 -0
- package/NPM_README.md +2029 -0
- package/README.md +297 -40
- package/SKILL-API.md +634 -0
- package/SKILL-CLI.md +454 -0
- package/cidr.js +83 -0
- package/cli/apps.js +585 -0
- package/cli/auth.js +280 -0
- package/cli/client.js +115 -0
- package/cli/config.js +173 -0
- package/cli/diagnostics.js +387 -0
- package/cli/firewall.js +100 -0
- package/cli/fp.js +638 -0
- package/cli/init.js +201 -0
- package/cli/monitor.js +440 -0
- package/cli/run.js +148 -0
- package/cli/security.js +980 -0
- package/cli/ui.js +386 -0
- package/cli/utils.js +127 -0
- package/cli.js +466 -455
- package/console-instrumentation.js +147 -136
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
- package/docs/API-KEYS-GUIDE.md +233 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP-SUMMARY.md +331 -0
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-FIX.md +261 -0
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +1 -35
- package/docs/COMPLETION-REPORT.md +408 -0
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +880 -652
- package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
- package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
- package/docs/FINAL-SOLUTION.md +335 -0
- package/docs/FIREWALL-GUIDE.md +426 -0
- package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
- package/docs/INDEX.md +22 -4
- package/docs/LOGGING-GUIDE.md +701 -708
- package/docs/LOGGING-QUICKSTART.md +234 -255
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
- package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
- package/docs/NEXTJS-GUIDE.md +14 -14
- package/docs/NEXTJS-QUICKSTART.md +1 -1
- package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
- package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
- package/docs/NUXT-GUIDE.md +166 -0
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +19 -10
- package/docs/SOLUTION-SUMMARY.md +312 -0
- package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
- package/examples/README.md +6 -6
- package/examples/instrumentation-with-auto-capture.ts +1 -1
- package/examples/nextjs-env-example.txt +2 -2
- package/examples/nextjs-instrumentation.js +1 -1
- package/examples/nextjs-instrumentation.ts +1 -1
- package/examples/nextjs-with-logging-example.md +6 -6
- package/examples/nextjs-with-options.ts +1 -1
- package/examples/test-nextjs-setup.js +1 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-only.js +38 -0
- package/firewall-tcp.js +74 -0
- package/firewall.js +720 -0
- package/free-trial-banner.js +174 -0
- package/nextjs-auto-capture.js +199 -207
- package/nextjs-middleware.js +186 -181
- package/nextjs-webpack-config.js +88 -53
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +639 -647
- package/nuxt-server-plugin.mjs +423 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +186 -164
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +39 -4
- package/resolve-ip.js +77 -0
- package/tracing.d.ts +2 -1
- package/tracing.js +295 -34
- package/web-vite.mjs +239 -156
- package/LICENSE +0 -15
package/firewall.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { createMatcher } = require('./cidr');
|
|
6
|
+
const { resolveClientIp } = require('./resolve-ip');
|
|
7
|
+
|
|
8
|
+
let _options = null;
|
|
9
|
+
let _matcher = null;
|
|
10
|
+
let _syncTimer = null;
|
|
11
|
+
let _pollTimer = null;
|
|
12
|
+
let _lastModified = null;
|
|
13
|
+
let _lastVersion = null;
|
|
14
|
+
let _lastSyncEtag = null;
|
|
15
|
+
let _initialized = false;
|
|
16
|
+
let _consecutiveErrors = 0;
|
|
17
|
+
let _layers = [];
|
|
18
|
+
let _rawIps = [];
|
|
19
|
+
let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
|
|
20
|
+
let _localhostFallbackTried = false;
|
|
21
|
+
|
|
22
|
+
// Allowlist state
|
|
23
|
+
let _allowlistMatcher = null;
|
|
24
|
+
let _allowlistRawIps = [];
|
|
25
|
+
let _lastAllowlistModified = null;
|
|
26
|
+
let _lastAllowlistVersion = null;
|
|
27
|
+
let _lastAllowlistSyncEtag = null;
|
|
28
|
+
|
|
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;
|
|
34
|
+
|
|
35
|
+
// In-flight guard and 429 back-off
|
|
36
|
+
let _pollInflight = false;
|
|
37
|
+
let _retryAfterUntil = 0;
|
|
38
|
+
|
|
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 });
|
|
42
|
+
|
|
43
|
+
// Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
|
|
44
|
+
let _useUnifiedSync = true;
|
|
45
|
+
|
|
46
|
+
// ────── Circuit Breaker ──────
|
|
47
|
+
|
|
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);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
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
|
+
}
|
|
66
|
+
|
|
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
|
|
78
|
+
}
|
|
79
|
+
|
|
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
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ────── HTTP helpers ──────
|
|
93
|
+
|
|
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) {
|
|
107
|
+
const mod = url.startsWith('https') ? https : http;
|
|
108
|
+
const parsed = new URL(url);
|
|
109
|
+
|
|
110
|
+
const req = mod.request({
|
|
111
|
+
hostname: parsed.hostname,
|
|
112
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
113
|
+
path: parsed.pathname + parsed.search,
|
|
114
|
+
method: 'GET',
|
|
115
|
+
headers: {
|
|
116
|
+
'Authorization': `Bearer ${_options.apiKey}`,
|
|
117
|
+
'User-Agent': 'securenow-firewall-sdk',
|
|
118
|
+
...extraHeaders,
|
|
119
|
+
},
|
|
120
|
+
timeout,
|
|
121
|
+
agent: agentFor(url),
|
|
122
|
+
}, (res) => {
|
|
123
|
+
let data = '';
|
|
124
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
125
|
+
res.on('end', () => { callback(null, res, data); });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
req.on('error', (err) => callback(err));
|
|
129
|
+
req.on('timeout', () => { req.destroy(); callback(new Error('Request timed out')); });
|
|
130
|
+
req.end();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ────── Unified Sync (v2 — single request for everything) ──────
|
|
134
|
+
|
|
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++;
|
|
145
|
+
|
|
146
|
+
if (res.statusCode === 304) {
|
|
147
|
+
return callback(null, { blChanged: false, alChanged: false });
|
|
148
|
+
}
|
|
149
|
+
|
|
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
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
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
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
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}`));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
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;
|
|
242
|
+
|
|
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
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function legacyVersionCheck(callback) {
|
|
264
|
+
const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
|
|
265
|
+
const headers = {};
|
|
266
|
+
if (_lastVersion) headers['If-None-Match'] = _lastVersion;
|
|
267
|
+
|
|
268
|
+
httpGet(url, headers, 5000, (err, res, data) => {
|
|
269
|
+
if (err) return callback(err);
|
|
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}`));
|
|
274
|
+
|
|
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 });
|
|
324
|
+
}
|
|
325
|
+
|
|
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();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
legacyAllowlistVersionCheck((err, changed) => {
|
|
349
|
+
if (err) firstError = firstError || err;
|
|
350
|
+
alChanged = changed;
|
|
351
|
+
checkDone();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
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;
|
|
369
|
+
if (err) {
|
|
370
|
+
_consecutiveErrors++;
|
|
371
|
+
_stats.errors++;
|
|
372
|
+
maybeOpenCircuit();
|
|
373
|
+
if (_options.log) console.warn('[securenow] Firewall: poll failed:', err.message);
|
|
374
|
+
return callback(err);
|
|
375
|
+
}
|
|
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
|
+
};
|
|
390
|
+
|
|
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
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function scheduleNextPoll() {
|
|
406
|
+
const baseMs = (_options.versionCheckInterval || 10) * 1000;
|
|
407
|
+
const backoffMs = Math.min(baseMs * Math.pow(2, _consecutiveErrors), 120_000);
|
|
408
|
+
const delayMs = jitter(backoffMs);
|
|
409
|
+
|
|
410
|
+
_pollTimer = setTimeout(() => {
|
|
411
|
+
pollOnce(() => { scheduleNextPoll(); });
|
|
412
|
+
}, delayMs);
|
|
413
|
+
if (_pollTimer.unref) _pollTimer.unref();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function startSyncLoop() {
|
|
417
|
+
const fullSyncIntervalMs = (_options.syncInterval || 300) * 1000;
|
|
418
|
+
const BASE_RETRY = 5000;
|
|
419
|
+
const MAX_RETRY = 120_000;
|
|
420
|
+
let _initAttempt = 0;
|
|
421
|
+
|
|
422
|
+
function initialSync() {
|
|
423
|
+
// Use unified endpoint for initial sync too
|
|
424
|
+
const syncFn = _useUnifiedSync ? doUnifiedSync : (cb) => doLegacyPoll(cb);
|
|
425
|
+
|
|
426
|
+
syncFn((err, result) => {
|
|
427
|
+
if (err) {
|
|
428
|
+
const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
|
|
429
|
+
if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
|
|
430
|
+
_localhostFallbackTried = true;
|
|
431
|
+
const origUrl = _options.apiUrl;
|
|
432
|
+
_options.apiUrl = 'http://localhost:4000';
|
|
433
|
+
if (_options.log) console.log('[securenow] Firewall: %s unreachable, trying http://localhost:4000', origUrl);
|
|
434
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
435
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (result && result.useLegacy) {
|
|
439
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
440
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
|
|
444
|
+
if (_options.failMode === 'closed') {
|
|
445
|
+
_matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
|
|
446
|
+
}
|
|
447
|
+
_initAttempt++;
|
|
448
|
+
const delay = Math.min(BASE_RETRY * Math.pow(2, _initAttempt), MAX_RETRY);
|
|
449
|
+
const retryTimer = setTimeout(initialSync, jitter(delay));
|
|
450
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (result && result.useLegacy) {
|
|
455
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
456
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
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);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
initialSync();
|
|
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);
|
|
490
|
+
if (_syncTimer.unref) _syncTimer.unref();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ────── Layer 1: HTTP Handler ──────
|
|
494
|
+
|
|
495
|
+
const _origHttpCreate = http.createServer;
|
|
496
|
+
const _origHttpsCreate = https.createServer;
|
|
497
|
+
let _httpPatched = false;
|
|
498
|
+
|
|
499
|
+
function blockedHtml(ip) {
|
|
500
|
+
const maskedIp = ip || 'unknown';
|
|
501
|
+
return `<!DOCTYPE html>
|
|
502
|
+
<html lang="en">
|
|
503
|
+
<head>
|
|
504
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
505
|
+
<title>Access Blocked — Security Alert</title>
|
|
506
|
+
<style>
|
|
507
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
508
|
+
body{min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a0a;color:#e5e5e5}
|
|
509
|
+
.wrap{text-align:center;max-width:540px;padding:2.5rem 2rem}
|
|
510
|
+
.icon{width:64px;height:64px;margin:0 auto 1.5rem;border-radius:50%;background:rgba(220,38,38,.12);display:flex;align-items:center;justify-content:center}
|
|
511
|
+
.icon svg{width:32px;height:32px;fill:none;stroke:#dc2626;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
512
|
+
.badge{display:inline-block;padding:.25rem .75rem;border-radius:999px;background:rgba(220,38,38,.15);color:#f87171;font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;margin-bottom:1.25rem}
|
|
513
|
+
h1{font-size:1.6rem;font-weight:700;margin-bottom:.6rem;color:#fff}
|
|
514
|
+
.sub{font-size:1rem;color:#f87171;font-weight:600;margin-bottom:1.25rem}
|
|
515
|
+
p{font-size:.9rem;line-height:1.7;color:#a1a1aa;margin-bottom:.5rem}
|
|
516
|
+
.ip-box{display:inline-block;margin:1rem 0;padding:.6rem 1.5rem;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(220,38,38,.25);font-family:"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;font-size:.95rem;color:#fca5a5;letter-spacing:.03em}
|
|
517
|
+
.divider{width:48px;height:2px;background:rgba(220,38,38,.3);margin:1.5rem auto}
|
|
518
|
+
.contact{font-size:.85rem;color:#71717a;line-height:1.7}
|
|
519
|
+
.contact a{color:#f87171;text-decoration:none;font-weight:500}
|
|
520
|
+
.contact a:hover{text-decoration:underline}
|
|
521
|
+
.footer{margin-top:2rem;font-size:.7rem;color:#3f3f46}
|
|
522
|
+
.powered{margin-top:1.5rem;font-size:.75rem;color:#52525b}.powered a{color:#a1a1aa;text-decoration:none;font-weight:600;transition:color .2s}.powered a:hover{color:#f87171;text-decoration:underline}
|
|
523
|
+
</style>
|
|
524
|
+
</head>
|
|
525
|
+
<body>
|
|
526
|
+
<div class="wrap">
|
|
527
|
+
<div class="icon"><svg viewBox="0 0 24 24"><path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg></div>
|
|
528
|
+
<span class="badge">Security Alert</span>
|
|
529
|
+
<h1>Access Blocked</h1>
|
|
530
|
+
<p class="sub">Malicious activity detected from your IP address</p>
|
|
531
|
+
<p>Your IP has been flagged and blocked due to suspicious or malicious behavior originating from your network. Our security team has been notified and is actively investigating.</p>
|
|
532
|
+
<div class="ip-box">${maskedIp}</div>
|
|
533
|
+
<div class="divider"></div>
|
|
534
|
+
<p class="contact">If you believe this is a mistake, please contact us at<br><a href="mailto:contact@securenow.ai?subject=Blocked%20IP%20Appeal%20-%20${encodeURIComponent(maskedIp)}&body=IP:%20${encodeURIComponent(maskedIp)}%0ATimestamp:%20${encodeURIComponent(new Date().toISOString())}%0A%0APlease%20describe%20why%20you%20believe%20this%20block%20is%20incorrect:">contact@securenow.ai</a><br>Include your IP address and the time of this incident.</p>
|
|
535
|
+
<p class="footer">Ref: ${maskedIp} — ${new Date().toISOString()} — HTTP 403</p>
|
|
536
|
+
<p class="powered">Protected by <a href="https://securenow.ai" rel="dofollow" target="_blank">SecureNow</a></p>
|
|
537
|
+
</div>
|
|
538
|
+
</body>
|
|
539
|
+
</html>`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function wrapListener(originalListener) {
|
|
543
|
+
return function firewallGuard(req, res) {
|
|
544
|
+
_stats.allowed++;
|
|
545
|
+
return originalListener.call(this, req, res);
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function sendBlockResponse(req, res, ip) {
|
|
550
|
+
const code = (_options && _options.statusCode) || 403;
|
|
551
|
+
const accept = req.headers['accept'] || '';
|
|
552
|
+
if (accept.includes('text/html')) {
|
|
553
|
+
res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
554
|
+
res.end(blockedHtml(ip));
|
|
555
|
+
} else {
|
|
556
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
557
|
+
res.end(JSON.stringify({ error: 'Forbidden', ip }));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function firewallRequestHandler(req, res) {
|
|
562
|
+
const ip = resolveClientIp(req);
|
|
563
|
+
|
|
564
|
+
// Allowlist check: if active, only listed IPs are allowed through
|
|
565
|
+
if (_allowlistMatcher && _allowlistMatcher.stats().total > 0) {
|
|
566
|
+
if (!_allowlistMatcher.isBlocked(ip)) {
|
|
567
|
+
_stats.blocked++;
|
|
568
|
+
if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP (not in allowlist)', ip);
|
|
569
|
+
sendBlockResponse(req, res, ip);
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Blocklist check
|
|
576
|
+
if (_matcher && _matcher.isBlocked(ip)) {
|
|
577
|
+
_stats.blocked++;
|
|
578
|
+
if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
|
|
579
|
+
sendBlockResponse(req, res, ip);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const _origEmit = http.Server.prototype.emit;
|
|
587
|
+
let _emitPatched = false;
|
|
588
|
+
|
|
589
|
+
function patchEmitLayer() {
|
|
590
|
+
if (_emitPatched) return;
|
|
591
|
+
_emitPatched = true;
|
|
592
|
+
|
|
593
|
+
http.Server.prototype.emit = function(event, req, res, ...rest) {
|
|
594
|
+
if (event === 'request' && req && res && !res.headersSent) {
|
|
595
|
+
if (firewallRequestHandler(req, res)) return true;
|
|
596
|
+
}
|
|
597
|
+
return _origEmit.call(this, event, req, res, ...rest);
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function patchHttpLayer() {
|
|
602
|
+
if (_httpPatched) return;
|
|
603
|
+
_httpPatched = true;
|
|
604
|
+
|
|
605
|
+
patchEmitLayer();
|
|
606
|
+
|
|
607
|
+
http.createServer = function(...args) {
|
|
608
|
+
if (typeof args[args.length - 1] === 'function') {
|
|
609
|
+
args[args.length - 1] = wrapListener(args[args.length - 1]);
|
|
610
|
+
}
|
|
611
|
+
return _origHttpCreate.apply(this, args);
|
|
612
|
+
};
|
|
613
|
+
Object.assign(http.createServer, _origHttpCreate);
|
|
614
|
+
|
|
615
|
+
https.createServer = function(...args) {
|
|
616
|
+
if (typeof args[args.length - 1] === 'function') {
|
|
617
|
+
args[args.length - 1] = wrapListener(args[args.length - 1]);
|
|
618
|
+
}
|
|
619
|
+
return _origHttpsCreate.apply(this, args);
|
|
620
|
+
};
|
|
621
|
+
Object.assign(https.createServer, _origHttpsCreate);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ────── Init ──────
|
|
625
|
+
|
|
626
|
+
function init(options) {
|
|
627
|
+
_options = options;
|
|
628
|
+
|
|
629
|
+
if (_options.log) console.log('[securenow] Firewall: ENABLED');
|
|
630
|
+
|
|
631
|
+
patchHttpLayer();
|
|
632
|
+
if (_options.log) console.log('[securenow] Firewall: Layer 1 (HTTP 403) active');
|
|
633
|
+
|
|
634
|
+
if (_options.tcp) {
|
|
635
|
+
try {
|
|
636
|
+
const tcpLayer = require('./firewall-tcp');
|
|
637
|
+
tcpLayer.init(() => _matcher, _options, () => _allowlistMatcher);
|
|
638
|
+
_layers.push(tcpLayer);
|
|
639
|
+
if (_options.log) console.log('[securenow] Firewall: Layer 2 (TCP drop) active');
|
|
640
|
+
} catch (e) {
|
|
641
|
+
if (_options.log) console.warn('[securenow] Firewall: Layer 2 (TCP drop) failed:', e.message);
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
if (_options.log) console.log('[securenow] Firewall: Layer 2 (TCP drop) disabled (set SECURENOW_FIREWALL_TCP=1)');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (_options.iptables) {
|
|
648
|
+
try {
|
|
649
|
+
const iptablesLayer = require('./firewall-iptables');
|
|
650
|
+
iptablesLayer.init(_options);
|
|
651
|
+
_layers.push(iptablesLayer);
|
|
652
|
+
if (_options.log) console.log('[securenow] Firewall: Layer 3 (iptables) active');
|
|
653
|
+
} catch (e) {
|
|
654
|
+
if (_options.log) console.warn('[securenow] Firewall: Layer 3 (iptables) failed:', e.message);
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
if (_options.log) console.log('[securenow] Firewall: Layer 3 (iptables) disabled (set SECURENOW_FIREWALL_IPTABLES=1)');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (_options.cloud) {
|
|
661
|
+
try {
|
|
662
|
+
const cloudLayer = require('./firewall-cloud');
|
|
663
|
+
cloudLayer.init(_options);
|
|
664
|
+
_layers.push(cloudLayer);
|
|
665
|
+
if (_options.log) console.log('[securenow] Firewall: Layer 4 (Cloud WAF) active (%s)', _options.cloud);
|
|
666
|
+
} catch (e) {
|
|
667
|
+
if (_options.log) console.warn('[securenow] Firewall: Layer 4 (Cloud WAF) failed:', e.message);
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
if (_options.log) console.log('[securenow] Firewall: Layer 4 (Cloud WAF) disabled (set SECURENOW_FIREWALL_CLOUD=cloudflare|aws|gcp)');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
startSyncLoop();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function shutdown() {
|
|
677
|
+
if (_pollTimer) { clearTimeout(_pollTimer); _pollTimer = null; }
|
|
678
|
+
if (_syncTimer) { clearInterval(_syncTimer); _syncTimer = null; }
|
|
679
|
+
|
|
680
|
+
_circuitState = 'closed';
|
|
681
|
+
_circuitOpenedAt = 0;
|
|
682
|
+
_consecutiveErrors = 0;
|
|
683
|
+
_pollInflight = false;
|
|
684
|
+
_retryAfterUntil = 0;
|
|
685
|
+
|
|
686
|
+
_httpAgent.destroy();
|
|
687
|
+
_httpsAgent.destroy();
|
|
688
|
+
|
|
689
|
+
for (const layer of _layers) {
|
|
690
|
+
try { if (typeof layer.shutdown === 'function') layer.shutdown(); } catch (_) {}
|
|
691
|
+
}
|
|
692
|
+
_layers = [];
|
|
693
|
+
|
|
694
|
+
if (_emitPatched) {
|
|
695
|
+
http.Server.prototype.emit = _origEmit;
|
|
696
|
+
_emitPatched = false;
|
|
697
|
+
}
|
|
698
|
+
if (_httpPatched) {
|
|
699
|
+
http.createServer = _origHttpCreate;
|
|
700
|
+
https.createServer = _origHttpsCreate;
|
|
701
|
+
_httpPatched = false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function getStats() {
|
|
706
|
+
return {
|
|
707
|
+
..._stats,
|
|
708
|
+
matcher: _matcher ? _matcher.stats() : null,
|
|
709
|
+
allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
|
|
710
|
+
initialized: _initialized,
|
|
711
|
+
circuitState: _circuitState,
|
|
712
|
+
consecutiveErrors: _consecutiveErrors,
|
|
713
|
+
unifiedSync: _useUnifiedSync,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getMatcher() { return _matcher; }
|
|
718
|
+
function getAllowlistMatcher() { return _allowlistMatcher; }
|
|
719
|
+
|
|
720
|
+
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher };
|