securenow 5.17.1 → 6.0.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/LICENSE +15 -0
- package/README.md +40 -243
- package/cli.js +455 -425
- package/console-instrumentation.js +136 -147
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +455 -1339
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +1 -1
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +652 -880
- package/docs/EXPRESS-BODY-CAPTURE.md +12 -13
- package/docs/EXPRESS-SETUP-GUIDE.md +720 -719
- package/docs/INDEX.md +4 -22
- package/docs/LOGGING-GUIDE.md +708 -701
- package/docs/LOGGING-QUICKSTART.md +239 -234
- 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-WRAPPER-APPROACH.md +1 -1
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +10 -19
- 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/nextjs-auto-capture.js +207 -199
- package/nextjs-middleware.js +181 -186
- package/nextjs-webpack-config.js +53 -88
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +135 -190
- package/package.json +45 -67
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +4 -39
- package/tracing.d.ts +1 -2
- package/tracing.js +22 -287
- package/web-vite.mjs +156 -239
- package/CONSUMING-APPS-GUIDE.md +0 -455
- package/NPM_README.md +0 -1958
- package/SKILL-API.md +0 -600
- package/SKILL-CLI.md +0 -419
- package/cidr.js +0 -83
- package/cli/apps.js +0 -585
- package/cli/auth.js +0 -280
- package/cli/client.js +0 -115
- package/cli/config.js +0 -173
- package/cli/firewall.js +0 -100
- package/cli/fp.js +0 -638
- package/cli/init.js +0 -201
- package/cli/monitor.js +0 -545
- package/cli/run.js +0 -133
- package/cli/security.js +0 -1064
- package/cli/ui.js +0 -386
- package/docs/API-KEYS-GUIDE.md +0 -233
- package/docs/AUTO-SETUP-SUMMARY.md +0 -331
- package/docs/BODY-CAPTURE-FIX.md +0 -261
- package/docs/COMPLETION-REPORT.md +0 -408
- package/docs/FINAL-SOLUTION.md +0 -335
- package/docs/FIREWALL-GUIDE.md +0 -426
- package/docs/IMPLEMENTATION-SUMMARY.md +0 -410
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +0 -323
- package/docs/NEXTJS-SETUP-COMPLETE.md +0 -795
- package/docs/NUXT-GUIDE.md +0 -166
- package/docs/SOLUTION-SUMMARY.md +0 -312
- package/firewall-cloud.js +0 -212
- package/firewall-iptables.js +0 -139
- package/firewall-only.js +0 -38
- package/firewall-tcp.js +0 -74
- package/firewall.js +0 -720
- package/free-trial-banner.js +0 -174
- package/nuxt-server-plugin.mjs +0 -423
- package/nuxt.d.ts +0 -60
- package/nuxt.mjs +0 -75
- package/resolve-ip.js +0 -77
package/cli/monitor.js
DELETED
|
@@ -1,545 +0,0 @@
|
|
|
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
|
-
// ── Traces ──
|
|
12
|
-
|
|
13
|
-
async function tracesList(args, flags) {
|
|
14
|
-
requireAuth();
|
|
15
|
-
const appKey = resolveApp(flags);
|
|
16
|
-
if (!appKey) {
|
|
17
|
-
ui.error('No app specified. Use --app <key> or set a default with `securenow config set defaultApp <key>`');
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const s = ui.spinner('Fetching traces');
|
|
22
|
-
try {
|
|
23
|
-
const query = {
|
|
24
|
-
appKeys: appKey,
|
|
25
|
-
limit: flags.limit || 20,
|
|
26
|
-
};
|
|
27
|
-
if (flags.start) query.from = flags.start;
|
|
28
|
-
if (flags.end) query.to = flags.end;
|
|
29
|
-
|
|
30
|
-
const data = await api.get('/traces', { query });
|
|
31
|
-
const traces = data.traces || [];
|
|
32
|
-
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
33
|
-
|
|
34
|
-
if (flags.json) { ui.json(traces); return; }
|
|
35
|
-
|
|
36
|
-
console.log('');
|
|
37
|
-
const rows = traces.map(t => [
|
|
38
|
-
ui.c.dim(ui.truncate(t.traceID || t.traceId || t._id, 16)),
|
|
39
|
-
t.operationName || t.name || t.serviceName || '—',
|
|
40
|
-
ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
|
|
41
|
-
ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
|
|
42
|
-
t.httpMethod || t.method || '—',
|
|
43
|
-
ui.truncate(t.httpUrl || t.url || t.httpRoute || '', 40),
|
|
44
|
-
ui.timeAgo(t.timestamp),
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
ui.table(['Trace ID', 'Operation', 'Status', 'Duration', 'Method', 'URL', 'Time'], rows);
|
|
48
|
-
console.log('');
|
|
49
|
-
} catch (err) {
|
|
50
|
-
s.fail('Failed to fetch traces');
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function tracesShow(args, flags) {
|
|
56
|
-
requireAuth();
|
|
57
|
-
const traceId = args[0];
|
|
58
|
-
if (!traceId) {
|
|
59
|
-
ui.error('Trace ID is required. Usage: securenow traces show <traceId>');
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const s = ui.spinner('Fetching trace details');
|
|
64
|
-
try {
|
|
65
|
-
const appKey = resolveApp(flags);
|
|
66
|
-
const traceQuery = appKey ? { appKeys: appKey } : {};
|
|
67
|
-
const data = await api.get(`/traces/${traceId}`, { query: traceQuery });
|
|
68
|
-
s.stop('Trace loaded');
|
|
69
|
-
|
|
70
|
-
if (flags.json) { ui.json(data); return; }
|
|
71
|
-
|
|
72
|
-
const spans = data.spans || [];
|
|
73
|
-
console.log('');
|
|
74
|
-
ui.heading(`Trace ${data.traceId || traceId}`);
|
|
75
|
-
|
|
76
|
-
if (spans.length) {
|
|
77
|
-
ui.subheading(`Spans (${spans.length})`);
|
|
78
|
-
console.log('');
|
|
79
|
-
const rows = spans.map(span => [
|
|
80
|
-
ui.c.dim(ui.truncate(span.spanID || span.spanId, 16)),
|
|
81
|
-
span.operationName || span.name || '—',
|
|
82
|
-
ui.httpStatusColor(span.statusCode || span.responseStatusCode || '—'),
|
|
83
|
-
ui.durationColor(span.durationNano ? span.durationNano / 1e6 : span.duration),
|
|
84
|
-
span.kind || '—',
|
|
85
|
-
]);
|
|
86
|
-
ui.table(['Span ID', 'Operation', 'Status', 'Duration', 'Kind'], rows);
|
|
87
|
-
} else {
|
|
88
|
-
console.log('');
|
|
89
|
-
ui.info('No spans found for this trace.');
|
|
90
|
-
}
|
|
91
|
-
console.log('');
|
|
92
|
-
} catch (err) {
|
|
93
|
-
s.fail('Failed to fetch trace');
|
|
94
|
-
throw err;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function tracesAnalyze(args, flags) {
|
|
99
|
-
requireAuth();
|
|
100
|
-
const traceId = args[0];
|
|
101
|
-
if (!traceId) {
|
|
102
|
-
ui.error('Trace ID is required. Usage: securenow traces analyze <traceId>');
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const s = ui.spinner('Analyzing trace with AI');
|
|
107
|
-
try {
|
|
108
|
-
let result = await api.post('/traces/analyze', { traceId });
|
|
109
|
-
|
|
110
|
-
if (result.analysisId && result.status === 'running') {
|
|
111
|
-
const analysisId = result.analysisId;
|
|
112
|
-
for (let i = 0; i < 120; i++) {
|
|
113
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
114
|
-
result = await api.get(`/traces/analyze/${analysisId}`);
|
|
115
|
-
if (result.status === 'completed' || result.status === 'failed') break;
|
|
116
|
-
}
|
|
117
|
-
if (result.status === 'failed') throw new Error(result.error || 'Analysis failed');
|
|
118
|
-
if (result.status === 'running') throw new Error('Analysis timed out');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
s.stop('Analysis complete');
|
|
122
|
-
|
|
123
|
-
if (flags.json) { ui.json(result); return; }
|
|
124
|
-
|
|
125
|
-
const analysis = result.analysis;
|
|
126
|
-
console.log('');
|
|
127
|
-
ui.heading('AI Trace Analysis');
|
|
128
|
-
console.log('');
|
|
129
|
-
|
|
130
|
-
if (typeof analysis === 'object' && analysis !== null) {
|
|
131
|
-
if (analysis.summary) {
|
|
132
|
-
ui.subheading('Summary');
|
|
133
|
-
console.log(`\n ${analysis.summary}\n`);
|
|
134
|
-
}
|
|
135
|
-
if (analysis.riskLevel) {
|
|
136
|
-
console.log(` ${ui.c.bold('Risk Level:')} ${ui.statusBadge(analysis.riskLevel)}\n`);
|
|
137
|
-
}
|
|
138
|
-
if (analysis.securityIssues?.length) {
|
|
139
|
-
ui.subheading('Security Issues');
|
|
140
|
-
console.log('');
|
|
141
|
-
analysis.securityIssues.forEach((issue, i) => {
|
|
142
|
-
console.log(` ${i + 1}. ${typeof issue === 'string' ? issue : issue.description || JSON.stringify(issue)}`);
|
|
143
|
-
});
|
|
144
|
-
console.log('');
|
|
145
|
-
}
|
|
146
|
-
if (analysis.recommendations?.length) {
|
|
147
|
-
ui.subheading('Recommendations');
|
|
148
|
-
console.log('');
|
|
149
|
-
analysis.recommendations.forEach((rec, i) => {
|
|
150
|
-
console.log(` ${i + 1}. ${typeof rec === 'string' ? rec : rec.description || JSON.stringify(rec)}`);
|
|
151
|
-
});
|
|
152
|
-
console.log('');
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
console.log(analysis || JSON.stringify(result, null, 2));
|
|
156
|
-
console.log('');
|
|
157
|
-
}
|
|
158
|
-
} catch (err) {
|
|
159
|
-
s.fail('Analysis failed');
|
|
160
|
-
throw err;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ── Logs ──
|
|
165
|
-
|
|
166
|
-
// Parse a duration string like "6h", "30m", "2d", "90s" into minutes.
|
|
167
|
-
// Returns null on unparseable input so the caller can error out clearly
|
|
168
|
-
// instead of silently falling through to a default.
|
|
169
|
-
function parseDurationToMinutes(value) {
|
|
170
|
-
if (value == null || value === '') return null;
|
|
171
|
-
const m = String(value).trim().match(/^(\d+)\s*(s|m|h|d)?$/i);
|
|
172
|
-
if (!m) return null;
|
|
173
|
-
const n = parseInt(m[1], 10);
|
|
174
|
-
const unit = (m[2] || 'm').toLowerCase();
|
|
175
|
-
if (unit === 's') return Math.max(1, Math.round(n / 60));
|
|
176
|
-
if (unit === 'm') return n;
|
|
177
|
-
if (unit === 'h') return n * 60;
|
|
178
|
-
if (unit === 'd') return n * 60 * 24;
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL'];
|
|
183
|
-
|
|
184
|
-
async function logsList(args, flags) {
|
|
185
|
-
requireAuth();
|
|
186
|
-
const appKey = resolveApp(flags);
|
|
187
|
-
if (!appKey) {
|
|
188
|
-
ui.error('No app specified. Use --app <key> or set a default.');
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let minutes = 60;
|
|
193
|
-
if (flags.since != null) {
|
|
194
|
-
const parsed = parseDurationToMinutes(flags.since);
|
|
195
|
-
if (parsed == null) {
|
|
196
|
-
ui.error(`Invalid --since value "${flags.since}". Expected e.g. 30m, 6h, 2d.`);
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
minutes = parsed;
|
|
200
|
-
} else if (flags.minutes != null) {
|
|
201
|
-
const n = parseInt(flags.minutes, 10);
|
|
202
|
-
if (isNaN(n) || n <= 0) {
|
|
203
|
-
ui.error(`Invalid --minutes value "${flags.minutes}".`);
|
|
204
|
-
process.exit(1);
|
|
205
|
-
}
|
|
206
|
-
minutes = n;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let severity = null;
|
|
210
|
-
if (flags.level) {
|
|
211
|
-
severity = String(flags.level).toUpperCase();
|
|
212
|
-
if (!LOG_LEVELS.includes(severity)) {
|
|
213
|
-
ui.error(`Invalid --level "${flags.level}". Expected one of: ${LOG_LEVELS.join(', ').toLowerCase()}.`);
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const s = ui.spinner('Fetching logs');
|
|
219
|
-
try {
|
|
220
|
-
const now = Date.now();
|
|
221
|
-
const query = {
|
|
222
|
-
appKeys: appKey,
|
|
223
|
-
limit: flags.limit || 200,
|
|
224
|
-
from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
|
|
225
|
-
to: flags.end || new Date(now).toISOString(),
|
|
226
|
-
};
|
|
227
|
-
if (severity) query.severity = severity;
|
|
228
|
-
|
|
229
|
-
const data = await api.get('/logs', { query });
|
|
230
|
-
const logs = data.logs || [];
|
|
231
|
-
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
232
|
-
|
|
233
|
-
if (flags.json) { ui.json(logs); return; }
|
|
234
|
-
|
|
235
|
-
console.log('');
|
|
236
|
-
for (const log of logs) {
|
|
237
|
-
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
238
|
-
const levelColor = {
|
|
239
|
-
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
240
|
-
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
241
|
-
}[level] || ui.c.white;
|
|
242
|
-
|
|
243
|
-
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
244
|
-
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
245
|
-
const body = log.body || log.message || log.severityText || '';
|
|
246
|
-
|
|
247
|
-
console.log(` ${time} ${levelColor(level.padEnd(7))} ${body}`);
|
|
248
|
-
|
|
249
|
-
if (log.traceId && flags.verbose) {
|
|
250
|
-
console.log(` ${ui.c.dim(` trace=${log.traceId} span=${log.spanId || ''}`)}`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
console.log('');
|
|
254
|
-
} catch (err) {
|
|
255
|
-
s.fail('Failed to fetch logs');
|
|
256
|
-
throw err;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function logsTrace(args, flags) {
|
|
261
|
-
requireAuth();
|
|
262
|
-
const traceId = args[0];
|
|
263
|
-
if (!traceId) {
|
|
264
|
-
ui.error('Trace ID required. Usage: securenow logs trace <traceId>');
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const s = ui.spinner('Fetching logs for trace');
|
|
269
|
-
try {
|
|
270
|
-
const data = await api.get(`/logs/trace/${traceId}`);
|
|
271
|
-
const logs = data.logs || [];
|
|
272
|
-
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
273
|
-
|
|
274
|
-
if (flags.json) { ui.json(logs); return; }
|
|
275
|
-
|
|
276
|
-
console.log('');
|
|
277
|
-
for (const log of logs) {
|
|
278
|
-
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
279
|
-
const levelColor = {
|
|
280
|
-
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
281
|
-
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
282
|
-
}[level] || ui.c.white;
|
|
283
|
-
|
|
284
|
-
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
285
|
-
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
286
|
-
console.log(` ${time} ${levelColor(level.padEnd(7))} ${log.body || log.message || ''}`);
|
|
287
|
-
}
|
|
288
|
-
console.log('');
|
|
289
|
-
} catch (err) {
|
|
290
|
-
s.fail('Failed to fetch logs');
|
|
291
|
-
throw err;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ── Issues ──
|
|
296
|
-
|
|
297
|
-
async function issuesList(args, flags) {
|
|
298
|
-
requireAuth();
|
|
299
|
-
const s = ui.spinner('Fetching issues');
|
|
300
|
-
try {
|
|
301
|
-
const query = {};
|
|
302
|
-
const appKey = resolveApp(flags);
|
|
303
|
-
if (appKey) query.serviceName = appKey;
|
|
304
|
-
if (flags.status) query.status = flags.status;
|
|
305
|
-
|
|
306
|
-
const data = await api.get('/issues', { query });
|
|
307
|
-
const issues = data.issues || [];
|
|
308
|
-
s.stop(`Found ${issues.length} issue${issues.length !== 1 ? 's' : ''}`);
|
|
309
|
-
|
|
310
|
-
if (flags.json) { ui.json(data); return; }
|
|
311
|
-
|
|
312
|
-
console.log('');
|
|
313
|
-
const rows = issues.map(i => [
|
|
314
|
-
ui.c.dim(ui.truncate(i._id, 12)),
|
|
315
|
-
ui.statusBadge(i.severity || i.level || 'medium'),
|
|
316
|
-
ui.statusBadge(i.status || 'open'),
|
|
317
|
-
ui.truncate(i.title || i.message || i.type || '', 50),
|
|
318
|
-
i.serviceName || '—',
|
|
319
|
-
i.count != null ? String(i.count) : '—',
|
|
320
|
-
ui.timeAgo(i.lastSeen || i.createdAt),
|
|
321
|
-
]);
|
|
322
|
-
|
|
323
|
-
ui.table(['ID', 'Severity', 'Status', 'Title', 'App', 'Count', 'Last Seen'], rows);
|
|
324
|
-
if (data.total != null) {
|
|
325
|
-
console.log(ui.c.dim(` Total: ${data.total}`));
|
|
326
|
-
}
|
|
327
|
-
console.log('');
|
|
328
|
-
} catch (err) {
|
|
329
|
-
s.fail('Failed to fetch issues');
|
|
330
|
-
throw err;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async function issuesShow(args, flags) {
|
|
335
|
-
requireAuth();
|
|
336
|
-
const id = args[0];
|
|
337
|
-
if (!id) {
|
|
338
|
-
ui.error('Issue ID required. Usage: securenow issues show <id>');
|
|
339
|
-
process.exit(1);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const s = ui.spinner('Fetching issue');
|
|
343
|
-
try {
|
|
344
|
-
const data = await api.get(`/issues/${id}`);
|
|
345
|
-
const issue = data.issue || data;
|
|
346
|
-
s.stop('Issue loaded');
|
|
347
|
-
|
|
348
|
-
if (flags.json) { ui.json(issue); return; }
|
|
349
|
-
|
|
350
|
-
console.log('');
|
|
351
|
-
ui.heading(issue.title || issue.type || `Issue ${id}`);
|
|
352
|
-
console.log('');
|
|
353
|
-
ui.keyValue([
|
|
354
|
-
['ID', issue._id],
|
|
355
|
-
['Status', ui.statusBadge(issue.status || 'open')],
|
|
356
|
-
['Severity', ui.statusBadge(issue.severity || issue.level || 'medium')],
|
|
357
|
-
['App', issue.serviceName || '—'],
|
|
358
|
-
['Type', issue.type || '—'],
|
|
359
|
-
['Count', issue.count != null ? String(issue.count) : '—'],
|
|
360
|
-
['First Seen', issue.firstSeen ? new Date(issue.firstSeen).toLocaleString() : '—'],
|
|
361
|
-
['Last Seen', issue.lastSeen ? new Date(issue.lastSeen).toLocaleString() : '—'],
|
|
362
|
-
]);
|
|
363
|
-
|
|
364
|
-
if (issue.message || issue.description) {
|
|
365
|
-
ui.subheading('Description');
|
|
366
|
-
console.log(`\n ${issue.message || issue.description}\n`);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (issue.analysis) {
|
|
370
|
-
ui.subheading('AI Analysis');
|
|
371
|
-
console.log(`\n ${issue.analysis}\n`);
|
|
372
|
-
}
|
|
373
|
-
console.log('');
|
|
374
|
-
} catch (err) {
|
|
375
|
-
s.fail('Failed to fetch issue');
|
|
376
|
-
throw err;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async function issuesResolve(args, flags) {
|
|
381
|
-
requireAuth();
|
|
382
|
-
const id = args[0];
|
|
383
|
-
if (!id) {
|
|
384
|
-
ui.error('Issue ID required. Usage: securenow issues resolve <id>');
|
|
385
|
-
process.exit(1);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const s = ui.spinner('Resolving issue');
|
|
389
|
-
try {
|
|
390
|
-
await api.patch(`/issues/${id}`, { status: 'resolved' });
|
|
391
|
-
s.stop('Issue resolved');
|
|
392
|
-
} catch (err) {
|
|
393
|
-
s.fail('Failed to resolve issue');
|
|
394
|
-
throw err;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ── Notifications ──
|
|
399
|
-
|
|
400
|
-
async function notificationsList(args, flags) {
|
|
401
|
-
requireAuth();
|
|
402
|
-
const s = ui.spinner('Fetching notifications');
|
|
403
|
-
try {
|
|
404
|
-
const query = { limit: flags.limit || 20, page: flags.page || 1 };
|
|
405
|
-
const data = await api.get('/notifications', { query });
|
|
406
|
-
const notifications = data.notifications || [];
|
|
407
|
-
const pagination = data.pagination;
|
|
408
|
-
s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
|
|
409
|
-
|
|
410
|
-
if (flags.json) { ui.json(data); return; }
|
|
411
|
-
|
|
412
|
-
console.log('');
|
|
413
|
-
const rows = notifications.map(n => [
|
|
414
|
-
ui.c.dim(ui.truncate(n._id, 12)),
|
|
415
|
-
ui.statusBadge(n.read ? 'read' : 'unread'),
|
|
416
|
-
ui.truncate(n.title || n.message || n.type || '', 50),
|
|
417
|
-
n.ip || '—',
|
|
418
|
-
n.severity ? ui.statusBadge(n.severity) : '—',
|
|
419
|
-
ui.timeAgo(n.createdAt),
|
|
420
|
-
]);
|
|
421
|
-
|
|
422
|
-
ui.table(['ID', 'Status', 'Title', 'IP', 'Severity', 'Time'], rows);
|
|
423
|
-
console.log('');
|
|
424
|
-
} catch (err) {
|
|
425
|
-
s.fail('Failed to fetch notifications');
|
|
426
|
-
throw err;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
async function notificationsRead(args, flags) {
|
|
431
|
-
requireAuth();
|
|
432
|
-
const id = args[0];
|
|
433
|
-
if (!id) {
|
|
434
|
-
ui.error('Notification ID required. Usage: securenow notifications read <id>');
|
|
435
|
-
process.exit(1);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const s = ui.spinner('Marking as read');
|
|
439
|
-
try {
|
|
440
|
-
await api.put(`/notifications/${id}/read`);
|
|
441
|
-
s.stop('Notification marked as read');
|
|
442
|
-
} catch (err) {
|
|
443
|
-
s.fail('Failed to mark notification');
|
|
444
|
-
throw err;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
async function notificationsReadAll() {
|
|
449
|
-
requireAuth();
|
|
450
|
-
const s = ui.spinner('Marking all as read');
|
|
451
|
-
try {
|
|
452
|
-
await api.put('/notifications/read-all');
|
|
453
|
-
s.stop('All notifications marked as read');
|
|
454
|
-
} catch (err) {
|
|
455
|
-
s.fail('Failed to mark notifications');
|
|
456
|
-
throw err;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async function notificationsUnread() {
|
|
461
|
-
requireAuth();
|
|
462
|
-
try {
|
|
463
|
-
const data = await api.get('/notifications/unread-count');
|
|
464
|
-
const count = data.count ?? 0;
|
|
465
|
-
console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
|
|
466
|
-
} catch (err) {
|
|
467
|
-
throw err;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// ── Status / Dashboard Overview ──
|
|
472
|
-
|
|
473
|
-
async function status(args, flags) {
|
|
474
|
-
requireAuth();
|
|
475
|
-
const s = ui.spinner('Fetching dashboard overview');
|
|
476
|
-
try {
|
|
477
|
-
const [appsData, unreadData] = await Promise.all([
|
|
478
|
-
api.get('/applications'),
|
|
479
|
-
api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
|
|
480
|
-
]);
|
|
481
|
-
|
|
482
|
-
const apps = appsData.applications || [];
|
|
483
|
-
s.stop('Dashboard loaded');
|
|
484
|
-
|
|
485
|
-
console.log('');
|
|
486
|
-
ui.heading('SecureNow Dashboard');
|
|
487
|
-
console.log('');
|
|
488
|
-
|
|
489
|
-
ui.keyValue([
|
|
490
|
-
['Applications', String(apps.length)],
|
|
491
|
-
['Unread Alerts', String(unreadData.count ?? 0)],
|
|
492
|
-
]);
|
|
493
|
-
|
|
494
|
-
if (apps.length > 0) {
|
|
495
|
-
ui.subheading('Applications');
|
|
496
|
-
console.log('');
|
|
497
|
-
const rows = apps.map(app => [
|
|
498
|
-
app.name,
|
|
499
|
-
ui.c.dim(app.key),
|
|
500
|
-
app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('—'),
|
|
501
|
-
]);
|
|
502
|
-
ui.table(['Name', 'Key', 'Hosts'], rows);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const appKey = resolveApp(flags);
|
|
506
|
-
if (appKey) {
|
|
507
|
-
try {
|
|
508
|
-
const protectionData = await api.get('/applications/protection-status');
|
|
509
|
-
const statuses = protectionData.statuses;
|
|
510
|
-
if (statuses && Object.keys(statuses).length > 0) {
|
|
511
|
-
ui.subheading('Protection Status');
|
|
512
|
-
console.log('');
|
|
513
|
-
const rows = Object.entries(statuses).map(([id, s]) => [
|
|
514
|
-
ui.c.dim(ui.truncate(id, 12)),
|
|
515
|
-
s.protected ? ui.c.green('● protected') : ui.c.red('○ unprotected'),
|
|
516
|
-
String(s.traceCount || 0),
|
|
517
|
-
s.lastTrace ? ui.timeAgo(s.lastTrace) : ui.c.dim('—'),
|
|
518
|
-
]);
|
|
519
|
-
ui.table(['App ID', 'Status', 'Traces (15m)', 'Last Trace'], rows);
|
|
520
|
-
}
|
|
521
|
-
} catch {}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
console.log('');
|
|
525
|
-
} catch (err) {
|
|
526
|
-
s.fail('Failed to load dashboard');
|
|
527
|
-
throw err;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
module.exports = {
|
|
532
|
-
tracesList,
|
|
533
|
-
tracesShow,
|
|
534
|
-
tracesAnalyze,
|
|
535
|
-
logsList,
|
|
536
|
-
logsTrace,
|
|
537
|
-
issuesList,
|
|
538
|
-
issuesShow,
|
|
539
|
-
issuesResolve,
|
|
540
|
-
notificationsList,
|
|
541
|
-
notificationsRead,
|
|
542
|
-
notificationsReadAll,
|
|
543
|
-
notificationsUnread,
|
|
544
|
-
status,
|
|
545
|
-
};
|
package/cli/run.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { spawn } = require('child_process');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const ui = require('./ui');
|
|
7
|
-
|
|
8
|
-
function isESM(scriptPath) {
|
|
9
|
-
if (scriptPath.endsWith('.mjs')) return true;
|
|
10
|
-
if (scriptPath.endsWith('.cjs')) return false;
|
|
11
|
-
|
|
12
|
-
let dir = path.resolve(path.dirname(scriptPath));
|
|
13
|
-
while (true) {
|
|
14
|
-
const pkgPath = path.join(dir, 'package.json');
|
|
15
|
-
if (fs.existsSync(pkgPath)) {
|
|
16
|
-
try {
|
|
17
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
18
|
-
return pkg.type === 'module';
|
|
19
|
-
} catch { return false; }
|
|
20
|
-
}
|
|
21
|
-
const parent = path.dirname(dir);
|
|
22
|
-
if (parent === dir) break;
|
|
23
|
-
dir = parent;
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function resolveSecurenowRegister() {
|
|
29
|
-
// When running from the published package, securenow/register resolves to our own register.js.
|
|
30
|
-
// When running from the monorepo, resolve relative to this file.
|
|
31
|
-
try {
|
|
32
|
-
return require.resolve('securenow/register');
|
|
33
|
-
} catch {
|
|
34
|
-
return path.resolve(__dirname, '..', 'register.js');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resolveESMHook() {
|
|
39
|
-
// --import uses ESM resolution which requires file:// URLs for absolute paths
|
|
40
|
-
// on Windows. Using the bare specifier lets Node's own resolver handle it.
|
|
41
|
-
try {
|
|
42
|
-
const resolved = require.resolve('@opentelemetry/instrumentation/hook.mjs');
|
|
43
|
-
const { pathToFileURL } = require('url');
|
|
44
|
-
return pathToFileURL(resolved).href;
|
|
45
|
-
} catch {
|
|
46
|
-
return '@opentelemetry/instrumentation/hook.mjs';
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* securenow run [node-flags...] <script> [app-args...]
|
|
52
|
-
*
|
|
53
|
-
* Spawns node with the correct OTel preload flags.
|
|
54
|
-
* Passes through Node flags (--watch, --inspect, etc.) and app arguments.
|
|
55
|
-
*
|
|
56
|
-
* We re-parse from process.argv directly so that flags like --watch and
|
|
57
|
-
* --inspect are forwarded to node verbatim (the CLI's own arg parser
|
|
58
|
-
* would otherwise consume them).
|
|
59
|
-
*/
|
|
60
|
-
function run(rawArgs) {
|
|
61
|
-
// rawArgs comes directly from process.argv, everything after `run` (or the file path)
|
|
62
|
-
if (!rawArgs || rawArgs.length === 0) {
|
|
63
|
-
ui.error('Missing script path.');
|
|
64
|
-
console.log('');
|
|
65
|
-
console.log(` ${ui.c.bold('Usage:')} securenow run [node-flags] <script> [app-args]`);
|
|
66
|
-
console.log('');
|
|
67
|
-
console.log(` ${ui.c.bold('Examples:')}`);
|
|
68
|
-
console.log(` securenow run src/index.js`);
|
|
69
|
-
console.log(` securenow run --watch src/index.js`);
|
|
70
|
-
console.log(` securenow run --inspect src/server.js --port 3000`);
|
|
71
|
-
console.log('');
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const args = rawArgs;
|
|
76
|
-
|
|
77
|
-
// Split into: node flags (before the script), script path, and app args (after)
|
|
78
|
-
const nodeFlags = [];
|
|
79
|
-
let scriptIdx = -1;
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < args.length; i++) {
|
|
82
|
-
if (args[i].startsWith('-')) {
|
|
83
|
-
nodeFlags.push(args[i]);
|
|
84
|
-
} else {
|
|
85
|
-
scriptIdx = i;
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (scriptIdx === -1) {
|
|
91
|
-
ui.error('No script path found. Provide a .js/.mjs/.ts file to run.');
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const scriptPath = args[scriptIdx];
|
|
96
|
-
const appArgs = args.slice(scriptIdx + 1);
|
|
97
|
-
|
|
98
|
-
const esm = isESM(scriptPath);
|
|
99
|
-
const registerPath = resolveSecurenowRegister();
|
|
100
|
-
|
|
101
|
-
const otelFlags = [];
|
|
102
|
-
if (esm) {
|
|
103
|
-
otelFlags.push('--import', resolveESMHook());
|
|
104
|
-
}
|
|
105
|
-
otelFlags.push('--require', registerPath);
|
|
106
|
-
|
|
107
|
-
const finalArgs = [...otelFlags, ...nodeFlags, scriptPath, ...appArgs];
|
|
108
|
-
|
|
109
|
-
const nodeExe = process.execPath;
|
|
110
|
-
|
|
111
|
-
if (process.env.SECURENOW_DEBUG) {
|
|
112
|
-
console.log(`[securenow] ${ui.c.dim('exec:')} ${nodeExe} ${finalArgs.join(' ')}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const child = spawn(nodeExe, finalArgs, {
|
|
116
|
-
stdio: 'inherit',
|
|
117
|
-
env: process.env,
|
|
118
|
-
cwd: process.cwd(),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
child.on('close', (code) => process.exit(code ?? 1));
|
|
122
|
-
child.on('error', (err) => {
|
|
123
|
-
ui.error(`Failed to start: ${err.message}`);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Forward termination signals to the child
|
|
128
|
-
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
129
|
-
process.on(sig, () => child.kill(sig));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
module.exports = { run };
|