securenow 7.7.7 → 7.7.8
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/cli/firewall.js +5 -4
- package/cli/rateLimits.js +245 -0
- package/cli.js +43 -1
- package/firewall.js +59 -31
- package/package.json +1 -1
package/cli/firewall.js
CHANGED
|
@@ -36,10 +36,11 @@ async function status(args, flags) {
|
|
|
36
36
|
['App scope', appKey || 'all apps'],
|
|
37
37
|
['Environment', data.environment || environment],
|
|
38
38
|
['Last updated', data.updatedAt || 'unknown'],
|
|
39
|
-
['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
|
|
40
|
-
['Allowlist updated', data.allowlistUpdatedAt || 'never'],
|
|
41
|
-
['
|
|
42
|
-
|
|
39
|
+
['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
|
|
40
|
+
['Allowlist updated', data.allowlistUpdatedAt || 'never'],
|
|
41
|
+
['Rate limit rules', String(data.rateLimitCount ?? 0)],
|
|
42
|
+
['Sync TTL', `${data.ttl || 60}s`],
|
|
43
|
+
]);
|
|
43
44
|
if (data.allowlistCount > 0) {
|
|
44
45
|
console.log('');
|
|
45
46
|
console.log(` ${ui.c.yellow('!')} Allowlist is active — only ${data.allowlistCount} IP(s) can reach your app`);
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { api, requireAuth } = require('./client');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const ui = require('./ui');
|
|
6
|
+
|
|
7
|
+
function resolveApp(flags) {
|
|
8
|
+
return flags.app || config.getDefaultApp();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolveEnvironment(flags, fallback = null) {
|
|
12
|
+
return flags.env || flags.environment || fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseWindow(value) {
|
|
16
|
+
if (value == null || value === '') return 60;
|
|
17
|
+
if (/^\d+$/.test(String(value))) return Number(value);
|
|
18
|
+
const raw = String(value).trim().toLowerCase();
|
|
19
|
+
const match = raw.match(/^(\d+)\s*(s|sec|second|seconds|m|min|minute|minutes|h|hr|hour|hours)$/);
|
|
20
|
+
if (!match) {
|
|
21
|
+
ui.error('Window must be seconds or look like 30s, 1m, or 1h.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const n = Number(match[1]);
|
|
25
|
+
const unit = match[2][0];
|
|
26
|
+
return unit === 'h' ? n * 3600 : unit === 'm' ? n * 60 : n;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildQuery(flags) {
|
|
30
|
+
const query = {};
|
|
31
|
+
if (flags.page) query.page = flags.page;
|
|
32
|
+
if (flags.limit) query.limit = flags.limit;
|
|
33
|
+
if (flags.status) query.status = flags.status;
|
|
34
|
+
if (flags.search) query.search = flags.search;
|
|
35
|
+
const appKey = resolveApp(flags);
|
|
36
|
+
if (appKey) query.appKey = appKey;
|
|
37
|
+
const environment = resolveEnvironment(flags, null);
|
|
38
|
+
if (environment) query.environment = environment;
|
|
39
|
+
return query;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function describeTarget(rule) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (rule.ip) parts.push(rule.ip);
|
|
45
|
+
if (rule.method && rule.method !== 'ALL') parts.push(rule.method);
|
|
46
|
+
if (rule.pathPattern) parts.push(`${rule.pathMatchMode || 'prefix'}:${rule.pathPattern}`);
|
|
47
|
+
return parts.join(' ') || 'all traffic';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function list(args, flags) {
|
|
51
|
+
requireAuth();
|
|
52
|
+
const s = ui.spinner('Fetching rate limits');
|
|
53
|
+
try {
|
|
54
|
+
const data = await api.get('/rate-limits', { query: buildQuery(flags) });
|
|
55
|
+
const rules = data.rateLimits || [];
|
|
56
|
+
s.stop(`Found ${rules.length} rate-limit rule${rules.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
57
|
+
|
|
58
|
+
if (flags.json) { ui.json(data); return; }
|
|
59
|
+
|
|
60
|
+
console.log('');
|
|
61
|
+
const rows = rules.map((r) => [
|
|
62
|
+
ui.c.dim(ui.truncate(r.id || r._id, 12)),
|
|
63
|
+
r.status === 'active' ? ui.statusBadge('active') : ui.statusBadge(r.status || 'unknown'),
|
|
64
|
+
describeTarget(r),
|
|
65
|
+
`${r.limit}/${r.windowSeconds}s`,
|
|
66
|
+
r.applicationKey || ui.c.dim('all apps'),
|
|
67
|
+
r.environment || ui.c.dim('all envs'),
|
|
68
|
+
r.expiresAt ? new Date(r.expiresAt).toLocaleString() : ui.c.dim('permanent'),
|
|
69
|
+
ui.timeAgo(r.createdAt),
|
|
70
|
+
]);
|
|
71
|
+
ui.table(['ID', 'Status', 'Target', 'Limit', 'App', 'Env', 'Expires', 'Created'], rows);
|
|
72
|
+
console.log('');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
s.fail('Failed to fetch rate limits');
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function show(args, flags) {
|
|
80
|
+
requireAuth();
|
|
81
|
+
const id = args[0];
|
|
82
|
+
if (!id) {
|
|
83
|
+
ui.error('Usage: securenow ratelimit show <id>');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const s = ui.spinner('Fetching rate limit');
|
|
87
|
+
try {
|
|
88
|
+
const data = await api.get(`/rate-limits/${id}`);
|
|
89
|
+
const r = data.rateLimit;
|
|
90
|
+
s.stop('Rate limit loaded');
|
|
91
|
+
if (flags.json) { ui.json(r); return; }
|
|
92
|
+
console.log('');
|
|
93
|
+
ui.heading(r.name || 'Rate limit');
|
|
94
|
+
console.log('');
|
|
95
|
+
ui.keyValue([
|
|
96
|
+
['ID', r.id || r._id || id],
|
|
97
|
+
['Status', r.status || '-'],
|
|
98
|
+
['Target', describeTarget(r)],
|
|
99
|
+
['Limit', `${r.limit}/${r.windowSeconds}s`],
|
|
100
|
+
['Key by', r.keyBy || 'ip'],
|
|
101
|
+
['App', r.applicationKey || 'all apps'],
|
|
102
|
+
['Environment', r.environment || 'all envs'],
|
|
103
|
+
['Reason', r.reason || '-'],
|
|
104
|
+
['Expires', r.expiresAt || 'permanent'],
|
|
105
|
+
]);
|
|
106
|
+
console.log('');
|
|
107
|
+
} catch (err) {
|
|
108
|
+
s.fail('Failed to fetch rate limit');
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function add(args, flags) {
|
|
114
|
+
requireAuth();
|
|
115
|
+
const ip = args[0] || flags.ip || '';
|
|
116
|
+
const pathPattern = flags.route || flags.path || flags.pattern || '';
|
|
117
|
+
|
|
118
|
+
if (!ip && !pathPattern) {
|
|
119
|
+
ui.error('Provide an IP/CIDR, --route, or both.');
|
|
120
|
+
ui.info('Example: securenow ratelimit add 203.0.113.10 --limit 30 --window 1m --duration 24h');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (!flags.limit) {
|
|
124
|
+
ui.error('Pass --limit <count>.');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const appKey = resolveApp(flags);
|
|
129
|
+
const environment = resolveEnvironment(flags, 'production');
|
|
130
|
+
const body = {
|
|
131
|
+
ip,
|
|
132
|
+
pathPattern,
|
|
133
|
+
pathMatchMode: flags.mode || flags['path-mode'] || (pathPattern ? 'prefix' : 'prefix'),
|
|
134
|
+
method: flags.method || 'ALL',
|
|
135
|
+
keyBy: flags['key-by'] || flags.keyBy || 'ip',
|
|
136
|
+
limit: Number(flags.limit),
|
|
137
|
+
windowSeconds: parseWindow(flags.window || flags['window-seconds']),
|
|
138
|
+
reason: flags.reason || '',
|
|
139
|
+
name: flags.name || '',
|
|
140
|
+
duration: flags.duration || '',
|
|
141
|
+
expiresAt: flags.expiresAt || flags.expires || '',
|
|
142
|
+
};
|
|
143
|
+
if (appKey) body.appKey = appKey;
|
|
144
|
+
if (environment) body.environment = environment;
|
|
145
|
+
|
|
146
|
+
const s = ui.spinner('Creating rate limit');
|
|
147
|
+
try {
|
|
148
|
+
const data = await api.post('/rate-limits', body);
|
|
149
|
+
s.stop('Rate limit created');
|
|
150
|
+
if (flags.json) { ui.json(data); return; }
|
|
151
|
+
const r = data.rateLimit;
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(` ${ui.c.green('ACTIVE')} ${describeTarget(r)} at ${r.limit}/${r.windowSeconds}s`);
|
|
154
|
+
console.log(` ${ui.c.dim('ID:')} ${r.id || r._id}`);
|
|
155
|
+
console.log('');
|
|
156
|
+
} catch (err) {
|
|
157
|
+
s.fail('Failed to create rate limit');
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function setStatus(args, flags, status) {
|
|
163
|
+
requireAuth();
|
|
164
|
+
const id = args[0];
|
|
165
|
+
if (!id) {
|
|
166
|
+
ui.error(`Usage: securenow ratelimit ${status === 'active' ? 'enable' : 'disable'} <id>`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const s = ui.spinner(`${status === 'active' ? 'Enabling' : 'Disabling'} rate limit`);
|
|
170
|
+
try {
|
|
171
|
+
const data = await api.put(`/rate-limits/${id}`, { status });
|
|
172
|
+
s.stop(status === 'active' ? 'Rate limit enabled' : 'Rate limit disabled');
|
|
173
|
+
if (flags.json) ui.json(data);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
s.fail('Failed to update rate limit');
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function enable(args, flags) { return setStatus(args, flags, 'active'); }
|
|
181
|
+
async function disable(args, flags) { return setStatus(args, flags, 'disabled'); }
|
|
182
|
+
|
|
183
|
+
async function remove(args, flags) {
|
|
184
|
+
requireAuth();
|
|
185
|
+
const id = args[0];
|
|
186
|
+
if (!id) {
|
|
187
|
+
ui.error('Usage: securenow ratelimit remove <id> [--reason "..."]');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
if (!flags.force && !flags.yes) {
|
|
191
|
+
const ok = await ui.confirm('Remove this rate-limit rule?');
|
|
192
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
193
|
+
}
|
|
194
|
+
const s = ui.spinner('Removing rate limit');
|
|
195
|
+
try {
|
|
196
|
+
const opts = flags.reason ? { query: { reason: flags.reason } } : undefined;
|
|
197
|
+
const data = await api.delete(`/rate-limits/${id}`, opts);
|
|
198
|
+
s.stop('Rate limit removed');
|
|
199
|
+
if (flags.json) ui.json(data);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
s.fail('Failed to remove rate limit');
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function test(args, flags) {
|
|
207
|
+
requireAuth();
|
|
208
|
+
const ip = args[0] || flags.ip;
|
|
209
|
+
if (!ip) {
|
|
210
|
+
ui.error('Usage: securenow ratelimit test <ip> --path /api/login [--method POST]');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const query = {
|
|
214
|
+
ip,
|
|
215
|
+
path: flags.path || flags.route || '/',
|
|
216
|
+
method: flags.method || 'GET',
|
|
217
|
+
};
|
|
218
|
+
const appKey = resolveApp(flags);
|
|
219
|
+
if (appKey) query.appKey = appKey;
|
|
220
|
+
const environment = resolveEnvironment(flags, 'production');
|
|
221
|
+
if (environment) query.environment = environment;
|
|
222
|
+
|
|
223
|
+
const s = ui.spinner('Testing rate-limit match');
|
|
224
|
+
try {
|
|
225
|
+
const data = await api.get('/rate-limits/check', { query });
|
|
226
|
+
s.stop('Rate-limit check complete');
|
|
227
|
+
if (flags.json) { ui.json(data); return; }
|
|
228
|
+
console.log('');
|
|
229
|
+
if (data.limited) {
|
|
230
|
+
console.log(` ${ui.c.bold(ui.c.yellow('RATE LIMITED'))} - ${data.matches.length} matching rule${data.matches.length !== 1 ? 's' : ''}`);
|
|
231
|
+
for (const r of data.matches.slice(0, 5)) {
|
|
232
|
+
console.log(` ${ui.c.dim(r.id || r._id)} ${describeTarget(r)} ${r.limit}/${r.windowSeconds}s`);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
console.log(` ${ui.c.bold(ui.c.green('NOT LIMITED'))} - no matching rate-limit rules`);
|
|
236
|
+
}
|
|
237
|
+
console.log(` ${ui.c.dim(`App scope: ${appKey || 'all apps'} - Environment: ${data.environment || environment}`)}`);
|
|
238
|
+
console.log('');
|
|
239
|
+
} catch (err) {
|
|
240
|
+
s.fail('Failed to test rate limit');
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = { list, show, add, enable, disable, remove, test };
|
package/cli.js
CHANGED
|
@@ -253,6 +253,48 @@ const COMMANDS = {
|
|
|
253
253
|
},
|
|
254
254
|
defaultSub: 'status',
|
|
255
255
|
},
|
|
256
|
+
ratelimit: {
|
|
257
|
+
desc: 'Manage soft rate-limit remediation rules',
|
|
258
|
+
usage: 'securenow ratelimit <list|add|show|test|enable|disable|remove> [options]',
|
|
259
|
+
flags: {
|
|
260
|
+
app: 'Scope to app key (defaults to logged-in app)',
|
|
261
|
+
env: 'Scope to environment (default for create/test: production)',
|
|
262
|
+
environment: 'Alias for --env',
|
|
263
|
+
json: 'Output as JSON',
|
|
264
|
+
limit: 'Allowed requests per window',
|
|
265
|
+
window: 'Window size, e.g. 30s, 1m, 1h',
|
|
266
|
+
duration: 'Expiry, e.g. 24h or 7d',
|
|
267
|
+
route: 'Path pattern such as /api/login',
|
|
268
|
+
path: 'Alias for --route',
|
|
269
|
+
mode: 'Path mode: exact, prefix, or regex',
|
|
270
|
+
method: 'HTTP method, or ALL',
|
|
271
|
+
reason: 'Audit reason',
|
|
272
|
+
},
|
|
273
|
+
sub: {
|
|
274
|
+
list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
|
|
275
|
+
add: { desc: 'Create a rate-limit rule', usage: 'securenow ratelimit add [ip] --route /api/login --limit 5 --window 1m --duration 24h', run: (a, f) => require('./cli/rateLimits').add(a, f) },
|
|
276
|
+
show: { desc: 'Show one rate-limit rule', usage: 'securenow ratelimit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
|
|
277
|
+
test: { desc: 'Check whether a request would match rate-limit rules', usage: 'securenow ratelimit test <ip> --path /api/login --method POST', run: (a, f) => require('./cli/rateLimits').test(a, f) },
|
|
278
|
+
enable: { desc: 'Enable a rate-limit rule', usage: 'securenow ratelimit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
|
|
279
|
+
disable: { desc: 'Disable a rate-limit rule', usage: 'securenow ratelimit disable <id>', run: (a, f) => require('./cli/rateLimits').disable(a, f) },
|
|
280
|
+
remove: { desc: 'Remove a rate-limit rule', usage: 'securenow ratelimit remove <id> [--reason "..."]', run: (a, f) => require('./cli/rateLimits').remove(a, f) },
|
|
281
|
+
},
|
|
282
|
+
defaultSub: 'list',
|
|
283
|
+
},
|
|
284
|
+
'rate-limit': {
|
|
285
|
+
desc: 'Alias for ratelimit',
|
|
286
|
+
usage: 'securenow rate-limit <list|add|show|test|enable|disable|remove> [options]',
|
|
287
|
+
sub: {
|
|
288
|
+
list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
|
|
289
|
+
add: { desc: 'Create a rate-limit rule', usage: 'securenow rate-limit add [ip] --route /api/login --limit 5 --window 1m --duration 24h', run: (a, f) => require('./cli/rateLimits').add(a, f) },
|
|
290
|
+
show: { desc: 'Show one rate-limit rule', usage: 'securenow rate-limit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
|
|
291
|
+
test: { desc: 'Check whether a request would match rate-limit rules', usage: 'securenow rate-limit test <ip> --path /api/login --method POST', run: (a, f) => require('./cli/rateLimits').test(a, f) },
|
|
292
|
+
enable: { desc: 'Enable a rate-limit rule', usage: 'securenow rate-limit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
|
|
293
|
+
disable: { desc: 'Disable a rate-limit rule', usage: 'securenow rate-limit disable <id>', run: (a, f) => require('./cli/rateLimits').disable(a, f) },
|
|
294
|
+
remove: { desc: 'Remove a rate-limit rule', usage: 'securenow rate-limit remove <id> [--reason "..."]', run: (a, f) => require('./cli/rateLimits').remove(a, f) },
|
|
295
|
+
},
|
|
296
|
+
defaultSub: 'list',
|
|
297
|
+
},
|
|
256
298
|
automation: {
|
|
257
299
|
desc: 'Manage automation rules for blocklist actions',
|
|
258
300
|
usage: 'securenow automation <list|defaults|show|create|update|dry-run|execute|delete> [rule-id] [options]',
|
|
@@ -555,7 +597,7 @@ function showHelp(commandName) {
|
|
|
555
597
|
'Detect & Respond': ['human', 'notifications', 'alerts', 'fp'],
|
|
556
598
|
'Investigate': ['ip', 'forensics'],
|
|
557
599
|
'Firewall': ['firewall'],
|
|
558
|
-
'Remediation': ['automation', 'blocklist', 'allowlist', 'trusted'],
|
|
600
|
+
'Remediation': ['automation', 'ratelimit', 'blocklist', 'allowlist', 'trusted'],
|
|
559
601
|
'Telemetry': ['log', 'test-span'],
|
|
560
602
|
'Utilities': ['redact', 'cidr', 'doctor', 'env', 'mcp'],
|
|
561
603
|
'Settings': ['instances', 'config', 'version'],
|
package/firewall.js
CHANGED
|
@@ -30,11 +30,16 @@ let _remoteEnabled = true;
|
|
|
30
30
|
let _lastRemoteEnabled = null;
|
|
31
31
|
|
|
32
32
|
// Allowlist state
|
|
33
|
-
let _allowlistMatcher = null;
|
|
34
|
-
let _allowlistRawIps = [];
|
|
35
|
-
let _lastAllowlistModified = null;
|
|
36
|
-
let _lastAllowlistVersion = null;
|
|
37
|
-
let _lastAllowlistSyncEtag = null;
|
|
33
|
+
let _allowlistMatcher = null;
|
|
34
|
+
let _allowlistRawIps = [];
|
|
35
|
+
let _lastAllowlistModified = null;
|
|
36
|
+
let _lastAllowlistVersion = null;
|
|
37
|
+
let _lastAllowlistSyncEtag = null;
|
|
38
|
+
|
|
39
|
+
// Rate-limit policy state is synced in Phase 1 for forward compatibility.
|
|
40
|
+
// Enforcement is intentionally added in the next SDK phase.
|
|
41
|
+
let _rateLimitRules = [];
|
|
42
|
+
let _lastRateLimitVersion = null;
|
|
38
43
|
|
|
39
44
|
// Circuit breaker
|
|
40
45
|
const CIRCUIT_OPEN_THRESHOLD = 5;
|
|
@@ -295,6 +300,7 @@ function doUnifiedSync(callback) {
|
|
|
295
300
|
if (_options.environment) headers['X-SecureNow-Environment'] = _options.environment;
|
|
296
301
|
if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
|
|
297
302
|
if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
|
|
303
|
+
if (_lastRateLimitVersion) headers['X-Rate-Limit-Version'] = _lastRateLimitVersion;
|
|
298
304
|
if (_lastUnifiedEtag) headers['If-None-Match'] = _lastUnifiedEtag;
|
|
299
305
|
|
|
300
306
|
httpGet(url, headers, 8000, (err, res, data) => {
|
|
@@ -364,13 +370,24 @@ function doUnifiedSync(callback) {
|
|
|
364
370
|
}
|
|
365
371
|
}
|
|
366
372
|
|
|
367
|
-
if (body.allowlistIps) {
|
|
368
|
-
_allowlistRawIps = body.allowlistIps;
|
|
369
|
-
_allowlistMatcher = createMatcher(body.allowlistIps);
|
|
370
|
-
alChanged = true;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
373
|
+
if (body.allowlistIps) {
|
|
374
|
+
_allowlistRawIps = body.allowlistIps;
|
|
375
|
+
_allowlistMatcher = createMatcher(body.allowlistIps);
|
|
376
|
+
alChanged = true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (body.rateLimits) {
|
|
380
|
+
const newVer = body.rateLimits.version;
|
|
381
|
+
if (newVer !== _lastRateLimitVersion) {
|
|
382
|
+
_lastRateLimitVersion = newVer;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (Array.isArray(body.rateLimitRules)) {
|
|
387
|
+
_rateLimitRules = body.rateLimitRules;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
callback(null, { blChanged, alChanged, rlChanged: Array.isArray(body.rateLimitRules) });
|
|
374
391
|
} catch (e) {
|
|
375
392
|
callback(new Error(`Failed to parse sync response: ${e.message}`));
|
|
376
393
|
}
|
|
@@ -557,11 +574,14 @@ function pollOnce(callback) {
|
|
|
557
574
|
const s = _matcher.stats();
|
|
558
575
|
fwLog('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
559
576
|
}
|
|
560
|
-
if (result.alChanged && _options.log && _allowlistMatcher) {
|
|
561
|
-
const s = _allowlistMatcher.stats();
|
|
562
|
-
fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
563
|
-
}
|
|
564
|
-
|
|
577
|
+
if (result.alChanged && _options.log && _allowlistMatcher) {
|
|
578
|
+
const s = _allowlistMatcher.stats();
|
|
579
|
+
fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
580
|
+
}
|
|
581
|
+
if (result.rlChanged && _options.log) {
|
|
582
|
+
fwLog('[securenow] Firewall: re-synced %d rate-limit rules (enforcement pending SDK phase 2)', _rateLimitRules.length);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
565
585
|
callback(null);
|
|
566
586
|
};
|
|
567
587
|
|
|
@@ -639,12 +659,15 @@ function startSyncLoop() {
|
|
|
639
659
|
const s = _matcher.stats();
|
|
640
660
|
fwLog('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
641
661
|
}
|
|
642
|
-
if (_options.log && _allowlistMatcher) {
|
|
643
|
-
const s = _allowlistMatcher.stats();
|
|
644
|
-
if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
662
|
+
if (_options.log && _allowlistMatcher) {
|
|
663
|
+
const s = _allowlistMatcher.stats();
|
|
664
|
+
if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
|
|
665
|
+
}
|
|
666
|
+
if (_options.log && _rateLimitRules.length > 0) {
|
|
667
|
+
fwLog('[securenow] Firewall: synced %d rate-limit rules (enforcement pending SDK phase 2)', _rateLimitRules.length);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
648
671
|
|
|
649
672
|
initialSync();
|
|
650
673
|
scheduleNextPoll();
|
|
@@ -655,14 +678,17 @@ function startSyncLoop() {
|
|
|
655
678
|
// Force a full re-fetch by clearing versions so unified endpoint returns full data
|
|
656
679
|
const savedBlVer = _lastVersion;
|
|
657
680
|
const savedAlVer = _lastAllowlistVersion;
|
|
681
|
+
const savedRlVer = _lastRateLimitVersion;
|
|
658
682
|
const savedUnifiedEtag = _lastUnifiedEtag;
|
|
659
683
|
_lastVersion = null;
|
|
660
684
|
_lastAllowlistVersion = null;
|
|
685
|
+
_lastRateLimitVersion = null;
|
|
661
686
|
_lastUnifiedEtag = null;
|
|
662
687
|
pollOnce((err) => {
|
|
663
688
|
if (err) {
|
|
664
689
|
_lastVersion = savedBlVer;
|
|
665
690
|
_lastAllowlistVersion = savedAlVer;
|
|
691
|
+
_lastRateLimitVersion = savedRlVer;
|
|
666
692
|
_lastUnifiedEtag = savedUnifiedEtag;
|
|
667
693
|
}
|
|
668
694
|
});
|
|
@@ -912,9 +938,10 @@ function shutdown() {
|
|
|
912
938
|
function getStats() {
|
|
913
939
|
return {
|
|
914
940
|
..._stats,
|
|
915
|
-
matcher: _matcher ? _matcher.stats() : null,
|
|
916
|
-
allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
|
|
917
|
-
|
|
941
|
+
matcher: _matcher ? _matcher.stats() : null,
|
|
942
|
+
allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
|
|
943
|
+
rateLimitRules: _rateLimitRules.length,
|
|
944
|
+
initialized: _initialized,
|
|
918
945
|
circuitState: _circuitState,
|
|
919
946
|
consecutiveErrors: _consecutiveErrors,
|
|
920
947
|
unifiedSync: _useUnifiedSync,
|
|
@@ -927,8 +954,9 @@ function getStats() {
|
|
|
927
954
|
// Layers (TCP / iptables / cloud) read the matcher to populate kernel-level
|
|
928
955
|
// rules. When the remote toggle is off, return null so they treat the policy
|
|
929
956
|
// as "no IPs to block" without us mutating the cached matcher.
|
|
930
|
-
function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
|
|
931
|
-
function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
|
|
932
|
-
function
|
|
933
|
-
|
|
934
|
-
|
|
957
|
+
function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
|
|
958
|
+
function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
|
|
959
|
+
function getRateLimitRules() { return _remoteEnabled === false ? [] : _rateLimitRules.slice(); }
|
|
960
|
+
function isRemoteEnabled() { return _remoteEnabled !== false; }
|
|
961
|
+
|
|
962
|
+
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getRateLimitRules, isRemoteEnabled };
|
package/package.json
CHANGED