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 +170 -0
- package/cli.js +2 -0
- package/mcp/catalog.js +2233 -2187
- package/package.json +1 -1
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',
|