@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
273 lines (272 loc) • 12.8 kB
JavaScript
import { z } from 'zod';
import { toolNameSchema, validateArguments } from './legacy-validation.js';
import { NetworkError } from '../lib/network.js';
import { PUBLIC_TOOLS_PATH, VigilbaseApiError } from '../lib/client.js';
import { getOneAccessToken, getOptionalOneAccessToken, hasUsableSignIn, readCredentials } from '../auth/index.js';
import { SignInUnsupportedError } from '../auth/store.js';
import { formatExecuteResult, formatToolDescription, formatToolsList, isSuccessfulScan, } from '../lib/format.js';
import { isOneAuthRequiredTool, isOneExecutableTool, ONE_AUTH_REQUIRED_TOOLS, resolveToolDefinition, VigilbaseOneClient, } from '../lib/one-client.js';
import { EXIT, ok, outputJson, withSpinner, } from './helpers.js';
const ONE_SIGN_IN_REQUIRED = 'This tool needs a Vigilbase One sign-in: run `vigilbase login`.';
/**
* For --via one and auth-required tools only: any sign-in problem fails. Specific errors keep their
* message; those that don't already point to `vigilbase login` get it as their hint.
*/
async function requireOneSignIn(config) {
let accessToken;
try {
accessToken = await getOneAccessToken(config);
}
catch (error) {
// Windows cannot sign in at all, so pointing to login would mislead.
if (error instanceof Error && !(error instanceof SignInUnsupportedError) && !error.message.includes('`vigilbase login`')) {
Object.assign(error, { hint: ONE_SIGN_IN_REQUIRED });
}
throw error;
}
if (!accessToken)
throw new Error(ONE_SIGN_IN_REQUIRED);
return accessToken;
}
/** Public tools work without login. Auth-required tools need Vigilbase One. */
export const handleList = async (ctx) => {
validateArguments(ctx, 0);
const catalog = await withSpinner(ctx, 'Loading tool catalog', () => ctx.publicClient.listTools());
// Offline: list never refreshes, so it cannot leave a refresh half-done.
const authenticated = await hasUsableSignIn(ctx.config, ctx.writer.stderr);
if (ctx.mode === 'json') {
outputJson(ctx, {
...catalog,
authenticated,
authRequiredTools: [...ONE_AUTH_REQUIRED_TOOLS],
oneExecutableTools: authenticated
? [
...new Set([
...catalog.tools.map((tool) => tool.toolName),
...ONE_AUTH_REQUIRED_TOOLS,
]),
]
: catalog.tools.map((tool) => tool.toolName),
});
}
else {
formatToolsList(catalog, ctx.mode, ctx.writer, {
authRequiredTools: ONE_AUTH_REQUIRED_TOOLS,
isAuthenticated: authenticated,
});
}
return ok();
};
export const handleRun = async (ctx) => {
const usage = ctx.command === 'headers' ? 'vigilbase headers <url>'
: ctx.command === 'email' ? 'vigilbase email <domain>'
: 'vigilbase run <tool> <target>';
validateArguments(ctx, ctx.command === 'run' ? 2 : 1, ['via', 'isPublic'], usage);
let toolName;
let target;
if (ctx.command === 'headers') {
toolName = 'security-headers';
target = ctx.rest[0] ?? '';
}
else if (ctx.command === 'email') {
toolName = 'email-security';
target = ctx.rest[0] ?? '';
}
else {
toolName = ctx.rest[0] ?? '';
target = ctx.rest[1] ?? '';
}
if (!toolName || !target)
throw new Error(`Usage: ${usage}`);
toolName = toolNameSchema.parse(toolName);
target = z.string().trim().min(1).max(2048).refine((value) => !/[\u0000-\u001f\u007f]/.test(value), 'Target must not contain control characters.').parse(target);
const via = ctx.values.via;
if (ctx.values.isPublic && via === 'one') {
throw new Error('--public cannot be combined with --via one.');
}
if (via && via !== 'one' && via !== 'public') {
throw new Error('--via must be one of: public, one');
}
const forcePublic = Boolean(ctx.values.isPublic) || via === 'public';
const forceOne = via === 'one';
// Public tools stay free unless forced onto One. Auth-required tools need login.
// The public route is anonymous and never reads the credential store.
const useOne = !forcePublic && (forceOne || isOneAuthRequiredTool(toolName));
if (useOne) {
const accessToken = await requireOneSignIn(ctx.config);
if (!isOneExecutableTool(toolName)) {
throw new Error(`Tool "${toolName}" is not executable via Vigilbase One CLI yet.`);
}
const oneClient = new VigilbaseOneClient(ctx.config, accessToken, true);
const oneResult = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => oneClient.executeTool(toolName, target));
const unassessed = oneResult.code === 'scan_unassessed';
const toolRunId = oneResult.data?.toolRunId;
const result = {
success: oneResult.success === true,
toolName,
target,
grade: oneResult.data?.grade ?? null,
gradeScore: oneResult.data?.gradeScore ?? null,
// Only a completed scan, or one the scanner could not assess, carries the scanner's result.
result: oneResult.success === true || unassessed ? oneResult.data?.result ?? null : null,
// One's error text is server text; a failed scan prints the CLI's own message.
error: null,
uiUrl: `${ctx.config.oneBaseUrl}/dashboard/tools/${toolName}`,
...(unassessed ? { status: 422, code: 'scan_unassessed', ...(toolRunId ? { toolRunId } : {}) } : {}),
};
formatExecuteResult(result, ctx.mode, ctx.writer);
return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
}
const result = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => ctx.publicClient.executeTool(toolName, target));
formatExecuteResult(result, ctx.mode, ctx.writer);
return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
};
export const handleDescribe = async (ctx) => {
validateArguments(ctx, 1, ['isNoSkill'], 'vigilbase describe <tool>');
toolNameSchema.parse(ctx.rest[0]);
const toolName = ctx.rest[0];
if (!toolName)
throw new Error('Usage: vigilbase describe <tool>');
const catalog = await withSpinner(ctx, 'Loading tool catalog', () => ctx.publicClient.listTools());
const tool = resolveToolDefinition(toolName, catalog.tools, ctx.config.oneBaseUrl);
if (!tool) {
throw new Error(`Unknown tool "${toolName}". Run \`vigilbase list\` for the allowlist.`);
}
let skillMarkdown;
if (!ctx.values.isNoSkill) {
try {
skillMarkdown = await withSpinner(ctx, 'Fetching skill docs', () => ctx.publicClient.fetchSkillMarkdown(tool.skillUrl));
}
catch {
// Auth-required tools may not publish public skill docs yet.
}
}
formatToolDescription(tool, ctx.mode, ctx.writer, skillMarkdown);
return ok();
};
export const handleSkill = async (ctx) => {
validateArguments(ctx, 1, [], 'vigilbase skill <tool>');
toolNameSchema.parse(ctx.rest[0]);
const toolName = ctx.rest[0];
if (!toolName)
throw new Error('Usage: vigilbase skill <tool>');
const catalog = await withSpinner(ctx, 'Loading tool catalog', () => ctx.publicClient.listTools());
const definition = resolveToolDefinition(toolName, catalog.tools, ctx.config.oneBaseUrl);
if (!definition) {
throw new Error(`Unknown tool "${toolName}". Run \`vigilbase list\` for the allowlist.`);
}
const skillMarkdown = await withSpinner(ctx, 'Fetching skill docs', () => ctx.publicClient.fetchSkillMarkdown(definition.skillUrl));
if (ctx.mode === 'json') {
outputJson(ctx, {
toolName: definition.toolName,
skillUrl: definition.skillUrl,
content: skillMarkdown,
});
}
else {
ctx.writer.stdout(skillMarkdown.trimEnd());
}
return ok();
};
export const handleBadge = async (ctx) => {
validateArguments(ctx, 1, ['style'], 'vigilbase badge <domain>');
const domain = ctx.rest[0];
if (!domain)
throw new Error('Usage: vigilbase badge <domain>');
const styleRaw = ctx.values.style ?? 'default';
if (styleRaw !== 'default' && styleRaw !== 'compact' && styleRaw !== 'mini') {
throw new Error('--style must be one of: default, compact, mini');
}
const url = ctx.publicClient.badgeUrl(domain, styleRaw);
if (ctx.mode === 'json') {
outputJson(ctx, { domain, style: styleRaw, url });
}
else {
ctx.writer.stdout(url);
}
return ok();
};
export const handleDoctor = async (ctx) => {
validateArguments(ctx, 0);
let oneCheckUrl = `${ctx.config.oneBaseUrl}/api/auth/get-session`;
// A sign-in problem fails its own check, and One connectivity is then checked anonymously.
// The saved sign-in may refresh, with the configured timeout like any One command.
const signIn = getOptionalOneAccessToken(ctx.config);
const [publicResult, oneResult, signInResult] = await withSpinner(ctx, 'Checking public API and Vigilbase One', () => Promise.allSettled([
ctx.publicClient.listTools(),
(async () => {
const { accessToken } = await signIn;
const saved = accessToken ? await readCredentials(ctx.config) : null;
// An Identity session is checked at the userinfo endpoint its issuer's discovery names.
if (saved?.version === 2)
oneCheckUrl = `${saved.one.issuer}/.well-known/openid-configuration`;
// Signed out, the check stays anonymous and never reads the credential store.
const client = new VigilbaseOneClient(ctx.config, accessToken ?? '', Boolean(accessToken));
try {
const session = await client.getSession();
return { authenticated: Boolean(session.user?.id) };
}
catch (error) {
// A signed-out session is a valid connectivity result.
if (error instanceof VigilbaseApiError && error.status === 401) {
return { authenticated: false };
}
throw error;
}
finally {
if (client.userinfoEndpoint)
oneCheckUrl = client.userinfoEndpoint;
}
})(),
signIn.then(({ problem }) => {
if (problem)
throw new Error(problem);
}),
]));
const checkResult = (result, url) => {
if (result.status === 'fulfilled')
return { ok: true, ...(url ? { url } : {}) };
const error = result.reason;
return {
ok: false,
...(url ? { url } : {}),
error: error instanceof Error ? error.message : 'Connectivity check failed.',
...(error instanceof VigilbaseApiError || error instanceof NetworkError ? { status: error.status } : {}),
...(error instanceof NetworkError ? { code: error.code } : {}),
};
};
const checks = {
publicApi: checkResult(publicResult, `${ctx.config.publicBaseUrl}${PUBLIC_TOOLS_PATH}`),
one: checkResult(oneResult, oneCheckUrl),
signIn: checkResult(signInResult),
};
const isOk = checks.publicApi.ok && checks.one.ok && checks.signIn.ok;
const firstFailure = Object.values(checks).find((check) => !check.ok);
const catalog = publicResult.status === 'fulfilled' ? publicResult.value : null;
const payload = {
ok: isOk,
success: isOk,
...(firstFailure ? { error: firstFailure.error, status: firstFailure.status } : {}),
publicBaseUrl: ctx.config.publicBaseUrl,
oneBaseUrl: ctx.config.oneBaseUrl,
clientId: ctx.config.clientId,
authenticated: oneResult.status === 'fulfilled' && oneResult.value.authenticated,
toolCount: catalog?.tools.length ?? 0,
tools: catalog?.tools.map((tool) => tool.toolName) ?? [],
authRequiredTools: [...ONE_AUTH_REQUIRED_TOOLS],
openapi: catalog?.openapi ?? null,
skillsIndex: catalog?.skillsIndex ?? null,
rateLimit: catalog?.rateLimit ?? null,
checks,
};
if (ctx.mode === 'json') {
outputJson(ctx, payload);
}
else {
for (const [label, check] of Object.entries(checks)) {
ctx.writer.stdout(`${check.ok ? 'ok' : 'fail'} ${label}${check.url ? ` ${check.url}` : ''}${check.error ? ` — ${check.error}` : ''}`);
}
ctx.writer.stdout(`auth=${payload.authenticated ? 'in' : 'out'} tools=${payload.tools.join(',') || 'unavailable'}`);
}
return isOk ? ok() : EXIT.ERROR;
};