securenow 8.11.2 → 8.12.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/README.md +7 -8
- package/SKILL-API.md +0 -1
- package/SKILL-CLI.md +624 -632
- package/TELEMETRY-SQL.md +6 -1
- package/app-config.js +10 -7
- package/cidr.js +83 -83
- package/cli/apps.js +36 -8
- package/cli/client.js +102 -102
- package/cli/diagnostics.js +5 -1
- package/cli/firewall.js +141 -141
- package/cli/fp.js +638 -638
- package/cli/monitor.js +398 -398
- package/cli/run.js +132 -132
- package/cli/security.js +1866 -1856
- package/cli/ui.js +374 -374
- package/cli.js +923 -922
- package/firewall-cloud.js +202 -202
- package/firewall-iptables.js +139 -139
- package/firewall-only.js +46 -46
- package/firewall-tcp.js +74 -74
- package/firewall.js +1470 -1470
- package/mcp/catalog.js +2358 -2346
- package/nextjs-auto-capture.d.ts +29 -29
- package/nextjs-auto-capture.js +273 -273
- package/nextjs-middleware.d.ts +57 -57
- package/nextjs-middleware.js +267 -267
- package/nextjs-webpack-config.js +79 -79
- package/nextjs-wrapper.d.ts +95 -95
- package/nextjs-wrapper.js +233 -233
- package/nextjs.js +0 -9
- package/nuxt-server-plugin.mjs +0 -11
- package/nuxt.mjs +74 -74
- package/package.json +1 -2
- package/register.d.ts +42 -42
- package/register.js +44 -44
- package/resolve-ip.js +13 -13
- package/telemetry-schema.json +3 -1
- package/tracing.js +847 -853
- package/web-vite.mjs +0 -76
- package/free-trial-banner.js +0 -175
package/cli/monitor.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
3
|
const { api, requireAuth } = require('./client');
|
|
4
4
|
const config = require('./config');
|
|
5
5
|
const ui = require('./ui');
|
|
6
6
|
const appConfig = require('../app-config');
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
function resolveApp(flags) {
|
|
9
9
|
return flags.app || config.getDefaultApp();
|
|
10
10
|
}
|
|
@@ -18,105 +18,105 @@ function addEnvironment(query, flags) {
|
|
|
18
18
|
if (environment) query.environment = environment;
|
|
19
19
|
return query;
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
// ── Traces ──
|
|
23
|
-
|
|
24
|
-
async function tracesList(args, flags) {
|
|
25
|
-
requireAuth();
|
|
26
|
-
const appKey = resolveApp(flags);
|
|
27
|
-
if (!appKey) {
|
|
28
|
-
ui.error('No app specified. Use --app <key> or set a default with `securenow config set defaultApp <key>`');
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const s = ui.spinner('Fetching traces');
|
|
33
|
-
try {
|
|
21
|
+
|
|
22
|
+
// ── Traces ──
|
|
23
|
+
|
|
24
|
+
async function tracesList(args, flags) {
|
|
25
|
+
requireAuth();
|
|
26
|
+
const appKey = resolveApp(flags);
|
|
27
|
+
if (!appKey) {
|
|
28
|
+
ui.error('No app specified. Use --app <key> or set a default with `securenow config set defaultApp <key>`');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const s = ui.spinner('Fetching traces');
|
|
33
|
+
try {
|
|
34
34
|
const query = {
|
|
35
35
|
appKeys: appKey,
|
|
36
36
|
limit: flags.limit || 20,
|
|
37
37
|
};
|
|
38
38
|
addEnvironment(query, flags);
|
|
39
|
-
if (flags.start) query.from = flags.start;
|
|
40
|
-
if (flags.end) query.to = flags.end;
|
|
41
|
-
|
|
42
|
-
const data = await api.get('/traces', { query });
|
|
43
|
-
const traces = data.traces || [];
|
|
44
|
-
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
45
|
-
|
|
46
|
-
if (flags.json) { ui.json(traces); return; }
|
|
47
|
-
|
|
48
|
-
console.log('');
|
|
49
|
-
const rows = traces.map(t => [
|
|
39
|
+
if (flags.start) query.from = flags.start;
|
|
40
|
+
if (flags.end) query.to = flags.end;
|
|
41
|
+
|
|
42
|
+
const data = await api.get('/traces', { query });
|
|
43
|
+
const traces = data.traces || [];
|
|
44
|
+
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
45
|
+
|
|
46
|
+
if (flags.json) { ui.json(traces); return; }
|
|
47
|
+
|
|
48
|
+
console.log('');
|
|
49
|
+
const rows = traces.map(t => [
|
|
50
50
|
ui.c.dim(ui.truncate(t.traceID || t.traceId || t._id, 16)),
|
|
51
51
|
t.environment || 'production',
|
|
52
|
-
t.operationName || t.name || t.serviceName || '—',
|
|
53
|
-
ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
|
|
54
|
-
ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
|
|
55
|
-
t.httpMethod || t.method || '—',
|
|
56
|
-
ui.truncate(t.httpUrl || t.url || t.httpRoute || '', 40),
|
|
57
|
-
ui.timeAgo(t.timestamp),
|
|
58
|
-
]);
|
|
59
|
-
|
|
52
|
+
t.operationName || t.name || t.serviceName || '—',
|
|
53
|
+
ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
|
|
54
|
+
ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
|
|
55
|
+
t.httpMethod || t.method || '—',
|
|
56
|
+
ui.truncate(t.httpUrl || t.url || t.httpRoute || '', 40),
|
|
57
|
+
ui.timeAgo(t.timestamp),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
60
|
ui.table(['Trace ID', 'Env', 'Operation', 'Status', 'Duration', 'Method', 'URL', 'Time'], rows);
|
|
61
|
-
console.log('');
|
|
62
|
-
} catch (err) {
|
|
63
|
-
s.fail('Failed to fetch traces');
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function tracesShow(args, flags) {
|
|
69
|
-
requireAuth();
|
|
70
|
-
const traceId = args[0];
|
|
71
|
-
if (!traceId) {
|
|
72
|
-
ui.error('Trace ID is required. Usage: securenow traces show <traceId>');
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const s = ui.spinner('Fetching trace details');
|
|
77
|
-
try {
|
|
61
|
+
console.log('');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
s.fail('Failed to fetch traces');
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function tracesShow(args, flags) {
|
|
69
|
+
requireAuth();
|
|
70
|
+
const traceId = args[0];
|
|
71
|
+
if (!traceId) {
|
|
72
|
+
ui.error('Trace ID is required. Usage: securenow traces show <traceId>');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const s = ui.spinner('Fetching trace details');
|
|
77
|
+
try {
|
|
78
78
|
const appKey = resolveApp(flags);
|
|
79
79
|
const traceQuery = appKey ? { appKeys: appKey } : {};
|
|
80
80
|
addEnvironment(traceQuery, flags);
|
|
81
81
|
const data = await api.get(`/traces/${traceId}`, { query: traceQuery });
|
|
82
|
-
s.stop('Trace loaded');
|
|
83
|
-
|
|
84
|
-
if (flags.json) { ui.json(data); return; }
|
|
85
|
-
|
|
86
|
-
const spans = data.spans || [];
|
|
87
|
-
console.log('');
|
|
88
|
-
ui.heading(`Trace ${data.traceId || traceId}`);
|
|
89
|
-
|
|
90
|
-
if (spans.length) {
|
|
91
|
-
ui.subheading(`Spans (${spans.length})`);
|
|
92
|
-
console.log('');
|
|
93
|
-
const rows = spans.map(span => [
|
|
94
|
-
ui.c.dim(ui.truncate(span.spanID || span.spanId, 16)),
|
|
95
|
-
span.operationName || span.name || '—',
|
|
96
|
-
ui.httpStatusColor(span.statusCode || span.responseStatusCode || '—'),
|
|
97
|
-
ui.durationColor(span.durationNano ? span.durationNano / 1e6 : span.duration),
|
|
98
|
-
span.kind || '—',
|
|
99
|
-
]);
|
|
100
|
-
ui.table(['Span ID', 'Operation', 'Status', 'Duration', 'Kind'], rows);
|
|
101
|
-
} else {
|
|
102
|
-
console.log('');
|
|
103
|
-
ui.info('No spans found for this trace.');
|
|
104
|
-
}
|
|
105
|
-
console.log('');
|
|
106
|
-
} catch (err) {
|
|
107
|
-
s.fail('Failed to fetch trace');
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function tracesAnalyze(args, flags) {
|
|
113
|
-
requireAuth();
|
|
114
|
-
const traceId = args[0];
|
|
115
|
-
if (!traceId) {
|
|
116
|
-
ui.error('Trace ID is required. Usage: securenow traces analyze <traceId>');
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
|
|
82
|
+
s.stop('Trace loaded');
|
|
83
|
+
|
|
84
|
+
if (flags.json) { ui.json(data); return; }
|
|
85
|
+
|
|
86
|
+
const spans = data.spans || [];
|
|
87
|
+
console.log('');
|
|
88
|
+
ui.heading(`Trace ${data.traceId || traceId}`);
|
|
89
|
+
|
|
90
|
+
if (spans.length) {
|
|
91
|
+
ui.subheading(`Spans (${spans.length})`);
|
|
92
|
+
console.log('');
|
|
93
|
+
const rows = spans.map(span => [
|
|
94
|
+
ui.c.dim(ui.truncate(span.spanID || span.spanId, 16)),
|
|
95
|
+
span.operationName || span.name || '—',
|
|
96
|
+
ui.httpStatusColor(span.statusCode || span.responseStatusCode || '—'),
|
|
97
|
+
ui.durationColor(span.durationNano ? span.durationNano / 1e6 : span.duration),
|
|
98
|
+
span.kind || '—',
|
|
99
|
+
]);
|
|
100
|
+
ui.table(['Span ID', 'Operation', 'Status', 'Duration', 'Kind'], rows);
|
|
101
|
+
} else {
|
|
102
|
+
console.log('');
|
|
103
|
+
ui.info('No spans found for this trace.');
|
|
104
|
+
}
|
|
105
|
+
console.log('');
|
|
106
|
+
} catch (err) {
|
|
107
|
+
s.fail('Failed to fetch trace');
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function tracesAnalyze(args, flags) {
|
|
113
|
+
requireAuth();
|
|
114
|
+
const traceId = args[0];
|
|
115
|
+
if (!traceId) {
|
|
116
|
+
ui.error('Trace ID is required. Usage: securenow traces analyze <traceId>');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
120
|
const s = ui.spinner('Analyzing trace with AI');
|
|
121
121
|
try {
|
|
122
122
|
const environment = resolveEnvironment(flags);
|
|
@@ -140,167 +140,167 @@ async function tracesAnalyze(args, flags) {
|
|
|
140
140
|
};
|
|
141
141
|
if (environment) body.environment = environment;
|
|
142
142
|
let result = await api.post('/traces/analyze', body);
|
|
143
|
-
|
|
144
|
-
if (result.analysisId && result.status === 'running') {
|
|
145
|
-
const analysisId = result.analysisId;
|
|
146
|
-
for (let i = 0; i < 120; i++) {
|
|
147
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
148
|
-
result = await api.get(`/traces/analyze/${analysisId}`);
|
|
149
|
-
if (result.status === 'completed' || result.status === 'failed') break;
|
|
150
|
-
}
|
|
151
|
-
if (result.status === 'failed') throw new Error(result.error || 'Analysis failed');
|
|
152
|
-
if (result.status === 'running') throw new Error('Analysis timed out');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
s.stop('Analysis complete');
|
|
156
|
-
|
|
157
|
-
if (flags.json) { ui.json(result); return; }
|
|
158
|
-
|
|
159
|
-
const analysis = result.analysis;
|
|
160
|
-
console.log('');
|
|
161
|
-
ui.heading('AI Trace Analysis');
|
|
162
|
-
console.log('');
|
|
163
|
-
|
|
164
|
-
if (typeof analysis === 'object' && analysis !== null) {
|
|
165
|
-
if (analysis.summary) {
|
|
166
|
-
ui.subheading('Summary');
|
|
167
|
-
console.log(`\n ${analysis.summary}\n`);
|
|
168
|
-
}
|
|
169
|
-
if (analysis.riskLevel) {
|
|
170
|
-
console.log(` ${ui.c.bold('Risk Level:')} ${ui.statusBadge(analysis.riskLevel)}\n`);
|
|
171
|
-
}
|
|
172
|
-
if (analysis.securityIssues?.length) {
|
|
173
|
-
ui.subheading('Security Issues');
|
|
174
|
-
console.log('');
|
|
175
|
-
analysis.securityIssues.forEach((issue, i) => {
|
|
176
|
-
console.log(` ${i + 1}. ${typeof issue === 'string' ? issue : issue.description || JSON.stringify(issue)}`);
|
|
177
|
-
});
|
|
178
|
-
console.log('');
|
|
179
|
-
}
|
|
180
|
-
if (analysis.recommendations?.length) {
|
|
181
|
-
ui.subheading('Recommendations');
|
|
182
|
-
console.log('');
|
|
183
|
-
analysis.recommendations.forEach((rec, i) => {
|
|
184
|
-
console.log(` ${i + 1}. ${typeof rec === 'string' ? rec : rec.description || JSON.stringify(rec)}`);
|
|
185
|
-
});
|
|
186
|
-
console.log('');
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
console.log(analysis || JSON.stringify(result, null, 2));
|
|
190
|
-
console.log('');
|
|
191
|
-
}
|
|
192
|
-
} catch (err) {
|
|
193
|
-
s.fail('Analysis failed');
|
|
194
|
-
throw err;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ── Logs ──
|
|
199
|
-
|
|
200
|
-
// Parse a duration string like "6h", "30m", "2d", "90s" into minutes.
|
|
201
|
-
// Returns null on unparseable input so the caller can error out clearly
|
|
202
|
-
// instead of silently falling through to a default.
|
|
203
|
-
function parseDurationToMinutes(value) {
|
|
204
|
-
if (value == null || value === '') return null;
|
|
205
|
-
const m = String(value).trim().match(/^(\d+)\s*(s|m|h|d)?$/i);
|
|
206
|
-
if (!m) return null;
|
|
207
|
-
const n = parseInt(m[1], 10);
|
|
208
|
-
const unit = (m[2] || 'm').toLowerCase();
|
|
209
|
-
if (unit === 's') return Math.max(1, Math.round(n / 60));
|
|
210
|
-
if (unit === 'm') return n;
|
|
211
|
-
if (unit === 'h') return n * 60;
|
|
212
|
-
if (unit === 'd') return n * 60 * 24;
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL'];
|
|
217
|
-
|
|
218
|
-
async function logsList(args, flags) {
|
|
219
|
-
requireAuth();
|
|
220
|
-
const appKey = resolveApp(flags);
|
|
221
|
-
if (!appKey) {
|
|
222
|
-
ui.error('No app specified. Use --app <key> or set a default.');
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let minutes = 60;
|
|
227
|
-
if (flags.since != null) {
|
|
228
|
-
const parsed = parseDurationToMinutes(flags.since);
|
|
229
|
-
if (parsed == null) {
|
|
230
|
-
ui.error(`Invalid --since value "${flags.since}". Expected e.g. 30m, 6h, 2d.`);
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
minutes = parsed;
|
|
234
|
-
} else if (flags.minutes != null) {
|
|
235
|
-
const n = parseInt(flags.minutes, 10);
|
|
236
|
-
if (isNaN(n) || n <= 0) {
|
|
237
|
-
ui.error(`Invalid --minutes value "${flags.minutes}".`);
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
minutes = n;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
let severity = null;
|
|
244
|
-
if (flags.level) {
|
|
245
|
-
severity = String(flags.level).toUpperCase();
|
|
246
|
-
if (!LOG_LEVELS.includes(severity)) {
|
|
247
|
-
ui.error(`Invalid --level "${flags.level}". Expected one of: ${LOG_LEVELS.join(', ').toLowerCase()}.`);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const s = ui.spinner('Fetching logs');
|
|
253
|
-
try {
|
|
254
|
-
const now = Date.now();
|
|
255
|
-
const query = {
|
|
256
|
-
appKeys: appKey,
|
|
257
|
-
limit: flags.limit || 200,
|
|
258
|
-
from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
|
|
259
|
-
to: flags.end || new Date(now).toISOString(),
|
|
143
|
+
|
|
144
|
+
if (result.analysisId && result.status === 'running') {
|
|
145
|
+
const analysisId = result.analysisId;
|
|
146
|
+
for (let i = 0; i < 120; i++) {
|
|
147
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
148
|
+
result = await api.get(`/traces/analyze/${analysisId}`);
|
|
149
|
+
if (result.status === 'completed' || result.status === 'failed') break;
|
|
150
|
+
}
|
|
151
|
+
if (result.status === 'failed') throw new Error(result.error || 'Analysis failed');
|
|
152
|
+
if (result.status === 'running') throw new Error('Analysis timed out');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
s.stop('Analysis complete');
|
|
156
|
+
|
|
157
|
+
if (flags.json) { ui.json(result); return; }
|
|
158
|
+
|
|
159
|
+
const analysis = result.analysis;
|
|
160
|
+
console.log('');
|
|
161
|
+
ui.heading('AI Trace Analysis');
|
|
162
|
+
console.log('');
|
|
163
|
+
|
|
164
|
+
if (typeof analysis === 'object' && analysis !== null) {
|
|
165
|
+
if (analysis.summary) {
|
|
166
|
+
ui.subheading('Summary');
|
|
167
|
+
console.log(`\n ${analysis.summary}\n`);
|
|
168
|
+
}
|
|
169
|
+
if (analysis.riskLevel) {
|
|
170
|
+
console.log(` ${ui.c.bold('Risk Level:')} ${ui.statusBadge(analysis.riskLevel)}\n`);
|
|
171
|
+
}
|
|
172
|
+
if (analysis.securityIssues?.length) {
|
|
173
|
+
ui.subheading('Security Issues');
|
|
174
|
+
console.log('');
|
|
175
|
+
analysis.securityIssues.forEach((issue, i) => {
|
|
176
|
+
console.log(` ${i + 1}. ${typeof issue === 'string' ? issue : issue.description || JSON.stringify(issue)}`);
|
|
177
|
+
});
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
if (analysis.recommendations?.length) {
|
|
181
|
+
ui.subheading('Recommendations');
|
|
182
|
+
console.log('');
|
|
183
|
+
analysis.recommendations.forEach((rec, i) => {
|
|
184
|
+
console.log(` ${i + 1}. ${typeof rec === 'string' ? rec : rec.description || JSON.stringify(rec)}`);
|
|
185
|
+
});
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
console.log(analysis || JSON.stringify(result, null, 2));
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
s.fail('Analysis failed');
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Logs ──
|
|
199
|
+
|
|
200
|
+
// Parse a duration string like "6h", "30m", "2d", "90s" into minutes.
|
|
201
|
+
// Returns null on unparseable input so the caller can error out clearly
|
|
202
|
+
// instead of silently falling through to a default.
|
|
203
|
+
function parseDurationToMinutes(value) {
|
|
204
|
+
if (value == null || value === '') return null;
|
|
205
|
+
const m = String(value).trim().match(/^(\d+)\s*(s|m|h|d)?$/i);
|
|
206
|
+
if (!m) return null;
|
|
207
|
+
const n = parseInt(m[1], 10);
|
|
208
|
+
const unit = (m[2] || 'm').toLowerCase();
|
|
209
|
+
if (unit === 's') return Math.max(1, Math.round(n / 60));
|
|
210
|
+
if (unit === 'm') return n;
|
|
211
|
+
if (unit === 'h') return n * 60;
|
|
212
|
+
if (unit === 'd') return n * 60 * 24;
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL'];
|
|
217
|
+
|
|
218
|
+
async function logsList(args, flags) {
|
|
219
|
+
requireAuth();
|
|
220
|
+
const appKey = resolveApp(flags);
|
|
221
|
+
if (!appKey) {
|
|
222
|
+
ui.error('No app specified. Use --app <key> or set a default.');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let minutes = 60;
|
|
227
|
+
if (flags.since != null) {
|
|
228
|
+
const parsed = parseDurationToMinutes(flags.since);
|
|
229
|
+
if (parsed == null) {
|
|
230
|
+
ui.error(`Invalid --since value "${flags.since}". Expected e.g. 30m, 6h, 2d.`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
minutes = parsed;
|
|
234
|
+
} else if (flags.minutes != null) {
|
|
235
|
+
const n = parseInt(flags.minutes, 10);
|
|
236
|
+
if (isNaN(n) || n <= 0) {
|
|
237
|
+
ui.error(`Invalid --minutes value "${flags.minutes}".`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
minutes = n;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let severity = null;
|
|
244
|
+
if (flags.level) {
|
|
245
|
+
severity = String(flags.level).toUpperCase();
|
|
246
|
+
if (!LOG_LEVELS.includes(severity)) {
|
|
247
|
+
ui.error(`Invalid --level "${flags.level}". Expected one of: ${LOG_LEVELS.join(', ').toLowerCase()}.`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const s = ui.spinner('Fetching logs');
|
|
253
|
+
try {
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
const query = {
|
|
256
|
+
appKeys: appKey,
|
|
257
|
+
limit: flags.limit || 200,
|
|
258
|
+
from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
|
|
259
|
+
to: flags.end || new Date(now).toISOString(),
|
|
260
260
|
};
|
|
261
261
|
if (severity) query.severity = severity;
|
|
262
262
|
addEnvironment(query, flags);
|
|
263
263
|
|
|
264
264
|
const data = await api.get('/logs', { query });
|
|
265
|
-
const logs = data.logs || [];
|
|
266
|
-
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
267
|
-
|
|
268
|
-
if (flags.json) { ui.json(logs); return; }
|
|
269
|
-
|
|
270
|
-
console.log('');
|
|
271
|
-
for (const log of logs) {
|
|
272
|
-
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
273
|
-
const levelColor = {
|
|
274
|
-
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
275
|
-
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
276
|
-
}[level] || ui.c.white;
|
|
277
|
-
|
|
278
|
-
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
279
|
-
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
280
|
-
const body = log.body || log.message || log.severityText || '';
|
|
281
|
-
|
|
265
|
+
const logs = data.logs || [];
|
|
266
|
+
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
267
|
+
|
|
268
|
+
if (flags.json) { ui.json(logs); return; }
|
|
269
|
+
|
|
270
|
+
console.log('');
|
|
271
|
+
for (const log of logs) {
|
|
272
|
+
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
273
|
+
const levelColor = {
|
|
274
|
+
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
275
|
+
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
276
|
+
}[level] || ui.c.white;
|
|
277
|
+
|
|
278
|
+
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
279
|
+
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
280
|
+
const body = log.body || log.message || log.severityText || '';
|
|
281
|
+
|
|
282
282
|
const env = log.environment ? ` ${ui.c.dim(`[${log.environment}]`)}` : '';
|
|
283
283
|
console.log(` ${time} ${levelColor(level.padEnd(7))}${env} ${body}`);
|
|
284
|
-
|
|
285
|
-
if (log.traceId && flags.verbose) {
|
|
286
|
-
console.log(` ${ui.c.dim(` trace=${log.traceId} span=${log.spanId || ''}`)}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
console.log('');
|
|
290
|
-
} catch (err) {
|
|
291
|
-
s.fail('Failed to fetch logs');
|
|
292
|
-
throw err;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function logsTrace(args, flags) {
|
|
297
|
-
requireAuth();
|
|
298
|
-
const traceId = args[0];
|
|
299
|
-
if (!traceId) {
|
|
300
|
-
ui.error('Trace ID required. Usage: securenow logs trace <traceId>');
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
|
|
284
|
+
|
|
285
|
+
if (log.traceId && flags.verbose) {
|
|
286
|
+
console.log(` ${ui.c.dim(` trace=${log.traceId} span=${log.spanId || ''}`)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
console.log('');
|
|
290
|
+
} catch (err) {
|
|
291
|
+
s.fail('Failed to fetch logs');
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function logsTrace(args, flags) {
|
|
297
|
+
requireAuth();
|
|
298
|
+
const traceId = args[0];
|
|
299
|
+
if (!traceId) {
|
|
300
|
+
ui.error('Trace ID required. Usage: securenow logs trace <traceId>');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
304
|
const s = ui.spinner('Fetching logs for trace');
|
|
305
305
|
try {
|
|
306
306
|
const query = {};
|
|
@@ -308,144 +308,144 @@ async function logsTrace(args, flags) {
|
|
|
308
308
|
if (appKey) query.appKeys = appKey;
|
|
309
309
|
addEnvironment(query, flags);
|
|
310
310
|
const data = await api.get(`/logs/trace/${traceId}`, { query });
|
|
311
|
-
const logs = data.logs || [];
|
|
312
|
-
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
313
|
-
|
|
314
|
-
if (flags.json) { ui.json(logs); return; }
|
|
315
|
-
|
|
316
|
-
console.log('');
|
|
317
|
-
for (const log of logs) {
|
|
318
|
-
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
319
|
-
const levelColor = {
|
|
320
|
-
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
321
|
-
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
322
|
-
}[level] || ui.c.white;
|
|
323
|
-
|
|
324
|
-
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
325
|
-
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
311
|
+
const logs = data.logs || [];
|
|
312
|
+
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
313
|
+
|
|
314
|
+
if (flags.json) { ui.json(logs); return; }
|
|
315
|
+
|
|
316
|
+
console.log('');
|
|
317
|
+
for (const log of logs) {
|
|
318
|
+
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
319
|
+
const levelColor = {
|
|
320
|
+
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
321
|
+
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
322
|
+
}[level] || ui.c.white;
|
|
323
|
+
|
|
324
|
+
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
325
|
+
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
326
326
|
const env = log.environment ? ` ${ui.c.dim(`[${log.environment}]`)}` : '';
|
|
327
327
|
console.log(` ${time} ${levelColor(level.padEnd(7))}${env} ${log.body || log.message || ''}`);
|
|
328
|
-
}
|
|
329
|
-
console.log('');
|
|
330
|
-
} catch (err) {
|
|
331
|
-
s.fail('Failed to fetch logs');
|
|
332
|
-
throw err;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// ── Notifications ──
|
|
338
|
-
|
|
339
|
-
async function notificationsList(args, flags) {
|
|
340
|
-
requireAuth();
|
|
341
|
-
const s = ui.spinner('Fetching notifications');
|
|
342
|
-
try {
|
|
343
|
-
const query = { limit: flags.limit || 20, page: flags.page || 1 };
|
|
344
|
-
const data = await api.get('/notifications', { query });
|
|
345
|
-
const notifications = data.notifications || [];
|
|
346
|
-
const pagination = data.pagination;
|
|
347
|
-
s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
|
|
348
|
-
|
|
349
|
-
if (flags.json) { ui.json(data); return; }
|
|
350
|
-
|
|
351
|
-
console.log('');
|
|
352
|
-
const rows = notifications.map(n => [
|
|
353
|
-
ui.c.dim(ui.truncate(n._id, 12)),
|
|
354
|
-
ui.statusBadge(n.read ? 'read' : 'unread'),
|
|
355
|
-
ui.truncate(n.title || n.message || n.type || '', 50),
|
|
356
|
-
n.ip || '—',
|
|
357
|
-
n.severity ? ui.statusBadge(n.severity) : '—',
|
|
358
|
-
ui.timeAgo(n.createdAt),
|
|
359
|
-
]);
|
|
360
|
-
|
|
361
|
-
ui.table(['ID', 'Status', 'Title', 'IP', 'Severity', 'Time'], rows);
|
|
362
|
-
console.log('');
|
|
363
|
-
} catch (err) {
|
|
364
|
-
s.fail('Failed to fetch notifications');
|
|
365
|
-
throw err;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function notificationsRead(args, flags) {
|
|
370
|
-
requireAuth();
|
|
371
|
-
const id = args[0];
|
|
372
|
-
if (!id) {
|
|
373
|
-
ui.error('Notification ID required. Usage: securenow notifications read <id>');
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const s = ui.spinner('Marking as read');
|
|
378
|
-
try {
|
|
379
|
-
await api.put(`/notifications/${id}/read`);
|
|
380
|
-
s.stop('Notification marked as read');
|
|
381
|
-
} catch (err) {
|
|
382
|
-
s.fail('Failed to mark notification');
|
|
383
|
-
throw err;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function notificationsReadAll() {
|
|
388
|
-
requireAuth();
|
|
389
|
-
const s = ui.spinner('Marking all as read');
|
|
390
|
-
try {
|
|
391
|
-
await api.put('/notifications/read-all');
|
|
392
|
-
s.stop('All notifications marked as read');
|
|
393
|
-
} catch (err) {
|
|
394
|
-
s.fail('Failed to mark notifications');
|
|
395
|
-
throw err;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async function notificationsUnread() {
|
|
400
|
-
requireAuth();
|
|
401
|
-
try {
|
|
402
|
-
const data = await api.get('/notifications/unread-count');
|
|
403
|
-
const count = data.count ?? 0;
|
|
404
|
-
console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
|
|
405
|
-
} catch (err) {
|
|
406
|
-
throw err;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// ── Status / Dashboard Overview ──
|
|
411
|
-
|
|
412
|
-
async function status(args, flags) {
|
|
413
|
-
requireAuth();
|
|
414
|
-
const s = ui.spinner('Fetching dashboard overview');
|
|
415
|
-
try {
|
|
416
|
-
const [appsData, unreadData] = await Promise.all([
|
|
417
|
-
api.get('/applications'),
|
|
418
|
-
api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
|
|
419
|
-
]);
|
|
420
|
-
|
|
421
|
-
const apps = appsData.applications || [];
|
|
422
|
-
s.stop('Dashboard loaded');
|
|
423
|
-
|
|
424
|
-
console.log('');
|
|
425
|
-
ui.heading('SecureNow Dashboard');
|
|
426
|
-
console.log('');
|
|
427
|
-
|
|
428
|
-
ui.keyValue([
|
|
429
|
-
['Applications', String(apps.length)],
|
|
430
|
-
['Unread Alerts', String(unreadData.count ?? 0)],
|
|
431
|
-
]);
|
|
432
|
-
|
|
433
|
-
if (apps.length > 0) {
|
|
434
|
-
ui.subheading('Applications');
|
|
435
|
-
console.log('');
|
|
436
|
-
const rows = apps.map(app => [
|
|
437
|
-
app.name,
|
|
438
|
-
ui.c.dim(app.key),
|
|
439
|
-
app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('—'),
|
|
440
|
-
]);
|
|
441
|
-
ui.table(['Name', 'Key', 'Hosts'], rows);
|
|
442
|
-
}
|
|
443
|
-
|
|
328
|
+
}
|
|
329
|
+
console.log('');
|
|
330
|
+
} catch (err) {
|
|
331
|
+
s.fail('Failed to fetch logs');
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
// ── Notifications ──
|
|
338
|
+
|
|
339
|
+
async function notificationsList(args, flags) {
|
|
340
|
+
requireAuth();
|
|
341
|
+
const s = ui.spinner('Fetching notifications');
|
|
342
|
+
try {
|
|
343
|
+
const query = { limit: flags.limit || 20, page: flags.page || 1 };
|
|
344
|
+
const data = await api.get('/notifications', { query });
|
|
345
|
+
const notifications = data.notifications || [];
|
|
346
|
+
const pagination = data.pagination;
|
|
347
|
+
s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
|
|
348
|
+
|
|
349
|
+
if (flags.json) { ui.json(data); return; }
|
|
350
|
+
|
|
351
|
+
console.log('');
|
|
352
|
+
const rows = notifications.map(n => [
|
|
353
|
+
ui.c.dim(ui.truncate(n._id, 12)),
|
|
354
|
+
ui.statusBadge(n.read ? 'read' : 'unread'),
|
|
355
|
+
ui.truncate(n.title || n.message || n.type || '', 50),
|
|
356
|
+
n.ip || '—',
|
|
357
|
+
n.severity ? ui.statusBadge(n.severity) : '—',
|
|
358
|
+
ui.timeAgo(n.createdAt),
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
ui.table(['ID', 'Status', 'Title', 'IP', 'Severity', 'Time'], rows);
|
|
362
|
+
console.log('');
|
|
363
|
+
} catch (err) {
|
|
364
|
+
s.fail('Failed to fetch notifications');
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function notificationsRead(args, flags) {
|
|
370
|
+
requireAuth();
|
|
371
|
+
const id = args[0];
|
|
372
|
+
if (!id) {
|
|
373
|
+
ui.error('Notification ID required. Usage: securenow notifications read <id>');
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const s = ui.spinner('Marking as read');
|
|
378
|
+
try {
|
|
379
|
+
await api.put(`/notifications/${id}/read`);
|
|
380
|
+
s.stop('Notification marked as read');
|
|
381
|
+
} catch (err) {
|
|
382
|
+
s.fail('Failed to mark notification');
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function notificationsReadAll() {
|
|
388
|
+
requireAuth();
|
|
389
|
+
const s = ui.spinner('Marking all as read');
|
|
390
|
+
try {
|
|
391
|
+
await api.put('/notifications/read-all');
|
|
392
|
+
s.stop('All notifications marked as read');
|
|
393
|
+
} catch (err) {
|
|
394
|
+
s.fail('Failed to mark notifications');
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function notificationsUnread() {
|
|
400
|
+
requireAuth();
|
|
401
|
+
try {
|
|
402
|
+
const data = await api.get('/notifications/unread-count');
|
|
403
|
+
const count = data.count ?? 0;
|
|
404
|
+
console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── Status / Dashboard Overview ──
|
|
411
|
+
|
|
412
|
+
async function status(args, flags) {
|
|
413
|
+
requireAuth();
|
|
414
|
+
const s = ui.spinner('Fetching dashboard overview');
|
|
415
|
+
try {
|
|
416
|
+
const [appsData, unreadData] = await Promise.all([
|
|
417
|
+
api.get('/applications'),
|
|
418
|
+
api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
|
|
419
|
+
]);
|
|
420
|
+
|
|
421
|
+
const apps = appsData.applications || [];
|
|
422
|
+
s.stop('Dashboard loaded');
|
|
423
|
+
|
|
424
|
+
console.log('');
|
|
425
|
+
ui.heading('SecureNow Dashboard');
|
|
426
|
+
console.log('');
|
|
427
|
+
|
|
428
|
+
ui.keyValue([
|
|
429
|
+
['Applications', String(apps.length)],
|
|
430
|
+
['Unread Alerts', String(unreadData.count ?? 0)],
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
if (apps.length > 0) {
|
|
434
|
+
ui.subheading('Applications');
|
|
435
|
+
console.log('');
|
|
436
|
+
const rows = apps.map(app => [
|
|
437
|
+
app.name,
|
|
438
|
+
ui.c.dim(app.key),
|
|
439
|
+
app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('—'),
|
|
440
|
+
]);
|
|
441
|
+
ui.table(['Name', 'Key', 'Hosts'], rows);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
444
|
const appKey = resolveApp(flags);
|
|
445
445
|
const requestedEnv = resolveEnvironment(flags) || appConfig.resolveDeploymentEnvironment() || 'production';
|
|
446
446
|
const showAllEnvs = requestedEnv === 'all' || requestedEnv === '*';
|
|
447
447
|
{
|
|
448
|
-
try {
|
|
448
|
+
try {
|
|
449
449
|
const protectionData = await api.get('/applications/protection-status');
|
|
450
450
|
const statuses = protectionData.statuses;
|
|
451
451
|
if (statuses && Object.keys(statuses).length > 0) {
|
|
@@ -480,25 +480,25 @@ async function status(args, flags) {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
}
|
|
483
|
-
} catch {}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
console.log('');
|
|
487
|
-
} catch (err) {
|
|
488
|
-
s.fail('Failed to load dashboard');
|
|
489
|
-
throw err;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
module.exports = {
|
|
494
|
-
tracesList,
|
|
495
|
-
tracesShow,
|
|
496
|
-
tracesAnalyze,
|
|
497
|
-
logsList,
|
|
498
|
-
logsTrace,
|
|
499
|
-
notificationsList,
|
|
500
|
-
notificationsRead,
|
|
501
|
-
notificationsReadAll,
|
|
502
|
-
notificationsUnread,
|
|
503
|
-
status,
|
|
504
|
-
};
|
|
483
|
+
} catch {}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
console.log('');
|
|
487
|
+
} catch (err) {
|
|
488
|
+
s.fail('Failed to load dashboard');
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
module.exports = {
|
|
494
|
+
tracesList,
|
|
495
|
+
tracesShow,
|
|
496
|
+
tracesAnalyze,
|
|
497
|
+
logsList,
|
|
498
|
+
logsTrace,
|
|
499
|
+
notificationsList,
|
|
500
|
+
notificationsRead,
|
|
501
|
+
notificationsReadAll,
|
|
502
|
+
notificationsUnread,
|
|
503
|
+
status,
|
|
504
|
+
};
|