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.js
CHANGED
|
@@ -1,499 +1,510 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* SecureNow CLI
|
|
6
|
-
*
|
|
7
|
-
* Usage: npx securenow init [options]
|
|
8
|
-
*/
|
|
4
|
+
const ui = require('./cli/ui');
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const os = require('os');
|
|
13
|
-
const http = require('http');
|
|
14
|
-
const https = require('https');
|
|
15
|
-
const crypto = require('crypto');
|
|
16
|
-
const readline = require('readline');
|
|
17
|
-
const { exec } = require('child_process');
|
|
6
|
+
// ── Argument Parser ──
|
|
18
7
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
23
|
-
|
|
24
|
-
const commands = {
|
|
25
|
-
init: initCommand,
|
|
26
|
-
login: loginCommand,
|
|
27
|
-
logout: logoutCommand,
|
|
28
|
-
status: statusCommand,
|
|
29
|
-
apps: appsCommand,
|
|
30
|
-
help: helpCommand,
|
|
31
|
-
version: versionCommand,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Templates
|
|
35
|
-
const templates = {
|
|
36
|
-
typescript: `import { registerSecureNow } from 'securenow/nextjs';
|
|
37
|
-
|
|
38
|
-
export function register() {
|
|
39
|
-
registerSecureNow();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Configuration via .env.local:
|
|
44
|
-
*
|
|
45
|
-
* Required:
|
|
46
|
-
* SECURENOW_APPID=my-nextjs-app
|
|
47
|
-
*
|
|
48
|
-
* Optional:
|
|
49
|
-
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
50
|
-
* OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-key"
|
|
51
|
-
* OTEL_LOG_LEVEL=info
|
|
52
|
-
*/
|
|
53
|
-
`,
|
|
54
|
-
javascript: `const { registerSecureNow } = require('securenow/nextjs');
|
|
55
|
-
|
|
56
|
-
export function register() {
|
|
57
|
-
registerSecureNow();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Configuration via .env.local:
|
|
62
|
-
*
|
|
63
|
-
* Required:
|
|
64
|
-
* SECURENOW_APPID=my-nextjs-app
|
|
65
|
-
*
|
|
66
|
-
* Optional:
|
|
67
|
-
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
68
|
-
* OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-key"
|
|
69
|
-
* OTEL_LOG_LEVEL=info
|
|
70
|
-
*/
|
|
71
|
-
`,
|
|
72
|
-
env: `# SecureNow Configuration
|
|
73
|
-
# Required: Your application identifier
|
|
74
|
-
SECURENOW_APPID=my-nextjs-app
|
|
75
|
-
|
|
76
|
-
# Optional: Your SigNoz/OpenTelemetry collector endpoint
|
|
77
|
-
# Default: https://freetrial.securenow.ai:4318
|
|
78
|
-
SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
79
|
-
|
|
80
|
-
# Optional: API key or authentication headers
|
|
81
|
-
# OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-api-key-here"
|
|
82
|
-
|
|
83
|
-
# Optional: Log level (debug|info|warn|error)
|
|
84
|
-
# OTEL_LOG_LEVEL=info
|
|
85
|
-
`,
|
|
86
|
-
};
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const positional = [];
|
|
10
|
+
const flags = {};
|
|
87
11
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Construct file paths
|
|
111
|
-
const fileName = useTypeScript ? 'instrumentation.ts' : 'instrumentation.js';
|
|
112
|
-
const filePath = useSrc
|
|
113
|
-
? path.join(cwd, 'src', fileName)
|
|
114
|
-
: path.join(cwd, fileName);
|
|
115
|
-
|
|
116
|
-
// Check if file already exists
|
|
117
|
-
if (fs.existsSync(filePath) && !flags.force) {
|
|
118
|
-
console.log(`❌ ${useSrc ? 'src/' : ''}${fileName} already exists`);
|
|
119
|
-
console.log(' Use --force to overwrite\n');
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Create instrumentation file
|
|
124
|
-
try {
|
|
125
|
-
const template = useTypeScript ? templates.typescript : templates.javascript;
|
|
126
|
-
|
|
127
|
-
// Ensure src directory exists if needed
|
|
128
|
-
if (useSrc) {
|
|
129
|
-
fs.mkdirSync(path.join(cwd, 'src'), { recursive: true });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
fs.writeFileSync(filePath, template, 'utf8');
|
|
133
|
-
console.log(`✅ Created ${useSrc ? 'src/' : ''}${fileName}`);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
console.error(`❌ Failed to create instrumentation file: ${error.message}\n`);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Create .env.local if it doesn't exist
|
|
140
|
-
const envPath = path.join(cwd, '.env.local');
|
|
141
|
-
if (!fs.existsSync(envPath) || flags.force) {
|
|
142
|
-
try {
|
|
143
|
-
fs.writeFileSync(envPath, templates.env, 'utf8');
|
|
144
|
-
console.log('✅ Created .env.local template');
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.warn(`⚠️ Could not create .env.local: ${error.message}`);
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const arg = argv[i];
|
|
14
|
+
if (arg.startsWith('--')) {
|
|
15
|
+
const eqIdx = arg.indexOf('=');
|
|
16
|
+
if (eqIdx !== -1) {
|
|
17
|
+
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
18
|
+
} else {
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
if (next && !next.startsWith('-')) {
|
|
21
|
+
flags[arg.slice(2)] = next;
|
|
22
|
+
i++;
|
|
23
|
+
} else {
|
|
24
|
+
flags[arg.slice(2)] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
28
|
+
const shortMap = { f: 'force', y: 'yes', v: 'verbose', j: 'json' };
|
|
29
|
+
const long = shortMap[arg[1]] || arg[1];
|
|
30
|
+
flags[long] = true;
|
|
31
|
+
} else {
|
|
32
|
+
positional.push(arg);
|
|
147
33
|
}
|
|
148
|
-
} else {
|
|
149
|
-
console.log('ℹ️ .env.local already exists (skipped)');
|
|
150
34
|
}
|
|
151
|
-
|
|
152
|
-
// Success message
|
|
153
|
-
console.log('\n┌─────────────────────────────────────────────────┐');
|
|
154
|
-
console.log('│ 🎉 Setup Complete! │');
|
|
155
|
-
console.log('│ │');
|
|
156
|
-
console.log('│ Next steps: │');
|
|
157
|
-
console.log('│ │');
|
|
158
|
-
console.log('│ 1. Edit .env.local and configure: │');
|
|
159
|
-
console.log('│ SECURENOW_APPID=your-app-name │');
|
|
160
|
-
console.log('│ SECURENOW_INSTANCE=http://signoz:4318 │');
|
|
161
|
-
console.log('│ │');
|
|
162
|
-
console.log('│ 2. Start your Next.js app: npm run dev │');
|
|
163
|
-
console.log('│ │');
|
|
164
|
-
console.log('│ 3. Check SigNoz dashboard for traces! │');
|
|
165
|
-
console.log('│ │');
|
|
166
|
-
console.log('│ 📚 Documentation: │');
|
|
167
|
-
console.log('│ node_modules/securenow/NEXTJS-GUIDE.md │');
|
|
168
|
-
console.log('└─────────────────────────────────────────────────┘\n');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ─── Auth / config ──────────────────────────────────────────────
|
|
172
35
|
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
176
|
-
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
177
|
-
} catch {
|
|
178
|
-
return {};
|
|
179
|
-
}
|
|
36
|
+
return { positional, flags };
|
|
180
37
|
}
|
|
181
38
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
39
|
+
// ── Command Registry ──
|
|
40
|
+
|
|
41
|
+
const COMMANDS = {
|
|
42
|
+
init: {
|
|
43
|
+
desc: 'Set up SecureNow in the current project (instrumentation, config, env)',
|
|
44
|
+
usage: 'securenow init [--key <API_KEY>] [--ts|--js] [--src|--root] [--force]',
|
|
45
|
+
flags: {
|
|
46
|
+
key: 'API key to write to .env',
|
|
47
|
+
'api-key': 'Alias for --key',
|
|
48
|
+
typescript: 'Force TypeScript',
|
|
49
|
+
javascript: 'Force JavaScript',
|
|
50
|
+
src: 'Create in src/',
|
|
51
|
+
root: 'Create in root',
|
|
52
|
+
force: 'Overwrite existing',
|
|
53
|
+
},
|
|
54
|
+
run: (a, f) => require('./cli/init').init(a, f),
|
|
55
|
+
},
|
|
56
|
+
login: {
|
|
57
|
+
desc: 'Authenticate with SecureNow',
|
|
58
|
+
usage: 'securenow login [--token <TOKEN>] [--local]',
|
|
59
|
+
flags: { token: 'Authenticate with a token directly', local: 'Save credentials to this project only (.securenow/)' },
|
|
60
|
+
run: (a, f) => require('./cli/auth').login(a, f),
|
|
61
|
+
},
|
|
62
|
+
logout: {
|
|
63
|
+
desc: 'Clear stored credentials',
|
|
64
|
+
usage: 'securenow logout [--local]',
|
|
65
|
+
flags: { local: 'Clear project-local credentials only' },
|
|
66
|
+
run: (a, f) => require('./cli/auth').logout(a, f),
|
|
67
|
+
},
|
|
68
|
+
whoami: {
|
|
69
|
+
desc: 'Show current session info',
|
|
70
|
+
usage: 'securenow whoami',
|
|
71
|
+
run: () => require('./cli/auth').whoami(),
|
|
72
|
+
},
|
|
73
|
+
apps: {
|
|
74
|
+
desc: 'Manage applications',
|
|
75
|
+
usage: 'securenow apps <subcommand> [options]',
|
|
76
|
+
sub: {
|
|
77
|
+
list: { desc: 'List all applications', run: (a, f) => require('./cli/apps').list(a, f) },
|
|
78
|
+
create: { desc: 'Create a new application (interactive instance picker)', usage: 'securenow apps create <name> [--hosts host1,host2] [--instance <id>]', run: (a, f) => require('./cli/apps').create(a, f) },
|
|
79
|
+
info: { desc: 'Show application details', usage: 'securenow apps info <id>', run: (a, f) => require('./cli/apps').info(a, f) },
|
|
80
|
+
delete: { desc: 'Delete an application', usage: 'securenow apps delete <id> [--force]', run: (a, f) => require('./cli/apps').remove(a, f) },
|
|
81
|
+
default: { desc: 'Set default application', usage: 'securenow apps default <key>', run: (a, f) => require('./cli/apps').setDefault(a, f) },
|
|
82
|
+
discover: { desc: 'Discover subdomains and add as apps', usage: 'securenow apps discover [appId] [--domain example.com]', run: (a, f) => require('./cli/apps').discover(a, f) },
|
|
83
|
+
scan: { desc: 'Scan all app domains for new subdomains', usage: 'securenow apps scan [--yes]', run: (a, f) => require('./cli/apps').scan(a, f) },
|
|
84
|
+
},
|
|
85
|
+
defaultSub: 'list',
|
|
86
|
+
},
|
|
87
|
+
traces: {
|
|
88
|
+
desc: 'View and analyze traces',
|
|
89
|
+
usage: 'securenow traces <subcommand> [options]',
|
|
90
|
+
sub: {
|
|
91
|
+
list: { desc: 'List recent traces', flags: { app: 'App key', limit: 'Max results', start: 'Start time', end: 'End time' }, run: (a, f) => require('./cli/monitor').tracesList(a, f) },
|
|
92
|
+
show: { desc: 'Show trace details', usage: 'securenow traces show <traceId>', run: (a, f) => require('./cli/monitor').tracesShow(a, f) },
|
|
93
|
+
analyze: { desc: 'AI-analyze a trace', usage: 'securenow traces analyze <traceId>', run: (a, f) => require('./cli/monitor').tracesAnalyze(a, f) },
|
|
94
|
+
},
|
|
95
|
+
defaultSub: 'list',
|
|
96
|
+
},
|
|
97
|
+
logs: {
|
|
98
|
+
desc: 'View application logs',
|
|
99
|
+
usage: 'securenow logs [options]',
|
|
100
|
+
sub: {
|
|
101
|
+
list: { desc: 'List recent logs', flags: { app: 'App key', limit: 'Max results (default 200)', since: 'Time window (e.g. 30m, 6h, 2d)', minutes: 'Time window in minutes (alias for --since Nm)', level: 'Filter by level (error, warn, info, debug)', start: 'Start time (ISO 8601)', end: 'End time (ISO 8601)' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
|
|
102
|
+
trace: { desc: 'Show logs for a trace', usage: 'securenow logs trace <traceId>', run: (a, f) => require('./cli/monitor').logsTrace(a, f) },
|
|
103
|
+
},
|
|
104
|
+
defaultSub: 'list',
|
|
105
|
+
},
|
|
106
|
+
notifications: {
|
|
107
|
+
desc: 'Manage notifications',
|
|
108
|
+
usage: 'securenow notifications <subcommand> [options]',
|
|
109
|
+
sub: {
|
|
110
|
+
list: { desc: 'List notifications', flags: { limit: 'Max results', page: 'Page number' }, run: (a, f) => require('./cli/monitor').notificationsList(a, f) },
|
|
111
|
+
read: { desc: 'Mark notification as read', usage: 'securenow notifications read <id>', run: (a, f) => require('./cli/monitor').notificationsRead(a, f) },
|
|
112
|
+
'read-all': { desc: 'Mark all as read', run: () => require('./cli/monitor').notificationsReadAll() },
|
|
113
|
+
unread: { desc: 'Show unread count', run: () => require('./cli/monitor').notificationsUnread() },
|
|
114
|
+
},
|
|
115
|
+
defaultSub: 'list',
|
|
116
|
+
},
|
|
117
|
+
alerts: {
|
|
118
|
+
desc: 'Manage alerting',
|
|
119
|
+
usage: 'securenow alerts <subcommand> [options]',
|
|
120
|
+
sub: {
|
|
121
|
+
rules: {
|
|
122
|
+
desc: 'List, show, or update alert rules',
|
|
123
|
+
flags: {
|
|
124
|
+
json: 'Output as JSON',
|
|
125
|
+
'applications-all': 'With update: scope rule to all apps',
|
|
126
|
+
'no-applications-all': 'With update: scope to explicit --apps list',
|
|
127
|
+
apps: 'Comma-separated app keys (with update)',
|
|
128
|
+
},
|
|
129
|
+
run: (a, f) => require('./cli/security').alertRulesRoute(a, f),
|
|
130
|
+
},
|
|
131
|
+
channels: { desc: 'List alert channels', run: (a, f) => require('./cli/security').alertChannelsList(a, f) },
|
|
132
|
+
history: { desc: 'View alert history', flags: { limit: 'Max results' }, run: (a, f) => require('./cli/security').alertHistoryList(a, f) },
|
|
133
|
+
},
|
|
134
|
+
defaultSub: 'rules',
|
|
135
|
+
},
|
|
136
|
+
fp: {
|
|
137
|
+
desc: 'Manage false-positive exclusion rules',
|
|
138
|
+
usage: 'securenow fp <subcommand> [options]',
|
|
139
|
+
flags: { json: 'Output as JSON', conditions: 'JSON array of conditions', 'match-mode': 'Match mode: all (AND) or any (OR)', 'rule-scope': 'Scope: this_rule | specific_rules | all_existing | any_rule', 'target-rules': 'Comma-separated rule IDs (when --rule-scope specific_rules)', 'path-safe': 'Add path_safe_values condition (standard|strict)', 'query-safe': 'Add query_safe_values condition (standard|strict)', 'query-keys': 'Allowed query param names (comma-separated)', 'ua-safe': 'Add ua_safe_values condition (standard|strict)', 'headers-safe': 'Add headers_safe_values condition (standard|strict)', 'headers-keys': 'Allowed header names (comma-separated)' },
|
|
140
|
+
sub: {
|
|
141
|
+
list: { desc: 'List all exclusion rules', run: (a, f) => require('./cli/fp').list(a, f) },
|
|
142
|
+
show: { desc: 'Show exclusion rule details', usage: 'securenow fp show <id>', run: (a, f) => require('./cli/fp').show(a, f) },
|
|
143
|
+
create: { desc: 'Create an exclusion rule', usage: 'securenow fp create [--conditions \'[...]\'] [--path /api/event] [--method POST] [--path-safe standard] [--ua-safe standard] [--headers-safe standard] [--query-keys page,limit] [--headers-keys host,content-type] [--reason "..."] [--rule-scope any_rule|specific_rules|all_existing] [--target-rules id1,id2]', run: (a, f) => require('./cli/fp').create(a, f) },
|
|
144
|
+
edit: { desc: 'Edit an exclusion rule', usage: 'securenow fp edit <id> [--active true/false] [--conditions \'[...]\']', run: (a, f) => require('./cli/fp').edit(a, f) },
|
|
145
|
+
delete: { desc: 'Delete an exclusion rule', usage: 'securenow fp delete <id> [--yes]', run: (a, f) => require('./cli/fp').remove(a, f) },
|
|
146
|
+
'test-body': { desc: 'Test a request body against conditions', usage: 'securenow fp test-body <body|@file> --conditions \'[...]\'', run: (a, f) => require('./cli/fp').testBody(a, f) },
|
|
147
|
+
'dry-run': { desc: 'Dry-run conditions against live traces (last 3 days)', usage: 'securenow fp dry-run --conditions \'[...]\'', run: (a, f) => require('./cli/fp').dryRun(a, f) },
|
|
148
|
+
'ai-fill': { desc: 'AI-generate exclusion conditions', usage: 'securenow fp ai-fill [--description "..."] [--context \'{"method":"POST",...}\']', run: (a, f) => require('./cli/fp').aiFill(a, f) },
|
|
149
|
+
mark: { desc: 'Mark an IP as false positive on a notification', usage: 'securenow fp mark <notification-id> <ip> [--conditions \'[...]\'] [--reason "..."] [--rule-scope this_rule|specific_rules|all_existing|any_rule] [--target-rules id1,id2]', run: (a, f) => require('./cli/fp').mark(a, f) },
|
|
150
|
+
},
|
|
151
|
+
defaultSub: 'list',
|
|
152
|
+
},
|
|
153
|
+
firewall: {
|
|
154
|
+
desc: 'Firewall status and IP testing',
|
|
155
|
+
usage: 'securenow firewall <subcommand> [options]',
|
|
156
|
+
sub: {
|
|
157
|
+
status: { desc: 'Show firewall status, layers, and blocklist info', run: (a, f) => require('./cli/firewall').status(a, f) },
|
|
158
|
+
'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip>', run: (a, f) => require('./cli/firewall').testIp(a, f) },
|
|
159
|
+
},
|
|
160
|
+
defaultSub: 'status',
|
|
161
|
+
},
|
|
162
|
+
blocklist: {
|
|
163
|
+
desc: 'Manage IP blocklist',
|
|
164
|
+
usage: 'securenow blocklist <subcommand> [options]',
|
|
165
|
+
sub: {
|
|
166
|
+
list: { desc: 'List blocked IPs', run: (a, f) => require('./cli/security').blocklistList(a, f) },
|
|
167
|
+
add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
|
|
168
|
+
remove: { desc: 'Unblock an IP', usage: 'securenow blocklist remove <id>', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
|
|
169
|
+
stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
|
|
170
|
+
},
|
|
171
|
+
defaultSub: 'list',
|
|
172
|
+
},
|
|
173
|
+
allowlist: {
|
|
174
|
+
desc: 'Manage IP allowlist (only allow listed IPs)',
|
|
175
|
+
usage: 'securenow allowlist <subcommand> [options]',
|
|
176
|
+
sub: {
|
|
177
|
+
list: { desc: 'List allowed IPs', run: (a, f) => require('./cli/security').allowlistList(a, f) },
|
|
178
|
+
add: { desc: 'Allow an IP', usage: 'securenow allowlist add <ip> [--label <label>] [--reason <reason>]', run: (a, f) => require('./cli/security').allowlistAdd(a, f) },
|
|
179
|
+
remove: { desc: 'Remove an allowed IP', usage: 'securenow allowlist remove <id>', run: (a, f) => require('./cli/security').allowlistRemove(a, f) },
|
|
180
|
+
stats: { desc: 'Allowlist statistics', run: (a, f) => require('./cli/security').allowlistStats(a, f) },
|
|
181
|
+
},
|
|
182
|
+
defaultSub: 'list',
|
|
183
|
+
},
|
|
184
|
+
trusted: {
|
|
185
|
+
desc: 'Manage trusted IPs',
|
|
186
|
+
usage: 'securenow trusted <subcommand> [options]',
|
|
187
|
+
sub: {
|
|
188
|
+
list: { desc: 'List trusted IPs', run: (a, f) => require('./cli/security').trustedList(a, f) },
|
|
189
|
+
add: { desc: 'Add trusted IP', usage: 'securenow trusted add <ip> [--label <label>]', run: (a, f) => require('./cli/security').trustedAdd(a, f) },
|
|
190
|
+
remove: { desc: 'Remove trusted IP', usage: 'securenow trusted remove <id>', run: (a, f) => require('./cli/security').trustedRemove(a, f) },
|
|
191
|
+
},
|
|
192
|
+
defaultSub: 'list',
|
|
193
|
+
},
|
|
194
|
+
ip: {
|
|
195
|
+
desc: 'IP intelligence lookup',
|
|
196
|
+
usage: 'securenow ip <ip-address>',
|
|
197
|
+
sub: {
|
|
198
|
+
lookup: { desc: 'Look up IP intelligence', run: (a, f) => require('./cli/security').ipLookup(a, f) },
|
|
199
|
+
traces: { desc: 'Show traces for an IP', usage: 'securenow ip traces <ip>', run: (a, f) => require('./cli/security').ipTraces(a, f) },
|
|
200
|
+
},
|
|
201
|
+
defaultAction: (a, f) => require('./cli/security').ipLookup(a, f),
|
|
202
|
+
},
|
|
203
|
+
forensics: {
|
|
204
|
+
desc: 'Run forensic queries (natural language → SQL)',
|
|
205
|
+
usage: 'securenow forensics <query> [--app <key>]',
|
|
206
|
+
sub: {
|
|
207
|
+
query: { desc: 'Run a forensic query', flags: { app: 'App key to scope query' }, run: (a, f) => require('./cli/security').forensicsQuery(a, f) },
|
|
208
|
+
chat: { desc: 'Interactive forensics chat (scoped to an app)', usage: 'securenow forensics chat --app <key>', flags: { app: 'App key to chat with' }, run: (a, f) => require('./cli/security').forensicsChat(a, f) },
|
|
209
|
+
library: { desc: 'View saved queries', run: (a, f) => require('./cli/security').forensicsLibrary(a, f) },
|
|
210
|
+
},
|
|
211
|
+
defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
|
|
212
|
+
},
|
|
213
|
+
instances: {
|
|
214
|
+
desc: 'Manage ClickHouse instances',
|
|
215
|
+
usage: 'securenow instances <subcommand> [options]',
|
|
216
|
+
sub: {
|
|
217
|
+
list: { desc: 'List instances', run: (a, f) => require('./cli/security').instancesList(a, f) },
|
|
218
|
+
test: { desc: 'Test instance connection', usage: 'securenow instances test <id>', run: (a, f) => require('./cli/security').instancesTest(a, f) },
|
|
219
|
+
},
|
|
220
|
+
defaultSub: 'list',
|
|
221
|
+
},
|
|
222
|
+
analytics: {
|
|
223
|
+
desc: 'View response analytics',
|
|
224
|
+
usage: 'securenow analytics [--app <key>]',
|
|
225
|
+
run: (a, f) => require('./cli/security').analytics(a, f),
|
|
226
|
+
},
|
|
227
|
+
status: {
|
|
228
|
+
desc: 'Dashboard overview',
|
|
229
|
+
usage: 'securenow status [--app <key>]',
|
|
230
|
+
run: (a, f) => require('./cli/monitor').status(a, f),
|
|
231
|
+
},
|
|
232
|
+
config: {
|
|
233
|
+
desc: 'Manage CLI configuration',
|
|
234
|
+
usage: 'securenow config <set|get> [key] [value]',
|
|
235
|
+
sub: {
|
|
236
|
+
set: {
|
|
237
|
+
desc: 'Set a config value',
|
|
238
|
+
usage: 'securenow config set <key> <value>',
|
|
239
|
+
run: (a) => {
|
|
240
|
+
const conf = require('./cli/config');
|
|
241
|
+
const [key, ...rest] = a;
|
|
242
|
+
const value = rest.join(' ');
|
|
243
|
+
if (!key || !value) { ui.error('Usage: securenow config set <key> <value>'); process.exit(1); }
|
|
244
|
+
conf.setConfigValue(key, value);
|
|
245
|
+
ui.success(`${key} = ${value}`);
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
get: {
|
|
249
|
+
desc: 'Get a config value',
|
|
250
|
+
usage: 'securenow config get [key]',
|
|
251
|
+
run: (a) => {
|
|
252
|
+
const conf = require('./cli/config');
|
|
253
|
+
const key = a[0];
|
|
254
|
+
if (key) {
|
|
255
|
+
const val = conf.getConfigValue(key);
|
|
256
|
+
console.log(val != null ? val : ui.c.dim('(not set)'));
|
|
257
|
+
} else {
|
|
258
|
+
const all = conf.loadConfig();
|
|
259
|
+
console.log('');
|
|
260
|
+
ui.keyValue(Object.entries(all).map(([k, v]) => [k, v != null ? String(v) : ui.c.dim('(not set)')]));
|
|
261
|
+
console.log('');
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
path: {
|
|
266
|
+
desc: 'Show config file path',
|
|
267
|
+
run: () => {
|
|
268
|
+
const conf = require('./cli/config');
|
|
269
|
+
console.log(`Config: ${conf.CONFIG_FILE}`);
|
|
270
|
+
console.log(`Credentials: ${conf.CREDENTIALS_FILE}`);
|
|
271
|
+
if (conf.hasLocalCredentials()) {
|
|
272
|
+
console.log(`Local creds: ${conf.LOCAL_CREDENTIALS_FILE} ${require('./cli/ui').c.green('(active)')}`);
|
|
273
|
+
}
|
|
274
|
+
console.log(`Auth source: ${conf.getAuthSource()}`);
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
run: {
|
|
280
|
+
desc: 'Run a Node.js app with automatic OTel instrumentation',
|
|
281
|
+
usage: 'securenow run [node-flags] [--firewall-only] <script> [app-args]',
|
|
282
|
+
flags: {
|
|
283
|
+
watch: 'Enable Node.js watch mode',
|
|
284
|
+
inspect: 'Enable Node.js inspector',
|
|
285
|
+
'firewall-only': 'Preload firewall without OTel tracing overhead',
|
|
286
|
+
},
|
|
287
|
+
rawArgv: true,
|
|
288
|
+
run: (rawArgs) => require('./cli/run').run(rawArgs),
|
|
289
|
+
},
|
|
290
|
+
redact: {
|
|
291
|
+
desc: 'Redact sensitive fields from a JSON payload',
|
|
292
|
+
usage: "securenow redact '<json>' [--fields f1,f2] [--json]",
|
|
293
|
+
flags: { fields: 'Comma-separated extra field names to redact (added to defaults)' },
|
|
294
|
+
run: (a, f) => require('./cli/utils').redact(a, f),
|
|
295
|
+
},
|
|
296
|
+
cidr: {
|
|
297
|
+
desc: 'CIDR utilities (match and parse)',
|
|
298
|
+
usage: 'securenow cidr <match|parse> ...',
|
|
299
|
+
sub: {
|
|
300
|
+
match: {
|
|
301
|
+
desc: 'Check if an IP matches a CIDR list',
|
|
302
|
+
usage: 'securenow cidr match <ip> <cidr1,cidr2,...>',
|
|
303
|
+
run: (a, f) => require('./cli/utils').cidrMatch(a, f),
|
|
304
|
+
},
|
|
305
|
+
parse: {
|
|
306
|
+
desc: 'Parse a CIDR and show network/broadcast/size',
|
|
307
|
+
usage: 'securenow cidr parse <cidr>',
|
|
308
|
+
run: (a, f) => require('./cli/utils').cidrParse(a, f),
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
log: {
|
|
313
|
+
desc: 'Emit logs via OTLP (for scripts, cron, debugging)',
|
|
314
|
+
usage: 'securenow log send <message> [--level info|warn|error] [--attrs k=v,k=v]',
|
|
315
|
+
sub: {
|
|
316
|
+
send: {
|
|
317
|
+
desc: 'Send a single log record to the OTLP collector',
|
|
318
|
+
flags: {
|
|
319
|
+
level: 'Severity (trace|debug|info|warn|error|fatal)',
|
|
320
|
+
attrs: 'Comma-separated key=value attributes',
|
|
321
|
+
},
|
|
322
|
+
run: (a, f) => require('./cli/diagnostics').logSend(a, f),
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
defaultSub: 'send',
|
|
326
|
+
},
|
|
327
|
+
'test-span': {
|
|
328
|
+
desc: 'Emit a test span to verify collector connectivity',
|
|
329
|
+
usage: 'securenow test-span [<span-name>]',
|
|
330
|
+
run: (a, f) => require('./cli/diagnostics').testSpan(a, f),
|
|
331
|
+
},
|
|
332
|
+
doctor: {
|
|
333
|
+
desc: 'Diagnose SecureNow configuration and collector connectivity',
|
|
334
|
+
usage: 'securenow doctor [--json]',
|
|
335
|
+
run: (a, f) => require('./cli/diagnostics').doctor(a, f),
|
|
336
|
+
},
|
|
337
|
+
env: {
|
|
338
|
+
desc: 'Show resolved SecureNow configuration (service name, endpoints, env vars)',
|
|
339
|
+
usage: 'securenow env [--json]',
|
|
340
|
+
run: (a, f) => require('./cli/diagnostics').env(a, f),
|
|
341
|
+
},
|
|
342
|
+
version: {
|
|
343
|
+
desc: 'Show CLI version',
|
|
344
|
+
run: () => {
|
|
345
|
+
try {
|
|
346
|
+
const pkg = require('./package.json');
|
|
347
|
+
console.log(`securenow v${pkg.version}`);
|
|
348
|
+
} catch {
|
|
349
|
+
console.log('securenow (version unknown)');
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
};
|
|
192
354
|
|
|
193
|
-
|
|
194
|
-
const cmd =
|
|
195
|
-
process.platform === 'darwin' ? `open "${url}"` :
|
|
196
|
-
process.platform === 'win32' ? `start "" "${url}"` :
|
|
197
|
-
`xdg-open "${url}"`;
|
|
198
|
-
exec(cmd, () => {});
|
|
199
|
-
}
|
|
355
|
+
// ── Help System ──
|
|
200
356
|
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
357
|
+
function showBanner() {
|
|
358
|
+
console.log('');
|
|
359
|
+
console.log(` ${ui.c.bold(ui.c.cyan('SecureNow CLI'))} ${ui.c.dim('— Security observability from the terminal')}`);
|
|
360
|
+
console.log('');
|
|
361
|
+
}
|
|
204
362
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
363
|
+
function showHelp(commandName) {
|
|
364
|
+
if (commandName && COMMANDS[commandName]) {
|
|
365
|
+
const cmd = COMMANDS[commandName];
|
|
366
|
+
showBanner();
|
|
367
|
+
console.log(` ${ui.c.bold(commandName)} — ${cmd.desc}`);
|
|
368
|
+
console.log('');
|
|
369
|
+
if (cmd.usage) {
|
|
370
|
+
console.log(` ${ui.c.bold('USAGE')}`);
|
|
371
|
+
console.log(` ${cmd.usage}`);
|
|
372
|
+
console.log('');
|
|
210
373
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
374
|
+
if (cmd.sub) {
|
|
375
|
+
console.log(` ${ui.c.bold('SUBCOMMANDS')}`);
|
|
376
|
+
const entries = Object.entries(cmd.sub);
|
|
377
|
+
const maxLen = entries.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
378
|
+
for (const [name, sub] of entries) {
|
|
379
|
+
console.log(` ${ui.c.cyan(name.padEnd(maxLen + 2))} ${sub.desc}`);
|
|
380
|
+
}
|
|
381
|
+
console.log('');
|
|
217
382
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
console.log('Opening browser to authenticate...');
|
|
231
|
-
console.log('If it doesn\'t open, paste this URL:\n ' + authUrl + '\n');
|
|
232
|
-
openBrowser(authUrl);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
setTimeout(() => {
|
|
236
|
-
console.error('\n⏱ Login timed out after 5 minutes.');
|
|
237
|
-
try { server.close(); } catch {}
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}, 5 * 60 * 1000).unref();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function logoutCommand() {
|
|
243
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
244
|
-
try { fs.unlinkSync(CONFIG_FILE); } catch {}
|
|
383
|
+
if (cmd.flags) {
|
|
384
|
+
console.log(` ${ui.c.bold('FLAGS')}`);
|
|
385
|
+
for (const [flag, desc] of Object.entries(cmd.flags)) {
|
|
386
|
+
console.log(` --${ui.c.cyan(flag.padEnd(16))} ${desc}`);
|
|
387
|
+
}
|
|
388
|
+
console.log('');
|
|
389
|
+
}
|
|
390
|
+
console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
|
|
391
|
+
console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
|
|
392
|
+
console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help`);
|
|
393
|
+
console.log('');
|
|
394
|
+
return;
|
|
245
395
|
}
|
|
246
|
-
console.log('✅ Logged out');
|
|
247
|
-
}
|
|
248
396
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
397
|
+
showBanner();
|
|
398
|
+
|
|
399
|
+
const groups = {
|
|
400
|
+
'Run': ['run'],
|
|
401
|
+
'Authentication': ['login', 'logout', 'whoami'],
|
|
402
|
+
'Applications': ['apps', 'init', 'status'],
|
|
403
|
+
'Observe': ['traces', 'logs', 'analytics'],
|
|
404
|
+
'Detect & Respond': ['notifications', 'alerts', 'fp'],
|
|
405
|
+
'Investigate': ['ip', 'forensics'],
|
|
406
|
+
'Firewall': ['firewall'],
|
|
407
|
+
'Remediation': ['blocklist', 'allowlist', 'trusted'],
|
|
408
|
+
'Telemetry': ['log', 'test-span'],
|
|
409
|
+
'Utilities': ['redact', 'cidr', 'doctor', 'env'],
|
|
410
|
+
'Settings': ['instances', 'config', 'version'],
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
for (const [group, cmds] of Object.entries(groups)) {
|
|
414
|
+
console.log(` ${ui.c.bold(group)}`);
|
|
415
|
+
for (const name of cmds) {
|
|
416
|
+
const cmd = COMMANDS[name];
|
|
417
|
+
if (cmd) {
|
|
418
|
+
console.log(` ${ui.c.cyan(name.padEnd(18))} ${cmd.desc}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
console.log('');
|
|
254
422
|
}
|
|
255
|
-
console.log('✅ Logged in');
|
|
256
|
-
console.log(' Config: ' + CONFIG_FILE);
|
|
257
|
-
console.log(' API: ' + API_URL);
|
|
258
|
-
apiRequest('GET', '/api/applications', null, cfg.token)
|
|
259
|
-
.then((data) => {
|
|
260
|
-
const count = (data && (data.applications || data)) ? (data.applications || data).length : 0;
|
|
261
|
-
console.log(' Apps: ' + count);
|
|
262
|
-
})
|
|
263
|
-
.catch((err) => {
|
|
264
|
-
console.log(' Apps: (could not reach API: ' + err.message + ')');
|
|
265
|
-
process.exit(1);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ─── HTTP helper ────────────────────────────────────────────────
|
|
270
423
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
method,
|
|
278
|
-
hostname: u.hostname,
|
|
279
|
-
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
280
|
-
path: u.pathname + u.search,
|
|
281
|
-
headers: {
|
|
282
|
-
'Content-Type': 'application/json',
|
|
283
|
-
'Accept': 'application/json',
|
|
284
|
-
'User-Agent': 'securenow-cli',
|
|
285
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
286
|
-
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
287
|
-
},
|
|
288
|
-
}, (res) => {
|
|
289
|
-
let chunks = '';
|
|
290
|
-
res.on('data', (c) => { chunks += c; });
|
|
291
|
-
res.on('end', () => {
|
|
292
|
-
if (res.statusCode === 401) return reject(new Error('Unauthorized — run: npx securenow login'));
|
|
293
|
-
if (res.statusCode >= 400) {
|
|
294
|
-
let msg = `HTTP ${res.statusCode}`;
|
|
295
|
-
try { const j = JSON.parse(chunks); if (j.error) msg = j.error; } catch {}
|
|
296
|
-
return reject(new Error(msg));
|
|
297
|
-
}
|
|
298
|
-
try { resolve(chunks ? JSON.parse(chunks) : {}); }
|
|
299
|
-
catch { resolve({}); }
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
req.on('error', reject);
|
|
303
|
-
if (data) req.write(data);
|
|
304
|
-
req.end();
|
|
305
|
-
});
|
|
424
|
+
console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
|
|
425
|
+
console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
|
|
426
|
+
console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help for a command`);
|
|
427
|
+
console.log('');
|
|
428
|
+
console.log(` ${ui.c.dim('Run')} securenow help <command> ${ui.c.dim('for detailed usage')}`);
|
|
429
|
+
console.log('');
|
|
306
430
|
}
|
|
307
431
|
|
|
308
|
-
//
|
|
432
|
+
// ── Main Router ──
|
|
309
433
|
|
|
310
|
-
function
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
if (sub === 'list' || !sub) return appsList(rest);
|
|
314
|
-
if (sub === 'create' || sub === 'add') return appsCreate(rest);
|
|
315
|
-
console.error('Unknown apps subcommand. Try: list, create');
|
|
316
|
-
process.exit(1);
|
|
317
|
-
}
|
|
434
|
+
async function main() {
|
|
435
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
436
|
+
const cmdName = positional[0] || 'help';
|
|
318
437
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
console.error('❌ Not logged in. Run: npx securenow login');
|
|
323
|
-
process.exit(1);
|
|
438
|
+
if (cmdName === 'help' || flags.help) {
|
|
439
|
+
showHelp(flags.help === true ? positional[0] : flags.help || positional[1] || (cmdName !== 'help' ? cmdName : null));
|
|
440
|
+
return;
|
|
324
441
|
}
|
|
325
|
-
return cfg.token;
|
|
326
|
-
}
|
|
327
442
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const apps = data.applications || data || [];
|
|
334
|
-
if (json) { console.log(JSON.stringify(apps, null, 2)); return; }
|
|
335
|
-
if (!apps.length) { console.log('(no applications yet — npx securenow apps create)'); return; }
|
|
336
|
-
console.log('\nApplications:\n');
|
|
337
|
-
for (const a of apps) {
|
|
338
|
-
console.log(` • ${a.name}${a.key ? ' ' + a.key : ''}`);
|
|
339
|
-
if (a.hosts && a.hosts.length) console.log(` hosts: ${a.hosts.join(', ')}`);
|
|
443
|
+
const cmd = COMMANDS[cmdName];
|
|
444
|
+
if (!cmd) {
|
|
445
|
+
// Auto-detect: if the first arg looks like a file path, treat it as `securenow run <file>`
|
|
446
|
+
if (/\.(m?[jt]sx?|cjs)$/.test(cmdName) || cmdName.includes('/') || cmdName.includes('\\')) {
|
|
447
|
+
return COMMANDS.run.run(process.argv.slice(2));
|
|
340
448
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
console.error('❌ ' + err.message);
|
|
449
|
+
ui.error(`Unknown command: ${cmdName}`);
|
|
450
|
+
ui.info('Run `securenow help` for a list of commands.');
|
|
344
451
|
process.exit(1);
|
|
345
452
|
}
|
|
346
|
-
}
|
|
347
453
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
async function appsCreate(args) {
|
|
356
|
-
const token = requireAuth();
|
|
357
|
-
const flags = {};
|
|
358
|
-
for (let i = 0; i < args.length; i++) {
|
|
359
|
-
if (args[i] === '--name') flags.name = args[++i];
|
|
360
|
-
else if (args[i] === '--hosts') flags.hosts = args[++i];
|
|
361
|
-
}
|
|
362
|
-
const name = flags.name || await prompt('Application name: ');
|
|
363
|
-
if (!name) { console.error('❌ Name is required'); process.exit(1); }
|
|
364
|
-
const hostsStr = flags.hosts != null ? flags.hosts : await prompt('Hosts (comma-separated, optional): ');
|
|
365
|
-
const hosts = hostsStr ? hostsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
366
|
-
try {
|
|
367
|
-
const data = await apiRequest('POST', '/api/applications', { name, hosts }, token);
|
|
368
|
-
const app = data.application || data;
|
|
369
|
-
console.log('\n✅ Created application: ' + (app.name || name));
|
|
370
|
-
if (app.key) console.log(' Key: ' + app.key);
|
|
371
|
-
console.log('\nNext: add SECURENOW_APPID=' + (app.name || name) + ' to your .env.local\n');
|
|
372
|
-
} catch (err) {
|
|
373
|
-
console.error('❌ ' + err.message);
|
|
374
|
-
process.exit(1);
|
|
454
|
+
if (cmd.rawArgv) {
|
|
455
|
+
// Pass raw argv (everything after the command name) so the command can
|
|
456
|
+
// forward flags like --watch, --inspect verbatim to child processes.
|
|
457
|
+
const cmdIdx = process.argv.indexOf(cmdName);
|
|
458
|
+
const rawArgs = cmdIdx !== -1 ? process.argv.slice(cmdIdx + 1) : positional.slice(1);
|
|
459
|
+
await cmd.run(rawArgs);
|
|
460
|
+
return;
|
|
375
461
|
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function helpCommand() {
|
|
379
|
-
console.log(`
|
|
380
|
-
SecureNow CLI - OpenTelemetry instrumentation for Next.js
|
|
381
462
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
login Authenticate with SecureNow (opens browser)
|
|
387
|
-
logout Remove saved credentials
|
|
388
|
-
status Show current auth + API connectivity
|
|
389
|
-
init Scaffold instrumentation in your Next.js project
|
|
390
|
-
apps list List your applications
|
|
391
|
-
apps create Create a new application (interactive)
|
|
392
|
-
help Show this help message
|
|
393
|
-
version Show package version
|
|
394
|
-
|
|
395
|
-
OPTIONS:
|
|
396
|
-
--typescript, --ts Force TypeScript (creates instrumentation.ts)
|
|
397
|
-
--javascript, --js Force JavaScript (creates instrumentation.js)
|
|
398
|
-
--src Create file in src/ folder
|
|
399
|
-
--root Create file in project root
|
|
400
|
-
--force, -f Overwrite existing files
|
|
401
|
-
|
|
402
|
-
EXAMPLES:
|
|
403
|
-
# First-time setup
|
|
404
|
-
npx securenow login
|
|
405
|
-
npx securenow apps create --name my-app
|
|
406
|
-
npx securenow init
|
|
407
|
-
|
|
408
|
-
# Auto-detect and setup
|
|
409
|
-
npx securenow init
|
|
463
|
+
if (cmd.run && !cmd.sub) {
|
|
464
|
+
await cmd.run(positional.slice(1), flags);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
410
467
|
|
|
411
|
-
|
|
412
|
-
|
|
468
|
+
if (cmd.sub) {
|
|
469
|
+
let subName = positional[1];
|
|
470
|
+
let subArgs = positional.slice(2);
|
|
413
471
|
|
|
414
|
-
|
|
415
|
-
|
|
472
|
+
if (subName && cmd.sub[subName]) {
|
|
473
|
+
if (flags.help) {
|
|
474
|
+
showHelp(cmdName);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
await cmd.sub[subName].run(subArgs, flags);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
416
480
|
|
|
417
|
-
|
|
418
|
-
|
|
481
|
+
if (cmd.defaultAction) {
|
|
482
|
+
const allArgs = subName ? [subName, ...subArgs] : [];
|
|
483
|
+
await cmd.defaultAction(allArgs, flags);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
419
486
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
487
|
+
if (cmd.defaultSub) {
|
|
488
|
+
const allArgs = subName ? [subName, ...subArgs] : [];
|
|
489
|
+
await cmd.sub[cmd.defaultSub].run(allArgs, flags);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
426
492
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const packageJson = require('./package.json');
|
|
430
|
-
console.log(`securenow v${packageJson.version}`);
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.log('securenow (version unknown)');
|
|
493
|
+
showHelp(cmdName);
|
|
494
|
+
return;
|
|
433
495
|
}
|
|
434
|
-
}
|
|
435
496
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const flags = {};
|
|
439
|
-
|
|
440
|
-
for (let i = 0; i < args.length; i++) {
|
|
441
|
-
const arg = args[i];
|
|
442
|
-
|
|
443
|
-
if (arg === '--typescript' || arg === '--ts') {
|
|
444
|
-
flags.typescript = true;
|
|
445
|
-
} else if (arg === '--javascript' || arg === '--js') {
|
|
446
|
-
flags.javascript = true;
|
|
447
|
-
} else if (arg === '--src') {
|
|
448
|
-
flags.src = true;
|
|
449
|
-
} else if (arg === '--root') {
|
|
450
|
-
flags.root = true;
|
|
451
|
-
} else if (arg === '--force' || arg === '-f') {
|
|
452
|
-
flags.force = true;
|
|
453
|
-
}
|
|
497
|
+
if (cmd.run) {
|
|
498
|
+
await cmd.run(positional.slice(1), flags);
|
|
454
499
|
}
|
|
455
|
-
|
|
456
|
-
return flags;
|
|
457
500
|
}
|
|
458
501
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (!fs.existsSync(packageJsonPath)) return false;
|
|
463
|
-
|
|
464
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
465
|
-
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
466
|
-
|
|
467
|
-
return !!deps.next;
|
|
468
|
-
} catch (error) {
|
|
469
|
-
return false;
|
|
502
|
+
main().catch((err) => {
|
|
503
|
+
if (err.name !== 'CLIError') {
|
|
504
|
+
ui.error(err.message || 'An unexpected error occurred');
|
|
470
505
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// Main
|
|
474
|
-
function main() {
|
|
475
|
-
const args = process.argv.slice(2);
|
|
476
|
-
const command = args[0] || 'help';
|
|
477
|
-
const commandFn = commands[command];
|
|
478
|
-
|
|
479
|
-
if (!commandFn) {
|
|
480
|
-
console.error(`Unknown command: ${command}`);
|
|
481
|
-
console.log('Run "npx securenow help" for usage\n');
|
|
482
|
-
process.exit(1);
|
|
506
|
+
if (process.env.SECURENOW_DEBUG) {
|
|
507
|
+
console.error(err.stack || err);
|
|
483
508
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (require.main === module) {
|
|
489
|
-
main();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
module.exports = { main };
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
509
|
+
process.exit(1);
|
|
510
|
+
});
|