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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
4
  const ui = require('./cli/ui');
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 (_lastModified) {
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 startSyncLoop() {
84
- const intervalMs = (_options.syncInterval || 60) * 1000;
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: initial sync failed:', err.message);
89
- if (_options.failMode === 'closed') {
90
- _matcher = createMatcher([]); // block everything by default via empty matcher
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
- _syncTimer = setInterval(() => {
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 (using stale list):', err.message);
103
- } else if (changed && stats && _options.log) {
104
- console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
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
- }, intervalMs);
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} &mdash; ${new Date().toISOString()} &mdash; HTTP 403</p>
246
+ </div>
247
+ </body>
248
+ </html>`;
249
+ }
250
+
118
251
  function wrapListener(originalListener) {
119
252
  return function firewallGuard(req, res) {
120
- if (_matcher) {
121
- const ip = resolveClientIp(req);
122
- if (_matcher.isBlocked(ip)) {
123
- _stats.blocked++;
124
- if (_options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
125
- const code = _options.statusCode || 403;
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
- _stats.allowed++;
132
- return originalListener.call(this, req, res);
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
- // Restore original createServer
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
- syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 60,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.11.2",
3
+ "version": "5.12.1",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
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
- syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 60,
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',