securenow 7.7.15 → 7.8.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/NPM_README.md +35 -22
- package/README.md +50 -32
- package/SKILL-API.md +49 -25
- package/SKILL-CLI.md +61 -40
- package/app-config.js +127 -18
- package/cli/apiKey.js +7 -7
- package/cli/apps.js +3 -3
- package/cli/auth.js +113 -31
- package/cli/client.js +14 -13
- package/cli/config.js +219 -45
- package/cli/credentials.js +3 -3
- package/cli/diagnostics.js +35 -10
- package/cli/firewall.js +19 -7
- package/cli/init.js +5 -5
- package/cli/security.js +31 -11
- package/cli.js +57 -22
- package/firewall-only.js +4 -4
- package/firewall.js +172 -45
- package/mcp/catalog.js +43 -30
- package/mcp/server.js +73 -12
- package/nextjs.js +49 -11
- package/nuxt-server-plugin.mjs +8 -4
- package/otel-defaults.js +11 -0
- package/package.json +2 -1
- package/tracing.js +49 -12
- package/web-vite.mjs +3 -0
package/firewall.js
CHANGED
|
@@ -17,10 +17,12 @@ 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 = [];
|
|
23
24
|
let _eventTimer = null;
|
|
25
|
+
let _remainingApiUrlFallbacks = [];
|
|
24
26
|
|
|
25
27
|
// Remote toggle - set by /firewall/sync when an appKey is in scope. Default
|
|
26
28
|
// true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
|
|
@@ -36,8 +38,8 @@ let _lastAllowlistModified = null;
|
|
|
36
38
|
let _lastAllowlistVersion = null;
|
|
37
39
|
let _lastAllowlistSyncEtag = null;
|
|
38
40
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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.
|
|
41
43
|
let _rateLimitRules = [];
|
|
42
44
|
let _lastRateLimitVersion = null;
|
|
43
45
|
let _rateLimitBuckets = new Map();
|
|
@@ -60,7 +62,7 @@ const _httpsAgent = new https.Agent({ keepAlive: false });
|
|
|
60
62
|
const EVENT_FLUSH_INTERVAL_MS = 2_000;
|
|
61
63
|
const EVENT_BATCH_SIZE = 25;
|
|
62
64
|
const EVENT_QUEUE_MAX = 1_000;
|
|
63
|
-
const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']);
|
|
65
|
+
const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND']);
|
|
64
66
|
|
|
65
67
|
// Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
|
|
66
68
|
let _useUnifiedSync = true;
|
|
@@ -156,7 +158,13 @@ function agentFor(url) {
|
|
|
156
158
|
function isTransientNetworkError(err) {
|
|
157
159
|
if (!err) return false;
|
|
158
160
|
if (err.code && TRANSIENT_NETWORK_CODES.has(err.code)) return true;
|
|
159
|
-
return /socket hang up|connection reset|ECONNRESET/i.test(String(err.message || ''));
|
|
161
|
+
return /socket hang up|connection reset|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(String(err.message || ''));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isApiReachabilityError(err) {
|
|
165
|
+
if (isTransientNetworkError(err)) return true;
|
|
166
|
+
const text = `${err && err.code || ''} ${err && err.message || ''}`;
|
|
167
|
+
return /TLS|SSL|certificate|CERT_|UNABLE_TO_VERIFY|self signed/i.test(text);
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
function formatRequestError(err) {
|
|
@@ -167,6 +175,33 @@ function formatRequestError(err) {
|
|
|
167
175
|
return parts.join(' ');
|
|
168
176
|
}
|
|
169
177
|
|
|
178
|
+
function resetApiUrlFallbacks() {
|
|
179
|
+
const seen = new Set([_options && _options.apiUrl].filter(Boolean));
|
|
180
|
+
_remainingApiUrlFallbacks = [];
|
|
181
|
+
for (const candidate of Array.isArray(_options && _options.apiUrlFallbacks) ? _options.apiUrlFallbacks : []) {
|
|
182
|
+
const url = String(candidate || '').trim().replace(/\/$/, '');
|
|
183
|
+
if (!url || seen.has(url)) continue;
|
|
184
|
+
seen.add(url);
|
|
185
|
+
_remainingApiUrlFallbacks.push(url);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function switchToNextApiUrl(reason) {
|
|
190
|
+
if (!_options || _remainingApiUrlFallbacks.length === 0) return false;
|
|
191
|
+
const previous = _options.apiUrl;
|
|
192
|
+
_options.apiUrl = _remainingApiUrlFallbacks.shift();
|
|
193
|
+
_lastUnifiedEtag = null;
|
|
194
|
+
_lastSyncEtag = null;
|
|
195
|
+
_lastAllowlistSyncEtag = null;
|
|
196
|
+
if (_options.log) {
|
|
197
|
+
fwWarn('[securenow] Firewall: %s unreachable (%s), retrying sync via %s',
|
|
198
|
+
previous,
|
|
199
|
+
reason || 'network error',
|
|
200
|
+
_options.apiUrl);
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
170
205
|
function requestOnce(method, url, body, extraHeaders, timeout, callback) {
|
|
171
206
|
const mod = url.startsWith('https') ? https : http;
|
|
172
207
|
const parsed = new URL(url);
|
|
@@ -288,9 +323,44 @@ function reportFirewallEvent(event) {
|
|
|
288
323
|
if (_eventQueue.length >= EVENT_BATCH_SIZE) flushFirewallEvents();
|
|
289
324
|
else scheduleEventFlush();
|
|
290
325
|
}
|
|
291
|
-
|
|
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
|
+
|
|
292
362
|
// Unified Sync (v2 - single request for everything)
|
|
293
|
-
|
|
363
|
+
|
|
294
364
|
function doUnifiedSync(callback) {
|
|
295
365
|
const query = new URLSearchParams();
|
|
296
366
|
if (_options.appKey) query.set('app', _options.appKey);
|
|
@@ -355,13 +425,10 @@ function doUnifiedSync(callback) {
|
|
|
355
425
|
}
|
|
356
426
|
}
|
|
357
427
|
|
|
358
|
-
if (body.blocklistIps) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
notifyLayers(body.blocklistIps);
|
|
363
|
-
blChanged = true;
|
|
364
|
-
}
|
|
428
|
+
if (body.blocklistIps || body.blocklistRules) {
|
|
429
|
+
setBlocklistData(body.blocklistIps || [], body.blocklistRules || []);
|
|
430
|
+
blChanged = true;
|
|
431
|
+
}
|
|
365
432
|
|
|
366
433
|
// Update allowlist version + data
|
|
367
434
|
if (body.allowlist) {
|
|
@@ -411,16 +478,13 @@ function legacyBlocklistSync(callback) {
|
|
|
411
478
|
if (res.statusCode === 429) { handleRetryAfter(res); }
|
|
412
479
|
if (res.statusCode !== 200) return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
413
480
|
|
|
414
|
-
try {
|
|
415
|
-
const body = JSON.parse(data);
|
|
416
|
-
const ips = body.ips || [];
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
_stats.syncs++;
|
|
422
|
-
notifyLayers(ips);
|
|
423
|
-
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());
|
|
424
488
|
} catch (e) {
|
|
425
489
|
callback(new Error(`Failed to parse blocklist: ${e.message}`));
|
|
426
490
|
}
|
|
@@ -562,9 +626,15 @@ function pollOnce(callback) {
|
|
|
562
626
|
|
|
563
627
|
const done = (err, result) => {
|
|
564
628
|
_pollInflight = false;
|
|
565
|
-
if (err) {
|
|
566
|
-
|
|
567
|
-
|
|
629
|
+
if (err) {
|
|
630
|
+
if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
|
|
631
|
+
_pollInflight = false;
|
|
632
|
+
const retryTimer = setTimeout(() => pollOnce(callback), 1000);
|
|
633
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
_consecutiveErrors++;
|
|
637
|
+
_stats.errors++;
|
|
568
638
|
maybeOpenCircuit();
|
|
569
639
|
if (_options.log) fwWarn('[securenow] Firewall: poll failed:', formatRequestError(err));
|
|
570
640
|
return callback(err);
|
|
@@ -572,16 +642,17 @@ function pollOnce(callback) {
|
|
|
572
642
|
_consecutiveErrors = 0;
|
|
573
643
|
resetCircuit();
|
|
574
644
|
if (result) {
|
|
575
|
-
if (result.blChanged && _options.log && _matcher) {
|
|
576
|
-
const s = _matcher.stats();
|
|
577
|
-
fwLog('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)
|
|
578
|
-
|
|
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
|
+
}
|
|
579
650
|
if (result.alChanged && _options.log && _allowlistMatcher) {
|
|
580
651
|
const s = _allowlistMatcher.stats();
|
|
581
652
|
fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
582
653
|
}
|
|
583
654
|
if (result.rlChanged && _options.log) {
|
|
584
|
-
fwLog('[securenow] Firewall: re-synced %d rate-limit rules
|
|
655
|
+
fwLog('[securenow] Firewall: re-synced %d rate-limit rules', _rateLimitRules.length);
|
|
585
656
|
}
|
|
586
657
|
}
|
|
587
658
|
callback(null);
|
|
@@ -623,9 +694,14 @@ function startSyncLoop() {
|
|
|
623
694
|
const syncFn = _useUnifiedSync ? doUnifiedSync : (cb) => doLegacyPoll(cb);
|
|
624
695
|
|
|
625
696
|
syncFn((err, result) => {
|
|
626
|
-
if (err) {
|
|
627
|
-
const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
|
|
628
|
-
if (
|
|
697
|
+
if (err) {
|
|
698
|
+
const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
|
|
699
|
+
if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
|
|
700
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
701
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
|
|
629
705
|
_localhostFallbackTried = true;
|
|
630
706
|
const origUrl = _options.apiUrl;
|
|
631
707
|
_options.apiUrl = 'http://localhost:4000';
|
|
@@ -656,17 +732,18 @@ function startSyncLoop() {
|
|
|
656
732
|
return;
|
|
657
733
|
}
|
|
658
734
|
|
|
659
|
-
_initialized = true;
|
|
660
|
-
if (_options.log && _matcher) {
|
|
661
|
-
const s = _matcher.stats();
|
|
662
|
-
fwLog('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)
|
|
663
|
-
|
|
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
|
+
}
|
|
664
741
|
if (_options.log && _allowlistMatcher) {
|
|
665
742
|
const s = _allowlistMatcher.stats();
|
|
666
743
|
if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
667
744
|
}
|
|
668
745
|
if (_options.log && _rateLimitRules.length > 0) {
|
|
669
|
-
fwLog('[securenow] Firewall: synced %d rate-limit rules
|
|
746
|
+
fwLog('[securenow] Firewall: synced %d rate-limit rules', _rateLimitRules.length);
|
|
670
747
|
}
|
|
671
748
|
});
|
|
672
749
|
}
|
|
@@ -814,7 +891,7 @@ function rulePathMatches(rule, path) {
|
|
|
814
891
|
if (mode === 'regex') {
|
|
815
892
|
try { return new RegExp(pattern).test(path); } catch (_) { return false; }
|
|
816
893
|
}
|
|
817
|
-
return path.startsWith(pattern);
|
|
894
|
+
return path.startsWith(normalizePrefixPattern(pattern));
|
|
818
895
|
}
|
|
819
896
|
|
|
820
897
|
function rateLimitKey(rule, ip) {
|
|
@@ -823,6 +900,28 @@ function rateLimitKey(rule, ip) {
|
|
|
823
900
|
return `${id}|${subject}`;
|
|
824
901
|
}
|
|
825
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
|
+
|
|
826
925
|
function checkRateLimitRules(req, ip) {
|
|
827
926
|
if (!_rateLimitRules || _rateLimitRules.length === 0) return null;
|
|
828
927
|
const method = String(req.method || 'GET').toUpperCase();
|
|
@@ -918,6 +1017,24 @@ function firewallRequestHandler(req, res) {
|
|
|
918
1017
|
return true;
|
|
919
1018
|
}
|
|
920
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
|
+
|
|
921
1038
|
const rateLimitDecision = checkRateLimitRules(req, ip);
|
|
922
1039
|
if (rateLimitDecision) {
|
|
923
1040
|
_stats.rateLimited++;
|
|
@@ -981,10 +1098,15 @@ function patchHttpLayer() {
|
|
|
981
1098
|
|
|
982
1099
|
// Init
|
|
983
1100
|
|
|
984
|
-
function init(options) {
|
|
985
|
-
_options = options;
|
|
1101
|
+
function init(options) {
|
|
1102
|
+
_options = options || {};
|
|
1103
|
+
_options.apiUrl = String(_options.apiUrl || '').trim().replace(/\/$/, '');
|
|
1104
|
+
_localhostFallbackTried = false;
|
|
1105
|
+
_useUnifiedSync = true;
|
|
1106
|
+
resetApiUrlFallbacks();
|
|
986
1107
|
|
|
987
1108
|
if (_options.log) fwLog('[securenow] Firewall: ENABLED');
|
|
1109
|
+
if (_options.log && _options.apiUrl) fwLog('[securenow] Firewall: sync endpoint=%s/api/v1/firewall/sync', _options.apiUrl);
|
|
988
1110
|
if (_options.log && _options.environment) fwLog('[securenow] Firewall: environment=%s', _options.environment);
|
|
989
1111
|
|
|
990
1112
|
patchHttpLayer();
|
|
@@ -1043,8 +1165,11 @@ function shutdown() {
|
|
|
1043
1165
|
_consecutiveErrors = 0;
|
|
1044
1166
|
_pollInflight = false;
|
|
1045
1167
|
_retryAfterUntil = 0;
|
|
1168
|
+
_localhostFallbackTried = false;
|
|
1169
|
+
_blocklistRules = [];
|
|
1046
1170
|
_rateLimitRules = [];
|
|
1047
1171
|
_rateLimitBuckets = new Map();
|
|
1172
|
+
_remainingApiUrlFallbacks = [];
|
|
1048
1173
|
|
|
1049
1174
|
_httpAgent.destroy();
|
|
1050
1175
|
_httpsAgent.destroy();
|
|
@@ -1067,8 +1192,9 @@ function shutdown() {
|
|
|
1067
1192
|
|
|
1068
1193
|
function getStats() {
|
|
1069
1194
|
return {
|
|
1070
|
-
..._stats,
|
|
1195
|
+
..._stats,
|
|
1071
1196
|
matcher: _matcher ? _matcher.stats() : null,
|
|
1197
|
+
blocklistRules: _blocklistRules.length,
|
|
1072
1198
|
allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
|
|
1073
1199
|
rateLimitRules: _rateLimitRules.length,
|
|
1074
1200
|
rateLimitBuckets: _rateLimitBuckets.size,
|
|
@@ -1087,7 +1213,8 @@ function getStats() {
|
|
|
1087
1213
|
// as "no IPs to block" without us mutating the cached matcher.
|
|
1088
1214
|
function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
|
|
1089
1215
|
function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
|
|
1216
|
+
function getBlocklistRules() { return _remoteEnabled === false ? [] : _blocklistRules.slice(); }
|
|
1090
1217
|
function getRateLimitRules() { return _remoteEnabled === false ? [] : _rateLimitRules.slice(); }
|
|
1091
1218
|
function isRemoteEnabled() { return _remoteEnabled !== false; }
|
|
1092
1219
|
|
|
1093
|
-
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getRateLimitRules, isRemoteEnabled };
|
|
1220
|
+
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getBlocklistRules, getRateLimitRules, isRemoteEnabled };
|
package/mcp/catalog.js
CHANGED
|
@@ -10,14 +10,15 @@ const MARKDOWN = 'text/markdown';
|
|
|
10
10
|
const UNIVERSAL_SECURENOW_SETUP_PROMPT = `You are working in an existing JavaScript or TypeScript app. Set up SecureNow end-to-end for the framework/runtime already used by this repo. Treat this as a real onboarding, not just a package install.
|
|
11
11
|
|
|
12
12
|
Primary goals:
|
|
13
|
-
- Use the latest published SecureNow npm package. Require securenow@7.
|
|
13
|
+
- Use the latest published SecureNow npm package. Require securenow@7.8.0 or newer for split admin/runtime credentials.
|
|
14
14
|
- By default, enable tracing, logs, POST request body capture, multipart metadata capture, and the SecureNow firewall.
|
|
15
15
|
- If I explicitly ask for firewall-only mode, keep the same install/login/verification gates, but use firewall-only preload and do not add tracing, logging, or OTel instrumentation.
|
|
16
|
-
- The firewall must protect the selected SecureNow app, use SecureNow's own blocklist/allowlist/IPDB data, and respect that app's SecureNow IPDB confidence threshold. Do not add custom IP reputation providers or custom auto-blocking.
|
|
16
|
+
- The firewall must protect the selected SecureNow app, use SecureNow's own blocklist/allowlist/IPDB data, and respect that app's SecureNow IPDB confidence threshold. Do not add custom IP reputation providers or custom auto-blocking.
|
|
17
|
+
- Do not confuse IP Allowlist with Trusted IPs. IP Allowlist is restrictive deny-by-default: when any allowlist entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. Use Trusted IPs for known-safe monitors, office/VPN traffic, or false-positive suppression. Only use allowlist after explicit human approval to lock the app/environment to known IPs.
|
|
17
18
|
|
|
18
19
|
Safety rules:
|
|
19
|
-
- Do not print full API keys, JWTs, tokens, or .securenow/credentials.json. Mask secrets.
|
|
20
|
-
- Do not commit secrets. Ignore only local SecureNow credential files (.securenow/credentials.json and .securenow/credentials.*.json); keep the .securenow/ directory itself trackable for repo-owned docs/templates.
|
|
20
|
+
- Do not print full API keys, JWTs, tokens, or local SecureNow credential files (.securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, or .securenow/credentials.*.json). Mask secrets.
|
|
21
|
+
- Do not commit secrets. Ignore only local SecureNow credential files (.securenow/admin.json, .securenow/runtime.json, .securenow/credentials.json, and .securenow/credentials.*.json); keep the .securenow/ directory itself trackable for repo-owned docs/templates.
|
|
21
22
|
- Do not manually browse to a SecureNow auth URL. Always start auth with npx securenow login so the CLI generates the required callback and state.
|
|
22
23
|
- If the browser says "Missing callback parameter", you opened the wrong URL: rerun npx securenow login from the project root.
|
|
23
24
|
- Do not skip login, app selection, firewall connection, or verification unless I explicitly say to.
|
|
@@ -31,33 +32,35 @@ Runbook:
|
|
|
31
32
|
2. Install or upgrade SecureNow with the detected package manager, using securenow@latest. Verify the actual installed version with:
|
|
32
33
|
node -p "require('./node_modules/securenow/package.json').version"
|
|
33
34
|
npx securenow version
|
|
34
|
-
Stop and fix the install if either is below 7.
|
|
35
|
+
Stop and fix the install if either is below 7.8.0 or npx still resolves an older local package.
|
|
35
36
|
3. Read the installed package surface before editing files: node_modules/securenow/package.json, README/NPM_README, SKILL-API, SKILL-CLI, npx securenow help, and relevant subcommand help for login/init/firewall/doctor/env/test-span/log/mcp.
|
|
36
|
-
4. Mandatory auth gate:
|
|
37
|
-
- Run npx securenow whoami from the project root.
|
|
38
|
-
- If
|
|
39
|
-
-
|
|
40
|
-
-
|
|
37
|
+
4. Mandatory auth/runtime gate:
|
|
38
|
+
- Run npx securenow whoami from the project root.
|
|
39
|
+
- If admin auth is missing, run npx securenow admin login from the project root and wait for the browser flow.
|
|
40
|
+
- If runtime app config is missing, run npx securenow app connect from the project root and wait for the browser flow.
|
|
41
|
+
- After the CLI exits, rerun npx securenow whoami.
|
|
42
|
+
- Do not proceed to app edits or verification until whoami shows the required lane(s). SDK setup needs runtime app config; admin/global MCP operations need admin auth.
|
|
41
43
|
5. Validate project-local credentials without exposing secrets:
|
|
42
|
-
- Confirm .securenow/credentials.json exists.
|
|
43
|
-
- Confirm
|
|
44
|
-
- Confirm
|
|
45
|
-
- Confirm .securenow/
|
|
46
|
-
|
|
44
|
+
- Confirm .securenow/runtime.json exists for SDK runtime setup, or legacy .securenow/credentials.json exists for old installs.
|
|
45
|
+
- Confirm the runtime file has SecureNow's default config/explanations block.
|
|
46
|
+
- Confirm the runtime file has an app key/name/instance and a firewall API key after app selection.
|
|
47
|
+
- Confirm .securenow/admin.json exists only when admin CLI/MCP auth is needed.
|
|
48
|
+
- Confirm .securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
|
|
49
|
+
6. Run npx securenow init. If it fails with ui.header is not a function or another CLI bug, upgrade to securenow@latest, verify >=7.8.0, and retry. Do not silently ignore init failures.
|
|
47
50
|
7. Configure the least invasive framework-specific integration:
|
|
48
51
|
- Next.js: preserve instrumentation.js/ts. Register securenow/nextjs only when NEXT_RUNTIME is nodejs. In ESM files, use createRequire before require("securenow/nextjs"). Include require("securenow/nextjs-auto-capture") for body capture. For Next 15+, add securenow to serverExternalPackages. For older Next.js, use experimental.serverComponentsExternalPackages. Preserve proxy.js/middleware.js.
|
|
49
52
|
- Nuxt/Nitro: use the documented securenow/nuxt module or Nitro server plugin.
|
|
50
53
|
- Express/Fastify/NestJS/Koa/Hapi/Hono/raw Node: preload securenow/register through existing scripts, NODE_OPTIONS, PM2 node_args, Docker CMD, or the process manager already used.
|
|
51
54
|
- Firewall-only: preload securenow/firewall-only or use the documented securenow run --firewall-only command. Do not add OTel/tracing/logging in this mode.
|
|
52
55
|
- Vite/browser-only: use only documented browser integration and state that server firewall protection requires a server runtime.
|
|
53
|
-
8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/credentials.json:
|
|
56
|
+
8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/runtime.json, with legacy .securenow/credentials.json and generated runtime credential files still supported:
|
|
54
57
|
- config.logging.enabled: true
|
|
55
58
|
- config.capture.body: true
|
|
56
59
|
- config.capture.multipart: true
|
|
57
60
|
- config.firewall.enabled: true
|
|
58
61
|
- config.firewall.failMode: "open"
|
|
59
62
|
- config.capture.maxBodySize: 10240
|
|
60
|
-
For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json. Do not recommend env vars unless the user explicitly asks for legacy fallbacks.
|
|
63
|
+
For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json or <app-root>/.securenow/credentials.production.json. Do not recommend env vars unless the user explicitly asks for legacy fallbacks.
|
|
61
64
|
Local credentials should use config.runtime.deploymentEnvironment="local". Production runtime credentials should use "production". The app key stays the same; traces, logs, firewall status, forensics, and CLI/MCP queries are scoped by environment.
|
|
62
65
|
9. Verify firewall and threshold:
|
|
63
66
|
- Run npx securenow firewall apps and npx securenow firewall status.
|
|
@@ -1108,19 +1111,22 @@ const TOOLS = [
|
|
|
1108
1111
|
{
|
|
1109
1112
|
name: 'securenow_blocklist_add',
|
|
1110
1113
|
title: 'Add Blocked IP',
|
|
1111
|
-
description: 'Add an IP/CIDR to the blocklist. Write action; requires confirmation.',
|
|
1114
|
+
description: 'Add an IP/CIDR to the blocklist, optionally scoped to a route/method. Write action; requires confirmation.',
|
|
1112
1115
|
scope: 'blocklist:write',
|
|
1113
1116
|
readOnly: false,
|
|
1114
1117
|
confirm: true,
|
|
1115
1118
|
method: 'POST',
|
|
1116
1119
|
endpoint: '/blocklist',
|
|
1117
|
-
bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment'],
|
|
1120
|
+
bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment', 'pathPattern', 'pathMatchMode', 'method'],
|
|
1118
1121
|
inputSchema: objectSchema({
|
|
1119
1122
|
ip: string('IPv4 address or CIDR.'),
|
|
1120
1123
|
reason: string('Reason for blocking.'),
|
|
1121
1124
|
expiresAt: string('Optional expiry time as ISO 8601.'),
|
|
1122
1125
|
metadata: { type: 'object', additionalProperties: true, description: 'Optional metadata.' },
|
|
1123
1126
|
appKey: string('Optional application key to scope this block. Omit for all apps.'),
|
|
1127
|
+
pathPattern: string('Optional route/path pattern, for example /admin*. Omit to block all routes.'),
|
|
1128
|
+
pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'], description: 'Path matching mode. Defaults to prefix.' },
|
|
1129
|
+
method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], description: 'HTTP method scope. Defaults to ALL.' },
|
|
1124
1130
|
...environmentInput,
|
|
1125
1131
|
...confirmSchema,
|
|
1126
1132
|
}, ['ip', 'confirm', 'reason']),
|
|
@@ -1169,8 +1175,8 @@ const TOOLS = [
|
|
|
1169
1175
|
},
|
|
1170
1176
|
{
|
|
1171
1177
|
name: 'securenow_allowlist_list',
|
|
1172
|
-
title: 'List Allowlist',
|
|
1173
|
-
description: 'List
|
|
1178
|
+
title: 'List Restrictive Allowlist',
|
|
1179
|
+
description: 'List restrictive allowlist entries. If any active entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. This is not the Trusted IP list.',
|
|
1174
1180
|
scope: 'allowlist:read',
|
|
1175
1181
|
readOnly: true,
|
|
1176
1182
|
method: 'GET',
|
|
@@ -1184,24 +1190,26 @@ const TOOLS = [
|
|
|
1184
1190
|
},
|
|
1185
1191
|
{
|
|
1186
1192
|
name: 'securenow_allowlist_add',
|
|
1187
|
-
title: 'Add
|
|
1188
|
-
description: '
|
|
1193
|
+
title: 'Add Restrictive Allowlist IP',
|
|
1194
|
+
description: 'Dangerous production action: add an IP/CIDR to the restrictive allowlist. Once active, allowlist mode blocks every IP except listed entries. Do not use this to mark an IP trusted or suppress false positives; use securenow_trusted_add instead. Requires explicit human approval confirming deny-all behavior.',
|
|
1189
1195
|
scope: 'allowlist:write',
|
|
1190
1196
|
readOnly: false,
|
|
1191
1197
|
confirm: true,
|
|
1192
1198
|
method: 'POST',
|
|
1193
1199
|
endpoint: '/allowlist',
|
|
1194
|
-
|
|
1200
|
+
fixedBody: { initiatedBy: 'mcp' },
|
|
1201
|
+
bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys', 'environment', 'allowlistDenyAllApproved'],
|
|
1195
1202
|
inputSchema: objectSchema({
|
|
1196
1203
|
ip: string('IPv4 address or CIDR.'),
|
|
1197
1204
|
label: string('Human-readable label.'),
|
|
1198
|
-
reason: string('
|
|
1205
|
+
reason: string('Required human-approved reason. Must acknowledge that allowlist blocks all non-listed IPs and why Trusted IPs is not the correct action.'),
|
|
1199
1206
|
expiresAt: string('Optional expiry time as ISO 8601.'),
|
|
1200
1207
|
applicationsAll: boolean('Apply to all applications.'),
|
|
1201
1208
|
applicationKeys: arrayOfStrings('Application keys to scope this allowlist entry to.'),
|
|
1209
|
+
allowlistDenyAllApproved: boolean('Must be true only after the user explicitly approves deny-by-default allowlist behavior. Do not set this for trusted IPs, monitors, false positives, or investigation cleanup.'),
|
|
1202
1210
|
...environmentInput,
|
|
1203
1211
|
...confirmSchema,
|
|
1204
|
-
}, ['ip', 'confirm', 'reason']),
|
|
1212
|
+
}, ['ip', 'confirm', 'reason', 'allowlistDenyAllApproved']),
|
|
1205
1213
|
},
|
|
1206
1214
|
{
|
|
1207
1215
|
name: 'securenow_allowlist_remove',
|
|
@@ -1221,7 +1229,7 @@ const TOOLS = [
|
|
|
1221
1229
|
{
|
|
1222
1230
|
name: 'securenow_trusted_list',
|
|
1223
1231
|
title: 'List Trusted IPs',
|
|
1224
|
-
description: 'List trusted IPs.',
|
|
1232
|
+
description: 'List trusted IPs. Trusted IPs are for known-safe traffic and do not turn on deny-by-default allowlist mode.',
|
|
1225
1233
|
scope: 'trusted_ips:read',
|
|
1226
1234
|
readOnly: true,
|
|
1227
1235
|
method: 'GET',
|
|
@@ -1235,7 +1243,7 @@ const TOOLS = [
|
|
|
1235
1243
|
{
|
|
1236
1244
|
name: 'securenow_trusted_add',
|
|
1237
1245
|
title: 'Add Trusted IP',
|
|
1238
|
-
description: 'Add a trusted IP/CIDR. Write action; requires confirmation.',
|
|
1246
|
+
description: 'Add a trusted IP/CIDR for known-safe infrastructure, monitors, office/VPN traffic, or scoped false-positive suppression. This does not enable deny-by-default allowlist mode and is the correct tool when an IP should be trusted without blocking other visitors. Write action; requires confirmation.',
|
|
1239
1247
|
scope: 'trusted_ips:write',
|
|
1240
1248
|
readOnly: false,
|
|
1241
1249
|
confirm: true,
|
|
@@ -1417,7 +1425,7 @@ function promptMessages(name, args = {}) {
|
|
|
1417
1425
|
'Verify SecureNow default-on protection for this project.',
|
|
1418
1426
|
args.appKey ? `App key: ${args.appKey}` : null,
|
|
1419
1427
|
'Check npx securenow whoami, npx securenow api-key show, npx securenow firewall apps, and npx securenow firewall status.',
|
|
1420
|
-
'Confirm traces, logs, capture.body, capture.multipart, and firewall.enabled are enabled by .securenow/
|
|
1428
|
+
'Confirm traces, logs, capture.body, capture.multipart, and firewall.enabled are enabled by .securenow/runtime.json defaults unless explicitly set false. Legacy .securenow/credentials.json is still accepted.',
|
|
1421
1429
|
'Do not print full tokens or API keys.',
|
|
1422
1430
|
].filter(Boolean).join('\n'),
|
|
1423
1431
|
},
|
|
@@ -1436,7 +1444,7 @@ function promptMessages(name, args = {}) {
|
|
|
1436
1444
|
args.appKeys ? `Scope to app keys: ${args.appKeys}` : null,
|
|
1437
1445
|
`Environment scope: ${args.environment || 'production'}.`,
|
|
1438
1446
|
'Use IP intelligence first, then related traces/logs, then recommend remediation.',
|
|
1439
|
-
'Only block
|
|
1447
|
+
'Only block or trust the IP after explicit user confirmation. Never use IP Allowlist for false positives or trusted traffic; allowlist is deny-by-default and blocks all non-listed IPs.',
|
|
1440
1448
|
].filter(Boolean).join('\n'),
|
|
1441
1449
|
},
|
|
1442
1450
|
},
|
|
@@ -1461,6 +1469,7 @@ function promptMessages(name, args = {}) {
|
|
|
1461
1469
|
'Return one clear outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
|
|
1462
1470
|
'Use rate limiting only as temporary soft remediation for repeated route-specific abuse such as login brute force, credential stuffing, scraping/API bursts, enumeration, recon/probing, or repeated noisy payloads where risk/impact evidence is below the block threshold.',
|
|
1463
1471
|
'Do not rate-limit instead of blocking when there is confirmed exploit success, token/data exposure, SSRF reachability, file read, RCE, persistence, malware/C2, or riskScore >= 85 with high-confidence malicious evidence. Do not rate-limit benign false positives, trusted monitors, app-server/proxy attribution problems, isolated one-off requests, or broad noisy rules that need alert tuning.',
|
|
1472
|
+
'Do not create or recommend IP Allowlist entries during investigations unless the user explicitly asks to lock the app/environment to a known set of IPs. For safe monitors, offices, VPNs, or false positives, use Trusted IPs or scoped false-positive exclusions instead.',
|
|
1464
1473
|
confirmWrites
|
|
1465
1474
|
? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block, securenow_rate_limit_create_from_text plus securenow_human_action_decision_report_add(outcome=rate_limited), or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If you skip/mark ambiguous but still need to record the audit trail, call securenow_human_action_decision_report_add.'
|
|
1466
1475
|
: 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
|
|
@@ -1487,6 +1496,7 @@ function promptMessages(name, args = {}) {
|
|
|
1487
1496
|
'For each row choose exactly one outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
|
|
1488
1497
|
'Use Rate Limit only for repeated route-specific abuse where temporary friction is safer than blocking: login brute force/credential stuffing, password reset/account enumeration, scraping/API bursts, path or ID enumeration, recon/probing, or repeated noisy payloads without confirmed exploit success.',
|
|
1489
1498
|
'Do not rate-limit confirmed high-risk attacks that should be blocked, benign traffic that should be false-positive scoped, broad noisy rules that need tuning, app-server/proxy attribution problems, or isolated one-off requests.',
|
|
1499
|
+
'Do not create or recommend IP Allowlist entries while working this queue unless the user explicitly approves deny-by-default allowlist mode for the whole app/environment. Use Trusted IPs for trusted/bypass cases.',
|
|
1490
1500
|
confirmWrites
|
|
1491
1501
|
? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For skipped/ambiguous/rule-tuning-needed rows that should be auditable without changing IP status, use securenow_human_action_decision_report_add.'
|
|
1492
1502
|
: 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
|
|
@@ -1595,6 +1605,9 @@ function assertConfirmed(tool, args = {}) {
|
|
|
1595
1605
|
if (!args.reason || !String(args.reason).trim()) {
|
|
1596
1606
|
throw new Error(`${tool.name} requires a non-empty reason.`);
|
|
1597
1607
|
}
|
|
1608
|
+
if (tool.name === 'securenow_allowlist_add' && args.allowlistDenyAllApproved !== true) {
|
|
1609
|
+
throw new Error('securenow_allowlist_add is deny-by-default: it blocks every non-listed IP for the scoped app/environment. Pass allowlistDenyAllApproved:true only after explicit human approval. Use securenow_trusted_add for trusted IPs or false positives.');
|
|
1610
|
+
}
|
|
1598
1611
|
}
|
|
1599
1612
|
|
|
1600
1613
|
function buildApiRequest(tool, rawArgs = {}) {
|