securenow 8.0.3 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/security.js CHANGED
@@ -1,17 +1,17 @@
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
- }
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
15
 
16
16
  // ── Alert Rules ──
17
17
 
@@ -36,27 +36,30 @@ function ruleStatusBadge(rule) {
36
36
  /** Dispatch: list | show <id> | update <id> ... */
37
37
  async function alertRulesRoute(args, flags) {
38
38
  const sub = args[0];
39
+ if (sub === 'create' || sub === 'add') {
40
+ return alertRuleCreate(args.slice(1), flags);
41
+ }
39
42
  if (sub === 'show') {
40
43
  return alertRuleShow(args.slice(1), flags);
41
44
  }
42
- if (sub === 'update') {
43
- return alertRuleUpdate(args.slice(1), flags);
44
- }
45
- if (sub === 'test') {
46
- return alertRuleTest(args.slice(1), flags);
47
- }
48
- if (sub === 'dry-run-query' || sub === 'candidate-test') {
49
- return alertRuleCandidateTest(args.slice(1), flags);
50
- }
51
- if (sub === 'tune-query' || sub === 'query-update') {
52
- return alertRuleQueryUpdate(args.slice(1), flags);
53
- }
54
- if (sub === 'exclusions') {
55
- return alertRuleExclusions(args.slice(1), flags);
56
- }
57
- if (sub === 'list') {
58
- return alertRulesList(args.slice(1), flags);
59
- }
45
+ if (sub === 'update') {
46
+ return alertRuleUpdate(args.slice(1), flags);
47
+ }
48
+ if (sub === 'test') {
49
+ return alertRuleTest(args.slice(1), flags);
50
+ }
51
+ if (sub === 'dry-run-query' || sub === 'candidate-test') {
52
+ return alertRuleCandidateTest(args.slice(1), flags);
53
+ }
54
+ if (sub === 'tune-query' || sub === 'query-update') {
55
+ return alertRuleQueryUpdate(args.slice(1), flags);
56
+ }
57
+ if (sub === 'exclusions') {
58
+ return alertRuleExclusions(args.slice(1), flags);
59
+ }
60
+ if (sub === 'list') {
61
+ return alertRulesList(args.slice(1), flags);
62
+ }
60
63
  return alertRulesList(args, flags);
61
64
  }
62
65
 
@@ -88,6 +91,96 @@ async function alertRulesList(args, flags) {
88
91
  }
89
92
  }
90
93
 
