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