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/apps.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { api, requireAuth } = require('./client');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const ui = require('./ui');
|
|
6
|
+
|
|
7
|
+
const FREE_TRIAL_URL = 'https://freetrial.securenow.ai:4318';
|
|
8
|
+
const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
9
|
+
|
|
10
|
+
function instanceUrl(inst) {
|
|
11
|
+
if (!inst) return FREE_TRIAL_URL;
|
|
12
|
+
return `${inst.protocol || 'https'}://${inst.host}:4318`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function fetchInstanceMap() {
|
|
16
|
+
try {
|
|
17
|
+
const data = await api.get('/instances');
|
|
18
|
+
const instances = data.instances || [];
|
|
19
|
+
const map = {};
|
|
20
|
+
for (const inst of instances) {
|
|
21
|
+
map[inst._id] = inst;
|
|
22
|
+
}
|
|
23
|
+
return map;
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function list(args, flags) {
|
|
30
|
+
requireAuth();
|
|
31
|
+
const s = ui.spinner('Fetching applications');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const [appData, instMap] = await Promise.all([
|
|
35
|
+
api.get('/applications'),
|
|
36
|
+
fetchInstanceMap(),
|
|
37
|
+
]);
|
|
38
|
+
const apps = appData.applications || [];
|
|
39
|
+
s.stop(`Found ${apps.length} application${apps.length !== 1 ? 's' : ''}`);
|
|
40
|
+
console.log('');
|
|
41
|
+
|
|
42
|
+
if (flags.json) {
|
|
43
|
+
const enriched = apps.map(app => ({
|
|
44
|
+
...app,
|
|
45
|
+
instanceUrl: instanceUrl(app.instanceId ? instMap[app.instanceId] : null),
|
|
46
|
+
}));
|
|
47
|
+
ui.json(enriched);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const defaultApp = config.getDefaultApp();
|
|
52
|
+
const rows = apps.map(app => {
|
|
53
|
+
const inst = app.instanceId ? instMap[app.instanceId] : null;
|
|
54
|
+
const url = instanceUrl(inst);
|
|
55
|
+
const instLabel = inst ? `${inst.name} ${ui.c.dim(url)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(url)}`;
|
|
56
|
+
return [
|
|
57
|
+
app.name + (app.key === defaultApp ? ui.c.cyan(' (default)') : ''),
|
|
58
|
+
ui.c.dim(app.key),
|
|
59
|
+
instLabel,
|
|
60
|
+
ui.timeAgo(app.createdAt),
|
|
61
|
+
];
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
ui.table(['Name', 'Key', 'Instance', 'Created'], rows);
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
if (apps.length > 0) {
|
|
68
|
+
console.log(` ${ui.c.bold('Add to your .env:')}`);
|
|
69
|
+
const first = apps.find(a => a.key === defaultApp) || apps[0];
|
|
70
|
+
const firstInst = first.instanceId ? instMap[first.instanceId] : null;
|
|
71
|
+
console.log(` SECURENOW_APPID=${first.key}`);
|
|
72
|
+
console.log(` SECURENOW_INSTANCE=${instanceUrl(firstInst)}`);
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!defaultApp && apps.length > 0) {
|
|
77
|
+
ui.info(`Tip: Set a default app with ${ui.c.bold('securenow config set defaultApp <key>')}`);
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
s.fail('Failed to fetch applications');
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function create(args, flags) {
|
|
87
|
+
requireAuth();
|
|
88
|
+
|
|
89
|
+
let name = args[0];
|
|
90
|
+
if (!name) {
|
|
91
|
+
name = await ui.prompt('Application name');
|
|
92
|
+
if (!name) {
|
|
93
|
+
ui.error('Application name is required');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const body = { name };
|
|
99
|
+
if (flags.hosts) {
|
|
100
|
+
body.hosts = flags.hosts.split(',').map(h => h.trim());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (flags.instance) {
|
|
104
|
+
body.instanceId = flags.instance;
|
|
105
|
+
} else if (process.stdin.isTTY) {
|
|
106
|
+
body.instanceId = await pickInstance();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const selectedInstanceId = body.instanceId;
|
|
110
|
+
|
|
111
|
+
const s = ui.spinner(`Creating application "${name}"`);
|
|
112
|
+
try {
|
|
113
|
+
const result = await api.post('/applications', body);
|
|
114
|
+
const app = result.application || result;
|
|
115
|
+
s.stop(`Application created`);
|
|
116
|
+
|
|
117
|
+
let inst = null;
|
|
118
|
+
if (selectedInstanceId) {
|
|
119
|
+
try {
|
|
120
|
+
const instData = await api.get(`/instances/${selectedInstanceId}`);
|
|
121
|
+
inst = instData.instance || null;
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
const envUrl = instanceUrl(inst);
|
|
125
|
+
|
|
126
|
+
console.log('');
|
|
127
|
+
ui.keyValue([
|
|
128
|
+
['Name', app.name],
|
|
129
|
+
['Key', ui.c.green(ui.c.bold(app.key))],
|
|
130
|
+
['ID', ui.c.dim(app._id)],
|
|
131
|
+
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
|
|
132
|
+
['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(` ${ui.c.bold('Add to your .env.local:')}`);
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log(` SECURENOW_APPID=${app.key}`);
|
|
139
|
+
console.log(` SECURENOW_INSTANCE=${envUrl}`);
|
|
140
|
+
console.log('');
|
|
141
|
+
ui.info(`Set as default: ${ui.c.bold(`securenow config set defaultApp ${app.key}`)}`);
|
|
142
|
+
console.log('');
|
|
143
|
+
|
|
144
|
+
if (flags.json) {
|
|
145
|
+
ui.json({ ...app, instanceUrl: envUrl });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (app.hosts && app.hosts.length > 0) {
|
|
149
|
+
await offerSubdomainDiscovery(app.hosts, selectedInstanceId, flags);
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
s.fail('Failed to create application');
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function pickInstance() {
|
|
158
|
+
const s = ui.spinner('Loading instances');
|
|
159
|
+
let instances = [];
|
|
160
|
+
try {
|
|
161
|
+
const data = await api.get('/instances');
|
|
162
|
+
instances = data.instances || [];
|
|
163
|
+
s.stop('Instances loaded');
|
|
164
|
+
} catch {
|
|
165
|
+
s.stop('Could not load instances');
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const FREE_TRIAL_LABEL = `${ui.c.green('Free Trial')} ${ui.c.dim('— SecureNow managed instance (no setup needed)')}`;
|
|
170
|
+
|
|
171
|
+
const choices = [{ label: FREE_TRIAL_LABEL, value: null }];
|
|
172
|
+
|
|
173
|
+
for (const inst of instances) {
|
|
174
|
+
const status = inst.status === 'active' ? ui.c.green('●') : ui.c.red('●');
|
|
175
|
+
const apps = inst.linkedApps ? ui.c.dim(` (${inst.linkedApps} app${inst.linkedApps !== 1 ? 's' : ''})`) : '';
|
|
176
|
+
choices.push({
|
|
177
|
+
label: `${status} ${inst.name}${apps} ${ui.c.dim(`[${inst._id}]`)}`,
|
|
178
|
+
value: inst._id,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const appUrl = config.getAppUrl();
|
|
183
|
+
choices.push({
|
|
184
|
+
label: `${ui.c.cyan('+')} Create a new instance ${ui.c.dim('(opens browser)')}`,
|
|
185
|
+
value: '__new__',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const selected = await ui.select('Which instance should this app use?', choices);
|
|
189
|
+
|
|
190
|
+
if (selected === '__new__') {
|
|
191
|
+
const url = `${appUrl}/dashboard/settings/instances`;
|
|
192
|
+
ui.info(`Opening ${ui.c.underline(url)}`);
|
|
193
|
+
openBrowser(url);
|
|
194
|
+
ui.info('Create your instance in the browser, then run this command again.');
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return selected;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function openBrowser(url) {
|
|
202
|
+
const { execFileSync } = require('child_process');
|
|
203
|
+
try {
|
|
204
|
+
if (process.platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
|
|
205
|
+
else if (process.platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
|
|
206
|
+
else execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
|
207
|
+
} catch {
|
|
208
|
+
ui.warn(`Could not open browser. Visit: ${url}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function info(args, flags) {
|
|
213
|
+
requireAuth();
|
|
214
|
+
|
|
215
|
+
const id = args[0];
|
|
216
|
+
if (!id) {
|
|
217
|
+
ui.error('Application ID is required. Usage: securenow apps info <id>');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const s = ui.spinner('Fetching application details');
|
|
222
|
+
try {
|
|
223
|
+
const data = await api.get(`/applications/${id}`);
|
|
224
|
+
const app = data.application || data;
|
|
225
|
+
|
|
226
|
+
let inst = null;
|
|
227
|
+
if (app.instanceId) {
|
|
228
|
+
try {
|
|
229
|
+
const instData = await api.get(`/instances/${app.instanceId}`);
|
|
230
|
+
inst = instData.instance || null;
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
const envUrl = instanceUrl(inst);
|
|
234
|
+
s.stop('Application details loaded');
|
|
235
|
+
|
|
236
|
+
if (flags.json) {
|
|
237
|
+
ui.json({ ...app, instanceUrl: envUrl });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('');
|
|
242
|
+
ui.heading(app.name);
|
|
243
|
+
console.log('');
|
|
244
|
+
ui.keyValue([
|
|
245
|
+
['Key', ui.c.bold(app.key)],
|
|
246
|
+
['ID', ui.c.dim(app._id)],
|
|
247
|
+
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
|
|
248
|
+
['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
|
|
249
|
+
['Created', app.createdAt ? new Date(app.createdAt).toLocaleString() : '—'],
|
|
250
|
+
['Updated', app.updatedAt ? new Date(app.updatedAt).toLocaleString() : '—'],
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(` ${ui.c.bold('Environment variables:')}`);
|
|
255
|
+
console.log(` SECURENOW_APPID=${app.key}`);
|
|
256
|
+
console.log(` SECURENOW_INSTANCE=${envUrl}`);
|
|
257
|
+
console.log('');
|
|
258
|
+
} catch (err) {
|
|
259
|
+
s.fail('Failed to fetch application');
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function remove(args, flags) {
|
|
265
|
+
requireAuth();
|
|
266
|
+
|
|
267
|
+
const id = args[0];
|
|
268
|
+
if (!id) {
|
|
269
|
+
ui.error('Application ID is required. Usage: securenow apps delete <id>');
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!flags.force && !flags.yes) {
|
|
274
|
+
const ok = await ui.confirm('Are you sure you want to delete this application?');
|
|
275
|
+
if (!ok) {
|
|
276
|
+
ui.info('Cancelled');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const s = ui.spinner('Deleting application');
|
|
282
|
+
try {
|
|
283
|
+
await api.delete(`/applications/${id}`);
|
|
284
|
+
s.stop('Application deleted');
|
|
285
|
+
} catch (err) {
|
|
286
|
+
s.fail('Failed to delete application');
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function setDefault(args) {
|
|
292
|
+
const key = args[0];
|
|
293
|
+
if (!key) {
|
|
294
|
+
ui.error('App key is required. Usage: securenow apps default <key>');
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
config.setConfigValue('defaultApp', key);
|
|
298
|
+
ui.success(`Default application set to ${ui.c.bold(key)}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function extractRootDomains(hosts) {
|
|
302
|
+
const domains = new Set();
|
|
303
|
+
for (const host of hosts || []) {
|
|
304
|
+
let cleaned = host.trim().toLowerCase();
|
|
305
|
+
try {
|
|
306
|
+
if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
|
|
307
|
+
cleaned = new URL(cleaned).hostname;
|
|
308
|
+
}
|
|
309
|
+
} catch {}
|
|
310
|
+
if (DOMAIN_REGEX.test(cleaned)) {
|
|
311
|
+
const parts = cleaned.split('.');
|
|
312
|
+
const root = parts.length > 2 ? parts.slice(-2).join('.') : cleaned;
|
|
313
|
+
domains.add(root);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return [...domains];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function offerSubdomainDiscovery(hosts, instanceId, flags) {
|
|
320
|
+
if (!process.stdin.isTTY) return;
|
|
321
|
+
|
|
322
|
+
const domains = extractRootDomains(hosts);
|
|
323
|
+
if (domains.length === 0) return;
|
|
324
|
+
|
|
325
|
+
const shouldDiscover = await ui.confirm(
|
|
326
|
+
`Domain${domains.length > 1 ? 's' : ''} detected (${domains.join(', ')}). Discover subdomains and add them as apps?`
|
|
327
|
+
);
|
|
328
|
+
if (!shouldDiscover) return;
|
|
329
|
+
|
|
330
|
+
await discoverAndAdd(domains, instanceId, flags);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function discoverAndAdd(domains, instanceId, flags) {
|
|
334
|
+
const s = ui.spinner(`Scanning ${domains.length} domain${domains.length > 1 ? 's' : ''} for subdomains`);
|
|
335
|
+
|
|
336
|
+
let allSubdomains = [];
|
|
337
|
+
try {
|
|
338
|
+
for (const domain of domains) {
|
|
339
|
+
s.update(`Scanning ${domain}...`);
|
|
340
|
+
const result = await api.get(`/subdomains`, { query: { domain } });
|
|
341
|
+
if (result.success && result.subdomains) {
|
|
342
|
+
allSubdomains.push(...result.subdomains);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
s.stop(`Found ${allSubdomains.length} subdomain${allSubdomains.length !== 1 ? 's' : ''}`);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
s.fail('Failed to discover subdomains');
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (allSubdomains.length === 0) {
|
|
352
|
+
ui.info('No subdomains found.');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Filter out subdomains already registered as apps
|
|
357
|
+
let existingHosts;
|
|
358
|
+
try {
|
|
359
|
+
const appData = await api.get('/applications');
|
|
360
|
+
const apps = appData.applications || [];
|
|
361
|
+
existingHosts = new Set(apps.flatMap(a => (a.hosts || []).map(h => h.toLowerCase())));
|
|
362
|
+
} catch {
|
|
363
|
+
existingHosts = new Set();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const newSubdomains = allSubdomains.filter(
|
|
367
|
+
(s) => !existingHosts.has(s.subdomain.toLowerCase())
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (newSubdomains.length === 0) {
|
|
371
|
+
ui.info('All discovered subdomains are already registered as apps.');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log('');
|
|
376
|
+
ui.info(`${newSubdomains.length} new subdomain${newSubdomains.length !== 1 ? 's' : ''} found (${allSubdomains.length - newSubdomains.length} already tracked):`);
|
|
377
|
+
|
|
378
|
+
if (flags && flags.json) {
|
|
379
|
+
ui.json(newSubdomains);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const choices = newSubdomains.map((sub) => ({
|
|
384
|
+
label: sub.subdomain,
|
|
385
|
+
detail: `IP: ${sub.ip || 'none'}${sub.cloudflare ? ' · Cloudflare' : ''}`,
|
|
386
|
+
value: sub.subdomain,
|
|
387
|
+
}));
|
|
388
|
+
|
|
389
|
+
const selected = await ui.multiSelect(
|
|
390
|
+
'Which subdomains should be added as monitored apps?',
|
|
391
|
+
choices
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (selected.length === 0) {
|
|
395
|
+
ui.info('No subdomains selected.');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const createSpinner = ui.spinner(`Creating ${selected.length} application${selected.length !== 1 ? 's' : ''}`);
|
|
400
|
+
try {
|
|
401
|
+
const result = await api.post('/applications/bulk', {
|
|
402
|
+
subdomains: selected,
|
|
403
|
+
instanceId: instanceId || null,
|
|
404
|
+
});
|
|
405
|
+
const count = result.count || selected.length;
|
|
406
|
+
createSpinner.stop(`Created ${count} application${count !== 1 ? 's' : ''}`);
|
|
407
|
+
|
|
408
|
+
console.log('');
|
|
409
|
+
if (result.applications) {
|
|
410
|
+
const rows = result.applications.map((app) => [
|
|
411
|
+
app.name,
|
|
412
|
+
ui.c.dim(app.key),
|
|
413
|
+
app.hosts?.join(', ') || '',
|
|
414
|
+
]);
|
|
415
|
+
ui.table(['Name', 'Key', 'Hosts'], rows);
|
|
416
|
+
}
|
|
417
|
+
console.log('');
|
|
418
|
+
} catch (err) {
|
|
419
|
+
createSpinner.fail('Failed to create applications');
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function discover(args, flags) {
|
|
425
|
+
requireAuth();
|
|
426
|
+
|
|
427
|
+
const appId = args[0];
|
|
428
|
+
let domains = [];
|
|
429
|
+
let instanceId = null;
|
|
430
|
+
|
|
431
|
+
if (appId) {
|
|
432
|
+
const s = ui.spinner('Fetching application');
|
|
433
|
+
try {
|
|
434
|
+
const data = await api.get(`/applications/${appId}`);
|
|
435
|
+
const app = data.application || data;
|
|
436
|
+
s.stop(`Application: ${app.name}`);
|
|
437
|
+
domains = extractRootDomains(app.hosts);
|
|
438
|
+
instanceId = app.instanceId;
|
|
439
|
+
} catch (err) {
|
|
440
|
+
s.fail('Failed to fetch application');
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
} else if (flags.domain) {
|
|
444
|
+
domains = flags.domain.split(',').map(d => d.trim());
|
|
445
|
+
} else {
|
|
446
|
+
const domain = await ui.prompt('Domain to scan (e.g. example.com)');
|
|
447
|
+
if (!domain) {
|
|
448
|
+
ui.error('Domain is required');
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
domains = domain.split(',').map(d => d.trim());
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (domains.length === 0) {
|
|
455
|
+
ui.error('No domains found in application hosts. Specify a domain with --domain');
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (flags.instance) {
|
|
460
|
+
instanceId = flags.instance;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
await discoverAndAdd(domains, instanceId, flags);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function scan(args, flags) {
|
|
467
|
+
requireAuth();
|
|
468
|
+
const s = ui.spinner('Running subdomain discovery across all applications');
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
const appData = await api.get('/applications');
|
|
472
|
+
const apps = appData.applications || [];
|
|
473
|
+
|
|
474
|
+
const allDomains = new Set();
|
|
475
|
+
let firstInstanceId = null;
|
|
476
|
+
for (const app of apps) {
|
|
477
|
+
const roots = extractRootDomains(app.hosts);
|
|
478
|
+
for (const r of roots) allDomains.add(r);
|
|
479
|
+
if (!firstInstanceId && app.instanceId) firstInstanceId = app.instanceId;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (allDomains.size === 0) {
|
|
483
|
+
s.stop('No domains found across your applications');
|
|
484
|
+
ui.info('Add domains to your application hosts first, then run this command again.');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
s.update(`Found ${allDomains.size} unique domain${allDomains.size !== 1 ? 's' : ''}, scanning...`);
|
|
489
|
+
|
|
490
|
+
const existingHosts = new Set(
|
|
491
|
+
apps.flatMap((a) => (a.hosts || []).map((h) => h.toLowerCase()))
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
let totalNew = 0;
|
|
495
|
+
let totalCreated = 0;
|
|
496
|
+
const allNew = [];
|
|
497
|
+
|
|
498
|
+
for (const domain of allDomains) {
|
|
499
|
+
s.update(`Scanning ${domain}...`);
|
|
500
|
+
try {
|
|
501
|
+
const result = await api.get('/subdomains', { query: { domain } });
|
|
502
|
+
if (result.success && result.subdomains) {
|
|
503
|
+
const newSubs = result.subdomains.filter(
|
|
504
|
+
(sub) => !existingHosts.has(sub.subdomain.toLowerCase())
|
|
505
|
+
);
|
|
506
|
+
totalNew += newSubs.length;
|
|
507
|
+
allNew.push(...newSubs);
|
|
508
|
+
for (const sub of newSubs) {
|
|
509
|
+
existingHosts.add(sub.subdomain.toLowerCase());
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
ui.warn(`Failed to scan ${domain}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
s.stop(`Scanned ${allDomains.size} domain${allDomains.size !== 1 ? 's' : ''}, found ${totalNew} new subdomain${totalNew !== 1 ? 's' : ''}`);
|
|
518
|
+
|
|
519
|
+
if (totalNew === 0) {
|
|
520
|
+
ui.success('All subdomains are already tracked. Nothing to add.');
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (flags.yes || flags.force) {
|
|
525
|
+
const createSpinner = ui.spinner(`Creating ${totalNew} application${totalNew !== 1 ? 's' : ''}`);
|
|
526
|
+
try {
|
|
527
|
+
const subdomainNames = allNew.map((s) => s.subdomain);
|
|
528
|
+
const result = await api.post('/applications/bulk', {
|
|
529
|
+
subdomains: subdomainNames,
|
|
530
|
+
instanceId: firstInstanceId || null,
|
|
531
|
+
});
|
|
532
|
+
totalCreated = result.count || subdomainNames.length;
|
|
533
|
+
createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
createSpinner.fail('Failed to create applications');
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
} else if (process.stdin.isTTY) {
|
|
539
|
+
const choices = allNew.map((sub) => ({
|
|
540
|
+
label: sub.subdomain,
|
|
541
|
+
detail: `IP: ${sub.ip || 'none'}${sub.cloudflare ? ' · Cloudflare' : ''}`,
|
|
542
|
+
value: sub.subdomain,
|
|
543
|
+
}));
|
|
544
|
+
|
|
545
|
+
const selected = await ui.multiSelect(
|
|
546
|
+
'Which subdomains should be added as monitored apps?',
|
|
547
|
+
choices
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
if (selected.length === 0) {
|
|
551
|
+
ui.info('No subdomains selected.');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const createSpinner = ui.spinner(`Creating ${selected.length} application${selected.length !== 1 ? 's' : ''}`);
|
|
556
|
+
try {
|
|
557
|
+
const result = await api.post('/applications/bulk', {
|
|
558
|
+
subdomains: selected,
|
|
559
|
+
instanceId: firstInstanceId || null,
|
|
560
|
+
});
|
|
561
|
+
totalCreated = result.count || selected.length;
|
|
562
|
+
createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
createSpinner.fail('Failed to create applications');
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
console.log('');
|
|
569
|
+
for (const sub of allNew) {
|
|
570
|
+
console.log(` ${sub.subdomain} ${ui.c.dim(`(IP: ${sub.ip || 'none'})`)}`);
|
|
571
|
+
}
|
|
572
|
+
console.log('');
|
|
573
|
+
ui.info(`Run with --yes to auto-create all ${totalNew} apps.`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (flags.json) {
|
|
577
|
+
ui.json({ domainsScanned: allDomains.size, newSubdomains: totalNew, appsCreated: totalCreated });
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
s.fail('Subdomain scan failed');
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
module.exports = { list, create, info, remove, setDefault, discover, scan };
|