securenow 6.0.2 → 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 +639 -647
- package/nuxt-server-plugin.mjs +423 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +186 -164
- 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 +295 -34
- package/web-vite.mjs +239 -156
- package/LICENSE +0 -15
package/cli/init.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const ui = require('./ui');
|
|
6
|
+
|
|
7
|
+
const INSTRUMENTATION_JS = `export async function register() {
|
|
8
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
9
|
+
const { registerSecureNow } = require('securenow/nextjs');
|
|
10
|
+
registerSecureNow();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const INSTRUMENTATION_TS = `export async function register() {
|
|
16
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
17
|
+
const { registerSecureNow } = require('securenow/nextjs');
|
|
18
|
+
registerSecureNow();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
function detectProject(dir) {
|
|
24
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
25
|
+
if (!fs.existsSync(pkgPath)) return { framework: 'unknown' };
|
|
26
|
+
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
28
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
29
|
+
|
|
30
|
+
if (allDeps.next) return { framework: 'nextjs', pkg, pkgPath, nextVersion: allDeps.next };
|
|
31
|
+
if (allDeps.nuxt) return { framework: 'nuxt', pkg, pkgPath };
|
|
32
|
+
if (allDeps.express) return { framework: 'express', pkg, pkgPath };
|
|
33
|
+
if (allDeps.fastify) return { framework: 'fastify', pkg, pkgPath };
|
|
34
|
+
if (allDeps.koa) return { framework: 'koa', pkg, pkgPath };
|
|
35
|
+
if (allDeps.hapi || allDeps['@hapi/hapi']) return { framework: 'hapi', pkg, pkgPath };
|
|
36
|
+
return { framework: 'node', pkg, pkgPath };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findInstrumentationFile(dir) {
|
|
40
|
+
for (const name of ['instrumentation.ts', 'instrumentation.js', 'src/instrumentation.ts', 'src/instrumentation.js']) {
|
|
41
|
+
const p = path.join(dir, name);
|
|
42
|
+
if (fs.existsSync(p)) return p;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findNextConfig(dir) {
|
|
48
|
+
for (const name of ['next.config.js', 'next.config.mjs', 'next.config.ts']) {
|
|
49
|
+
const p = path.join(dir, name);
|
|
50
|
+
if (fs.existsSync(p)) return p;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasTypeScript(dir) {
|
|
56
|
+
return fs.existsSync(path.join(dir, 'tsconfig.json'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function init(_args, flags) {
|
|
60
|
+
const dir = process.cwd();
|
|
61
|
+
const project = detectProject(dir);
|
|
62
|
+
|
|
63
|
+
ui.header('SecureNow Project Setup');
|
|
64
|
+
|
|
65
|
+
if (project.framework === 'unknown') {
|
|
66
|
+
ui.error('No package.json found. Run this command in your project root.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ui.info(`Detected framework: ${ui.bold(project.framework)}`);
|
|
71
|
+
|
|
72
|
+
if (project.framework === 'nextjs') {
|
|
73
|
+
await initNextJs(dir, project, flags);
|
|
74
|
+
} else if (project.framework === 'nuxt') {
|
|
75
|
+
initNuxt(dir, project);
|
|
76
|
+
} else {
|
|
77
|
+
initNode(dir, project);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
initEnv(dir, flags);
|
|
81
|
+
|
|
82
|
+
console.log('');
|
|
83
|
+
ui.success('Setup complete! Run your app to verify.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function initNextJs(dir, project, flags) {
|
|
87
|
+
const useTs = hasTypeScript(dir);
|
|
88
|
+
const ext = useTs ? 'ts' : 'js';
|
|
89
|
+
|
|
90
|
+
const existing = findInstrumentationFile(dir);
|
|
91
|
+
if (existing) {
|
|
92
|
+
ui.info(`instrumentation file already exists: ${path.relative(dir, existing)}`);
|
|
93
|
+
} else {
|
|
94
|
+
const filePath = path.join(dir, `instrumentation.${ext}`);
|
|
95
|
+
const content = useTs ? INSTRUMENTATION_TS : INSTRUMENTATION_JS;
|
|
96
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
97
|
+
ui.success(`Created instrumentation.${ext}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const configPath = findNextConfig(dir);
|
|
101
|
+
if (configPath) {
|
|
102
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
103
|
+
if (content.includes('withSecureNow')) {
|
|
104
|
+
ui.info('next.config already uses withSecureNow — skipping');
|
|
105
|
+
} else if (content.includes('serverExternalPackages') && content.includes('securenow')) {
|
|
106
|
+
ui.info('next.config already externalizes securenow — skipping');
|
|
107
|
+
} else if (content.includes('serverComponentsExternalPackages') && content.includes('securenow')) {
|
|
108
|
+
ui.info('next.config already externalizes securenow — skipping');
|
|
109
|
+
} else {
|
|
110
|
+
ui.warn(`Update your ${path.basename(configPath)} to use withSecureNow():`);
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(` const { withSecureNow } = require('securenow/nextjs-webpack-config');`);
|
|
113
|
+
console.log(` module.exports = withSecureNow({ /* your config */ });`);
|
|
114
|
+
console.log('');
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const newConfigPath = path.join(dir, 'next.config.js');
|
|
118
|
+
fs.writeFileSync(newConfigPath, `const { withSecureNow } = require('securenow/nextjs-webpack-config');\n\nmodule.exports = withSecureNow({\n reactStrictMode: true,\n});\n`, 'utf8');
|
|
119
|
+
ui.success('Created next.config.js with withSecureNow()');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function initNuxt(dir, project) {
|
|
124
|
+
const configPath = path.join(dir, 'nuxt.config.ts');
|
|
125
|
+
if (fs.existsSync(configPath)) {
|
|
126
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
127
|
+
if (content.includes('securenow/nuxt')) {
|
|
128
|
+
ui.info('nuxt.config already references securenow/nuxt — skipping');
|
|
129
|
+
} else {
|
|
130
|
+
ui.warn('Add securenow/nuxt to your nuxt.config modules:');
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(" modules: ['securenow/nuxt'],");
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
ui.warn('Add securenow/nuxt to your nuxt.config modules array.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function initNode(dir, project) {
|
|
141
|
+
const pkg = project.pkg;
|
|
142
|
+
const scripts = pkg.scripts || {};
|
|
143
|
+
|
|
144
|
+
const startScript = scripts.start || '';
|
|
145
|
+
if (startScript.includes('securenow/register')) {
|
|
146
|
+
ui.info('start script already uses securenow/register — skipping');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (startScript) {
|
|
151
|
+
ui.warn('Update your start script to include the securenow preload:');
|
|
152
|
+
console.log('');
|
|
153
|
+
if (startScript.includes('node ')) {
|
|
154
|
+
const updated = startScript.replace('node ', 'node -r securenow/register ');
|
|
155
|
+
console.log(` "start": "${updated}"`);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(` "start": "node -r securenow/register ${startScript.replace(/^node\s+/, '')}"`);
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
} else {
|
|
161
|
+
ui.warn('Add a start script with the securenow preload:');
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(' "start": "node -r securenow/register src/index.js"');
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function initEnv(dir, flags) {
|
|
169
|
+
const envFiles = ['.env', '.env.local'];
|
|
170
|
+
let envPath = null;
|
|
171
|
+
|
|
172
|
+
for (const f of envFiles) {
|
|
173
|
+
const p = path.join(dir, f);
|
|
174
|
+
if (fs.existsSync(p)) {
|
|
175
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
176
|
+
if (content.includes('SECURENOW_API_KEY')) {
|
|
177
|
+
ui.info(`SECURENOW_API_KEY already set in ${f}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
envPath = p;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!envPath) envPath = path.join(dir, '.env.local');
|
|
186
|
+
|
|
187
|
+
const apiKey = flags.key || flags['api-key'] || '';
|
|
188
|
+
if (apiKey) {
|
|
189
|
+
const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
|
|
190
|
+
const sep = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
191
|
+
fs.appendFileSync(envPath, `${sep}SECURENOW_API_KEY=${apiKey}\n`, 'utf8');
|
|
192
|
+
ui.success(`Added SECURENOW_API_KEY to ${path.basename(envPath)}`);
|
|
193
|
+
} else {
|
|
194
|
+
ui.warn(`Add your API key to ${path.basename(envPath)}:`);
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(' SECURENOW_API_KEY=snk_live_...');
|
|
197
|
+
console.log('');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { init };
|
package/cli/monitor.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
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
|
+
|
|
296
|
+
// ── Notifications ──
|
|
297
|
+
|
|
298
|
+
async function notificationsList(args, flags) {
|
|
299
|
+
requireAuth();
|
|
300
|
+
const s = ui.spinner('Fetching notifications');
|
|
301
|
+
try {
|
|
302
|
+
const query = { limit: flags.limit || 20, page: flags.page || 1 };
|
|
303
|
+
const data = await api.get('/notifications', { query });
|
|
304
|
+
const notifications = data.notifications || [];
|
|
305
|
+
const pagination = data.pagination;
|
|
306
|
+
s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
|
|
307
|
+
|
|
308
|
+
if (flags.json) { ui.json(data); return; }
|
|
309
|
+
|
|
310
|
+
console.log('');
|
|
311
|
+
const rows = notifications.map(n => [
|
|
312
|
+
ui.c.dim(ui.truncate(n._id, 12)),
|
|
313
|
+
ui.statusBadge(n.read ? 'read' : 'unread'),
|
|
314
|
+
ui.truncate(n.title || n.message || n.type || '', 50),
|
|
315
|
+
n.ip || '—',
|
|
316
|
+
n.severity ? ui.statusBadge(n.severity) : '—',
|
|
317
|
+
ui.timeAgo(n.createdAt),
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
ui.table(['ID', 'Status', 'Title', 'IP', 'Severity', 'Time'], rows);
|
|
321
|
+
console.log('');
|
|
322
|
+
} catch (err) {
|
|
323
|
+
s.fail('Failed to fetch notifications');
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function notificationsRead(args, flags) {
|
|
329
|
+
requireAuth();
|
|
330
|
+
const id = args[0];
|
|
331
|
+
if (!id) {
|
|
332
|
+
ui.error('Notification ID required. Usage: securenow notifications read <id>');
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const s = ui.spinner('Marking as read');
|
|
337
|
+
try {
|
|
338
|
+
await api.put(`/notifications/${id}/read`);
|
|
339
|
+
s.stop('Notification marked as read');
|
|
340
|
+
} catch (err) {
|
|
341
|
+
s.fail('Failed to mark notification');
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function notificationsReadAll() {
|
|
347
|
+
requireAuth();
|
|
348
|
+
const s = ui.spinner('Marking all as read');
|
|
349
|
+
try {
|
|
350
|
+
await api.put('/notifications/read-all');
|
|
351
|
+
s.stop('All notifications marked as read');
|
|
352
|
+
} catch (err) {
|
|
353
|
+
s.fail('Failed to mark notifications');
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function notificationsUnread() {
|
|
359
|
+
requireAuth();
|
|
360
|
+
try {
|
|
361
|
+
const data = await api.get('/notifications/unread-count');
|
|
362
|
+
const count = data.count ?? 0;
|
|
363
|
+
console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── Status / Dashboard Overview ──
|
|
370
|
+
|
|
371
|
+
async function status(args, flags) {
|
|
372
|
+
requireAuth();
|
|
373
|
+
const s = ui.spinner('Fetching dashboard overview');
|
|
374
|
+
try {
|
|
375
|
+
const [appsData, unreadData] = await Promise.all([
|
|
376
|
+
api.get('/applications'),
|
|
377
|
+
api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
const apps = appsData.applications || [];
|
|
381
|
+
s.stop('Dashboard loaded');
|
|
382
|
+
|
|
383
|
+
console.log('');
|
|
384
|
+
ui.heading('SecureNow Dashboard');
|
|
385
|
+
console.log('');
|
|
386
|
+
|
|
387
|
+
ui.keyValue([
|
|
388
|
+
['Applications', String(apps.length)],
|
|
389
|
+
['Unread Alerts', String(unreadData.count ?? 0)],
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
if (apps.length > 0) {
|
|
393
|
+
ui.subheading('Applications');
|
|
394
|
+
console.log('');
|
|
395
|
+
const rows = apps.map(app => [
|
|
396
|
+
app.name,
|
|
397
|
+
ui.c.dim(app.key),
|
|
398
|
+
app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('—'),
|
|
399
|
+
]);
|
|
400
|
+
ui.table(['Name', 'Key', 'Hosts'], rows);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const appKey = resolveApp(flags);
|
|
404
|
+
if (appKey) {
|
|
405
|
+
try {
|
|
406
|
+
const protectionData = await api.get('/applications/protection-status');
|
|
407
|
+
const statuses = protectionData.statuses;
|
|
408
|
+
if (statuses && Object.keys(statuses).length > 0) {
|
|
409
|
+
ui.subheading('Protection Status');
|
|
410
|
+
console.log('');
|
|
411
|
+
const rows = Object.entries(statuses).map(([id, s]) => [
|
|
412
|
+
ui.c.dim(ui.truncate(id, 12)),
|
|
413
|
+
s.protected ? ui.c.green('● protected') : ui.c.red('○ unprotected'),
|
|
414
|
+
String(s.traceCount || 0),
|
|
415
|
+
s.lastTrace ? ui.timeAgo(s.lastTrace) : ui.c.dim('—'),
|
|
416
|
+
]);
|
|
417
|
+
ui.table(['App ID', 'Status', 'Traces (15m)', 'Last Trace'], rows);
|
|
418
|
+
}
|
|
419
|
+
} catch {}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log('');
|
|
423
|
+
} catch (err) {
|
|
424
|
+
s.fail('Failed to load dashboard');
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
tracesList,
|
|
431
|
+
tracesShow,
|
|
432
|
+
tracesAnalyze,
|
|
433
|
+
logsList,
|
|
434
|
+
logsTrace,
|
|
435
|
+
notificationsList,
|
|
436
|
+
notificationsRead,
|
|
437
|
+
notificationsReadAll,
|
|
438
|
+
notificationsUnread,
|
|
439
|
+
status,
|
|
440
|
+
};
|