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.
Files changed (85) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +40 -243
  3. package/cli.js +455 -425
  4. package/console-instrumentation.js +136 -147
  5. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +455 -1339
  6. package/docs/ARCHITECTURE.md +3 -3
  7. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  8. package/docs/AUTO-SETUP.md +4 -4
  9. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  10. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  11. package/docs/CHANGELOG-NEXTJS.md +1 -1
  12. package/docs/CUSTOMER-GUIDE.md +16 -16
  13. package/docs/EASIEST-SETUP.md +5 -5
  14. package/docs/ENVIRONMENT-VARIABLES.md +652 -880
  15. package/docs/EXPRESS-BODY-CAPTURE.md +12 -13
  16. package/docs/EXPRESS-SETUP-GUIDE.md +720 -719
  17. package/docs/INDEX.md +4 -22
  18. package/docs/LOGGING-GUIDE.md +708 -701
  19. package/docs/LOGGING-QUICKSTART.md +239 -234
  20. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  21. package/docs/NEXTJS-GUIDE.md +14 -14
  22. package/docs/NEXTJS-QUICKSTART.md +1 -1
  23. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  24. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  25. package/docs/REDACTION-EXAMPLES.md +1 -1
  26. package/docs/REQUEST-BODY-CAPTURE.md +10 -19
  27. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  28. package/examples/README.md +6 -6
  29. package/examples/instrumentation-with-auto-capture.ts +1 -1
  30. package/examples/nextjs-env-example.txt +2 -2
  31. package/examples/nextjs-instrumentation.js +1 -1
  32. package/examples/nextjs-instrumentation.ts +1 -1
  33. package/examples/nextjs-with-logging-example.md +6 -6
  34. package/examples/nextjs-with-options.ts +1 -1
  35. package/examples/test-nextjs-setup.js +1 -1
  36. package/nextjs-auto-capture.js +207 -199
  37. package/nextjs-middleware.js +181 -186
  38. package/nextjs-webpack-config.js +53 -88
  39. package/nextjs-wrapper.js +158 -158
  40. package/nextjs.d.ts +1 -1
  41. package/nextjs.js +135 -190
  42. package/package.json +45 -67
  43. package/postinstall.js +6 -6
  44. package/register.d.ts +1 -1
  45. package/register.js +4 -39
  46. package/tracing.d.ts +1 -2
  47. package/tracing.js +22 -287
  48. package/web-vite.mjs +156 -239
  49. package/CONSUMING-APPS-GUIDE.md +0 -455
  50. package/NPM_README.md +0 -1958
  51. package/SKILL-API.md +0 -600
  52. package/SKILL-CLI.md +0 -419
  53. package/cidr.js +0 -83
  54. package/cli/apps.js +0 -585
  55. package/cli/auth.js +0 -280
  56. package/cli/client.js +0 -115
  57. package/cli/config.js +0 -173
  58. package/cli/firewall.js +0 -100
  59. package/cli/fp.js +0 -638
  60. package/cli/init.js +0 -201
  61. package/cli/monitor.js +0 -545
  62. package/cli/run.js +0 -133
  63. package/cli/security.js +0 -1064
  64. package/cli/ui.js +0 -386
  65. package/docs/API-KEYS-GUIDE.md +0 -233
  66. package/docs/AUTO-SETUP-SUMMARY.md +0 -331
  67. package/docs/BODY-CAPTURE-FIX.md +0 -261
  68. package/docs/COMPLETION-REPORT.md +0 -408
  69. package/docs/FINAL-SOLUTION.md +0 -335
  70. package/docs/FIREWALL-GUIDE.md +0 -426
  71. package/docs/IMPLEMENTATION-SUMMARY.md +0 -410
  72. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +0 -323
  73. package/docs/NEXTJS-SETUP-COMPLETE.md +0 -795
  74. package/docs/NUXT-GUIDE.md +0 -166
  75. package/docs/SOLUTION-SUMMARY.md +0 -312
  76. package/firewall-cloud.js +0 -212
  77. package/firewall-iptables.js +0 -139
  78. package/firewall-only.js +0 -38
  79. package/firewall-tcp.js +0 -74
  80. package/firewall.js +0 -720
  81. package/free-trial-banner.js +0 -174
  82. package/nuxt-server-plugin.mjs +0 -423
  83. package/nuxt.d.ts +0 -60
  84. package/nuxt.mjs +0 -75
  85. package/resolve-ip.js +0 -77
