@vigilbase/cli 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,23 @@
1
1
  import { z } from 'zod';
2
2
  import { assertCommandFlags } from './helpers.js';
3
- export function validateArguments(ctx, count, flags = []) {
4
- if (ctx.rest.length !== count)
3
+ /**
4
+ * Exactly `count` positional arguments. When some are missing, they are named from the trailing `<placeholders>`
5
+ * of `usage` (for example `vigilbase run <tool> <target>`), so `usage` lists only positional placeholders.
6
+ */
7
+ export function validateArguments(ctx, count, flags = [], usage) {
8
+ if (ctx.rest.length < count)
9
+ throw new Error(missingArguments(ctx.command, count - ctx.rest.length, usage));
10
+ if (ctx.rest.length > count)
5
11
  throw new Error(`Unexpected arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`);
6
12
  assertCommandFlags(ctx, flags);
7
13
  }
14
+ /** "Missing <target>. Usage: vigilbase headers <url>", or a pointer to --help without a usage. */
15
+ export function missingArguments(command, missingCount, usage) {
16
+ const missing = usage?.match(/<[^>]+>/g)?.slice(-missingCount) ?? [];
17
+ if (!usage || missing.length !== missingCount)
18
+ return `Missing arguments. Run \`vigilbase ${command} --help\` for usage.`;
19
+ return `Missing ${missing.join(' and ')}. Usage: ${usage}`;
20
+ }
8
21
  export function limitValue(raw, fallback, max) {
9
22
  if (raw === undefined)
10
23
  return fallback;
@@ -134,7 +134,8 @@ function parse(schema, value) {
134
134
  function parseLocalOptions(ctx, subcommand, args) {
135
135
  const takesFile = subcommand === 'inspect' || subcommand === 'submit';
136
136
  if (takesFile ? args.length !== 1 : args.length !== 0) {
137
- throw new Error(takesFile ? `Usage: vigilbase cloudflare-checkup ${subcommand} <file>` : usage);
137
+ const fileUsage = `Usage: vigilbase cloudflare-checkup ${subcommand} <file>`;
138
+ throw new Error(!takesFile ? usage : args.length === 0 ? `Missing <file>. ${fileUsage}` : fileUsage);
138
139
  }
139
140
  const values = ctx.values;
140
141
  const requested = parse(z.enum(['auto', 'profile', 'env', 'default'], { message: '--cf-auth must be auto, profile, env or default.' }), values['cf-auth'] ?? (subcommand === 'run' ? 'auto' : 'profile'));
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { executeOneOperation, formatOnePayload } from '../lib/one-operations.js';
3
3
  import { assertCommandFlags, ok, outputJson, requireOneClient, withSpinner } from './helpers.js';
4
+ import { missingArguments } from './legacy-validation.js';
4
5
  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'];
5
6
  function flag(ctx, name, max = 2048) {
6
7
  const value = ctx.values[name];
@@ -26,8 +27,28 @@ function identifier(value) {
26
27
  throw new Error('An ID containing only letters, numbers, underscores, or hyphens is required.');
27
28
  return result.data;
28
29
  }
30
+ const customerSubcommands = ['show', 'summary', 'services', 'endpoints', 'contacts', 'organization', 'watchdog-targets', 'watchdog-scans', 'reports', 'investigations', 'contracts', 'billing', 'invoices', 'subscriptions', 'projects'];
31
+ /** Positional placeholders of each subcommand (as in ONE_HELP), so a missing argument is named. */
32
+ const POSITIONALS = {
33
+ customers: {
34
+ ...Object.fromEntries(customerSubcommands.map((command) => [command, '<customer-id>'])),
35
+ report: '<customer-id> <report-id>',
36
+ investigation: '<customer-id> <investigation-id>',
37
+ },
38
+ orgs: { show: '<organization-id>' },
39
+ monitoring: { metrics: '<target-id>...', targets: '<customer-id>', status: '<customer-id>', history: '<customer-id>' },
40
+ };
41
+ function positionalUsage(ctx) {
42
+ const [command] = ctx.rest;
43
+ const placeholders = POSITIONALS[ctx.command];
44
+ if (command === undefined || !placeholders || !Object.hasOwn(placeholders, command))
45
+ return undefined;
46
+ return `vigilbase ${ctx.command} ${command} ${placeholders[command]}`;
47
+ }
29
48
  function check(ctx, arity, flags = []) {
30
- if (ctx.rest.length !== arity)
49
+ if (ctx.rest.length < arity)
50
+ throw new Error(missingArguments(ctx.command, arity - ctx.rest.length, positionalUsage(ctx)));
51
+ if (ctx.rest.length > arity)
31
52
  throw new Error(`Unexpected arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`);
32
53
  assertCommandFlags(ctx, flags);
33
54
  }
@@ -114,8 +135,10 @@ export function buildOneOperation(ctx) {
114
135
  return operation(fleetRoutes[command]);
115
136
  }
116
137
  if (command === 'metrics') {
117
- check(ctx, ctx.rest.length);
118
- const ids = z.array(z.string().uuid()).min(1).max(50).parse(ctx.rest.slice(1));
138
+ check(ctx, Math.max(ctx.rest.length, 2));
139
+ const ids = ctx.rest.slice(1).map(uuid);
140
+ if (ids.length > 50)
141
+ throw new Error('monitoring metrics accepts at most 50 target IDs.');
119
142
  return operation('/api/monitoring/metrics', { target_ids: [...new Set(ids)].join(',') });
120
143
  }
121
144
  if (command === 'targets' || command === 'status' || command === 'history') {
@@ -133,8 +156,11 @@ export function buildOneOperation(ctx) {
133
156
  }
134
157
  }
135
158
  if (ctx.command === 'support' && command === 'tickets') {
136
- check(ctx, ctx.rest.length === 0 ? 0 : 1, ['isAll', 'limit']);
137
- return operation('/api/support/tickets', { includeAll: ctx.values.isAll ? true : undefined, limit: integer(ctx, 'limit', 1, 100) });
159
+ check(ctx, ctx.rest.length === 0 ? 0 : 1, ['isAll', 'limit', 'customer-id']);
160
+ return operation('/api/support/tickets', {
161
+ includeAll: ctx.values.isAll ? true : undefined, limit: integer(ctx, 'limit', 1, 100),
162
+ customerId: ctx.values['customer-id'] === undefined ? undefined : uuid(ctx.values['customer-id']),
163
+ });
138
164
  }
139
165
  throw new Error(`Unknown ${ctx.command} command: ${command}. Run \`vigilbase ${ctx.command} --help\` for usage.`);
140
166
  }
@@ -192,8 +218,9 @@ These commands read organization context; they do not switch the active organiza
192
218
  history <customer-id> --range 24h|7d|30d --region CODE --target-id UUID
193
219
  All operations require read:monitoring and the API's tenant access checks.
194
220
  History defaults to 7d. Health metadata preserves unknown, partial, and unavailable states.`,
195
- support: `Usage: vigilbase support tickets [--all] [--limit 1..100] [--json]
221
+ support: `Usage: vigilbase support tickets [--all] [--limit 1..100] [--customer-id <uuid>] [--json]
196
222
  Lists current organization's open tickets; --all includes closed tickets.
223
+ Vigilbase staff must choose the customer with --customer-id.
197
224
  The API defaults to 50 results. Requires support:access and a linked Pylon account.
198
225
  The response retains hasMore; the API does not expose cursor pagination.`,
199
226
  };
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { toolNameSchema, validateArguments } from './legacy-validation.js';
3
3
  import { NetworkError } from '../lib/network.js';
4
- import { VigilbaseApiError } from '../lib/client.js';
4
+ import { PUBLIC_TOOLS_PATH, VigilbaseApiError } from '../lib/client.js';
5
5
  import { getOneAccessToken, getOptionalOneAccessToken, hasUsableSignIn, readCredentials } from '../auth/index.js';
6
6
  import { SignInUnsupportedError } from '../auth/store.js';
7
- import { formatExecuteResult, formatToolDescription, formatToolsList, } from '../lib/format.js';
7
+ import { formatExecuteResult, formatToolDescription, formatToolsList, isSuccessfulScan, } from '../lib/format.js';
8
8
  import { isOneAuthRequiredTool, isOneExecutableTool, ONE_AUTH_REQUIRED_TOOLS, resolveToolDefinition, VigilbaseOneClient, } from '../lib/one-client.js';
9
9
  import { EXIT, ok, outputJson, withSpinner, } from './helpers.js';
10
10
  const ONE_SIGN_IN_REQUIRED = 'This tool needs a Vigilbase One sign-in: run `vigilbase login`.';
@@ -58,7 +58,10 @@ export const handleList = async (ctx) => {
58
58
  return ok();
59
59
  };
60
60
  export const handleRun = async (ctx) => {
61
- validateArguments(ctx, ctx.command === 'headers' || ctx.command === 'email' ? 1 : 2, ['via', 'isPublic']);
61
+ const usage = ctx.command === 'headers' ? 'vigilbase headers <url>'
62
+ : ctx.command === 'email' ? 'vigilbase email <domain>'
63
+ : 'vigilbase run <tool> <target>';
64
+ validateArguments(ctx, ctx.command === 'run' ? 2 : 1, ['via', 'isPublic'], usage);
62
65
  let toolName;
63
66
  let target;
64
67
  if (ctx.command === 'headers') {
@@ -73,11 +76,8 @@ export const handleRun = async (ctx) => {
73
76
  toolName = ctx.rest[0] ?? '';
74
77
  target = ctx.rest[1] ?? '';
75
78
  }
76
- if (!toolName || !target) {
77
- throw new Error(ctx.command === 'run'
78
- ? 'Usage: vigilbase run <tool> <target>'
79
- : `Usage: vigilbase ${ctx.command} <target>`);
80
- }
79
+ if (!toolName || !target)
80
+ throw new Error(`Usage: ${usage}`);
81
81
  toolName = toolNameSchema.parse(toolName);
82
82
  target = z.string().trim().min(1).max(2048).refine((value) => !/[\u0000-\u001f\u007f]/.test(value), 'Target must not contain control characters.').parse(target);
83
83
  const via = ctx.values.via;
@@ -99,24 +99,30 @@ export const handleRun = async (ctx) => {
99
99
  }
100
100
  const oneClient = new VigilbaseOneClient(ctx.config, accessToken, true);
101
101
  const oneResult = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => oneClient.executeTool(toolName, target));
102
- formatExecuteResult({
103
- success: Boolean(oneResult.success),
102
+ const unassessed = oneResult.code === 'scan_unassessed';
103
+ const toolRunId = oneResult.data?.toolRunId;
104
+ const result = {
105
+ success: oneResult.success === true,
104
106
  toolName,
105
107
  target,
106
108
  grade: oneResult.data?.grade ?? null,
107
109
  gradeScore: oneResult.data?.gradeScore ?? null,
108
- result: oneResult.data?.result ?? null,
109
- error: oneResult.error ?? null,
110
+ // Only a completed scan, or one the scanner could not assess, carries the scanner's result.
111
+ result: oneResult.success === true || unassessed ? oneResult.data?.result ?? null : null,
112
+ // One's error text is server text; a failed scan prints the CLI's own message.
113
+ error: null,
110
114
  uiUrl: `${ctx.config.oneBaseUrl}/dashboard/tools/${toolName}`,
111
- }, ctx.mode, ctx.writer);
112
- return oneResult.success ? ok() : EXIT.ERROR;
115
+ ...(unassessed ? { status: 422, code: 'scan_unassessed', ...(toolRunId ? { toolRunId } : {}) } : {}),
116
+ };
117
+ formatExecuteResult(result, ctx.mode, ctx.writer);
118
+ return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
113
119
  }
114
120
  const result = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => ctx.publicClient.executeTool(toolName, target));
115
121
  formatExecuteResult(result, ctx.mode, ctx.writer);
116
- return result.success ? ok() : EXIT.ERROR;
122
+ return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
117
123
  };
118
124
  export const handleDescribe = async (ctx) => {
119
- validateArguments(ctx, 1, ['isNoSkill']);
125
+ validateArguments(ctx, 1, ['isNoSkill'], 'vigilbase describe <tool>');
120
126
  toolNameSchema.parse(ctx.rest[0]);
121
127
  const toolName = ctx.rest[0];
122
128
  if (!toolName)
@@ -139,7 +145,7 @@ export const handleDescribe = async (ctx) => {
139
145
  return ok();
140
146
  };
141
147
  export const handleSkill = async (ctx) => {
142
- validateArguments(ctx, 1);
148
+ validateArguments(ctx, 1, [], 'vigilbase skill <tool>');
143
149
  toolNameSchema.parse(ctx.rest[0]);
144
150
  const toolName = ctx.rest[0];
145
151
  if (!toolName)
@@ -163,7 +169,7 @@ export const handleSkill = async (ctx) => {
163
169
  return ok();
164
170
  };
165
171
  export const handleBadge = async (ctx) => {
166
- validateArguments(ctx, 1, ['style']);
172
+ validateArguments(ctx, 1, ['style'], 'vigilbase badge <domain>');
167
173
  const domain = ctx.rest[0];
168
174
  if (!domain)
169
175
  throw new Error('Usage: vigilbase badge <domain>');
@@ -230,7 +236,7 @@ export const handleDoctor = async (ctx) => {
230
236
  };
231
237
  };
232
238
  const checks = {
233
- publicApi: checkResult(publicResult, `${ctx.config.publicBaseUrl}/api/agent/tools`),
239
+ publicApi: checkResult(publicResult, `${ctx.config.publicBaseUrl}${PUBLIC_TOOLS_PATH}`),
234
240
  one: checkResult(oneResult, oneCheckUrl),
235
241
  signIn: checkResult(signInResult),
236
242
  };
@@ -10,7 +10,7 @@ export const handleRuns = async (ctx) => {
10
10
  toolName = toolNameSchema.optional().parse(ctx.values.tool);
11
11
  }
12
12
  else if (subcommand === 'get' || subcommand === 'show') {
13
- validateArguments(ctx, 2);
13
+ validateArguments(ctx, 2, [], `vigilbase runs ${subcommand} <UUID>`);
14
14
  z.string().uuid().parse(ctx.rest[1]);
15
15
  }
16
16
  else if (z.string().uuid().safeParse(subcommand).success)
@@ -2,4 +2,4 @@ import { type SecurityRequest } from '../lib/security-client.js';
2
2
  import { type CommandContext, type CommandHandler } from './helpers.js';
3
3
  export declare function buildSecurityRequest(ctx: CommandContext): SecurityRequest;
4
4
  export declare const handleSecurity: CommandHandler;
5
- export declare const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --limit --asset-id --provider --dataset --search\n --start --end --sort --frs-min --cursor\n audit --limit --cursor --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips|bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>, --customer-id <One customer UUID> (internal View-as).\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for every endpoint, supported value, alias, and pagination contract.\n";
5
+ export declare const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --limit --asset-id --provider --dataset --search\n --start --end --sort --frs-min --cursor\n audit --limit --cursor --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id <asset-id>\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips|bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>, --customer-id <One customer UUID> (internal View-as).\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for every endpoint, supported value, alias, and pagination contract.\n";
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { SecurityClient } from '../lib/security-client.js';
3
3
  import { assertCommandFlags, asArray, asRecord, ok, outputJson, requireOneClient, withSpinner } from './helpers.js';
4
+ import { missingArguments } from './legacy-validation.js';
4
5
  const statuses = ['open', 'active', 'acknowledged', 'investigating', 'mitigating', 'mitigated', 'resolved', 'false_positive'];
5
6
  const updateStatuses = ['acknowledged', 'mitigating', 'resolved', 'false_positive'];
6
7
  function textFlag(ctx, flag) {
@@ -20,8 +21,11 @@ function integer(ctx, flag, max, fallback) {
20
21
  function allowed(ctx, flags) {
21
22
  assertCommandFlags(ctx, [...flags, 'customer-id']);
22
23
  }
23
- function arity(ctx, length) {
24
- if (ctx.rest.length !== length)
24
+ /** `usage` follows `vigilbase security` and ends with the positional placeholders, so a missing one is named. */
25
+ function arity(ctx, length, usage) {
26
+ if (ctx.rest.length < length)
27
+ throw new Error(missingArguments('security', length - ctx.rest.length, usage && `vigilbase security ${usage}`));
28
+ if (ctx.rest.length > length)
25
29
  throw new Error('Unexpected security arguments. Run `vigilbase security --help` for usage.');
26
30
  }
27
31
  function uuid(value) {
@@ -92,7 +96,7 @@ function extendedSecurityRequest(ctx, scope) {
92
96
  const views = { 'device-health': 'deviceHealth', 'app-tests': 'appTests', gateway: 'gateway', access: 'access', sessions: 'sessions', 'device-posture': 'devicePosture' };
93
97
  if (!sub || !views[sub])
94
98
  throw new Error('Choose a zero-trust command: device-health, app-tests, gateway, access, sessions, device-posture.');
95
- arity(ctx, sub === 'gateway' ? 3 : 2);
99
+ arity(ctx, sub === 'gateway' ? 3 : 2, 'zero-trust gateway <dns|http|network>');
96
100
  const query = window();
97
101
  if (sub === 'gateway')
98
102
  query.surface = z.enum(['dns', 'http', 'network']).parse(id);
@@ -128,28 +132,28 @@ function extendedSecurityRequest(ctx, scope) {
128
132
  }
129
133
  if (group === 'assets' && sub !== undefined) {
130
134
  const routes = {
131
- allowlist: [`/domains/${uuid(id)}/allowlist`, 'ipList'], blocklist: [`/domains/${uuid(id)}/blocklist`, 'ipList'],
132
- remediation: [`/assets/${uuid(id)}/remediation-settings`, 'assetRemediation'], autonomy: [`/assets/${uuid(id)}/vigil-autonomy`, 'autonomy'],
133
- datasets: [`/fabric/assets/${uuid(id)}/datasets`, 'datasets'], graph: [`/fabric/assets/${uuid(id)}/graph`, 'graph'],
135
+ allowlist: (assetId) => [`/domains/${assetId}/allowlist`, 'ipList'], blocklist: (assetId) => [`/domains/${assetId}/blocklist`, 'ipList'],
136
+ remediation: (assetId) => [`/assets/${assetId}/remediation-settings`, 'assetRemediation'], autonomy: (assetId) => [`/assets/${assetId}/vigil-autonomy`, 'autonomy'],
137
+ datasets: (assetId) => [`/fabric/assets/${assetId}/datasets`, 'datasets'], graph: (assetId) => [`/fabric/assets/${assetId}/graph`, 'graph'],
134
138
  };
135
- const route = routes[sub];
139
+ const route = Object.hasOwn(routes, sub) ? routes[sub] : undefined;
136
140
  if (!route)
137
141
  throw new Error(`Unknown assets command: ${sub}`);
138
- arity(ctx, 3);
142
+ arity(ctx, 3, `assets ${sub} <asset-id>`);
139
143
  if (sub !== 'graph')
140
144
  allowed(ctx, []);
141
- return request(...route, sub === 'graph' ? graph(false) : {});
145
+ return request(...route(uuid(id)), sub === 'graph' ? graph(false) : {});
142
146
  }
143
147
  if (group === 'fabric') {
144
- arity(ctx, 2);
145
- allowed(ctx, []);
146
148
  if (sub !== 'default-asset')
147
149
  throw new Error('Usage: vigilbase security fabric default-asset');
150
+ arity(ctx, 2);
151
+ allowed(ctx, []);
148
152
  return request('/fabric/default-asset', 'defaultAsset');
149
153
  }
150
154
  if (group === 'connections' && sub && ['available-assets', 'remediation', 'logpush-status', 'worker-stats'].includes(sub)) {
151
155
  const isJob = sub === 'logpush-status' || sub === 'worker-stats';
152
- arity(ctx, isJob ? 4 : 3);
156
+ arity(ctx, isJob ? 4 : 3, isJob ? `connections ${sub} <connection-id> <job-id>` : `connections ${sub} <id>`);
153
157
  allowed(ctx, []);
154
158
  const connectionId = uuid(id);
155
159
  if (sub === 'available-assets')
@@ -165,7 +169,7 @@ function extendedSecurityRequest(ctx, scope) {
165
169
  }
166
170
  if (group === 'alerts') {
167
171
  if (sub === 'show' || sub === 'get') {
168
- arity(ctx, 3);
172
+ arity(ctx, 3, `alerts ${sub} <id>`);
169
173
  allowed(ctx, []);
170
174
  return request(`/alerts/${uuid(id)}`, 'alert');
171
175
  }
@@ -179,7 +183,7 @@ function extendedSecurityRequest(ctx, scope) {
179
183
  return request('/alerts', 'alerts', { limit: integer(ctx, 'limit', 100, 100), asset_id: ctx.values['asset-id'] !== undefined ? uuid(ctx.values['asset-id']) : undefined, since });
180
184
  }
181
185
  if (group === 'incidents' && (sub === 'fabric' || sub === 'share-links')) {
182
- arity(ctx, 3);
186
+ arity(ctx, 3, `incidents ${sub} <id>`);
183
187
  if (sub === 'share-links')
184
188
  allowed(ctx, []);
185
189
  return request(`/incidents/${uuid(id)}/${sub}`, sub === 'fabric' ? 'graph' : 'shareLinks', sub === 'fabric' ? graph(true) : {});
@@ -211,17 +215,17 @@ export function buildSecurityRequest(ctx) {
211
215
  }
212
216
  if (['incidents', 'connections', 'reports'].includes(group)) {
213
217
  if (command === 'show' || command === 'get') {
214
- arity(ctx, 3);
218
+ arity(ctx, 3, `${group} ${command} <id>`);
215
219
  allowed(ctx, []);
216
220
  return request(`/${group}/${uuid(ctx.rest[2])}`, group === 'incidents' ? 'incident' : group === 'connections' ? 'connection' : 'report');
217
221
  }
218
222
  if (group === 'incidents' && command === 'timeline') {
219
- arity(ctx, 3);
223
+ arity(ctx, 3, 'incidents timeline <id>');
220
224
  allowed(ctx, ['before', 'limit']);
221
225
  return request(`/incidents/${uuid(ctx.rest[2])}/timeline`, 'timeline', { limit: integer(ctx, 'limit', 200, 100), before: textFlag(ctx, 'before') });
222
226
  }
223
227
  if (group === 'incidents' && command === 'update') {
224
- arity(ctx, 4);
228
+ arity(ctx, 4, 'incidents update <id> <status>');
225
229
  allowed(ctx, ['note', 'isConfirm']);
226
230
  const id = uuid(ctx.rest[2]);
227
231
  const status = z.enum(updateStatuses).parse(ctx.rest[3]);
@@ -250,10 +254,18 @@ export function buildSecurityRequest(ctx) {
250
254
  order_direction: z.enum(['asc', 'desc']).optional().parse(textFlag(ctx, 'order-direction')),
251
255
  });
252
256
  }
253
- if (group === 'assets' || group === 'detections') {
257
+ if (group === 'assets') {
258
+ arity(ctx, 1);
259
+ allowed(ctx, []);
260
+ return request('/domains', 'assets');
261
+ }
262
+ if (group === 'detections') {
263
+ // Detections are configured per asset; the API has no tenant-wide list.
254
264
  arity(ctx, 1);
255
- allowed(ctx, group === 'detections' ? ['asset-id'] : []);
256
- return request(group === 'assets' ? '/domains' : '/detections', group, group === 'detections' && ctx.values['asset-id'] !== undefined ? { asset_id: uuid(ctx.values['asset-id']) } : {});
265
+ allowed(ctx, ['asset-id']);
266
+ if (ctx.values['asset-id'] === undefined)
267
+ throw new Error('security detections requires --asset-id <uuid>. List asset IDs with `vigilbase security assets`.');
268
+ return request('/detections', 'detections', { asset_id: uuid(ctx.values['asset-id']) });
257
269
  }
258
270
  if (group === 'logs') {
259
271
  arity(ctx, 1);
@@ -289,7 +301,7 @@ export function buildSecurityRequest(ctx) {
289
301
  arity(ctx, 2);
290
302
  return request('/intel/quota', 'quota');
291
303
  }
292
- arity(ctx, 3);
304
+ arity(ctx, 3, 'intel <ip|asn|cidr|ja3|ja4> <query>');
293
305
  const type = z.enum(['ip', 'asn', 'cidr', 'ja3', 'ja4']).parse(command);
294
306
  const q = z.string().trim().min(1).max(512).parse(ctx.rest[2]);
295
307
  return request('/intel/lookup', 'intel', { q, type });
@@ -387,4 +399,4 @@ export const handleSecurity = async (ctx) => {
387
399
  display(ctx, request, payload);
388
400
  return ok();
389
401
  };
390
- export const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --limit --asset-id --provider --dataset --search\n --start --end --sort --frs-min --cursor\n audit --limit --cursor --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips|bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>, --customer-id <One customer UUID> (internal View-as).\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for every endpoint, supported value, alias, and pagination contract.\n";
402
+ export const SECURITY_HELP = "vigilbase security \u2014 managed security in Vigilbase One\n\n [overview]\n incidents [list] --status --severity --attack-category\n --order-by --order-direction --limit --offset\n incidents show <id>\n incidents timeline <id> --limit --before\n incidents fabric <id> --dataset --min-weight --limit\n incidents share-links <id>\n incidents update <id> <status> --confirm [--note]\n alerts [list] --limit --asset-id --start\n alerts show <id>\n logs --limit --asset-id --provider --dataset --search\n --start --end --sort --frs-min --cursor\n audit --limit --cursor --action-prefix --outcome\n assets\n assets <allowlist|blocklist|remediation|autonomy|datasets> <asset-id>\n assets graph <asset-id> --dataset --hours --min-weight --limit\n fabric default-asset\n detections --asset-id <asset-id>\n providers\n connections [list]\n connections show <id>\n connections <available-assets|remediation> <id>\n connections <logpush-status|worker-stats> <connection-id> <job-id>\n analytics [overview] --metrics + time-window flags\n analytics <security-analysis|bot-activity|top-attacker-ips|bot-asns|dns-security|page-shield>\n analytics provider-events --provider --dataset --start --end\n [--asset-id --search --limit]\n zero-trust <device-health|app-tests|access|sessions|device-posture>\n zero-trust gateway <dns|http|network>\n cloudflare-account [inventory]\n cloudflare-account <audit|ingestion|posture>\n notifications [list] --limit\n notifications <settings|preferences|slack-channels>\n reports [list] --limit --offset\n reports show <id>\n reports settings\n intel quota\n intel <ip|asn|cidr|ja3|ja4> <query>\n settings <lifecycle-emails|broadcast-emails|applications>\n\nTime-window flags: --hours (1\u2013720, default 24) or --start/--end ISO timestamps;\noptional --asset-id. Apply to analytics (except provider-events), zero-trust,\nand cloudflare-account audit/ingestion/posture. Inventory has no window flags.\nLimits: list incidents/reports/audit 1\u2013100 (default 50); alerts/notifications\n1\u2013100 (default 100); timeline 1\u2013200 (default 100); logs 1\u20131000 (default 100);\nprovider-events 1\u2013500 (default 200); graph 1\u20131000 (default 200).\nOffsets: 0\u20131000000. Graph min-weight: 1\u20131000000 (default 1).\nAll: --json, --timeout <ms>, --customer-id <One customer UUID> (internal View-as).\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for every endpoint, supported value, alias, and pagination contract.\n";
@@ -7,15 +7,29 @@ export declare class VigilbaseApiError extends Error {
7
7
  readonly code?: string;
8
8
  /** The server's sanitised explanation, set only for known vigilbase.com problem errors. */
9
9
  readonly reason?: string;
10
+ /** CLI-authored remediation, never server text (see one-error-hints.ts). */
11
+ readonly hint?: string;
12
+ /**
13
+ * The message is the CLI's own text, never the server's, so formatError prints it although the
14
+ * error carries a body. Unmarked errors with a body print only a generic `Request failed`.
15
+ */
16
+ readonly isCliAuthored: boolean;
17
+ /** The One tool run a failed scan recorded. */
18
+ readonly toolRunId?: string;
10
19
  constructor(message: string, options: {
11
20
  status: number;
12
21
  body?: unknown;
13
22
  retryAfterSec?: number;
14
23
  code?: string;
15
24
  reason?: string;
25
+ hint?: string;
26
+ isCliAuthored?: boolean;
27
+ toolRunId?: string;
16
28
  });
17
29
  }
18
30
  export declare function retryAfterSeconds(response: Response, body: unknown): number | undefined;
31
+ /** The versioned public tools API. The unversioned /api/agent/tools aliases sunset on 2027-09-01. */
32
+ export declare const PUBLIC_TOOLS_PATH = "/api/v1/agent/tools";
19
33
  /**
20
34
  * Public (unauthenticated) vigilbase.com tools client.
21
35
  * Vigilbase One authenticated calls will use a separate client + device-auth tokens.
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { singleLine } from './format.js';
2
+ import { SCAN_FAILED_MESSAGE, singleLine } from './format.js';
3
3
  import { parseJsonResponse, requestText } from './network.js';
4
4
  import { fetchSkillText } from './skill-fetch.js';
5
5
  import { AGENT_API_ERROR_CODES, } from '../types.js';
@@ -10,6 +10,15 @@ export class VigilbaseApiError extends Error {
10
10
  code;
11
11
  /** The server's sanitised explanation, set only for known vigilbase.com problem errors. */
12
12
  reason;
13
+ /** CLI-authored remediation, never server text (see one-error-hints.ts). */
14
+ hint;
15
+ /**
16
+ * The message is the CLI's own text, never the server's, so formatError prints it although the
17
+ * error carries a body. Unmarked errors with a body print only a generic `Request failed`.
18
+ */
19
+ isCliAuthored;
20
+ /** The One tool run a failed scan recorded. */
21
+ toolRunId;
13
22
  constructor(message, options) {
14
23
  super(message);
15
24
  this.name = 'VigilbaseApiError';
@@ -18,6 +27,9 @@ export class VigilbaseApiError extends Error {
18
27
  this.retryAfterSec = options.retryAfterSec;
19
28
  this.code = options.code;
20
29
  this.reason = options.reason;
30
+ this.hint = options.hint;
31
+ this.isCliAuthored = options.isCliAuthored ?? false;
32
+ this.toolRunId = options.toolRunId;
21
33
  }
22
34
  }
23
35
  export function retryAfterSeconds(response, body) {
@@ -58,6 +70,7 @@ function problemReason(response, body) {
58
70
  const message = singleLine(problem.data.error.message);
59
71
  return message ? { code: code.data, message } : undefined;
60
72
  }
73
+ /** `fallback` is the CLI's own message, used when com gave no reason it accepts. */
61
74
  function publicApiError(fallback, response, body) {
62
75
  const reason = problemReason(response, body);
63
76
  return new VigilbaseApiError(reason?.message ?? fallback, {
@@ -66,8 +79,11 @@ function publicApiError(fallback, response, body) {
66
79
  retryAfterSec: retryAfterSeconds(response, body),
67
80
  code: reason?.code,
68
81
  reason: reason?.message,
82
+ isCliAuthored: !reason,
69
83
  });
70
84
  }
85
+ /** The versioned public tools API. The unversioned /api/agent/tools aliases sunset on 2027-09-01. */
86
+ export const PUBLIC_TOOLS_PATH = '/api/v1/agent/tools';
71
87
  /**
72
88
  * Public (unauthenticated) vigilbase.com tools client.
73
89
  * Vigilbase One authenticated calls will use a separate client + device-auth tokens.
@@ -93,7 +109,7 @@ export class VigilbaseClient {
93
109
  return { response, body: parseJsonResponse(response, text) };
94
110
  }
95
111
  async listTools() {
96
- const { response, body } = await this.request('/api/agent/tools');
112
+ const { response, body } = await this.request(PUBLIC_TOOLS_PATH);
97
113
  if (!response.ok) {
98
114
  throw publicApiError(`Failed to list tools (HTTP ${response.status})`, response, body);
99
115
  }
@@ -110,7 +126,7 @@ export class VigilbaseClient {
110
126
  return body;
111
127
  }
112
128
  async executeTool(toolName, target) {
113
- const { response, body } = await this.request(`/api/agent/tools/${encodeURIComponent(toolName)}/execute`, {
129
+ const { response, body } = await this.request(`${PUBLIC_TOOLS_PATH}/${encodeURIComponent(toolName)}/execute`, {
114
130
  method: 'POST',
115
131
  headers: { 'Content-Type': 'application/json' },
116
132
  body: JSON.stringify({ target }),
@@ -126,7 +142,7 @@ export class VigilbaseClient {
126
142
  throw new VigilbaseApiError('Invalid tool execution response. Check the API URL or retry later.', { status: response.status });
127
143
  }
128
144
  if (!parsed.success) {
129
- throw publicApiError('Tool execution was unsuccessful.', response, body);
145
+ throw publicApiError(SCAN_FAILED_MESSAGE, response, body);
130
146
  }
131
147
  return parsed;
132
148
  }
@@ -61,7 +61,7 @@ export class CloudflareClient {
61
61
  method: 'POST',
62
62
  headers: { 'Content-Type': 'application/json' },
63
63
  body: JSON.stringify({ customerId: scope.customerId, accountId: scope.accountId }),
64
- });
64
+ }, { hasCustomerScope: Boolean(scope.customerId) });
65
65
  }
66
66
  const params = new URLSearchParams();
67
67
  if (scope.customerId)
@@ -3,6 +3,8 @@ export type Writer = {
3
3
  stdout: (line?: string) => void;
4
4
  stderr: (line?: string) => void;
5
5
  };
6
+ /** A scan that ended without a result: com's copy (apps/com/lib/tools/execute-internal-scan.ts). */
7
+ export declare const SCAN_FAILED_MESSAGE = "The scan couldn't be completed. Try again later.";
6
8
  /** Untrusted API data must never become terminal control sequences or bidi overrides. */
7
9
  export declare function sanitizeTerminal(value: string): string;
8
10
  /** One sanitised line, cut to `maxLength` characters with an ellipsis. */
@@ -16,5 +18,7 @@ export declare function formatToolsList(catalog: AgentToolsCatalogResponse, mode
16
18
  isAuthenticated?: boolean;
17
19
  }): void;
18
20
  export declare function formatToolDescription(tool: AgentToolDefinition, mode: CliOutputMode, writer: Writer, skillMarkdown?: string): void;
21
+ /** Whether a scan produced a result; the exit code follows it. */
22
+ export declare function isSuccessfulScan(result: AgentToolExecuteResponse): boolean;
19
23
  export declare function formatExecuteResult(result: AgentToolExecuteResponse, mode: CliOutputMode, writer: Writer): void;
20
24
  export declare function formatError(error: unknown, mode: CliOutputMode, writer: Writer): void;
@@ -1,4 +1,7 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
+ /** A scan that ended without a result: com's copy (apps/com/lib/tools/execute-internal-scan.ts). */
3
+ export const SCAN_FAILED_MESSAGE = "The scan couldn't be completed. Try again later.";
4
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
5
  /** Untrusted API data must never become terminal control sequences or bidi overrides. */
3
6
  export function sanitizeTerminal(value) {
4
7
  return stripVTControlCharacters(value).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, '');
@@ -245,27 +248,42 @@ export function formatToolDescription(tool, mode, writer, skillMarkdown) {
245
248
  writer.stdout(skillMarkdown.trimEnd());
246
249
  }
247
250
  }
251
+ /**
252
+ * Why a scan failed, or null when it has a result. A scan com could not grade comes back
253
+ * inside a successful response as `status: "error"` with a message and no grade.
254
+ */
255
+ function scanFailure(result) {
256
+ if (result.success && result.result?.status !== 'error')
257
+ return null;
258
+ const message = result.result?.status === 'error'
259
+ ? result.result.message
260
+ : result.success ? undefined : typeof result.error === 'string' ? result.error : result.error?.message;
261
+ // Vigilbase's scanner wrote it, but it is bounded like any server reason.
262
+ return (typeof message === 'string' && singleLine(message)) || SCAN_FAILED_MESSAGE;
263
+ }
264
+ /** Whether a scan produced a result; the exit code follows it. */
265
+ export function isSuccessfulScan(result) {
266
+ return scanFailure(result) === null;
267
+ }
248
268
  export function formatExecuteResult(result, mode, writer) {
269
+ const failure = scanFailure(result);
249
270
  if (mode === 'json') {
250
- printJson(writer, result);
271
+ // A scan without a result fails in JSON as in the exit code.
272
+ // A failed scan has no grade, whatever placeholder the scanner used.
273
+ printJson(writer, failure === null ? result : { ...result, success: false, error: failure, grade: null, gradeScore: null });
251
274
  return;
252
275
  }
253
276
  const tool = result.toolName ?? 'tool';
254
277
  const target = result.target ?? '';
255
- const grade = result.grade != null
278
+ const grade = failure === null && result.grade != null
256
279
  ? `${result.grade}${result.gradeScore != null ? ` ${result.gradeScore}` : ''}`
257
280
  : 'n/a';
258
- const head = result.success
259
- ? `✓ ${tool} · ${target} · ${grade}`
260
- : `✗ ${tool} · ${target} · ${grade}`;
261
- if (result.success)
262
- writer.stdout(head);
263
- else
264
- writer.stderr(head);
265
- const error = typeof result.error === 'string' ? result.error : result.error?.message;
266
- if (error) {
267
- writer.stderr(` ${error}`);
281
+ if (failure !== null) {
282
+ writer.stderr(`✗ ${tool} · ${target} · ${grade}`);
283
+ writer.stderr(` ${failure}`);
284
+ return;
268
285
  }
286
+ writer.stdout(`✓ ${tool} · ${target} · ${grade}`);
269
287
  for (const line of summarizeResultCompact(result.result)) {
270
288
  writer.stdout(` ${line}`);
271
289
  }
@@ -273,7 +291,8 @@ export function formatExecuteResult(result, mode, writer) {
273
291
  export function formatError(error, mode, writer) {
274
292
  let message = error instanceof Error ? error.message : String(error);
275
293
  let reason = '';
276
- // Server bodies can contain credentials, customer data, or arbitrary error text.
294
+ // Server bodies can contain credentials, customer data, or arbitrary error text, so an error that
295
+ // carries one shows only text the CLI wrote, or the reason the public client accepted.
277
296
  if (error && typeof error === 'object' && 'body' in error && error.body != null && 'status' in error && typeof error.status === 'number') {
278
297
  if ('code' in error && error.code === 'FLAREHAWK_VIEW_AS_REQUIRED') {
279
298
  message = 'Select a customer with --customer-id <customer UUID> for security commands.';
@@ -281,7 +300,8 @@ export function formatError(error, mode, writer) {
281
300
  else {
282
301
  // Only the reason the public client accepted from a known problem+json error is shown.
283
302
  reason = 'reason' in error && typeof error.reason === 'string' ? singleLine(error.reason) : '';
284
- message = reason || `Request failed (HTTP ${error.status}).`;
303
+ const isCliAuthored = 'isCliAuthored' in error && error.isCliAuthored === true;
304
+ message = reason || (isCliAuthored ? message : `Request failed (HTTP ${error.status}).`);
285
305
  }
286
306
  }
287
307
  const payload = {
@@ -310,6 +330,10 @@ export function formatError(error, mode, writer) {
310
330
  if (hint)
311
331
  payload.hint = hint;
312
332
  }
333
+ // The One tool run a failed scan recorded, for `vigilbase runs get`.
334
+ if ('toolRunId' in error && typeof error.toolRunId === 'string' && UUID.test(error.toolRunId)) {
335
+ payload.toolRunId = error.toolRunId;
336
+ }
313
337
  }
314
338
  if (mode === 'json') {
315
339
  printJson(writer, payload);