UNPKG

@vigilbase/cli

Version:

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

227 lines (226 loc) • 13.1 kB
import { z } from 'zod'; import { executeOneOperation, formatOnePayload } from '../lib/one-operations.js'; import { assertCommandFlags, ok, outputJson, requireOneClient, withSpinner } from './helpers.js'; import { missingArguments } from './legacy-validation.js'; const regions = ['ams', 'nyc', 'lon', 'sgp', 'iad', 'sfo', 'fra', 'dub', 'tok', 'syd', 'cpt', 'gru', 'dxb', 'ewr', 'lhr', 'jnb', 'atl', 'blr', 'bom', 'cdg', 'del', 'dfw', 'hnl', 'icn', 'itm', 'lax', 'mad', 'man', 'mel', 'mex', 'mia', 'mxp', 'nrt', 'ord', 'sao', 'scl', 'sea', 'sjc', 'sto', 'tlv', 'waw', 'yto']; function flag(ctx, name, max = 2048) { const value = ctx.values[name]; return value === undefined ? undefined : z.string().trim().min(1, `--${name} must not be empty.`).max(max).parse(value); } function integer(ctx, name, min, max) { const value = flag(ctx, name); if (value === undefined) return undefined; if (!/^\d+$/.test(value)) throw new Error(`--${name} must be an integer.`); return z.number().int().min(min).max(max).parse(Number(value)); } function uuid(value) { const result = z.string().uuid().safeParse(value); if (!result.success) throw new Error('A valid UUID is required. Copy the full customer, target, or report ID from its list command.'); return result.data; } function identifier(value) { const result = z.string().trim().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/).safeParse(value); if (!result.success) throw new Error('An ID containing only letters, numbers, underscores, or hyphens is required.'); return result.data; } const customerSubcommands = ['show', 'summary', 'services', 'endpoints', 'contacts', 'organization', 'watchdog-targets', 'watchdog-scans', 'reports', 'investigations', 'contracts', 'billing', 'invoices', 'subscriptions', 'projects']; /** Positional placeholders of each subcommand (as in ONE_HELP), so a missing argument is named. */ const POSITIONALS = { customers: { ...Object.fromEntries(customerSubcommands.map((command) => [command, '<customer-id>'])), report: '<customer-id> <report-id>', investigation: '<customer-id> <investigation-id>', }, orgs: { show: '<organization-id>' }, monitoring: { metrics: '<target-id>...', targets: '<customer-id>', status: '<customer-id>', history: '<customer-id>' }, }; function positionalUsage(ctx) { const [command] = ctx.rest; const placeholders = POSITIONALS[ctx.command]; if (command === undefined || !placeholders || !Object.hasOwn(placeholders, command)) return undefined; return `vigilbase ${ctx.command} ${command} ${placeholders[command]}`; } function check(ctx, arity, flags = []) { if (ctx.rest.length < arity) throw new Error(missingArguments(ctx.command, arity - ctx.rest.length, positionalUsage(ctx))); if (ctx.rest.length > arity) throw new Error(`Unexpected arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`); assertCommandFlags(ctx, flags); } export function buildOneOperation(ctx) { const command = ctx.rest[0] ?? (ctx.command === 'monitoring' ? 'health' : ctx.command === 'support' ? 'tickets' : 'list'); const operation = (path, query) => ({ path, query, label: `${ctx.command} ${command}` }); if (ctx.command === 'customers') { if (command === 'list' || command === 'ls') { check(ctx, ctx.rest.length === 0 ? 0 : 1); return operation('/api/customers'); } const simpleRoutes = { show: '', summary: '/summary', services: '/service-records', endpoints: '/endpoints', contacts: '/contacts', organization: '/organization', 'watchdog-targets': '/watchdog/targets', billing: '/billing', subscriptions: '/stripe/subscriptions', projects: '/projects', }; if (Object.hasOwn(simpleRoutes, command)) { check(ctx, 2); return operation(`/api/customers/${uuid(ctx.rest[1])}${simpleRoutes[command]}`); } if (command === 'watchdog-scans') { check(ctx, 2, ['limit', 'offset', 'target-id']); return operation(`/api/customers/${uuid(ctx.rest[1])}/watchdog/scans`, { limit: integer(ctx, 'limit', 1, 100), offset: integer(ctx, 'offset', 0, 1_000_000), target_id: ctx.values['target-id'] === undefined ? undefined : uuid(ctx.values['target-id']), }); } if (command === 'reports' || command === 'report') { check(ctx, command === 'reports' ? 2 : 3, command === 'reports' ? ['limit'] : []); const base = `/api/customers/${uuid(ctx.rest[1])}/recon/reports`; return command === 'reports' ? operation(base, { limit: integer(ctx, 'limit', 1, 100) }) : operation(`${base}/${uuid(ctx.rest[2])}`); } if (command === 'investigations') { check(ctx, 2, ['limit', 'cursor', 'before', 'issue-id']); const before = flag(ctx, 'before'); const cursor = flag(ctx, 'cursor', 512); if (before && cursor) throw new Error('--before and --cursor are mutually exclusive.'); if (before) z.iso.datetime({ offset: true }).parse(before); return operation(`/api/customers/${uuid(ctx.rest[1])}/vigil/investigations`, { limit: integer(ctx, 'limit', 1, 100), cursor, before, issueId: flag(ctx, 'issue-id', 256), }); } if (command === 'investigation') { check(ctx, 3, ['revision']); return operation(`/api/customers/${uuid(ctx.rest[1])}/vigil/investigations/${uuid(ctx.rest[2])}`, { revision: integer(ctx, 'revision', 1, 2_147_483_647) }); } if (command === 'contracts') { check(ctx, 2, ['status']); return operation(`/api/customers/${uuid(ctx.rest[1])}/contracts`, { status: z.enum(['draft', 'pending', 'active', 'expired', 'terminated']).optional().parse(flag(ctx, 'status')) }); } if (command === 'invoices') { check(ctx, 2, ['status', 'limit']); return operation(`/api/customers/${uuid(ctx.rest[1])}/stripe/invoices`, { limit: integer(ctx, 'limit', 1, 100), status: z.enum(['draft', 'open', 'paid', 'uncollectible', 'void']).optional().parse(flag(ctx, 'status')), }); } } if (ctx.command === 'orgs') { if (command === 'list' || command === 'ls') { check(ctx, ctx.rest.length === 0 ? 0 : 1); return operation('/api/user/organizations'); } if (command === 'show') { check(ctx, 2); return operation(`/api/organizations/${identifier(ctx.rest[1])}`); } if (command === 'roles') { check(ctx, 1); return operation('/api/auth/orgs/permission-roles'); } if (command === 'members') { check(ctx, ctx.rest[1] === undefined ? 1 : 2); return operation(ctx.rest[1] === undefined ? '/api/auth/orgs/members' : `/api/organizations/${identifier(ctx.rest[1])}/members`); } } if (ctx.command === 'monitoring') { const fleetRoutes = { health: '/api/monitoring/health', probes: '/api/monitoring/probes', 'probe-health': '/api/monitoring/probes/health' }; if (Object.hasOwn(fleetRoutes, command)) { check(ctx, ctx.rest.length === 0 ? 0 : 1); return operation(fleetRoutes[command]); } if (command === 'metrics') { check(ctx, Math.max(ctx.rest.length, 2)); const ids = ctx.rest.slice(1).map(uuid); if (ids.length > 50) throw new Error('monitoring metrics accepts at most 50 target IDs.'); return operation('/api/monitoring/metrics', { target_ids: [...new Set(ids)].join(',') }); } if (command === 'targets' || command === 'status' || command === 'history') { check(ctx, 2, command === 'status' ? ['isLight'] : command === 'history' ? ['range', 'region', 'target-id'] : []); const path = `/api/customers/${uuid(ctx.rest[1])}/monitoring/${command}`; if (command === 'targets') return operation(path); if (command === 'status') return operation(path, { light: ctx.values.isLight ? true : undefined }); return operation(path, { range: z.enum(['24h', '7d', '30d']).optional().parse(flag(ctx, 'range')), region: z.enum(regions).optional().parse(flag(ctx, 'region')), target_id: ctx.values['target-id'] === undefined ? undefined : uuid(ctx.values['target-id']), }); } } if (ctx.command === 'support' && command === 'tickets') { check(ctx, ctx.rest.length === 0 ? 0 : 1, ['isAll', 'limit', 'customer-id']); return operation('/api/support/tickets', { includeAll: ctx.values.isAll ? true : undefined, limit: integer(ctx, 'limit', 1, 100), customerId: ctx.values['customer-id'] === undefined ? undefined : uuid(ctx.values['customer-id']), }); } throw new Error(`Unknown ${ctx.command} command: ${command}. Run \`vigilbase ${ctx.command} --help\` for usage.`); } export const handleOne = async (ctx) => { const operation = buildOneOperation(ctx); const client = await requireOneClient(ctx); const payload = await withSpinner(ctx, `Loading ${operation.label}`, () => executeOneOperation(client, operation)); if (ctx.mode === 'json') outputJson(ctx, payload); else { ctx.writer.stdout(operation.label); for (const line of formatOnePayload(payload)) ctx.writer.stdout(line); } return ok(); }; export const ONE_HELP = { customers: `Usage: vigilbase customers <command> [--json] list List visible customers (also the default) show <customer-id> Full customer details summary <customer-id> Lightweight service/integration summary services <customer-id> Service records endpoints <customer-id> Endpoint inventory contacts <customer-id> Customer contacts organization <customer-id> Organization linkage watchdog-targets <customer-id> Security/uptime targets watchdog-scans <customer-id> --limit 1..100 --offset 0..1000000 --target-id UUID reports <customer-id> Recon reports; --limit 1..100 report <customer-id> <report-id> Recon report detail investigations <customer-id> --limit 1..100 --cursor TEXT or --before ISO; --issue-id TEXT investigation <customer-id> <id> --revision 1..2147483647 contracts <customer-id> --status draft|pending|active|expired|terminated billing <customer-id> Billing overview invoices <customer-id> --limit 1..100 --status draft|open|paid|uncollectible|void subscriptions <customer-id> Stripe subscriptions projects <customer-id> Linked projects (internal operators) Customer, target, report, and investigation IDs must be UUIDs. Investigations require internal Vigil access. All operations use your One login and the API's customer/partner permissions. See ONE.md for API mappings, pagination defaults, and examples.`, orgs: `Usage: vigilbase orgs <command> [--json] list Your organizations (also the default) show <organization-id> Organization details (internal read:admin) members Current organization's members members <org-id> Another organization's members (internal read:admin) roles Current organization's assignable role templates Roles require organization management access. Organization IDs may be opaque IDs. These commands read organization context; they do not switch the active organization.`, monitoring: `Usage: vigilbase monitoring <command> [--json] health Metrics backend and alerting readiness (also the default) probes Registered probe heartbeats (internal operators) probe-health Live probe health, with database fallback metrics <target-id>... Metrics for 1..50 target UUIDs targets <customer-id> Customer's monitoring targets status <customer-id> Current status; --light skips latency/SSL provider reads history <customer-id> --range 24h|7d|30d --region CODE --target-id UUID All operations require read:monitoring and the API's tenant access checks. History defaults to 7d. Health metadata preserves unknown, partial, and unavailable states.`, support: `Usage: vigilbase support tickets [--all] [--limit 1..100] [--customer-id <uuid>] [--json] Lists current organization's open tickets; --all includes closed tickets. Vigilbase staff must choose the customer with --customer-id. The API defaults to 50 results. Requires support:access and a linked Pylon account. The response retains hasMore; the API does not expose cursor pagination.`, };