package/cli.js CHANGED
@@ -1,469 +1,499 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const ui = require('./cli/ui');
4
+ /**
5
+ * SecureNow CLI
6
+ *
7
+ * Usage: npx securenow init [options]
8
+ */
5
9
 
6
- // ── Argument Parser ──
10
+ const fs = require('fs');
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');
7
18
 
8
- function parseArgs(argv) {
9
- const positional = [];
10
- const flags = {};
19
+ const APP_URL = (process.env.SECURENOW_APP_URL || 'https://app.securenow.ai').replace(/\/$/, '');
20
+ const API_URL = (process.env.SECURENOW_API_URL || 'https://api.securenow.ai').replace(/\/$/, '');
21
+ const CONFIG_DIR = path.join(os.homedir(), '.securenow');
22
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
23
 
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);
33
- }
34
- }
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';
35
37
 
36
- return { positional, flags };
38
+ export function register() {
39
+ registerSecureNow();
37
40
  }
38
41
 
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>]',
45
- flags: { key: 'API key to write to .env', 'api-key': 'Alias for --key' },
46
- run: (a, f) => require('./cli/init').init(a, f),
47
- },
48
- login: {
49
- desc: 'Authenticate with SecureNow',
50
- usage: 'securenow login [--token <TOKEN>] [--local]',
51
- flags: { token: 'Authenticate with a token directly', local: 'Save credentials to this project only (.securenow/)' },
52
- run: (a, f) => require('./cli/auth').login(a, f),
53
- },
54
- logout: {
55
- desc: 'Clear stored credentials',
56
- usage: 'securenow logout [--local]',
57
- flags: { local: 'Clear project-local credentials only' },
58
- run: (a, f) => require('./cli/auth').logout(a, f),
59
- },
60
- whoami: {
61
- desc: 'Show current session info',
62
- usage: 'securenow whoami',
63
- run: () => require('./cli/auth').whoami(),
64
- },
65
- apps: {
66
- desc: 'Manage applications',
67
- usage: 'securenow apps <subcommand> [options]',
68
- sub: {
69
- list: { desc: 'List all applications', run: (a, f) => require('./cli/apps').list(a, f) },
70
- 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) },
71
- info: { desc: 'Show application details', usage: 'securenow apps info <id>', run: (a, f) => require('./cli/apps').info(a, f) },
72
- delete: { desc: 'Delete an application', usage: 'securenow apps delete <id> [--force]', run: (a, f) => require('./cli/apps').remove(a, f) },
73
- default: { desc: 'Set default application', usage: 'securenow apps default <key>', run: (a, f) => require('./cli/apps').setDefault(a, f) },
74
- 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) },
75
- scan: { desc: 'Scan all app domains for new subdomains', usage: 'securenow apps scan [--yes]', run: (a, f) => require('./cli/apps').scan(a, f) },
76
- },
77
- defaultSub: 'list',
78
- },
79
- traces: {
80
- desc: 'View and analyze traces',
81
- usage: 'securenow traces <subcommand> [options]',
82
- sub: {
83
- 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) },
84
- show: { desc: 'Show trace details', usage: 'securenow traces show <traceId>', run: (a, f) => require('./cli/monitor').tracesShow(a, f) },
85
- analyze: { desc: 'AI-analyze a trace', usage: 'securenow traces analyze <traceId>', run: (a, f) => require('./cli/monitor').tracesAnalyze(a, f) },
86
- },
87
- defaultSub: 'list',
88
- },
89
- logs: {
90
- desc: 'View application logs',
91
- usage: 'securenow logs [options]',
92
- sub: {
93
- 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) },
94
- trace: { desc: 'Show logs for a trace', usage: 'securenow logs trace <traceId>', run: (a, f) => require('./cli/monitor').logsTrace(a, f) },
95
- },
96
- defaultSub: 'list',
97
- },
98
- issues: {
99
- desc: 'Manage security issues',
100
- usage: 'securenow issues <subcommand> [options]',
101
- sub: {
102
- list: { desc: 'List issues', flags: { app: 'App key', status: 'Filter by status' }, run: (a, f) => require('./cli/monitor').issuesList(a, f) },
103
- show: { desc: 'Show issue details', usage: 'securenow issues show <id>', run: (a, f) => require('./cli/monitor').issuesShow(a, f) },
104
- resolve: { desc: 'Resolve an issue', usage: 'securenow issues resolve <id>', run: (a, f) => require('./cli/monitor').issuesResolve(a, f) },
105
- },
106
- defaultSub: 'list',
107
- },
108
- notifications: {
109
- desc: 'Manage notifications',
110
- usage: 'securenow notifications <subcommand> [options]',
111
- sub: {
112
- list: { desc: 'List notifications', flags: { limit: 'Max results', page: 'Page number' }, run: (a, f) => require('./cli/monitor').notificationsList(a, f) },
113
- read: { desc: 'Mark notification as read', usage: 'securenow notifications read <id>', run: (a, f) => require('./cli/monitor').notificationsRead(a, f) },
114
- 'read-all': { desc: 'Mark all as read', run: () => require('./cli/monitor').notificationsReadAll() },
115
- unread: { desc: 'Show unread count', run: () => require('./cli/monitor').notificationsUnread() },
116
- },
117
- defaultSub: 'list',
118
- },
119
- alerts: {
120
- desc: 'Manage alerting',
121
- usage: 'securenow alerts <subcommand> [options]',
122
- sub: {
123
- rules: {
124
- desc: 'List, show, or update alert rules',
125
- flags: {
126
- json: 'Output as JSON',
127
- 'applications-all': 'With update: scope rule to all apps',
128
- 'no-applications-all': 'With update: scope to explicit --apps list',
129
- apps: 'Comma-separated app keys (with update)',
130
- },
131
- run: (a, f) => require('./cli/security').alertRulesRoute(a, f),
132
- },
133
- channels: { desc: 'List alert channels', run: (a, f) => require('./cli/security').alertChannelsList(a, f) },
134
- history: { desc: 'View alert history', flags: { limit: 'Max results' }, run: (a, f) => require('./cli/security').alertHistoryList(a, f) },
135
- },
136
- defaultSub: 'rules',
137
- },
138
- fp: {
139
- desc: 'Manage false-positive exclusion rules',
140
- usage: 'securenow fp <subcommand> [options]',
141
- 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)' },
142
- sub: {
143
- list: { desc: 'List all exclusion rules', run: (a, f) => require('./cli/fp').list(a, f) },
144
- show: { desc: 'Show exclusion rule details', usage: 'securenow fp show <id>', run: (a, f) => require('./cli/fp').show(a, f) },
145
- 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) },
146
- edit: { desc: 'Edit an exclusion rule', usage: 'securenow fp edit <id> [--active true/false] [--conditions \'[...]\']', run: (a, f) => require('./cli/fp').edit(a, f) },
147
- delete: { desc: 'Delete an exclusion rule', usage: 'securenow fp delete <id> [--yes]', run: (a, f) => require('./cli/fp').remove(a, f) },
148
- '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) },
149
- '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) },
150
- '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) },
151
- 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) },
152
- },
153
- defaultSub: 'list',
154
- },
155
- firewall: {
156
- desc: 'Firewall status and IP testing',
157
- usage: 'securenow firewall <subcommand> [options]',
158
- sub: {
159
- status: { desc: 'Show firewall status, layers, and blocklist info', run: (a, f) => require('./cli/firewall').status(a, f) },
160
- '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) },
161
- },
162
- defaultSub: 'status',
163
- },
164
- blocklist: {
165
- desc: 'Manage IP blocklist',
166
- usage: 'securenow blocklist <subcommand> [options]',
167
- sub: {
168
- list: { desc: 'List blocked IPs', run: (a, f) => require('./cli/security').blocklistList(a, f) },
169
- add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
170
- remove: { desc: 'Unblock an IP', usage: 'securenow blocklist remove <id>', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
171
- stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
172
- },
173
- defaultSub: 'list',
174
- },
175
- allowlist: {
176
- desc: 'Manage IP allowlist (only allow listed IPs)',
177
- usage: 'securenow allowlist <subcommand> [options]',
178
- sub: {
179
- list: { desc: 'List allowed IPs', run: (a, f) => require('./cli/security').allowlistList(a, f) },
180
- add: { desc: 'Allow an IP', usage: 'securenow allowlist add <ip> [--label <label>] [--reason <reason>]', run: (a, f) => require('./cli/security').allowlistAdd(a, f) },
181
- remove: { desc: 'Remove an allowed IP', usage: 'securenow allowlist remove <id>', run: (a, f) => require('./cli/security').allowlistRemove(a, f) },
182
- stats: { desc: 'Allowlist statistics', run: (a, f) => require('./cli/security').allowlistStats(a, f) },
183
- },
184
- defaultSub: 'list',
185
- },
186
- trusted: {
187
- desc: 'Manage trusted IPs',
188
- usage: 'securenow trusted <subcommand> [options]',
189
- sub: {
190
- list: { desc: 'List trusted IPs', run: (a, f) => require('./cli/security').trustedList(a, f) },
191
- add: { desc: 'Add trusted IP', usage: 'securenow trusted add <ip> [--label <label>]', run: (a, f) => require('./cli/security').trustedAdd(a, f) },
192
- remove: { desc: 'Remove trusted IP', usage: 'securenow trusted remove <id>', run: (a, f) => require('./cli/security').trustedRemove(a, f) },
193
- },
194
- defaultSub: 'list',
195
- },
196
- ip: {
197
- desc: 'IP intelligence lookup',
198
- usage: 'securenow ip <ip-address>',
199
- sub: {
200
- lookup: { desc: 'Look up IP intelligence', run: (a, f) => require('./cli/security').ipLookup(a, f) },
201
- traces: { desc: 'Show traces for an IP', usage: 'securenow ip traces <ip>', run: (a, f) => require('./cli/security').ipTraces(a, f) },
202
- },
203
- defaultAction: (a, f) => require('./cli/security').ipLookup(a, f),
204
- },
205
- forensics: {
206
- desc: 'Run forensic queries (natural language → SQL)',
207
- usage: 'securenow forensics <query> [--app <key>]',
208
- sub: {
209
- query: { desc: 'Run a forensic query', flags: { app: 'App key to scope query' }, run: (a, f) => require('./cli/security').forensicsQuery(a, f) },
210
- 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) },
211
- library: { desc: 'View saved queries', run: (a, f) => require('./cli/security').forensicsLibrary(a, f) },
212
- },
213
- defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
214
- },
215
- 'api-map': {
216
- desc: 'View API map',
217
- usage: 'securenow api-map [stats]',
218
- sub: {
219
- list: { desc: 'List discovered API endpoints', run: (a, f) => require('./cli/security').apiMapList(a, f) },
220
- stats: { desc: 'API map statistics', run: (a, f) => require('./cli/security').apiMapStats(a, f) },
221
- },
222
- defaultSub: 'list',
223
- },
224
- instances: {
225
- desc: 'Manage ClickHouse instances',
226
- usage: 'securenow instances <subcommand> [options]',
227
- sub: {
228
- list: { desc: 'List instances', run: (a, f) => require('./cli/security').instancesList(a, f) },
229
- test: { desc: 'Test instance connection', usage: 'securenow instances test <id>', run: (a, f) => require('./cli/security').instancesTest(a, f) },
230
- },
231
- defaultSub: 'list',
232
- },
233
- analytics: {
234
- desc: 'View response analytics',
235
- usage: 'securenow analytics [--app <key>]',
236
- run: (a, f) => require('./cli/security').analytics(a, f),
237
- },
238
- status: {
239
- desc: 'Dashboard overview',
240
- usage: 'securenow status [--app <key>]',
241
- run: (a, f) => require('./cli/monitor').status(a, f),
242
- },
243
- config: {
244
- desc: 'Manage CLI configuration',
245
- usage: 'securenow config <set|get> [key] [value]',
246
- sub: {
247
- set: {
248
- desc: 'Set a config value',
249
- usage: 'securenow config set <key> <value>',
250
- run: (a) => {
251
- const conf = require('./cli/config');
252
- const [key, ...rest] = a;
253
- const value = rest.join(' ');
254
- if (!key || !value) { ui.error('Usage: securenow config set <key> <value>'); process.exit(1); }
255
- conf.setConfigValue(key, value);
256
- ui.success(`${key} = ${value}`);
257
- },
258
- },
259
- get: {
260
- desc: 'Get a config value',
261
- usage: 'securenow config get [key]',
262
- run: (a) => {
263
- const conf = require('./cli/config');
264
- const key = a[0];
265
- if (key) {
266
- const val = conf.getConfigValue(key);
267
- console.log(val != null ? val : ui.c.dim('(not set)'));
268
- } else {
269
- const all = conf.loadConfig();
270
- console.log('');
271
- ui.keyValue(Object.entries(all).map(([k, v]) => [k, v != null ? String(v) : ui.c.dim('(not set)')]));
272
- console.log('');
273
- }
274
- },
275
- },
276
- path: {
277
- desc: 'Show config file path',
278
- run: () => {
279
- const conf = require('./cli/config');
280
- console.log(`Config: ${conf.CONFIG_FILE}`);
281
- console.log(`Credentials: ${conf.CREDENTIALS_FILE}`);
282
- if (conf.hasLocalCredentials()) {
283
- console.log(`Local creds: ${conf.LOCAL_CREDENTIALS_FILE} ${require('./cli/ui').c.green('(active)')}`);
284
- }
285
- console.log(`Auth source: ${conf.getAuthSource()}`);
286
- },
287
- },
288
- },
289
- },
290
- init: {
291
- desc: 'Initialize SecureNow instrumentation',
292
- usage: 'securenow init [--ts|--js] [--src|--root] [--force]',
293
- flags: { typescript: 'Force TypeScript', javascript: 'Force JavaScript', src: 'Create in src/', root: 'Create in root', force: 'Overwrite existing' },
294
- run: (a, f) => require('./cli/init').init(a, f),
295
- },
296
- run: {
297
- desc: 'Run a Node.js app with automatic OTel instrumentation',
298
- usage: 'securenow run [node-flags] <script> [app-args]',
299
- flags: { watch: 'Enable Node.js watch mode', inspect: 'Enable Node.js inspector' },
300
- rawArgv: true,
301
- run: (rawArgs) => require('./cli/run').run(rawArgs),
302
- },
303
- version: {
304
- desc: 'Show CLI version',
305
- run: () => {
306
- try {
307
- const pkg = require('./package.json');
308
- console.log(`securenow v${pkg.version}`);
309
- } catch {
310
- console.log('securenow (version unknown)');
311
- }
312
- },
313
- },
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
+ `,
314
86
  };
315
87
 
316
- // ── Help System ──
88
+ function initCommand(args) {
89
+ const flags = parseFlags(args);
90
+ const cwd = process.cwd();
91
+
92
+ console.log('\n🚀 SecureNow Setup\n');
93
+
94
+ // Check if Next.js project
95
+ const isNextJs = isNextJsProject(cwd);
96
+ if (!isNextJs && !flags.force) {
97
+ console.log('⚠️ This doesn\'t appear to be a Next.js project.');
98
+ console.log(' If you want to proceed anyway, use: npx securenow init --force\n');
99
+ process.exit(1);
100
+ }
101
+
102
+ // Determine TypeScript or JavaScript
103
+ const useTypeScript = flags.typescript ||
104
+ (!flags.javascript && fs.existsSync(path.join(cwd, 'tsconfig.json')));
105
+
106
+ // Determine if using src folder
107
+ const useSrc = flags.src ||
108
+ (!flags.root && fs.existsSync(path.join(cwd, 'src')));
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}`);
147
+ }
148
+ } else {
149
+ console.log('ℹ️ .env.local already exists (skipped)');
150
+ }
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 ──────────────────────────────────────────────
317
172
 
