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