securenow 8.8.0 → 8.9.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
@@ -48,6 +48,15 @@ async function alertRulesRoute(args, flags) {
48
48
  if (sub === 'set-sql' || sub === 'edit-sql') {
49
49
  return alertRuleSetSql(args.slice(1), flags);
50
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
+ }
51
60
  if (sub === 'delete' || sub === 'rm' || sub === 'remove') {
52
61
  return alertRuleDelete(args.slice(1), flags);
53
62
  }
@@ -464,6 +473,113 @@ async function alertRuleSetSql(args, flags) {
464
473
  }
465
474
  }
466
475
 
476
+ // List a rule's configuration version history (newest first). Each material update (SQL,
477
+ // instant config, settings), duplicate, and rollback is a version; rollback restores a
478
+ // prior snapshot and is itself recorded as a new version.
479
+ async function alertRuleVersions(args, flags) {
480
+ requireAuth();
481
+ const id = args[0];
482
+ if (!id) {
483
+ ui.error('Usage: securenow alerts rules versions <rule-id> [--limit N] [--page N]');
484
+ process.exit(1);
485
+ }
486
+ const query = {};
487
+ if (flags.limit) query.limit = flags.limit;
488
+ if (flags.page) query.page = flags.page;
489
+ const s = ui.spinner('Fetching version history');
490
+ let data;
491
+ try {
492
+ data = await api.get(`/alert-rules/${id}/versions`, Object.keys(query).length ? { query } : undefined);
493
+ s.stop(`Found ${data.total ?? (data.versions || []).length} version${(data.total ?? 0) === 1 ? '' : 's'}`);
494
+ } catch (err) {
495
+ s.fail(`Failed to fetch versions: ${err.message}`);
496
+ process.exit(1);
497
+ }
498
+ if (flags.json) { ui.json(data); return; }
499
+ const rows = data.versions || [];
500
+ if (rows.length === 0) { ui.info('No versions recorded yet.'); return; }
501
+ console.log('');
502
+ ui.info(`Current: v${data.currentVersion}`);
503
+ for (const v of rows) {
504
+ const marker = v.isCurrent ? ui.c.green('→') : ' ';
505
+ const when = v.createdAt ? new Date(v.createdAt).toISOString().replace('T', ' ').slice(0, 19) : '';
506
+ const extra = v.changeType === 'rollback' && v.rolledBackFrom ? ` (from v${v.rolledBackFrom})` : '';
507
+ console.log(`${marker} ${ui.c.bold('v' + v.version).padEnd(14)} ${String(v.changeType + extra).padEnd(22)} ${ui.c.dim(when)} ${v.createdByName || ''}`);
508
+ if (v.reason) console.log(` ${ui.c.dim(ui.truncate(v.reason, 90))}`);
509
+ }
510
+ console.log('');
511
+ ui.info(`Show: securenow alerts rules version ${ui.truncate(id, 12)} <n> | Roll back: securenow alerts rules rollback ${ui.truncate(id, 12)} <n>`);
512
+ }
513
+
514
+ // Show one version's full material snapshot (diff-able SQL / instant / settings).
515
+ async function alertRuleVersionShow(args, flags) {
516
+ requireAuth();
517
+ const id = args[0];
518
+ const version = args[1];
519
+ if (!id || !version) {
520
+ ui.error('Usage: securenow alerts rules version <rule-id> <version-number>');
521
+ process.exit(1);
522
+ }
523
+ const s = ui.spinner(`Fetching v${version}`);
524
+ let data;
525
+ try {
526
+ data = await api.get(`/alert-rules/${id}/versions/${version}`);
527
+ s.stop(`Loaded v${version}`);
528
+ } catch (err) {
529
+ s.fail(`Failed: ${err.message}`);
530
+ process.exit(1);
531
+ }
532
+ if (flags.json) { ui.json(data); return; }
533
+ const v = data.version || {};
534
+ const snap = v.snapshot || {};
535
+ console.log('');
536
+ ui.success(`v${v.version}${v.isCurrent ? ' (current)' : ''} — ${v.changeType}`);
537
+ console.log(` When ${v.createdAt || ''}`);
538
+ if (v.createdByName) console.log(` By ${v.createdByName}`);
539
+ if (v.reason) console.log(` Reason ${v.reason}`);
540
+ if (v.rolledBackFrom) console.log(` Restored v${v.rolledBackFrom}`);
541
+ console.log('');
542
+ console.log(` Name ${snap.name || ''}`);
543
+ console.log(` Mode ${snap.mode || ''} / ${snap.executionMode || ''} severity ${snap.severity || '-'}`);
544
+ if (snap.instant && snap.instant.enabled) {
545
+ console.log(` Instant enabled block=${snap.instant.block} (${(snap.instant.conditions || []).length} condition${(snap.instant.conditions || []).length === 1 ? '' : 's'})`);
546
+ }
547
+ if (snap.query && snap.query.sqlQuery) {
548
+ console.log(' SQL:');
549
+ console.log(snap.query.sqlQuery.split('\n').map((l) => ' ' + ui.c.dim(l)).join('\n'));
550
+ }
551
+ }
552
+
553
+ // Roll a rule back to a prior version — restores that version's full snapshot and records it
554
+ // as a new version (append-only history). System (read-only template) rules can't be rolled back.
555
+ async function alertRuleRollback(args, flags) {
556
+ requireAuth();
557
+ const id = args[0];
558
+ const version = args[1] || flags.version;
559
+ if (!id || !version) {
560
+ ui.error('Usage: securenow alerts rules rollback <rule-id> <version-number> [--reason "..."] [--yes]');
561
+ process.exit(1);
562
+ }
563
+ if (!flags.yes && !flags.force) {
564
+ const ok = await ui.confirm(`Roll back rule ${ui.truncate(id, 12)} to v${version}? Restores that config and creates a new version.`);
565
+ if (!ok) { ui.info('Cancelled'); return; }
566
+ }
567
+ const body = { version: Number(version) };
568
+ if (flags.reason) body.reason = String(flags.reason);
569
+ const s = ui.spinner(`Rolling back to v${version}`);
570
+ let data;
571
+ try {
572
+ data = await api.post(`/alert-rules/${id}/rollback`, body);
573
+ s.stop(`Rolled back to v${version}`);
574
+ } catch (err) {
575
+ s.fail(`Failed to roll back: ${err.message}`);
576
+ process.exit(1);
577
+ }
578
+ if (flags.json) { ui.json(data); return; }
579
+ ui.success(`Restored from v${data.rolledBackFrom} → now at v${data.newVersion}`);
580
+ ui.info(`History: securenow alerts rules versions ${ui.truncate(id, 12)}`);
581
+ }
582
+
467
583
  async function alertRuleDelete(args, flags) {
468
584
  requireAuth();
469
585
  const id = args[0];
@@ -967,6 +1083,58 @@ async function blocklistRemove(args, flags) {
967
1083
  }
968
1084
  }
