securenow 5.2.2 → 5.3.1

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.
@@ -0,0 +1,681 @@
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
+ async function alertRulesList(args, flags) {
14
+ requireAuth();
15
+ const s = ui.spinner('Fetching alert rules');
16
+ try {
17
+ const data = await api.get('/alert-rules');
18
+ const rules = data.alertRules || [];
19
+ s.stop(`Found ${rules.length} rule${rules.length !== 1 ? 's' : ''}`);
20
+
21
+ if (flags.json) { ui.json(rules); return; }
22
+
23
+ console.log('');
24
+ const rows = rules.map(r => [
25
+ ui.c.dim(ui.truncate(r._id, 12)),
26
+ r.name || r.type || '—',
27
+ ui.statusBadge(r.enabled !== false ? 'enabled' : 'disabled'),
28
+ r.severity ? ui.statusBadge(r.severity) : '—',
29
+ r.type || '—',
30
+ r.serviceName || ui.c.dim('all'),
31
+ ]);
32
+ ui.table(['ID', 'Name', 'Status', 'Severity', 'Type', 'App'], rows);
33
+ console.log('');
34
+ } catch (err) {
35
+ s.fail('Failed to fetch alert rules');
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ // ── Alert Channels ──
41
+
42
+ async function alertChannelsList(args, flags) {
43
+ requireAuth();
44
+ const s = ui.spinner('Fetching alert channels');
45
+ try {
46
+ const data = await api.get('/alert-channels');
47
+ const channels = data.alertChannels || [];
48
+ s.stop(`Found ${channels.length} channel${channels.length !== 1 ? 's' : ''}`);
49
+
50
+ if (flags.json) { ui.json(channels); return; }
51
+
52
+ console.log('');
53
+ const rows = channels.map(ch => [
54
+ ui.c.dim(ui.truncate(ch._id, 12)),
55
+ ch.name || '—',
56
+ ch.type || '—',
57
+ ui.statusBadge(ch.enabled !== false ? 'enabled' : 'disabled'),
58
+ ch.target || ch.email || ch.webhookUrl || '—',
59
+ ]);
60
+ ui.table(['ID', 'Name', 'Type', 'Status', 'Target'], rows);
61
+ console.log('');
62
+ } catch (err) {
63
+ s.fail('Failed to fetch alert channels');
64
+ throw err;
65
+ }
66
+ }
67
+
68
+ // ── Alert History ──
69
+
70
+ async function alertHistoryList(args, flags) {
71
+ requireAuth();
72
+ const s = ui.spinner('Fetching alert history');
73
+ try {
74
+ const query = { limit: flags.limit || 20 };
75
+ const data = await api.get('/alert-history', { query });
76
+ const history = data.alerts || [];
77
+ s.stop(`Found ${history.length} alert${history.length !== 1 ? 's' : ''}${data.totalItems ? ` (${data.totalItems} total)` : ''}`);
78
+
79
+ if (flags.json) { ui.json(data); return; }
80
+
81
+ console.log('');
82
+ const rows = history.map(h => [
83
+ ui.c.dim(ui.truncate(h._id, 12)),
84
+ h.ruleName || h.type || '—',
85
+ h.severity ? ui.statusBadge(h.severity) : '—',
86
+ ui.truncate(h.message || h.description || '', 40),
87
+ h.serviceName || '—',
88
+ ui.timeAgo(h.triggeredAt || h.createdAt),
89
+ ]);
90
+ ui.table(['ID', 'Rule', 'Severity', 'Message', 'App', 'Triggered'], rows);
91
+ console.log('');
92
+ } catch (err) {
93
+ s.fail('Failed to fetch alert history');
94
+ throw err;
95
+ }
96
+ }
97
+
98
+ // ── Blocklist ──
99
+
100
+ async function blocklistList(args, flags) {
101
+ requireAuth();
102
+ const s = ui.spinner('Fetching blocklist');
103
+ try {
104
+ const data = await api.get('/blocklist');
105
+ const items = data.blockedIps || [];
106
+ s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
107
+
108
+ if (flags.json) { ui.json(data); return; }
109
+
110
+ console.log('');
111
+ const rows = items.map(b => [
112
+ ui.c.dim(ui.truncate(b._id, 12)),
113
+ ui.c.red(b.ip || b.cidr || '—'),
114
+ ui.truncate(b.reason || '', 40),
115
+ b.source || '—',
116
+ ui.timeAgo(b.createdAt),
117
+ b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
118
+ ]);
119
+ ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'Added', 'Expires'], rows);
120
+ console.log('');
121
+ } catch (err) {
122
+ s.fail('Failed to fetch blocklist');
123
+ throw err;
124
+ }
125
+ }
126
+
127
+ async function blocklistAdd(args, flags) {
128
+ requireAuth();
129
+ let ip = args[0];
130
+ if (!ip) {
131
+ ip = await ui.prompt('IP address or CIDR to block');
132
+ if (!ip) { ui.error('IP is required'); process.exit(1); }
133
+ }
134
+
135
+ const body = { ip };
136
+ if (flags.reason) body.reason = flags.reason;
137
+ if (flags.duration) body.duration = flags.duration;
138
+ if (flags.app) body.serviceName = flags.app;
139
+
140
+ const s = ui.spinner(`Blocking ${ip}`);
141
+ try {
142
+ await api.post('/blocklist', body);
143
+ s.stop(`${ip} added to blocklist`);
144
+ } catch (err) {
145
+ s.fail('Failed to add to blocklist');
146
+ throw err;
147
+ }
148
+ }
149
+
150
+ async function blocklistRemove(args, flags) {
151
+ requireAuth();
152
+ const id = args[0];
153
+ if (!id) {
154
+ ui.error('Blocklist entry ID required. Usage: securenow blocklist remove <id>');
155
+ process.exit(1);
156
+ }
157
+
158
+ if (!flags.force && !flags.yes) {
159
+ const ok = await ui.confirm('Remove this IP from blocklist?');
160
+ if (!ok) { ui.info('Cancelled'); return; }
161
+ }
162
+
163
+ const s = ui.spinner('Removing from blocklist');
164
+ try {
165
+ await api.delete(`/blocklist/${id}`);
166
+ s.stop('Removed from blocklist');
167
+ } catch (err) {
168
+ s.fail('Failed to remove from blocklist');
169
+ throw err;
170
+ }
171
+ }
172
+
173
+ async function blocklistStats(args, flags) {
174
+ requireAuth();
175
+ const s = ui.spinner('Fetching blocklist stats');
176
+ try {
177
+ const data = await api.get('/blocklist/stats');
178
+ const stats = data.stats || data;
179
+ s.stop('Stats loaded');
180
+
181
+ if (flags.json) { ui.json(stats); return; }
182
+
183
+ console.log('');
184
+ ui.heading('Blocklist Statistics');
185
+ console.log('');
186
+ ui.keyValue([
187
+ ['Total Active', String(stats.totalActive ?? '—')],
188
+ ['Total Removed', String(stats.totalRemoved ?? '—')],
189
+ ['Manual Blocks', String(stats.manualCount ?? '—')],
190
+ ['Automation Blocks', String(stats.automationCount ?? '—')],
191
+ ['Active Rules', String(stats.activeAutomationRules ?? '—')],
192
+ ]);
193
+ console.log('');
194
+ } catch (err) {
195
+ s.fail('Failed to fetch stats');
196
+ throw err;
197
+ }
198
+ }
199
+
200
+ // ── Trusted IPs ──
201
+
202
+ async function trustedList(args, flags) {
203
+ requireAuth();
204
+ const s = ui.spinner('Fetching trusted IPs');
205
+ try {
206
+ const data = await api.get('/trusted-ips');
207
+ const items = data.trustedIps || [];
208
+ s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
209
+
210
+ if (flags.json) { ui.json(items); return; }
211
+
212
+ console.log('');
213
+ const rows = items.map(t => [
214
+ ui.c.dim(ui.truncate(t._id, 12)),
215
+ ui.c.green(t.ip || t.cidr || '—'),
216
+ t.label || t.description || ui.c.dim('—'),
217
+ ui.timeAgo(t.createdAt),
218
+ ]);
219
+ ui.table(['ID', 'IP/CIDR', 'Label', 'Added'], rows);
220
+ console.log('');
221
+ } catch (err) {
222
+ s.fail('Failed to fetch trusted IPs');
223
+ throw err;
224
+ }
225
+ }
226
+
227
+ async function trustedAdd(args, flags) {
228
+ requireAuth();
229
+ let ip = args[0];
230
+ if (!ip) {
231
+ ip = await ui.prompt('IP address or CIDR to trust');
232
+ if (!ip) { ui.error('IP is required'); process.exit(1); }
233
+ }
234
+
235
+ const body = { ip };
236
+ if (flags.label) body.label = flags.label;
237
+ if (flags.description) body.description = flags.description;
238
+
239
+ const s = ui.spinner(`Adding ${ip} to trusted IPs`);
240
+ try {
241
+ await api.post('/trusted-ips', body);
242
+ s.stop(`${ip} added to trusted IPs`);
243
+ } catch (err) {
244
+ s.fail('Failed to add trusted IP');
245
+ throw err;
246
+ }
247
+ }
248
+
249
+ async function trustedRemove(args, flags) {
250
+ requireAuth();
251
+ const id = args[0];
252
+ if (!id) {
253
+ ui.error('Trusted IP entry ID required. Usage: securenow trusted remove <id>');
254
+ process.exit(1);
255
+ }
256
+
257
+ if (!flags.force && !flags.yes) {
258
+ const ok = await ui.confirm('Remove this IP from trusted list?');
259
+ if (!ok) { ui.info('Cancelled'); return; }
260
+ }
261
+
262
+ const s = ui.spinner('Removing from trusted IPs');
263
+ try {
264
+ await api.delete(`/trusted-ips/${id}`);
265
+ s.stop('Removed from trusted IPs');
266
+ } catch (err) {
267
+ s.fail('Failed to remove trusted IP');
268
+ throw err;
269
+ }
270
+ }
271
+
272
+ // ── Forensics ──
273
+
274
+ async function forensicsQuery(args, flags) {
275
+ requireAuth();
276
+ const query = args.join(' ');
277
+ if (!query) {
278
+ ui.error('Query required. Usage: securenow forensics "your natural language query"');
279
+ process.exit(1);
280
+ }
281
+
282
+ const s = ui.spinner('Submitting forensic query');
283
+ try {
284
+ const body = { query };
285
+ if (flags.instance) body.instanceId = flags.instance;
286
+
287
+ const job = await api.post('/forensics/query', body);
288
+ const jobId = job.jobId;
289
+
290
+ if (!jobId) {
291
+ s.stop('Query complete');
292
+ if (flags.json) { ui.json(job); return; }
293
+ if (job.result) {
294
+ console.log('');
295
+ if (job.sqlquery) {
296
+ ui.subheading('Generated SQL');
297
+ console.log(`\n ${ui.c.dim(job.sqlquery)}\n`);
298
+ }
299
+ const data = job.result;
300
+ if (Array.isArray(data) && data.length > 0) {
301
+ const headers = Object.keys(data[0]);
302
+ const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
303
+ ui.table(headers, rows);
304
+ } else {
305
+ ui.json(data);
306
+ }
307
+ console.log('');
308
+ }
309
+ return;
310
+ }
311
+
312
+ s.update('Processing query...');
313
+
314
+ let result;
315
+ const maxAttempts = 60;
316
+ for (let i = 0; i < maxAttempts; i++) {
317
+ await new Promise(r => setTimeout(r, 2000));
318
+ result = await api.get(`/forensics/query/status/${jobId}`);
319
+ if (result.status === 'completed' || result.status === 'failed') break;
320
+ s.update(`Processing query... (${(i + 1) * 2}s)`);
321
+ }
322
+
323
+ if (result.status === 'failed') {
324
+ s.fail('Query failed');
325
+ ui.error(result.error || 'Unknown error');
326
+ return;
327
+ }
328
+
329
+ if (result.status !== 'completed') {
330
+ s.fail('Query timed out');
331
+ ui.warn('Query is still processing. Check back later.');
332
+ return;
333
+ }
334
+
335
+ s.stop('Query complete');
336
+
337
+ if (flags.json) { ui.json(result); return; }
338
+
339
+ console.log('');
340
+ if (result.sqlquery) {
341
+ ui.subheading('Generated SQL');
342
+ console.log(`\n ${ui.c.dim(result.sqlquery)}\n`);
343
+ }
344
+
345
+ if (result.result) {
346
+ const data = result.result;
347
+ ui.subheading(`Results (${result.rowCount ?? (Array.isArray(data) ? data.length : '?')} rows)`);
348
+ console.log('');
349
+
350
+ if (Array.isArray(data) && data.length > 0) {
351
+ const headers = Object.keys(data[0]);
352
+ const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
353
+ ui.table(headers, rows);
354
+ } else {
355
+ ui.json(data);
356
+ }
357
+ }
358
+ console.log('');
359
+ } catch (err) {
360
+ s.fail('Forensic query failed');
361
+ throw err;
362
+ }
363
+ }
364
+
365
+ async function forensicsLibrary(args, flags) {
366
+ requireAuth();
367
+ const s = ui.spinner('Fetching query library');
368
+ try {
369
+ const data = await api.get('/forensics/query-library');
370
+ const queries = data.data || [];
371
+ s.stop(`Found ${queries.length} saved quer${queries.length !== 1 ? 'ies' : 'y'}`);
372
+
373
+ if (flags.json) { ui.json(queries); return; }
374
+
375
+ console.log('');
376
+ const rows = queries.map(q => [
377
+ ui.c.dim(ui.truncate(q._id, 12)),
378
+ q.name || q.title || '—',
379
+ ui.truncate(q.description || q.query || '', 50),
380
+ ui.timeAgo(q.createdAt),
381
+ ]);
382
+ ui.table(['ID', 'Name', 'Description', 'Created'], rows);
383
+ console.log('');
384
+ } catch (err) {
385
+ s.fail('Failed to fetch query library');
386
+ throw err;
387
+ }
388
+ }
389
+
390
+ // ── IP Lookup ──
391
+
392
+ async function ipLookup(args, flags) {
393
+ requireAuth();
394
+ const ip = args[0];
395
+ if (!ip) {
396
+ ui.error('IP address required. Usage: securenow ip <ip-address>');
397
+ process.exit(1);
398
+ }
399
+
400
+ const s = ui.spinner(`Looking up ${ip}`);
401
+ try {
402
+ const data = await api.get(`/ip/${ip}`);
403
+ s.stop('IP intelligence loaded');
404
+
405
+ if (flags.json) { ui.json(data); return; }
406
+
407
+ console.log('');
408
+ ui.heading(`IP Intelligence: ${data.ip || ip}`);
409
+ console.log('');
410
+
411
+ const pairs = [];
412
+ if (data.countryName || data.countryCode) pairs.push(['Country', `${data.countryName || ''} ${data.countryCode ? `(${data.countryCode})` : ''}`.trim()]);
413
+ if (data.domain) pairs.push(['Domain', data.domain]);
414
+ if (data.isp) pairs.push(['ISP', data.isp]);
415
+ if (data.usageType) pairs.push(['Usage Type', data.usageType]);
416
+ if (data.abuseConfidenceScore != null) pairs.push(['Abuse Score', `${data.abuseConfidenceScore}/100`]);
417
+ if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
418
+ if (data.verdict) pairs.push(['Verdict', data.verdict]);
419
+ if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
420
+ if (data.isBot != null) pairs.push(['Bot', data.isBot ? ui.c.yellow('Yes') : 'No']);
421
+ if (data.activityType) pairs.push(['Activity', data.activityType]);
422
+ if (data.totalReports != null) pairs.push(['Total Reports', String(data.totalReports)]);
423
+ if (data.lastReportedAt) pairs.push(['Last Reported', new Date(data.lastReportedAt).toLocaleString()]);
424
+
425
+ if (pairs.length) {
426
+ ui.keyValue(pairs);
427
+ }
428
+
429
+ if (data.riskFactors?.length) {
430
+ ui.subheading('Risk Factors');
431
+ console.log('');
432
+ data.riskFactors.forEach(f => console.log(` • ${f}`));
433
+ }
434
+
435
+ if (data.attackTypes?.length) {
436
+ ui.subheading('Attack Types');
437
+ console.log('');
438
+ data.attackTypes.forEach(a => console.log(` • ${a}`));
439
+ }
440
+
441
+ if (data.summary) {
442
+ ui.subheading('Summary');
443
+ console.log(`\n ${data.summary}`);
444
+ }
445
+ console.log('');
446
+ } catch (err) {
447
+ s.fail('IP lookup failed');
448
+ throw err;
449
+ }
450
+ }
451
+
452
+ async function ipTraces(args, flags) {
453
+ requireAuth();
454
+ const ip = args[0];
455
+ if (!ip) {
456
+ ui.error('IP address required. Usage: securenow ip traces <ip>');
457
+ process.exit(1);
458
+ }
459
+
460
+ const s = ui.spinner(`Fetching traces for ${ip}`);
461
+ try {
462
+ const data = await api.get(`/ip/${ip}/traces`);
463
+ const traces = data.traces || [];
464
+ s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
465
+
466
+ if (flags.json) { ui.json(data); return; }
467
+
468
+ console.log('');
469
+ const rows = traces.map(t => [
470
+ ui.c.dim(ui.truncate(t.traceID || t.traceId, 16)),
471
+ t.httpMethod || t.method || '—',
472
+ ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
473
+ ui.truncate(t.httpUrl || t.url || '', 40),
474
+ ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
475
+ ui.timeAgo(t.timestamp),
476
+ ]);
477
+ ui.table(['Trace ID', 'Method', 'Status', 'URL', 'Duration', 'Time'], rows);
478
+ console.log('');
479
+ } catch (err) {
480
+ s.fail('Failed to fetch traces');
481
+ throw err;
482
+ }
483
+ }
484
+
485
+ // ── API Map ──
486
+
487
+ async function apiMapList(args, flags) {
488
+ requireAuth();
489
+ const s = ui.spinner('Fetching API map');
490
+ try {
491
+ const data = await api.get('/api-map');
492
+ const apiMap = data.apiMap;
493
+ s.stop('API map loaded');
494
+
495
+ if (flags.json) { ui.json(data); return; }
496
+
497
+ if (!apiMap) {
498
+ console.log('');
499
+ ui.info(data.message || 'No API map discovered yet. Run discovery from the dashboard.');
500
+ console.log('');
501
+ return;
502
+ }
503
+
504
+ console.log('');
505
+ if (apiMap.apps && typeof apiMap.apps === 'object') {
506
+ for (const [appName, appData] of Object.entries(apiMap.apps)) {
507
+ ui.subheading(appName);
508
+ console.log('');
509
+ const endpoints = appData.endpoints || [];
510
+ if (endpoints.length) {
511
+ const rows = endpoints.map(e => [
512
+ e.method || '—',
513
+ e.path || e.route || '—',
514
+ e.requestCount != null ? String(e.requestCount) : '—',
515
+ e.description || ui.c.dim('—'),
516
+ ]);
517
+ ui.table(['Method', 'Path', 'Requests', 'Description'], rows);
518
+ } else {
519
+ ui.info('No endpoints discovered for this app.');
520
+ }
521
+ console.log('');
522
+ }
523
+ } else {
524
+ ui.json(apiMap);
525
+ }
526
+ } catch (err) {
527
+ s.fail('Failed to fetch API map');
528
+ throw err;
529
+ }
530
+ }
531
+
532
+ async function apiMapStats(args, flags) {
533
+ requireAuth();
534
+ const s = ui.spinner('Fetching API map stats');
535
+ try {
536
+ const data = await api.get('/api-map/stats');
537
+ const stats = data.stats;
538
+ s.stop('Stats loaded');
539
+
540
+ if (flags.json) { ui.json(stats); return; }
541
+
542
+ if (!stats) {
543
+ console.log('');
544
+ ui.info('No API map stats available.');
545
+ console.log('');
546
+ return;
547
+ }
548
+
549
+ console.log('');
550
+ ui.heading('API Map Statistics');
551
+ console.log('');
552
+ ui.keyValue([
553
+ ['Total Apps', String(stats.totalApps ?? '—')],
554
+ ['Total Endpoints', String(stats.totalEndpoints ?? '—')],
555
+ ['Total Requests', String(stats.totalRequests ?? '—')],
556
+ ['Discovery Status', stats.discoveryStatus || '—'],
557
+ ['Last Discovered', stats.lastDiscoveredAt ? new Date(stats.lastDiscoveredAt).toLocaleString() : '—'],
558
+ ['Version', String(stats.version ?? '—')],
559
+ ]);
560
+ console.log('');
561
+ } catch (err) {
562
+ s.fail('Failed to fetch stats');
563
+ throw err;
564
+ }
565
+ }
566
+
567
+ // ── Instances ──
568
+
569
+ async function instancesList(args, flags) {
570
+ requireAuth();
571
+ const s = ui.spinner('Fetching instances');
572
+ try {
573
+ const data = await api.get('/instances');
574
+ const instances = data.instances || [];
575
+ s.stop(`Found ${instances.length} instance${instances.length !== 1 ? 's' : ''}`);
576
+
577
+ if (flags.json) { ui.json(instances); return; }
578
+
579
+ console.log('');
580
+ const rows = instances.map(inst => [
581
+ ui.c.dim(ui.truncate(inst._id, 12)),
582
+ inst.name || inst.host || '—',
583
+ inst.host || '—',
584
+ inst.port != null ? String(inst.port) : '—',
585
+ inst.linkedApps != null ? String(inst.linkedApps) : '—',
586
+ ui.timeAgo(inst.createdAt),
587
+ ]);
588
+ ui.table(['ID', 'Name', 'Host', 'Port', 'Linked Apps', 'Added'], rows);
589
+ console.log('');
590
+ } catch (err) {
591
+ s.fail('Failed to fetch instances');
592
+ throw err;
593
+ }
594
+ }
595
+
596
+ async function instancesTest(args, flags) {
597
+ requireAuth();
598
+ const id = args[0];
599
+ if (!id) {
600
+ ui.error('Instance ID required. Usage: securenow instances test <id>');
601
+ process.exit(1);
602
+ }
603
+
604
+ const s = ui.spinner('Testing instance connection');
605
+ try {
606
+ const result = await api.post(`/instances/${id}/test`);
607
+ if (result.success) {
608
+ s.stop(`Connection successful${result.storageGb ? ` (${result.storageGb} GB storage)` : ''}`);
609
+ } else {
610
+ s.fail(result.message || 'Connection failed');
611
+ }
612
+
613
+ if (flags.json) ui.json(result);
614
+ } catch (err) {
615
+ s.fail('Instance test failed');
616
+ throw err;
617
+ }
618
+ }
619
+
620
+ // ── Analytics ──
621
+
622
+ async function analytics(args, flags) {
623
+ requireAuth();
624
+ const s = ui.spinner('Fetching analytics');
625
+ try {
626
+ const query = {};
627
+ const appKey = resolveApp(flags);
628
+ if (flags.instance) query.instanceId = flags.instance;
629
+
630
+ const endpoints = ['2xx-responses', '3xx-responses', '4xx-responses', '5xx-responses', '500-errors'];
631
+ const results = await Promise.all(
632
+ endpoints.map(ep => api.get(`/analytics/${ep}`, { query }).catch(() => null))
633
+ );
634
+
635
+ s.stop('Analytics loaded');
636
+
637
+ if (flags.json) {
638
+ const data = {};
639
+ endpoints.forEach((ep, i) => { data[ep] = results[i]; });
640
+ ui.json(data);
641
+ return;
642
+ }
643
+
644
+ console.log('');
645
+ ui.heading('Analytics Overview');
646
+ console.log('');
647
+
648
+ const pairs = endpoints.map((ep, i) => {
649
+ const val = results[i];
650
+ const count = val?.meta?.count ?? '—';
651
+ return [ep, String(count)];
652
+ });
653
+ ui.keyValue(pairs);
654
+ console.log('');
655
+ } catch (err) {
656
+ s.fail('Failed to fetch analytics');
657
+ throw err;
658
+ }
659
+ }
660
+
661
+ module.exports = {
662
+ alertRulesList,
663
+ alertChannelsList,
664
+ alertHistoryList,
665
+ blocklistList,
666
+ blocklistAdd,
667
+ blocklistRemove,
668
+ blocklistStats,
669
+ trustedList,
670
+ trustedAdd,
671
+ trustedRemove,
672
+ forensicsQuery,
673
+ forensicsLibrary,
674
+ ipLookup,
675
+ ipTraces,
676
+ apiMapList,
677
+ apiMapStats,
678
+ instancesList,
679
+ instancesTest,
680
+ analytics,
681
+ };