@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
347 lines (341 loc) • 19.8 kB
JavaScript
import { z } from 'zod';
import { CloudflareClient, accountResourceSchema, cloudflareScopeSchema, workspaceSectionSchema, } from '../lib/cloudflare-client.js';
import { assertCommandFlags, asArray, asRecord, ok, outputJson, requireOneClient, withSpinner, } from './helpers.js';
const usage = 'Usage: vigilbase cf [zones|zone <id>|accounts|account <id>|workspace [section]|dns <zone-id>|waf <zone-id>|speed-settings <zone-id>|audit-logs <account-id>|zero-trust <account-id>|developer-platform <account-id>|magic-transit <account-id>|analytics|checkup] [--customer-id …] [--account-id …] [--json]';
const checkupUsage = 'Usage: vigilbase cf checkup [status|start|progress <run-id>|result <run-id>] [--customer-id …] [--account-id …] [--json]';
export const CLOUDFLARE_HELP = `Usage: vigilbase cf <command> [options]
Alias: vigilbase cloudflare
Inventory and workspace:
zones List zones (default); --customer-id, --account-id, --zone-id
zone <zone-id> Zone detail; --customer-id selects live details, optional --account-id
accounts List accessible accounts; --customer-id, --account-id
account <account-id> Stored account detail; --customer-id
workspace [section] Workspace bootstrap or section; --customer-id, --account-id, --zone-id
Sections: overview, reports, zones, rule-analyzer, accounts, checkups, settings
Zone reads (--customer-id <uuid> required; IDs can use --zone-id):
dns <zone-id> DNS records, TTL and proxy state
waf <zone-id> WAF rules, expressions, analytics and suggestions
speed-settings <zone-id> Performance settings (internal users only)
Account reads (optional --customer-id; IDs can use --account-id):
audit-logs <account-id> Latest provider audit events (up to 100; service-read permission)
zero-trust <account-id> Users, Access applications, tunnels and authentication analytics
developer-platform <account-id>
Workers, Workers analytics and Pages
magic-transit <account-id> Prefixes, routes and traffic analytics
Zero Trust, Developer Platform and Magic Transit require deployment-read permission.
Analytics and checkups:
analytics --customer-id <uuid> [--account-id <id> | --zone-id <id>] [--days 1..30]
Default 7 days; live zone analytics uses the most recent 23 hours.
checkup [status] Latest completed checkup; --customer-id, --account-id
checkup start Start and persist a scan; --customer-id, --account-id
checkup progress <run-uuid> Read saved progress once
checkup result <run-uuid> Read the stored Cloudflare checkup result
checkups aliases checkup; list/show alias status; run aliases start.
Common options: --json, --one-url <origin>, --timeout <milliseconds>, --help
Customer IDs are UUIDs; account and zone IDs are 32 hexadecimal characters.
One login, tenant bindings and permissions apply to every command.
Checkup start creates a scan run. These commands do not edit provider configuration.
See CLOUDFLARE.md for full grammar, API mapping, data limits and examples.`;
function validate(schema, input) {
const result = schema.safeParse(input);
if (!result.success)
throw new Error(result.error.issues.map((issue) => issue.message).join(' '));
return result.data;
}
function dataOf(payload) {
return asRecord(payload.data) ?? payload;
}
function writeLine(ctx, value) {
// DNS contents, rule expressions, and evidence can contain terminal controls.
ctx.writer.stdout(value.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' '));
}
function fields(ctx, data) {
for (const [key, value] of Object.entries(data)) {
if (value === undefined || value === null)
continue;
writeLine(ctx, `${key.padEnd(20)} ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`);
}
}
function zones(ctx, values) {
const items = asArray(values).map(asRecord).filter((item) => item !== null);
if (!items.length)
writeLine(ctx, 'No Cloudflare zones.');
for (const zone of items) {
const plan = zone.planName ?? zone.plan_name ?? asRecord(zone.plan)?.name;
writeLine(ctx, `${String(zone.id ?? zone.zone_id)} ${String(zone.name ?? zone.zone_name).padEnd(28)} ${String(zone.status ?? '-')}${plan ? ` ${String(plan)}` : ''}`);
}
}
export const handleCloudflare = async (ctx) => {
assertCommandFlags(ctx, ['customer-id', 'account-id', 'zone-id', 'days']);
const subcommand = (ctx.rest[0] ?? 'zones').toLowerCase();
const scope = validate(cloudflareScopeSchema, {
customerId: ctx.values['customer-id'],
accountId: ctx.values['account-id'],
zoneId: ctx.values['zone-id'],
});
if (ctx.values.days !== undefined && subcommand !== 'analytics')
throw new Error('--days is supported only by cf analytics.');
if (scope.accountId && ['dns', 'waf', 'speed-settings'].includes(subcommand))
throw new Error(`cf ${subcommand} accepts a zone ID and --customer-id; --account-id is not supported.`);
if (scope.zoneId && ['accounts', 'account', ...accountResourceSchema.options, 'checkup', 'checkups'].includes(subcommand))
throw new Error(`cf ${subcommand} does not support --zone-id.`);
const args = ctx.rest.slice(1);
const noExtraArgs = (count, message = usage) => {
if (args.length > count)
throw new Error(message);
};
const emit = (payload, render) => {
if (payload.success === false)
throw new Error('Cloudflare request failed.');
if (ctx.mode === 'json')
outputJson(ctx, payload);
else
render(dataOf(payload));
return ok();
};
// Validate commands before requesting credentials or making API calls.
if (!['zones', 'zone', 'accounts', 'workspace', 'dns', 'waf', 'speed-settings', 'account', ...accountResourceSchema.options, 'analytics', 'checkup', 'checkups'].includes(subcommand)) {
throw new Error(usage);
}
let zoneId;
let accountId;
const accountResource = accountResourceSchema.safeParse(subcommand);
if (accountResource.success || subcommand === 'account') {
noExtraArgs(1);
if (args[0] && scope.accountId && args[0] !== scope.accountId)
throw new Error('The positional account ID and --account-id must match.');
accountId = args[0] ?? scope.accountId;
if (!accountId)
throw new Error(`Missing <account-id>. Usage: vigilbase cf ${subcommand} <account-id> [--customer-id <uuid>] [--json]`);
validate(cloudflareScopeSchema, { accountId });
}
else if (['dns', 'waf', 'zone', 'speed-settings'].includes(subcommand)) {
noExtraArgs(1);
if (args[0] && scope.zoneId && args[0] !== scope.zoneId)
throw new Error('The positional zone ID and --zone-id must match.');
zoneId = args[0] ?? scope.zoneId;
// Preserve the historical `cf zone` alias for the zone inventory.
if (subcommand !== 'zone' && !zoneId)
throw new Error(`Missing <zone-id>. Usage: vigilbase cf ${subcommand} <zone-id> --customer-id <uuid> [--json]`);
if (zoneId)
validate(cloudflareScopeSchema, { zoneId });
// One answers 400 to every caller that leaves the customer out of a zone read.
if (subcommand !== 'zone' && !scope.customerId)
throw new Error(`cf ${subcommand} requires --customer-id <uuid>. Find it with \`vigilbase cf accounts --json\`.`);
}
else if (!['workspace', 'checkup', 'checkups'].includes(subcommand)) {
noExtraArgs(0);
}
const section = subcommand === 'workspace'
? (noExtraArgs(1), args[0] ? validate(workspaceSectionSchema, args[0]) : undefined)
: undefined;
let days = 7;
if (subcommand === 'analytics') {
if (!scope.customerId)
throw new Error('Cloudflare analytics requires --customer-id <uuid>. Find it with `vigilbase cf accounts --json`.');
if (scope.zoneId && scope.accountId)
throw new Error('Choose either --zone-id or --account-id for analytics.');
days = validate(z.coerce.number().int().min(1).max(30), ctx.values.days ?? 7);
}
const action = (args[0] ?? 'status').toLowerCase();
let runId;
if (subcommand === 'checkup' || subcommand === 'checkups') {
if (['progress', 'result'].includes(action)) {
noExtraArgs(2, checkupUsage);
if (args[1] === undefined)
throw new Error(`Missing <run-uuid>. Usage: vigilbase cf checkup ${action} <run-uuid> [--json]`);
runId = validate(z.string().uuid('A checkup run UUID is required.'), args[1]);
if (scope.customerId || scope.accountId)
throw new Error(`cf checkup ${action} is scoped by run ID and does not accept --customer-id or --account-id.`);
}
else if (['status', 'list', 'show', 'start', 'run'].includes(action)) {
noExtraArgs(1, checkupUsage);
}
else
throw new Error(checkupUsage);
}
const one = await requireOneClient(ctx);
const cloudflare = new CloudflareClient(one);
if (subcommand === 'speed-settings') {
const payload = await withSpinner(ctx, 'Loading Cloudflare speed settings', () => cloudflare.speedSettings(zoneId, scope.customerId));
return emit(payload, (data) => {
if (!Object.keys(data).length)
writeLine(ctx, 'No speed settings returned; provider availability or permissions may limit this view.');
else
fields(ctx, data);
});
}
if (accountResource.success) {
const payload = await withSpinner(ctx, `Loading Cloudflare ${subcommand}`, () => cloudflare.accountResource(accountResource.data, accountId, scope.customerId));
return emit(payload, (data) => {
if (subcommand === 'audit-logs') {
const logs = asArray(data.logs);
if (!logs.length)
writeLine(ctx, 'No audit logs returned; provider availability or permissions may limit this view.');
for (const raw of logs) {
const log = asRecord(raw);
if (!log)
continue;
const action = asRecord(log.action);
const actor = asRecord(log.actor);
const resource = asRecord(log.resource);
writeLine(ctx, `${String(log.id ?? '-')} ${String(log.when ?? '-')} ${String(action?.type ?? '-')} ${action?.result === true ? 'success' : action?.result === false ? 'failed' : '-'} ${String(actor?.email ?? actor?.id ?? '-')} ${String(resource?.type ?? '-')} ${String(resource?.id ?? '-')}`);
}
}
else
fields(ctx, data);
});
}
if (subcommand === 'account') {
const payload = await withSpinner(ctx, 'Loading Cloudflare account', () => cloudflare.workspace({ ...scope, accountId }));
if (payload.success === false)
return emit(payload, () => { });
const matches = asArray(dataOf(payload).accounts).map(asRecord).filter((item) => item?.id === accountId);
if (!matches.length)
throw new Error('Cloudflare account not found in the selected workspace.');
const customerIds = new Set(matches.map((item) => item?.customerId));
if (customerIds.size > 1)
throw new Error('This Cloudflare account is linked to multiple customers. Specify --customer-id.');
const account = matches.find((item) => item?.status === 'active' || item?.status === 'trialing') ?? matches[0];
return emit({ success: true, data: account }, (data) => fields(ctx, data));
}
if (subcommand === 'zones' || subcommand === 'zone') {
if (zoneId && scope.customerId) {
const payload = await withSpinner(ctx, 'Loading live Cloudflare zone', () => cloudflare.zoneDetail(scope.customerId, zoneId));
if (payload.success === false)
return emit(payload, () => { });
const zone = dataOf(payload);
if (zone.id !== zoneId)
throw new Error('The server returned a different Cloudflare zone.');
if (scope.accountId && asRecord(zone.account)?.id !== scope.accountId)
throw new Error('Cloudflare zone does not belong to the selected account.');
return emit(payload, (data) => fields(ctx, data));
}
let payload = await withSpinner(ctx, 'Loading Cloudflare zones', () => scope.customerId || scope.accountId || scope.zoneId || zoneId
? cloudflare.workspace({ ...scope, zoneId: zoneId ?? scope.zoneId }, 'zones')
: one.getCloudflareZones());
if (payload.success === false)
return emit(payload, () => { });
if (scope.accountId || scope.zoneId) {
const data = dataOf(payload);
const selectedZones = asArray(data.zones).filter((raw) => {
const zone = asRecord(raw);
return zone && (!scope.accountId || (zone.accountId ?? zone.account_id ?? zone.cloudflare_account_id) === scope.accountId)
&& (!scope.zoneId || (zone.id ?? zone.zone_id) === scope.zoneId);
});
payload = payload.data ? { ...payload, data: { ...data, zones: selectedZones } } : { ...payload, zones: selectedZones };
}
if (zoneId) {
const zone = asArray(dataOf(payload).zones).map(asRecord).find((item) => (item?.id ?? item?.zone_id) === zoneId);
if (!zone)
throw new Error('Cloudflare zone not found in the selected workspace.');
return emit({ success: true, data: zone }, (data) => fields(ctx, data));
}
return emit(payload, (data) => zones(ctx, data.zones));
}
if (subcommand === 'accounts' || subcommand === 'workspace') {
let payload = await withSpinner(ctx, 'Loading Cloudflare workspace', () => cloudflare.workspace(scope, section));
if (subcommand === 'accounts' && scope.accountId) {
const data = dataOf(payload);
const accounts = asArray(data.accounts).filter((raw) => asRecord(raw)?.id === scope.accountId);
payload = { ...payload, data: { ...data, accounts } };
}
return emit(payload, (data) => {
if (subcommand === 'accounts') {
const accounts = asArray(data.accounts);
if (!accounts.length)
writeLine(ctx, 'No Cloudflare accounts.');
for (const raw of accounts) {
const account = asRecord(raw);
if (account)
writeLine(ctx, `${String(account.id)} ${String(account.name).padEnd(28)} ${String(account.status)} ${String(account.zoneCount ?? 0)} zones${account.customerId ? ` customer ${String(account.customerId)}` : ''}`);
}
}
else
fields(ctx, data);
});
}
if (subcommand === 'dns' || subcommand === 'waf') {
const payload = await withSpinner(ctx, `Loading Cloudflare ${subcommand.toUpperCase()}`, () => cloudflare.zoneRecords(subcommand, zoneId, scope.customerId));
return emit(payload, (data) => {
const records = asArray(data[subcommand === 'dns' ? 'records' : 'rules']);
if (!records.length)
writeLine(ctx, `No ${subcommand === 'dns' ? 'DNS records' : 'WAF rules'}.`);
for (const raw of records) {
const record = asRecord(raw);
if (!record)
continue;
if (subcommand === 'dns') {
writeLine(ctx, `${String(record.id)} ${String(record.type).padEnd(6)} ${String(record.name).padEnd(28)} ${String(record.content)} TTL ${String(record.ttl)} ${record.proxied ? 'proxied' : 'DNS only'}`);
}
else {
writeLine(ctx, `${String(record.id)} ${String(record.action).padEnd(18)} ${record.paused ? 'paused' : 'active'} ${String(record.description ?? '')}`);
const expression = asRecord(record.filter)?.expression;
if (expression)
writeLine(ctx, ` ${String(expression)}`);
}
}
if (subcommand === 'waf') {
writeLine(ctx, `Rule analytics: ${data.analyticsAvailable ? 'available' : 'unavailable'}`);
for (const raw of asArray(data.suggestions)) {
const suggestion = asRecord(raw);
if (suggestion)
writeLine(ctx, `[${String(suggestion.severity)}] ${String(suggestion.title)}: ${String(suggestion.recommendation)}`);
}
}
});
}
if (subcommand === 'analytics') {
const payload = await withSpinner(ctx, 'Loading Cloudflare analytics', () => cloudflare.analytics(scope, days));
return emit(payload, (data) => {
if (scope.zoneId)
writeLine(ctx, 'Live zone analytics use the most recent 23 hours; stored usage may cover less.');
fields(ctx, asRecord(data.analytics) ?? data);
});
}
if (action === 'progress') {
const payload = await withSpinner(ctx, 'Loading Cloudflare checkup progress', () => cloudflare.checkupProgress(runId));
return emit(payload, (data) => {
writeLine(ctx, `checkup ${String(data.status)} ${String(data.progressPct ?? 0)}%`);
for (const raw of asArray(data.events)) {
const event = asRecord(raw);
if (event)
writeLine(ctx, `${String(event.createdAt ?? '')} ${String(event.message ?? '')}`);
}
if (data.errorMessage)
writeLine(ctx, `error ${String(data.errorMessage)}`);
});
}
if (action === 'result') {
const payload = await withSpinner(ctx, 'Loading Cloudflare checkup result', () => one.getToolRun(runId));
const run = dataOf(payload);
if (run.tool_name !== 'cloudflare-checkup')
throw new Error('This run is not a Cloudflare checkup.');
return emit(payload, (data) => {
writeLine(ctx, `checkup ${String(data.status)}`);
if (data.grade)
writeLine(ctx, `grade ${String(data.grade)} ${String(data.grade_score ?? '')}`);
if (data.results)
fields(ctx, asRecord(data.results) ?? { results: data.results });
else
writeLine(ctx, 'No result is available yet.');
});
}
const isStart = action === 'start' || action === 'run';
const payload = await withSpinner(ctx, isStart ? 'Starting Cloudflare checkup' : 'Loading Cloudflare checkup', () => cloudflare.checkup(scope, isStart));
return emit(payload, (data) => {
if (isStart) {
writeLine(ctx, `started ${String(data.toolRunId ?? data.id ?? 'ok')} ${String(data.status ?? 'running')}`);
return;
}
const run = asRecord(data.latestRun);
const report = asRecord(data.report);
writeLine(ctx, `checkup ${data.requiresSetup ? 'setup required' : String(run?.status ?? (report ? 'completed' : 'no completed runs'))}`);
if (run?.id)
writeLine(ctx, `run ${String(run.id)}`);
if (run?.grade ?? report?.grade)
writeLine(ctx, `grade ${String(run?.grade ?? report?.grade)}`);
if (report)
fields(ctx, report);
});
};