securenow 7.7.16 → 8.0.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/NPM_README.md +44 -36
- package/README.md +56 -38
- package/SKILL-API.md +51 -27
- package/SKILL-CLI.md +67 -45
- package/app-config.js +90 -160
- package/cli/apiKey.js +21 -12
- package/cli/apps.js +3 -3
- package/cli/auth.js +114 -32
- package/cli/client.js +14 -13
- package/cli/config.js +219 -52
- package/cli/credentials.js +4 -4
- package/cli/diagnostics.js +5 -6
- package/cli/firewall.js +19 -7
- package/cli/human.js +13 -8
- package/cli/init.js +5 -5
- package/cli/run.js +1 -5
- package/cli/security.js +31 -11
- package/cli/utils.js +2 -3
- package/cli.js +68 -35
- package/console-instrumentation.js +1 -1
- package/firewall-only.js +7 -11
- package/firewall.js +110 -35
- package/mcp/catalog.js +582 -45
- package/mcp/server.js +73 -12
- package/nextjs-auto-capture.js +3 -6
- package/nextjs-middleware.js +2 -4
- package/nextjs-wrapper.js +3 -6
- package/nextjs.js +4 -11
- package/nuxt-server-plugin.mjs +7 -4
- package/otel-defaults.js +11 -0
- package/package.json +3 -3
- package/rate-limits.js +0 -2
- package/register-vite.js +5 -12
- package/register.js +5 -13
- package/resolve-ip.js +1 -1
- package/tracing.d.ts +1 -1
- package/tracing.js +6 -3
- package/web-vite.mjs +58 -62
package/firewall.js
CHANGED
|
@@ -17,6 +17,7 @@ let _initialized = false;
|
|
|
17
17
|
let _consecutiveErrors = 0;
|
|
18
18
|
let _layers = [];
|
|
19
19
|
let _rawIps = [];
|
|
20
|
+
let _blocklistRules = [];
|
|
20
21
|
let _stats = { syncs: 0, blocked: 0, rateLimited: 0, allowed: 0, versionChecks: 0, errors: 0, suppressedDisabled: 0 };
|
|
21
22
|
let _localhostFallbackTried = false;
|
|
22
23
|
let _eventQueue = [];
|
|
@@ -37,8 +38,8 @@ let _lastAllowlistModified = null;
|
|
|
37
38
|
let _lastAllowlistVersion = null;
|
|
38
39
|
let _lastAllowlistSyncEtag = null;
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
41
|
+
// Route/method-scoped policy state. Global IP/CIDR blocks stay in _matcher so
|
|
42
|
+
// TCP, OS firewall, and Cloud WAF layers never over-enforce an HTTP-only rule.
|
|
42
43
|
let _rateLimitRules = [];
|
|
43
44
|
let _lastRateLimitVersion = null;
|
|
44
45
|
let _rateLimitBuckets = new Map();
|
|
@@ -322,9 +323,44 @@ function reportFirewallEvent(event) {
|
|
|
322
323
|
if (_eventQueue.length >= EVENT_BATCH_SIZE) flushFirewallEvents();
|
|
323
324
|
else scheduleEventFlush();
|
|
324
325
|
}
|
|
325
|
-
|
|
326
|
+
|
|
327
|
+
function normalizePrefixPattern(pattern) {
|
|
328
|
+
const value = String(pattern || '');
|
|
329
|
+
return value.endsWith('*') ? value.slice(0, -1) : value;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeBlocklistRules(rules) {
|
|
333
|
+
if (!Array.isArray(rules)) return [];
|
|
334
|
+
return rules
|
|
335
|
+
.map((rule) => {
|
|
336
|
+
if (!rule || !rule.ip) return null;
|
|
337
|
+
const pathMatchMode = ['exact', 'prefix', 'regex'].includes(String(rule.pathMatchMode || '').toLowerCase())
|
|
338
|
+
? String(rule.pathMatchMode).toLowerCase()
|
|
339
|
+
: 'prefix';
|
|
340
|
+
return {
|
|
341
|
+
...rule,
|
|
342
|
+
ip: String(rule.ip || '').trim(),
|
|
343
|
+
method: String(rule.method || 'ALL').toUpperCase(),
|
|
344
|
+
pathPattern: pathMatchMode === 'prefix'
|
|
345
|
+
? normalizePrefixPattern(rule.pathPattern)
|
|
346
|
+
: String(rule.pathPattern || '').trim(),
|
|
347
|
+
pathMatchMode,
|
|
348
|
+
};
|
|
349
|
+
})
|
|
350
|
+
.filter(Boolean);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function setBlocklistData(ips, rules) {
|
|
354
|
+
const normalizedIps = Array.isArray(ips) ? ips : [];
|
|
355
|
+
_rawIps = normalizedIps;
|
|
356
|
+
_blocklistRules = normalizeBlocklistRules(rules);
|
|
357
|
+
_matcher = createMatcher(normalizedIps);
|
|
358
|
+
_stats.syncs++;
|
|
359
|
+
notifyLayers(normalizedIps);
|
|
360
|
+
}
|
|
361
|
+
|
|
326
362
|
// Unified Sync (v2 - single request for everything)
|
|
327
|
-
|
|
363
|
+
|
|
328
364
|
function doUnifiedSync(callback) {
|
|
329
365
|
const query = new URLSearchParams();
|
|
330
366
|
if (_options.appKey) query.set('app', _options.appKey);
|
|
@@ -389,13 +425,10 @@ function doUnifiedSync(callback) {
|
|
|
389
425
|
}
|
|
390
426
|
}
|
|
391
427
|
|
|
392
|
-
if (body.blocklistIps) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
notifyLayers(body.blocklistIps);
|
|
397
|
-
blChanged = true;
|
|
398
|
-
}
|
|
428
|
+
if (body.blocklistIps || body.blocklistRules) {
|
|
429
|
+
setBlocklistData(body.blocklistIps || [], body.blocklistRules || []);
|
|
430
|
+
blChanged = true;
|
|
431
|
+
}
|
|
399
432
|
|
|
400
433
|
// Update allowlist version + data
|
|
401
434
|
if (body.allowlist) {
|
|
@@ -445,16 +478,13 @@ function legacyBlocklistSync(callback) {
|
|
|
445
478
|
if (res.statusCode === 429) { handleRetryAfter(res); }
|
|
446
479
|
if (res.statusCode !== 200) return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
447
480
|
|
|
448
|
-
try {
|
|
449
|
-
const body = JSON.parse(data);
|
|
450
|
-
const ips = body.ips || [];
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
_stats.syncs++;
|
|
456
|
-
notifyLayers(ips);
|
|
457
|
-
callback(null, true, _matcher.stats());
|
|
481
|
+
try {
|
|
482
|
+
const body = JSON.parse(data);
|
|
483
|
+
const ips = body.ips || [];
|
|
484
|
+
setBlocklistData(ips, body.rules || body.blocklistRules || []);
|
|
485
|
+
_lastModified = res.headers['last-modified'] || null;
|
|
486
|
+
if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
|
|
487
|
+
callback(null, true, _matcher.stats());
|
|
458
488
|
} catch (e) {
|
|
459
489
|
callback(new Error(`Failed to parse blocklist: ${e.message}`));
|
|
460
490
|
}
|
|
@@ -612,16 +642,17 @@ function pollOnce(callback) {
|
|
|
612
642
|
_consecutiveErrors = 0;
|
|
613
643
|
resetCircuit();
|
|
614
644
|
if (result) {
|
|
615
|
-
if (result.blChanged && _options.log && _matcher) {
|
|
616
|
-
const s = _matcher.stats();
|
|
617
|
-
fwLog('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)
|
|
618
|
-
|
|
645
|
+
if (result.blChanged && _options.log && _matcher) {
|
|
646
|
+
const s = _matcher.stats();
|
|
647
|
+
fwLog('[securenow] Firewall: re-synced %d global blocked IPs (%d exact + %d CIDR ranges) and %d scoped block rules',
|
|
648
|
+
s.total, s.exact, s.cidr, _blocklistRules.length);
|
|
649
|
+
}
|
|
619
650
|
if (result.alChanged && _options.log && _allowlistMatcher) {
|
|
620
651
|
const s = _allowlistMatcher.stats();
|
|
621
652
|
fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
622
653
|
}
|
|
623
654
|
if (result.rlChanged && _options.log) {
|
|
624
|
-
fwLog('[securenow] Firewall: re-synced %d rate-limit rules
|
|
655
|
+
fwLog('[securenow] Firewall: re-synced %d rate-limit rules', _rateLimitRules.length);
|
|
625
656
|
}
|
|
626
657
|
}
|
|
627
658
|
callback(null);
|
|
@@ -701,17 +732,18 @@ function startSyncLoop() {
|
|
|
701
732
|
return;
|
|
702
733
|
}
|
|
703
734
|
|
|
704
|
-
_initialized = true;
|
|
705
|
-
if (_options.log && _matcher) {
|
|
706
|
-
const s = _matcher.stats();
|
|
707
|
-
fwLog('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)
|
|
708
|
-
|
|
735
|
+
_initialized = true;
|
|
736
|
+
if (_options.log && _matcher) {
|
|
737
|
+
const s = _matcher.stats();
|
|
738
|
+
fwLog('[securenow] Firewall: synced %d global blocked IPs (%d exact + %d CIDR ranges) and %d scoped block rules',
|
|
739
|
+
s.total, s.exact, s.cidr, _blocklistRules.length);
|
|
740
|
+
}
|
|
709
741
|
if (_options.log && _allowlistMatcher) {
|
|
710
742
|
const s = _allowlistMatcher.stats();
|
|
711
743
|
if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
712
744
|
}
|
|
713
745
|
if (_options.log && _rateLimitRules.length > 0) {
|
|
714
|
-
fwLog('[securenow] Firewall: synced %d rate-limit rules
|
|
746
|
+
fwLog('[securenow] Firewall: synced %d rate-limit rules', _rateLimitRules.length);
|
|
715
747
|
}
|
|
716
748
|
});
|
|
717
749
|
}
|
|
@@ -859,7 +891,7 @@ function rulePathMatches(rule, path) {
|
|
|
859
891
|
if (mode === 'regex') {
|
|
860
892
|
try { return new RegExp(pattern).test(path); } catch (_) { return false; }
|
|
861
893
|
}
|
|
862
|
-
return path.startsWith(pattern);
|
|
894
|
+
return path.startsWith(normalizePrefixPattern(pattern));
|
|
863
895
|
}
|
|
864
896
|
|
|
865
897
|
function rateLimitKey(rule, ip) {
|
|
@@ -868,6 +900,28 @@ function rateLimitKey(rule, ip) {
|
|
|
868
900
|
return `${id}|${subject}`;
|
|
869
901
|
}
|
|
870
902
|
|
|
903
|
+
function checkBlocklistRules(req, ip) {
|
|
904
|
+
if (!_blocklistRules || _blocklistRules.length === 0) return null;
|
|
905
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
906
|
+
const path = requestPath(req);
|
|
907
|
+
|
|
908
|
+
for (const rule of _blocklistRules) {
|
|
909
|
+
if (!rule) continue;
|
|
910
|
+
const ruleMethod = String(rule.method || 'ALL').toUpperCase();
|
|
911
|
+
if (ruleMethod !== 'ALL' && ruleMethod !== method) continue;
|
|
912
|
+
if (!ruleIpMatches(rule, ip)) continue;
|
|
913
|
+
if (!rulePathMatches(rule, path)) continue;
|
|
914
|
+
return {
|
|
915
|
+
rule,
|
|
916
|
+
ruleId: rule.id || rule._id || '',
|
|
917
|
+
matchedEntry: rule.ip || ip,
|
|
918
|
+
path,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
|
|
871
925
|
function checkRateLimitRules(req, ip) {
|
|
872
926
|
if (!_rateLimitRules || _rateLimitRules.length === 0) return null;
|
|
873
927
|
const method = String(req.method || 'GET').toUpperCase();
|
|
@@ -963,6 +1017,24 @@ function firewallRequestHandler(req, res) {
|
|
|
963
1017
|
return true;
|
|
964
1018
|
}
|
|
965
1019
|
|
|
1020
|
+
const blockRuleDecision = checkBlocklistRules(req, ip);
|
|
1021
|
+
if (blockRuleDecision) {
|
|
1022
|
+
_stats.blocked++;
|
|
1023
|
+
if (_options && _options.log) {
|
|
1024
|
+
fwLog('[securenow] Firewall: blocked %s via HTTP (rule=%s)', ip, blockRuleDecision.ruleId || 'scoped');
|
|
1025
|
+
}
|
|
1026
|
+
reportFirewallEvent({
|
|
1027
|
+
source: 'blocklist',
|
|
1028
|
+
ip,
|
|
1029
|
+
matchedEntry: blockRuleDecision.matchedEntry || ip,
|
|
1030
|
+
method: req.method || '',
|
|
1031
|
+
path: blockRuleDecision.path || req.url || '',
|
|
1032
|
+
userAgent: req.headers['user-agent'] || '',
|
|
1033
|
+
});
|
|
1034
|
+
sendBlockResponse(req, res, ip);
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
966
1038
|
const rateLimitDecision = checkRateLimitRules(req, ip);
|
|
967
1039
|
if (rateLimitDecision) {
|
|
968
1040
|
_stats.rateLimited++;
|
|
@@ -1094,6 +1166,7 @@ function shutdown() {
|
|
|
1094
1166
|
_pollInflight = false;
|
|
1095
1167
|
_retryAfterUntil = 0;
|
|
1096
1168
|
_localhostFallbackTried = false;
|
|
1169
|
+
_blocklistRules = [];
|
|
1097
1170
|
_rateLimitRules = [];
|
|
1098
1171
|
_rateLimitBuckets = new Map();
|
|
1099
1172
|
_remainingApiUrlFallbacks = [];
|
|
@@ -1119,8 +1192,9 @@ function shutdown() {
|
|
|
1119
1192
|
|
|
1120
1193
|
function getStats() {
|
|
1121
1194
|
return {
|
|
1122
|
-
..._stats,
|
|
1195
|
+
..._stats,
|
|
1123
1196
|
matcher: _matcher ? _matcher.stats() : null,
|
|
1197
|
+
blocklistRules: _blocklistRules.length,
|
|
1124
1198
|
allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
|
|
1125
1199
|
rateLimitRules: _rateLimitRules.length,
|
|
1126
1200
|
rateLimitBuckets: _rateLimitBuckets.size,
|
|
@@ -1139,7 +1213,8 @@ function getStats() {
|
|
|
1139
1213
|
// as "no IPs to block" without us mutating the cached matcher.
|
|
1140
1214
|
function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
|
|
1141
1215
|
function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
|
|
1216
|
+
function getBlocklistRules() { return _remoteEnabled === false ? [] : _blocklistRules.slice(); }
|
|
1142
1217
|
function getRateLimitRules() { return _remoteEnabled === false ? [] : _rateLimitRules.slice(); }
|
|
1143
1218
|
function isRemoteEnabled() { return _remoteEnabled !== false; }
|
|
1144
1219
|
|
|
1145
|
-
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getRateLimitRules, isRemoteEnabled };
|
|
1220
|
+
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getBlocklistRules, getRateLimitRules, isRemoteEnabled };
|