securenow 8.11.2 → 8.11.3
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/SKILL-CLI.md +623 -632
- package/TELEMETRY-SQL.md +6 -1
- package/cidr.js +83 -83
- package/cli/client.js +102 -102
- package/cli/diagnostics.js +4 -0
- package/cli/firewall.js +141 -141
- package/cli/fp.js +638 -638
- package/cli/monitor.js +398 -398
- package/cli/run.js +132 -132
- package/cli/security.js +1866 -1856
- package/cli/ui.js +374 -374
- package/cli.js +923 -922
- package/firewall-cloud.js +202 -202
- package/firewall-iptables.js +139 -139
- package/firewall-only.js +46 -46
- package/firewall-tcp.js +74 -74
- package/firewall.js +1470 -1470
- package/mcp/catalog.js +2358 -2346
- package/nextjs-auto-capture.d.ts +29 -29
- package/nextjs-auto-capture.js +273 -273
- package/nextjs-middleware.d.ts +57 -57
- package/nextjs-middleware.js +267 -267
- package/nextjs-webpack-config.js +79 -79
- package/nextjs-wrapper.d.ts +95 -95
- package/nextjs-wrapper.js +233 -233
- package/nuxt.mjs +74 -74
- package/package.json +1 -1
- package/register.d.ts +42 -42
- package/register.js +44 -44
- package/resolve-ip.js +13 -13
- package/telemetry-schema.json +3 -1
- package/tracing.js +853 -853
package/cli/security.js
CHANGED
|
@@ -1,1856 +1,1866 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const { api, requireAuth } = require('./client');
|
|
5
|
-
const config = require('./config');
|
|
6
|
-
const ui = require('./ui');
|
|
7
|
-
|
|
8
|
-
function resolveApp(flags) {
|
|
9
|
-
return flags.app || config.getDefaultApp();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function resolveEnvironment(flags, fallback = null) {
|
|
13
|
-
return flags.env || flags.environment || fallback;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ── Alert Rules ──
|
|
17
|
-
|
|
18
|
-
function formatRuleApplicationsCell(rule) {
|
|
19
|
-
if (rule.applicationsAll) {
|
|
20
|
-
return ui.c.cyan('all apps');
|
|
21
|
-
}
|
|
22
|
-
const keys = rule.applications || [];
|
|
23
|
-
if (keys.length === 0) return ui.c.dim('—');
|
|
24
|
-
const joined = keys.join(', ');
|
|
25
|
-
return joined.length > 48 ? `${joined.slice(0, 45)}…` : joined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function ruleStatusBadge(rule) {
|
|
29
|
-
const st = rule.status || (rule.enabled !== false ? 'Active' : 'Disabled');
|
|
30
|
-
if (st === 'Active') return ui.statusBadge('active');
|
|
31
|
-
if (st === 'Disabled') return ui.statusBadge('disabled');
|
|
32
|
-
if (st === 'Paused') return ui.statusBadge('paused');
|
|
33
|
-
return st;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Dispatch: list | show <id> | update <id> ... */
|
|
37
|
-
async function alertRulesRoute(args, flags) {
|
|
38
|
-
const sub = args[0];
|
|
39
|
-
if (sub === 'create' || sub === 'add') {
|
|
40
|
-
return alertRuleCreate(args.slice(1), flags);
|
|
41
|
-
}
|
|
42
|
-
if (sub === 'show') {
|
|
43
|
-
return alertRuleShow(args.slice(1), flags);
|
|
44
|
-
}
|
|
45
|
-
if (sub === 'update') {
|
|
46
|
-
return alertRuleUpdate(args.slice(1), flags);
|
|
47
|
-
}
|
|
48
|
-
if (sub === 'set-sql' || sub === 'edit-sql') {
|
|
49
|
-
return alertRuleSetSql(args.slice(1), flags);
|
|
50
|
-
}
|
|
51
|
-
if (sub === 'versions' || sub === 'history') {
|
|
52
|
-
return alertRuleVersions(args.slice(1), flags);
|
|
53
|
-
}
|
|
54
|
-
if (sub === 'version') {
|
|
55
|
-
return alertRuleVersionShow(args.slice(1), flags);
|
|
56
|
-
}
|
|
57
|
-
if (sub === 'rollback' || sub === 'revert') {
|
|
58
|
-
return alertRuleRollback(args.slice(1), flags);
|
|
59
|
-
}
|
|
60
|
-
if (sub === 'delete' || sub === 'rm' || sub === 'remove') {
|
|
61
|
-
return alertRuleDelete(args.slice(1), flags);
|
|
62
|
-
}
|
|
63
|
-
if (sub === 'duplicate' || sub === 'clone') {
|
|
64
|
-
return alertRuleDuplicate(args.slice(1), flags);
|
|
65
|
-
}
|
|
66
|
-
if (sub === 'test') {
|
|
67
|
-
return alertRuleTest(args.slice(1), flags);
|
|
68
|
-
}
|
|
69
|
-
if (sub === 'dry-run-query' || sub === 'candidate-test' || sub === 'validate') {
|
|
70
|
-
return alertRuleCandidateTest(args.slice(1), flags);
|
|
71
|
-
}
|
|
72
|
-
if (sub === 'tune-query' || sub === 'query-update') {
|
|
73
|
-
return alertRuleQueryUpdate(args.slice(1), flags);
|
|
74
|
-
}
|
|
75
|
-
if (sub === 'exclusions') {
|
|
76
|
-
return alertRuleExclusions(args.slice(1), flags);
|
|
77
|
-
}
|
|
78
|
-
if (sub === 'promote') {
|
|
79
|
-
return alertRuleSetMode(args.slice(1), flags, 'prod');
|
|
80
|
-
}
|
|
81
|
-
if (sub === 'demote') {
|
|
82
|
-
return alertRuleSetMode(args.slice(1), flags, 'test');
|
|
83
|
-
}
|
|
84
|
-
if (sub === 'list') {
|
|
85
|
-
return alertRulesList(args.slice(1), flags);
|
|
86
|
-
}
|
|
87
|
-
return alertRulesList(args, flags);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Promote (test -> prod) or demote (prod -> test) one or more rules. In test mode a
|
|
91
|
-
// rule detects only — instant.block, automations, and the autonomous operator all
|
|
92
|
-
// skip its notifications. Promoting to prod makes those mitigations eligible.
|
|
93
|
-
async function alertRuleSetMode(args, flags, mode) {
|
|
94
|
-
requireAuth();
|
|
95
|
-
const ids = args.filter(Boolean);
|
|
96
|
-
const verb = mode === 'prod' ? 'promote' : 'demote';
|
|
97
|
-
if (ids.length === 0) {
|
|
98
|
-
ui.error(`Usage: securenow alerts rules ${verb} <rule-id> [<rule-id> ...] [--yes]`);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
if (mode === 'prod' && !flags.yes && !flags.force) {
|
|
102
|
-
ui.warn('Promoting to prod ARMS automatic mitigation (instant.block, automations, autonomous operator) for matching detections.');
|
|
103
|
-
const ok = await ui.confirm(`Promote ${ids.length} rule${ids.length !== 1 ? 's' : ''} to prod (mitigations become eligible)?`);
|
|
104
|
-
if (!ok) { ui.info('Cancelled'); return; }
|
|
105
|
-
}
|
|
106
|
-
let okCount = 0;
|
|
107
|
-
const failures = [];
|
|
108
|
-
for (const id of ids) {
|
|
109
|
-
const s = ui.spinner(`${mode === 'prod' ? 'Promoting' : 'Demoting'} ${id}`);
|
|
110
|
-
try {
|
|
111
|
-
await api.put(`/alert-rules/${id}`, { mode });
|
|
112
|
-
s.stop(`${ui.truncate(id, 12)} → ${mode}`);
|
|
113
|
-
okCount++;
|
|
114
|
-
} catch (err) {
|
|
115
|
-
s.fail(`Failed on ${ui.truncate(id, 12)}: ${err.message}`);
|
|
116
|
-
failures.push(id);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (flags.json) { ui.json({ mode, updated: okCount, failed: failures }); return; }
|
|
120
|
-
console.log('');
|
|
121
|
-
ui.success(`${okCount}/${ids.length} rule${ids.length !== 1 ? 's' : ''} set to ${mode}.`);
|
|
122
|
-
if (mode === 'prod') ui.info('Enable the matching automations to let mitigations act. Reverse anytime: securenow alerts rules demote <id>');
|
|
123
|
-
else ui.info('Detect-only — no mitigation will act. Promote when confident: securenow alerts rules promote <id>');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Duplicate any rule (including a read-only system template) into a new CUSTOM rule you
|
|
127
|
-
// own. System rules are view-only examples — fork one with this to enable / edit it. The
|
|
128
|
-
// copy defaults to Disabled (opt in with --enable / --status active); SQL, instant.block,
|
|
129
|
-
// schedule, scope, and mode are copied from the source.
|
|
130
|
-
async function alertRuleDuplicate(args, flags) {
|
|
131
|
-
requireAuth();
|
|
132
|
-
const id = args[0];
|
|
133
|
-
if (!id) {
|
|
134
|
-
ui.error('Usage: securenow alerts rules duplicate <rule-id> [--enable] [--status active|disabled|paused] [--name "..."] [--mode test|prod]');
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
const body = {};
|
|
138
|
-
if (flags.enable) body.status = 'Active';
|
|
139
|
-
else if (flags.disable) body.status = 'Disabled';
|
|
140
|
-
else if (flags.pause) body.status = 'Paused';
|
|
141
|
-
else if (flags.status) {
|
|
142
|
-
const s = String(flags.status).toLowerCase();
|
|
143
|
-
body.status = s === 'active' ? 'Active' : s === 'paused' ? 'Paused' : 'Disabled';
|
|
144
|
-
}
|
|
145
|
-
if (flags.name) body.name = String(flags.name);
|
|
146
|
-
if (flags.mode === 'test' || flags.mode === 'prod') body.mode = flags.mode;
|
|
147
|
-
|
|
148
|
-
const spin = ui.spinner(`Duplicating ${ui.truncate(id, 12)}`);
|
|
149
|
-
let data;
|
|
150
|
-
try {
|
|
151
|
-
data = await api.post(`/alert-rules/${id}/duplicate`, body);
|
|
152
|
-
spin.stop('Duplicated');
|
|
153
|
-
} catch (err) {
|
|
154
|
-
spin.fail(`Failed to duplicate: ${err.message}`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (flags.json) { ui.json(data); return; }
|
|
159
|
-
const r = data.alertRule || {};
|
|
160
|
-
const newId = r.id || r._id || '';
|
|
161
|
-
console.log('');
|
|
162
|
-
ui.success(`Created custom rule ${newId}`);
|
|
163
|
-
console.log(` Name ${r.name || ''}`);
|
|
164
|
-
console.log(` Status ${r.status || ''}`);
|
|
165
|
-
console.log(` Mode ${r.mode || ''}`);
|
|
166
|
-
console.log(` Exec mode ${r.executionMode || ''}`);
|
|
167
|
-
console.log(` Custom ${r.isSystem ? 'NO — still a system rule (unexpected)' : 'yes (editable / enable-able)'}`);
|
|
168
|
-
if (data.duplicatedFromSystem) ui.info('Forked from a read-only system template — edit or enable this custom copy freely.');
|
|
169
|
-
if (r.status !== 'Active') ui.info(`Disabled by default. Enable with: securenow alerts rules update ${newId} --enable`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function alertRulesList(args, flags) {
|
|
173
|
-
requireAuth();
|
|
174
|
-
// Server-side filters (API supports ?mode/status/active/isSystem).
|
|
175
|
-
const query = {};
|
|
176
|
-
if (flags.mode === 'test' || flags.mode === 'prod') query.mode = flags.mode;
|
|
177
|
-
if (['Active', 'Disabled', 'Paused'].includes(flags.status)) query.status = flags.status;
|
|
178
|
-
if (flags.active === true || flags.active === 'true') query.active = 'true';
|
|
179
|
-
else if (flags.active === false || flags.active === 'false') query.active = 'false';
|
|
180
|
-
if (flags.system === true || flags.system === 'true') query.isSystem = 'true';
|
|
181
|
-
else if (flags.user === true || flags.user === 'true') query.isSystem = 'false';
|
|
182
|
-
|
|
183
|
-
const s = ui.spinner('Fetching alert rules');
|
|
184
|
-
try {
|
|
185
|
-
const data = await api.get('/alert-rules', Object.keys(query).length ? { query } : undefined);
|
|
186
|
-
const rules = data.alertRules || [];
|
|
187
|
-
s.stop(`Found ${rules.length} rule${rules.length !== 1 ? 's' : ''}`);
|
|
188
|
-
|
|
189
|
-
if (flags.json) { ui.json(rules); return; }
|
|
190
|
-
|
|
191
|
-
console.log('');
|
|
192
|
-
const rows = rules.map((r) => [
|
|
193
|
-
ui.c.dim(ui.truncate(r._id || r.id, 12)),
|
|
194
|
-
r.name || '—',
|
|
195
|
-
ruleStatusBadge(r),
|
|
196
|
-
r.mode === 'test' ? ui.c.yellow('test') : ui.c.dim('prod'),
|
|
197
|
-
formatRuleApplicationsCell(r),
|
|
198
|
-
r.schedule?.enabled === false ? ui.c.dim('off') : (r.schedule?.description || r.schedule?.cronExpression || '—'),
|
|
199
|
-
]);
|
|
200
|
-
ui.table(['ID', 'Name', 'Status', 'Mode', 'Applications', 'Schedule'], rows);
|
|
201
|
-
console.log('');
|
|
202
|
-
console.log(ui.c.dim(' Filters: --mode test|prod --status Active|Disabled|Paused --active true|false --system|--user'));
|
|
203
|
-
console.log(ui.c.dim(' Mode: test = detect-only (no mitigation, human review). Promote: securenow alerts rules promote <id>'));
|
|
204
|
-
console.log('');
|
|
205
|
-
} catch (err) {
|
|
206
|
-
s.fail('Failed to fetch alert rules');
|
|
207
|
-
throw err;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function alertRuleCreate(args, flags) {
|
|
212
|
-
requireAuth();
|
|
213
|
-
const name = flags.name;
|
|
214
|
-
const usage = 'Usage: securenow alerts rules create --name "..." --sql <sql|@file|-> (--applications-all | --apps k1,k2) '
|
|
215
|
-
+ '[--severity critical|high|medium|low] [--schedule "*/15 * * * *"] [--throttle-minutes 15 | --no-throttle] '
|
|
216
|
-
+ '[--description "..."] [--nlp "plain-English intent"] [--category custom] [--channel id1,id2] [--query-mapping-id <id>]';
|
|
217
|
-
if (!name) {
|
|
218
|
-
ui.error(usage);
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const queryMappingId = flags['query-mapping-id'] || flags.queryMappingId;
|
|
223
|
-
const sqlQuery = readSqlArg(flags);
|
|
224
|
-
if (!queryMappingId && !sqlQuery) {
|
|
225
|
-
ui.error('Provide the detection query: --sql <sql|@file|-> (or reuse one with --query-mapping-id <id>).');
|
|
226
|
-
console.log(ui.c.dim(' Scope app keys in SQL with the __USER_APP_KEYS__ placeholder
|
|
227
|
-
console.log(ui.c.dim("
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
if (flags.
|
|
245
|
-
if (flags
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
body.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
ui.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
ui.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
['
|
|
307
|
-
['
|
|
308
|
-
['
|
|
309
|
-
['
|
|
310
|
-
['
|
|
311
|
-
['
|
|
312
|
-
['
|
|
313
|
-
['
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
console.log('');
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
['
|
|
351
|
-
['
|
|
352
|
-
['
|
|
353
|
-
['
|
|
354
|
-
['
|
|
355
|
-
['
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
flags['
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
body.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
else if (flags.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
ui.
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
s.
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
ui.info(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
s.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
console.log(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (v.
|
|
541
|
-
console.log(
|
|
542
|
-
console.log(`
|
|
543
|
-
console.log(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
s.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (
|
|
642
|
-
return
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
ui.
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
console.log('');
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (flags.
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const
|
|
962
|
-
if (
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
]
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
console.log('');
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
console.log('');
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
s.
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
ui.
|
|
1452
|
-
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
if (msg
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
if (!
|
|
1641
|
-
ui.
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
if (
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
if (data.
|
|
1668
|
-
if (data.
|
|
1669
|
-
|
|
1670
|
-
if (pairs.
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
if (data.
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
if (
|
|
1681
|
-
ui.
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
console.log('');
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
console.log('');
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { api, requireAuth } = require('./client');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const ui = require('./ui');
|
|
7
|
+
|
|
8
|
+
function resolveApp(flags) {
|
|
9
|
+
return flags.app || config.getDefaultApp();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveEnvironment(flags, fallback = null) {
|
|
13
|
+
return flags.env || flags.environment || fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── Alert Rules ──
|
|
17
|
+
|
|
18
|
+
function formatRuleApplicationsCell(rule) {
|
|
19
|
+
if (rule.applicationsAll) {
|
|
20
|
+
return ui.c.cyan('all apps');
|
|
21
|
+
}
|
|
22
|
+
const keys = rule.applications || [];
|
|
23
|
+
if (keys.length === 0) return ui.c.dim('—');
|
|
24
|
+
const joined = keys.join(', ');
|
|
25
|
+
return joined.length > 48 ? `${joined.slice(0, 45)}…` : joined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ruleStatusBadge(rule) {
|
|
29
|
+
const st = rule.status || (rule.enabled !== false ? 'Active' : 'Disabled');
|
|
30
|
+
if (st === 'Active') return ui.statusBadge('active');
|
|
31
|
+
if (st === 'Disabled') return ui.statusBadge('disabled');
|
|
32
|
+
if (st === 'Paused') return ui.statusBadge('paused');
|
|
33
|
+
return st;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Dispatch: list | show <id> | update <id> ... */
|
|
37
|
+
async function alertRulesRoute(args, flags) {
|
|
38
|
+
const sub = args[0];
|
|
39
|
+
if (sub === 'create' || sub === 'add') {
|
|
40
|
+
return alertRuleCreate(args.slice(1), flags);
|
|
41
|
+
}
|
|
42
|
+
if (sub === 'show') {
|
|
43
|
+
return alertRuleShow(args.slice(1), flags);
|
|
44
|
+
}
|
|
45
|
+
if (sub === 'update') {
|
|
46
|
+
return alertRuleUpdate(args.slice(1), flags);
|
|
47
|
+
}
|
|
48
|
+
if (sub === 'set-sql' || sub === 'edit-sql') {
|
|
49
|
+
return alertRuleSetSql(args.slice(1), flags);
|
|
50
|
+
}
|
|
51
|
+
if (sub === 'versions' || sub === 'history') {
|
|
52
|
+
return alertRuleVersions(args.slice(1), flags);
|
|
53
|
+
}
|
|
54
|
+
if (sub === 'version') {
|
|
55
|
+
return alertRuleVersionShow(args.slice(1), flags);
|
|
56
|
+
}
|
|
57
|
+
if (sub === 'rollback' || sub === 'revert') {
|
|
58
|
+
return alertRuleRollback(args.slice(1), flags);
|
|
59
|
+
}
|
|
60
|
+
if (sub === 'delete' || sub === 'rm' || sub === 'remove') {
|
|
61
|
+
return alertRuleDelete(args.slice(1), flags);
|
|
62
|
+
}
|
|
63
|
+
if (sub === 'duplicate' || sub === 'clone') {
|
|
64
|
+
return alertRuleDuplicate(args.slice(1), flags);
|
|
65
|
+
}
|
|
66
|
+
if (sub === 'test') {
|
|
67
|
+
return alertRuleTest(args.slice(1), flags);
|
|
68
|
+
}
|
|
69
|
+
if (sub === 'dry-run-query' || sub === 'candidate-test' || sub === 'validate') {
|
|
70
|
+
return alertRuleCandidateTest(args.slice(1), flags);
|
|
71
|
+
}
|
|
72
|
+
if (sub === 'tune-query' || sub === 'query-update') {
|
|
73
|
+
return alertRuleQueryUpdate(args.slice(1), flags);
|
|
74
|
+
}
|
|
75
|
+
if (sub === 'exclusions') {
|
|
76
|
+
return alertRuleExclusions(args.slice(1), flags);
|
|
77
|
+
}
|
|
78
|
+
if (sub === 'promote') {
|
|
79
|
+
return alertRuleSetMode(args.slice(1), flags, 'prod');
|
|
80
|
+
}
|
|
81
|
+
if (sub === 'demote') {
|
|
82
|
+
return alertRuleSetMode(args.slice(1), flags, 'test');
|
|
83
|
+
}
|
|
84
|
+
if (sub === 'list') {
|
|
85
|
+
return alertRulesList(args.slice(1), flags);
|
|
86
|
+
}
|
|
87
|
+
return alertRulesList(args, flags);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Promote (test -> prod) or demote (prod -> test) one or more rules. In test mode a
|
|
91
|
+
// rule detects only — instant.block, automations, and the autonomous operator all
|
|
92
|
+
// skip its notifications. Promoting to prod makes those mitigations eligible.
|
|
93
|
+
async function alertRuleSetMode(args, flags, mode) {
|
|
94
|
+
requireAuth();
|
|
95
|
+
const ids = args.filter(Boolean);
|
|
96
|
+
const verb = mode === 'prod' ? 'promote' : 'demote';
|
|
97
|
+
if (ids.length === 0) {
|
|
98
|
+
ui.error(`Usage: securenow alerts rules ${verb} <rule-id> [<rule-id> ...] [--yes]`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (mode === 'prod' && !flags.yes && !flags.force) {
|
|
102
|
+
ui.warn('Promoting to prod ARMS automatic mitigation (instant.block, automations, autonomous operator) for matching detections.');
|
|
103
|
+
const ok = await ui.confirm(`Promote ${ids.length} rule${ids.length !== 1 ? 's' : ''} to prod (mitigations become eligible)?`);
|
|
104
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
105
|
+
}
|
|
106
|
+
let okCount = 0;
|
|
107
|
+
const failures = [];
|
|
108
|
+
for (const id of ids) {
|
|
109
|
+
const s = ui.spinner(`${mode === 'prod' ? 'Promoting' : 'Demoting'} ${id}`);
|
|
110
|
+
try {
|
|
111
|
+
await api.put(`/alert-rules/${id}`, { mode });
|
|
112
|
+
s.stop(`${ui.truncate(id, 12)} → ${mode}`);
|
|
113
|
+
okCount++;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
s.fail(`Failed on ${ui.truncate(id, 12)}: ${err.message}`);
|
|
116
|
+
failures.push(id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (flags.json) { ui.json({ mode, updated: okCount, failed: failures }); return; }
|
|
120
|
+
console.log('');
|
|
121
|
+
ui.success(`${okCount}/${ids.length} rule${ids.length !== 1 ? 's' : ''} set to ${mode}.`);
|
|
122
|
+
if (mode === 'prod') ui.info('Enable the matching automations to let mitigations act. Reverse anytime: securenow alerts rules demote <id>');
|
|
123
|
+
else ui.info('Detect-only — no mitigation will act. Promote when confident: securenow alerts rules promote <id>');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Duplicate any rule (including a read-only system template) into a new CUSTOM rule you
|
|
127
|
+
// own. System rules are view-only examples — fork one with this to enable / edit it. The
|
|
128
|
+
// copy defaults to Disabled (opt in with --enable / --status active); SQL, instant.block,
|
|
129
|
+
// schedule, scope, and mode are copied from the source.
|
|
130
|
+
async function alertRuleDuplicate(args, flags) {
|
|
131
|
+
requireAuth();
|
|
132
|
+
const id = args[0];
|
|
133
|
+
if (!id) {
|
|
134
|
+
ui.error('Usage: securenow alerts rules duplicate <rule-id> [--enable] [--status active|disabled|paused] [--name "..."] [--mode test|prod]');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const body = {};
|
|
138
|
+
if (flags.enable) body.status = 'Active';
|
|
139
|
+
else if (flags.disable) body.status = 'Disabled';
|
|
140
|
+
else if (flags.pause) body.status = 'Paused';
|
|
141
|
+
else if (flags.status) {
|
|
142
|
+
const s = String(flags.status).toLowerCase();
|
|
143
|
+
body.status = s === 'active' ? 'Active' : s === 'paused' ? 'Paused' : 'Disabled';
|
|
144
|
+
}
|
|
145
|
+
if (flags.name) body.name = String(flags.name);
|
|
146
|
+
if (flags.mode === 'test' || flags.mode === 'prod') body.mode = flags.mode;
|
|
147
|
+
|
|
148
|
+
const spin = ui.spinner(`Duplicating ${ui.truncate(id, 12)}`);
|
|
149
|
+
let data;
|
|
150
|
+
try {
|
|
151
|
+
data = await api.post(`/alert-rules/${id}/duplicate`, body);
|
|
152
|
+
spin.stop('Duplicated');
|
|
153
|
+
} catch (err) {
|
|
154
|
+
spin.fail(`Failed to duplicate: ${err.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (flags.json) { ui.json(data); return; }
|
|
159
|
+
const r = data.alertRule || {};
|
|
160
|
+
const newId = r.id || r._id || '';
|
|
161
|
+
console.log('');
|
|
162
|
+
ui.success(`Created custom rule ${newId}`);
|
|
163
|
+
console.log(` Name ${r.name || ''}`);
|
|
164
|
+
console.log(` Status ${r.status || ''}`);
|
|
165
|
+
console.log(` Mode ${r.mode || ''}`);
|
|
166
|
+
console.log(` Exec mode ${r.executionMode || ''}`);
|
|
167
|
+
console.log(` Custom ${r.isSystem ? 'NO — still a system rule (unexpected)' : 'yes (editable / enable-able)'}`);
|
|
168
|
+
if (data.duplicatedFromSystem) ui.info('Forked from a read-only system template — edit or enable this custom copy freely.');
|
|
169
|
+
if (r.status !== 'Active') ui.info(`Disabled by default. Enable with: securenow alerts rules update ${newId} --enable`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function alertRulesList(args, flags) {
|
|
173
|
+
requireAuth();
|
|
174
|
+
// Server-side filters (API supports ?mode/status/active/isSystem).
|
|
175
|
+
const query = {};
|
|
176
|
+
if (flags.mode === 'test' || flags.mode === 'prod') query.mode = flags.mode;
|
|
177
|
+
if (['Active', 'Disabled', 'Paused'].includes(flags.status)) query.status = flags.status;
|
|
178
|
+
if (flags.active === true || flags.active === 'true') query.active = 'true';
|
|
179
|
+
else if (flags.active === false || flags.active === 'false') query.active = 'false';
|
|
180
|
+
if (flags.system === true || flags.system === 'true') query.isSystem = 'true';
|
|
181
|
+
else if (flags.user === true || flags.user === 'true') query.isSystem = 'false';
|
|
182
|
+
|
|
183
|
+
const s = ui.spinner('Fetching alert rules');
|
|
184
|
+
try {
|
|
185
|
+
const data = await api.get('/alert-rules', Object.keys(query).length ? { query } : undefined);
|
|
186
|
+
const rules = data.alertRules || [];
|
|
187
|
+
s.stop(`Found ${rules.length} rule${rules.length !== 1 ? 's' : ''}`);
|
|
188
|
+
|
|
189
|
+
if (flags.json) { ui.json(rules); return; }
|
|
190
|
+
|
|
191
|
+
console.log('');
|
|
192
|
+
const rows = rules.map((r) => [
|
|
193
|
+
ui.c.dim(ui.truncate(r._id || r.id, 12)),
|
|
194
|
+
r.name || '—',
|
|
195
|
+
ruleStatusBadge(r),
|
|
196
|
+
r.mode === 'test' ? ui.c.yellow('test') : ui.c.dim('prod'),
|
|
197
|
+
formatRuleApplicationsCell(r),
|
|
198
|
+
r.schedule?.enabled === false ? ui.c.dim('off') : (r.schedule?.description || r.schedule?.cronExpression || '—'),
|
|
199
|
+
]);
|
|
200
|
+
ui.table(['ID', 'Name', 'Status', 'Mode', 'Applications', 'Schedule'], rows);
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(ui.c.dim(' Filters: --mode test|prod --status Active|Disabled|Paused --active true|false --system|--user'));
|
|
203
|
+
console.log(ui.c.dim(' Mode: test = detect-only (no mitigation, human review). Promote: securenow alerts rules promote <id>'));
|
|
204
|
+
console.log('');
|
|
205
|
+
} catch (err) {
|
|
206
|
+
s.fail('Failed to fetch alert rules');
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function alertRuleCreate(args, flags) {
|
|
212
|
+
requireAuth();
|
|
213
|
+
const name = flags.name;
|
|
214
|
+
const usage = 'Usage: securenow alerts rules create --name "..." --sql <sql|@file|-> (--applications-all | --apps k1,k2) '
|
|
215
|
+
+ '[--severity critical|high|medium|low] [--schedule "*/15 * * * *"] [--throttle-minutes 15 | --no-throttle] '
|
|
216
|
+
+ '[--description "..."] [--nlp "plain-English intent"] [--category custom] [--channel id1,id2] [--query-mapping-id <id>]';
|
|
217
|
+
if (!name) {
|
|
218
|
+
ui.error(usage);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const queryMappingId = flags['query-mapping-id'] || flags.queryMappingId;
|
|
223
|
+
const sqlQuery = readSqlArg(flags);
|
|
224
|
+
if (!queryMappingId && !sqlQuery) {
|
|
225
|
+
ui.error('Provide the detection query: --sql <sql|@file|-> (or reuse one with --query-mapping-id <id>).');
|
|
226
|
+
console.log(ui.c.dim(' Scope app keys in SQL with the __USER_APP_KEYS__ placeholder. The column differs by table:'));
|
|
227
|
+
console.log(ui.c.dim(" traces: WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__) AND timestamp >= now64(9) - INTERVAL 15 MINUTE"));
|
|
228
|
+
console.log(ui.c.dim(" logs: WHERE resources_string['service.name'] IN (__USER_APP_KEYS__) AND timestamp >= toUnixTimestamp64Nano(now64() - INTERVAL 15 MINUTE)"));
|
|
229
|
+
console.log(ui.c.dim(' See `securenow telemetry schema` for both tables.'));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const hasAll = flags['applications-all'] === true || flags['applications-all'] === 'true';
|
|
234
|
+
const appsStr = flags.apps || flags.app;
|
|
235
|
+
if (!hasAll && !appsStr) {
|
|
236
|
+
ui.error('Scope the rule with --applications-all or --apps key1,key2');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const body = { name };
|
|
241
|
+
if (flags.description) body.description = flags.description;
|
|
242
|
+
if (queryMappingId) body.queryMappingId = queryMappingId;
|
|
243
|
+
else body.sqlQuery = sqlQuery;
|
|
244
|
+
if (flags.nlp || flags.text) body.nlpQuery = flags.nlp || flags.text;
|
|
245
|
+
if (flags.category) body.category = flags.category;
|
|
246
|
+
if (flags.severity) body.severity = String(flags.severity).toLowerCase();
|
|
247
|
+
if (flags['execution-mode']) body.executionMode = flags['execution-mode'];
|
|
248
|
+
|
|
249
|
+
if (hasAll) {
|
|
250
|
+
body.applicationsAll = true;
|
|
251
|
+
body.applications = [];
|
|
252
|
+
} else {
|
|
253
|
+
body.applicationsAll = false;
|
|
254
|
+
body.applications = String(appsStr).split(',').map((x) => x.trim()).filter(Boolean);
|
|
255
|
+
if (body.applications.length === 0) {
|
|
256
|
+
ui.error('No application keys parsed from --apps');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const cron = flags.schedule || flags.cron;
|
|
262
|
+
if (cron) {
|
|
263
|
+
body.schedule = {
|
|
264
|
+
enabled: true,
|
|
265
|
+
cronExpression: cron,
|
|
266
|
+
description: flags['schedule-description'] || cron,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (flags['no-throttle']) body.throttle = { enabled: false, minutes: 0 };
|
|
270
|
+
else if (flags['throttle-minutes']) body.throttle = { enabled: true, minutes: Number(flags['throttle-minutes']) };
|
|
271
|
+
|
|
272
|
+
const channelStr = flags.channel || flags.channels;
|
|
273
|
+
if (channelStr) {
|
|
274
|
+
body.alertChannelIds = String(channelStr).split(',').map((x) => x.trim()).filter(Boolean);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const s = ui.spinner('Creating alert rule');
|
|
278
|
+
let createdId = null;
|
|
279
|
+
try {
|
|
280
|
+
const data = await api.post('/alert-rules', body);
|
|
281
|
+
const r = data.alertRule || data;
|
|
282
|
+
createdId = r._id || r.id || null;
|
|
283
|
+
s.stop('Alert rule created');
|
|
284
|
+
|
|
285
|
+
// Transactional safety: `create` only runs scope/safety validation, not a real
|
|
286
|
+
// query execution — so a query that is valid-but-wrong (e.g. an unknown column)
|
|
287
|
+
// is accepted and would silently never fire. Custom-rule SQL also can't be
|
|
288
|
+
// patched via the API, so a broken rule would be stuck. We therefore dry-run the
|
|
289
|
+
// new scheduled rule and roll it back (delete) on a hard query failure, unless
|
|
290
|
+
// --no-validate is passed. A "complete" dry-run with 0 rows is healthy, not a failure.
|
|
291
|
+
const scheduled = r.executionMode !== 'instant' && r.schedule?.enabled !== false;
|
|
292
|
+
if (createdId && scheduled && !flags['no-validate']) {
|
|
293
|
+
const v = await validateRuleById(createdId, flags);
|
|
294
|
+
if (v && v.status === 'failed') {
|
|
295
|
+
const rolledBack = await api.delete(`/alert-rules/${createdId}`).then(() => true).catch(() => false);
|
|
296
|
+
ui.error(`Rule query failed validation and was ${rolledBack ? 'rolled back (deleted)' : 'left in place (auto-delete failed — remove it manually)'}:`);
|
|
297
|
+
ui.error(` ${v.error || 'unknown query error'}`);
|
|
298
|
+
ui.info('Fix the SQL and re-create, or validate first: securenow alerts rules validate --sql @rule.sql --app <key>');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (flags.json) { ui.json(data); return; }
|
|
304
|
+
console.log('');
|
|
305
|
+
ui.keyValue([
|
|
306
|
+
['ID', createdId || '-'],
|
|
307
|
+
['Name', r.name || '-'],
|
|
308
|
+
['Status', r.status || '-'],
|
|
309
|
+
['Mode', r.executionMode || 'scheduled'],
|
|
310
|
+
['Severity', r.severity || '-'],
|
|
311
|
+
['Applications', r.applicationsAll ? 'all apps' : ((r.applications || []).join(', ') || '-')],
|
|
312
|
+
['Schedule', r.schedule?.enabled === false ? 'disabled' : (r.schedule?.description || r.schedule?.cronExpression || '-')],
|
|
313
|
+
['Throttle', r.throttle?.enabled ? `${r.throttle.minutes} min` : 'off'],
|
|
314
|
+
['Query', r.queryMappingId?.name || r.queryMappingId || '-'],
|
|
315
|
+
['Validation', flags['no-validate'] ? 'skipped' : 'dry-run clean'],
|
|
316
|
+
]);
|
|
317
|
+
console.log('');
|
|
318
|
+
ui.success(`Created. View it with: securenow alerts rules show ${createdId}`);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
s.fail('Failed to create alert rule');
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function alertRuleShow(args, flags) {
|
|
326
|
+
requireAuth();
|
|
327
|
+
const id = args[0];
|
|
328
|
+
if (!id) {
|
|
329
|
+
ui.error('Usage: securenow alerts rules show <rule-id>');
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
const s = ui.spinner('Fetching alert rule');
|
|
333
|
+
try {
|
|
334
|
+
const data = await api.get(`/alert-rules/${id}`);
|
|
335
|
+
const r = data.alertRule;
|
|
336
|
+
s.stop('');
|
|
337
|
+
|
|
338
|
+
if (flags.json) {
|
|
339
|
+
ui.json(r);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log('');
|
|
344
|
+
ui.heading(r.name || 'Alert rule');
|
|
345
|
+
console.log('');
|
|
346
|
+
const appLine = r.applicationsAll
|
|
347
|
+
? 'All applications (current & future)'
|
|
348
|
+
: (r.applications && r.applications.length > 0 ? r.applications.join(', ') : '—');
|
|
349
|
+
ui.keyValue([
|
|
350
|
+
['ID', r._id || r.id || id],
|
|
351
|
+
['Status', r.status || '—'],
|
|
352
|
+
['Mode', r.mode === 'test' ? ui.c.yellow('test (detect-only, no mitigation)') : (r.mode || 'prod')],
|
|
353
|
+
['System rule', r.isSystem ? 'yes' : 'no'],
|
|
354
|
+
['Applications', appLine],
|
|
355
|
+
['Schedule', r.schedule?.enabled === false ? 'disabled' : (r.schedule?.description || r.schedule?.cronExpression || '—')],
|
|
356
|
+
['Throttle', r.throttle?.enabled ? `${r.throttle.minutes} min` : 'off'],
|
|
357
|
+
['Query', r.queryMappingId?.name || '—'],
|
|
358
|
+
]);
|
|
359
|
+
console.log('');
|
|
360
|
+
} catch (err) {
|
|
361
|
+
s.fail('Failed to fetch alert rule');
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function alertRuleUpdate(args, flags) {
|
|
367
|
+
requireAuth();
|
|
368
|
+
// `update <id> --sql ...` is an alias for set-sql so SQL edits and scope edits
|
|
369
|
+
// share one verb. set-sql handles its own validation/feedback.
|
|
370
|
+
if (flags.sql || flags.query || flags.file) {
|
|
371
|
+
return alertRuleSetSql(args, flags);
|
|
372
|
+
}
|
|
373
|
+
const id = args[0];
|
|
374
|
+
if (!id) {
|
|
375
|
+
ui.error('Usage: securenow alerts rules update <rule-id> [--applications-all | --apps <k1,k2>] [--status Active|Disabled|Paused | --enable | --disable | --pause] [--mode test|prod]');
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const hasAll =
|
|
380
|
+
flags['applications-all'] === true ||
|
|
381
|
+
flags['applications-all'] === 'true';
|
|
382
|
+
const hasNoAll =
|
|
383
|
+
flags['no-applications-all'] === true ||
|
|
384
|
+
flags['no-applications-all'] === 'true';
|
|
385
|
+
const appsStr = flags.apps;
|
|
386
|
+
|
|
387
|
+
if (hasAll && hasNoAll) {
|
|
388
|
+
ui.error('Use only one of --applications-all or --no-applications-all');
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
if (hasAll && appsStr) {
|
|
392
|
+
ui.error('Cannot combine --applications-all with --apps');
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const body = {};
|
|
397
|
+
if (hasAll) {
|
|
398
|
+
body.applicationsAll = true;
|
|
399
|
+
body.applications = [];
|
|
400
|
+
} else if (hasNoAll || appsStr) {
|
|
401
|
+
body.applicationsAll = false;
|
|
402
|
+
if (!appsStr) {
|
|
403
|
+
ui.error('Pass --apps key1,key2 when using --no-applications-all, or omit --no-applications-all and use --apps only');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
body.applications = String(appsStr)
|
|
407
|
+
.split(',')
|
|
408
|
+
.map((x) => x.trim())
|
|
409
|
+
.filter(Boolean);
|
|
410
|
+
if (body.applications.length === 0) {
|
|
411
|
+
ui.error('No application keys parsed from --apps');
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Status: --status X, or convenience --enable / --disable / --pause.
|
|
417
|
+
let status = null;
|
|
418
|
+
if (flags.enable === true || flags.enable === 'true') status = 'Active';
|
|
419
|
+
else if (flags.disable === true || flags.disable === 'true') status = 'Disabled';
|
|
420
|
+
else if (flags.pause === true || flags.pause === 'true') status = 'Paused';
|
|
421
|
+
else if (flags.status) {
|
|
422
|
+
const match = ['Active', 'Disabled', 'Paused'].find((x) => x.toLowerCase() === String(flags.status).toLowerCase());
|
|
423
|
+
if (!match) { ui.error('--status must be Active, Disabled, or Paused'); process.exit(1); }
|
|
424
|
+
status = match;
|
|
425
|
+
}
|
|
426
|
+
if (status) body.status = status;
|
|
427
|
+
|
|
428
|
+
// Lifecycle mode promote/demote (also available as: alerts rules promote|demote <id>).
|
|
429
|
+
if (flags.mode === 'test' || flags.mode === 'prod') body.mode = flags.mode;
|
|
430
|
+
|
|
431
|
+
if (Object.keys(body).length === 0) {
|
|
432
|
+
ui.error('Nothing to update. Use --applications-all/--apps, --status (or --enable/--disable/--pause), or --mode test|prod (or: alerts rules promote/demote <id>).');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const s = ui.spinner('Updating alert rule');
|
|
437
|
+
try {
|
|
438
|
+
await api.put(`/alert-rules/${id}`, body);
|
|
439
|
+
s.stop('Updated');
|
|
440
|
+
const extras = [body.status && `status → ${body.status}`, body.mode && `mode → ${body.mode}`].filter(Boolean);
|
|
441
|
+
ui.success('Alert rule updated' + (extras.length ? ` (${extras.join(', ')})` : ''));
|
|
442
|
+
} catch (err) {
|
|
443
|
+
s.fail('Failed to update alert rule');
|
|
444
|
+
throw err;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function alertRuleSetSql(args, flags) {
|
|
449
|
+
requireAuth();
|
|
450
|
+
const id = args[0];
|
|
451
|
+
const sqlQuery = readSqlArg(flags);
|
|
452
|
+
if (!id || !sqlQuery) {
|
|
453
|
+
ui.error('Usage: securenow alerts rules set-sql <rule-id> --sql <sql|@file|-> [--nlp "intent"] [--no-validate]');
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
const body = { sqlQuery };
|
|
457
|
+
if (flags.nlp || flags.text) body.nlpQuery = flags.nlp || flags.text;
|
|
458
|
+
if (flags.category) body.category = flags.category;
|
|
459
|
+
|
|
460
|
+
const s = ui.spinner('Updating rule SQL');
|
|
461
|
+
try {
|
|
462
|
+
const data = await api.put(`/alert-rules/${id}`, body);
|
|
463
|
+
s.stop('Rule SQL updated');
|
|
464
|
+
const ok = flags['no-validate'] ? null : await validateRuleById(id, flags);
|
|
465
|
+
if (ok && ok.status === 'failed') {
|
|
466
|
+
ui.warn(`Saved, but the new SQL failed a dry-run: ${ok.error || 'unknown error'}`);
|
|
467
|
+
ui.info('Fix the SQL and run set-sql again, or delete the rule with: securenow alerts rules delete ' + id);
|
|
468
|
+
} else if (ok) {
|
|
469
|
+
ui.success('Saved and dry-run clean.');
|
|
470
|
+
}
|
|
471
|
+
if (flags.json) ui.json(data);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
s.fail('Failed to update rule SQL');
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// List a rule's configuration version history (newest first). Each material update (SQL,
|
|
479
|
+
// instant config, settings), duplicate, and rollback is a version; rollback restores a
|
|
480
|
+
// prior snapshot and is itself recorded as a new version.
|
|
481
|
+
async function alertRuleVersions(args, flags) {
|
|
482
|
+
requireAuth();
|
|
483
|
+
const id = args[0];
|
|
484
|
+
if (!id) {
|
|
485
|
+
ui.error('Usage: securenow alerts rules versions <rule-id> [--limit N] [--page N]');
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
const query = {};
|
|
489
|
+
if (flags.limit) query.limit = flags.limit;
|
|
490
|
+
if (flags.page) query.page = flags.page;
|
|
491
|
+
const s = ui.spinner('Fetching version history');
|
|
492
|
+
let data;
|
|
493
|
+
try {
|
|
494
|
+
data = await api.get(`/alert-rules/${id}/versions`, Object.keys(query).length ? { query } : undefined);
|
|
495
|
+
s.stop(`Found ${data.total ?? (data.versions || []).length} version${(data.total ?? 0) === 1 ? '' : 's'}`);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
s.fail(`Failed to fetch versions: ${err.message}`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
if (flags.json) { ui.json(data); return; }
|
|
501
|
+
const rows = data.versions || [];
|
|
502
|
+
if (rows.length === 0) { ui.info('No versions recorded yet.'); return; }
|
|
503
|
+
console.log('');
|
|
504
|
+
ui.info(`Current: v${data.currentVersion}`);
|
|
505
|
+
for (const v of rows) {
|
|
506
|
+
const marker = v.isCurrent ? ui.c.green('→') : ' ';
|
|
507
|
+
const when = v.createdAt ? new Date(v.createdAt).toISOString().replace('T', ' ').slice(0, 19) : '';
|
|
508
|
+
const extra = v.changeType === 'rollback' && v.rolledBackFrom ? ` (from v${v.rolledBackFrom})` : '';
|
|
509
|
+
console.log(`${marker} ${ui.c.bold('v' + v.version).padEnd(14)} ${String(v.changeType + extra).padEnd(22)} ${ui.c.dim(when)} ${v.createdByName || ''}`);
|
|
510
|
+
if (v.reason) console.log(` ${ui.c.dim(ui.truncate(v.reason, 90))}`);
|
|
511
|
+
}
|
|
512
|
+
console.log('');
|
|
513
|
+
ui.info(`Show: securenow alerts rules version ${ui.truncate(id, 12)} <n> | Roll back: securenow alerts rules rollback ${ui.truncate(id, 12)} <n>`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Show one version's full material snapshot (diff-able SQL / instant / settings).
|
|
517
|
+
async function alertRuleVersionShow(args, flags) {
|
|
518
|
+
requireAuth();
|
|
519
|
+
const id = args[0];
|
|
520
|
+
const version = args[1];
|
|
521
|
+
if (!id || !version) {
|
|
522
|
+
ui.error('Usage: securenow alerts rules version <rule-id> <version-number>');
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
const s = ui.spinner(`Fetching v${version}`);
|
|
526
|
+
let data;
|
|
527
|
+
try {
|
|
528
|
+
data = await api.get(`/alert-rules/${id}/versions/${version}`);
|
|
529
|
+
s.stop(`Loaded v${version}`);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
s.fail(`Failed: ${err.message}`);
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
if (flags.json) { ui.json(data); return; }
|
|
535
|
+
const v = data.version || {};
|
|
536
|
+
const snap = v.snapshot || {};
|
|
537
|
+
console.log('');
|
|
538
|
+
ui.success(`v${v.version}${v.isCurrent ? ' (current)' : ''} — ${v.changeType}`);
|
|
539
|
+
console.log(` When ${v.createdAt || ''}`);
|
|
540
|
+
if (v.createdByName) console.log(` By ${v.createdByName}`);
|
|
541
|
+
if (v.reason) console.log(` Reason ${v.reason}`);
|
|
542
|
+
if (v.rolledBackFrom) console.log(` Restored v${v.rolledBackFrom}`);
|
|
543
|
+
console.log('');
|
|
544
|
+
console.log(` Name ${snap.name || ''}`);
|
|
545
|
+
console.log(` Mode ${snap.mode || ''} / ${snap.executionMode || ''} severity ${snap.severity || '-'}`);
|
|
546
|
+
if (snap.instant && snap.instant.enabled) {
|
|
547
|
+
console.log(` Instant enabled block=${snap.instant.block} (${(snap.instant.conditions || []).length} condition${(snap.instant.conditions || []).length === 1 ? '' : 's'})`);
|
|
548
|
+
}
|
|
549
|
+
if (snap.query && snap.query.sqlQuery) {
|
|
550
|
+
console.log(' SQL:');
|
|
551
|
+
console.log(snap.query.sqlQuery.split('\n').map((l) => ' ' + ui.c.dim(l)).join('\n'));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Roll a rule back to a prior version — restores that version's full snapshot and records it
|
|
556
|
+
// as a new version (append-only history). System (read-only template) rules can't be rolled back.
|
|
557
|
+
async function alertRuleRollback(args, flags) {
|
|
558
|
+
requireAuth();
|
|
559
|
+
const id = args[0];
|
|
560
|
+
const version = args[1] || flags.version;
|
|
561
|
+
if (!id || !version) {
|
|
562
|
+
ui.error('Usage: securenow alerts rules rollback <rule-id> <version-number> [--reason "..."] [--yes]');
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
if (!flags.yes && !flags.force) {
|
|
566
|
+
const ok = await ui.confirm(`Roll back rule ${ui.truncate(id, 12)} to v${version}? Restores that config and creates a new version.`);
|
|
567
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
568
|
+
}
|
|
569
|
+
const body = { version: Number(version) };
|
|
570
|
+
if (flags.reason) body.reason = String(flags.reason);
|
|
571
|
+
const s = ui.spinner(`Rolling back to v${version}`);
|
|
572
|
+
let data;
|
|
573
|
+
try {
|
|
574
|
+
data = await api.post(`/alert-rules/${id}/rollback`, body);
|
|
575
|
+
s.stop(`Rolled back to v${version}`);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
s.fail(`Failed to roll back: ${err.message}`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
if (flags.json) { ui.json(data); return; }
|
|
581
|
+
ui.success(`Restored from v${data.rolledBackFrom} → now at v${data.newVersion}`);
|
|
582
|
+
ui.info(`History: securenow alerts rules versions ${ui.truncate(id, 12)}`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function alertRuleDelete(args, flags) {
|
|
586
|
+
requireAuth();
|
|
587
|
+
const id = args[0];
|
|
588
|
+
if (!id) {
|
|
589
|
+
ui.error('Usage: securenow alerts rules delete <rule-id> [--yes]');
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
if (!flags.force && !flags.yes) {
|
|
593
|
+
const ok = await ui.confirm(`Delete alert rule ${id}? This cannot be undone (system rules can only be disabled).`);
|
|
594
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
595
|
+
}
|
|
596
|
+
const s = ui.spinner('Deleting alert rule');
|
|
597
|
+
try {
|
|
598
|
+
const data = await api.delete(`/alert-rules/${id}`);
|
|
599
|
+
s.stop('Alert rule deleted');
|
|
600
|
+
if (flags.json) { ui.json(data); return; }
|
|
601
|
+
ui.success(`Deleted alert rule ${id}`);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
s.fail('Failed to delete alert rule');
|
|
604
|
+
throw err;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Run a dry-run test of an existing rule and return { status, error, resultCount }.
|
|
609
|
+
// Used to validate a rule after create/set-sql. Never throws — returns null on error.
|
|
610
|
+
async function validateRuleById(id, flags = {}) {
|
|
611
|
+
try {
|
|
612
|
+
let data = await api.post(`/alert-rules/${id}/test`, { mode: 'dry_run' });
|
|
613
|
+
if (data.testId) {
|
|
614
|
+
for (let i = 0; i < 40; i++) {
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
616
|
+
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
617
|
+
if (['complete', 'failed'].includes(data.status)) break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return { status: data.status, error: data.error, resultCount: data.resultCount };
|
|
621
|
+
} catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Pick an existing rule id to host a candidate dry-run (the test endpoint needs a
|
|
627
|
+
// rule to attach the candidate SQL to; a dry-run never mutates it). The host must
|
|
628
|
+
// cover the requested app, or the API rejects the test.
|
|
629
|
+
async function resolveHostRuleId(appKey) {
|
|
630
|
+
const data = await api.get('/alert-rules');
|
|
631
|
+
const rules = data.alertRules || data.rules || [];
|
|
632
|
+
if (!rules.length) return null;
|
|
633
|
+
const covers = (r) => !appKey || r.applicationsAll || (r.applications || []).includes(appKey);
|
|
634
|
+
const host = rules.find((r) => covers(r) && r.status === 'Active') || rules.find(covers);
|
|
635
|
+
if (!host) return null;
|
|
636
|
+
return host._id || host.id || null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function readSqlArg(flags) {
|
|
640
|
+
const raw = flags.sql || flags.query || flags.file;
|
|
641
|
+
if (!raw) return null;
|
|
642
|
+
if (flags.file) return fs.readFileSync(flags.file, 'utf8');
|
|
643
|
+
const text = String(raw);
|
|
644
|
+
if (text === '-') return fs.readFileSync(0, 'utf8');
|
|
645
|
+
if (text.startsWith('@')) return fs.readFileSync(text.slice(1), 'utf8');
|
|
646
|
+
return text;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function alertRuleTest(args, flags) {
|
|
650
|
+
requireAuth();
|
|
651
|
+
const id = args[0];
|
|
652
|
+
if (!id) {
|
|
653
|
+
ui.error('Usage: securenow alerts rules test <rule-id> [--app <key>] [--mode dry_run|live] [--wait]');
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Default to a read-only dry-run. `--mode live` inserts synthetic spans into the app's
|
|
658
|
+
// telemetry, so it must be asked for explicitly.
|
|
659
|
+
const body = { mode: typeof flags.mode === 'string' ? flags.mode : 'dry_run' };
|
|
660
|
+
if (flags.app) body.applicationKey = flags.app;
|
|
661
|
+
const s = ui.spinner('Starting alert rule test');
|
|
662
|
+
try {
|
|
663
|
+
let data = await api.post(`/alert-rules/${id}/test`, body);
|
|
664
|
+
if (flags.wait && data.testId) {
|
|
665
|
+
s.update('Waiting for alert rule test results');
|
|
666
|
+
for (let i = 0; i < 40; i++) {
|
|
667
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
668
|
+
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
669
|
+
if (['complete', 'failed'].includes(data.status)) break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
s.stop('Alert rule test ready');
|
|
673
|
+
if (flags.json) { ui.json(data); return; }
|
|
674
|
+
console.log('');
|
|
675
|
+
ui.keyValue([
|
|
676
|
+
['Test ID', data.testId || '-'],
|
|
677
|
+
['Mode', data.mode || body.mode],
|
|
678
|
+
['Status', data.status || '-'],
|
|
679
|
+
['Result count', String(data.resultCount ?? '-')],
|
|
680
|
+
['Error', data.error || '-'],
|
|
681
|
+
...(data.warning ? [['Warning', data.warning]] : []),
|
|
682
|
+
]);
|
|
683
|
+
console.log('');
|
|
684
|
+
} catch (err) {
|
|
685
|
+
s.fail('Failed to test alert rule');
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function alertRuleCandidateTest(args, flags) {
|
|
691
|
+
requireAuth();
|
|
692
|
+
let id = args[0];
|
|
693
|
+
const candidateSqlQuery = readSqlArg(flags);
|
|
694
|
+
if (!candidateSqlQuery) {
|
|
695
|
+
ui.error('Usage: securenow alerts rules validate --sql <sql|@file|-> [--app <key>] (or dry-run-query <rule-id> --sql ...)');
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
// The test endpoint attaches the candidate SQL to an existing rule. When no
|
|
699
|
+
// rule id is given, auto-pick a host so a candidate can be validated before any
|
|
700
|
+
// rule exists for it. A brand-new account has no rules at all (none are pre-built):
|
|
701
|
+
// its first rule is validated by `alerts rules create`, which dry-runs the SQL itself.
|
|
702
|
+
if (!id) {
|
|
703
|
+
id = await resolveHostRuleId(flags.app);
|
|
704
|
+
if (!id) {
|
|
705
|
+
ui.error(flags.app
|
|
706
|
+
? `No existing rule covers app ${flags.app} to host the dry-run. Create one rule for it first, or pass <rule-id>.`
|
|
707
|
+
: 'No existing rule to host the dry-run. Create one rule first, or pass <rule-id>.');
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// `validate` is meant to return a verdict, so wait for the result by default.
|
|
712
|
+
const wait = flags.wait || flags['no-wait'] !== true;
|
|
713
|
+
|
|
714
|
+
const body = { mode: 'dry_run', candidateSqlQuery };
|
|
715
|
+
if (flags.app) body.applicationKey = flags.app;
|
|
716
|
+
|
|
717
|
+
const s = ui.spinner('Starting candidate SQL dry-run');
|
|
718
|
+
try {
|
|
719
|
+
let data = await api.post(`/alert-rules/${id}/test`, body);
|
|
720
|
+
if (wait && data.testId) {
|
|
721
|
+
s.update('Waiting for candidate SQL dry-run results');
|
|
722
|
+
for (let i = 0; i < 40; i++) {
|
|
723
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
724
|
+
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
725
|
+
if (['complete', 'failed'].includes(data.status)) break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
s.stop('Candidate dry-run ready');
|
|
729
|
+
if (flags.json) { ui.json(data); return; }
|
|
730
|
+
console.log('');
|
|
731
|
+
ui.keyValue([
|
|
732
|
+
['Test ID', data.testId || '-'],
|
|
733
|
+
['Status', data.status || '-'],
|
|
734
|
+
['Candidate', data.candidate ? 'yes' : 'no'],
|
|
735
|
+
['Current SQL hash', data.currentSqlHash || '-'],
|
|
736
|
+
['Candidate SQL hash', data.candidateSqlHash || '-'],
|
|
737
|
+
['Result count', String(data.resultCount ?? '-')],
|
|
738
|
+
['Error', data.error || '-'],
|
|
739
|
+
...(data.warning ? [['Warning', data.warning]] : []),
|
|
740
|
+
]);
|
|
741
|
+
console.log('');
|
|
742
|
+
} catch (err) {
|
|
743
|
+
s.fail('Failed to dry-run candidate SQL');
|
|
744
|
+
throw err;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function alertRuleQueryUpdate(args, flags) {
|
|
749
|
+
requireAuth();
|
|
750
|
+
const id = args[0];
|
|
751
|
+
const sqlQuery = readSqlArg(flags);
|
|
752
|
+
const reason = flags.reason;
|
|
753
|
+
if (!id || !sqlQuery || !reason) {
|
|
754
|
+
ui.error('Usage: securenow alerts rules tune-query <rule-id> --sql <sql|@file|-> --reason "..." --apply-globally --yes');
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
if (!flags['apply-globally'] && flags.applyGlobally !== true) {
|
|
758
|
+
ui.error('System query tuning requires --apply-globally.');
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!flags.force && !flags.yes) {
|
|
763
|
+
const ok = await ui.confirm('Update the shared system query mapping for all customer copies?');
|
|
764
|
+
if (!ok) {
|
|
765
|
+
ui.info('Cancelled');
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const body = {
|
|
771
|
+
sqlQuery,
|
|
772
|
+
reason,
|
|
773
|
+
applyGlobally: true,
|
|
774
|
+
reactivatePausedCopies: !!(flags['reactivate-paused'] || flags.reactivatePausedCopies),
|
|
775
|
+
};
|
|
776
|
+
if (flags['expected-hash']) body.expectedCurrentSqlHash = flags['expected-hash'];
|
|
777
|
+
if (flags.notification) body.reviewNotificationId = flags.notification;
|
|
778
|
+
if (flags.note) body.reviewNote = flags.note;
|
|
779
|
+
|
|
780
|
+
const s = ui.spinner('Updating system query mapping');
|
|
781
|
+
try {
|
|
782
|
+
const data = await api.put(`/alert-rules/${id}/query-mapping`, body);
|
|
783
|
+
s.stop('System query mapping updated');
|
|
784
|
+
if (flags.json) { ui.json(data); return; }
|
|
785
|
+
console.log('');
|
|
786
|
+
ui.keyValue([
|
|
787
|
+
['Query mapping', data.queryMapping?.name || data.queryMapping?.id || '-'],
|
|
788
|
+
['Previous SQL hash', data.queryMapping?.previousSqlHash || '-'],
|
|
789
|
+
['New SQL hash', data.queryMapping?.newSqlHash || '-'],
|
|
790
|
+
['Affected system rules', String(data.affectedSystemRules ?? '-')],
|
|
791
|
+
['Reactivated paused copies', data.reactivatedPausedCopies ? 'yes' : 'no'],
|
|
792
|
+
]);
|
|
793
|
+
console.log('');
|
|
794
|
+
} catch (err) {
|
|
795
|
+
s.fail('Failed to update system query mapping');
|
|
796
|
+
throw err;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function alertRuleExclusions(args, flags) {
|
|
801
|
+
requireAuth();
|
|
802
|
+
const id = args[0];
|
|
803
|
+
const action = args[1] || 'list';
|
|
804
|
+
if (!id) {
|
|
805
|
+
ui.error('Usage: securenow alerts rules exclusions <rule-id> [list|add|delete] [exclusion-id]');
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
if (action === 'add') {
|
|
811
|
+
const body = {};
|
|
812
|
+
if (flags.conditions) body.conditions = JSON.parse(flags.conditions);
|
|
813
|
+
if (flags['match-mode']) body.matchMode = flags['match-mode'];
|
|
814
|
+
if (flags.reason) body.reason = flags.reason;
|
|
815
|
+
if (flags.path) body.pathPattern = flags.path;
|
|
816
|
+
const data = await api.post(`/alert-rules/${id}/exclusions`, body);
|
|
817
|
+
if (flags.json) { ui.json(data); return; }
|
|
818
|
+
ui.success('Exclusion added');
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (action === 'delete' || action === 'remove') {
|
|
823
|
+
const exclusionId = args[2];
|
|
824
|
+
if (!exclusionId) {
|
|
825
|
+
ui.error('Exclusion id required.');
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
const data = await api.delete(`/alert-rules/${id}/exclusions/${exclusionId}`);
|
|
829
|
+
if (flags.json) { ui.json(data); return; }
|
|
830
|
+
ui.success('Exclusion removed');
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const data = await api.get(`/alert-rules/${id}/exclusions`);
|
|
835
|
+
const exclusions = data.exclusions || [];
|
|
836
|
+
if (flags.json) { ui.json(data); return; }
|
|
837
|
+
console.log('');
|
|
838
|
+
const rows = exclusions.map((e) => [
|
|
839
|
+
ui.c.dim(ui.truncate(e._id, 12)),
|
|
840
|
+
e.isActive === false ? ui.statusBadge('disabled') : ui.statusBadge('active'),
|
|
841
|
+
e.matchMode || 'all',
|
|
842
|
+
ui.truncate(e.reason || e.pathPattern || JSON.stringify(e.conditions || []), 72),
|
|
843
|
+
]);
|
|
844
|
+
ui.table(['ID', 'Status', 'Mode', 'Reason/Pattern'], rows);
|
|
845
|
+
console.log('');
|
|
846
|
+
} catch (err) {
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ── Alert Channels ──
|
|
852
|
+
|
|
853
|
+
async function alertChannelsList(args, flags) {
|
|
854
|
+
requireAuth();
|
|
855
|
+
const s = ui.spinner('Fetching alert channels');
|
|
856
|
+
try {
|
|
857
|
+
const data = await api.get('/alert-channels');
|
|
858
|
+
const channels = data.alertChannels || [];
|
|
859
|
+
s.stop(`Found ${channels.length} channel${channels.length !== 1 ? 's' : ''}`);
|
|
860
|
+
|
|
861
|
+
if (flags.json) { ui.json(channels); return; }
|
|
862
|
+
|
|
863
|
+
console.log('');
|
|
864
|
+
const rows = channels.map(ch => [
|
|
865
|
+
ui.c.dim(ui.truncate(ch._id, 12)),
|
|
866
|
+
ch.name || '—',
|
|
867
|
+
ch.type || '—',
|
|
868
|
+
ui.statusBadge(ch.enabled !== false ? 'enabled' : 'disabled'),
|
|
869
|
+
ch.target || ch.email || ch.webhookUrl || '—',
|
|
870
|
+
]);
|
|
871
|
+
ui.table(['ID', 'Name', 'Type', 'Status', 'Target'], rows);
|
|
872
|
+
console.log('');
|
|
873
|
+
} catch (err) {
|
|
874
|
+
s.fail('Failed to fetch alert channels');
|
|
875
|
+
throw err;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ── Alert History ──
|
|
880
|
+
|
|
881
|
+
async function alertHistoryList(args, flags) {
|
|
882
|
+
requireAuth();
|
|
883
|
+
const s = ui.spinner('Fetching alert history');
|
|
884
|
+
try {
|
|
885
|
+
const query = { limit: flags.limit || 20 };
|
|
886
|
+
const data = await api.get('/alert-history', { query });
|
|
887
|
+
const history = data.alerts || [];
|
|
888
|
+
s.stop(`Found ${history.length} alert${history.length !== 1 ? 's' : ''}${data.totalItems ? ` (${data.totalItems} total)` : ''}`);
|
|
889
|
+
|
|
890
|
+
if (flags.json) { ui.json(data); return; }
|
|
891
|
+
|
|
892
|
+
console.log('');
|
|
893
|
+
const rows = history.map(h => [
|
|
894
|
+
ui.c.dim(ui.truncate(h._id, 12)),
|
|
895
|
+
h.ruleName || h.type || '—',
|
|
896
|
+
h.severity ? ui.statusBadge(h.severity) : '—',
|
|
897
|
+
ui.truncate(h.message || h.description || '', 40),
|
|
898
|
+
h.serviceName || '—',
|
|
899
|
+
ui.timeAgo(h.triggeredAt || h.createdAt),
|
|
900
|
+
]);
|
|
901
|
+
ui.table(['ID', 'Rule', 'Severity', 'Message', 'App', 'Triggered'], rows);
|
|
902
|
+
console.log('');
|
|
903
|
+
} catch (err) {
|
|
904
|
+
s.fail('Failed to fetch alert history');
|
|
905
|
+
throw err;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ── Blocklist ──
|
|
910
|
+
|
|
911
|
+
function describeBlockTarget(entry) {
|
|
912
|
+
const parts = [entry.ip || entry.cidr || '-'];
|
|
913
|
+
if (entry.method && entry.method !== 'ALL') parts.push(entry.method);
|
|
914
|
+
if (entry.pathPattern) parts.push(`${entry.pathMatchMode || 'prefix'}:${entry.pathPattern}`);
|
|
915
|
+
return parts.join(' ');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function blocklistList(args, flags) {
|
|
919
|
+
requireAuth();
|
|
920
|
+
const s = ui.spinner('Fetching blocklist');
|
|
921
|
+
try {
|
|
922
|
+
const query = {};
|
|
923
|
+
if (flags.page) query.page = flags.page;
|
|
924
|
+
if (flags.limit) query.limit = flags.limit;
|
|
925
|
+
if (flags.app) query.appKey = flags.app;
|
|
926
|
+
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
927
|
+
if (flags.status) query.status = flags.status;
|
|
928
|
+
if (flags.search) query.search = flags.search;
|
|
929
|
+
if (flags.view) query.view = flags.view;
|
|
930
|
+
if (flags.approvalStatus || flags['approval-status']) {
|
|
931
|
+
query.approvalStatus = flags.approvalStatus || flags['approval-status'];
|
|
932
|
+
}
|
|
933
|
+
const data = await api.get('/blocklist', { query });
|
|
934
|
+
const items = data.blockedIps || [];
|
|
935
|
+
s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
936
|
+
|
|
937
|
+
if (flags.json) { ui.json(data); return; }
|
|
938
|
+
|
|
939
|
+
console.log('');
|
|
940
|
+
const rows = items.map(b => [
|
|
941
|
+
ui.c.dim(ui.truncate(b._id, 12)),
|
|
942
|
+
ui.c.red(describeBlockTarget(b)),
|
|
943
|
+
ui.truncate(b.reason || '', 40),
|
|
944
|
+
b.source || '—',
|
|
945
|
+
b.status || 'active',
|
|
946
|
+
b.applicationKey || ui.c.dim('all apps'),
|
|
947
|
+
b.environment || ui.c.dim('all envs'),
|
|
948
|
+
ui.timeAgo(b.createdAt),
|
|
949
|
+
b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
|
|
950
|
+
b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
951
|
+
]);
|
|
952
|
+
ui.table(['ID', 'Target', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
|
|
953
|
+
console.log('');
|
|
954
|
+
} catch (err) {
|
|
955
|
+
s.fail('Failed to fetch blocklist');
|
|
956
|
+
throw err;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function revokeRoute(args, flags) {
|
|
961
|
+
const sub = args[0];
|
|
962
|
+
if (sub === 'list') return revokeList(args.slice(1), flags);
|
|
963
|
+
if (sub === 'restore') return revokeRestore(args.slice(1), flags);
|
|
964
|
+
if (sub === 'session' || sub === 'user') return revokeAdd(sub, args.slice(1), flags);
|
|
965
|
+
ui.error('Usage: securenow revoke <session <id> | user <id> | list | restore <id>> [--reason "..."] [--duration 24h] [--app <key>] [--env <env>]');
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function revokeAdd(type, args, flags) {
|
|
970
|
+
requireAuth();
|
|
971
|
+
const value = args[0];
|
|
972
|
+
if (!value) {
|
|
973
|
+
ui.error(`Usage: securenow revoke ${type} <${type === 'session' ? 'session-id' : 'user-id'}> [--reason "..."] [--duration 24h] [--app <key>] [--env <env>]`);
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
const body = { type, value, source: 'cli' };
|
|
977
|
+
if (flags.reason) body.reason = flags.reason;
|
|
978
|
+
if (flags.duration) body.duration = flags.duration;
|
|
979
|
+
if (flags.app || flags.apps) body.applicationKey = flags.app || flags.apps;
|
|
980
|
+
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
981
|
+
|
|
982
|
+
const s = ui.spinner(`Revoking ${type} ${value}`);
|
|
983
|
+
try {
|
|
984
|
+
const data = await api.post('/revocations', body);
|
|
985
|
+
s.stop(`Revoked ${type} ${ui.truncate(value, 24)}`);
|
|
986
|
+
if (flags.json) { ui.json(data); return; }
|
|
987
|
+
const r = data.revocation || data;
|
|
988
|
+
ui.keyValue([
|
|
989
|
+
['ID', r._id || r.id || '-'],
|
|
990
|
+
['Type', r.type || type],
|
|
991
|
+
['Value', r.value || value],
|
|
992
|
+
['Scope', r.applicationKey || 'all apps'],
|
|
993
|
+
['Expires', r.expiresAt ? new Date(r.expiresAt).toISOString() : 'never'],
|
|
994
|
+
]);
|
|
995
|
+
} catch (err) {
|
|
996
|
+
s.fail('Failed to revoke');
|
|
997
|
+
throw err;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function revokeList(args, flags) {
|
|
1002
|
+
requireAuth();
|
|
1003
|
+
const s = ui.spinner('Fetching revocations');
|
|
1004
|
+
try {
|
|
1005
|
+
const query = {};
|
|
1006
|
+
if (flags.type) query.type = flags.type;
|
|
1007
|
+
if (flags.status) query.status = flags.status;
|
|
1008
|
+
const data = await api.get('/revocations', { query });
|
|
1009
|
+
const entries = data.revocations || [];
|
|
1010
|
+
s.stop(`Found ${entries.length} revocation${entries.length !== 1 ? 's' : ''}`);
|
|
1011
|
+
if (flags.json) { ui.json(entries); return; }
|
|
1012
|
+
console.log('');
|
|
1013
|
+
ui.table(['ID', 'Type', 'Value', 'Scope', 'Expires'], entries.map((r) => [
|
|
1014
|
+
ui.c.dim(ui.truncate(r._id || r.id, 12)),
|
|
1015
|
+
r.type,
|
|
1016
|
+
ui.truncate(r.value, 28),
|
|
1017
|
+
r.applicationKey || ui.c.dim('all'),
|
|
1018
|
+
r.expiresAt ? new Date(r.expiresAt).toISOString().slice(0, 16) : ui.c.dim('never'),
|
|
1019
|
+
]));
|
|
1020
|
+
console.log('');
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
s.fail('Failed to list revocations');
|
|
1023
|
+
throw err;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function revokeRestore(args, flags) {
|
|
1028
|
+
requireAuth();
|
|
1029
|
+
const id = args[0];
|
|
1030
|
+
if (!id) { ui.error('Usage: securenow revoke restore <id> [--reason "..."]'); process.exit(1); }
|
|
1031
|
+
const s = ui.spinner('Restoring');
|
|
1032
|
+
try {
|
|
1033
|
+
const data = await api.post(`/revocations/${id}/restore`, { reason: flags.reason || '' });
|
|
1034
|
+
s.stop('Revocation lifted');
|
|
1035
|
+
if (flags.json) ui.json(data);
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
s.fail('Failed to restore');
|
|
1038
|
+
throw err;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function blocklistAdd(args, flags) {
|
|
1043
|
+
requireAuth();
|
|
1044
|
+
let ip = args[0];
|
|
1045
|
+
if (!ip) {
|
|
1046
|
+
ip = await ui.prompt('IP address or CIDR to block');
|
|
1047
|
+
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const body = { ip };
|
|
1051
|
+
if (flags.reason) body.reason = flags.reason;
|
|
1052
|
+
if (flags.duration) body.duration = flags.duration;
|
|
1053
|
+
if (flags.app) body.appKey = flags.app;
|
|
1054
|
+
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
1055
|
+
if (flags.route || flags.path || flags.pattern) body.pathPattern = flags.route || flags.path || flags.pattern;
|
|
1056
|
+
if (flags.mode || flags['path-mode']) body.pathMatchMode = flags.mode || flags['path-mode'];
|
|
1057
|
+
if (flags.method) body.method = flags.method;
|
|
1058
|
+
|
|
1059
|
+
const target = describeBlockTarget({ ip, method: body.method, pathPattern: body.pathPattern, pathMatchMode: body.pathMatchMode });
|
|
1060
|
+
const s = ui.spinner(`Blocking ${target}`);
|
|
1061
|
+
try {
|
|
1062
|
+
const data = await api.post('/blocklist', body);
|
|
1063
|
+
s.stop(`${target} added to blocklist`);
|
|
1064
|
+
if (flags.json) ui.json(data);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
s.fail('Failed to add to blocklist');
|
|
1067
|
+
throw err;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
async function blocklistRemove(args, flags) {
|
|
1072
|
+
requireAuth();
|
|
1073
|
+
const id = args[0];
|
|
1074
|
+
if (!id) {
|
|
1075
|
+
ui.error('Blocklist entry ID required. Usage: securenow blocklist unblock <id> [--reason <reason>]');
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (!flags.force && !flags.yes) {
|
|
1080
|
+
const ok = await ui.confirm('Unblock this IP? Firewall enforcement stops, but block report and history stay saved.');
|
|
1081
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const s = ui.spinner('Unblocking IP');
|
|
1085
|
+
try {
|
|
1086
|
+
const body = {};
|
|
1087
|
+
if (flags.reason) body.reason = flags.reason;
|
|
1088
|
+
await api.post(`/blocklist/${id}/unblock`, body);
|
|
1089
|
+
s.stop('Unblocked; block report and history retained');
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
s.fail('Failed to unblock IP');
|
|
1092
|
+
throw err;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Approve a block that the AI prepared but parked pending customer approval
|
|
1097
|
+
// (e.g. the autopilot held it under block_risk_score_below_policy). Enforces the IP.
|
|
1098
|
+
async function blocklistApprove(args, flags) {
|
|
1099
|
+
requireAuth();
|
|
1100
|
+
const id = args[0];
|
|
1101
|
+
if (!id) {
|
|
1102
|
+
ui.error('Usage: securenow blocklist approve <pending-block-id> [--reason <reason>] [--yes]');
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
if (!flags.force && !flags.yes) {
|
|
1106
|
+
const ok = await ui.confirm('Approve this pending block? The IP will be enforced by the firewall.');
|
|
1107
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1108
|
+
}
|
|
1109
|
+
const s = ui.spinner('Approving pending block');
|
|
1110
|
+
try {
|
|
1111
|
+
const body = {};
|
|
1112
|
+
if (flags.reason) body.reason = flags.reason;
|
|
1113
|
+
const data = await api.post(`/blocklist/${id}/approve`, body);
|
|
1114
|
+
s.stop('Pending block approved — IP now enforced');
|
|
1115
|
+
if (flags.json) ui.json(data);
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
s.fail('Failed to approve pending block');
|
|
1118
|
+
throw err;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Reject a pending block: the IP is NOT blocked, the entry is removed, and a scoped
|
|
1123
|
+
// no-block decision is recorded so auto-block won't re-block this IP for the same rule.
|
|
1124
|
+
async function blocklistReject(args, flags) {
|
|
1125
|
+
requireAuth();
|
|
1126
|
+
const id = args[0];
|
|
1127
|
+
if (!id) {
|
|
1128
|
+
ui.error('Usage: securenow blocklist reject <pending-block-id> [--reason <reason>] [--yes]');
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
if (!flags.force && !flags.yes) {
|
|
1132
|
+
const ok = await ui.confirm('Reject this pending block? The IP will NOT be blocked; a scoped no-block decision is recorded for the rule that proposed it.');
|
|
1133
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1134
|
+
}
|
|
1135
|
+
const s = ui.spinner('Rejecting pending block');
|
|
1136
|
+
try {
|
|
1137
|
+
const body = {};
|
|
1138
|
+
if (flags.reason) body.reason = flags.reason;
|
|
1139
|
+
const data = await api.post(`/blocklist/${id}/reject`, body);
|
|
1140
|
+
s.stop('Pending block rejected — IP cleared (scoped no-block decision recorded)');
|
|
1141
|
+
if (flags.json) ui.json(data);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
s.fail('Failed to reject pending block');
|
|
1144
|
+
throw err;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function blocklistStats(args, flags) {
|
|
1149
|
+
requireAuth();
|
|
1150
|
+
const s = ui.spinner('Fetching blocklist stats');
|
|
1151
|
+
try {
|
|
1152
|
+
const data = await api.get('/blocklist/stats');
|
|
1153
|
+
const stats = data.stats || data;
|
|
1154
|
+
s.stop('Stats loaded');
|
|
1155
|
+
|
|
1156
|
+
if (flags.json) { ui.json(stats); return; }
|
|
1157
|
+
|
|
1158
|
+
console.log('');
|
|
1159
|
+
ui.heading('Blocklist Statistics');
|
|
1160
|
+
console.log('');
|
|
1161
|
+
ui.keyValue([
|
|
1162
|
+
['Total Active', String(stats.totalActive ?? '—')],
|
|
1163
|
+
['Total Removed', String(stats.totalRemoved ?? '—')],
|
|
1164
|
+
['Manual Blocks', String(stats.manualCount ?? '—')],
|
|
1165
|
+
['Automation Blocks', String(stats.automationCount ?? '—')],
|
|
1166
|
+
['Active Rules', String(stats.activeAutomationRules ?? '—')],
|
|
1167
|
+
]);
|
|
1168
|
+
console.log('');
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
s.fail('Failed to fetch stats');
|
|
1171
|
+
throw err;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// ── Allowlist ──
|
|
1176
|
+
|
|
1177
|
+
async function allowlistList(args, flags) {
|
|
1178
|
+
requireAuth();
|
|
1179
|
+
const s = ui.spinner('Fetching allowlist');
|
|
1180
|
+
try {
|
|
1181
|
+
const query = {};
|
|
1182
|
+
if (flags.page) query.page = flags.page;
|
|
1183
|
+
if (flags.limit) query.limit = flags.limit;
|
|
1184
|
+
if (flags.app) query.appKey = flags.app;
|
|
1185
|
+
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
1186
|
+
const data = await api.get('/allowlist', { query });
|
|
1187
|
+
const items = data.allowedIps || [];
|
|
1188
|
+
s.stop(`Found ${items.length} allowed IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
1189
|
+
|
|
1190
|
+
if (flags.json) { ui.json(data); return; }
|
|
1191
|
+
|
|
1192
|
+
console.log('');
|
|
1193
|
+
const rows = items.map(a => [
|
|
1194
|
+
ui.c.dim(ui.truncate(a._id, 12)),
|
|
1195
|
+
ui.c.green(a.ip || '—'),
|
|
1196
|
+
a.label || ui.c.dim('—'),
|
|
1197
|
+
ui.truncate(a.reason || '', 40),
|
|
1198
|
+
a.applicationKey || ui.c.dim('all apps'),
|
|
1199
|
+
a.environment || ui.c.dim('all envs'),
|
|
1200
|
+
ui.timeAgo(a.createdAt),
|
|
1201
|
+
a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
1202
|
+
]);
|
|
1203
|
+
ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'App', 'Env', 'Added', 'Expires'], rows);
|
|
1204
|
+
console.log('');
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
s.fail('Failed to fetch allowlist');
|
|
1207
|
+
throw err;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async function allowlistAdd(args, flags) {
|
|
1212
|
+
requireAuth();
|
|
1213
|
+
let ip = args[0];
|
|
1214
|
+
if (!ip) {
|
|
1215
|
+
ip = await ui.prompt('IP address or CIDR to allow');
|
|
1216
|
+
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const body = { ip };
|
|
1220
|
+
if (flags.label) body.label = flags.label;
|
|
1221
|
+
if (flags.reason) body.reason = flags.reason;
|
|
1222
|
+
if (flags.app) {
|
|
1223
|
+
body.applicationsAll = false;
|
|
1224
|
+
body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
|
|
1225
|
+
}
|
|
1226
|
+
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
1227
|
+
|
|
1228
|
+
if (!flags.force && !flags.yes) {
|
|
1229
|
+
ui.warn('IP Allowlist is deny-by-default: once active, only listed IPs can reach the scoped app/environment and all others are blocked.');
|
|
1230
|
+
ui.info('Use `securenow trusted add` instead if this IP should be trusted without locking out normal visitors.');
|
|
1231
|
+
const ok = await ui.confirm('Add this IP to the restrictive allowlist?');
|
|
1232
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1233
|
+
}
|
|
1234
|
+
body.allowlistDenyAllApproved = true;
|
|
1235
|
+
|
|
1236
|
+
const s = ui.spinner(`Allowing ${ip}`);
|
|
1237
|
+
try {
|
|
1238
|
+
await api.post('/allowlist', body);
|
|
1239
|
+
s.stop(`${ip} added to allowlist`);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
s.fail('Failed to add to allowlist');
|
|
1242
|
+
throw err;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async function allowlistRemove(args, flags) {
|
|
1247
|
+
requireAuth();
|
|
1248
|
+
const id = args[0];
|
|
1249
|
+
if (!id) {
|
|
1250
|
+
ui.error('Allowlist entry ID required. Usage: securenow allowlist remove <id>');
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (!flags.force && !flags.yes) {
|
|
1255
|
+
const ok = await ui.confirm('Remove this IP from the allowlist?');
|
|
1256
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const s = ui.spinner('Removing from allowlist');
|
|
1260
|
+
try {
|
|
1261
|
+
await api.delete(`/allowlist/${id}`);
|
|
1262
|
+
s.stop('Removed from allowlist');
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
s.fail('Failed to remove from allowlist');
|
|
1265
|
+
throw err;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
async function allowlistStats(args, flags) {
|
|
1270
|
+
requireAuth();
|
|
1271
|
+
const s = ui.spinner('Fetching allowlist stats');
|
|
1272
|
+
try {
|
|
1273
|
+
const data = await api.get('/allowlist/stats');
|
|
1274
|
+
const stats = data.stats || data;
|
|
1275
|
+
s.stop('Stats loaded');
|
|
1276
|
+
|
|
1277
|
+
if (flags.json) { ui.json(stats); return; }
|
|
1278
|
+
|
|
1279
|
+
console.log('');
|
|
1280
|
+
ui.heading('Allowlist Statistics');
|
|
1281
|
+
console.log('');
|
|
1282
|
+
ui.keyValue([
|
|
1283
|
+
['Total Active', String(stats.totalActive ?? '—')],
|
|
1284
|
+
['Total Removed', String(stats.totalRemoved ?? '—')],
|
|
1285
|
+
]);
|
|
1286
|
+
console.log('');
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
s.fail('Failed to fetch stats');
|
|
1289
|
+
throw err;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ── Trusted IPs ──
|
|
1294
|
+
|
|
1295
|
+
async function trustedList(args, flags) {
|
|
1296
|
+
requireAuth();
|
|
1297
|
+
const s = ui.spinner('Fetching trusted IPs');
|
|
1298
|
+
try {
|
|
1299
|
+
const query = {};
|
|
1300
|
+
if (flags.app) query.appKey = flags.app;
|
|
1301
|
+
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
1302
|
+
const data = await api.get('/trusted-ips', { query });
|
|
1303
|
+
const items = data.trustedIps || [];
|
|
1304
|
+
s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
|
|
1305
|
+
|
|
1306
|
+
if (flags.json) { ui.json(items); return; }
|
|
1307
|
+
|
|
1308
|
+
console.log('');
|
|
1309
|
+
const rows = items.map(t => [
|
|
1310
|
+
ui.c.dim(ui.truncate(t._id, 12)),
|
|
1311
|
+
ui.c.green(t.ip || t.cidr || '—'),
|
|
1312
|
+
t.label || t.description || ui.c.dim('—'),
|
|
1313
|
+
t.applicationKey || ui.c.dim('all apps'),
|
|
1314
|
+
t.environment || ui.c.dim('all envs'),
|
|
1315
|
+
ui.timeAgo(t.createdAt),
|
|
1316
|
+
]);
|
|
1317
|
+
ui.table(['ID', 'IP/CIDR', 'Label', 'App', 'Env', 'Added'], rows);
|
|
1318
|
+
console.log('');
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
s.fail('Failed to fetch trusted IPs');
|
|
1321
|
+
throw err;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function trustedAdd(args, flags) {
|
|
1326
|
+
requireAuth();
|
|
1327
|
+
let ip = args[0];
|
|
1328
|
+
if (!ip) {
|
|
1329
|
+
ip = await ui.prompt('IP address or CIDR to trust');
|
|
1330
|
+
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const body = { ip };
|
|
1334
|
+
if (flags.label) body.label = flags.label;
|
|
1335
|
+
if (flags.description) body.description = flags.description;
|
|
1336
|
+
if (flags.note) body.note = flags.note;
|
|
1337
|
+
if (flags.app) {
|
|
1338
|
+
body.applicationsAll = false;
|
|
1339
|
+
body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
|
|
1340
|
+
}
|
|
1341
|
+
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
1342
|
+
|
|
1343
|
+
const s = ui.spinner(`Adding ${ip} to trusted IPs`);
|
|
1344
|
+
try {
|
|
1345
|
+
await api.post('/trusted-ips', body);
|
|
1346
|
+
s.stop(`${ip} added to trusted IPs`);
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
s.fail('Failed to add trusted IP');
|
|
1349
|
+
throw err;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
async function trustedRemove(args, flags) {
|
|
1354
|
+
requireAuth();
|
|
1355
|
+
const id = args[0];
|
|
1356
|
+
if (!id) {
|
|
1357
|
+
ui.error('Trusted IP entry ID required. Usage: securenow trusted remove <id>');
|
|
1358
|
+
process.exit(1);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (!flags.force && !flags.yes) {
|
|
1362
|
+
const ok = await ui.confirm('Remove this IP from trusted list?');
|
|
1363
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const s = ui.spinner('Removing from trusted IPs');
|
|
1367
|
+
try {
|
|
1368
|
+
await api.delete(`/trusted-ips/${id}`);
|
|
1369
|
+
s.stop('Removed from trusted IPs');
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
s.fail('Failed to remove trusted IP');
|
|
1372
|
+
throw err;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// ── Helpers ──
|
|
1377
|
+
|
|
1378
|
+
async function resolveAppId(flags) {
|
|
1379
|
+
const appKey = flags.app || config.getDefaultApp();
|
|
1380
|
+
if (!appKey) return null;
|
|
1381
|
+
const data = await api.get('/applications');
|
|
1382
|
+
const apps = data.applications || [];
|
|
1383
|
+
const match = apps.find(a => a.key === appKey);
|
|
1384
|
+
return match ? { id: match._id, key: match.key, name: match.name } : null;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// ── Forensics ──
|
|
1388
|
+
|
|
1389
|
+
async function forensicsQuery(args, flags) {
|
|
1390
|
+
requireAuth();
|
|
1391
|
+
const query = args.join(' ');
|
|
1392
|
+
if (!query) {
|
|
1393
|
+
ui.error('Query required. Usage: securenow forensics "your natural language query" [--app <key>]');
|
|
1394
|
+
process.exit(1);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const s = ui.spinner('Submitting forensic query');
|
|
1398
|
+
try {
|
|
1399
|
+
const body = { query, environment: resolveEnvironment(flags, 'production') };
|
|
1400
|
+
const resolved = await resolveAppId(flags);
|
|
1401
|
+
if (resolved) {
|
|
1402
|
+
body.applicationId = resolved.id;
|
|
1403
|
+
} else if (flags.instance) {
|
|
1404
|
+
body.instanceId = flags.instance;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const job = await api.post('/forensics/query', body);
|
|
1408
|
+
const jobId = job.jobId;
|
|
1409
|
+
|
|
1410
|
+
if (!jobId) {
|
|
1411
|
+
s.stop('Query complete');
|
|
1412
|
+
if (flags.json) { ui.json(job); return; }
|
|
1413
|
+
if (job.result) {
|
|
1414
|
+
console.log('');
|
|
1415
|
+
if (job.sqlquery) {
|
|
1416
|
+
ui.subheading('Generated SQL');
|
|
1417
|
+
console.log(`\n ${ui.c.dim(job.sqlquery)}\n`);
|
|
1418
|
+
}
|
|
1419
|
+
const data = job.result;
|
|
1420
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
1421
|
+
const headers = Object.keys(data[0]);
|
|
1422
|
+
const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
|
|
1423
|
+
ui.table(headers, rows);
|
|
1424
|
+
} else {
|
|
1425
|
+
ui.json(data);
|
|
1426
|
+
}
|
|
1427
|
+
console.log('');
|
|
1428
|
+
}
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
s.update('Processing query...');
|
|
1433
|
+
|
|
1434
|
+
let result;
|
|
1435
|
+
const maxAttempts = 60;
|
|
1436
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1437
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1438
|
+
result = await api.get(`/forensics/query/status/${jobId}`);
|
|
1439
|
+
if (result.status === 'completed' || result.status === 'failed') break;
|
|
1440
|
+
s.update(`Processing query... (${(i + 1) * 2}s)`);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (result.status === 'failed') {
|
|
1444
|
+
s.fail('Query failed');
|
|
1445
|
+
ui.error(result.error || 'Unknown error');
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (result.status !== 'completed') {
|
|
1450
|
+
s.fail('Query timed out');
|
|
1451
|
+
ui.warn('Query is still processing. Check back later.');
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
s.stop('Query complete');
|
|
1456
|
+
|
|
1457
|
+
if (flags.json) { ui.json(result); return; }
|
|
1458
|
+
|
|
1459
|
+
console.log('');
|
|
1460
|
+
if (result.sqlquery) {
|
|
1461
|
+
ui.subheading('Generated SQL');
|
|
1462
|
+
console.log(`\n ${ui.c.dim(result.sqlquery)}\n`);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (result.result) {
|
|
1466
|
+
const data = result.result;
|
|
1467
|
+
ui.subheading(`Results (${result.rowCount ?? (Array.isArray(data) ? data.length : '?')} rows)`);
|
|
1468
|
+
console.log('');
|
|
1469
|
+
|
|
1470
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
1471
|
+
const headers = Object.keys(data[0]);
|
|
1472
|
+
const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
|
|
1473
|
+
ui.table(headers, rows);
|
|
1474
|
+
} else {
|
|
1475
|
+
ui.json(data);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
console.log('');
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
s.fail('Forensic query failed');
|
|
1481
|
+
throw err;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async function forensicsLibrary(args, flags) {
|
|
1486
|
+
requireAuth();
|
|
1487
|
+
const s = ui.spinner('Fetching query library');
|
|
1488
|
+
try {
|
|
1489
|
+
const data = await api.get('/forensics/query-library');
|
|
1490
|
+
const queries = data.data || [];
|
|
1491
|
+
s.stop(`Found ${queries.length} saved quer${queries.length !== 1 ? 'ies' : 'y'}`);
|
|
1492
|
+
|
|
1493
|
+
if (flags.json) { ui.json(queries); return; }
|
|
1494
|
+
|
|
1495
|
+
console.log('');
|
|
1496
|
+
const rows = queries.map(q => [
|
|
1497
|
+
ui.c.dim(ui.truncate(q._id, 12)),
|
|
1498
|
+
q.name || q.title || '—',
|
|
1499
|
+
ui.truncate(q.description || q.query || '', 50),
|
|
1500
|
+
ui.timeAgo(q.createdAt),
|
|
1501
|
+
]);
|
|
1502
|
+
ui.table(['ID', 'Name', 'Description', 'Created'], rows);
|
|
1503
|
+
console.log('');
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
s.fail('Failed to fetch query library');
|
|
1506
|
+
throw err;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// ── Forensics Chat ──
|
|
1511
|
+
|
|
1512
|
+
async function forensicsChat(args, flags) {
|
|
1513
|
+
requireAuth();
|
|
1514
|
+
const resolved = await resolveAppId(flags);
|
|
1515
|
+
if (!resolved) {
|
|
1516
|
+
ui.error('App required. Usage: securenow forensics chat --app <key>');
|
|
1517
|
+
ui.info('Set a default with: securenow apps default <key>');
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
console.log('');
|
|
1522
|
+
ui.heading(`Forensics Chat — ${resolved.name || resolved.key}`);
|
|
1523
|
+
console.log(ui.c.dim(` App: ${resolved.key} (${resolved.id})`));
|
|
1524
|
+
console.log(ui.c.dim(' Type your question, or "exit" to quit.\n'));
|
|
1525
|
+
|
|
1526
|
+
const environment = resolveEnvironment(flags, 'production');
|
|
1527
|
+
console.log(ui.c.dim(` Env: ${environment}`));
|
|
1528
|
+
|
|
1529
|
+
let conversationId = null;
|
|
1530
|
+
|
|
1531
|
+
const readline = require('readline');
|
|
1532
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
1533
|
+
|
|
1534
|
+
const askLine = () => new Promise((resolve) => {
|
|
1535
|
+
rl.question(ui.c.cyan(' you > '), (answer) => resolve(answer.trim()));
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
try {
|
|
1539
|
+
while (true) {
|
|
1540
|
+
const message = await askLine();
|
|
1541
|
+
if (!message || message.toLowerCase() === 'exit' || message.toLowerCase() === 'quit') {
|
|
1542
|
+
console.log('');
|
|
1543
|
+
ui.info('Chat ended.');
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const s = ui.spinner('Thinking');
|
|
1548
|
+
try {
|
|
1549
|
+
const body = { message, applicationKey: resolved.key, environment };
|
|
1550
|
+
if (conversationId) body.conversationId = conversationId;
|
|
1551
|
+
|
|
1552
|
+
const chatRes = await api.post('/forensics/chat', body);
|
|
1553
|
+
conversationId = chatRes.conversationId;
|
|
1554
|
+
|
|
1555
|
+
if (chatRes.status === 'processing') {
|
|
1556
|
+
let result;
|
|
1557
|
+
for (let i = 0; i < 150; i++) {
|
|
1558
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1559
|
+
result = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
1560
|
+
if (result.status === 'complete' || result.status === 'failed' || result.status === 'awaiting_confirmation') break;
|
|
1561
|
+
const progress = result._progress;
|
|
1562
|
+
if (progress?.action) s.update(progress.action);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (!result || result.status === 'processing') {
|
|
1566
|
+
s.fail('Response timed out');
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
s.stop('Done');
|
|
1571
|
+
printChatMessage(result.message);
|
|
1572
|
+
|
|
1573
|
+
if (result.status === 'awaiting_confirmation') {
|
|
1574
|
+
const proceed = await ui.confirm('Proceed with this query?');
|
|
1575
|
+
if (proceed) {
|
|
1576
|
+
const cs = ui.spinner('Executing query');
|
|
1577
|
+
await api.post(`/forensics/chat/confirm/${conversationId}`, { environment });
|
|
1578
|
+
let confirmResult;
|
|
1579
|
+
for (let i = 0; i < 90; i++) {
|
|
1580
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1581
|
+
confirmResult = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
1582
|
+
if (confirmResult.status !== 'processing') break;
|
|
1583
|
+
}
|
|
1584
|
+
cs.stop('Done');
|
|
1585
|
+
if (confirmResult?.message) printChatMessage(confirmResult.message);
|
|
1586
|
+
} else {
|
|
1587
|
+
ui.info('Query skipped. Ask a different question or refine.');
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
} else if (chatRes.message) {
|
|
1591
|
+
s.stop('Done');
|
|
1592
|
+
printChatMessage(chatRes.message);
|
|
1593
|
+
} else {
|
|
1594
|
+
s.stop('Done');
|
|
1595
|
+
}
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
s.fail('Error');
|
|
1598
|
+
ui.error(err.message);
|
|
1599
|
+
}
|
|
1600
|
+
console.log('');
|
|
1601
|
+
}
|
|
1602
|
+
} finally {
|
|
1603
|
+
rl.close();
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function printChatMessage(msg) {
|
|
1608
|
+
if (!msg) return;
|
|
1609
|
+
console.log('');
|
|
1610
|
+
if (msg.content) {
|
|
1611
|
+
console.log(` ${ui.c.green('ai >')} ${msg.content}`);
|
|
1612
|
+
}
|
|
1613
|
+
if (msg.sqlQuery) {
|
|
1614
|
+
console.log('');
|
|
1615
|
+
ui.subheading('Generated SQL');
|
|
1616
|
+
console.log(`\n ${ui.c.dim(msg.sqlQuery)}\n`);
|
|
1617
|
+
}
|
|
1618
|
+
if (msg.results && Array.isArray(msg.results) && msg.results.length > 0) {
|
|
1619
|
+
ui.subheading(`Results (${msg.resultCount ?? msg.results.length} rows)`);
|
|
1620
|
+
console.log('');
|
|
1621
|
+
const headers = Object.keys(msg.results[0]);
|
|
1622
|
+
const rows = msg.results.map(row => headers.map(h => String(row[h] ?? '')));
|
|
1623
|
+
ui.table(headers, rows);
|
|
1624
|
+
}
|
|
1625
|
+
if (msg.estimation) {
|
|
1626
|
+
const est = msg.estimation;
|
|
1627
|
+
const rowLabel = est.timedOut
|
|
1628
|
+
? 'very large (estimation timed out)'
|
|
1629
|
+
: est.estimatedRows != null
|
|
1630
|
+
? `~${Number(est.estimatedRows).toLocaleString()}`
|
|
1631
|
+
: 'unknown';
|
|
1632
|
+
console.log(`\n ${ui.c.yellow('!')} Estimated rows: ${rowLabel}`);
|
|
1633
|
+
if (est.suggestions?.length) {
|
|
1634
|
+
console.log(ui.c.dim(' Suggestions:'));
|
|
1635
|
+
for (const sug of est.suggestions) {
|
|
1636
|
+
console.log(ui.c.dim(` • ${sug}`));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (msg.error && !msg.results) {
|
|
1641
|
+
console.log(`\n ${ui.c.red('Error:')} ${msg.error}`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// ── IP Lookup ──
|
|
1646
|
+
|
|
1647
|
+
async function ipLookup(args, flags) {
|
|
1648
|
+
requireAuth();
|
|
1649
|
+
const ip = args[0];
|
|
1650
|
+
if (!ip) {
|
|
1651
|
+
ui.error('IP address required. Usage: securenow ip <ip-address>');
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
const s = ui.spinner(`Looking up ${ip}`);
|
|
1656
|
+
try {
|
|
1657
|
+
const data = await api.get(`/ip/${ip}`);
|
|
1658
|
+
s.stop('IP intelligence loaded');
|
|
1659
|
+
|
|
1660
|
+
if (flags.json) { ui.json(data); return; }
|
|
1661
|
+
|
|
1662
|
+
console.log('');
|
|
1663
|
+
ui.heading(`IP Intelligence: ${data.ip || ip}`);
|
|
1664
|
+
console.log('');
|
|
1665
|
+
|
|
1666
|
+
const pairs = [];
|
|
1667
|
+
if (data.countryName || data.countryCode) pairs.push(['Country', `${data.countryName || ''} ${data.countryCode ? `(${data.countryCode})` : ''}`.trim()]);
|
|
1668
|
+
if (data.domain) pairs.push(['Domain', data.domain]);
|
|
1669
|
+
if (data.isp) pairs.push(['ISP', data.isp]);
|
|
1670
|
+
if (data.usageType) pairs.push(['Usage Type', data.usageType]);
|
|
1671
|
+
if (data.abuseConfidenceScore != null) pairs.push(['SecureNow IPDB Score', `${data.abuseConfidenceScore}/100`]);
|
|
1672
|
+
if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
|
|
1673
|
+
if (data.verdict) pairs.push(['Verdict', data.verdict]);
|
|
1674
|
+
if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
|
|
1675
|
+
if (data.isBot != null) pairs.push(['Bot', data.isBot ? ui.c.yellow('Yes') : 'No']);
|
|
1676
|
+
if (data.activityType) pairs.push(['Activity', data.activityType]);
|
|
1677
|
+
if (data.totalReports != null) pairs.push(['Total Reports', String(data.totalReports)]);
|
|
1678
|
+
if (data.lastReportedAt) pairs.push(['Last Reported', new Date(data.lastReportedAt).toLocaleString()]);
|
|
1679
|
+
|
|
1680
|
+
if (pairs.length) {
|
|
1681
|
+
ui.keyValue(pairs);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (data.riskFactors?.length) {
|
|
1685
|
+
ui.subheading('Risk Factors');
|
|
1686
|
+
console.log('');
|
|
1687
|
+
data.riskFactors.forEach(f => console.log(` • ${f}`));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (data.attackTypes?.length) {
|
|
1691
|
+
ui.subheading('Attack Types');
|
|
1692
|
+
console.log('');
|
|
1693
|
+
data.attackTypes.forEach(a => console.log(` • ${a}`));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
if (data.summary) {
|
|
1697
|
+
ui.subheading('Summary');
|
|
1698
|
+
console.log(`\n ${data.summary}`);
|
|
1699
|
+
}
|
|
1700
|
+
console.log('');
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
s.fail('IP lookup failed');
|
|
1703
|
+
throw err;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async function ipTraces(args, flags) {
|
|
1708
|
+
requireAuth();
|
|
1709
|
+
const ip = args[0];
|
|
1710
|
+
if (!ip) {
|
|
1711
|
+
ui.error('IP address required. Usage: securenow ip traces <ip>');
|
|
1712
|
+
process.exit(1);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const s = ui.spinner(`Fetching traces for ${ip}`);
|
|
1716
|
+
try {
|
|
1717
|
+
const query = {};
|
|
1718
|
+
const appKey = resolveApp(flags);
|
|
1719
|
+
if (appKey) query.appKeys = appKey;
|
|
1720
|
+
const environment = resolveEnvironment(flags, null);
|
|
1721
|
+
if (environment) query.environment = environment;
|
|
1722
|
+
const data = await api.get(`/ip/${ip}/traces`, { query });
|
|
1723
|
+
const traces = data.traces || [];
|
|
1724
|
+
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
1725
|
+
|
|
1726
|
+
if (flags.json) { ui.json(data); return; }
|
|
1727
|
+
|
|
1728
|
+
console.log('');
|
|
1729
|
+
const rows = traces.map(t => [
|
|
1730
|
+
ui.c.dim(ui.truncate(t.traceID || t.traceId, 16)),
|
|
1731
|
+
t.httpMethod || t.method || '—',
|
|
1732
|
+
ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
|
|
1733
|
+
ui.truncate(t.httpUrl || t.url || '', 40),
|
|
1734
|
+
ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
|
|
1735
|
+
ui.timeAgo(t.timestamp),
|
|
1736
|
+
]);
|
|
1737
|
+
ui.table(['Trace ID', 'Method', 'Status', 'URL', 'Duration', 'Time'], rows);
|
|
1738
|
+
console.log('');
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
s.fail('Failed to fetch traces');
|
|
1741
|
+
throw err;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// ── Instances ──
|
|
1746
|
+
|
|
1747
|
+
async function instancesList(args, flags) {
|
|
1748
|
+
requireAuth();
|
|
1749
|
+
const s = ui.spinner('Fetching instances');
|
|
1750
|
+
try {
|
|
1751
|
+
const data = await api.get('/instances');
|
|
1752
|
+
const instances = data.instances || [];
|
|
1753
|
+
s.stop(`Found ${instances.length} instance${instances.length !== 1 ? 's' : ''}`);
|
|
1754
|
+
|
|
1755
|
+
if (flags.json) { ui.json(instances); return; }
|
|
1756
|
+
|
|
1757
|
+
console.log('');
|
|
1758
|
+
const rows = instances.map(inst => [
|
|
1759
|
+
ui.c.dim(ui.truncate(inst._id, 12)),
|
|
1760
|
+
inst.name || inst.host || '—',
|
|
1761
|
+
inst.host || '—',
|
|
1762
|
+
inst.port != null ? String(inst.port) : '—',
|
|
1763
|
+
inst.linkedApps != null ? String(inst.linkedApps) : '—',
|
|
1764
|
+
ui.timeAgo(inst.createdAt),
|
|
1765
|
+
]);
|
|
1766
|
+
ui.table(['ID', 'Name', 'Host', 'Port', 'Linked Apps', 'Added'], rows);
|
|
1767
|
+
console.log('');
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
s.fail('Failed to fetch instances');
|
|
1770
|
+
throw err;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
async function instancesTest(args, flags) {
|
|
1775
|
+
requireAuth();
|
|
1776
|
+
const id = args[0];
|
|
1777
|
+
if (!id) {
|
|
1778
|
+
ui.error('Instance ID required. Usage: securenow instances test <id>');
|
|
1779
|
+
process.exit(1);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const s = ui.spinner('Testing instance connection');
|
|
1783
|
+
try {
|
|
1784
|
+
const result = await api.post(`/instances/${id}/test`);
|
|
1785
|
+
if (result.success) {
|
|
1786
|
+
s.stop(`Connection successful${result.storageGb ? ` (${result.storageGb} GB storage)` : ''}`);
|
|
1787
|
+
} else {
|
|
1788
|
+
s.fail(result.message || 'Connection failed');
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (flags.json) ui.json(result);
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
s.fail('Instance test failed');
|
|
1794
|
+
throw err;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// ── Analytics ──
|
|
1799
|
+
|
|
1800
|
+
async function analytics(args, flags) {
|
|
1801
|
+
requireAuth();
|
|
1802
|
+
const s = ui.spinner('Fetching analytics');
|
|
1803
|
+
try {
|
|
1804
|
+
const query = {};
|
|
1805
|
+
const appKey = resolveApp(flags);
|
|
1806
|
+
if (flags.instance) query.instanceId = flags.instance;
|
|
1807
|
+
|
|
1808
|
+
const data = await api.get('/analytics/summary', { query });
|
|
1809
|
+
|
|
1810
|
+
s.stop('Analytics loaded');
|
|
1811
|
+
|
|
1812
|
+
if (flags.json) {
|
|
1813
|
+
ui.json(data);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
console.log('');
|
|
1818
|
+
ui.heading('Analytics Overview');
|
|
1819
|
+
console.log('');
|
|
1820
|
+
|
|
1821
|
+
const groups = ['2xx-responses', '3xx-responses', '4xx-responses', '5xx-responses', '500-errors'];
|
|
1822
|
+
const pairs = groups.map(ep => {
|
|
1823
|
+
const val = data[ep];
|
|
1824
|
+
const count = val?.meta?.count ?? '—';
|
|
1825
|
+
return [ep, String(count)];
|
|
1826
|
+
});
|
|
1827
|
+
ui.keyValue(pairs);
|
|
1828
|
+
console.log('');
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
s.fail('Failed to fetch analytics');
|
|
1831
|
+
throw err;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
module.exports = {
|
|
1836
|
+
alertRulesRoute,
|
|
1837
|
+
alertRulesList,
|
|
1838
|
+
alertRuleShow,
|
|
1839
|
+
alertRuleUpdate,
|
|
1840
|
+
alertRuleTest,
|
|
1841
|
+
alertRuleExclusions,
|
|
1842
|
+
alertChannelsList,
|
|
1843
|
+
alertHistoryList,
|
|
1844
|
+
blocklistList,
|
|
1845
|
+
blocklistAdd,
|
|
1846
|
+
blocklistRemove,
|
|
1847
|
+
blocklistApprove,
|
|
1848
|
+
blocklistReject,
|
|
1849
|
+
blocklistStats,
|
|
1850
|
+
revokeRoute,
|
|
1851
|
+
allowlistList,
|
|
1852
|
+
allowlistAdd,
|
|
1853
|
+
allowlistRemove,
|
|
1854
|
+
allowlistStats,
|
|
1855
|
+
trustedList,
|
|
1856
|
+
trustedAdd,
|
|
1857
|
+
trustedRemove,
|
|
1858
|
+
forensicsQuery,
|
|
1859
|
+
forensicsChat,
|
|
1860
|
+
forensicsLibrary,
|
|
1861
|
+
ipLookup,
|
|
1862
|
+
ipTraces,
|
|
1863
|
+
instancesList,
|
|
1864
|
+
instancesTest,
|
|
1865
|
+
analytics,
|
|
1866
|
+
};
|