94
+ async function alertRuleCreate(args, flags) {
95
+ requireAuth();
96
+ const name = flags.name;
97
+ const usage = 'Usage: securenow alerts rules create --name "..." --sql <sql|@file|-> (--applications-all | --apps k1,k2) '
98
+ + '[--severity critical|high|medium|low] [--schedule "*/15 * * * *"] [--throttle-minutes 15 | --no-throttle] '
99
+ + '[--description "..."] [--nlp "plain-English intent"] [--category custom] [--channel id1,id2] [--query-mapping-id <id>]';
100
+ if (!name) {
101
+ ui.error(usage);
102
+ process.exit(1);
103
+ }
104
+
105
+ const queryMappingId = flags['query-mapping-id'] || flags.queryMappingId;
106
+ const sqlQuery = readSqlArg(flags);
107
+ if (!queryMappingId && !sqlQuery) {
108
+ ui.error('Provide the detection query: --sql <sql|@file|-> (or reuse one with --query-mapping-id <id>).');
109
+ console.log(ui.c.dim(' Scope app keys in SQL with the __USER_APP_KEYS__ placeholder, e.g.'));
110
+ console.log(ui.c.dim(" ...WHERE resource_string_service$$name IN (__USER_APP_KEYS__) ..."));
111
+ process.exit(1);
112
+ }
113
+
114
+ const hasAll = flags['applications-all'] === true || flags['applications-all'] === 'true';
115
+ const appsStr = flags.apps || flags.app;
116
+ if (!hasAll && !appsStr) {
117
+ ui.error('Scope the rule with --applications-all or --apps key1,key2');
118
+ process.exit(1);
119
+ }
120
+
121
+ const body = { name };
122
+ if (flags.description) body.description = flags.description;
123
+ if (queryMappingId) body.queryMappingId = queryMappingId;
124
+ else body.sqlQuery = sqlQuery;
125
+ if (flags.nlp || flags.text) body.nlpQuery = flags.nlp || flags.text;
126
+ if (flags.category) body.category = flags.category;
127
+ if (flags.severity) body.severity = String(flags.severity).toLowerCase();
128
+ if (flags['execution-mode']) body.executionMode = flags['execution-mode'];
129
+
130
+ if (hasAll) {
131
+ body.applicationsAll = true;
132
+ body.applications = [];
133
+ } else {
134
+ body.applicationsAll = false;
135
+ body.applications = String(appsStr).split(',').map((x) => x.trim()).filter(Boolean);
136
+ if (body.applications.length === 0) {
137
+ ui.error('No application keys parsed from --apps');
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ const cron = flags.schedule || flags.cron;
143
+ if (cron) {
144
+ body.schedule = {
145
+ enabled: true,
146
+ cronExpression: cron,
147
+ description: flags['schedule-description'] || cron,
148
+ };
149
+ }
150
+ if (flags['no-throttle']) body.throttle = { enabled: false, minutes: 0 };
151
+ else if (flags['throttle-minutes']) body.throttle = { enabled: true, minutes: Number(flags['throttle-minutes']) };
152
+
153
+ const channelStr = flags.channel || flags.channels;
154
+ if (channelStr) {
155
+ body.alertChannelIds = String(channelStr).split(',').map((x) => x.trim()).filter(Boolean);
156
+ }
157
+
158
+ const s = ui.spinner('Creating alert rule');
159
+ try {
160
+ const data = await api.post('/alert-rules', body);
161
+ const r = data.alertRule || data;
162
+ s.stop('Alert rule created');
163
+ if (flags.json) { ui.json(data); return; }
164
+ console.log('');
165
+ ui.keyValue([
166
+ ['ID', r._id || r.id || '-'],
167
+ ['Name', r.name || '-'],
168
+ ['Status', r.status || '-'],
169
+ ['Mode', r.executionMode || 'scheduled'],
170
+ ['Severity', r.severity || '-'],
171
+ ['Applications', r.applicationsAll ? 'all apps' : ((r.applications || []).join(', ') || '-')],
172
+ ['Schedule', r.schedule?.enabled === false ? 'disabled' : (r.schedule?.description || r.schedule?.cronExpression || '-')],
173
+ ['Throttle', r.throttle?.enabled ? `${r.throttle.minutes} min` : 'off'],
174
+ ['Query', r.queryMappingId?.name || r.queryMappingId || '-'],
175
+ ]);
176
+ console.log('');
177
+ ui.success(`Created. View it with: securenow alerts rules show ${r._id || r.id}`);
178
+ } catch (err) {
179
+ s.fail('Failed to create alert rule');
180
+ throw err;
181
+ }
182
+ }
183
+
91
184
  async function alertRuleShow(args, flags) {
92
185
  requireAuth();
93
186
  const id = args[0];
@@ -128,7 +221,7 @@ async function alertRuleShow(args, flags) {
128
221
  }
129
222
  }
130
223
 
131
- async function alertRuleUpdate(args, flags) {
224
+ async function alertRuleUpdate(args, flags) {
132
225
  requireAuth();
133
226
  const id = args[0];
134
227
  if (!id) {
@@ -185,201 +278,201 @@ async function alertRuleUpdate(args, flags) {
185
278
  s.fail('Failed to update alert rule');
186
279
  throw err;
187
280
  }
188
- }
189
-
190
- function readSqlArg(flags) {
191
- const raw = flags.sql || flags.query || flags.file;
192
- if (!raw) return null;
193
- if (flags.file) return fs.readFileSync(flags.file, 'utf8');
194
- const text = String(raw);
195
- if (text === '-') return fs.readFileSync(0, 'utf8');
196
- if (text.startsWith('@')) return fs.readFileSync(text.slice(1), 'utf8');
197
- return text;
198
- }
199
-
200
- async function alertRuleTest(args, flags) {
201
- requireAuth();
202
- const id = args[0];
203
- if (!id) {
204
- ui.error('Usage: securenow alerts rules test <rule-id> [--app <key>] [--mode dry_run] [--wait]');
205
- process.exit(1);
206
- }
207
-
208
- const body = {};
209
- if (flags.app) body.applicationKey = flags.app;
210
- if (flags.mode) body.mode = flags.mode;
211
- const s = ui.spinner('Starting alert rule test');
212
- try {
213
- let data = await api.post(`/alert-rules/${id}/test`, body);
214
- if (flags.wait && data.testId) {
215
- s.update('Waiting for alert rule test results');
216
- for (let i = 0; i < 40; i++) {
217
- await new Promise((resolve) => setTimeout(resolve, 2000));
218
- data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
219
- if (['complete', 'failed'].includes(data.status)) break;
220
- }
221
- }
222
- s.stop('Alert rule test ready');
223
- if (flags.json) { ui.json(data); return; }
224
- console.log('');
225
- ui.keyValue([
226
- ['Test ID', data.testId || '-'],
227
- ['Mode', data.mode || body.mode || 'live'],
228
- ['Status', data.status || '-'],
229
- ['Result count', String(data.resultCount ?? '-')],
230
- ['Error', data.error || '-'],
231
- ]);
232
- console.log('');
233
- } catch (err) {
234
- s.fail('Failed to test alert rule');
235
- throw err;
236
- }
237
- }
238
-
239
- async function alertRuleCandidateTest(args, flags) {
240
- requireAuth();
241
- const id = args[0];
242
- const candidateSqlQuery = readSqlArg(flags);
243
- if (!id || !candidateSqlQuery) {
244
- ui.error('Usage: securenow alerts rules dry-run-query <rule-id> --sql <sql|@file|-> [--app <key>] [--wait]');
245
- process.exit(1);
246
- }
247
-
248
- const body = { mode: 'dry_run', candidateSqlQuery };
249
- if (flags.app) body.applicationKey = flags.app;
250
-
251
- const s = ui.spinner('Starting candidate SQL dry-run');
252
- try {
253
- let data = await api.post(`/alert-rules/${id}/test`, body);
254
- if (flags.wait && data.testId) {
255
- s.update('Waiting for candidate SQL dry-run results');
256
- for (let i = 0; i < 40; i++) {
257
- await new Promise((resolve) => setTimeout(resolve, 2000));
258
- data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
259
- if (['complete', 'failed'].includes(data.status)) break;
260
- }
261
- }
262
- s.stop('Candidate dry-run ready');
263
- if (flags.json) { ui.json(data); return; }
264
- console.log('');
265
- ui.keyValue([
266
- ['Test ID', data.testId || '-'],
267
- ['Status', data.status || '-'],
268
- ['Candidate', data.candidate ? 'yes' : 'no'],
269
- ['Current SQL hash', data.currentSqlHash || '-'],
270
- ['Candidate SQL hash', data.candidateSqlHash || '-'],
271
- ['Result count', String(data.resultCount ?? '-')],
272
- ['Error', data.error || '-'],
273
- ]);
274
- console.log('');
275
- } catch (err) {
276
- s.fail('Failed to dry-run candidate SQL');
277
- throw err;
278
- }
279
- }
280
-
281
- async function alertRuleQueryUpdate(args, flags) {
282
- requireAuth();
283
- const id = args[0];
284
- const sqlQuery = readSqlArg(flags);
285
- const reason = flags.reason;
286
- if (!id || !sqlQuery || !reason) {
287
- ui.error('Usage: securenow alerts rules tune-query <rule-id> --sql <sql|@file|-> --reason "..." --apply-globally --yes');
288
- process.exit(1);
289
- }
290
- if (!flags['apply-globally'] && flags.applyGlobally !== true) {
291
- ui.error('System query tuning requires --apply-globally.');
292
- process.exit(1);
293
- }
294
-
295
- if (!flags.force && !flags.yes) {
296
- const ok = await ui.confirm('Update the shared system query mapping for all customer copies?');
297
- if (!ok) {
298
- ui.info('Cancelled');
299
- return;
300
- }
301
- }
302
-
303
- const body = {
304
- sqlQuery,
305
- reason,
306
- applyGlobally: true,
307
- reactivatePausedCopies: !!(flags['reactivate-paused'] || flags.reactivatePausedCopies),
308
- };
309
- if (flags['expected-hash']) body.expectedCurrentSqlHash = flags['expected-hash'];
310
- if (flags.notification) body.reviewNotificationId = flags.notification;
311
- if (flags.note) body.reviewNote = flags.note;
312
-
313
- const s = ui.spinner('Updating system query mapping');
314
- try {
315
- const data = await api.put(`/alert-rules/${id}/query-mapping`, body);
316
- s.stop('System query mapping updated');
317
- if (flags.json) { ui.json(data); return; }
318
- console.log('');
319
- ui.keyValue([
320
- ['Query mapping', data.queryMapping?.name || data.queryMapping?.id || '-'],
321
- ['Previous SQL hash', data.queryMapping?.previousSqlHash || '-'],
322
- ['New SQL hash', data.queryMapping?.newSqlHash || '-'],
323
- ['Affected system rules', String(data.affectedSystemRules ?? '-')],
324
- ['Reactivated paused copies', data.reactivatedPausedCopies ? 'yes' : 'no'],
325
- ]);
326
- console.log('');
327
- } catch (err) {
328
- s.fail('Failed to update system query mapping');
329
- throw err;
330
- }
331
- }
332
-
333
- async function alertRuleExclusions(args, flags) {
334
- requireAuth();
335
- const id = args[0];
336
- const action = args[1] || 'list';
337
- if (!id) {
338
- ui.error('Usage: securenow alerts rules exclusions <rule-id> [list|add|delete] [exclusion-id]');
339
- process.exit(1);
340
- }
341
-
342
- try {
343
- if (action === 'add') {
344
- const body = {};
345
- if (flags.conditions) body.conditions = JSON.parse(flags.conditions);
346
- if (flags['match-mode']) body.matchMode = flags['match-mode'];
347
- if (flags.reason) body.reason = flags.reason;
348
- if (flags.path) body.pathPattern = flags.path;
349
- const data = await api.post(`/alert-rules/${id}/exclusions`, body);
350
- if (flags.json) { ui.json(data); return; }
351
- ui.success('Exclusion added');
352
- return;
353
- }
354
-
355
- if (action === 'delete' || action === 'remove') {
356
- const exclusionId = args[2];
357
- if (!exclusionId) {
358
- ui.error('Exclusion id required.');
359
- process.exit(1);
360
- }
361
- const data = await api.delete(`/alert-rules/${id}/exclusions/${exclusionId}`);
362
- if (flags.json) { ui.json(data); return; }
363
- ui.success('Exclusion removed');
364
- return;
365
- }
366
-
367
- const data = await api.get(`/alert-rules/${id}/exclusions`);
368
- const exclusions = data.exclusions || [];
369
- if (flags.json) { ui.json(data); return; }
370
- console.log('');
371
- const rows = exclusions.map((e) => [
372
- ui.c.dim(ui.truncate(e._id, 12)),
373
- e.isActive === false ? ui.statusBadge('disabled') : ui.statusBadge('active'),
374
- e.matchMode || 'all',
375
- ui.truncate(e.reason || e.pathPattern || JSON.stringify(e.conditions || []), 72),
376
- ]);
377
- ui.table(['ID', 'Status', 'Mode', 'Reason/Pattern'], rows);
378
- console.log('');
379
- } catch (err) {
380
- throw err;
381
- }
382
- }
281
+ }
282
+
283
+ function readSqlArg(flags) {
284
+ const raw = flags.sql || flags.query || flags.file;
285
+ if (!raw) return null;
286
+ if (flags.file) return fs.readFileSync(flags.file, 'utf8');
287
+ const text = String(raw);
288
+ if (text === '-') return fs.readFileSync(0, 'utf8');
289
+ if (text.startsWith('@')) return fs.readFileSync(text.slice(1), 'utf8');
290
+ return text;
291
+ }
292
+
293
+ async function alertRuleTest(args, flags) {
294
+ requireAuth();
295
+ const id = args[0];
296
+ if (!id) {
297
+ ui.error('Usage: securenow alerts rules test <rule-id> [--app <key>] [--mode dry_run] [--wait]');
298
+ process.exit(1);
299
+ }
300
+
301
+ const body = {};
302
+ if (flags.app) body.applicationKey = flags.app;
303
+ if (flags.mode) body.mode = flags.mode;
304
+ const s = ui.spinner('Starting alert rule test');
305
+ try {
306
+ let data = await api.post(`/alert-rules/${id}/test`, body);
307
+ if (flags.wait && data.testId) {
308
+ s.update('Waiting for alert rule test results');
309
+ for (let i = 0; i < 40; i++) {
310
+ await new Promise((resolve) => setTimeout(resolve, 2000));
311
+ data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
312
+ if (['complete', 'failed'].includes(data.status)) break;
313
+ }
314
+ }
315
+ s.stop('Alert rule test ready');
316
+ if (flags.json) { ui.json(data); return; }
317
+ console.log('');
318
+ ui.keyValue([
319
+ ['Test ID', data.testId || '-'],
320
+ ['Mode', data.mode || body.mode || 'live'],
321
+ ['Status', data.status || '-'],
322
+ ['Result count', String(data.resultCount ?? '-')],
323
+ ['Error', data.error || '-'],
324
+ ]);
325
+ console.log('');
326
+ } catch (err) {
327
+ s.fail('Failed to test alert rule');
328
+ throw err;
329
+ }
330
+ }
331
+
332
+ async function alertRuleCandidateTest(args, flags) {
333
+ requireAuth();
334
+ const id = args[0];
335
+ const candidateSqlQuery = readSqlArg(flags);
336
+ if (!id || !candidateSqlQuery) {
337
+ ui.error('Usage: securenow alerts rules dry-run-query <rule-id> --sql <sql|@file|-> [--app <key>] [--wait]');
338
+ process.exit(1);
339
+ }
340
+
341
+ const body = { mode: 'dry_run', candidateSqlQuery };
342
+ if (flags.app) body.applicationKey = flags.app;
343
+
344
+ const s = ui.spinner('Starting candidate SQL dry-run');
345
+ try {
346
+ let data = await api.post(`/alert-rules/${id}/test`, body);
347
+ if (flags.wait && data.testId) {
348
+ s.update('Waiting for candidate SQL dry-run results');
349
+ for (let i = 0; i < 40; i++) {
350
+ await new Promise((resolve) => setTimeout(resolve, 2000));
351
+ data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
352
+ if (['complete', 'failed'].includes(data.status)) break;
353
+ }
354
+ }
355
+ s.stop('Candidate dry-run ready');
356
+ if (flags.json) { ui.json(data); return; }
357
+ console.log('');
358
+ ui.keyValue([
359
+ ['Test ID', data.testId || '-'],
360
+ ['Status', data.status || '-'],
361
+ ['Candidate', data.candidate ? 'yes' : 'no'],
362
+ ['Current SQL hash', data.currentSqlHash || '-'],
363
+ ['Candidate SQL hash', data.candidateSqlHash || '-'],
364
+ ['Result count', String(data.resultCount ?? '-')],
365
+ ['Error', data.error || '-'],
366
+ ]);
367
+ console.log('');
368
+ } catch (err) {
369
+ s.fail('Failed to dry-run candidate SQL');
370
+ throw err;
371
+ }
372
+ }
373
+
374
+ async function alertRuleQueryUpdate(args, flags) {
375
+ requireAuth();
376
+ const id = args[0];
377
+ const sqlQuery = readSqlArg(flags);
378
+ const reason = flags.reason;
379
+ if (!id || !sqlQuery || !reason) {
380
+ ui.error('Usage: securenow alerts rules tune-query <rule-id> --sql <sql|@file|-> --reason "..." --apply-globally --yes');
381
+ process.exit(1);
382
+ }
383
+ if (!flags['apply-globally'] && flags.applyGlobally !== true) {
384
+ ui.error('System query tuning requires --apply-globally.');
385
+ process.exit(1);
386
+ }
387
+
388
+ if (!flags.force && !flags.yes) {
389
+ const ok = await ui.confirm('Update the shared system query mapping for all customer copies?');
390
+ if (!ok) {
391
+ ui.info('Cancelled');
392
+ return;
393
+ }
394
+ }
395
+
396
+ const body = {
397
+ sqlQuery,
398
+ reason,
399
+ applyGlobally: true,
400
+ reactivatePausedCopies: !!(flags['reactivate-paused'] || flags.reactivatePausedCopies),
401
+ };
402
+ if (flags['expected-hash']) body.expectedCurrentSqlHash = flags['expected-hash'];
403
+ if (flags.notification) body.reviewNotificationId = flags.notification;
404
+ if (flags.note) body.reviewNote = flags.note;
405
+
406
+ const s = ui.spinner('Updating system query mapping');
407
+ try {
408
+ const data = await api.put(`/alert-rules/${id}/query-mapping`, body);
409
+ s.stop('System query mapping updated');
410
+ if (flags.json) { ui.json(data); return; }
411
+ console.log('');
412
+ ui.keyValue([
413
+ ['Query mapping', data.queryMapping?.name || data.queryMapping?.id || '-'],
414
+ ['Previous SQL hash', data.queryMapping?.previousSqlHash || '-'],
415
+ ['New SQL hash', data.queryMapping?.newSqlHash || '-'],
416
+ ['Affected system rules', String(data.affectedSystemRules ?? '-')],
417
+ ['Reactivated paused copies', data.reactivatedPausedCopies ? 'yes' : 'no'],
418
+ ]);
419
+ console.log('');
420
+ } catch (err) {
421
+ s.fail('Failed to update system query mapping');
422
+ throw err;
423
+ }
424
+ }
425
+
426
+ async function alertRuleExclusions(args, flags) {
427
+ requireAuth();
428
+ const id = args[0];
429
+ const action = args[1] || 'list';
430
+ if (!id) {
431
+ ui.error('Usage: securenow alerts rules exclusions <rule-id> [list|add|delete] [exclusion-id]');
432
+ process.exit(1);
433
+ }
434
+
435
+ try {
436
+ if (action === 'add') {
437
+ const body = {};
438
+ if (flags.conditions) body.conditions = JSON.parse(flags.conditions);
439
+ if (flags['match-mode']) body.matchMode = flags['match-mode'];
440
+ if (flags.reason) body.reason = flags.reason;
441
+ if (flags.path) body.pathPattern = flags.path;
442
+ const data = await api.post(`/alert-rules/${id}/exclusions`, body);
443
+ if (flags.json) { ui.json(data); return; }
444
+ ui.success('Exclusion added');
445
+ return;
446
+ }
447
+
448
+ if (action === 'delete' || action === 'remove') {
449
+ const exclusionId = args[2];
450
+ if (!exclusionId) {
451
+ ui.error('Exclusion id required.');
452
+ process.exit(1);
453
+ }
454
+ const data = await api.delete(`/alert-rules/${id}/exclusions/${exclusionId}`);
455
+ if (flags.json) { ui.json(data); return; }
456
+ ui.success('Exclusion removed');
457
+ return;
458
+ }
459
+
460
+ const data = await api.get(`/alert-rules/${id}/exclusions`);
461
+ const exclusions = data.exclusions || [];
462
+ if (flags.json) { ui.json(data); return; }
463
+ console.log('');
464
+ const rows = exclusions.map((e) => [
465
+ ui.c.dim(ui.truncate(e._id, 12)),
466
+ e.isActive === false ? ui.statusBadge('disabled') : ui.statusBadge('active'),
467
+ e.matchMode || 'all',
468
+ ui.truncate(e.reason || e.pathPattern || JSON.stringify(e.conditions || []), 72),
469
+ ]);
470
+ ui.table(['ID', 'Status', 'Mode', 'Reason/Pattern'], rows);
471
+ console.log('');
472
+ } catch (err) {
473
+ throw err;
474
+ }
475
+ }
383
476
 
384
477
  // ── Alert Channels ──
385
478
 
@@ -441,29 +534,29 @@ async function alertHistoryList(args, flags) {
441
534
 
442
535
  // ── Blocklist ──
443
536
 
444
- function describeBlockTarget(entry) {
445
- const parts = [entry.ip || entry.cidr || '-'];
446
- if (entry.method && entry.method !== 'ALL') parts.push(entry.method);
447
- if (entry.pathPattern) parts.push(`${entry.pathMatchMode || 'prefix'}:${entry.pathPattern}`);
448
- return parts.join(' ');
449
- }
450
-
451
- async function blocklistList(args, flags) {
537
+ function describeBlockTarget(entry) {
538
+ const parts = [entry.ip || entry.cidr || '-'];
539
+ if (entry.method && entry.method !== 'ALL') parts.push(entry.method);
540
+ if (entry.pathPattern) parts.push(`${entry.pathMatchMode || 'prefix'}:${entry.pathPattern}`);
541
+ return parts.join(' ');
542
+ }
543
+
544
+ async function blocklistList(args, flags) {
452
545
  requireAuth();
453
546
  const s = ui.spinner('Fetching blocklist');
454
547
  try {
455
- const query = {};
456
- if (flags.page) query.page = flags.page;
457
- if (flags.limit) query.limit = flags.limit;
458
- if (flags.app) query.appKey = flags.app;
459
- if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
460
- if (flags.status) query.status = flags.status;
461
- if (flags.search) query.search = flags.search;
462
- if (flags.view) query.view = flags.view;
463
- if (flags.approvalStatus || flags['approval-status']) {
464
- query.approvalStatus = flags.approvalStatus || flags['approval-status'];
465
- }
466
- const data = await api.get('/blocklist', { query });
548
+ const query = {};
549
+ if (flags.page) query.page = flags.page;
550
+ if (flags.limit) query.limit = flags.limit;
551
+ if (flags.app) query.appKey = flags.app;
552
+ if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
553
+ if (flags.status) query.status = flags.status;
554
+ if (flags.search) query.search = flags.search;
555
+ if (flags.view) query.view = flags.view;
556
+ if (flags.approvalStatus || flags['approval-status']) {
557
+ query.approvalStatus = flags.approvalStatus || flags['approval-status'];
558
+ }
559
+ const data = await api.get('/blocklist', { query });
467
560
  const items = data.blockedIps || [];
468
561
  s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
469
562
 
@@ -472,17 +565,17 @@ async function blocklistList(args, flags) {
472
565
  console.log('');
473
566
  const rows = items.map(b => [
474
567
  ui.c.dim(ui.truncate(b._id, 12)),
475
- ui.c.red(describeBlockTarget(b)),
476
- ui.truncate(b.reason || '', 40),
477
- b.source || '—',
478
- b.status || 'active',
479
- b.applicationKey || ui.c.dim('all apps'),
480
- b.environment || ui.c.dim('all envs'),
481
- ui.timeAgo(b.createdAt),
482
- b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
483
- b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
484
- ]);
485
- ui.table(['ID', 'Target', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
568
+ ui.c.red(describeBlockTarget(b)),
569
+ ui.truncate(b.reason || '', 40),
570
+ b.source || '—',
571
+ b.status || 'active',
572
+ b.applicationKey || ui.c.dim('all apps'),
573
+ b.environment || ui.c.dim('all envs'),
574
+ ui.timeAgo(b.createdAt),
575
+ b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
576
+ b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
577
+ ]);
578
+ ui.table(['ID', 'Target', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
486
579
  console.log('');
487
580
  } catch (err) {
488
581
  s.fail('Failed to fetch blocklist');
@@ -498,21 +591,21 @@ async function blocklistAdd(args, flags) {
498
591
  if (!ip) { ui.error('IP is required'); process.exit(1); }
499
592
  }
500
593
 
501
- const body = { ip };
502
- if (flags.reason) body.reason = flags.reason;
503
- if (flags.duration) body.duration = flags.duration;
504
- if (flags.app) body.appKey = flags.app;
505
- if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
506
- if (flags.route || flags.path || flags.pattern) body.pathPattern = flags.route || flags.path || flags.pattern;
507
- if (flags.mode || flags['path-mode']) body.pathMatchMode = flags.mode || flags['path-mode'];
508
- if (flags.method) body.method = flags.method;
509
-
510
- const target = describeBlockTarget({ ip, method: body.method, pathPattern: body.pathPattern, pathMatchMode: body.pathMatchMode });
511
- const s = ui.spinner(`Blocking ${target}`);
512
- try {
513
- const data = await api.post('/blocklist', body);
514
- s.stop(`${target} added to blocklist`);
515
- if (flags.json) ui.json(data);
594
+ const body = { ip };
595
+ if (flags.reason) body.reason = flags.reason;
596
+ if (flags.duration) body.duration = flags.duration;
597
+ if (flags.app) body.appKey = flags.app;
598
+ if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
599
+ if (flags.route || flags.path || flags.pattern) body.pathPattern = flags.route || flags.path || flags.pattern;
600
+ if (flags.mode || flags['path-mode']) body.pathMatchMode = flags.mode || flags['path-mode'];
601
+ if (flags.method) body.method = flags.method;
602
+
603
+ const target = describeBlockTarget({ ip, method: body.method, pathPattern: body.pathPattern, pathMatchMode: body.pathMatchMode });
604
+ const s = ui.spinner(`Blocking ${target}`);
605
+ try {
606
+ const data = await api.post('/blocklist', body);
607
+ s.stop(`${target} added to blocklist`);
608
+ if (flags.json) ui.json(data);
516
609
  } catch (err) {
517
610
  s.fail('Failed to add to blocklist');
518
611
  throw err;
@@ -521,28 +614,28 @@ async function blocklistAdd(args, flags) {
521
614
 
522
615
  async function blocklistRemove(args, flags) {
523
616
  requireAuth();
524
- const id = args[0];
525
- if (!id) {
526
- ui.error('Blocklist entry ID required. Usage: securenow blocklist unblock <id> [--reason <reason>]');
527
- process.exit(1);
528
- }
529
-
530
- if (!flags.force && !flags.yes) {
531
- const ok = await ui.confirm('Unblock this IP? Firewall enforcement stops, but block report and history stay saved.');
532
- if (!ok) { ui.info('Cancelled'); return; }
533
- }
534
-
535
- const s = ui.spinner('Unblocking IP');
536
- try {
537
- const body = {};
538
- if (flags.reason) body.reason = flags.reason;
539
- await api.post(`/blocklist/${id}/unblock`, body);
540
- s.stop('Unblocked; block report and history retained');
541
- } catch (err) {
542
- s.fail('Failed to unblock IP');
543
- throw err;
544
- }
545
- }
617
+ const id = args[0];
618
+ if (!id) {
619
+ ui.error('Blocklist entry ID required. Usage: securenow blocklist unblock <id> [--reason <reason>]');
620
+ process.exit(1);
621
+ }
622
+
623
+ if (!flags.force && !flags.yes) {
624
+ const ok = await ui.confirm('Unblock this IP? Firewall enforcement stops, but block report and history stay saved.');
625
+ if (!ok) { ui.info('Cancelled'); return; }
626
+ }
627
+
628
+ const s = ui.spinner('Unblocking IP');
629
+ try {
630
+ const body = {};
631
+ if (flags.reason) body.reason = flags.reason;
632
+ await api.post(`/blocklist/${id}/unblock`, body);
633
+ s.stop('Unblocked; block report and history retained');
634
+ } catch (err) {
635
+ s.fail('Failed to unblock IP');
636
+ throw err;
637
+ }
638
+ }
546
639
 
547
640
  async function blocklistStats(args, flags) {
548
641
  requireAuth();
@@ -577,12 +670,12 @@ async function allowlistList(args, flags) {
577
670
  requireAuth();
578
671
  const s = ui.spinner('Fetching allowlist');
579
672
  try {
580
- const query = {};
581
- if (flags.page) query.page = flags.page;
582
- if (flags.limit) query.limit = flags.limit;
583
- if (flags.app) query.appKey = flags.app;
584
- if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
585
- const data = await api.get('/allowlist', { query });
673
+ const query = {};
674
+ if (flags.page) query.page = flags.page;
675
+ if (flags.limit) query.limit = flags.limit;
676
+ if (flags.app) query.appKey = flags.app;
677
+ if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
678
+ const data = await api.get('/allowlist', { query });
586
679
  const items = data.allowedIps || [];
587
680
  s.stop(`Found ${items.length} allowed IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
588
681
 
@@ -591,15 +684,15 @@ async function allowlistList(args, flags) {
591
684
  console.log('');
592
685
  const rows = items.map(a => [
593
686
  ui.c.dim(ui.truncate(a._id, 12)),
594
- ui.c.green(a.ip || '—'),
595
- a.label || ui.c.dim('—'),
596
- ui.truncate(a.reason || '', 40),
597
- a.applicationKey || ui.c.dim('all apps'),
598
- a.environment || ui.c.dim('all envs'),
599
- ui.timeAgo(a.createdAt),
600
- a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
601
- ]);
602
- ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'App', 'Env', 'Added', 'Expires'], rows);
687
+ ui.c.green(a.ip || '—'),
688
+ a.label || ui.c.dim('—'),
689
+ ui.truncate(a.reason || '', 40),
690
+ a.applicationKey || ui.c.dim('all apps'),
691
+ a.environment || ui.c.dim('all envs'),
692
+ ui.timeAgo(a.createdAt),
693
+ a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
694
+ ]);
695
+ ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'App', 'Env', 'Added', 'Expires'], rows);
603
696
  console.log('');
604
697
  } catch (err) {
605
698
  s.fail('Failed to fetch allowlist');
@@ -615,25 +708,25 @@ async function allowlistAdd(args, flags) {
615
708
  if (!ip) { ui.error('IP is required'); process.exit(1); }
616
709
  }
617
710
 
618
- const body = { ip };
619
- if (flags.label) body.label = flags.label;
620
- if (flags.reason) body.reason = flags.reason;
621
- if (flags.app) {
622
- body.applicationsAll = false;
623
- body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
624
- }
625
- if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
626
-
627
- if (!flags.force && !flags.yes) {
628
- ui.warn('IP Allowlist is deny-by-default: once active, only listed IPs can reach the scoped app/environment and all others are blocked.');
629
- ui.info('Use `securenow trusted add` instead if this IP should be trusted without locking out normal visitors.');
630
- const ok = await ui.confirm('Add this IP to the restrictive allowlist?');
631
- if (!ok) { ui.info('Cancelled'); return; }
632
- }
633
- body.allowlistDenyAllApproved = true;
634
-
635
- const s = ui.spinner(`Allowing ${ip}`);
636
- try {
711
+ const body = { ip };
712
+ if (flags.label) body.label = flags.label;
713
+ if (flags.reason) body.reason = flags.reason;
714
+ if (flags.app) {
715
+ body.applicationsAll = false;
716
+ body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
717
+ }
718
+ if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
719
+
720
+ if (!flags.force && !flags.yes) {
721
+ ui.warn('IP Allowlist is deny-by-default: once active, only listed IPs can reach the scoped app/environment and all others are blocked.');
722
+ ui.info('Use `securenow trusted add` instead if this IP should be trusted without locking out normal visitors.');
723
+ const ok = await ui.confirm('Add this IP to the restrictive allowlist?');
724
+ if (!ok) { ui.info('Cancelled'); return; }
725
+ }
726
+ body.allowlistDenyAllApproved = true;
727
+
728
+ const s = ui.spinner(`Allowing ${ip}`);
729
+ try {
637
730
  await api.post('/allowlist', body);
638
731
  s.stop(`${ip} added to allowlist`);
639
732
  } catch (err) {
@@ -695,10 +788,10 @@ async function trustedList(args, flags) {
695
788
  requireAuth();
696
789
  const s = ui.spinner('Fetching trusted IPs');
697
790
  try {
698
- const query = {};
699
- if (flags.app) query.appKey = flags.app;
700
- if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
701
- const data = await api.get('/trusted-ips', { query });
791
+ const query = {};
792
+ if (flags.app) query.appKey = flags.app;
793
+ if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
794
+ const data = await api.get('/trusted-ips', { query });
702
795
  const items = data.trustedIps || [];
703
796
  s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
704
797
 
@@ -706,14 +799,14 @@ async function trustedList(args, flags) {
706
799
 
707
800
  console.log('');
708
801
  const rows = items.map(t => [
709
- ui.c.dim(ui.truncate(t._id, 12)),
710
- ui.c.green(t.ip || t.cidr || '—'),
711
- t.label || t.description || ui.c.dim('—'),
712
- t.applicationKey || ui.c.dim('all apps'),
713
- t.environment || ui.c.dim('all envs'),
714
- ui.timeAgo(t.createdAt),
715
- ]);
716
- ui.table(['ID', 'IP/CIDR', 'Label', 'App', 'Env', 'Added'], rows);
802
+ ui.c.dim(ui.truncate(t._id, 12)),
803
+ ui.c.green(t.ip || t.cidr || '—'),
804
+ t.label || t.description || ui.c.dim('—'),
805
+ t.applicationKey || ui.c.dim('all apps'),
806
+ t.environment || ui.c.dim('all envs'),
807
+ ui.timeAgo(t.createdAt),
808
+ ]);
809
+ ui.table(['ID', 'IP/CIDR', 'Label', 'App', 'Env', 'Added'], rows);
717
810
  console.log('');
718
811
  } catch (err) {
719
812
  s.fail('Failed to fetch trusted IPs');
@@ -729,15 +822,15 @@ async function trustedAdd(args, flags) {
729
822
  if (!ip) { ui.error('IP is required'); process.exit(1); }
730
823
  }
731
824
 
732
- const body = { ip };
733
- if (flags.label) body.label = flags.label;
734
- if (flags.description) body.description = flags.description;
735
- if (flags.note) body.note = flags.note;
736
- if (flags.app) {
737
- body.applicationsAll = false;
738
- body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
739
- }
740
- if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
825
+ const body = { ip };
826
+ if (flags.label) body.label = flags.label;
827
+ if (flags.description) body.description = flags.description;
828
+ if (flags.note) body.note = flags.note;
829
+ if (flags.app) {
830
+ body.applicationsAll = false;
831
+ body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
832
+ }
833
+ if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
741
834
 
742
835
  const s = ui.spinner(`Adding ${ip} to trusted IPs`);
743
836
  try {
@@ -795,7 +888,7 @@ async function forensicsQuery(args, flags) {
795
888
 
796
889
  const s = ui.spinner('Submitting forensic query');
797
890
  try {
798
- const body = { query, environment: resolveEnvironment(flags, 'production') };
891
+ const body = { query, environment: resolveEnvironment(flags, 'production') };
799
892
  const resolved = await resolveAppId(flags);
800
893
  if (resolved) {
801
894
  body.applicationId = resolved.id;
@@ -922,10 +1015,10 @@ async function forensicsChat(args, flags) {
922
1015
  console.log(ui.c.dim(` App: ${resolved.key} (${resolved.id})`));
923
1016
  console.log(ui.c.dim(' Type your question, or "exit" to quit.\n'));
924
1017
 
925
- const environment = resolveEnvironment(flags, 'production');
926
- console.log(ui.c.dim(` Env: ${environment}`));
927
-
928
- let conversationId = null;
1018
+ const environment = resolveEnvironment(flags, 'production');
1019
+ console.log(ui.c.dim(` Env: ${environment}`));
1020
+
1021
+ let conversationId = null;
929
1022
 
930
1023
  const readline = require('readline');
931
1024
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
@@ -945,7 +1038,7 @@ async function forensicsChat(args, flags) {
945
1038
 
946
1039
  const s = ui.spinner('Thinking');
947
1040
  try {
948
- const body = { message, applicationKey: resolved.key, environment };
1041
+ const body = { message, applicationKey: resolved.key, environment };
949
1042
  if (conversationId) body.conversationId = conversationId;
950
1043
 
951
1044
  const chatRes = await api.post('/forensics/chat', body);
@@ -955,7 +1048,7 @@ async function forensicsChat(args, flags) {
955
1048
  let result;
956
1049
  for (let i = 0; i < 150; i++) {
957
1050
  await new Promise(r => setTimeout(r, 2000));
958
- result = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
1051
+ result = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
959
1052
  if (result.status === 'complete' || result.status === 'failed' || result.status === 'awaiting_confirmation') break;
960
1053
  const progress = result._progress;
961
1054
  if (progress?.action) s.update(progress.action);
@@ -973,11 +1066,11 @@ async function forensicsChat(args, flags) {
973
1066
  const proceed = await ui.confirm('Proceed with this query?');
974
1067
  if (proceed) {
975
1068
  const cs = ui.spinner('Executing query');
976
- await api.post(`/forensics/chat/confirm/${conversationId}`, { environment });
1069
+ await api.post(`/forensics/chat/confirm/${conversationId}`, { environment });
977
1070
  let confirmResult;
978
1071
  for (let i = 0; i < 90; i++) {
979
1072
  await new Promise(r => setTimeout(r, 2000));
980
- confirmResult = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
1073
+ confirmResult = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
981
1074
  if (confirmResult.status !== 'processing') break;
982
1075
  }
983
1076
  cs.stop('Done');
@@ -1067,7 +1160,7 @@ async function ipLookup(args, flags) {
1067
1160
  if (data.domain) pairs.push(['Domain', data.domain]);
1068
1161
  if (data.isp) pairs.push(['ISP', data.isp]);
1069
1162
  if (data.usageType) pairs.push(['Usage Type', data.usageType]);
1070
- if (data.abuseConfidenceScore != null) pairs.push(['SecureNow IPDB Score', `${data.abuseConfidenceScore}/100`]);
1163
+ if (data.abuseConfidenceScore != null) pairs.push(['SecureNow IPDB Score', `${data.abuseConfidenceScore}/100`]);
1071
1164
  if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
1072
1165
  if (data.verdict) pairs.push(['Verdict', data.verdict]);
1073
1166
  if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
@@ -1111,14 +1204,14 @@ async function ipTraces(args, flags) {
1111
1204
  process.exit(1);
1112
1205
  }
1113
1206
 
1114
- const s = ui.spinner(`Fetching traces for ${ip}`);
1115
- try {
1116
- const query = {};
1117
- const appKey = resolveApp(flags);
1118
- if (appKey) query.appKeys = appKey;
1119
- const environment = resolveEnvironment(flags, null);
1120
- if (environment) query.environment = environment;
1121
- const data = await api.get(`/ip/${ip}/traces`, { query });
1207
+ const s = ui.spinner(`Fetching traces for ${ip}`);
1208
+ try {
1209
+ const query = {};
1210
+ const appKey = resolveApp(flags);
1211
+ if (appKey) query.appKeys = appKey;
1212
+ const environment = resolveEnvironment(flags, null);
1213
+ if (environment) query.environment = environment;
1214
+ const data = await api.get(`/ip/${ip}/traces`, { query });
1122
1215
  const traces = data.traces || [];
1123
1216
  s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
1124
1217
 
@@ -1233,11 +1326,11 @@ async function analytics(args, flags) {
1233
1326
 
1234
1327
  module.exports = {
1235
1328
  alertRulesRoute,
1236
- alertRulesList,
1237
- alertRuleShow,
1238
- alertRuleUpdate,
1239
- alertRuleTest,
1240
- alertRuleExclusions,
1329
+ alertRulesList,
1330
+ alertRuleShow,
1331
+ alertRuleUpdate,
1332
+ alertRuleTest,
1333
+ alertRuleExclusions,
1241
1334
  alertChannelsList,
1242
1335
  alertHistoryList,
1243
1336
  blocklistList,