securenow 5.11.2 → 5.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +1 -1
- package/firewall.js +193 -28
- package/nextjs.js +3 -1
- package/package.json +1 -1
- package/tracing.js +2 -1
package/cli.js
CHANGED
package/firewall.js
CHANGED
|
@@ -8,11 +8,15 @@ const { resolveClientIp } = require('./resolve-ip');
|
|
|
8
8
|
let _options = null;
|
|
9
9
|
let _matcher = null;
|
|
10
10
|
let _syncTimer = null;
|
|
11
|
+
let _versionTimer = null;
|
|
11
12
|
let _lastModified = null;
|
|
13
|
+
let _lastVersion = null;
|
|
14
|
+
let _lastSyncEtag = null;
|
|
12
15
|
let _initialized = false;
|
|
16
|
+
let _consecutiveErrors = 0;
|
|
13
17
|
let _layers = [];
|
|
14
18
|
let _rawIps = [];
|
|
15
|
-
let _stats = { syncs: 0, blocked: 0, allowed: 0 };
|
|
19
|
+
let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
|
|
16
20
|
|
|
17
21
|
// ────── Blocklist Sync ──────
|
|
18
22
|
|
|
@@ -37,7 +41,9 @@ function syncBlocklist(callback) {
|
|
|
37
41
|
timeout: 10000,
|
|
38
42
|
};
|
|
39
43
|
|
|
40
|
-
if (
|
|
44
|
+
if (_lastSyncEtag) {
|
|
45
|
+
reqOptions.headers['If-None-Match'] = _lastSyncEtag;
|
|
46
|
+
} else if (_lastModified) {
|
|
41
47
|
reqOptions.headers['If-Modified-Since'] = _lastModified;
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -60,6 +66,7 @@ function syncBlocklist(callback) {
|
|
|
60
66
|
_rawIps = ips;
|
|
61
67
|
_matcher = createMatcher(ips);
|
|
62
68
|
_lastModified = res.headers['last-modified'] || null;
|
|
69
|
+
if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
|
|
63
70
|
_stats.syncs++;
|
|
64
71
|
notifyLayers(ips);
|
|
65
72
|
callback(null, true, _matcher.stats());
|
|
@@ -80,32 +87,117 @@ function notifyLayers(ips) {
|
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
function
|
|
84
|
-
const
|
|
90
|
+
function checkVersion(callback) {
|
|
91
|
+
const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
|
|
92
|
+
const mod = url.startsWith('https') ? https : http;
|
|
93
|
+
const parsed = new URL(url);
|
|
94
|
+
|
|
95
|
+
const headers = {
|
|
96
|
+
'Authorization': `Bearer ${_options.apiKey}`,
|
|
97
|
+
'User-Agent': 'securenow-firewall-sdk',
|
|
98
|
+
};
|
|
99
|
+
if (_lastVersion) headers['If-None-Match'] = _lastVersion;
|
|
100
|
+
|
|
101
|
+
const req = mod.request({
|
|
102
|
+
hostname: parsed.hostname,
|
|
103
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
104
|
+
path: parsed.pathname + parsed.search,
|
|
105
|
+
method: 'GET',
|
|
106
|
+
headers,
|
|
107
|
+
timeout: 5000,
|
|
108
|
+
}, (res) => {
|
|
109
|
+
_stats.versionChecks++;
|
|
110
|
+
|
|
111
|
+
if (res.statusCode === 304) {
|
|
112
|
+
_consecutiveErrors = 0;
|
|
113
|
+
res.resume();
|
|
114
|
+
callback(null, false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let data = '';
|
|
119
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
120
|
+
res.on('end', () => {
|
|
121
|
+
if (res.statusCode !== 200) {
|
|
122
|
+
_consecutiveErrors++;
|
|
123
|
+
_stats.errors++;
|
|
124
|
+
callback(null, false);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
_consecutiveErrors = 0;
|
|
128
|
+
try {
|
|
129
|
+
const body = JSON.parse(data);
|
|
130
|
+
const version = body.version || null;
|
|
131
|
+
const changed = version !== _lastVersion;
|
|
132
|
+
if (changed) _lastVersion = version;
|
|
133
|
+
callback(null, changed);
|
|
134
|
+
} catch (_e) { callback(null, false); }
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
req.on('error', () => { _consecutiveErrors++; _stats.errors++; callback(null, false); });
|
|
139
|
+
req.on('timeout', () => { req.destroy(); _consecutiveErrors++; _stats.errors++; callback(null, false); });
|
|
140
|
+
req.end();
|
|
141
|
+
}
|
|
85
142
|
|
|
143
|
+
function doFullSync() {
|
|
86
144
|
syncBlocklist((err, changed, stats) => {
|
|
87
145
|
if (err) {
|
|
88
|
-
if (_options.log) console.warn('[securenow] Firewall:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
_matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
|
|
92
|
-
}
|
|
93
|
-
} else if (changed && stats) {
|
|
94
|
-
if (_options.log) console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
|
|
146
|
+
if (_options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err.message);
|
|
147
|
+
} else if (changed && stats && _options.log) {
|
|
148
|
+
console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
|
|
95
149
|
}
|
|
96
|
-
_initialized = true;
|
|
97
150
|
});
|
|
151
|
+
}
|
|
98
152
|
|
|
99
|
-
|
|
153
|
+
function jitter(baseMs) {
|
|
154
|
+
return baseMs * (0.8 + Math.random() * 0.4);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scheduleNextVersionCheck() {
|
|
158
|
+
const baseMs = (_options.versionCheckInterval || 10) * 1000;
|
|
159
|
+
const backoffMs = Math.min(baseMs * Math.pow(2, _consecutiveErrors), 120_000);
|
|
160
|
+
const delayMs = jitter(backoffMs);
|
|
161
|
+
|
|
162
|
+
_versionTimer = setTimeout(() => {
|
|
163
|
+
checkVersion((_err, changed) => {
|
|
164
|
+
if (changed) {
|
|
165
|
+
if (_options.log) console.log('[securenow] Firewall: blocklist version changed, syncing…');
|
|
166
|
+
doFullSync();
|
|
167
|
+
}
|
|
168
|
+
scheduleNextVersionCheck();
|
|
169
|
+
});
|
|
170
|
+
}, delayMs);
|
|
171
|
+
if (_versionTimer.unref) _versionTimer.unref();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function startSyncLoop() {
|
|
175
|
+
const fullSyncIntervalMs = (_options.syncInterval || 300) * 1000;
|
|
176
|
+
const RETRY_DELAY = 5000;
|
|
177
|
+
|
|
178
|
+
function initialSync() {
|
|
100
179
|
syncBlocklist((err, changed, stats) => {
|
|
101
180
|
if (err) {
|
|
102
|
-
if (_options.log) console.warn('[securenow] Firewall: sync failed
|
|
103
|
-
|
|
104
|
-
|
|
181
|
+
if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
|
|
182
|
+
if (_options.failMode === 'closed') {
|
|
183
|
+
_matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
|
|
184
|
+
}
|
|
185
|
+
const retryTimer = setTimeout(initialSync, RETRY_DELAY);
|
|
186
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (changed && stats) {
|
|
190
|
+
if (_options.log) console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
|
|
105
191
|
}
|
|
192
|
+
_initialized = true;
|
|
106
193
|
});
|
|
107
|
-
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
initialSync();
|
|
108
197
|
|
|
198
|
+
scheduleNextVersionCheck();
|
|
199
|
+
|
|
200
|
+
_syncTimer = setInterval(() => { doFullSync(); }, fullSyncIntervalMs);
|
|
109
201
|
if (_syncTimer.unref) _syncTimer.unref();
|
|
110
202
|
}
|
|
111
203
|
|
|
@@ -115,21 +207,87 @@ const _origHttpCreate = http.createServer;
|
|
|
115
207
|
const _origHttpsCreate = https.createServer;
|
|
116
208
|
let _httpPatched = false;
|
|
117
209
|
|
|
210
|
+
function blockedHtml(ip) {
|
|
211
|
+
const maskedIp = ip || 'unknown';
|
|
212
|
+
return `<!DOCTYPE html>
|
|
213
|
+
<html lang="en">
|
|
214
|
+
<head>
|
|
215
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
216
|
+
<title>Access Blocked — Security Alert</title>
|
|
217
|
+
<style>
|
|
218
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
219
|
+
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}
|
|
220
|
+
.wrap{text-align:center;max-width:540px;padding:2.5rem 2rem}
|
|
221
|
+
.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}
|
|
222
|
+
.icon svg{width:32px;height:32px;fill:none;stroke:#dc2626;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
223
|
+
.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}
|
|
224
|
+
h1{font-size:1.6rem;font-weight:700;margin-bottom:.6rem;color:#fff}
|
|
225
|
+
.sub{font-size:1rem;color:#f87171;font-weight:600;margin-bottom:1.25rem}
|
|
226
|
+
p{font-size:.9rem;line-height:1.7;color:#a1a1aa;margin-bottom:.5rem}
|
|
227
|
+
.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}
|
|
228
|
+
.divider{width:48px;height:2px;background:rgba(220,38,38,.3);margin:1.5rem auto}
|
|
229
|
+
.contact{font-size:.85rem;color:#71717a;line-height:1.7}
|
|
230
|
+
.contact a{color:#f87171;text-decoration:none;font-weight:500}
|
|
231
|
+
.contact a:hover{text-decoration:underline}
|
|
232
|
+
.footer{margin-top:2rem;font-size:.7rem;color:#3f3f46}
|
|
233
|
+
</style>
|
|
234
|
+
</head>
|
|
235
|
+
<body>
|
|
236
|
+
<div class="wrap">
|
|
237
|
+
<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>
|
|
238
|
+
<span class="badge">Security Alert</span>
|
|
239
|
+
<h1>Access Blocked</h1>
|
|
240
|
+
<p class="sub">Malicious activity detected from your IP address</p>
|
|
241
|
+
<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>
|
|
242
|
+
<div class="ip-box">${maskedIp}</div>
|
|
243
|
+
<div class="divider"></div>
|
|
244
|
+
<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>
|
|
245
|
+
<p class="footer">Ref: ${maskedIp} — ${new Date().toISOString()} — HTTP 403</p>
|
|
246
|
+
</div>
|
|
247
|
+
</body>
|
|
248
|
+
</html>`;
|
|
249
|
+
}
|
|
250
|
+
|
|
118
251
|
function wrapListener(originalListener) {
|
|
119
252
|
return function firewallGuard(req, res) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
253
|
+
_stats.allowed++;
|
|
254
|
+
return originalListener.call(this, req, res);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function firewallRequestHandler(req, res) {
|
|
259
|
+
if (_matcher) {
|
|
260
|
+
const ip = resolveClientIp(req);
|
|
261
|
+
if (_matcher.isBlocked(ip)) {
|
|
262
|
+
_stats.blocked++;
|
|
263
|
+
if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
|
|
264
|
+
const code = (_options && _options.statusCode) || 403;
|
|
265
|
+
const accept = req.headers['accept'] || '';
|
|
266
|
+
if (accept.includes('text/html')) {
|
|
267
|
+
res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
268
|
+
res.end(blockedHtml(ip));
|
|
269
|
+
} else {
|
|
126
270
|
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
127
|
-
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
128
|
-
return;
|
|
271
|
+
res.end(JSON.stringify({ error: 'Forbidden', ip }));
|
|
129
272
|
}
|
|
273
|
+
return true;
|
|
130
274
|
}
|
|
131
|
-
|
|
132
|
-
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const _origEmit = http.Server.prototype.emit;
|
|
280
|
+
let _emitPatched = false;
|
|
281
|
+
|
|
282
|
+
function patchEmitLayer() {
|
|
283
|
+
if (_emitPatched) return;
|
|
284
|
+
_emitPatched = true;
|
|
285
|
+
|
|
286
|
+
http.Server.prototype.emit = function(event, req, res, ...rest) {
|
|
287
|
+
if (event === 'request' && req && res && !res.headersSent) {
|
|
288
|
+
if (firewallRequestHandler(req, res)) return true;
|
|
289
|
+
}
|
|
290
|
+
return _origEmit.call(this, event, req, res, ...rest);
|
|
133
291
|
};
|
|
134
292
|
}
|
|
135
293
|
|
|
@@ -137,6 +295,9 @@ function patchHttpLayer() {
|
|
|
137
295
|
if (_httpPatched) return;
|
|
138
296
|
_httpPatched = true;
|
|
139
297
|
|
|
298
|
+
// Patch Server.prototype.emit to intercept requests on already-created servers
|
|
299
|
+
patchEmitLayer();
|
|
300
|
+
|
|
140
301
|
http.createServer = function(...args) {
|
|
141
302
|
if (typeof args[args.length - 1] === 'function') {
|
|
142
303
|
args[args.length - 1] = wrapListener(args[args.length - 1]);
|
|
@@ -211,6 +372,7 @@ function init(options) {
|
|
|
211
372
|
}
|
|
212
373
|
|
|
213
374
|
function shutdown() {
|
|
375
|
+
if (_versionTimer) { clearTimeout(_versionTimer); _versionTimer = null; }
|
|
214
376
|
if (_syncTimer) { clearInterval(_syncTimer); _syncTimer = null; }
|
|
215
377
|
|
|
216
378
|
for (const layer of _layers) {
|
|
@@ -218,7 +380,10 @@ function shutdown() {
|
|
|
218
380
|
}
|
|
219
381
|
_layers = [];
|
|
220
382
|
|
|
221
|
-
|
|
383
|
+
if (_emitPatched) {
|
|
384
|
+
http.Server.prototype.emit = _origEmit;
|
|
385
|
+
_emitPatched = false;
|
|
386
|
+
}
|
|
222
387
|
if (_httpPatched) {
|
|
223
388
|
http.createServer = _origHttpCreate;
|
|
224
389
|
https.createServer = _origHttpsCreate;
|
package/nextjs.js
CHANGED
|
@@ -548,7 +548,8 @@ function registerSecureNow(options = {}) {
|
|
|
548
548
|
require('./firewall').init({
|
|
549
549
|
apiKey: firewallApiKey,
|
|
550
550
|
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
551
|
-
|
|
551
|
+
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
552
|
+
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
552
553
|
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
553
554
|
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
554
555
|
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
@@ -561,6 +562,7 @@ function registerSecureNow(options = {}) {
|
|
|
561
562
|
}
|
|
562
563
|
}
|
|
563
564
|
|
|
565
|
+
|
|
564
566
|
console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
|
|
565
567
|
console.log('[securenow] 📊 Auto-capturing comprehensive request metadata:');
|
|
566
568
|
console.log('[securenow] • IP addresses (x-forwarded-for, x-real-ip, socket)');
|
package/package.json
CHANGED
package/tracing.js
CHANGED
|
@@ -556,7 +556,8 @@ const sdk = new NodeSDK({
|
|
|
556
556
|
require('./firewall').init({
|
|
557
557
|
apiKey: firewallApiKey,
|
|
558
558
|
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
559
|
-
|
|
559
|
+
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
560
|
+
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
560
561
|
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
561
562
|
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
562
563
|
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|