securenow 7.6.8 → 7.7.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
@@ -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,
package/cli.js CHANGED
@@ -164,7 +164,7 @@ const COMMANDS = {
164
164
  },
165
165
  human: {
166
166
  desc: 'Work the human action queue prepared by SecureNow AI',
167
- usage: 'securenow human <list|show|block|fp|prompt|work> [row|notificationId:ip] [options]',
167
+ usage: 'securenow human <list|show|block|fp|action|prompt|work> [row|notificationId:ip] [options]',
168
168
  flags: {
169
169
  json: 'Output as JSON',
170
170
  page: 'Queue page number',
@@ -178,6 +178,9 @@ const COMMANDS = {
178
178
  'rule-scope': 'False-positive scope: this_rule | specific_rules | all_existing | any_rule',
179
179
  'create-exclusion': 'Create a restrictive exclusion when marking false positive',
180
180
  'apply-existing': 'Apply false-positive decision to existing matching rows',
181
+ status: 'Case action status: approved, rejected, executed, failed, or proposed',
182
+ 'action-key': 'Case-level proposed action key',
183
+ result: 'Case action result JSON',
181
184
  mode: 'Prompt mode label for MCP prompt output',
182
185
  },
183
186
  sub: {
@@ -185,6 +188,7 @@ const COMMANDS = {
185
188
  show: { desc: 'Show one row with AI report, proofs, DAG, and trace links', usage: 'securenow human show <row|notificationId:ip>', run: (a, f) => require('./cli/human').show(a, f) },
186
189
  block: { desc: 'Approve the AI block recommendation for a row', usage: 'securenow human block <row|notificationId:ip> --yes --reason "..."', run: (a, f) => require('./cli/human').block(a, f) },
187
190
  fp: { desc: 'Mark a row as a scoped false positive', usage: 'securenow human fp <row|notificationId:ip> --yes --reason "..."', run: (a, f) => require('./cli/human').fp(a, f) },
191
+ action: { desc: 'Approve/reject/execute a case-level proposed action', usage: 'securenow human action <row|notificationId> [actionKey] --status approved --yes --reason "..."', run: (a, f) => require('./cli/human').action(a, f) },
188
192
  prompt: { desc: 'Print a Codex/Claude MCP prompt for row or queue work', usage: 'securenow human prompt [row|notificationId:ip] [--limit 10]', run: (a, f) => require('./cli/human').prompt(a, f) },
189
193
  work: { desc: 'List the queue and print the MCP runbook to work it deeply', usage: 'securenow human work [--limit 10]', run: (a, f) => require('./cli/human').work(a, f) },
190
194
  },
@@ -226,47 +230,86 @@ const COMMANDS = {
226
230
  },
227
231
  defaultSub: 'list',
228
232
  },
229
- firewall: {
230
- desc: 'Firewall status, per-app toggle, and IP testing',
231
- usage: 'securenow firewall <subcommand> [options]',
232
- flags: { app: 'App key (defaults to logged-in app)', json: 'Output as JSON' },
233
+ firewall: {
234
+ desc: 'Firewall status, per-app toggle, and IP testing',
235
+ usage: 'securenow firewall <subcommand> [options]',
236
+ flags: { app: 'App key (defaults to logged-in app)', json: 'Output as JSON' },
233
237
  sub: {
234
238
  status: { desc: 'Show firewall status, layers, and blocklist info', flags: { env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').status(a, f) },
235
239
  apps: { desc: 'List apps with their firewall on/off state', run: (a, f) => require('./cli/firewall').appsList(a, f) },
236
240
  enable: { desc: 'Turn the firewall ON for an app environment', usage: 'securenow firewall enable [--app <key>] [--env production]', flags: { app: 'App key (defaults to logged-in app)', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').enable(a, f) },
237
241
  disable: { desc: 'Turn the firewall OFF for an app environment', usage: 'securenow firewall disable [--app <key>] [--env local]', flags: { app: 'App key (defaults to logged-in app)', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').disable(a, f) },
238
242
  'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip> [--env production]', flags: { env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').testIp(a, f) },
239
- },
240
- defaultSub: 'status',
241
- },
242
- blocklist: {
243
- desc: 'Manage IP blocklist',
244
- usage: 'securenow blocklist <subcommand> [options]',
245
- sub: {
246
- list: { desc: 'List blocked IPs', run: (a, f) => require('./cli/security').blocklistList(a, f) },
247
- add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
243
+ },
244
+ defaultSub: 'status',
245
+ },
246
+ automation: {
247
+ desc: 'Manage automation rules for blocklist actions',
248
+ usage: 'securenow automation <list|show|create|update|dry-run|execute|delete> [rule-id] [options]',
249
+ flags: {
250
+ json: 'Output as JSON',
251
+ body: 'Full JSON body for create/update',
252
+ file: 'Read JSON body from file',
253
+ name: 'Rule name',
254
+ description: 'Rule description',
255
+ conditions: 'Conditions JSON array',
256
+ actions: 'Actions JSON array',
257
+ logic: 'Condition logic: AND or OR',
258
+ status: 'Rule status: active or disabled',
259
+ app: 'Comma-separated app key(s) to scope the rule',
260
+ apps: 'Alias for --app',
261
+ 'applications-all': 'Scope rule to all apps',
262
+ env: 'Comma-separated environment(s) to scope the rule',
263
+ environment: 'Alias for --env',
264
+ environments: 'Alias for --env',
265
+ 'environments-all': 'Scope rule to all environments',
266
+ limit: 'Dry-run notification scan limit',
267
+ 'sample-limit': 'Dry-run sample count',
268
+ yes: 'Confirm execute/delete',
269
+ force: 'Alias for --yes',
270
+ },
271
+ sub: {
272
+ list: { desc: 'List automation rules', run: (a, f) => require('./cli/automation').list(a, f) },
273
+ show: { desc: 'Show automation rule details', usage: 'securenow automation show <rule-id>', run: (a, f) => require('./cli/automation').show(a, f) },
274
+ create: { desc: 'Create automation rule', usage: 'securenow automation create --name "..." --conditions \'[...]\' --actions \'[...]\'', run: (a, f) => require('./cli/automation').create(a, f) },
275
+ update: { desc: 'Update automation rule', usage: 'securenow automation update <rule-id> --body \'{...}\'', run: (a, f) => require('./cli/automation').update(a, f) },
276
+ 'dry-run': { desc: 'Preview automation matches without writing blocklist entries', usage: 'securenow automation dry-run <rule-id>', run: (a, f) => require('./cli/automation').dryRun(a, f) },
277
+ execute: { desc: 'Execute an automation rule now', usage: 'securenow automation execute <rule-id> --yes', run: (a, f) => require('./cli/automation').execute(a, f) },
278
+ delete: { desc: 'Delete an automation rule', usage: 'securenow automation delete <rule-id> --yes', run: (a, f) => require('./cli/automation').remove(a, f) },
279
+ },
280
+ defaultSub: 'list',
281
+ },
282
+ blocklist: {
283
+ desc: 'Manage IP blocklist',
284
+ usage: 'securenow blocklist <subcommand> [options]',
285
+ flags: { app: 'Scope to app key', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', json: 'Output as JSON' },
286
+ sub: {
287
+ list: { desc: 'List blocked IPs', run: (a, f) => require('./cli/security').blocklistList(a, f) },
288
+ add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--app <key>] [--env production] [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
248
289
  remove: { desc: 'Unblock an IP', usage: 'securenow blocklist remove <id>', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
249
290
  stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
250
291
  },
251
292
  defaultSub: 'list',
252
293
  },
253
- allowlist: {
254
- desc: 'Manage IP allowlist (only allow listed IPs)',
255
- usage: 'securenow allowlist <subcommand> [options]',
256
- sub: {
257
- list: { desc: 'List allowed IPs', run: (a, f) => require('./cli/security').allowlistList(a, f) },
258
- add: { desc: 'Allow an IP', usage: 'securenow allowlist add <ip> [--label <label>] [--reason <reason>]', run: (a, f) => require('./cli/security').allowlistAdd(a, f) },
294
+ allowlist: {
295
+ desc: 'Manage IP allowlist (only allow listed IPs)',
296
+ usage: 'securenow allowlist <subcommand> [options]',
297
+ flags: { app: 'Scope to app key(s), comma-separated', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', json: 'Output as JSON' },
298
+ sub: {
299
+ list: { desc: 'List allowed IPs', run: (a, f) => require('./cli/security').allowlistList(a, f) },
300
+ add: { desc: 'Allow an IP', usage: 'securenow allowlist add <ip> [--app <key>] [--env local] [--label <label>] [--reason <reason>]', run: (a, f) => require('./cli/security').allowlistAdd(a, f) },
259
301
  remove: { desc: 'Remove an allowed IP', usage: 'securenow allowlist remove <id>', run: (a, f) => require('./cli/security').allowlistRemove(a, f) },
260
302
  stats: { desc: 'Allowlist statistics', run: (a, f) => require('./cli/security').allowlistStats(a, f) },
261
303
  },
262
304
  defaultSub: 'list',
263
305
  },
264
- trusted: {
265
- desc: 'Manage trusted IPs',
266
- usage: 'securenow trusted <subcommand> [options]',
267
- sub: {
268
- list: { desc: 'List trusted IPs', run: (a, f) => require('./cli/security').trustedList(a, f) },
269
- add: { desc: 'Add trusted IP', usage: 'securenow trusted add <ip> [--label <label>]', run: (a, f) => require('./cli/security').trustedAdd(a, f) },
306
+ trusted: {
307
+ desc: 'Manage trusted IPs',
308
+ usage: 'securenow trusted <subcommand> [options]',
309
+ flags: { app: 'Scope to app key(s), comma-separated', env: 'Scope to environment', environment: 'Alias for --env', json: 'Output as JSON' },
310
+ sub: {
311
+ list: { desc: 'List trusted IPs', run: (a, f) => require('./cli/security').trustedList(a, f) },
312
+ add: { desc: 'Add trusted IP', usage: 'securenow trusted add <ip> [--app <key>] [--env local] [--label <label>]', run: (a, f) => require('./cli/security').trustedAdd(a, f) },
270
313
  remove: { desc: 'Remove trusted IP', usage: 'securenow trusted remove <id>', run: (a, f) => require('./cli/security').trustedRemove(a, f) },
271
314
  },
272
315
  defaultSub: 'list',
@@ -499,8 +542,8 @@ function showHelp(commandName) {
499
542
  'Observe': ['traces', 'logs', 'analytics'],
500
543
  'Detect & Respond': ['human', 'notifications', 'alerts', 'fp'],
501
544
  'Investigate': ['ip', 'forensics'],
502
- 'Firewall': ['firewall'],
503
- 'Remediation': ['blocklist', 'allowlist', 'trusted'],
545
+ 'Firewall': ['firewall'],
546
+ 'Remediation': ['automation', 'blocklist', 'allowlist', 'trusted'],
504
547
  'Telemetry': ['log', 'test-span'],
505
548
  'Utilities': ['redact', 'cidr', 'doctor', 'env', 'mcp'],
506
549
  'Settings': ['instances', 'config', 'version'],