securenow 7.6.9 → 7.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,7 +40,7 @@ function buildRuntimeCredentials(options = {}) {
40
40
  },
41
41
  _securenow: {
42
42
  ...(creds._securenow || {}),
43
- note: 'Runtime SecureNow credentials and SDK defaults. Mount or copy this JSON as .securenow/credentials.json in production. Do not commit it.',
43
+ note: 'Runtime SecureNow credentials and SDK defaults. Mount or copy this JSON as .securenow/credentials.json or .securenow/credentials.<environment>.json in production. Do not commit it.',
44
44
  runtimeOnly: 'This file intentionally omits CLI OAuth fields: token, email, and expiresAt.',
45
45
  production: 'Production can use this same file shape instead of environment variables.',
46
46
  },
@@ -83,6 +83,7 @@ async function runtime(_args, flags) {
83
83
 
84
84
  ui.success(`Wrote runtime credentials to ${output}`);
85
85
  ui.info('Deploy this JSON as .securenow/credentials.json on the server/container.');
86
+ ui.info('SDK v7.7.1+ can also read this generated filename directly when credentials.json is absent.');
86
87
  ui.info(`Environment: ${envName}`);
87
88
  ui.info(`App: ${creds.app?.name || '(unnamed)'} ${creds.app?.key ? `(${creds.app.key})` : ''}`);
88
89
  ui.info(`Firewall key: ${creds.apiKey ? maskSecret(creds.apiKey) : '(missing)'}`);
package/cli/firewall.js CHANGED
@@ -7,13 +7,16 @@ function resolveEnvironment(flags) {
7
7
  return flags.env || flags.environment || 'production';
8
8
  }
9
9
 
10
- async function status(args, flags) {
11
- requireAuth();
12
- const s = ui.spinner('Checking firewall status');
13
-
14
- try {
10
+ async function status(args, flags) {
11
+ requireAuth();
12
+ const s = ui.spinner('Checking firewall status');
13
+
14
+ try {
15
15
  const environment = resolveEnvironment(flags);
16
- const data = await api.get('/firewall/status', { query: { environment } });
16
+ const appKey = await resolveAppKey(flags);
17
+ const query = { environment };
18
+ if (appKey) query.appKey = appKey;
19
+ const data = await api.get('/firewall/status', { query });
17
20
 
18
21
  s.stop('Firewall status retrieved');
19
22
 
@@ -23,10 +26,14 @@ async function status(args, flags) {
23
26
  }
24
27
 
25
28
  console.log('');
26
- console.log(` ${ui.c.bold(ui.c.green('Firewall: ENABLED'))}`);
29
+ const enabledLabel = data.firewallEnabled === false
30
+ ? ui.c.bold(ui.c.yellow('Firewall: DISABLED FOR APP/ENV'))
31
+ : ui.c.bold(ui.c.green('Firewall: ENABLED'));
32
+ console.log(` ${enabledLabel}`);
27
33
  console.log('');
28
34
  ui.keyValue([
29
35
  ['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
36
+ ['App scope', appKey || 'all apps'],
30
37
  ['Environment', data.environment || environment],
31
38
  ['Last updated', data.updatedAt || 'unknown'],
32
39
  ['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
@@ -66,7 +73,10 @@ async function testIp(args, flags) {
66
73
 
67
74
  try {
68
75
  const environment = resolveEnvironment(flags);
69
- const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`, { query: { environment } });
76
+ const appKey = await resolveAppKey(flags);
77
+ const query = { environment };
78
+ if (appKey) query.appKey = appKey;
79
+ const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`, { query });
70
80
 
71
81
  s.stop(`IP ${ip} checked`);
72
82
 
@@ -75,8 +85,14 @@ async function testIp(args, flags) {
75
85
  return;
76
86
  }
77
87
 
78
- console.log('');
79
- if (data.blocked) {
88
+ console.log('');
89
+ if (data.firewallEnabled === false) {
90
+ console.log(` ${ui.c.bold(ui.c.yellow('NOT ENFORCED'))} — firewall is disabled for ${appKey || 'this app'} (${data.environment || environment})`);
91
+ if (data.reason) console.log(` ${ui.c.dim(`Reason: ${data.reason}`)}`);
92
+ console.log('');
93
+ return;
94
+ }
95
+ if (data.blocked) {
80
96
  if (data.allowlistActive && !data.allowlisted) {
81
97
  console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is not on the allowlist`);
82
98
  console.log(` ${ui.c.dim('Allowlist is active — only listed IPs are permitted')}`);
@@ -92,8 +108,9 @@ async function testIp(args, flags) {
92
108
  } else {
93
109
  console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is not in the blocklist`);
94
110
  }
95
- }
96
- console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries`)}`);
111
+ }
112
+ console.log(` ${ui.c.dim(`App scope: ${appKey || 'all apps'} · Environment: ${data.environment || environment}`)}`);
113
+ console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries`)}`);
97
114
  if (data.allowlistActive) {
98
115
  console.log(` ${ui.c.dim('Allowlist is active')}`);
99
116
  }
package/cli/human.js CHANGED
@@ -214,6 +214,40 @@ async function show(args, flags) {
214
214
  const s = ui.spinner('Fetching human action detail');
215
215
  try {
216
216
  const { task, row } = await resolveTask(ref, flags);
217
+ if (task.kind === 'case_action' || task.actionKey) {
218
+ const [notification, agentCase] = await Promise.all([
219
+ api.get(`/notifications/${encodeURIComponent(task.notificationId)}`).catch(() => ({})),
220
+ api.get(`/notifications/${encodeURIComponent(task.notificationId)}/agent-case`).catch(() => ({})),
221
+ ]);
222
+ s.stop('Case action loaded');
223
+ if (flags.json) {
224
+ ui.json({ task, notification, agentCase });
225
+ return;
226
+ }
227
+ console.log('');
228
+ printTaskSummary(task, row);
229
+ console.log('');
230
+ ui.subheading('Case action');
231
+ ui.keyValue([
232
+ ['Action key', task.actionKey || '-'],
233
+ ['Type', task.actionType || task.action?.type || '-'],
234
+ ['Title', task.action?.title || task.title || '-'],
235
+ ['Description', ui.truncate(task.action?.description || '', 140)],
236
+ ['Status', task.action?.status || 'proposed'],
237
+ ]);
238
+ if ((agentCase.proposedActions || []).length) {
239
+ console.log('');
240
+ const rows = agentCase.proposedActions.map((action) => [
241
+ action.actionKey || '-',
242
+ action.type || '-',
243
+ action.status || '-',
244
+ ui.truncate(action.title || action.description || '', 80),
245
+ ]);
246
+ ui.table(['Key', 'Type', 'Status', 'Action'], rows);
247
+ }
248
+ console.log('');
249
+ return;
250
+ }
217
251
  const [ipReport, notification] = await Promise.all([
218
252
  api.get(`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}`),
219
253
  api.get(`/notifications/${encodeURIComponent(task.notificationId)}`).catch(() => ({})),
@@ -248,6 +282,63 @@ async function show(args, flags) {
248
282
  }
249
283
  }
250
284
 
285
+ async function action(args, flags) {
286
+ requireAuth();
287
+ const ref = args[0];
288
+ if (!ref) {
289
+ ui.error('Usage: securenow human action <row|notificationId> [actionKey] --status approved|rejected|executed|failed --yes --reason "..."');
290
+ process.exit(1);
291
+ }
292
+
293
+ const parsed = parseTaskRef(ref);
294
+ let notificationId = parsed?.notificationId;
295
+ let actionKey = flags.actionKey || flags['action-key'] || args[1];
296
+ let task = null;
297
+ let row = null;
298
+
299
+ if (parsed?.kind === 'row' || !actionKey) {
300
+ const resolved = await resolveTask(ref, flags);
301
+ task = resolved.task;
302
+ row = resolved.row;
303
+ notificationId = task.notificationId;
304
+ actionKey = actionKey || task.actionKey;
305
+ }
306
+
307
+ const status = flags.status || args[2];
308
+ if (!notificationId || !actionKey || !status) {
309
+ ui.error('notificationId, actionKey, and --status are required.');
310
+ process.exit(1);
311
+ }
312
+
313
+ if (!['proposed', 'approved', 'rejected', 'executed', 'failed'].includes(status)) {
314
+ ui.error('Status must be one of: proposed, approved, rejected, executed, failed');
315
+ process.exit(1);
316
+ }
317
+
318
+ if (!flags.yes && !flags.force) {
319
+ const label = task ? `row ${row || ''} (${actionKey})` : `${notificationId}/${actionKey}`;
320
+ const ok = await ui.confirm(`Set case action ${label} to ${status}?`);
321
+ if (!ok) { ui.info('Cancelled'); return; }
322
+ }
323
+
324
+ const result = flags.result ? JSON.parse(flags.result) : {};
325
+ if (flags.reason) result.reason = flags.reason;
326
+
327
+ const s = ui.spinner('Updating case action');
328
+ try {
329
+ const data = await api.put(
330
+ `/notifications/${encodeURIComponent(notificationId)}/agent-case/actions/${encodeURIComponent(actionKey)}`,
331
+ { status, result }
332
+ );
333
+ s.stop('Case action updated');
334
+ if (flags.json) { ui.json(data); return; }
335
+ ui.success(`${actionKey} -> ${data.action?.status || status}`);
336
+ } catch (err) {
337
+ s.fail('Failed to update case action');
338
+ throw err;
339
+ }
340
+ }
341
+
251
342
  async function block(args, flags) {
252
343
  requireAuth();
253
344
  const ref = args[0];
@@ -328,10 +419,12 @@ function mcpPromptText(ref, flags = {}) {
328
419
  : '2. For each task, call securenow_notifications_get and securenow_human_action_report for the notificationId and IP.',
329
420
  '3. Read the AI report, finalDecision, DAG steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
330
421
  '4. Open/inspect trace evidence with securenow_traces_show and securenow_logs_for_trace when trace IDs are available.',
331
- '5. Decide one of only two outcomes: block the IP, or mark a scoped false positive. If evidence is ambiguous, report why and stop for that row.',
422
+ '5. Decide one outcome: block the IP, mark a scoped false positive, recommend alert-rule tuning, or skip if evidence is ambiguous.',
332
423
  '6. For block decisions, call securenow_human_action_block with confirm:true and a precise reason.',
333
424
  '7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, and a precise reason.',
334
- '8. Summarize each row handled, skipped, and still waiting. Do not globally trust an IP by default.',
425
+ '8. For case-level tune_rule/create_exclusion rows, inspect the notification case and use securenow_human_case_action_update only when the proposed action is safe.',
426
+ '9. If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
427
+ '10. Summarize each row handled, skipped, rule tuning needed, and still waiting. Do not globally trust an IP by default.',
335
428
  '',
336
429
  'Safety:',
337
430
  '- Do not call write tools without confirm:true and a reason.',
@@ -359,6 +452,7 @@ async function work(args, flags) {
359
452
  module.exports = {
360
453
  list,
361
454
  show,
455
+ action,
362
456
  block,
363
457
  fp,
364
458
  prompt,
package/cli/security.js CHANGED
@@ -38,12 +38,18 @@ async function alertRulesRoute(args, flags) {
38
38
  if (sub === 'show') {
39
39
  return alertRuleShow(args.slice(1), flags);
40
40
  }
41
- if (sub === 'update') {
42
- return alertRuleUpdate(args.slice(1), flags);
43
- }
44
- if (sub === 'list') {
45
- return alertRulesList(args.slice(1), flags);
46
- }
41
+ if (sub === 'update') {
42
+ return alertRuleUpdate(args.slice(1), flags);
43
+ }
44
+ if (sub === 'test') {
45
+ return alertRuleTest(args.slice(1), flags);
46
+ }
47
+ if (sub === 'exclusions') {
48
+ return alertRuleExclusions(args.slice(1), flags);
49
+ }
50
+ if (sub === 'list') {
51
+ return alertRulesList(args.slice(1), flags);
52
+ }
47
53
  return alertRulesList(args, flags);
48
54
  }
49
55
 
@@ -115,7 +121,7 @@ async function alertRuleShow(args, flags) {
115
121
  }
116
122
  }
117
123
 
118
- async function alertRuleUpdate(args, flags) {
124
+ async function alertRuleUpdate(args, flags) {
119
125
  requireAuth();
120
126
  const id = args[0];
121
127
  if (!id) {
@@ -172,7 +178,97 @@ async function alertRuleUpdate(args, flags) {
172
178
  s.fail('Failed to update alert rule');
173
179
  throw err;
174
180
  }
175
- }
181
+ }
182
+
183
+ async function alertRuleTest(args, flags) {
184
+ requireAuth();
185
+ const id = args[0];
186
+ if (!id) {
187
+ ui.error('Usage: securenow alerts rules test <rule-id> [--app <key>] [--mode dry_run] [--wait]');
188
+ process.exit(1);
189
+ }
190
+
191
+ const body = {};
192
+ if (flags.app) body.applicationKey = flags.app;
193
+ if (flags.mode) body.mode = flags.mode;
194
+ const s = ui.spinner('Starting alert rule test');
195
+ try {
196
+ let data = await api.post(`/alert-rules/${id}/test`, body);
197
+ if (flags.wait && data.testId) {
198
+ s.update('Waiting for alert rule test results');
199
+ for (let i = 0; i < 40; i++) {
200
+ await new Promise((resolve) => setTimeout(resolve, 2000));
201
+ data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
202
+ if (['complete', 'failed'].includes(data.status)) break;
203
+ }
204
+ }
205
+ s.stop('Alert rule test ready');
206
+ if (flags.json) { ui.json(data); return; }
207
+ console.log('');
208
+ ui.keyValue([
209
+ ['Test ID', data.testId || '-'],
210
+ ['Mode', data.mode || body.mode || 'live'],
211
+ ['Status', data.status || '-'],
212
+ ['Result count', String(data.resultCount ?? '-')],
213
+ ['Error', data.error || '-'],
214
+ ]);
215
+ console.log('');
216
+ } catch (err) {
217
+ s.fail('Failed to test alert rule');
218
+ throw err;
219
+ }
220
+ }
221
+
222
+ async function alertRuleExclusions(args, flags) {
223
+ requireAuth();
224
+ const id = args[0];
225
+ const action = args[1] || 'list';
226
+ if (!id) {
227
+ ui.error('Usage: securenow alerts rules exclusions <rule-id> [list|add|delete] [exclusion-id]');
228
+ process.exit(1);
229
+ }
230
+
231
+ try {
232
+ if (action === 'add') {
233
+ const body = {};
234
+ if (flags.conditions) body.conditions = JSON.parse(flags.conditions);
235
+ if (flags['match-mode']) body.matchMode = flags['match-mode'];
236
+ if (flags.reason) body.reason = flags.reason;
237
+ if (flags.path) body.pathPattern = flags.path;
238
+ const data = await api.post(`/alert-rules/${id}/exclusions`, body);
239
+ if (flags.json) { ui.json(data); return; }
240
+ ui.success('Exclusion added');
241
+ return;
242
+ }
243
+
244
+ if (action === 'delete' || action === 'remove') {
245
+ const exclusionId = args[2];
246
+ if (!exclusionId) {
247
+ ui.error('Exclusion id required.');
248
+ process.exit(1);
249
+ }
250
+ const data = await api.delete(`/alert-rules/${id}/exclusions/${exclusionId}`);
251
+ if (flags.json) { ui.json(data); return; }
252
+ ui.success('Exclusion removed');
253
+ return;
254
+ }
255
+
256
+ const data = await api.get(`/alert-rules/${id}/exclusions`);
257
+ const exclusions = data.exclusions || [];
258
+ if (flags.json) { ui.json(data); return; }
259
+ console.log('');
260
+ const rows = exclusions.map((e) => [
261
+ ui.c.dim(ui.truncate(e._id, 12)),
262
+ e.isActive === false ? ui.statusBadge('disabled') : ui.statusBadge('active'),
263
+ e.matchMode || 'all',
264
+ ui.truncate(e.reason || e.pathPattern || JSON.stringify(e.conditions || []), 72),
265
+ ]);
266
+ ui.table(['ID', 'Status', 'Mode', 'Reason/Pattern'], rows);
267
+ console.log('');
268
+ } catch (err) {
269
+ throw err;
270
+ }
271
+ }
176
272
 
177
273
  // ── Alert Channels ──
178
274
 
@@ -238,7 +334,12 @@ async function blocklistList(args, flags) {
238
334
  requireAuth();
239
335
  const s = ui.spinner('Fetching blocklist');
240
336
  try {
241
- const data = await api.get('/blocklist');
337
+ const query = {};
338
+ if (flags.page) query.page = flags.page;
339
+ if (flags.limit) query.limit = flags.limit;
340
+ if (flags.app) query.appKey = flags.app;
341
+ if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
342
+ const data = await api.get('/blocklist', { query });
242
343
  const items = data.blockedIps || [];
243
344
  s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
244
345
 
@@ -249,11 +350,13 @@ async function blocklistList(args, flags) {
249
350
  ui.c.dim(ui.truncate(b._id, 12)),
250
351
  ui.c.red(b.ip || b.cidr || '—'),
251
352
  ui.truncate(b.reason || '', 40),
252
- b.source || '—',
253
- ui.timeAgo(b.createdAt),
254
- b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
255
- ]);
256
- ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'Added', 'Expires'], rows);
353
+ b.source || '—',
354
+ b.applicationKey || ui.c.dim('all apps'),
355
+ b.environment || ui.c.dim('all envs'),
356
+ ui.timeAgo(b.createdAt),
357
+ b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
358
+ ]);
359
+ ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'App', 'Env', 'Added', 'Expires'], rows);
257
360
  console.log('');
258
361
  } catch (err) {
259
362
  s.fail('Failed to fetch blocklist');
@@ -269,10 +372,11 @@ async function blocklistAdd(args, flags) {
269
372
  if (!ip) { ui.error('IP is required'); process.exit(1); }
270
373
  }
271
374
 
272
- const body = { ip };
273
- if (flags.reason) body.reason = flags.reason;
274
- if (flags.duration) body.duration = flags.duration;
275
- if (flags.app) body.serviceName = flags.app;
375
+ const body = { ip };
376
+ if (flags.reason) body.reason = flags.reason;
377
+ if (flags.duration) body.duration = flags.duration;
378
+ if (flags.app) body.appKey = flags.app;
379
+ if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
276
380
 
277
381
  const s = ui.spinner(`Blocking ${ip}`);
278
382
  try {
@@ -340,7 +444,12 @@ async function allowlistList(args, flags) {
340
444
  requireAuth();
341
445
  const s = ui.spinner('Fetching allowlist');
342
446
  try {
343
- const data = await api.get('/allowlist');
447
+ const query = {};
448
+ if (flags.page) query.page = flags.page;
449
+ if (flags.limit) query.limit = flags.limit;
450
+ if (flags.app) query.appKey = flags.app;
451
+ if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
452
+ const data = await api.get('/allowlist', { query });
344
453
  const items = data.allowedIps || [];
345
454
  s.stop(`Found ${items.length} allowed IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
346
455
 
@@ -349,13 +458,15 @@ async function allowlistList(args, flags) {
349
458
  console.log('');
350
459
  const rows = items.map(a => [
351
460
  ui.c.dim(ui.truncate(a._id, 12)),
352
- ui.c.green(a.ip || '—'),
353
- a.label || ui.c.dim('—'),
354
- ui.truncate(a.reason || '', 40),
355
- ui.timeAgo(a.createdAt),
356
- a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
357
- ]);
358
- ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'Added', 'Expires'], rows);
461
+ ui.c.green(a.ip || '—'),
462
+ a.label || ui.c.dim('—'),
463
+ ui.truncate(a.reason || '', 40),
464
+ a.applicationKey || ui.c.dim('all apps'),
465
+ a.environment || ui.c.dim('all envs'),
466
+ ui.timeAgo(a.createdAt),
467
+ a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
468
+ ]);
469
+ ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'App', 'Env', 'Added', 'Expires'], rows);
359
470
  console.log('');
360
471
  } catch (err) {
361
472
  s.fail('Failed to fetch allowlist');
@@ -371,9 +482,14 @@ async function allowlistAdd(args, flags) {
371
482
  if (!ip) { ui.error('IP is required'); process.exit(1); }
372
483
  }
373
484
 
374
- const body = { ip };
375
- if (flags.label) body.label = flags.label;
376
- if (flags.reason) body.reason = flags.reason;
485
+ const body = { ip };
486
+ if (flags.label) body.label = flags.label;
487
+ if (flags.reason) body.reason = flags.reason;
488
+ if (flags.app) {
489
+ body.applicationsAll = false;
490
+ body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
491
+ }
492
+ if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
377
493
 
378
494
  const s = ui.spinner(`Allowing ${ip}`);
379
495
  try {
@@ -438,7 +554,10 @@ async function trustedList(args, flags) {
438
554
  requireAuth();
439
555
  const s = ui.spinner('Fetching trusted IPs');
440
556
  try {
441
- const data = await api.get('/trusted-ips');
557
+ const query = {};
558
+ if (flags.app) query.appKey = flags.app;
559
+ if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
560
+ const data = await api.get('/trusted-ips', { query });
442
561
  const items = data.trustedIps || [];
443
562
  s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
444
563
 
@@ -446,12 +565,14 @@ async function trustedList(args, flags) {
446
565
 
447
566
  console.log('');
448
567
  const rows = items.map(t => [
449
- ui.c.dim(ui.truncate(t._id, 12)),
450
- ui.c.green(t.ip || t.cidr || '—'),
451
- t.label || t.description || ui.c.dim('—'),
452
- ui.timeAgo(t.createdAt),
453
- ]);
454
- ui.table(['ID', 'IP/CIDR', 'Label', 'Added'], rows);
568
+ ui.c.dim(ui.truncate(t._id, 12)),
569
+ ui.c.green(t.ip || t.cidr || '—'),
570
+ t.label || t.description || ui.c.dim('—'),
571
+ t.applicationKey || ui.c.dim('all apps'),
572
+ t.environment || ui.c.dim('all envs'),
573
+ ui.timeAgo(t.createdAt),
574
+ ]);
575
+ ui.table(['ID', 'IP/CIDR', 'Label', 'App', 'Env', 'Added'], rows);
455
576
  console.log('');
456
577
  } catch (err) {
457
578
  s.fail('Failed to fetch trusted IPs');
@@ -467,9 +588,15 @@ async function trustedAdd(args, flags) {
467
588
  if (!ip) { ui.error('IP is required'); process.exit(1); }
468
589
  }
469
590
 
470
- const body = { ip };
471
- if (flags.label) body.label = flags.label;
472
- if (flags.description) body.description = flags.description;
591
+ const body = { ip };
592
+ if (flags.label) body.label = flags.label;
593
+ if (flags.description) body.description = flags.description;
594
+ if (flags.note) body.note = flags.note;
595
+ if (flags.app) {
596
+ body.applicationsAll = false;
597
+ body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
598
+ }
599
+ if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
473
600
 
474
601
  const s = ui.spinner(`Adding ${ip} to trusted IPs`);
475
602
  try {
@@ -965,9 +1092,11 @@ async function analytics(args, flags) {
965
1092
 
966
1093
  module.exports = {
967
1094
  alertRulesRoute,
968
- alertRulesList,
969
- alertRuleShow,
970
- alertRuleUpdate,
1095
+ alertRulesList,
1096
+ alertRuleShow,
1097
+ alertRuleUpdate,
1098
+ alertRuleTest,
1099
+ alertRuleExclusions,
971
1100
  alertChannelsList,
972
1101
  alertHistoryList,
973
1102
  blocklistList,