securenow 6.0.1 → 6.1.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/CONSUMING-APPS-GUIDE.md +455 -0
- package/NPM_README.md +2029 -0
- package/README.md +297 -40
- package/SKILL-API.md +634 -0
- package/SKILL-CLI.md +454 -0
- package/cidr.js +83 -0
- package/cli/apps.js +585 -0
- package/cli/auth.js +280 -0
- package/cli/client.js +115 -0
- package/cli/config.js +173 -0
- package/cli/diagnostics.js +387 -0
- package/cli/firewall.js +100 -0
- package/cli/fp.js +638 -0
- package/cli/init.js +201 -0
- package/cli/monitor.js +440 -0
- package/cli/run.js +148 -0
- package/cli/security.js +980 -0
- package/cli/ui.js +386 -0
- package/cli/utils.js +127 -0
- package/cli.js +466 -455
- package/console-instrumentation.js +147 -136
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
- package/docs/API-KEYS-GUIDE.md +233 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP-SUMMARY.md +331 -0
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-FIX.md +261 -0
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +1 -35
- package/docs/COMPLETION-REPORT.md +408 -0
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +880 -652
- package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
- package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
- package/docs/FINAL-SOLUTION.md +335 -0
- package/docs/FIREWALL-GUIDE.md +426 -0
- package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
- package/docs/INDEX.md +22 -4
- package/docs/LOGGING-GUIDE.md +701 -708
- package/docs/LOGGING-QUICKSTART.md +234 -255
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
- package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
- package/docs/NEXTJS-GUIDE.md +14 -14
- package/docs/NEXTJS-QUICKSTART.md +1 -1
- package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
- package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
- package/docs/NUXT-GUIDE.md +166 -0
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +19 -10
- package/docs/SOLUTION-SUMMARY.md +312 -0
- package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
- package/examples/README.md +6 -6
- package/examples/instrumentation-with-auto-capture.ts +1 -1
- package/examples/nextjs-env-example.txt +2 -2
- package/examples/nextjs-instrumentation.js +1 -1
- package/examples/nextjs-instrumentation.ts +1 -1
- package/examples/nextjs-with-logging-example.md +6 -6
- package/examples/nextjs-with-options.ts +1 -1
- package/examples/test-nextjs-setup.js +1 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-only.js +38 -0
- package/firewall-tcp.js +74 -0
- package/firewall.js +720 -0
- package/free-trial-banner.js +174 -0
- package/nextjs-auto-capture.js +199 -207
- package/nextjs-middleware.js +186 -181
- package/nextjs-webpack-config.js +88 -53
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +224 -198
- package/nuxt-server-plugin.mjs +423 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +67 -45
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +39 -4
- package/resolve-ip.js +77 -0
- package/tracing.d.ts +2 -1
- package/tracing.js +333 -31
- package/web-vite.mjs +239 -156
- package/LICENSE +0 -15
package/cli/security.js
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { api, requireAuth } = require('./client');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const ui = require('./ui');
|
|
6
|
+
|
|
7
|
+
function resolveApp(flags) {
|
|
8
|
+
return flags.app || config.getDefaultApp();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ── Alert Rules ──
|
|
12
|
+
|
|
13
|
+
function formatRuleApplicationsCell(rule) {
|
|
14
|
+
if (rule.applicationsAll) {
|
|
15
|
+
return ui.c.cyan('all apps');
|
|
16
|
+
}
|
|
17
|
+
const keys = rule.applications || [];
|
|
18
|
+
if (keys.length === 0) return ui.c.dim('—');
|
|
19
|
+
const joined = keys.join(', ');
|
|
20
|
+
return joined.length > 48 ? `${joined.slice(0, 45)}…` : joined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ruleStatusBadge(rule) {
|
|
24
|
+
const st = rule.status || (rule.enabled !== false ? 'Active' : 'Disabled');
|
|
25
|
+
if (st === 'Active') return ui.statusBadge('active');
|
|
26
|
+
if (st === 'Disabled') return ui.statusBadge('disabled');
|
|
27
|
+
if (st === 'Paused') return ui.statusBadge('paused');
|
|
28
|
+
return st;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Dispatch: list | show <id> | update <id> ... */
|
|
32
|
+
async function alertRulesRoute(args, flags) {
|
|
33
|
+
const sub = args[0];
|
|
34
|
+
if (sub === 'show') {
|
|
35
|
+
return alertRuleShow(args.slice(1), flags);
|
|
36
|
+
}
|
|
37
|
+
if (sub === 'update') {
|
|
38
|
+
return alertRuleUpdate(args.slice(1), flags);
|
|
39
|
+
}
|
|
40
|
+
if (sub === 'list') {
|
|
41
|
+
return alertRulesList(args.slice(1), flags);
|
|
42
|
+
}
|
|
43
|
+
return alertRulesList(args, flags);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function alertRulesList(args, flags) {
|
|
47
|
+
requireAuth();
|
|
48
|
+
const s = ui.spinner('Fetching alert rules');
|
|
49
|
+
try {
|
|
50
|
+
const data = await api.get('/alert-rules');
|
|
51
|
+
const rules = data.alertRules || [];
|
|
52
|
+
s.stop(`Found ${rules.length} rule${rules.length !== 1 ? 's' : ''}`);
|
|
53
|
+
|
|
54
|
+
if (flags.json) { ui.json(rules); return; }
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
const rows = rules.map((r) => [
|
|
58
|
+
ui.c.dim(ui.truncate(r._id, 12)),
|
|
59
|
+
r.name || '—',
|
|
60
|
+
ruleStatusBadge(r),
|
|
61
|
+
formatRuleApplicationsCell(r),
|
|
62
|
+
r.schedule?.enabled === false ? ui.c.dim('off') : (r.schedule?.description || r.schedule?.cronExpression || '—'),
|
|
63
|
+
]);
|
|
64
|
+
ui.table(['ID', 'Name', 'Status', 'Applications', 'Schedule'], rows);
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(ui.c.dim(' Applications: "all apps" = all current & future active apps. show <id> · update <id> --applications-all · --apps k1,k2'));
|
|
67
|
+
console.log('');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
s.fail('Failed to fetch alert rules');
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function alertRuleShow(args, flags) {
|
|
75
|
+
requireAuth();
|
|
76
|
+
const id = args[0];
|
|
77
|
+
if (!id) {
|
|
78
|
+
ui.error('Usage: securenow alerts rules show <rule-id>');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const s = ui.spinner('Fetching alert rule');
|
|
82
|
+
try {
|
|
83
|
+
const data = await api.get(`/alert-rules/${id}`);
|
|
84
|
+
const r = data.alertRule;
|
|
85
|
+
s.stop('');
|
|
86
|
+
|
|
87
|
+
if (flags.json) {
|
|
88
|
+
ui.json(r);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log('');
|
|
93
|
+
ui.heading(r.name || 'Alert rule');
|
|
94
|
+
console.log('');
|
|
95
|
+
const appLine = r.applicationsAll
|
|
96
|
+
? 'All applications (current & future)'
|
|
97
|
+
: (r.applications && r.applications.length > 0 ? r.applications.join(', ') : '—');
|
|
98
|
+
ui.keyValue([
|
|
99
|
+
['ID', r._id || r.id || id],
|
|
100
|
+
['Status', r.status || '—'],
|
|
101
|
+
['System rule', r.isSystem ? 'yes' : 'no'],
|
|
102
|
+
['Applications', appLine],
|
|
103
|
+
['Schedule', r.schedule?.enabled === false ? 'disabled' : (r.schedule?.description || r.schedule?.cronExpression || '—')],
|
|
104
|
+
['Throttle', r.throttle?.enabled ? `${r.throttle.minutes} min` : 'off'],
|
|
105
|
+
['Query', r.queryMappingId?.name || '—'],
|
|
106
|
+
]);
|
|
107
|
+
console.log('');
|
|
108
|
+
} catch (err) {
|
|
109
|
+
s.fail('Failed to fetch alert rule');
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function alertRuleUpdate(args, flags) {
|
|
115
|
+
requireAuth();
|
|
116
|
+
const id = args[0];
|
|
117
|
+
if (!id) {
|
|
118
|
+
ui.error('Usage: securenow alerts rules update <rule-id> (--applications-all | --apps <k1,k2>)');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const hasAll =
|
|
123
|
+
flags['applications-all'] === true ||
|
|
124
|
+
flags['applications-all'] === 'true';
|
|
125
|
+
const hasNoAll =
|
|
126
|
+
flags['no-applications-all'] === true ||
|
|
127
|
+
flags['no-applications-all'] === 'true';
|
|
128
|
+
const appsStr = flags.apps;
|
|
129
|
+
|
|
130
|
+
if (hasAll && hasNoAll) {
|
|
131
|
+
ui.error('Use only one of --applications-all or --no-applications-all');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
if (hasAll && appsStr) {
|
|
135
|
+
ui.error('Cannot combine --applications-all with --apps');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body = {};
|
|
140
|
+
if (hasAll) {
|
|
141
|
+
body.applicationsAll = true;
|
|
142
|
+
body.applications = [];
|
|
143
|
+
} else if (hasNoAll || appsStr) {
|
|
144
|
+
body.applicationsAll = false;
|
|
145
|
+
if (!appsStr) {
|
|
146
|
+
ui.error('Pass --apps key1,key2 when using --no-applications-all, or omit --no-applications-all and use --apps only');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
body.applications = String(appsStr)
|
|
150
|
+
.split(',')
|
|
151
|
+
.map((x) => x.trim())
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
if (body.applications.length === 0) {
|
|
154
|
+
ui.error('No application keys parsed from --apps');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
ui.error('Nothing to update. Use --applications-all or --apps key1,key2');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const s = ui.spinner('Updating alert rule');
|
|
163
|
+
try {
|
|
164
|
+
await api.put(`/alert-rules/${id}`, body);
|
|
165
|
+
s.stop('Updated');
|
|
166
|
+
ui.success('Alert rule updated');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
s.fail('Failed to update alert rule');
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Alert Channels ──
|
|
174
|
+
|
|
175
|
+
async function alertChannelsList(args, flags) {
|
|
176
|
+
requireAuth();
|
|
177
|
+
const s = ui.spinner('Fetching alert channels');
|
|
178
|
+
try {
|
|
179
|
+
const data = await api.get('/alert-channels');
|
|
180
|
+
const channels = data.alertChannels || [];
|
|
181
|
+
s.stop(`Found ${channels.length} channel${channels.length !== 1 ? 's' : ''}`);
|
|
182
|
+
|
|
183
|
+
if (flags.json) { ui.json(channels); return; }
|
|
184
|
+
|
|
185
|
+
console.log('');
|
|
186
|
+
const rows = channels.map(ch => [
|
|
187
|
+
ui.c.dim(ui.truncate(ch._id, 12)),
|
|
188
|
+
ch.name || '—',
|
|
189
|
+
ch.type || '—',
|
|
190
|
+
ui.statusBadge(ch.enabled !== false ? 'enabled' : 'disabled'),
|
|
191
|
+
ch.target || ch.email || ch.webhookUrl || '—',
|
|
192
|
+
]);
|
|
193
|
+
ui.table(['ID', 'Name', 'Type', 'Status', 'Target'], rows);
|
|
194
|
+
console.log('');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
s.fail('Failed to fetch alert channels');
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Alert History ──
|
|
202
|
+
|
|
203
|
+
async function alertHistoryList(args, flags) {
|
|
204
|
+
requireAuth();
|
|
205
|
+
const s = ui.spinner('Fetching alert history');
|
|
206
|
+
try {
|
|
207
|
+
const query = { limit: flags.limit || 20 };
|
|
208
|
+
const data = await api.get('/alert-history', { query });
|
|
209
|
+
const history = data.alerts || [];
|
|
210
|
+
s.stop(`Found ${history.length} alert${history.length !== 1 ? 's' : ''}${data.totalItems ? ` (${data.totalItems} total)` : ''}`);
|
|
211
|
+
|
|
212
|
+
if (flags.json) { ui.json(data); return; }
|
|
213
|
+
|
|
214
|
+
console.log('');
|
|
215
|
+
const rows = history.map(h => [
|
|
216
|
+
ui.c.dim(ui.truncate(h._id, 12)),
|
|
217
|
+
h.ruleName || h.type || '—',
|
|
218
|
+
h.severity ? ui.statusBadge(h.severity) : '—',
|
|
219
|
+
ui.truncate(h.message || h.description || '', 40),
|
|
220
|
+
h.serviceName || '—',
|
|
221
|
+
ui.timeAgo(h.triggeredAt || h.createdAt),
|
|
222
|
+
]);
|
|
223
|
+
ui.table(['ID', 'Rule', 'Severity', 'Message', 'App', 'Triggered'], rows);
|
|
224
|
+
console.log('');
|
|
225
|
+
} catch (err) {
|
|
226
|
+
s.fail('Failed to fetch alert history');
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Blocklist ──
|
|
232
|
+
|
|
233
|
+
async function blocklistList(args, flags) {
|
|
234
|
+
requireAuth();
|
|
235
|
+
const s = ui.spinner('Fetching blocklist');
|
|
236
|
+
try {
|
|
237
|
+
const data = await api.get('/blocklist');
|
|
238
|
+
const items = data.blockedIps || [];
|
|
239
|
+
s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
240
|
+
|
|
241
|
+
if (flags.json) { ui.json(data); return; }
|
|
242
|
+
|
|
243
|
+
console.log('');
|
|
244
|
+
const rows = items.map(b => [
|
|
245
|
+
ui.c.dim(ui.truncate(b._id, 12)),
|
|
246
|
+
ui.c.red(b.ip || b.cidr || '—'),
|
|
247
|
+
ui.truncate(b.reason || '', 40),
|
|
248
|
+
b.source || '—',
|
|
249
|
+
ui.timeAgo(b.createdAt),
|
|
250
|
+
b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
251
|
+
]);
|
|
252
|
+
ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'Added', 'Expires'], rows);
|
|
253
|
+
console.log('');
|
|
254
|
+
} catch (err) {
|
|
255
|
+
s.fail('Failed to fetch blocklist');
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function blocklistAdd(args, flags) {
|
|
261
|
+
requireAuth();
|
|
262
|
+
let ip = args[0];
|
|
263
|
+
if (!ip) {
|
|
264
|
+
ip = await ui.prompt('IP address or CIDR to block');
|
|
265
|
+
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const body = { ip };
|
|
269
|
+
if (flags.reason) body.reason = flags.reason;
|
|
270
|
+
if (flags.duration) body.duration = flags.duration;
|
|
271
|
+
if (flags.app) body.serviceName = flags.app;
|
|
272
|
+
|
|
273
|
+
const s = ui.spinner(`Blocking ${ip}`);
|
|
274
|
+
try {
|
|
275
|
+
await api.post('/blocklist', body);
|
|
276
|
+
s.stop(`${ip} added to blocklist`);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
s.fail('Failed to add to blocklist');
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function blocklistRemove(args, flags) {
|
|
284
|
+
requireAuth();
|
|
285
|
+
const id = args[0];
|
|
286
|
+
if (!id) {
|
|
287
|
+
ui.error('Blocklist entry ID required. Usage: securenow blocklist remove <id>');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!flags.force && !flags.yes) {
|
|
292
|
+
const ok = await ui.confirm('Remove this IP from blocklist?');
|
|
293
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const s = ui.spinner('Removing from blocklist');
|
|
297
|
+
try {
|
|
298
|
+
await api.delete(`/blocklist/${id}`);
|
|
299
|
+
s.stop('Removed from blocklist');
|
|
300
|
+
} catch (err) {
|
|
301
|
+
s.fail('Failed to remove from blocklist');
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function blocklistStats(args, flags) {
|
|
307
|
+
requireAuth();
|
|
308
|
+
const s = ui.spinner('Fetching blocklist stats');
|
|
309
|
+
try {
|
|
310
|
+
const data = await api.get('/blocklist/stats');
|
|
311
|
+
const stats = data.stats || data;
|
|
312
|
+
s.stop('Stats loaded');
|
|
313
|
+
|
|
314
|
+
if (flags.json) { ui.json(stats); return; }
|
|
315
|
+
|
|
316
|
+
console.log('');
|
|
317
|
+
ui.heading('Blocklist Statistics');
|
|
318
|
+
console.log('');
|
|
319
|
+
ui.keyValue([
|
|
320
|
+
['Total Active', String(stats.totalActive ?? '—')],
|
|
321
|
+
['Total Removed', String(stats.totalRemoved ?? '—')],
|
|
322
|
+
['Manual Blocks', String(stats.manualCount ?? '—')],
|
|
323
|
+
['Automation Blocks', String(stats.automationCount ?? '—')],
|
|
324
|
+
['Active Rules', String(stats.activeAutomationRules ?? '—')],
|
|
325
|
+
]);
|
|
326
|
+
console.log('');
|
|
327
|
+
} catch (err) {
|
|
328
|
+
s.fail('Failed to fetch stats');
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Allowlist ──
|
|
334
|
+
|
|
335
|
+
async function allowlistList(args, flags) {
|
|
336
|
+
requireAuth();
|
|
337
|
+
const s = ui.spinner('Fetching allowlist');
|
|
338
|
+
try {
|
|
339
|
+
const data = await api.get('/allowlist');
|
|
340
|
+
const items = data.allowedIps || [];
|
|
341
|
+
s.stop(`Found ${items.length} allowed IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
342
|
+
|
|
343
|
+
if (flags.json) { ui.json(data); return; }
|
|
344
|
+
|
|
345
|
+
console.log('');
|
|
346
|
+
const rows = items.map(a => [
|
|
347
|
+
ui.c.dim(ui.truncate(a._id, 12)),
|
|
348
|
+
ui.c.green(a.ip || '—'),
|
|
349
|
+
a.label || ui.c.dim('—'),
|
|
350
|
+
ui.truncate(a.reason || '', 40),
|
|
351
|
+
ui.timeAgo(a.createdAt),
|
|
352
|
+
a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
353
|
+
]);
|
|
354
|
+
ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'Added', 'Expires'], rows);
|
|
355
|
+
console.log('');
|
|
356
|
+
} catch (err) {
|
|
357
|
+
s.fail('Failed to fetch allowlist');
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function allowlistAdd(args, flags) {
|
|
363
|
+
requireAuth();
|
|
364
|
+
let ip = args[0];
|
|
365
|
+
if (!ip) {
|
|
366
|
+
ip = await ui.prompt('IP address or CIDR to allow');
|
|
367
|
+
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const body = { ip };
|
|
371
|
+
if (flags.label) body.label = flags.label;
|
|
372
|
+
if (flags.reason) body.reason = flags.reason;
|
|
373
|
+
|
|
374
|
+
const s = ui.spinner(`Allowing ${ip}`);
|
|
375
|
+
try {
|
|
376
|
+
await api.post('/allowlist', body);
|
|
377
|
+
s.stop(`${ip} added to allowlist`);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
s.fail('Failed to add to allowlist');
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function allowlistRemove(args, flags) {
|
|
385
|
+
requireAuth();
|
|
386
|
+
const id = args[0];
|
|
387
|
+
if (!id) {
|
|
388
|
+
ui.error('Allowlist entry ID required. Usage: securenow allowlist remove <id>');
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!flags.force && !flags.yes) {
|
|
393
|
+
const ok = await ui.confirm('Remove this IP from the allowlist?');
|
|
394
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const s = ui.spinner('Removing from allowlist');
|
|
398
|
+
try {
|
|
399
|
+
await api.delete(`/allowlist/${id}`);
|
|
400
|
+
s.stop('Removed from allowlist');
|
|
401
|
+
} catch (err) {
|
|
402
|
+
s.fail('Failed to remove from allowlist');
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function allowlistStats(args, flags) {
|
|
408
|
+
requireAuth();
|
|
409
|
+
const s = ui.spinner('Fetching allowlist stats');
|
|
410
|
+
try {
|
|
411
|
+
const data = await api.get('/allowlist/stats');
|
|
412
|
+
const stats = data.stats || data;
|
|
413
|
+
s.stop('Stats loaded');
|
|
414
|
+
|
|
415
|
+
if (flags.json) { ui.json(stats); return; }
|
|
416
|
+
|
|
417
|
+
console.log('');
|
|
418
|
+
ui.heading('Allowlist Statistics');
|
|
419
|
+
console.log('');
|
|
420
|
+
ui.keyValue([
|
|
421
|
+
['Total Active', String(stats.totalActive ?? '—')],
|
|
422
|
+
['Total Removed', String(stats.totalRemoved ?? '—')],
|
|
423
|
+
]);
|
|
424
|
+
console.log('');
|
|
425
|
+
} catch (err) {
|
|
426
|
+
s.fail('Failed to fetch stats');
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Trusted IPs ──
|
|
432
|
+
|
|
433
|
+
async function trustedList(args, flags) {
|
|
434
|
+
requireAuth();
|
|
435
|
+
const s = ui.spinner('Fetching trusted IPs');
|
|
436
|
+
try {
|
|
437
|
+
const data = await api.get('/trusted-ips');
|
|
438
|
+
const items = data.trustedIps || [];
|
|
439
|
+
s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
|
|
440
|
+
|
|
441
|
+
if (flags.json) { ui.json(items); return; }
|
|
442
|
+
|
|
443
|
+
console.log('');
|
|
444
|
+
const rows = items.map(t => [
|
|
445
|
+
ui.c.dim(ui.truncate(t._id, 12)),
|
|
446
|
+
ui.c.green(t.ip || t.cidr || '—'),
|
|
447
|
+
t.label || t.description || ui.c.dim('—'),
|
|
448
|
+
ui.timeAgo(t.createdAt),
|
|
449
|
+
]);
|
|
450
|
+
ui.table(['ID', 'IP/CIDR', 'Label', 'Added'], rows);
|
|
451
|
+
console.log('');
|
|
452
|
+
} catch (err) {
|
|
453
|
+
s.fail('Failed to fetch trusted IPs');
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function trustedAdd(args, flags) {
|
|
459
|
+
requireAuth();
|
|
460
|
+
let ip = args[0];
|
|
461
|
+
if (!ip) {
|
|
462
|
+
ip = await ui.prompt('IP address or CIDR to trust');
|
|
463
|
+
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const body = { ip };
|
|
467
|
+
if (flags.label) body.label = flags.label;
|
|
468
|
+
if (flags.description) body.description = flags.description;
|
|
469
|
+
|
|
470
|
+
const s = ui.spinner(`Adding ${ip} to trusted IPs`);
|
|
471
|
+
try {
|
|
472
|
+
await api.post('/trusted-ips', body);
|
|
473
|
+
s.stop(`${ip} added to trusted IPs`);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
s.fail('Failed to add trusted IP');
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function trustedRemove(args, flags) {
|
|
481
|
+
requireAuth();
|
|
482
|
+
const id = args[0];
|
|
483
|
+
if (!id) {
|
|
484
|
+
ui.error('Trusted IP entry ID required. Usage: securenow trusted remove <id>');
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!flags.force && !flags.yes) {
|
|
489
|
+
const ok = await ui.confirm('Remove this IP from trusted list?');
|
|
490
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const s = ui.spinner('Removing from trusted IPs');
|
|
494
|
+
try {
|
|
495
|
+
await api.delete(`/trusted-ips/${id}`);
|
|
496
|
+
s.stop('Removed from trusted IPs');
|
|
497
|
+
} catch (err) {
|
|
498
|
+
s.fail('Failed to remove trusted IP');
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── Helpers ──
|
|
504
|
+
|
|
505
|
+
async function resolveAppId(flags) {
|
|
506
|
+
const appKey = flags.app || config.getDefaultApp();
|
|
507
|
+
if (!appKey) return null;
|
|
508
|
+
const data = await api.get('/applications');
|
|
509
|
+
const apps = data.applications || [];
|
|
510
|
+
const match = apps.find(a => a.key === appKey);
|
|
511
|
+
return match ? { id: match._id, key: match.key, name: match.name } : null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── Forensics ──
|
|
515
|
+
|
|
516
|
+
async function forensicsQuery(args, flags) {
|
|
517
|
+
requireAuth();
|
|
518
|
+
const query = args.join(' ');
|
|
519
|
+
if (!query) {
|
|
520
|
+
ui.error('Query required. Usage: securenow forensics "your natural language query" [--app <key>]');
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const s = ui.spinner('Submitting forensic query');
|
|
525
|
+
try {
|
|
526
|
+
const body = { query };
|
|
527
|
+
const resolved = await resolveAppId(flags);
|
|
528
|
+
if (resolved) {
|
|
529
|
+
body.applicationId = resolved.id;
|
|
530
|
+
} else if (flags.instance) {
|
|
531
|
+
body.instanceId = flags.instance;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const job = await api.post('/forensics/query', body);
|
|
535
|
+
const jobId = job.jobId;
|
|
536
|
+
|
|
537
|
+
if (!jobId) {
|
|
538
|
+
s.stop('Query complete');
|
|
539
|
+
if (flags.json) { ui.json(job); return; }
|
|
540
|
+
if (job.result) {
|
|
541
|
+
console.log('');
|
|
542
|
+
if (job.sqlquery) {
|
|
543
|
+
ui.subheading('Generated SQL');
|
|
544
|
+
console.log(`\n ${ui.c.dim(job.sqlquery)}\n`);
|
|
545
|
+
}
|
|
546
|
+
const data = job.result;
|
|
547
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
548
|
+
const headers = Object.keys(data[0]);
|
|
549
|
+
const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
|
|
550
|
+
ui.table(headers, rows);
|
|
551
|
+
} else {
|
|
552
|
+
ui.json(data);
|
|
553
|
+
}
|
|
554
|
+
console.log('');
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
s.update('Processing query...');
|
|
560
|
+
|
|
561
|
+
let result;
|
|
562
|
+
const maxAttempts = 60;
|
|
563
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
564
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
565
|
+
result = await api.get(`/forensics/query/status/${jobId}`);
|
|
566
|
+
if (result.status === 'completed' || result.status === 'failed') break;
|
|
567
|
+
s.update(`Processing query... (${(i + 1) * 2}s)`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (result.status === 'failed') {
|
|
571
|
+
s.fail('Query failed');
|
|
572
|
+
ui.error(result.error || 'Unknown error');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (result.status !== 'completed') {
|
|
577
|
+
s.fail('Query timed out');
|
|
578
|
+
ui.warn('Query is still processing. Check back later.');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
s.stop('Query complete');
|
|
583
|
+
|
|
584
|
+
if (flags.json) { ui.json(result); return; }
|
|
585
|
+
|
|
586
|
+
console.log('');
|
|
587
|
+
if (result.sqlquery) {
|
|
588
|
+
ui.subheading('Generated SQL');
|
|
589
|
+
console.log(`\n ${ui.c.dim(result.sqlquery)}\n`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (result.result) {
|
|
593
|
+
const data = result.result;
|
|
594
|
+
ui.subheading(`Results (${result.rowCount ?? (Array.isArray(data) ? data.length : '?')} rows)`);
|
|
595
|
+
console.log('');
|
|
596
|
+
|
|
597
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
598
|
+
const headers = Object.keys(data[0]);
|
|
599
|
+
const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
|
|
600
|
+
ui.table(headers, rows);
|
|
601
|
+
} else {
|
|
602
|
+
ui.json(data);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
console.log('');
|
|
606
|
+
} catch (err) {
|
|
607
|
+
s.fail('Forensic query failed');
|
|
608
|
+
throw err;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function forensicsLibrary(args, flags) {
|
|
613
|
+
requireAuth();
|
|
614
|
+
const s = ui.spinner('Fetching query library');
|
|
615
|
+
try {
|
|
616
|
+
const data = await api.get('/forensics/query-library');
|
|
617
|
+
const queries = data.data || [];
|
|
618
|
+
s.stop(`Found ${queries.length} saved quer${queries.length !== 1 ? 'ies' : 'y'}`);
|
|
619
|
+
|
|
620
|
+
if (flags.json) { ui.json(queries); return; }
|
|
621
|
+
|
|
622
|
+
console.log('');
|
|
623
|
+
const rows = queries.map(q => [
|
|
624
|
+
ui.c.dim(ui.truncate(q._id, 12)),
|
|
625
|
+
q.name || q.title || '—',
|
|
626
|
+
ui.truncate(q.description || q.query || '', 50),
|
|
627
|
+
ui.timeAgo(q.createdAt),
|
|
628
|
+
]);
|
|
629
|
+
ui.table(['ID', 'Name', 'Description', 'Created'], rows);
|
|
630
|
+
console.log('');
|
|
631
|
+
} catch (err) {
|
|
632
|
+
s.fail('Failed to fetch query library');
|
|
633
|
+
throw err;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Forensics Chat ──
|
|
638
|
+
|
|
639
|
+
async function forensicsChat(args, flags) {
|
|
640
|
+
requireAuth();
|
|
641
|
+
const resolved = await resolveAppId(flags);
|
|
642
|
+
if (!resolved) {
|
|
643
|
+
ui.error('App required. Usage: securenow forensics chat --app <key>');
|
|
644
|
+
ui.info('Set a default with: securenow apps default <key>');
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
console.log('');
|
|
649
|
+
ui.heading(`Forensics Chat — ${resolved.name || resolved.key}`);
|
|
650
|
+
console.log(ui.c.dim(` App: ${resolved.key} (${resolved.id})`));
|
|
651
|
+
console.log(ui.c.dim(' Type your question, or "exit" to quit.\n'));
|
|
652
|
+
|
|
653
|
+
let conversationId = null;
|
|
654
|
+
|
|
655
|
+
const readline = require('readline');
|
|
656
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
657
|
+
|
|
658
|
+
const askLine = () => new Promise((resolve) => {
|
|
659
|
+
rl.question(ui.c.cyan(' you > '), (answer) => resolve(answer.trim()));
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
while (true) {
|
|
664
|
+
const message = await askLine();
|
|
665
|
+
if (!message || message.toLowerCase() === 'exit' || message.toLowerCase() === 'quit') {
|
|
666
|
+
console.log('');
|
|
667
|
+
ui.info('Chat ended.');
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const s = ui.spinner('Thinking');
|
|
672
|
+
try {
|
|
673
|
+
const body = { message, applicationKey: resolved.key };
|
|
674
|
+
if (conversationId) body.conversationId = conversationId;
|
|
675
|
+
|
|
676
|
+
const chatRes = await api.post('/forensics/chat', body);
|
|
677
|
+
conversationId = chatRes.conversationId;
|
|
678
|
+
|
|
679
|
+
if (chatRes.status === 'processing') {
|
|
680
|
+
let result;
|
|
681
|
+
for (let i = 0; i < 150; i++) {
|
|
682
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
683
|
+
result = await api.get(`/forensics/chat/status/${conversationId}`);
|
|
684
|
+
if (result.status === 'complete' || result.status === 'failed' || result.status === 'awaiting_confirmation') break;
|
|
685
|
+
const progress = result._progress;
|
|
686
|
+
if (progress?.action) s.update(progress.action);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (!result || result.status === 'processing') {
|
|
690
|
+
s.fail('Response timed out');
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
s.stop('Done');
|
|
695
|
+
printChatMessage(result.message);
|
|
696
|
+
|
|
697
|
+
if (result.status === 'awaiting_confirmation') {
|
|
698
|
+
const proceed = await ui.confirm('Proceed with this query?');
|
|
699
|
+
if (proceed) {
|
|
700
|
+
const cs = ui.spinner('Executing query');
|
|
701
|
+
await api.post(`/forensics/chat/confirm/${conversationId}`);
|
|
702
|
+
let confirmResult;
|
|
703
|
+
for (let i = 0; i < 90; i++) {
|
|
704
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
705
|
+
confirmResult = await api.get(`/forensics/chat/status/${conversationId}`);
|
|
706
|
+
if (confirmResult.status !== 'processing') break;
|
|
707
|
+
}
|
|
708
|
+
cs.stop('Done');
|
|
709
|
+
if (confirmResult?.message) printChatMessage(confirmResult.message);
|
|
710
|
+
} else {
|
|
711
|
+
ui.info('Query skipped. Ask a different question or refine.');
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} else if (chatRes.message) {
|
|
715
|
+
s.stop('Done');
|
|
716
|
+
printChatMessage(chatRes.message);
|
|
717
|
+
} else {
|
|
718
|
+
s.stop('Done');
|
|
719
|
+
}
|
|
720
|
+
} catch (err) {
|
|
721
|
+
s.fail('Error');
|
|
722
|
+
ui.error(err.message);
|
|
723
|
+
}
|
|
724
|
+
console.log('');
|
|
725
|
+
}
|
|
726
|
+
} finally {
|
|
727
|
+
rl.close();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function printChatMessage(msg) {
|
|
732
|
+
if (!msg) return;
|
|
733
|
+
console.log('');
|
|
734
|
+
if (msg.content) {
|
|
735
|
+
console.log(` ${ui.c.green('ai >')} ${msg.content}`);
|
|
736
|
+
}
|
|
737
|
+
if (msg.sqlQuery) {
|
|
738
|
+
console.log('');
|
|
739
|
+
ui.subheading('Generated SQL');
|
|
740
|
+
console.log(`\n ${ui.c.dim(msg.sqlQuery)}\n`);
|
|
741
|
+
}
|
|
742
|
+
if (msg.results && Array.isArray(msg.results) && msg.results.length > 0) {
|
|
743
|
+
ui.subheading(`Results (${msg.resultCount ?? msg.results.length} rows)`);
|
|
744
|
+
console.log('');
|
|
745
|
+
const headers = Object.keys(msg.results[0]);
|
|
746
|
+
const rows = msg.results.map(row => headers.map(h => String(row[h] ?? '')));
|
|
747
|
+
ui.table(headers, rows);
|
|
748
|
+
}
|
|
749
|
+
if (msg.estimation) {
|
|
750
|
+
const est = msg.estimation;
|
|
751
|
+
const rowLabel = est.timedOut
|
|
752
|
+
? 'very large (estimation timed out)'
|
|
753
|
+
: est.estimatedRows != null
|
|
754
|
+
? `~${Number(est.estimatedRows).toLocaleString()}`
|
|
755
|
+
: 'unknown';
|
|
756
|
+
console.log(`\n ${ui.c.yellow('!')} Estimated rows: ${rowLabel}`);
|
|
757
|
+
if (est.suggestions?.length) {
|
|
758
|
+
console.log(ui.c.dim(' Suggestions:'));
|
|
759
|
+
for (const sug of est.suggestions) {
|
|
760
|
+
console.log(ui.c.dim(` • ${sug}`));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (msg.error && !msg.results) {
|
|
765
|
+
console.log(`\n ${ui.c.red('Error:')} ${msg.error}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── IP Lookup ──
|
|
770
|
+
|
|
771
|
+
async function ipLookup(args, flags) {
|
|
772
|
+
requireAuth();
|
|
773
|
+
const ip = args[0];
|
|
774
|
+
if (!ip) {
|
|
775
|
+
ui.error('IP address required. Usage: securenow ip <ip-address>');
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const s = ui.spinner(`Looking up ${ip}`);
|
|
780
|
+
try {
|
|
781
|
+
const data = await api.get(`/ip/${ip}`);
|
|
782
|
+
s.stop('IP intelligence loaded');
|
|
783
|
+
|
|
784
|
+
if (flags.json) { ui.json(data); return; }
|
|
785
|
+
|
|
786
|
+
console.log('');
|
|
787
|
+
ui.heading(`IP Intelligence: ${data.ip || ip}`);
|
|
788
|
+
console.log('');
|
|
789
|
+
|
|
790
|
+
const pairs = [];
|
|
791
|
+
if (data.countryName || data.countryCode) pairs.push(['Country', `${data.countryName || ''} ${data.countryCode ? `(${data.countryCode})` : ''}`.trim()]);
|
|
792
|
+
if (data.domain) pairs.push(['Domain', data.domain]);
|
|
793
|
+
if (data.isp) pairs.push(['ISP', data.isp]);
|
|
794
|
+
if (data.usageType) pairs.push(['Usage Type', data.usageType]);
|
|
795
|
+
if (data.abuseConfidenceScore != null) pairs.push(['Abuse Score', `${data.abuseConfidenceScore}/100`]);
|
|
796
|
+
if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
|
|
797
|
+
if (data.verdict) pairs.push(['Verdict', data.verdict]);
|
|
798
|
+
if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
|
|
799
|
+
if (data.isBot != null) pairs.push(['Bot', data.isBot ? ui.c.yellow('Yes') : 'No']);
|
|
800
|
+
if (data.activityType) pairs.push(['Activity', data.activityType]);
|
|
801
|
+
if (data.totalReports != null) pairs.push(['Total Reports', String(data.totalReports)]);
|
|
802
|
+
if (data.lastReportedAt) pairs.push(['Last Reported', new Date(data.lastReportedAt).toLocaleString()]);
|
|
803
|
+
|
|
804
|
+
if (pairs.length) {
|
|
805
|
+
ui.keyValue(pairs);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (data.riskFactors?.length) {
|
|
809
|
+
ui.subheading('Risk Factors');
|
|
810
|
+
console.log('');
|
|
811
|
+
data.riskFactors.forEach(f => console.log(` • ${f}`));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (data.attackTypes?.length) {
|
|
815
|
+
ui.subheading('Attack Types');
|
|
816
|
+
console.log('');
|
|
817
|
+
data.attackTypes.forEach(a => console.log(` • ${a}`));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (data.summary) {
|
|
821
|
+
ui.subheading('Summary');
|
|
822
|
+
console.log(`\n ${data.summary}`);
|
|
823
|
+
}
|
|
824
|
+
console.log('');
|
|
825
|
+
} catch (err) {
|
|
826
|
+
s.fail('IP lookup failed');
|
|
827
|
+
throw err;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function ipTraces(args, flags) {
|
|
832
|
+
requireAuth();
|
|
833
|
+
const ip = args[0];
|
|
834
|
+
if (!ip) {
|
|
835
|
+
ui.error('IP address required. Usage: securenow ip traces <ip>');
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const s = ui.spinner(`Fetching traces for ${ip}`);
|
|
840
|
+
try {
|
|
841
|
+
const data = await api.get(`/ip/${ip}/traces`);
|
|
842
|
+
const traces = data.traces || [];
|
|
843
|
+
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
844
|
+
|
|
845
|
+
if (flags.json) { ui.json(data); return; }
|
|
846
|
+
|
|
847
|
+
console.log('');
|
|
848
|
+
const rows = traces.map(t => [
|
|
849
|
+
ui.c.dim(ui.truncate(t.traceID || t.traceId, 16)),
|
|
850
|
+
t.httpMethod || t.method || '—',
|
|
851
|
+
ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
|
|
852
|
+
ui.truncate(t.httpUrl || t.url || '', 40),
|
|
853
|
+
ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
|
|
854
|
+
ui.timeAgo(t.timestamp),
|
|
855
|
+
]);
|
|
856
|
+
ui.table(['Trace ID', 'Method', 'Status', 'URL', 'Duration', 'Time'], rows);
|
|
857
|
+
console.log('');
|
|
858
|
+
} catch (err) {
|
|
859
|
+
s.fail('Failed to fetch traces');
|
|
860
|
+
throw err;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ── Instances ──
|
|
865
|
+
|
|
866
|
+
async function instancesList(args, flags) {
|
|
867
|
+
requireAuth();
|
|
868
|
+
const s = ui.spinner('Fetching instances');
|
|
869
|
+
try {
|
|
870
|
+
const data = await api.get('/instances');
|
|
871
|
+
const instances = data.instances || [];
|
|
872
|
+
s.stop(`Found ${instances.length} instance${instances.length !== 1 ? 's' : ''}`);
|
|
873
|
+
|
|
874
|
+
if (flags.json) { ui.json(instances); return; }
|
|
875
|
+
|
|
876
|
+
console.log('');
|
|
877
|
+
const rows = instances.map(inst => [
|
|
878
|
+
ui.c.dim(ui.truncate(inst._id, 12)),
|
|
879
|
+
inst.name || inst.host || '—',
|
|
880
|
+
inst.host || '—',
|
|
881
|
+
inst.port != null ? String(inst.port) : '—',
|
|
882
|
+
inst.linkedApps != null ? String(inst.linkedApps) : '—',
|
|
883
|
+
ui.timeAgo(inst.createdAt),
|
|
884
|
+
]);
|
|
885
|
+
ui.table(['ID', 'Name', 'Host', 'Port', 'Linked Apps', 'Added'], rows);
|
|
886
|
+
console.log('');
|
|
887
|
+
} catch (err) {
|
|
888
|
+
s.fail('Failed to fetch instances');
|
|
889
|
+
throw err;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function instancesTest(args, flags) {
|
|
894
|
+
requireAuth();
|
|
895
|
+
const id = args[0];
|
|
896
|
+
if (!id) {
|
|
897
|
+
ui.error('Instance ID required. Usage: securenow instances test <id>');
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const s = ui.spinner('Testing instance connection');
|
|
902
|
+
try {
|
|
903
|
+
const result = await api.post(`/instances/${id}/test`);
|
|
904
|
+
if (result.success) {
|
|
905
|
+
s.stop(`Connection successful${result.storageGb ? ` (${result.storageGb} GB storage)` : ''}`);
|
|
906
|
+
} else {
|
|
907
|
+
s.fail(result.message || 'Connection failed');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (flags.json) ui.json(result);
|
|
911
|
+
} catch (err) {
|
|
912
|
+
s.fail('Instance test failed');
|
|
913
|
+
throw err;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── Analytics ──
|
|
918
|
+
|
|
919
|
+
async function analytics(args, flags) {
|
|
920
|
+
requireAuth();
|
|
921
|
+
const s = ui.spinner('Fetching analytics');
|
|
922
|
+
try {
|
|
923
|
+
const query = {};
|
|
924
|
+
const appKey = resolveApp(flags);
|
|
925
|
+
if (flags.instance) query.instanceId = flags.instance;
|
|
926
|
+
|
|
927
|
+
const data = await api.get('/analytics/summary', { query });
|
|
928
|
+
|
|
929
|
+
s.stop('Analytics loaded');
|
|
930
|
+
|
|
931
|
+
if (flags.json) {
|
|
932
|
+
ui.json(data);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
console.log('');
|
|
937
|
+
ui.heading('Analytics Overview');
|
|
938
|
+
console.log('');
|
|
939
|
+
|
|
940
|
+
const groups = ['2xx-responses', '3xx-responses', '4xx-responses', '5xx-responses', '500-errors'];
|
|
941
|
+
const pairs = groups.map(ep => {
|
|
942
|
+
const val = data[ep];
|
|
943
|
+
const count = val?.meta?.count ?? '—';
|
|
944
|
+
return [ep, String(count)];
|
|
945
|
+
});
|
|
946
|
+
ui.keyValue(pairs);
|
|
947
|
+
console.log('');
|
|
948
|
+
} catch (err) {
|
|
949
|
+
s.fail('Failed to fetch analytics');
|
|
950
|
+
throw err;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
module.exports = {
|
|
955
|
+
alertRulesRoute,
|
|
956
|
+
alertRulesList,
|
|
957
|
+
alertRuleShow,
|
|
958
|
+
alertRuleUpdate,
|
|
959
|
+
alertChannelsList,
|
|
960
|
+
alertHistoryList,
|
|
961
|
+
blocklistList,
|
|
962
|
+
blocklistAdd,
|
|
963
|
+
blocklistRemove,
|
|
964
|
+
blocklistStats,
|
|
965
|
+
allowlistList,
|
|
966
|
+
allowlistAdd,
|
|
967
|
+
allowlistRemove,
|
|
968
|
+
allowlistStats,
|
|
969
|
+
trustedList,
|
|
970
|
+
trustedAdd,
|
|
971
|
+
trustedRemove,
|
|
972
|
+
forensicsQuery,
|
|
973
|
+
forensicsChat,
|
|
974
|
+
forensicsLibrary,
|
|
975
|
+
ipLookup,
|
|
976
|
+
ipTraces,
|
|
977
|
+
instancesList,
|
|
978
|
+
instancesTest,
|
|
979
|
+
analytics,
|
|
980
|
+
};
|