969
1085
 
1086
+ // Approve a block that the AI prepared but parked pending customer approval
1087
+ // (e.g. the autopilot held it under block_risk_score_below_policy). Enforces the IP.
1088
+ async function blocklistApprove(args, flags) {
1089
+ requireAuth();
1090
+ const id = args[0];
1091
+ if (!id) {
1092
+ ui.error('Usage: securenow blocklist approve <pending-block-id> [--reason <reason>] [--yes]');
1093
+ process.exit(1);
1094
+ }
1095
+ if (!flags.force && !flags.yes) {
1096
+ const ok = await ui.confirm('Approve this pending block? The IP will be enforced by the firewall.');
1097
+ if (!ok) { ui.info('Cancelled'); return; }
1098
+ }
1099
+ const s = ui.spinner('Approving pending block');
1100
+ try {
1101
+ const body = {};
1102
+ if (flags.reason) body.reason = flags.reason;
1103
+ const data = await api.post(`/blocklist/${id}/approve`, body);
1104
+ s.stop('Pending block approved — IP now enforced');
1105
+ if (flags.json) ui.json(data);
1106
+ } catch (err) {
1107
+ s.fail('Failed to approve pending block');
1108
+ throw err;
1109
+ }
1110
+ }
1111
+
1112
+ // Reject a pending block: the IP is NOT blocked, the entry is removed, and a scoped
1113
+ // no-block decision is recorded so auto-block won't re-block this IP for the same rule.
1114
+ async function blocklistReject(args, flags) {
1115
+ requireAuth();
1116
+ const id = args[0];
1117
+ if (!id) {
1118
+ ui.error('Usage: securenow blocklist reject <pending-block-id> [--reason <reason>] [--yes]');
1119
+ process.exit(1);
1120
+ }
1121
+ if (!flags.force && !flags.yes) {
1122
+ 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.');
1123
+ if (!ok) { ui.info('Cancelled'); return; }
1124
+ }
1125
+ const s = ui.spinner('Rejecting pending block');
1126
+ try {
1127
+ const body = {};
1128
+ if (flags.reason) body.reason = flags.reason;
1129
+ const data = await api.post(`/blocklist/${id}/reject`, body);
1130
+ s.stop('Pending block rejected — IP cleared (scoped no-block decision recorded)');
1131
+ if (flags.json) ui.json(data);
1132
+ } catch (err) {
1133
+ s.fail('Failed to reject pending block');
1134
+ throw err;
1135
+ }
1136
+ }
1137
+
970
1138
  async function blocklistStats(args, flags) {
971
1139
  requireAuth();
972
1140
  const s = ui.spinner('Fetching blocklist stats');
@@ -1666,6 +1834,8 @@ module.exports = {
1666
1834
  blocklistList,
1667
1835
  blocklistAdd,
1668
1836
  blocklistRemove,
1837
+ blocklistApprove,
1838
+ blocklistReject,
1669
1839
  blocklistStats,
1670
1840
  revokeRoute,
1671
1841
  allowlistList,
package/cli.js CHANGED
@@ -473,6 +473,8 @@ const COMMANDS = {
473
473
  add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--route /admin*] [--mode prefix] [--method GET] [--app <key>] [--env production] [--duration 24h] [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
474
474
  unblock: { desc: 'Unblock an IP and keep block history', usage: 'securenow blocklist unblock <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
475
475
  remove: { desc: 'Unblock an IP (compatibility alias)', usage: 'securenow blocklist remove <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
476
+ approve: { desc: 'Approve a pending block (enforce the IP)', usage: 'securenow blocklist approve <pending-block-id> [--reason <reason>] [--yes]', run: (a, f) => require('./cli/security').blocklistApprove(a, f) },
477
+ reject: { desc: 'Reject a pending block (clear the IP, record a scoped no-block decision)', usage: 'securenow blocklist reject <pending-block-id> [--reason <reason>] [--yes]', run: (a, f) => require('./cli/security').blocklistReject(a, f) },
476
478
  stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
477
479
  },
478
480
  defaultSub: 'list',