@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
285 lines (284 loc) • 14.8 kB
JavaScript
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;
}
}
}