UNPKG

@vigilbase/cli

Version:

Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.

285 lines (284 loc) • 14.8 kB
import { parseArgs } from 'node:util'; import { DeviceAuthError } from './auth/index.js'; import { VigilbaseApiError, VigilbaseClient } from './lib/client.js'; import { createConfig, createLocalCommandConfig } from './lib/config.js'; import { renderBanner, shouldShowBanner } from './lib/banner.js'; import { createWriter, formatError } from './lib/format.js'; import { HELP_TAGLINE, HELP_TEXT, HELP_USAGE, helpForCommand, isCatalogTool } from './lib/help.js'; import { wasRendered } from './lib/tui.js'; import { getCommandHandler } from './commands/index.js'; import { LocalCheckupError } from './local/cloudflare-checkup/errors.js'; import { EXIT } from './types.js'; import { CLI_VERSION } from './version.js'; export { CLI_VERSION }; function parseCli(argv) { const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, strict: true, options: { json: { type: 'boolean', short: 'j', default: false }, help: { type: 'boolean', short: 'h', default: false }, version: { type: 'boolean', short: 'V', default: false }, 'base-url': { type: 'string' }, 'one-url': { type: 'string' }, 'auth-url': { type: 'string' }, 'client-id': { type: 'string' }, timeout: { type: 'string' }, style: { type: 'string' }, 'no-skill': { type: 'boolean', default: false }, via: { type: 'string' }, public: { type: 'boolean', default: false }, token: { type: 'string' }, 'token-stdin': { type: 'boolean', default: false }, 'no-open': { type: 'boolean', default: false }, email: { type: 'string' }, role: { type: 'string' }, confirm: { type: 'boolean', default: false }, 'account-id': { type: 'string' }, tool: { type: 'string' }, limit: { type: 'string' }, 'target-id': { type: 'string' }, 'customer-id': { type: 'string' }, 'zone-id': { type: 'string' }, 'days': { type: 'string' }, 'offset': { type: 'string' }, 'status': { type: 'string' }, 'severity': { type: 'string' }, 'attack-category': { type: 'string' }, 'order-by': { type: 'string' }, 'order-direction': { type: 'string' }, 'asset-id': { type: 'string' }, 'provider': { type: 'string' }, 'dataset': { type: 'string' }, 'search': { type: 'string' }, 'start': { type: 'string' }, 'end': { type: 'string' }, 'sort': { type: 'string' }, 'frs-min': { type: 'string' }, 'before': { type: 'string' }, 'note': { type: 'string' }, 'cursor': { type: 'string' }, 'action-prefix': { type: 'string' }, 'outcome': { type: 'string' }, 'hours': { type: 'string' }, 'metrics': { type: 'string' }, 'min-weight': { type: 'string' }, 'range': { type: 'string' }, 'region': { type: 'string' }, 'issue-id': { type: 'string' }, 'revision': { type: 'string' }, all: { type: 'boolean', default: false }, light: { type: 'boolean', default: false }, profile: { type: 'string' }, 'cf-auth': { type: 'string' }, 'cf-bin': { type: 'string' }, out: { type: 'string' }, overwrite: { type: 'boolean', default: false }, 'dry-run': { type: 'boolean', default: false }, 'max-requests-per-5min': { type: 'string' }, yes: { type: 'boolean', default: false }, }, }); return { positionals, values: { 'customer-id': typeof values['customer-id'] === 'string' ? values['customer-id'] : undefined, 'zone-id': typeof values['zone-id'] === 'string' ? values['zone-id'] : undefined, 'days': typeof values['days'] === 'string' ? values['days'] : undefined, 'offset': typeof values['offset'] === 'string' ? values['offset'] : undefined, 'status': typeof values['status'] === 'string' ? values['status'] : undefined, 'severity': typeof values['severity'] === 'string' ? values['severity'] : undefined, 'attack-category': typeof values['attack-category'] === 'string' ? values['attack-category'] : undefined, 'order-by': typeof values['order-by'] === 'string' ? values['order-by'] : undefined, 'order-direction': typeof values['order-direction'] === 'string' ? values['order-direction'] : undefined, 'asset-id': typeof values['asset-id'] === 'string' ? values['asset-id'] : undefined, 'provider': typeof values['provider'] === 'string' ? values['provider'] : undefined, 'dataset': typeof values['dataset'] === 'string' ? values['dataset'] : undefined, 'search': typeof values['search'] === 'string' ? values['search'] : undefined, 'start': typeof values['start'] === 'string' ? values['start'] : undefined, 'end': typeof values['end'] === 'string' ? values['end'] : undefined, 'sort': typeof values['sort'] === 'string' ? values['sort'] : undefined, 'frs-min': typeof values['frs-min'] === 'string' ? values['frs-min'] : undefined, 'before': typeof values['before'] === 'string' ? values['before'] : undefined, 'note': typeof values['note'] === 'string' ? values['note'] : undefined, 'cursor': typeof values['cursor'] === 'string' ? values['cursor'] : undefined, 'action-prefix': typeof values['action-prefix'] === 'string' ? values['action-prefix'] : undefined, 'outcome': typeof values['outcome'] === 'string' ? values['outcome'] : undefined, 'hours': typeof values['hours'] === 'string' ? values['hours'] : undefined, 'metrics': typeof values['metrics'] === 'string' ? values['metrics'] : undefined, 'min-weight': typeof values['min-weight'] === 'string' ? values['min-weight'] : undefined, 'range': typeof values['range'] === 'string' ? values['range'] : undefined, 'region': typeof values['region'] === 'string' ? values['region'] : undefined, 'issue-id': typeof values['issue-id'] === 'string' ? values['issue-id'] : undefined, 'revision': typeof values['revision'] === 'string' ? values['revision'] : undefined, isAll: Boolean(values.all), isLight: Boolean(values.light), isJson: Boolean(values.json), isHelp: Boolean(values.help), isVersion: Boolean(values.version), 'base-url': typeof values['base-url'] === 'string' ? values['base-url'] : undefined, 'one-url': typeof values['one-url'] === 'string' ? values['one-url'] : undefined, 'auth-url': typeof values['auth-url'] === 'string' ? values['auth-url'] : undefined, 'client-id': typeof values['client-id'] === 'string' ? values['client-id'] : undefined, timeout: typeof values.timeout === 'string' ? values.timeout : undefined, style: typeof values.style === 'string' ? values.style : undefined, isNoSkill: Boolean(values['no-skill']), via: typeof values.via === 'string' ? values.via : undefined, isPublic: Boolean(values.public), token: typeof values.token === 'string' ? values.token : undefined, isTokenStdin: Boolean(values['token-stdin']), isNoOpen: Boolean(values['no-open']), email: typeof values.email === 'string' ? values.email : undefined, role: typeof values.role === 'string' ? values.role : undefined, isConfirm: Boolean(values.confirm), 'account-id': typeof values['account-id'] === 'string' ? values['account-id'] : undefined, tool: typeof values.tool === 'string' ? values.tool : undefined, limit: typeof values.limit === 'string' ? values.limit : undefined, 'target-id': typeof values['target-id'] === 'string' ? values['target-id'] : undefined, profile: typeof values.profile === 'string' ? values.profile : undefined, 'cf-auth': typeof values['cf-auth'] === 'string' ? values['cf-auth'] : undefined, 'cf-bin': typeof values['cf-bin'] === 'string' ? values['cf-bin'] : undefined, out: typeof values.out === 'string' ? values.out : undefined, isOverwrite: Boolean(values.overwrite), isDryRun: Boolean(values['dry-run']), 'max-requests-per-5min': typeof values['max-requests-per-5min'] === 'string' ? values['max-requests-per-5min'] : undefined, isYes: Boolean(values.yes), }, }; } function parseTimeout(raw) { if (raw === undefined) return undefined; const value = Number(raw); if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value <= 0 || value > 2_147_483_647) { throw new Error('--timeout must be an integer between 1 and 2147483647 milliseconds.'); } return value; } function resolveMode(jsonFlag) { return jsonFlag ? 'json' : 'text'; } export async function runCli(options = {}) { const argv = options.argv ?? process.argv.slice(2); const stdoutStream = options.stdout ?? process.stdout; const writer = createWriter(stdoutStream, options.stderr); const previousEnv = process.env; if (options.env) { process.env = options.env; } try { const parsed = parseCli(argv); const mode = resolveMode(parsed.values.isJson); if (parsed.values.isVersion || parsed.values.isHelp || !parsed.positionals.length || parsed.positionals[0] === 'help') { const informational = new Set(['isJson', 'isHelp', 'isVersion', 'base-url', 'one-url', 'auth-url', 'client-id', 'timeout']); for (const [key, value] of Object.entries(parsed.values)) { if (value === undefined || value === false || informational.has(key)) continue; const flag = key.startsWith('is') ? key.slice(2).replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : key; throw new Error(`--${flag} is not supported with help or version.`); } if (parsed.values.isVersion && parsed.positionals.length) throw new Error('Usage: vigilbase --version'); if (parsed.positionals[0] === 'help' && parsed.positionals.length > 2) throw new Error(HELP_USAGE); } if (parsed.values.isVersion) { if (mode === 'json') { writer.stdout(JSON.stringify({ name: '@vigilbase/cli', version: CLI_VERSION }, null, 2)); } else if (!stdoutStream.isTTY) { // Pipes and files get only the version, for scripts. writer.stdout(CLI_VERSION); } else if (shouldShowBanner(mode)) { writer.stdout(renderBanner({ version: CLI_VERSION, tagline: HELP_TAGLINE })); } else { writer.stdout(`@vigilbase/cli ${CLI_VERSION}`); } return EXIT.OK; } const [command, ...rest] = parsed.positionals; const helpCommand = command === 'help' ? rest[0] : parsed.values.isHelp ? command : undefined; if (parsed.values.isHelp || !command || command === 'help' || command === '--help') { const specific = helpCommand ? helpForCommand(helpCommand) : null; if (helpCommand && !specific) { throw new Error(`Unknown command "${helpCommand}". Run \`vigilbase help\` for usage.`); } if (mode === 'json') { writer.stdout(JSON.stringify({ name: '@vigilbase/cli', version: CLI_VERSION, help: specific ?? HELP_TEXT, command: helpCommand ?? null, }, null, 2)); } else if (specific) { writer.stdout(specific); } else { const prefix = shouldShowBanner(mode) ? `${renderBanner({ version: CLI_VERSION, tagline: HELP_TAGLINE })}\n\n` : ''; writer.stdout(`${prefix}${HELP_TEXT}`); } return EXIT.OK; } // Local checkups validate --base-url themselves (resolveSubmitOrigin) and ignore VIGILBASE_* URLs. const config = command === 'local' || command === 'cloudflare-checkup' ? createLocalCommandConfig(parseTimeout(parsed.values.timeout)) : createConfig({ baseUrl: parsed.values['base-url'], oneBaseUrl: parsed.values['one-url'], identityBaseUrl: parsed.values['auth-url'], clientId: parsed.values['client-id'], timeoutMs: parseTimeout(parsed.values.timeout), }); const publicClient = new VigilbaseClient(config); const handler = getCommandHandler(command); if (!handler) { // Tools are not commands of their own (no aliases): point to how they run. throw new Error(isCatalogTool(command) ? `Unknown command "${command}". Run the tool with \`vigilbase run ${command} <target>\`.` : `Unknown command "${command}". Run \`vigilbase help\` for usage.`); } return await handler({ command, rest, values: parsed.values, mode, config, writer, publicClient, env: process.env, stdin: options.stdin, stdoutStream, stderrStream: options.stderr ?? process.stderr, loginHandler: options.loginHandler, }); } catch (error) { const mode = argv.includes('--json') || argv.includes('-j') ? 'json' : 'text'; // The terminal UI already showed it (■ and the hint) in its timeline. if (mode === 'json' || !wasRendered(error)) formatError(error, mode, writer); if ((error instanceof VigilbaseApiError || error instanceof DeviceAuthError) && error.status === 429) { return EXIT.RATE_LIMITED; } if (error instanceof LocalCheckupError && error.isRateLimited) { return EXIT.RATE_LIMITED; } return EXIT.ERROR; } finally { if (options.env) { process.env = previousEnv; } } }