securenow 8.0.3 → 8.2.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/NPM_README.md +37 -1
- package/SKILL-CLI.md +237 -229
- package/cli/diagnostics.js +48 -1
- package/cli/security.js +467 -374
- package/cli.js +451 -418
- package/events.d.ts +29 -0
- package/events.js +160 -0
- package/package.json +7 -1
package/cli/security.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const { api, requireAuth } = require('./client');
|
|
5
|
-
const config = require('./config');
|
|
6
|
-
const ui = require('./ui');
|
|
7
|
-
|
|
8
|
-
function resolveApp(flags) {
|
|
9
|
-
return flags.app || config.getDefaultApp();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function resolveEnvironment(flags, fallback = null) {
|
|
13
|
-
return flags.env || flags.environment || fallback;
|
|
14
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { api, requireAuth } = require('./client');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const ui = require('./ui');
|
|
7
|
+
|
|
8
|
+
function resolveApp(flags) {
|
|
9
|
+
return flags.app || config.getDefaultApp();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveEnvironment(flags, fallback = null) {
|
|
13
|
+
return flags.env || flags.environment || fallback;
|
|
14
|
+
}
|
|
15
15
|
|
|
16
16
|
// ── Alert Rules ──
|
|
17
17
|
|
|
@@ -36,27 +36,30 @@ function ruleStatusBadge(rule) {
|
|
|
36
36
|
/** Dispatch: list | show <id> | update <id> ... */
|
|
37
37
|
async function alertRulesRoute(args, flags) {
|
|
38
38
|
const sub = args[0];
|
|
39
|
+
if (sub === 'create' || sub === 'add') {
|
|
40
|
+
return alertRuleCreate(args.slice(1), flags);
|
|
41
|
+
}
|
|
39
42
|
if (sub === 'show') {
|
|
40
43
|
return alertRuleShow(args.slice(1), flags);
|
|
41
44
|
}
|
|
42
|
-
if (sub === 'update') {
|
|
43
|
-
return alertRuleUpdate(args.slice(1), flags);
|
|
44
|
-
}
|
|
45
|
-
if (sub === 'test') {
|
|
46
|
-
return alertRuleTest(args.slice(1), flags);
|
|
47
|
-
}
|
|
48
|
-
if (sub === 'dry-run-query' || sub === 'candidate-test') {
|
|
49
|
-
return alertRuleCandidateTest(args.slice(1), flags);
|
|
50
|
-
}
|
|
51
|
-
if (sub === 'tune-query' || sub === 'query-update') {
|
|
52
|
-
return alertRuleQueryUpdate(args.slice(1), flags);
|
|
53
|
-
}
|
|
54
|
-
if (sub === 'exclusions') {
|
|
55
|
-
return alertRuleExclusions(args.slice(1), flags);
|
|
56
|
-
}
|
|
57
|
-
if (sub === 'list') {
|
|
58
|
-
return alertRulesList(args.slice(1), flags);
|
|
59
|
-
}
|
|
45
|
+
if (sub === 'update') {
|
|
46
|
+
return alertRuleUpdate(args.slice(1), flags);
|
|
47
|
+
}
|
|
48
|
+
if (sub === 'test') {
|
|
49
|
+
return alertRuleTest(args.slice(1), flags);
|
|
50
|
+
}
|
|
51
|
+
if (sub === 'dry-run-query' || sub === 'candidate-test') {
|
|
52
|
+
return alertRuleCandidateTest(args.slice(1), flags);
|
|
53
|
+
}
|
|
54
|
+
if (sub === 'tune-query' || sub === 'query-update') {
|
|
55
|
+
return alertRuleQueryUpdate(args.slice(1), flags);
|
|
56
|
+
}
|
|
57
|
+
if (sub === 'exclusions') {
|
|
58
|
+
return alertRuleExclusions(args.slice(1), flags);
|
|
59
|
+
}
|
|
60
|
+
if (sub === 'list') {
|
|
61
|
+
return alertRulesList(args.slice(1), flags);
|
|
62
|
+
}
|
|
60
63
|
return alertRulesList(args, flags);
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -88,6 +91,96 @@ async function alertRulesList(args, flags) {
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
async function alertRuleCreate(args, flags) {
|
|
95
|
+
requireAuth();
|
|
96
|
+
const name = flags.name;
|
|
97
|
+
const usage = 'Usage: securenow alerts rules create --name "..." --sql <sql|@file|-> (--applications-all | --apps k1,k2) '
|
|
98
|
+
+ '[--severity critical|high|medium|low] [--schedule "*/15 * * * *"] [--throttle-minutes 15 | --no-throttle] '
|
|
99
|
+
+ '[--description "..."] [--nlp "plain-English intent"] [--category custom] [--channel id1,id2] [--query-mapping-id <id>]';
|
|
100
|
+
if (!name) {
|
|
101
|
+
ui.error(usage);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const queryMappingId = flags['query-mapping-id'] || flags.queryMappingId;
|
|
106
|
+
const sqlQuery = readSqlArg(flags);
|
|
107
|
+
if (!queryMappingId && !sqlQuery) {
|
|
108
|
+
ui.error('Provide the detection query: --sql <sql|@file|-> (or reuse one with --query-mapping-id <id>).');
|
|
109
|
+
console.log(ui.c.dim(' Scope app keys in SQL with the __USER_APP_KEYS__ placeholder, e.g.'));
|
|
110
|
+
console.log(ui.c.dim(" ...WHERE resource_string_service$$name IN (__USER_APP_KEYS__) ..."));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const hasAll = flags['applications-all'] === true || flags['applications-all'] === 'true';
|
|
115
|
+
const appsStr = flags.apps || flags.app;
|
|
116
|
+
if (!hasAll && !appsStr) {
|
|
117
|
+
ui.error('Scope the rule with --applications-all or --apps key1,key2');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const body = { name };
|
|
122
|
+
if (flags.description) body.description = flags.description;
|
|
123
|
+
if (queryMappingId) body.queryMappingId = queryMappingId;
|
|
124
|
+
else body.sqlQuery = sqlQuery;
|
|
125
|
+
if (flags.nlp || flags.text) body.nlpQuery = flags.nlp || flags.text;
|
|
126
|
+
if (flags.category) body.category = flags.category;
|
|
127
|
+
if (flags.severity) body.severity = String(flags.severity).toLowerCase();
|
|
128
|
+
if (flags['execution-mode']) body.executionMode = flags['execution-mode'];
|
|
129
|
+
|
|
130
|
+
if (hasAll) {
|
|
131
|
+
body.applicationsAll = true;
|
|
132
|
+
body.applications = [];
|
|
133
|
+
} else {
|
|
134
|
+
body.applicationsAll = false;
|
|
135
|
+
body.applications = String(appsStr).split(',').map((x) => x.trim()).filter(Boolean);
|
|
136
|
+
if (body.applications.length === 0) {
|
|
137
|
+
ui.error('No application keys parsed from --apps');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const cron = flags.schedule || flags.cron;
|
|
143
|
+
if (cron) {
|
|
144
|
+
body.schedule = {
|
|
145
|
+
enabled: true,
|
|
146
|
+
cronExpression: cron,
|
|
147
|
+
description: flags['schedule-description'] || cron,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (flags['no-throttle']) body.throttle = { enabled: false, minutes: 0 };
|
|
151
|
+
else if (flags['throttle-minutes']) body.throttle = { enabled: true, minutes: Number(flags['throttle-minutes']) };
|
|
152
|
+
|
|
153
|
+
const channelStr = flags.channel || flags.channels;
|
|
154
|
+
if (channelStr) {
|
|
155
|
+
body.alertChannelIds = String(channelStr).split(',').map((x) => x.trim()).filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const s = ui.spinner('Creating alert rule');
|
|
159
|
+
try {
|
|
160
|
+
const data = await api.post('/alert-rules', body);
|
|
161
|
+
const r = data.alertRule || data;
|
|
162
|
+
s.stop('Alert rule created');
|
|
163
|
+
if (flags.json) { ui.json(data); return; }
|
|
164
|
+
console.log('');
|
|
165
|
+
ui.keyValue([
|
|
166
|
+
['ID', r._id || r.id || '-'],
|
|
167
|
+
['Name', r.name || '-'],
|
|
168
|
+
['Status', r.status || '-'],
|
|
169
|
+
['Mode', r.executionMode || 'scheduled'],
|
|
170
|
+
['Severity', r.severity || '-'],
|
|
171
|
+
['Applications', r.applicationsAll ? 'all apps' : ((r.applications || []).join(', ') || '-')],
|
|
172
|
+
['Schedule', r.schedule?.enabled === false ? 'disabled' : (r.schedule?.description || r.schedule?.cronExpression || '-')],
|
|
173
|
+
['Throttle', r.throttle?.enabled ? `${r.throttle.minutes} min` : 'off'],
|
|
174
|
+
['Query', r.queryMappingId?.name || r.queryMappingId || '-'],
|
|
175
|
+
]);
|
|
176
|
+
console.log('');
|
|
177
|
+
ui.success(`Created. View it with: securenow alerts rules show ${r._id || r.id}`);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
s.fail('Failed to create alert rule');
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
91
184
|
async function alertRuleShow(args, flags) {
|
|
92
185
|
requireAuth();
|
|
93
186
|
const id = args[0];
|
|
@@ -128,7 +221,7 @@ async function alertRuleShow(args, flags) {
|
|
|
128
221
|
}
|
|
129
222
|
}
|
|
130
223
|
|
|
131
|
-
async function alertRuleUpdate(args, flags) {
|
|
224
|
+
async function alertRuleUpdate(args, flags) {
|
|
132
225
|
requireAuth();
|
|
133
226
|
const id = args[0];
|
|
134
227
|
if (!id) {
|
|
@@ -185,201 +278,201 @@ async function alertRuleUpdate(args, flags) {
|
|
|
185
278
|
s.fail('Failed to update alert rule');
|
|
186
279
|
throw err;
|
|
187
280
|
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function readSqlArg(flags) {
|
|
191
|
-
const raw = flags.sql || flags.query || flags.file;
|
|
192
|
-
if (!raw) return null;
|
|
193
|
-
if (flags.file) return fs.readFileSync(flags.file, 'utf8');
|
|
194
|
-
const text = String(raw);
|
|
195
|
-
if (text === '-') return fs.readFileSync(0, 'utf8');
|
|
196
|
-
if (text.startsWith('@')) return fs.readFileSync(text.slice(1), 'utf8');
|
|
197
|
-
return text;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function alertRuleTest(args, flags) {
|
|
201
|
-
requireAuth();
|
|
202
|
-
const id = args[0];
|
|
203
|
-
if (!id) {
|
|
204
|
-
ui.error('Usage: securenow alerts rules test <rule-id> [--app <key>] [--mode dry_run] [--wait]');
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const body = {};
|
|
209
|
-
if (flags.app) body.applicationKey = flags.app;
|
|
210
|
-
if (flags.mode) body.mode = flags.mode;
|
|
211
|
-
const s = ui.spinner('Starting alert rule test');
|
|
212
|
-
try {
|
|
213
|
-
let data = await api.post(`/alert-rules/${id}/test`, body);
|
|
214
|
-
if (flags.wait && data.testId) {
|
|
215
|
-
s.update('Waiting for alert rule test results');
|
|
216
|
-
for (let i = 0; i < 40; i++) {
|
|
217
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
218
|
-
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
219
|
-
if (['complete', 'failed'].includes(data.status)) break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
s.stop('Alert rule test ready');
|
|
223
|
-
if (flags.json) { ui.json(data); return; }
|
|
224
|
-
console.log('');
|
|
225
|
-
ui.keyValue([
|
|
226
|
-
['Test ID', data.testId || '-'],
|
|
227
|
-
['Mode', data.mode || body.mode || 'live'],
|
|
228
|
-
['Status', data.status || '-'],
|
|
229
|
-
['Result count', String(data.resultCount ?? '-')],
|
|
230
|
-
['Error', data.error || '-'],
|
|
231
|
-
]);
|
|
232
|
-
console.log('');
|
|
233
|
-
} catch (err) {
|
|
234
|
-
s.fail('Failed to test alert rule');
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async function alertRuleCandidateTest(args, flags) {
|
|
240
|
-
requireAuth();
|
|
241
|
-
const id = args[0];
|
|
242
|
-
const candidateSqlQuery = readSqlArg(flags);
|
|
243
|
-
if (!id || !candidateSqlQuery) {
|
|
244
|
-
ui.error('Usage: securenow alerts rules dry-run-query <rule-id> --sql <sql|@file|-> [--app <key>] [--wait]');
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const body = { mode: 'dry_run', candidateSqlQuery };
|
|
249
|
-
if (flags.app) body.applicationKey = flags.app;
|
|
250
|
-
|
|
251
|
-
const s = ui.spinner('Starting candidate SQL dry-run');
|
|
252
|
-
try {
|
|
253
|
-
let data = await api.post(`/alert-rules/${id}/test`, body);
|
|
254
|
-
if (flags.wait && data.testId) {
|
|
255
|
-
s.update('Waiting for candidate SQL dry-run results');
|
|
256
|
-
for (let i = 0; i < 40; i++) {
|
|
257
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
258
|
-
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
259
|
-
if (['complete', 'failed'].includes(data.status)) break;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
s.stop('Candidate dry-run ready');
|
|
263
|
-
if (flags.json) { ui.json(data); return; }
|
|
264
|
-
console.log('');
|
|
265
|
-
ui.keyValue([
|
|
266
|
-
['Test ID', data.testId || '-'],
|
|
267
|
-
['Status', data.status || '-'],
|
|
268
|
-
['Candidate', data.candidate ? 'yes' : 'no'],
|
|
269
|
-
['Current SQL hash', data.currentSqlHash || '-'],
|
|
270
|
-
['Candidate SQL hash', data.candidateSqlHash || '-'],
|
|
271
|
-
['Result count', String(data.resultCount ?? '-')],
|
|
272
|
-
['Error', data.error || '-'],
|
|
273
|
-
]);
|
|
274
|
-
console.log('');
|
|
275
|
-
} catch (err) {
|
|
276
|
-
s.fail('Failed to dry-run candidate SQL');
|
|
277
|
-
throw err;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function alertRuleQueryUpdate(args, flags) {
|
|
282
|
-
requireAuth();
|
|
283
|
-
const id = args[0];
|
|
284
|
-
const sqlQuery = readSqlArg(flags);
|
|
285
|
-
const reason = flags.reason;
|
|
286
|
-
if (!id || !sqlQuery || !reason) {
|
|
287
|
-
ui.error('Usage: securenow alerts rules tune-query <rule-id> --sql <sql|@file|-> --reason "..." --apply-globally --yes');
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
if (!flags['apply-globally'] && flags.applyGlobally !== true) {
|
|
291
|
-
ui.error('System query tuning requires --apply-globally.');
|
|
292
|
-
process.exit(1);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (!flags.force && !flags.yes) {
|
|
296
|
-
const ok = await ui.confirm('Update the shared system query mapping for all customer copies?');
|
|
297
|
-
if (!ok) {
|
|
298
|
-
ui.info('Cancelled');
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const body = {
|
|
304
|
-
sqlQuery,
|
|
305
|
-
reason,
|
|
306
|
-
applyGlobally: true,
|
|
307
|
-
reactivatePausedCopies: !!(flags['reactivate-paused'] || flags.reactivatePausedCopies),
|
|
308
|
-
};
|
|
309
|
-
if (flags['expected-hash']) body.expectedCurrentSqlHash = flags['expected-hash'];
|
|
310
|
-
if (flags.notification) body.reviewNotificationId = flags.notification;
|
|
311
|
-
if (flags.note) body.reviewNote = flags.note;
|
|
312
|
-
|
|
313
|
-
const s = ui.spinner('Updating system query mapping');
|
|
314
|
-
try {
|
|
315
|
-
const data = await api.put(`/alert-rules/${id}/query-mapping`, body);
|
|
316
|
-
s.stop('System query mapping updated');
|
|
317
|
-
if (flags.json) { ui.json(data); return; }
|
|
318
|
-
console.log('');
|
|
319
|
-
ui.keyValue([
|
|
320
|
-
['Query mapping', data.queryMapping?.name || data.queryMapping?.id || '-'],
|
|
321
|
-
['Previous SQL hash', data.queryMapping?.previousSqlHash || '-'],
|
|
322
|
-
['New SQL hash', data.queryMapping?.newSqlHash || '-'],
|
|
323
|
-
['Affected system rules', String(data.affectedSystemRules ?? '-')],
|
|
324
|
-
['Reactivated paused copies', data.reactivatedPausedCopies ? 'yes' : 'no'],
|
|
325
|
-
]);
|
|
326
|
-
console.log('');
|
|
327
|
-
} catch (err) {
|
|
328
|
-
s.fail('Failed to update system query mapping');
|
|
329
|
-
throw err;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async function alertRuleExclusions(args, flags) {
|
|
334
|
-
requireAuth();
|
|
335
|
-
const id = args[0];
|
|
336
|
-
const action = args[1] || 'list';
|
|
337
|
-
if (!id) {
|
|
338
|
-
ui.error('Usage: securenow alerts rules exclusions <rule-id> [list|add|delete] [exclusion-id]');
|
|
339
|
-
process.exit(1);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
if (action === 'add') {
|
|
344
|
-
const body = {};
|
|
345
|
-
if (flags.conditions) body.conditions = JSON.parse(flags.conditions);
|
|
346
|
-
if (flags['match-mode']) body.matchMode = flags['match-mode'];
|
|
347
|
-
if (flags.reason) body.reason = flags.reason;
|
|
348
|
-
if (flags.path) body.pathPattern = flags.path;
|
|
349
|
-
const data = await api.post(`/alert-rules/${id}/exclusions`, body);
|
|
350
|
-
if (flags.json) { ui.json(data); return; }
|
|
351
|
-
ui.success('Exclusion added');
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (action === 'delete' || action === 'remove') {
|
|
356
|
-
const exclusionId = args[2];
|
|
357
|
-
if (!exclusionId) {
|
|
358
|
-
ui.error('Exclusion id required.');
|
|
359
|
-
process.exit(1);
|
|
360
|
-
}
|
|
361
|
-
const data = await api.delete(`/alert-rules/${id}/exclusions/${exclusionId}`);
|
|
362
|
-
if (flags.json) { ui.json(data); return; }
|
|
363
|
-
ui.success('Exclusion removed');
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const data = await api.get(`/alert-rules/${id}/exclusions`);
|
|
368
|
-
const exclusions = data.exclusions || [];
|
|
369
|
-
if (flags.json) { ui.json(data); return; }
|
|
370
|
-
console.log('');
|
|
371
|
-
const rows = exclusions.map((e) => [
|
|
372
|
-
ui.c.dim(ui.truncate(e._id, 12)),
|
|
373
|
-
e.isActive === false ? ui.statusBadge('disabled') : ui.statusBadge('active'),
|
|
374
|
-
e.matchMode || 'all',
|
|
375
|
-
ui.truncate(e.reason || e.pathPattern || JSON.stringify(e.conditions || []), 72),
|
|
376
|
-
]);
|
|
377
|
-
ui.table(['ID', 'Status', 'Mode', 'Reason/Pattern'], rows);
|
|
378
|
-
console.log('');
|
|
379
|
-
} catch (err) {
|
|
380
|
-
throw err;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function readSqlArg(flags) {
|
|
284
|
+
const raw = flags.sql || flags.query || flags.file;
|
|
285
|
+
if (!raw) return null;
|
|
286
|
+
if (flags.file) return fs.readFileSync(flags.file, 'utf8');
|
|
287
|
+
const text = String(raw);
|
|
288
|
+
if (text === '-') return fs.readFileSync(0, 'utf8');
|
|
289
|
+
if (text.startsWith('@')) return fs.readFileSync(text.slice(1), 'utf8');
|
|
290
|
+
return text;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function alertRuleTest(args, flags) {
|
|
294
|
+
requireAuth();
|
|
295
|
+
const id = args[0];
|
|
296
|
+
if (!id) {
|
|
297
|
+
ui.error('Usage: securenow alerts rules test <rule-id> [--app <key>] [--mode dry_run] [--wait]');
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const body = {};
|
|
302
|
+
if (flags.app) body.applicationKey = flags.app;
|
|
303
|
+
if (flags.mode) body.mode = flags.mode;
|
|
304
|
+
const s = ui.spinner('Starting alert rule test');
|
|
305
|
+
try {
|
|
306
|
+
let data = await api.post(`/alert-rules/${id}/test`, body);
|
|
307
|
+
if (flags.wait && data.testId) {
|
|
308
|
+
s.update('Waiting for alert rule test results');
|
|
309
|
+
for (let i = 0; i < 40; i++) {
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
311
|
+
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
312
|
+
if (['complete', 'failed'].includes(data.status)) break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
s.stop('Alert rule test ready');
|
|
316
|
+
if (flags.json) { ui.json(data); return; }
|
|
317
|
+
console.log('');
|
|
318
|
+
ui.keyValue([
|
|
319
|
+
['Test ID', data.testId || '-'],
|
|
320
|
+
['Mode', data.mode || body.mode || 'live'],
|
|
321
|
+
['Status', data.status || '-'],
|
|
322
|
+
['Result count', String(data.resultCount ?? '-')],
|
|
323
|
+
['Error', data.error || '-'],
|
|
324
|
+
]);
|
|
325
|
+
console.log('');
|
|
326
|
+
} catch (err) {
|
|
327
|
+
s.fail('Failed to test alert rule');
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function alertRuleCandidateTest(args, flags) {
|
|
333
|
+
requireAuth();
|
|
334
|
+
const id = args[0];
|
|
335
|
+
const candidateSqlQuery = readSqlArg(flags);
|
|
336
|
+
if (!id || !candidateSqlQuery) {
|
|
337
|
+
ui.error('Usage: securenow alerts rules dry-run-query <rule-id> --sql <sql|@file|-> [--app <key>] [--wait]');
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const body = { mode: 'dry_run', candidateSqlQuery };
|
|
342
|
+
if (flags.app) body.applicationKey = flags.app;
|
|
343
|
+
|
|
344
|
+
const s = ui.spinner('Starting candidate SQL dry-run');
|
|
345
|
+
try {
|
|
346
|
+
let data = await api.post(`/alert-rules/${id}/test`, body);
|
|
347
|
+
if (flags.wait && data.testId) {
|
|
348
|
+
s.update('Waiting for candidate SQL dry-run results');
|
|
349
|
+
for (let i = 0; i < 40; i++) {
|
|
350
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
351
|
+
data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
|
|
352
|
+
if (['complete', 'failed'].includes(data.status)) break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
s.stop('Candidate dry-run ready');
|
|
356
|
+
if (flags.json) { ui.json(data); return; }
|
|
357
|
+
console.log('');
|
|
358
|
+
ui.keyValue([
|
|
359
|
+
['Test ID', data.testId || '-'],
|
|
360
|
+
['Status', data.status || '-'],
|
|
361
|
+
['Candidate', data.candidate ? 'yes' : 'no'],
|
|
362
|
+
['Current SQL hash', data.currentSqlHash || '-'],
|
|
363
|
+
['Candidate SQL hash', data.candidateSqlHash || '-'],
|
|
364
|
+
['Result count', String(data.resultCount ?? '-')],
|
|
365
|
+
['Error', data.error || '-'],
|
|
366
|
+
]);
|
|
367
|
+
console.log('');
|
|
368
|
+
} catch (err) {
|
|
369
|
+
s.fail('Failed to dry-run candidate SQL');
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function alertRuleQueryUpdate(args, flags) {
|
|
375
|
+
requireAuth();
|
|
376
|
+
const id = args[0];
|
|
377
|
+
const sqlQuery = readSqlArg(flags);
|
|
378
|
+
const reason = flags.reason;
|
|
379
|
+
if (!id || !sqlQuery || !reason) {
|
|
380
|
+
ui.error('Usage: securenow alerts rules tune-query <rule-id> --sql <sql|@file|-> --reason "..." --apply-globally --yes');
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
if (!flags['apply-globally'] && flags.applyGlobally !== true) {
|
|
384
|
+
ui.error('System query tuning requires --apply-globally.');
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!flags.force && !flags.yes) {
|
|
389
|
+
const ok = await ui.confirm('Update the shared system query mapping for all customer copies?');
|
|
390
|
+
if (!ok) {
|
|
391
|
+
ui.info('Cancelled');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const body = {
|
|
397
|
+
sqlQuery,
|
|
398
|
+
reason,
|
|
399
|
+
applyGlobally: true,
|
|
400
|
+
reactivatePausedCopies: !!(flags['reactivate-paused'] || flags.reactivatePausedCopies),
|
|
401
|
+
};
|
|
402
|
+
if (flags['expected-hash']) body.expectedCurrentSqlHash = flags['expected-hash'];
|
|
403
|
+
if (flags.notification) body.reviewNotificationId = flags.notification;
|
|
404
|
+
if (flags.note) body.reviewNote = flags.note;
|
|
405
|
+
|
|
406
|
+
const s = ui.spinner('Updating system query mapping');
|
|
407
|
+
try {
|
|
408
|
+
const data = await api.put(`/alert-rules/${id}/query-mapping`, body);
|
|
409
|
+
s.stop('System query mapping updated');
|
|
410
|
+
if (flags.json) { ui.json(data); return; }
|
|
411
|
+
console.log('');
|
|
412
|
+
ui.keyValue([
|
|
413
|
+
['Query mapping', data.queryMapping?.name || data.queryMapping?.id || '-'],
|
|
414
|
+
['Previous SQL hash', data.queryMapping?.previousSqlHash || '-'],
|
|
415
|
+
['New SQL hash', data.queryMapping?.newSqlHash || '-'],
|
|
416
|
+
['Affected system rules', String(data.affectedSystemRules ?? '-')],
|
|
417
|
+
['Reactivated paused copies', data.reactivatedPausedCopies ? 'yes' : 'no'],
|
|
418
|
+
]);
|
|
419
|
+
console.log('');
|
|
420
|
+
} catch (err) {
|
|
421
|
+
s.fail('Failed to update system query mapping');
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function alertRuleExclusions(args, flags) {
|
|
427
|
+
requireAuth();
|
|
428
|
+
const id = args[0];
|
|
429
|
+
const action = args[1] || 'list';
|
|
430
|
+
if (!id) {
|
|
431
|
+
ui.error('Usage: securenow alerts rules exclusions <rule-id> [list|add|delete] [exclusion-id]');
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
if (action === 'add') {
|
|
437
|
+
const body = {};
|
|
438
|
+
if (flags.conditions) body.conditions = JSON.parse(flags.conditions);
|
|
439
|
+
if (flags['match-mode']) body.matchMode = flags['match-mode'];
|
|
440
|
+
if (flags.reason) body.reason = flags.reason;
|
|
441
|
+
if (flags.path) body.pathPattern = flags.path;
|
|
442
|
+
const data = await api.post(`/alert-rules/${id}/exclusions`, body);
|
|
443
|
+
if (flags.json) { ui.json(data); return; }
|
|
444
|
+
ui.success('Exclusion added');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (action === 'delete' || action === 'remove') {
|
|
449
|
+
const exclusionId = args[2];
|
|
450
|
+
if (!exclusionId) {
|
|
451
|
+
ui.error('Exclusion id required.');
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
const data = await api.delete(`/alert-rules/${id}/exclusions/${exclusionId}`);
|
|
455
|
+
if (flags.json) { ui.json(data); return; }
|
|
456
|
+
ui.success('Exclusion removed');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const data = await api.get(`/alert-rules/${id}/exclusions`);
|
|
461
|
+
const exclusions = data.exclusions || [];
|
|
462
|
+
if (flags.json) { ui.json(data); return; }
|
|
463
|
+
console.log('');
|
|
464
|
+
const rows = exclusions.map((e) => [
|
|
465
|
+
ui.c.dim(ui.truncate(e._id, 12)),
|
|
466
|
+
e.isActive === false ? ui.statusBadge('disabled') : ui.statusBadge('active'),
|
|
467
|
+
e.matchMode || 'all',
|
|
468
|
+
ui.truncate(e.reason || e.pathPattern || JSON.stringify(e.conditions || []), 72),
|
|
469
|
+
]);
|
|
470
|
+
ui.table(['ID', 'Status', 'Mode', 'Reason/Pattern'], rows);
|
|
471
|
+
console.log('');
|
|
472
|
+
} catch (err) {
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
383
476
|
|
|
384
477
|
// ── Alert Channels ──
|
|
385
478
|
|
|
@@ -441,29 +534,29 @@ async function alertHistoryList(args, flags) {
|
|
|
441
534
|
|
|
442
535
|
// ── Blocklist ──
|
|
443
536
|
|
|
444
|
-
function describeBlockTarget(entry) {
|
|
445
|
-
const parts = [entry.ip || entry.cidr || '-'];
|
|
446
|
-
if (entry.method && entry.method !== 'ALL') parts.push(entry.method);
|
|
447
|
-
if (entry.pathPattern) parts.push(`${entry.pathMatchMode || 'prefix'}:${entry.pathPattern}`);
|
|
448
|
-
return parts.join(' ');
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async function blocklistList(args, flags) {
|
|
537
|
+
function describeBlockTarget(entry) {
|
|
538
|
+
const parts = [entry.ip || entry.cidr || '-'];
|
|
539
|
+
if (entry.method && entry.method !== 'ALL') parts.push(entry.method);
|
|
540
|
+
if (entry.pathPattern) parts.push(`${entry.pathMatchMode || 'prefix'}:${entry.pathPattern}`);
|
|
541
|
+
return parts.join(' ');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function blocklistList(args, flags) {
|
|
452
545
|
requireAuth();
|
|
453
546
|
const s = ui.spinner('Fetching blocklist');
|
|
454
547
|
try {
|
|
455
|
-
const query = {};
|
|
456
|
-
if (flags.page) query.page = flags.page;
|
|
457
|
-
if (flags.limit) query.limit = flags.limit;
|
|
458
|
-
if (flags.app) query.appKey = flags.app;
|
|
459
|
-
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
460
|
-
if (flags.status) query.status = flags.status;
|
|
461
|
-
if (flags.search) query.search = flags.search;
|
|
462
|
-
if (flags.view) query.view = flags.view;
|
|
463
|
-
if (flags.approvalStatus || flags['approval-status']) {
|
|
464
|
-
query.approvalStatus = flags.approvalStatus || flags['approval-status'];
|
|
465
|
-
}
|
|
466
|
-
const data = await api.get('/blocklist', { query });
|
|
548
|
+
const query = {};
|
|
549
|
+
if (flags.page) query.page = flags.page;
|
|
550
|
+
if (flags.limit) query.limit = flags.limit;
|
|
551
|
+
if (flags.app) query.appKey = flags.app;
|
|
552
|
+
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
553
|
+
if (flags.status) query.status = flags.status;
|
|
554
|
+
if (flags.search) query.search = flags.search;
|
|
555
|
+
if (flags.view) query.view = flags.view;
|
|
556
|
+
if (flags.approvalStatus || flags['approval-status']) {
|
|
557
|
+
query.approvalStatus = flags.approvalStatus || flags['approval-status'];
|
|
558
|
+
}
|
|
559
|
+
const data = await api.get('/blocklist', { query });
|
|
467
560
|
const items = data.blockedIps || [];
|
|
468
561
|
s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
469
562
|
|
|
@@ -472,17 +565,17 @@ async function blocklistList(args, flags) {
|
|
|
472
565
|
console.log('');
|
|
473
566
|
const rows = items.map(b => [
|
|
474
567
|
ui.c.dim(ui.truncate(b._id, 12)),
|
|
475
|
-
ui.c.red(describeBlockTarget(b)),
|
|
476
|
-
ui.truncate(b.reason || '', 40),
|
|
477
|
-
b.source || '—',
|
|
478
|
-
b.status || 'active',
|
|
479
|
-
b.applicationKey || ui.c.dim('all apps'),
|
|
480
|
-
b.environment || ui.c.dim('all envs'),
|
|
481
|
-
ui.timeAgo(b.createdAt),
|
|
482
|
-
b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
|
|
483
|
-
b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
484
|
-
]);
|
|
485
|
-
ui.table(['ID', 'Target', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
|
|
568
|
+
ui.c.red(describeBlockTarget(b)),
|
|
569
|
+
ui.truncate(b.reason || '', 40),
|
|
570
|
+
b.source || '—',
|
|
571
|
+
b.status || 'active',
|
|
572
|
+
b.applicationKey || ui.c.dim('all apps'),
|
|
573
|
+
b.environment || ui.c.dim('all envs'),
|
|
574
|
+
ui.timeAgo(b.createdAt),
|
|
575
|
+
b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
|
|
576
|
+
b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
577
|
+
]);
|
|
578
|
+
ui.table(['ID', 'Target', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
|
|
486
579
|
console.log('');
|
|
487
580
|
} catch (err) {
|
|
488
581
|
s.fail('Failed to fetch blocklist');
|
|
@@ -498,21 +591,21 @@ async function blocklistAdd(args, flags) {
|
|
|
498
591
|
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
499
592
|
}
|
|
500
593
|
|
|
501
|
-
const body = { ip };
|
|
502
|
-
if (flags.reason) body.reason = flags.reason;
|
|
503
|
-
if (flags.duration) body.duration = flags.duration;
|
|
504
|
-
if (flags.app) body.appKey = flags.app;
|
|
505
|
-
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
506
|
-
if (flags.route || flags.path || flags.pattern) body.pathPattern = flags.route || flags.path || flags.pattern;
|
|
507
|
-
if (flags.mode || flags['path-mode']) body.pathMatchMode = flags.mode || flags['path-mode'];
|
|
508
|
-
if (flags.method) body.method = flags.method;
|
|
509
|
-
|
|
510
|
-
const target = describeBlockTarget({ ip, method: body.method, pathPattern: body.pathPattern, pathMatchMode: body.pathMatchMode });
|
|
511
|
-
const s = ui.spinner(`Blocking ${target}`);
|
|
512
|
-
try {
|
|
513
|
-
const data = await api.post('/blocklist', body);
|
|
514
|
-
s.stop(`${target} added to blocklist`);
|
|
515
|
-
if (flags.json) ui.json(data);
|
|
594
|
+
const body = { ip };
|
|
595
|
+
if (flags.reason) body.reason = flags.reason;
|
|
596
|
+
if (flags.duration) body.duration = flags.duration;
|
|
597
|
+
if (flags.app) body.appKey = flags.app;
|
|
598
|
+
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
599
|
+
if (flags.route || flags.path || flags.pattern) body.pathPattern = flags.route || flags.path || flags.pattern;
|
|
600
|
+
if (flags.mode || flags['path-mode']) body.pathMatchMode = flags.mode || flags['path-mode'];
|
|
601
|
+
if (flags.method) body.method = flags.method;
|
|
602
|
+
|
|
603
|
+
const target = describeBlockTarget({ ip, method: body.method, pathPattern: body.pathPattern, pathMatchMode: body.pathMatchMode });
|
|
604
|
+
const s = ui.spinner(`Blocking ${target}`);
|
|
605
|
+
try {
|
|
606
|
+
const data = await api.post('/blocklist', body);
|
|
607
|
+
s.stop(`${target} added to blocklist`);
|
|
608
|
+
if (flags.json) ui.json(data);
|
|
516
609
|
} catch (err) {
|
|
517
610
|
s.fail('Failed to add to blocklist');
|
|
518
611
|
throw err;
|
|
@@ -521,28 +614,28 @@ async function blocklistAdd(args, flags) {
|
|
|
521
614
|
|
|
522
615
|
async function blocklistRemove(args, flags) {
|
|
523
616
|
requireAuth();
|
|
524
|
-
const id = args[0];
|
|
525
|
-
if (!id) {
|
|
526
|
-
ui.error('Blocklist entry ID required. Usage: securenow blocklist unblock <id> [--reason <reason>]');
|
|
527
|
-
process.exit(1);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (!flags.force && !flags.yes) {
|
|
531
|
-
const ok = await ui.confirm('Unblock this IP? Firewall enforcement stops, but block report and history stay saved.');
|
|
532
|
-
if (!ok) { ui.info('Cancelled'); return; }
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const s = ui.spinner('Unblocking IP');
|
|
536
|
-
try {
|
|
537
|
-
const body = {};
|
|
538
|
-
if (flags.reason) body.reason = flags.reason;
|
|
539
|
-
await api.post(`/blocklist/${id}/unblock`, body);
|
|
540
|
-
s.stop('Unblocked; block report and history retained');
|
|
541
|
-
} catch (err) {
|
|
542
|
-
s.fail('Failed to unblock IP');
|
|
543
|
-
throw err;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
617
|
+
const id = args[0];
|
|
618
|
+
if (!id) {
|
|
619
|
+
ui.error('Blocklist entry ID required. Usage: securenow blocklist unblock <id> [--reason <reason>]');
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!flags.force && !flags.yes) {
|
|
624
|
+
const ok = await ui.confirm('Unblock this IP? Firewall enforcement stops, but block report and history stay saved.');
|
|
625
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const s = ui.spinner('Unblocking IP');
|
|
629
|
+
try {
|
|
630
|
+
const body = {};
|
|
631
|
+
if (flags.reason) body.reason = flags.reason;
|
|
632
|
+
await api.post(`/blocklist/${id}/unblock`, body);
|
|
633
|
+
s.stop('Unblocked; block report and history retained');
|
|
634
|
+
} catch (err) {
|
|
635
|
+
s.fail('Failed to unblock IP');
|
|
636
|
+
throw err;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
546
639
|
|
|
547
640
|
async function blocklistStats(args, flags) {
|
|
548
641
|
requireAuth();
|
|
@@ -577,12 +670,12 @@ async function allowlistList(args, flags) {
|
|
|
577
670
|
requireAuth();
|
|
578
671
|
const s = ui.spinner('Fetching allowlist');
|
|
579
672
|
try {
|
|
580
|
-
const query = {};
|
|
581
|
-
if (flags.page) query.page = flags.page;
|
|
582
|
-
if (flags.limit) query.limit = flags.limit;
|
|
583
|
-
if (flags.app) query.appKey = flags.app;
|
|
584
|
-
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
585
|
-
const data = await api.get('/allowlist', { query });
|
|
673
|
+
const query = {};
|
|
674
|
+
if (flags.page) query.page = flags.page;
|
|
675
|
+
if (flags.limit) query.limit = flags.limit;
|
|
676
|
+
if (flags.app) query.appKey = flags.app;
|
|
677
|
+
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
678
|
+
const data = await api.get('/allowlist', { query });
|
|
586
679
|
const items = data.allowedIps || [];
|
|
587
680
|
s.stop(`Found ${items.length} allowed IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
588
681
|
|
|
@@ -591,15 +684,15 @@ async function allowlistList(args, flags) {
|
|
|
591
684
|
console.log('');
|
|
592
685
|
const rows = items.map(a => [
|
|
593
686
|
ui.c.dim(ui.truncate(a._id, 12)),
|
|
594
|
-
ui.c.green(a.ip || '—'),
|
|
595
|
-
a.label || ui.c.dim('—'),
|
|
596
|
-
ui.truncate(a.reason || '', 40),
|
|
597
|
-
a.applicationKey || ui.c.dim('all apps'),
|
|
598
|
-
a.environment || ui.c.dim('all envs'),
|
|
599
|
-
ui.timeAgo(a.createdAt),
|
|
600
|
-
a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
601
|
-
]);
|
|
602
|
-
ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'App', 'Env', 'Added', 'Expires'], rows);
|
|
687
|
+
ui.c.green(a.ip || '—'),
|
|
688
|
+
a.label || ui.c.dim('—'),
|
|
689
|
+
ui.truncate(a.reason || '', 40),
|
|
690
|
+
a.applicationKey || ui.c.dim('all apps'),
|
|
691
|
+
a.environment || ui.c.dim('all envs'),
|
|
692
|
+
ui.timeAgo(a.createdAt),
|
|
693
|
+
a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
694
|
+
]);
|
|
695
|
+
ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'App', 'Env', 'Added', 'Expires'], rows);
|
|
603
696
|
console.log('');
|
|
604
697
|
} catch (err) {
|
|
605
698
|
s.fail('Failed to fetch allowlist');
|
|
@@ -615,25 +708,25 @@ async function allowlistAdd(args, flags) {
|
|
|
615
708
|
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
616
709
|
}
|
|
617
710
|
|
|
618
|
-
const body = { ip };
|
|
619
|
-
if (flags.label) body.label = flags.label;
|
|
620
|
-
if (flags.reason) body.reason = flags.reason;
|
|
621
|
-
if (flags.app) {
|
|
622
|
-
body.applicationsAll = false;
|
|
623
|
-
body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
|
|
624
|
-
}
|
|
625
|
-
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
626
|
-
|
|
627
|
-
if (!flags.force && !flags.yes) {
|
|
628
|
-
ui.warn('IP Allowlist is deny-by-default: once active, only listed IPs can reach the scoped app/environment and all others are blocked.');
|
|
629
|
-
ui.info('Use `securenow trusted add` instead if this IP should be trusted without locking out normal visitors.');
|
|
630
|
-
const ok = await ui.confirm('Add this IP to the restrictive allowlist?');
|
|
631
|
-
if (!ok) { ui.info('Cancelled'); return; }
|
|
632
|
-
}
|
|
633
|
-
body.allowlistDenyAllApproved = true;
|
|
634
|
-
|
|
635
|
-
const s = ui.spinner(`Allowing ${ip}`);
|
|
636
|
-
try {
|
|
711
|
+
const body = { ip };
|
|
712
|
+
if (flags.label) body.label = flags.label;
|
|
713
|
+
if (flags.reason) body.reason = flags.reason;
|
|
714
|
+
if (flags.app) {
|
|
715
|
+
body.applicationsAll = false;
|
|
716
|
+
body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
|
|
717
|
+
}
|
|
718
|
+
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
719
|
+
|
|
720
|
+
if (!flags.force && !flags.yes) {
|
|
721
|
+
ui.warn('IP Allowlist is deny-by-default: once active, only listed IPs can reach the scoped app/environment and all others are blocked.');
|
|
722
|
+
ui.info('Use `securenow trusted add` instead if this IP should be trusted without locking out normal visitors.');
|
|
723
|
+
const ok = await ui.confirm('Add this IP to the restrictive allowlist?');
|
|
724
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
725
|
+
}
|
|
726
|
+
body.allowlistDenyAllApproved = true;
|
|
727
|
+
|
|
728
|
+
const s = ui.spinner(`Allowing ${ip}`);
|
|
729
|
+
try {
|
|
637
730
|
await api.post('/allowlist', body);
|
|
638
731
|
s.stop(`${ip} added to allowlist`);
|
|
639
732
|
} catch (err) {
|
|
@@ -695,10 +788,10 @@ async function trustedList(args, flags) {
|
|
|
695
788
|
requireAuth();
|
|
696
789
|
const s = ui.spinner('Fetching trusted IPs');
|
|
697
790
|
try {
|
|
698
|
-
const query = {};
|
|
699
|
-
if (flags.app) query.appKey = flags.app;
|
|
700
|
-
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
701
|
-
const data = await api.get('/trusted-ips', { query });
|
|
791
|
+
const query = {};
|
|
792
|
+
if (flags.app) query.appKey = flags.app;
|
|
793
|
+
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
794
|
+
const data = await api.get('/trusted-ips', { query });
|
|
702
795
|
const items = data.trustedIps || [];
|
|
703
796
|
s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
|
|
704
797
|
|
|
@@ -706,14 +799,14 @@ async function trustedList(args, flags) {
|
|
|
706
799
|
|
|
707
800
|
console.log('');
|
|
708
801
|
const rows = items.map(t => [
|
|
709
|
-
ui.c.dim(ui.truncate(t._id, 12)),
|
|
710
|
-
ui.c.green(t.ip || t.cidr || '—'),
|
|
711
|
-
t.label || t.description || ui.c.dim('—'),
|
|
712
|
-
t.applicationKey || ui.c.dim('all apps'),
|
|
713
|
-
t.environment || ui.c.dim('all envs'),
|
|
714
|
-
ui.timeAgo(t.createdAt),
|
|
715
|
-
]);
|
|
716
|
-
ui.table(['ID', 'IP/CIDR', 'Label', 'App', 'Env', 'Added'], rows);
|
|
802
|
+
ui.c.dim(ui.truncate(t._id, 12)),
|
|
803
|
+
ui.c.green(t.ip || t.cidr || '—'),
|
|
804
|
+
t.label || t.description || ui.c.dim('—'),
|
|
805
|
+
t.applicationKey || ui.c.dim('all apps'),
|
|
806
|
+
t.environment || ui.c.dim('all envs'),
|
|
807
|
+
ui.timeAgo(t.createdAt),
|
|
808
|
+
]);
|
|
809
|
+
ui.table(['ID', 'IP/CIDR', 'Label', 'App', 'Env', 'Added'], rows);
|
|
717
810
|
console.log('');
|
|
718
811
|
} catch (err) {
|
|
719
812
|
s.fail('Failed to fetch trusted IPs');
|
|
@@ -729,15 +822,15 @@ async function trustedAdd(args, flags) {
|
|
|
729
822
|
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
730
823
|
}
|
|
731
824
|
|
|
732
|
-
const body = { ip };
|
|
733
|
-
if (flags.label) body.label = flags.label;
|
|
734
|
-
if (flags.description) body.description = flags.description;
|
|
735
|
-
if (flags.note) body.note = flags.note;
|
|
736
|
-
if (flags.app) {
|
|
737
|
-
body.applicationsAll = false;
|
|
738
|
-
body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
|
|
739
|
-
}
|
|
740
|
-
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
825
|
+
const body = { ip };
|
|
826
|
+
if (flags.label) body.label = flags.label;
|
|
827
|
+
if (flags.description) body.description = flags.description;
|
|
828
|
+
if (flags.note) body.note = flags.note;
|
|
829
|
+
if (flags.app) {
|
|
830
|
+
body.applicationsAll = false;
|
|
831
|
+
body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
|
|
832
|
+
}
|
|
833
|
+
if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
|
|
741
834
|
|
|
742
835
|
const s = ui.spinner(`Adding ${ip} to trusted IPs`);
|
|
743
836
|
try {
|
|
@@ -795,7 +888,7 @@ async function forensicsQuery(args, flags) {
|
|
|
795
888
|
|
|
796
889
|
const s = ui.spinner('Submitting forensic query');
|
|
797
890
|
try {
|
|
798
|
-
const body = { query, environment: resolveEnvironment(flags, 'production') };
|
|
891
|
+
const body = { query, environment: resolveEnvironment(flags, 'production') };
|
|
799
892
|
const resolved = await resolveAppId(flags);
|
|
800
893
|
if (resolved) {
|
|
801
894
|
body.applicationId = resolved.id;
|
|
@@ -922,10 +1015,10 @@ async function forensicsChat(args, flags) {
|
|
|
922
1015
|
console.log(ui.c.dim(` App: ${resolved.key} (${resolved.id})`));
|
|
923
1016
|
console.log(ui.c.dim(' Type your question, or "exit" to quit.\n'));
|
|
924
1017
|
|
|
925
|
-
const environment = resolveEnvironment(flags, 'production');
|
|
926
|
-
console.log(ui.c.dim(` Env: ${environment}`));
|
|
927
|
-
|
|
928
|
-
let conversationId = null;
|
|
1018
|
+
const environment = resolveEnvironment(flags, 'production');
|
|
1019
|
+
console.log(ui.c.dim(` Env: ${environment}`));
|
|
1020
|
+
|
|
1021
|
+
let conversationId = null;
|
|
929
1022
|
|
|
930
1023
|
const readline = require('readline');
|
|
931
1024
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -945,7 +1038,7 @@ async function forensicsChat(args, flags) {
|
|
|
945
1038
|
|
|
946
1039
|
const s = ui.spinner('Thinking');
|
|
947
1040
|
try {
|
|
948
|
-
const body = { message, applicationKey: resolved.key, environment };
|
|
1041
|
+
const body = { message, applicationKey: resolved.key, environment };
|
|
949
1042
|
if (conversationId) body.conversationId = conversationId;
|
|
950
1043
|
|
|
951
1044
|
const chatRes = await api.post('/forensics/chat', body);
|
|
@@ -955,7 +1048,7 @@ async function forensicsChat(args, flags) {
|
|
|
955
1048
|
let result;
|
|
956
1049
|
for (let i = 0; i < 150; i++) {
|
|
957
1050
|
await new Promise(r => setTimeout(r, 2000));
|
|
958
|
-
result = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
1051
|
+
result = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
959
1052
|
if (result.status === 'complete' || result.status === 'failed' || result.status === 'awaiting_confirmation') break;
|
|
960
1053
|
const progress = result._progress;
|
|
961
1054
|
if (progress?.action) s.update(progress.action);
|
|
@@ -973,11 +1066,11 @@ async function forensicsChat(args, flags) {
|
|
|
973
1066
|
const proceed = await ui.confirm('Proceed with this query?');
|
|
974
1067
|
if (proceed) {
|
|
975
1068
|
const cs = ui.spinner('Executing query');
|
|
976
|
-
await api.post(`/forensics/chat/confirm/${conversationId}`, { environment });
|
|
1069
|
+
await api.post(`/forensics/chat/confirm/${conversationId}`, { environment });
|
|
977
1070
|
let confirmResult;
|
|
978
1071
|
for (let i = 0; i < 90; i++) {
|
|
979
1072
|
await new Promise(r => setTimeout(r, 2000));
|
|
980
|
-
confirmResult = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
1073
|
+
confirmResult = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
981
1074
|
if (confirmResult.status !== 'processing') break;
|
|
982
1075
|
}
|
|
983
1076
|
cs.stop('Done');
|
|
@@ -1067,7 +1160,7 @@ async function ipLookup(args, flags) {
|
|
|
1067
1160
|
if (data.domain) pairs.push(['Domain', data.domain]);
|
|
1068
1161
|
if (data.isp) pairs.push(['ISP', data.isp]);
|
|
1069
1162
|
if (data.usageType) pairs.push(['Usage Type', data.usageType]);
|
|
1070
|
-
if (data.abuseConfidenceScore != null) pairs.push(['SecureNow IPDB Score', `${data.abuseConfidenceScore}/100`]);
|
|
1163
|
+
if (data.abuseConfidenceScore != null) pairs.push(['SecureNow IPDB Score', `${data.abuseConfidenceScore}/100`]);
|
|
1071
1164
|
if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
|
|
1072
1165
|
if (data.verdict) pairs.push(['Verdict', data.verdict]);
|
|
1073
1166
|
if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
|
|
@@ -1111,14 +1204,14 @@ async function ipTraces(args, flags) {
|
|
|
1111
1204
|
process.exit(1);
|
|
1112
1205
|
}
|
|
1113
1206
|
|
|
1114
|
-
const s = ui.spinner(`Fetching traces for ${ip}`);
|
|
1115
|
-
try {
|
|
1116
|
-
const query = {};
|
|
1117
|
-
const appKey = resolveApp(flags);
|
|
1118
|
-
if (appKey) query.appKeys = appKey;
|
|
1119
|
-
const environment = resolveEnvironment(flags, null);
|
|
1120
|
-
if (environment) query.environment = environment;
|
|
1121
|
-
const data = await api.get(`/ip/${ip}/traces`, { query });
|
|
1207
|
+
const s = ui.spinner(`Fetching traces for ${ip}`);
|
|
1208
|
+
try {
|
|
1209
|
+
const query = {};
|
|
1210
|
+
const appKey = resolveApp(flags);
|
|
1211
|
+
if (appKey) query.appKeys = appKey;
|
|
1212
|
+
const environment = resolveEnvironment(flags, null);
|
|
1213
|
+
if (environment) query.environment = environment;
|
|
1214
|
+
const data = await api.get(`/ip/${ip}/traces`, { query });
|
|
1122
1215
|
const traces = data.traces || [];
|
|
1123
1216
|
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
1124
1217
|
|
|
@@ -1233,11 +1326,11 @@ async function analytics(args, flags) {
|
|
|
1233
1326
|
|
|
1234
1327
|
module.exports = {
|
|
1235
1328
|
alertRulesRoute,
|
|
1236
|
-
alertRulesList,
|
|
1237
|
-
alertRuleShow,
|
|
1238
|
-
alertRuleUpdate,
|
|
1239
|
-
alertRuleTest,
|
|
1240
|
-
alertRuleExclusions,
|
|
1329
|
+
alertRulesList,
|
|
1330
|
+
alertRuleShow,
|
|
1331
|
+
alertRuleUpdate,
|
|
1332
|
+
alertRuleTest,
|
|
1333
|
+
alertRuleExclusions,
|
|
1241
1334
|
alertChannelsList,
|
|
1242
1335
|
alertHistoryList,
|
|
1243
1336
|
blocklistList,
|