318
- function showBanner() {
319
- console.log('');
320
- console.log(` ${ui.c.bold(ui.c.cyan('SecureNow CLI'))} ${ui.c.dim('— Security observability from the terminal')}`);
321
- console.log('');
173
+ function loadConfig() {
174
+ try {
175
+ if (!fs.existsSync(CONFIG_FILE)) return {};
176
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
177
+ } catch {
178
+ return {};
179
+ }
322
180
  }
323
181
 
324
- function showHelp(commandName) {
325
- if (commandName && COMMANDS[commandName]) {
326
- const cmd = COMMANDS[commandName];
327
- showBanner();
328
- console.log(` ${ui.c.bold(commandName)} ${cmd.desc}`);
329
- console.log('');
330
- if (cmd.usage) {
331
- console.log(` ${ui.c.bold('USAGE')}`);
332
- console.log(` ${cmd.usage}`);
333
- console.log('');
334
- }
335
- if (cmd.sub) {
336
- console.log(` ${ui.c.bold('SUBCOMMANDS')}`);
337
- const entries = Object.entries(cmd.sub);
338
- const maxLen = entries.reduce((m, [k]) => Math.max(m, k.length), 0);
339
- for (const [name, sub] of entries) {
340
- console.log(` ${ui.c.cyan(name.padEnd(maxLen + 2))} ${sub.desc}`);
341
- }
342
- console.log('');
182
+ function saveConfig(cfg) {
183
+ try {
184
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
185
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
186
+ try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
187
+ } catch (err) {
188
+ console.error(`❌ Could not write ${CONFIG_FILE}: ${err.message}`);
189
+ process.exit(1);
190
+ }
191
+ }
192
+
193
+ function openBrowser(url) {
194
+ const cmd =
195
+ process.platform === 'darwin' ? `open "${url}"` :
196
+ process.platform === 'win32' ? `start "" "${url}"` :
197
+ `xdg-open "${url}"`;
198
+ exec(cmd, () => {});
199
+ }
200
+
201
+ function loginCommand(args) {
202
+ const flags = parseFlags(args);
203
+ const state = crypto.randomBytes(16).toString('hex');
204
+
205
+ const server = http.createServer((req, res) => {
206
+ const url = new URL(req.url, 'http://127.0.0.1');
207
+ if (url.pathname !== '/callback') {
208
+ res.writeHead(404); res.end();
209
+ return;
343
210
  }
344
- if (cmd.flags) {
345
- console.log(` ${ui.c.bold('FLAGS')}`);
346
- for (const [flag, desc] of Object.entries(cmd.flags)) {
347
- console.log(` --${ui.c.cyan(flag.padEnd(16))} ${desc}`);
348
- }
349
- console.log('');
211
+ const token = url.searchParams.get('token');
212
+ const gotState = url.searchParams.get('state');
213
+ if (!token || gotState !== state) {
214
+ res.writeHead(400, { 'Content-Type': 'text/html' });
215
+ res.end('<h1>Auth failed</h1><p>You can close this tab and try again.</p>');
216
+ return;
350
217
  }
351
- console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
352
- console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
353
- console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help`);
354
- console.log('');
355
- return;
218
+ saveConfig({ token, savedAt: new Date().toISOString() });
219
+ res.writeHead(200, { 'Content-Type': 'text/html' });
220
+ res.end('<h1>✅ Logged in to SecureNow</h1><p>You can close this tab and return to your terminal.</p>');
221
+ console.log('\n✅ Logged in. Token saved to', CONFIG_FILE, '\n');
222
+ setTimeout(() => { server.close(); process.exit(0); }, 200);
223
+ });
224
+
225
+ server.listen(0, '127.0.0.1', () => {
226
+ const port = server.address().port;
227
+ const callback = `http://127.0.0.1:${port}/callback`;
228
+ const authUrl = `${APP_URL}/cli/auth?callback=${encodeURIComponent(callback)}&state=${state}${flags.force ? '&force_login=1' : ''}`;
229
+ console.log('\n🔐 SecureNow Login\n');
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 {}
356
245
  }
246
+ console.log('✅ Logged out');
247
+ }
357
248
 
358
- showBanner();
359
-
360
- const groups = {
361
- 'Run': ['run'],
362
- 'Authentication': ['login', 'logout', 'whoami'],
363
- 'Applications': ['apps', 'init', 'status'],
364
- 'Observe': ['traces', 'logs', 'analytics'],
365
- 'Detect & Respond': ['issues', 'notifications', 'alerts', 'fp'],
366
- 'Investigate': ['ip', 'forensics', 'api-map'],
367
- 'Firewall': ['firewall'],
368
- 'Remediation': ['blocklist', 'allowlist', 'trusted'],
369
- 'Settings': ['instances', 'config', 'version'],
370
- };
371
-
372
- for (const [group, cmds] of Object.entries(groups)) {
373
- console.log(` ${ui.c.bold(group)}`);
374
- for (const name of cmds) {
375
- const cmd = COMMANDS[name];
376
- if (cmd) {
377
- console.log(` ${ui.c.cyan(name.padEnd(18))} ${cmd.desc}`);
378
- }
379
- }
380
- console.log('');
249
+ function statusCommand() {
250
+ const cfg = loadConfig();
251
+ if (!cfg.token) {
252
+ console.log('❌ Not logged in. Run: npx securenow login');
253
+ process.exit(1);
381
254
  }
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 ────────────────────────────────────────────────
382
270
 
383
- console.log(` ${ui.c.bold('GLOBAL FLAGS')}`);
384
- console.log(` --${ui.c.cyan('json'.padEnd(16))} Output as JSON`);
385
- console.log(` --${ui.c.cyan('help'.padEnd(16))} Show help for a command`);
386
- console.log('');
387
- console.log(` ${ui.c.dim('Run')} securenow help <command> ${ui.c.dim('for detailed usage')}`);
388
- console.log('');
271
+ function apiRequest(method, pathname, body, token) {
272
+ return new Promise((resolve, reject) => {
273
+ const u = new URL(API_URL + pathname);
274
+ const lib = u.protocol === 'https:' ? https : http;
275
+ const data = body ? JSON.stringify(body) : null;
276
+ const req = lib.request({
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
+ });
389
306
  }
390
307
 
391
- // ── Main Router ──
308
+ // ─── apps ───────────────────────────────────────────────────────
392
309
 
393
- async function main() {
394
- const { positional, flags } = parseArgs(process.argv.slice(2));
395
- const cmdName = positional[0] || 'help';
310
+ function appsCommand(args) {
311
+ const sub = args[0];
312
+ const rest = args.slice(1);
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
+ }
396
318
 
397
- if (cmdName === 'help' || flags.help) {
398
- showHelp(flags.help === true ? positional[0] : flags.help || positional[1] || (cmdName !== 'help' ? cmdName : null));
399
- return;
319
+ function requireAuth() {
320
+ const cfg = loadConfig();
321
+ if (!cfg.token) {
322
+ console.error('❌ Not logged in. Run: npx securenow login');
323
+ process.exit(1);
400
324
  }
325
+ return cfg.token;
326
+ }
401
327
 
402
- const cmd = COMMANDS[cmdName];
403
- if (!cmd) {
404
- // Auto-detect: if the first arg looks like a file path, treat it as `securenow run <file>`
405
- if (/\.(m?[jt]sx?|cjs)$/.test(cmdName) || cmdName.includes('/') || cmdName.includes('\\')) {
406
- return COMMANDS.run.run(process.argv.slice(2));
328
+ async function appsList(args) {
329
+ const json = args.includes('--json');
330
+ const token = requireAuth();
331
+ try {
332
+ const data = await apiRequest('GET', '/api/applications', null, token);
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(', ')}`);
407
340
  }
408
- ui.error(`Unknown command: ${cmdName}`);
409
- ui.info('Run `securenow help` for a list of commands.');
341
+ console.log('');
342
+ } catch (err) {
343
+ console.error('❌ ' + err.message);
410
344
  process.exit(1);
411
345
  }
346
+ }
412
347
 
413
- if (cmd.rawArgv) {
414
- // Pass raw argv (everything after the command name) so the command can
415
- // forward flags like --watch, --inspect verbatim to child processes.
416
- const cmdIdx = process.argv.indexOf(cmdName);
417
- const rawArgs = cmdIdx !== -1 ? process.argv.slice(cmdIdx + 1) : positional.slice(1);
418
- await cmd.run(rawArgs);
419
- return;
420
- }
348
+ function prompt(question) {
349
+ return new Promise((resolve) => {
350
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
351
+ rl.question(question, (a) => { rl.close(); resolve(a.trim()); });
352
+ });
353
+ }
421
354
 
422
- if (cmd.run && !cmd.sub) {
423
- await cmd.run(positional.slice(1), flags);
424
- return;
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);
425
375
  }
376
+ }
426
377
 
427
- if (cmd.sub) {
428
- let subName = positional[1];
429
- let subArgs = positional.slice(2);
378
+ function helpCommand() {
379
+ console.log(`
380
+ SecureNow CLI - OpenTelemetry instrumentation for Next.js
430
381
 
431
- if (subName && cmd.sub[subName]) {
432
- if (flags.help) {
433
- showHelp(cmdName);
434
- return;
435
- }
436
- await cmd.sub[subName].run(subArgs, flags);
437
- return;
438
- }
382
+ USAGE:
383
+ npx securenow <command> [options]
439
384
 
440
- if (cmd.defaultAction) {
441
- const allArgs = subName ? [subName, ...subArgs] : [];
442
- await cmd.defaultAction(allArgs, flags);
443
- return;
444
- }
385
+ COMMANDS:
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
445
394
 
446
- if (cmd.defaultSub) {
447
- const allArgs = subName ? [subName, ...subArgs] : [];
448
- await cmd.sub[cmd.defaultSub].run(allArgs, flags);
449
- return;
450
- }
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
451
407
 
452
- showHelp(cmdName);
453
- return;
408
+ # Auto-detect and setup
409
+ npx securenow init
410
+
411
+ # Force TypeScript in src folder
412
+ npx securenow init --typescript --src
413
+
414
+ # Force JavaScript in root
415
+ npx securenow init --javascript --root
416
+
417
+ # Overwrite existing files
418
+ npx securenow init --force
419
+
420
+ DOCUMENTATION:
421
+ Quick Start: node_modules/securenow/NEXTJS-QUICKSTART.md
422
+ Full Guide: node_modules/securenow/NEXTJS-GUIDE.md
423
+ Examples: node_modules/securenow/examples/
424
+ `);
425
+ }
426
+
427
+ function versionCommand() {
428
+ try {
429
+ const packageJson = require('./package.json');
430
+ console.log(`securenow v${packageJson.version}`);
431
+ } catch (error) {
432
+ console.log('securenow (version unknown)');
454
433
  }
434
+ }
455
435
 
456
- if (cmd.run) {
457
- await cmd.run(positional.slice(1), flags);
436
+ // Utility functions
437
+ function parseFlags(args) {
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
+ }
458
454
  }
455
+
456
+ return flags;
459
457
  }
460
458
 
461
- main().catch((err) => {
462
- if (err.name !== 'CLIError') {
463
- ui.error(err.message || 'An unexpected error occurred');
459
+ function isNextJsProject(dir) {
460
+ try {
461
+ const packageJsonPath = path.join(dir, 'package.json');
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;
464
470
  }
465
- if (process.env.SECURENOW_DEBUG) {
466
- console.error(err.stack || err);
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);
467
483
  }
468
- process.exit(1);
469
- });
484
+
485
+ commandFn(args.slice(1));
486
+ }
487
+
488
+ if (require.main === module) {
489
+ main();
490
+ }
491
+
492
+ module.exports = { main };
493
+
494
+
495
+
496
+
497
+
498
+
499
+