securenow 8.7.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 +219 -0
- package/cli.js +867 -865
- package/mcp/catalog.js +2233 -2187
- package/package.json +193 -193
package/cli/security.js
CHANGED
|
@@ -48,9 +48,21 @@ 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
|
}
|
|
63
|
+
if (sub === 'duplicate' || sub === 'clone') {
|
|
64
|
+
return alertRuleDuplicate(args.slice(1), flags);
|
|
65
|
+
}
|
|
54
66
|
if (sub === 'test') {
|
|
55
67
|
return alertRuleTest(args.slice(1), flags);
|
|
56
68
|
}
|
|
@@ -111,6 +123,52 @@ async function alertRuleSetMode(args, flags, mode) {
|
|
|
111
123
|
else ui.info('Detect-only — no mitigation will act. Promote when confident: securenow alerts rules promote <id>');
|
|
112
124
|
}
|
|
113
125
|
|
|
126
|
+
// Duplicate any rule (including a read-only system template) into a new CUSTOM rule you
|
|
127
|
+
// own. System rules are view-only examples — fork one with this to enable / edit it. The
|
|
128
|
+
// copy defaults to Disabled (opt in with --enable / --status active); SQL, instant.block,
|
|
129
|
+
// schedule, scope, and mode are copied from the source.
|
|
130
|
+
async function alertRuleDuplicate(args, flags) {
|
|
131
|
+
requireAuth();
|
|
132
|
+
const id = args[0];
|
|
133
|
+
if (!id) {
|
|
134
|
+
ui.error('Usage: securenow alerts rules duplicate <rule-id> [--enable] [--status active|disabled|paused] [--name "..."] [--mode test|prod]');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const body = {};
|
|
138
|
+
if (flags.enable) body.status = 'Active';
|
|
139
|
+
else if (flags.disable) body.status = 'Disabled';
|
|
140
|
+
else if (flags.pause) body.status = 'Paused';
|
|
141
|
+
else if (flags.status) {
|
|
142
|
+
const s = String(flags.status).toLowerCase();
|
|
143
|
+
body.status = s === 'active' ? 'Active' : s === 'paused' ? 'Paused' : 'Disabled';
|
|
144
|
+
}
|
|
145
|
+
if (flags.name) body.name = String(flags.name);
|
|
146
|
+
if (flags.mode === 'test' || flags.mode === 'prod') body.mode = flags.mode;
|
|
147
|
+
|
|
148
|
+
const spin = ui.spinner(`Duplicating ${ui.truncate(id, 12)}`);
|
|
149
|
+
let data;
|
|
150
|
+
try {
|
|
151
|
+
data = await api.post(`/alert-rules/${id}/duplicate`, body);
|
|
152
|
+
spin.stop('Duplicated');
|
|
153
|
+
} catch (err) {
|
|
154
|
+
spin.fail(`Failed to duplicate: ${err.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (flags.json) { ui.json(data); return; }
|
|
159
|
+
const r = data.alertRule || {};
|
|
160
|
+
const newId = r.id || r._id || '';
|
|
161
|
+
console.log('');
|
|
162
|
+
ui.success(`Created custom rule ${newId}`);
|
|
163
|
+
console.log(` Name ${r.name || ''}`);
|
|
164
|
+
console.log(` Status ${r.status || ''}`);
|
|
165
|
+
console.log(` Mode ${r.mode || ''}`);
|
|
166
|
+
console.log(` Exec mode ${r.executionMode || ''}`);
|
|
167
|
+
console.log(` Custom ${r.isSystem ? 'NO — still a system rule (unexpected)' : 'yes (editable / enable-able)'}`);
|
|
168
|
+
if (data.duplicatedFromSystem) ui.info('Forked from a read-only system template — edit or enable this custom copy freely.');
|
|
169
|
+
if (r.status !== 'Active') ui.info(`Disabled by default. Enable with: securenow alerts rules update ${newId} --enable`);
|
|
170
|
+
}
|
|
171
|
+
|
|
114
172
|
async function alertRulesList(args, flags) {
|
|
115
173
|
requireAuth();
|
|
116
174
|
// Server-side filters (API supports ?mode/status/active/isSystem).
|
|
@@ -415,6 +473,113 @@ async function alertRuleSetSql(args, flags) {
|
|
|
415
473
|
}
|
|
416
474
|
}
|
|
417
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
|
+
|
|
418
583
|
async function alertRuleDelete(args, flags) {
|
|
419
584
|
requireAuth();
|
|
420
585
|
const id = args[0];
|
|
@@ -918,6 +1083,58 @@ async function blocklistRemove(args, flags) {
|
|
|
918
1083
|
}
|
|
919
1084
|
}
|
|
920
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
|
+
|
|
921
1138
|
async function blocklistStats(args, flags) {
|
|
922
1139
|
requireAuth();
|
|
923
1140
|
const s = ui.spinner('Fetching blocklist stats');
|
|
@@ -1617,6 +1834,8 @@ module.exports = {
|
|
|
1617
1834
|
blocklistList,
|
|
1618
1835
|
blocklistAdd,
|
|
1619
1836
|
blocklistRemove,
|
|
1837
|
+
blocklistApprove,
|
|
1838
|
+
blocklistReject,
|
|
1620
1839
|
blocklistStats,
|
|
1621
1840
|
revokeRoute,
|
|
1622
1841
|
allowlistList,
|