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/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
- // Rate-limit policy state is synced in Phase 1 for forward compatibility.
41
- // Enforcement is intentionally added in the next SDK phase.
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
- _rawIps = body.blocklistIps;
394
- _matcher = createMatcher(body.blocklistIps);
395
- _stats.syncs++;
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
- _rawIps = ips;
452
- _matcher = createMatcher(ips);
453
- _lastModified = res.headers['last-modified'] || null;
454
- if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
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)', s.total, s.exact, s.cidr);
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 (enforcement pending SDK phase 2)', _rateLimitRules.length);
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)', s.total, s.exact, s.cidr);
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 (enforcement pending SDK phase 2)', _rateLimitRules.length);
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 };