securenow 7.7.2 → 7.7.4
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/NPM_README.md +4 -4
- package/SKILL-API.md +5 -5
- package/app-config.js +4 -4
- package/console-instrumentation.js +2 -1
- package/firewall.js +111 -87
- package/package.json +1 -1
- package/tracing.js +2 -1
package/NPM_README.md
CHANGED
|
@@ -1092,8 +1092,8 @@ On startup, you'll see:
|
|
|
1092
1092
|
|
|
1093
1093
|
The firewall uses a version-based sync protocol for efficiency:
|
|
1094
1094
|
|
|
1095
|
-
1. **Version check** every 10 seconds (lightweight
|
|
1096
|
-
2. **Full blocklist sync** only when the version changes (or every
|
|
1095
|
+
1. **Version check** every 10 seconds (lightweight request with ETag; unchanged polls return 304 with no body)
|
|
1096
|
+
2. **Full blocklist sync** only when the version changes (or every hour as a safety net)
|
|
1097
1097
|
3. **In-memory matching** with a pre-compiled set (exact IPs) and sorted CIDR list for sub-millisecond lookups
|
|
1098
1098
|
4. **Exponential backoff** with jitter when the API is temporarily unreachable
|
|
1099
1099
|
5. **Allowlist support** -- trusted IPs are never blocked, even if they appear on the blocklist
|
|
@@ -1244,8 +1244,8 @@ Legacy env fallback aliases are listed below for existing installs only.
|
|
|
1244
1244
|
|----------|-------------|---------|
|
|
1245
1245
|
| `SECURENOW_API_KEY` | Legacy firewall key override. Prefer `apiKey` in `.securenow/credentials.json`. | from creds file |
|
|
1246
1246
|
| `SECURENOW_API_URL` | SecureNow API base URL. Auto-detected for co-located deployments (falls back to `http://localhost:4000` on ECONNREFUSED). | `https://api.securenow.ai` |
|
|
1247
|
-
| `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between
|
|
1248
|
-
| `SECURENOW_FIREWALL_SYNC_INTERVAL` |
|
|
1247
|
+
| `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between lightweight ETag checks. | `10` |
|
|
1248
|
+
| `SECURENOW_FIREWALL_SYNC_INTERVAL` | Safety-net full blocklist refresh interval in seconds. | `3600` |
|
|
1249
1249
|
| `SECURENOW_FIREWALL_FAIL_MODE` | `open` (allow when unavailable) or `closed` (block all). | `open` |
|
|
1250
1250
|
| `SECURENOW_FIREWALL_STATUS_CODE` | HTTP status code for blocked requests. | `403` |
|
|
1251
1251
|
| `SECURENOW_FIREWALL_LOG` | Log blocked requests and sync events to console. Set to `0` to silence. | `1` |
|
package/SKILL-API.md
CHANGED
|
@@ -314,8 +314,8 @@ const appConfig = require('securenow/app-config');
|
|
|
314
314
|
await firewall.init({
|
|
315
315
|
...appConfig.resolveFirewallOptions(),
|
|
316
316
|
apiUrl: 'https://api.securenow.ai',
|
|
317
|
-
syncInterval:
|
|
318
|
-
versionCheckInterval: 10, // lightweight
|
|
317
|
+
syncInterval: 3600, // safety-net full sync every hour
|
|
318
|
+
versionCheckInterval: 10, // lightweight ETag check every 10s
|
|
319
319
|
failMode: 'open', // 'open' or 'closed'
|
|
320
320
|
statusCode: 403,
|
|
321
321
|
log: true,
|
|
@@ -504,8 +504,8 @@ Local development and production use `.securenow/credentials.json`. Every settin
|
|
|
504
504
|
|----------|-------------|---------|
|
|
505
505
|
| `SECURENOW_API_KEY` | Legacy env override for the `apiKey` field (`snk_live_...`). Since v7.5.1, login writes the scoped firewall key to `.securenow/credentials.json`. | - |
|
|
506
506
|
| `SECURENOW_API_URL` | SecureNow API base URL | `https://api.securenow.ai` |
|
|
507
|
-
| `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between lightweight
|
|
508
|
-
| `SECURENOW_FIREWALL_SYNC_INTERVAL` |
|
|
507
|
+
| `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between lightweight ETag checks | `10` |
|
|
508
|
+
| `SECURENOW_FIREWALL_SYNC_INTERVAL` | Safety-net full blocklist refresh interval in seconds | `3600` |
|
|
509
509
|
| `SECURENOW_FIREWALL_FAIL_MODE` | `open` (allow all when unavailable) or `closed` | `open` |
|
|
510
510
|
| `SECURENOW_FIREWALL_STATUS_CODE` | HTTP status for blocked requests | `403` |
|
|
511
511
|
| `SECURENOW_FIREWALL_LOG` | Log blocked requests | `1` |
|
|
@@ -515,7 +515,7 @@ Local development and production use `.securenow/credentials.json`. Every settin
|
|
|
515
515
|
| `SECURENOW_FIREWALL_CLOUD_DRY_RUN` | `1` to log cloud pushes without applying | `0` |
|
|
516
516
|
| `SECURENOW_TRUSTED_PROXIES` | Comma-separated trusted proxy IPs | — |
|
|
517
517
|
|
|
518
|
-
**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
|
|
518
|
+
**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 lightweight ETag checks and initial sync retries.
|
|
519
519
|
|
|
520
520
|
### Cloud WAF Provider Variables
|
|
521
521
|
|
package/app-config.js
CHANGED
|
@@ -60,7 +60,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
60
60
|
enabled: true,
|
|
61
61
|
apiUrl: DEFAULT_API_URL,
|
|
62
62
|
versionCheckInterval: 10,
|
|
63
|
-
syncInterval:
|
|
63
|
+
syncInterval: 3600,
|
|
64
64
|
failMode: 'open',
|
|
65
65
|
statusCode: 403,
|
|
66
66
|
log: true,
|
|
@@ -111,8 +111,8 @@ const CONFIG_EXPLANATIONS = Object.freeze({
|
|
|
111
111
|
'config.runtime.hideBanner': 'Hide the free-trial response banner when using the managed free-trial collector.',
|
|
112
112
|
'config.firewall.enabled': 'Secure default: app firewall enforcement starts when apiKey is present and the dashboard toggle is on.',
|
|
113
113
|
'config.firewall.apiUrl': 'SecureNow API base URL for firewall sync.',
|
|
114
|
-
'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version checks.',
|
|
115
|
-
'config.firewall.syncInterval': 'Seconds between full firewall blocklist syncs.',
|
|
114
|
+
'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version/ETag checks.',
|
|
115
|
+
'config.firewall.syncInterval': 'Seconds between safety-net full firewall blocklist syncs.',
|
|
116
116
|
'config.firewall.failMode': 'open allows traffic if SecureNow is temporarily unreachable; closed blocks all on sync failure.',
|
|
117
117
|
'config.firewall.statusCode': 'HTTP status returned by application-layer firewall blocks.',
|
|
118
118
|
'config.firewall.log': 'Log firewall decisions locally.',
|
|
@@ -680,7 +680,7 @@ function resolveFirewallOptions() {
|
|
|
680
680
|
enabled: resolveFirewallEnabled(),
|
|
681
681
|
apiUrl: env('SECURENOW_API_URL') || DEFAULT_API_URL,
|
|
682
682
|
versionCheckInterval: numberEnv('SECURENOW_FIREWALL_VERSION_INTERVAL', 10, 1),
|
|
683
|
-
syncInterval: numberEnv('SECURENOW_FIREWALL_SYNC_INTERVAL',
|
|
683
|
+
syncInterval: numberEnv('SECURENOW_FIREWALL_SYNC_INTERVAL', 3600, 1),
|
|
684
684
|
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
685
685
|
statusCode: numberEnv('SECURENOW_FIREWALL_STATUS_CODE', 403, 100),
|
|
686
686
|
log: boolEnv('SECURENOW_FIREWALL_LOG', true),
|
|
@@ -43,13 +43,14 @@ if (console.__securenow_patched) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Store original console methods
|
|
46
|
-
const originalConsole = {
|
|
46
|
+
const originalConsole = console.__securenow_original || {
|
|
47
47
|
log: console.log,
|
|
48
48
|
info: console.info,
|
|
49
49
|
warn: console.warn,
|
|
50
50
|
error: console.error,
|
|
51
51
|
debug: console.debug,
|
|
52
52
|
};
|
|
53
|
+
if (!console.__securenow_original) console.__securenow_original = originalConsole;
|
|
53
54
|
|
|
54
55
|
// Map severity levels (OpenTelemetry standard)
|
|
55
56
|
const SeverityNumber = {
|
package/firewall.js
CHANGED
|
@@ -10,9 +10,10 @@ let _matcher = null;
|
|
|
10
10
|
let _syncTimer = null;
|
|
11
11
|
let _pollTimer = null;
|
|
12
12
|
let _lastModified = null;
|
|
13
|
-
let _lastVersion = null;
|
|
14
|
-
let _lastSyncEtag = null;
|
|
15
|
-
let
|
|
13
|
+
let _lastVersion = null;
|
|
14
|
+
let _lastSyncEtag = null;
|
|
15
|
+
let _lastUnifiedEtag = null;
|
|
16
|
+
let _initialized = false;
|
|
16
17
|
let _consecutiveErrors = 0;
|
|
17
18
|
let _layers = [];
|
|
18
19
|
let _rawIps = [];
|
|
@@ -21,7 +22,7 @@ let _localhostFallbackTried = false;
|
|
|
21
22
|
let _eventQueue = [];
|
|
22
23
|
let _eventTimer = null;
|
|
23
24
|
|
|
24
|
-
// Remote toggle
|
|
25
|
+
// Remote toggle - set by /firewall/sync when an appKey is in scope. Default
|
|
25
26
|
// true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
|
|
26
27
|
// When the dashboard / CLI flips this off, the next poll suppresses
|
|
27
28
|
// enforcement without restarting the host process.
|
|
@@ -56,9 +57,29 @@ const EVENT_QUEUE_MAX = 1_000;
|
|
|
56
57
|
const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']);
|
|
57
58
|
|
|
58
59
|
// Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
|
|
59
|
-
let _useUnifiedSync = true;
|
|
60
|
+
let _useUnifiedSync = true;
|
|
61
|
+
|
|
62
|
+
function originalConsoleMethod(method) {
|
|
63
|
+
const originals = console.__securenow_original || console.__securenowOriginalConsole;
|
|
64
|
+
return (originals && originals[method]) || console[method] || console.log;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function firewallConsole(method, ...args) {
|
|
68
|
+
if (!_options || !_options.log) return;
|
|
69
|
+
try {
|
|
70
|
+
originalConsoleMethod(method).apply(console, args);
|
|
71
|
+
} catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fwLog(...args) {
|
|
75
|
+
firewallConsole('log', ...args);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fwWarn(...args) {
|
|
79
|
+
firewallConsole('warn', ...args);
|
|
80
|
+
}
|
|
60
81
|
|
|
61
|
-
//
|
|
82
|
+
// Circuit Breaker
|
|
62
83
|
|
|
63
84
|
function maybeOpenCircuit() {
|
|
64
85
|
if (_circuitState === 'open') return;
|
|
@@ -66,7 +87,7 @@ function maybeOpenCircuit() {
|
|
|
66
87
|
_circuitState = 'open';
|
|
67
88
|
_circuitOpenedAt = Date.now();
|
|
68
89
|
if (_options && _options.log) {
|
|
69
|
-
|
|
90
|
+
fwWarn('[securenow] Firewall: circuit breaker OPEN - pausing polling for %ds', CIRCUIT_OPEN_COOLDOWN_MS / 1000);
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
93
|
}
|
|
@@ -74,7 +95,7 @@ function maybeOpenCircuit() {
|
|
|
74
95
|
function resetCircuit() {
|
|
75
96
|
if (_circuitState !== 'closed') {
|
|
76
97
|
_circuitState = 'closed';
|
|
77
|
-
if (_options && _options.log)
|
|
98
|
+
if (_options && _options.log) fwLog('[securenow] Firewall: circuit breaker CLOSED - API healthy');
|
|
78
99
|
}
|
|
79
100
|
_consecutiveErrors = 0;
|
|
80
101
|
}
|
|
@@ -99,12 +120,12 @@ function handleRetryAfter(res) {
|
|
|
99
120
|
if (secs > 0 && secs <= 300) {
|
|
100
121
|
_retryAfterUntil = Date.now() + secs * 1000;
|
|
101
122
|
if (_options && _options.log) {
|
|
102
|
-
|
|
123
|
+
fwWarn('[securenow] Firewall: API returned 429, backing off for %ds', secs);
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
126
|
}
|
|
106
127
|
|
|
107
|
-
//
|
|
128
|
+
// HTTP helpers
|
|
108
129
|
|
|
109
130
|
function buildUrl(apiUrl, path) {
|
|
110
131
|
return apiUrl.replace(/\/+$/, '') + '/api/v1' + path;
|
|
@@ -193,7 +214,7 @@ function requestWithRetry(method, url, body, extraHeaders, timeout, callback) {
|
|
|
193
214
|
if (!err || !isTransientNetworkError(err)) return callback(err, res, data);
|
|
194
215
|
|
|
195
216
|
if (_options && _options.log) {
|
|
196
|
-
|
|
217
|
+
fwWarn('[securenow] Firewall: transient API socket error, retrying once:', formatRequestError(err));
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
const retryTimer = setTimeout(() => {
|
|
@@ -234,7 +255,7 @@ function flushFirewallEvents() {
|
|
|
234
255
|
if (err || !res || res.statusCode >= 400) {
|
|
235
256
|
if (_options.log) {
|
|
236
257
|
const msg = err ? formatRequestError(err) : `API returned ${res.statusCode}`;
|
|
237
|
-
|
|
258
|
+
fwWarn('[securenow] Firewall: failed to report blocked-request ledger:', msg);
|
|
238
259
|
}
|
|
239
260
|
}
|
|
240
261
|
if (_eventQueue.length) scheduleEventFlush();
|
|
@@ -262,7 +283,7 @@ function reportFirewallEvent(event) {
|
|
|
262
283
|
else scheduleEventFlush();
|
|
263
284
|
}
|
|
264
285
|
|
|
265
|
-
//
|
|
286
|
+
// Unified Sync (v2 - single request for everything)
|
|
266
287
|
|
|
267
288
|
function doUnifiedSync(callback) {
|
|
268
289
|
const query = new URLSearchParams();
|
|
@@ -272,21 +293,23 @@ function doUnifiedSync(callback) {
|
|
|
272
293
|
const url = buildUrl(_options.apiUrl, '/firewall/sync') + suffix;
|
|
273
294
|
const headers = {};
|
|
274
295
|
if (_options.environment) headers['X-SecureNow-Environment'] = _options.environment;
|
|
275
|
-
if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
|
|
276
|
-
if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (res.
|
|
284
|
-
|
|
296
|
+
if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
|
|
297
|
+
if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
|
|
298
|
+
if (_lastUnifiedEtag) headers['If-None-Match'] = _lastUnifiedEtag;
|
|
299
|
+
|
|
300
|
+
httpGet(url, headers, 8000, (err, res, data) => {
|
|
301
|
+
if (err) return callback(err);
|
|
302
|
+
|
|
303
|
+
_stats.versionChecks++;
|
|
304
|
+
if (res.headers && res.headers.etag) _lastUnifiedEtag = res.headers.etag;
|
|
305
|
+
|
|
306
|
+
if (res.statusCode === 304) {
|
|
307
|
+
return callback(null, { blChanged: false, alChanged: false });
|
|
285
308
|
}
|
|
286
309
|
|
|
287
310
|
if (res.statusCode === 404) {
|
|
288
311
|
_useUnifiedSync = false;
|
|
289
|
-
if (_options.log)
|
|
312
|
+
if (_options.log) fwLog('[securenow] Firewall: /sync not available, using legacy endpoints');
|
|
290
313
|
return callback(null, { blChanged: false, alChanged: false, useLegacy: true });
|
|
291
314
|
}
|
|
292
315
|
|
|
@@ -304,30 +327,29 @@ function doUnifiedSync(callback) {
|
|
|
304
327
|
|
|
305
328
|
// Apply remote per-app toggle. Absent body.app means the backend either
|
|
306
329
|
// doesn't know about appKey-scoped sync (older API) or no appKey was
|
|
307
|
-
// sent
|
|
330
|
+
// sent - leave the previous value untouched (default true on first run).
|
|
308
331
|
if (body.app && typeof body.app.firewallEnabled === 'boolean') {
|
|
309
332
|
const next = body.app.firewallEnabled;
|
|
310
333
|
if (next !== _lastRemoteEnabled) {
|
|
311
334
|
_remoteEnabled = next;
|
|
312
335
|
if (_options.log) {
|
|
313
|
-
|
|
336
|
+
fwLog('[securenow] Firewall: remote toggle -> %s (app=%s)',
|
|
314
337
|
next ? 'ENABLED' : 'DISABLED', body.app.key || _options.appKey);
|
|
315
338
|
}
|
|
316
339
|
_lastRemoteEnabled = next;
|
|
317
340
|
}
|
|
318
341
|
}
|
|
319
342
|
|
|
320
|
-
// Update blocklist version + data
|
|
321
|
-
if (body.blocklist) {
|
|
322
|
-
const newVer = body.blocklist.version;
|
|
323
|
-
if (newVer !== _lastVersion) {
|
|
324
|
-
_lastVersion = newVer;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
_rawIps = body.blocklistIps;
|
|
343
|
+
// Update blocklist version + data
|
|
344
|
+
if (body.blocklist) {
|
|
345
|
+
const newVer = body.blocklist.version;
|
|
346
|
+
if (newVer !== _lastVersion) {
|
|
347
|
+
_lastVersion = newVer;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (body.blocklistIps) {
|
|
352
|
+
_rawIps = body.blocklistIps;
|
|
331
353
|
_matcher = createMatcher(body.blocklistIps);
|
|
332
354
|
_stats.syncs++;
|
|
333
355
|
notifyLayers(body.blocklistIps);
|
|
@@ -335,13 +357,12 @@ function doUnifiedSync(callback) {
|
|
|
335
357
|
}
|
|
336
358
|
|
|
337
359
|
// Update allowlist version + data
|
|
338
|
-
if (body.allowlist) {
|
|
339
|
-
const newVer = body.allowlist.version;
|
|
340
|
-
if (newVer !== _lastAllowlistVersion) {
|
|
341
|
-
_lastAllowlistVersion = newVer;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
360
|
+
if (body.allowlist) {
|
|
361
|
+
const newVer = body.allowlist.version;
|
|
362
|
+
if (newVer !== _lastAllowlistVersion) {
|
|
363
|
+
_lastAllowlistVersion = newVer;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
345
366
|
|
|
346
367
|
if (body.allowlistIps) {
|
|
347
368
|
_allowlistRawIps = body.allowlistIps;
|
|
@@ -356,7 +377,7 @@ function doUnifiedSync(callback) {
|
|
|
356
377
|
});
|
|
357
378
|
}
|
|
358
379
|
|
|
359
|
-
//
|
|
380
|
+
// Legacy Sync (v1 - separate endpoints, kept for backward compat)
|
|
360
381
|
|
|
361
382
|
function legacyBlocklistSync(callback) {
|
|
362
383
|
const url = buildFirewallUrl('/firewall/blocklist');
|
|
@@ -481,15 +502,15 @@ function doLegacyPoll(callback) {
|
|
|
481
502
|
|
|
482
503
|
if (blChanged) {
|
|
483
504
|
legacyBlocklistSync((err, changed, stats) => {
|
|
484
|
-
if (err && _options.log)
|
|
485
|
-
else if (changed && stats && _options.log)
|
|
505
|
+
if (err && _options.log) fwWarn('[securenow] Firewall: sync failed (using stale list):', formatRequestError(err));
|
|
506
|
+
else if (changed && stats && _options.log) fwLog('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
|
|
486
507
|
syncDone();
|
|
487
508
|
});
|
|
488
509
|
}
|
|
489
510
|
if (alChanged) {
|
|
490
511
|
legacyAllowlistSync((err, changed, stats) => {
|
|
491
|
-
if (err && _options.log)
|
|
492
|
-
else if (changed && stats && _options.log)
|
|
512
|
+
if (err && _options.log) fwWarn('[securenow] Firewall: allowlist sync failed:', formatRequestError(err));
|
|
513
|
+
else if (changed && stats && _options.log) fwLog('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
|
|
493
514
|
syncDone();
|
|
494
515
|
});
|
|
495
516
|
}
|
|
@@ -508,7 +529,7 @@ function doLegacyPoll(callback) {
|
|
|
508
529
|
});
|
|
509
530
|
}
|
|
510
531
|
|
|
511
|
-
//
|
|
532
|
+
// Unified poll loop
|
|
512
533
|
|
|
513
534
|
function notifyLayers(ips) {
|
|
514
535
|
for (const layer of _layers) {
|
|
@@ -526,7 +547,7 @@ function pollOnce(callback) {
|
|
|
526
547
|
_consecutiveErrors++;
|
|
527
548
|
_stats.errors++;
|
|
528
549
|
maybeOpenCircuit();
|
|
529
|
-
if (_options.log)
|
|
550
|
+
if (_options.log) fwWarn('[securenow] Firewall: poll failed:', formatRequestError(err));
|
|
530
551
|
return callback(err);
|
|
531
552
|
}
|
|
532
553
|
_consecutiveErrors = 0;
|
|
@@ -534,11 +555,11 @@ function pollOnce(callback) {
|
|
|
534
555
|
if (result) {
|
|
535
556
|
if (result.blChanged && _options.log && _matcher) {
|
|
536
557
|
const s = _matcher.stats();
|
|
537
|
-
|
|
558
|
+
fwLog('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
538
559
|
}
|
|
539
560
|
if (result.alChanged && _options.log && _allowlistMatcher) {
|
|
540
561
|
const s = _allowlistMatcher.stats();
|
|
541
|
-
|
|
562
|
+
fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
542
563
|
}
|
|
543
564
|
}
|
|
544
565
|
callback(null);
|
|
@@ -586,7 +607,7 @@ function startSyncLoop() {
|
|
|
586
607
|
_localhostFallbackTried = true;
|
|
587
608
|
const origUrl = _options.apiUrl;
|
|
588
609
|
_options.apiUrl = 'http://localhost:4000';
|
|
589
|
-
if (_options.log)
|
|
610
|
+
if (_options.log) fwLog('[securenow] Firewall: %s unreachable, trying http://localhost:4000', origUrl);
|
|
590
611
|
const retryTimer = setTimeout(initialSync, 1000);
|
|
591
612
|
if (retryTimer.unref) retryTimer.unref();
|
|
592
613
|
return;
|
|
@@ -596,7 +617,7 @@ function startSyncLoop() {
|
|
|
596
617
|
if (retryTimer.unref) retryTimer.unref();
|
|
597
618
|
return;
|
|
598
619
|
}
|
|
599
|
-
if (_options.log)
|
|
620
|
+
if (_options.log) fwWarn('[securenow] Firewall: initial sync failed:', formatRequestError(err));
|
|
600
621
|
if (_options.failMode === 'closed') {
|
|
601
622
|
_matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
|
|
602
623
|
}
|
|
@@ -616,11 +637,11 @@ function startSyncLoop() {
|
|
|
616
637
|
_initialized = true;
|
|
617
638
|
if (_options.log && _matcher) {
|
|
618
639
|
const s = _matcher.stats();
|
|
619
|
-
|
|
640
|
+
fwLog('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
620
641
|
}
|
|
621
642
|
if (_options.log && _allowlistMatcher) {
|
|
622
643
|
const s = _allowlistMatcher.stats();
|
|
623
|
-
if (s.total > 0)
|
|
644
|
+
if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
624
645
|
}
|
|
625
646
|
});
|
|
626
647
|
}
|
|
@@ -631,22 +652,25 @@ function startSyncLoop() {
|
|
|
631
652
|
// Safety-net full sync timer (less frequent, uses same path)
|
|
632
653
|
_syncTimer = setInterval(() => {
|
|
633
654
|
if (shouldSkipRequest()) return;
|
|
634
|
-
// Force a full re-fetch by clearing versions so unified endpoint returns full data
|
|
635
|
-
const savedBlVer = _lastVersion;
|
|
636
|
-
const savedAlVer = _lastAllowlistVersion;
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
655
|
+
// Force a full re-fetch by clearing versions so unified endpoint returns full data
|
|
656
|
+
const savedBlVer = _lastVersion;
|
|
657
|
+
const savedAlVer = _lastAllowlistVersion;
|
|
658
|
+
const savedUnifiedEtag = _lastUnifiedEtag;
|
|
659
|
+
_lastVersion = null;
|
|
660
|
+
_lastAllowlistVersion = null;
|
|
661
|
+
_lastUnifiedEtag = null;
|
|
662
|
+
pollOnce((err) => {
|
|
663
|
+
if (err) {
|
|
664
|
+
_lastVersion = savedBlVer;
|
|
665
|
+
_lastAllowlistVersion = savedAlVer;
|
|
666
|
+
_lastUnifiedEtag = savedUnifiedEtag;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}, fullSyncIntervalMs);
|
|
646
670
|
if (_syncTimer.unref) _syncTimer.unref();
|
|
647
671
|
}
|
|
648
672
|
|
|
649
|
-
//
|
|
673
|
+
// Layer 1: HTTP Handler
|
|
650
674
|
|
|
651
675
|
const _origHttpCreate = http.createServer;
|
|
652
676
|
const _origHttpsCreate = https.createServer;
|
|
@@ -658,7 +682,7 @@ function blockedHtml(ip) {
|
|
|
658
682
|
<html lang="en">
|
|
659
683
|
<head>
|
|
660
684
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
661
|
-
<title>Access Blocked
|
|
685
|
+
<title>Access Blocked - Security Alert</title>
|
|
662
686
|
<style>
|
|
663
687
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
664
688
|
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}
|
|
@@ -729,7 +753,7 @@ function firewallRequestHandler(req, res) {
|
|
|
729
753
|
if (_allowlistMatcher && _allowlistMatcher.stats().total > 0) {
|
|
730
754
|
if (!_allowlistMatcher.isBlocked(ip)) {
|
|
731
755
|
_stats.blocked++;
|
|
732
|
-
if (_options && _options.log)
|
|
756
|
+
if (_options && _options.log) fwLog('[securenow] Firewall: blocked %s via HTTP (not in allowlist)', ip);
|
|
733
757
|
reportFirewallEvent({
|
|
734
758
|
source: 'allowlist',
|
|
735
759
|
ip,
|
|
@@ -747,7 +771,7 @@ function firewallRequestHandler(req, res) {
|
|
|
747
771
|
// Blocklist check
|
|
748
772
|
if (_matcher && _matcher.isBlocked(ip)) {
|
|
749
773
|
_stats.blocked++;
|
|
750
|
-
if (_options && _options.log)
|
|
774
|
+
if (_options && _options.log) fwLog('[securenow] Firewall: blocked %s via HTTP', ip);
|
|
751
775
|
reportFirewallEvent({
|
|
752
776
|
source: 'blocklist',
|
|
753
777
|
ip,
|
|
@@ -801,28 +825,28 @@ function patchHttpLayer() {
|
|
|
801
825
|
Object.assign(https.createServer, _origHttpsCreate);
|
|
802
826
|
}
|
|
803
827
|
|
|
804
|
-
//
|
|
828
|
+
// Init
|
|
805
829
|
|
|
806
830
|
function init(options) {
|
|
807
831
|
_options = options;
|
|
808
832
|
|
|
809
|
-
if (_options.log)
|
|
810
|
-
if (_options.log && _options.environment)
|
|
833
|
+
if (_options.log) fwLog('[securenow] Firewall: ENABLED');
|
|
834
|
+
if (_options.log && _options.environment) fwLog('[securenow] Firewall: environment=%s', _options.environment);
|
|
811
835
|
|
|
812
836
|
patchHttpLayer();
|
|
813
|
-
if (_options.log)
|
|
837
|
+
if (_options.log) fwLog('[securenow] Firewall: Layer 1 (HTTP 403) active');
|
|
814
838
|
|
|
815
839
|
if (_options.tcp) {
|
|
816
840
|
try {
|
|
817
841
|
const tcpLayer = require('./firewall-tcp');
|
|
818
842
|
tcpLayer.init(() => _matcher, _options, () => _allowlistMatcher);
|
|
819
843
|
_layers.push(tcpLayer);
|
|
820
|
-
if (_options.log)
|
|
844
|
+
if (_options.log) fwLog('[securenow] Firewall: Layer 2 (TCP drop) active');
|
|
821
845
|
} catch (e) {
|
|
822
|
-
if (_options.log)
|
|
846
|
+
if (_options.log) fwWarn('[securenow] Firewall: Layer 2 (TCP drop) failed:', e.message);
|
|
823
847
|
}
|
|
824
848
|
} else {
|
|
825
|
-
if (_options.log)
|
|
849
|
+
if (_options.log) fwLog('[securenow] Firewall: Layer 2 (TCP drop) disabled (set config.firewall.tcp=true)');
|
|
826
850
|
}
|
|
827
851
|
|
|
828
852
|
if (_options.iptables) {
|
|
@@ -830,12 +854,12 @@ function init(options) {
|
|
|
830
854
|
const iptablesLayer = require('./firewall-iptables');
|
|
831
855
|
iptablesLayer.init(_options);
|
|
832
856
|
_layers.push(iptablesLayer);
|
|
833
|
-
if (_options.log)
|
|
857
|
+
if (_options.log) fwLog('[securenow] Firewall: Layer 3 (iptables) active');
|
|
834
858
|
} catch (e) {
|
|
835
|
-
if (_options.log)
|
|
859
|
+
if (_options.log) fwWarn('[securenow] Firewall: Layer 3 (iptables) failed:', e.message);
|
|
836
860
|
}
|
|
837
861
|
} else {
|
|
838
|
-
if (_options.log)
|
|
862
|
+
if (_options.log) fwLog('[securenow] Firewall: Layer 3 (iptables) disabled (set config.firewall.iptables=true)');
|
|
839
863
|
}
|
|
840
864
|
|
|
841
865
|
if (_options.cloud) {
|
|
@@ -843,12 +867,12 @@ function init(options) {
|
|
|
843
867
|
const cloudLayer = require('./firewall-cloud');
|
|
844
868
|
cloudLayer.init(_options);
|
|
845
869
|
_layers.push(cloudLayer);
|
|
846
|
-
if (_options.log)
|
|
870
|
+
if (_options.log) fwLog('[securenow] Firewall: Layer 4 (Cloud WAF) active (%s)', _options.cloud);
|
|
847
871
|
} catch (e) {
|
|
848
|
-
if (_options.log)
|
|
872
|
+
if (_options.log) fwWarn('[securenow] Firewall: Layer 4 (Cloud WAF) failed:', e.message);
|
|
849
873
|
}
|
|
850
874
|
} else {
|
|
851
|
-
if (_options.log)
|
|
875
|
+
if (_options.log) fwLog('[securenow] Firewall: Layer 4 (Cloud WAF) disabled (set config.firewall.cloud=cloudflare|aws|gcp)');
|
|
852
876
|
}
|
|
853
877
|
|
|
854
878
|
startSyncLoop();
|
|
@@ -907,4 +931,4 @@ function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
|
|
|
907
931
|
function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
|
|
908
932
|
function isRemoteEnabled() { return _remoteEnabled !== false; }
|
|
909
933
|
|
|
910
|
-
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, isRemoteEnabled };
|
|
934
|
+
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, isRemoteEnabled };
|
package/package.json
CHANGED
package/tracing.js
CHANGED
|
@@ -521,7 +521,8 @@ if (loggingEnabled) {
|
|
|
521
521
|
|
|
522
522
|
// Auto-patch console.* so every log/warn/error becomes an OTel log record
|
|
523
523
|
const _logger = loggerProvider.getLogger('console', '1.0.0');
|
|
524
|
-
const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
524
|
+
const _orig = console.__securenow_original || { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
525
|
+
if (!console.__securenow_original) console.__securenow_original = _orig;
|
|
525
526
|
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
526
527
|
function _emit(sn, st, args) {
|
|
527
528
|
try {
|