@vigilbase/cli 0.2.1 → 0.2.2

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,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,7 +1,7 @@
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
7
  import { formatExecuteResult, formatToolDescription, formatToolsList, } from '../lib/format.js';
@@ -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;
@@ -116,7 +116,7 @@ export const handleRun = async (ctx) => {
116
116
  return result.success ? ok() : EXIT.ERROR;
117
117
  };
118
118
  export const handleDescribe = async (ctx) => {
119
- validateArguments(ctx, 1, ['isNoSkill']);
119
+ validateArguments(ctx, 1, ['isNoSkill'], 'vigilbase describe <tool>');
120
120
  toolNameSchema.parse(ctx.rest[0]);
121
121
  const toolName = ctx.rest[0];
122
122
  if (!toolName)
@@ -139,7 +139,7 @@ export const handleDescribe = async (ctx) => {
139
139
  return ok();
140
140
  };
141
141
  export const handleSkill = async (ctx) => {
142
- validateArguments(ctx, 1);
142
+ validateArguments(ctx, 1, [], 'vigilbase skill <tool>');
143
143
  toolNameSchema.parse(ctx.rest[0]);
144
144
  const toolName = ctx.rest[0];
145
145
  if (!toolName)
@@ -163,7 +163,7 @@ export const handleSkill = async (ctx) => {
163
163
  return ok();
164
164
  };
165
165
  export const handleBadge = async (ctx) => {
166
- validateArguments(ctx, 1, ['style']);
166
+ validateArguments(ctx, 1, ['style'], 'vigilbase badge <domain>');
167
167
  const domain = ctx.rest[0];
168
168
  if (!domain)
169
169
  throw new Error('Usage: vigilbase badge <domain>');
@@ -230,7 +230,7 @@ export const handleDoctor = async (ctx) => {
230
230
  };
231
231
  };
232
232
  const checks = {
233
- publicApi: checkResult(publicResult, `${ctx.config.publicBaseUrl}/api/agent/tools`),
233
+ publicApi: checkResult(publicResult, `${ctx.config.publicBaseUrl}${PUBLIC_TOOLS_PATH}`),
234
234
  one: checkResult(oneResult, oneCheckUrl),
235
235
  signIn: checkResult(signInResult),
236
236
  };
@@ -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,20 @@ 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;
10
12
  constructor(message: string, options: {
11
13
  status: number;
12
14
  body?: unknown;
13
15
  retryAfterSec?: number;
14
16
  code?: string;
15
17
  reason?: string;
18
+ hint?: string;
16
19
  });
17
20
  }
18
21
  export declare function retryAfterSeconds(response: Response, body: unknown): number | undefined;
22
+ /** The versioned public tools API. The unversioned /api/agent/tools aliases sunset on 2027-09-01. */
23
+ export declare const PUBLIC_TOOLS_PATH = "/api/v1/agent/tools";
19
24
  /**
20
25
  * Public (unauthenticated) vigilbase.com tools client.
21
26
  * Vigilbase One authenticated calls will use a separate client + device-auth tokens.
@@ -10,6 +10,8 @@ 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;
13
15
  constructor(message, options) {
14
16
  super(message);
15
17
  this.name = 'VigilbaseApiError';
@@ -18,6 +20,7 @@ export class VigilbaseApiError extends Error {
18
20
  this.retryAfterSec = options.retryAfterSec;
19
21
  this.code = options.code;
20
22
  this.reason = options.reason;
23
+ this.hint = options.hint;
21
24
  }
22
25
  }
23
26
  export function retryAfterSeconds(response, body) {
@@ -68,6 +71,8 @@ function publicApiError(fallback, response, body) {
68
71
  reason: reason?.message,
69
72
  });
70
73
  }
74
+ /** The versioned public tools API. The unversioned /api/agent/tools aliases sunset on 2027-09-01. */
75
+ export const PUBLIC_TOOLS_PATH = '/api/v1/agent/tools';
71
76
  /**
72
77
  * Public (unauthenticated) vigilbase.com tools client.
73
78
  * Vigilbase One authenticated calls will use a separate client + device-auth tokens.
@@ -93,7 +98,7 @@ export class VigilbaseClient {
93
98
  return { response, body: parseJsonResponse(response, text) };
94
99
  }
95
100
  async listTools() {
96
- const { response, body } = await this.request('/api/agent/tools');
101
+ const { response, body } = await this.request(PUBLIC_TOOLS_PATH);
97
102
  if (!response.ok) {
98
103
  throw publicApiError(`Failed to list tools (HTTP ${response.status})`, response, body);
99
104
  }
@@ -110,7 +115,7 @@ export class VigilbaseClient {
110
115
  return body;
111
116
  }
112
117
  async executeTool(toolName, target) {
113
- const { response, body } = await this.request(`/api/agent/tools/${encodeURIComponent(toolName)}/execute`, {
118
+ const { response, body } = await this.request(`${PUBLIC_TOOLS_PATH}/${encodeURIComponent(toolName)}/execute`, {
114
119
  method: 'POST',
115
120
  headers: { 'Content-Type': 'application/json' },
116
121
  body: JSON.stringify({ target }),
@@ -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)
@@ -1,3 +1,6 @@
1
1
  export declare const HELP_TAGLINE = "one \u00B7 security \u00B7 tools";
2
+ /** A tool from the catalog the CLI knows offline: it runs as `run <tool> <target>`, not as a command. */
3
+ export declare function isCatalogTool(name: string): boolean;
2
4
  export declare const HELP_TEXT: string;
5
+ export declare const HELP_USAGE = "Usage: vigilbase help [<command> | <tool>]";
3
6
  export declare function helpForCommand(command: string): string | null;
package/dist/lib/help.js CHANGED
@@ -2,7 +2,21 @@ import { CLOUDFLARE_HELP } from '../commands/cloudflare.js';
2
2
  import { LOCAL_HELP } from '../commands/local.js';
3
3
  import { ONE_HELP } from '../commands/one.js';
4
4
  import { SECURITY_HELP } from '../commands/security.js';
5
+ import { isOneAuthRequiredTool, ONE_AUTH_REQUIRED_TOOLS } from './one-client.js';
5
6
  export const HELP_TAGLINE = 'one · security · tools';
7
+ /** vigilbase.com's public catalog, with each tool's shortcut command. `help <tool>` answers offline. */
8
+ const PUBLIC_TOOL_SHORTCUTS = {
9
+ 'security-headers': 'headers <url>',
10
+ 'email-security': 'email <domain>',
11
+ };
12
+ const PUBLIC_TOOLS = Object.keys(PUBLIC_TOOL_SHORTCUTS);
13
+ function isPublicTool(name) {
14
+ return Object.hasOwn(PUBLIC_TOOL_SHORTCUTS, name);
15
+ }
16
+ /** A tool from the catalog the CLI knows offline: it runs as `run <tool> <target>`, not as a command. */
17
+ export function isCatalogTool(name) {
18
+ return isPublicTool(name) || isOneAuthRequiredTool(name);
19
+ }
6
20
  /** Column where descriptions start in the main help: two-space indent, term, at least two spaces. */
7
21
  const DESCRIPTION_COLUMN = 31;
8
22
  /** Two-column help rows whose descriptions all start at DESCRIPTION_COLUMN. */
@@ -32,7 +46,7 @@ Public tools (no login)
32
46
  badge <domain> · doctor Badge URL · connectivity
33
47
 
34
48
  Auth-required tools (after login)
35
- website-security · react2shell · fortibleed-check
49
+ ${ONE_AUTH_REQUIRED_TOOLS.map((tool) => ` run ${tool} <target>`).join('\n')}
36
50
 
37
51
  On this machine (no login)
38
52
  cloudflare-checkup Run the Cloudflare Checkup on this machine in one
@@ -57,7 +71,7 @@ Account (login required)
57
71
  Options
58
72
  ${GLOBAL_OPTIONS}
59
73
 
60
- Use vigilbase <command> --help for command options.
74
+ Use vigilbase <command> --help or vigilbase help <tool> for usage.
61
75
 
62
76
  Examples
63
77
  vigilbase headers https://vigilbase.com
@@ -68,16 +82,40 @@ Examples
68
82
  vigilbase cf zones
69
83
  vigilbase security incidents --severity high
70
84
  `;
85
+ /** How to run a catalog tool: `help <tool>` and `<tool> --help`, since tools run as `run <tool> <target>`. */
86
+ function helpForTool(tool) {
87
+ if (isPublicTool(tool)) {
88
+ return `Usage: vigilbase run ${tool} <target> [--json] [--via public|one] [--public]
89
+
90
+ Public tool, no login needed. Shortcut: vigilbase ${PUBLIC_TOOL_SHORTCUTS[tool]}
91
+ Details: vigilbase describe ${tool}`;
92
+ }
93
+ if (isOneAuthRequiredTool(tool)) {
94
+ return `Usage: vigilbase run ${tool} <target> [--json]
95
+
96
+ Runs through Vigilbase One: run \`vigilbase login\` first.
97
+ Details: vigilbase describe ${tool}`;
98
+ }
99
+ return null;
100
+ }
101
+ export const HELP_USAGE = 'Usage: vigilbase help [<command> | <tool>]';
71
102
  export function helpForCommand(command) {
72
103
  switch (command) {
104
+ case 'help':
105
+ return `${HELP_USAGE}
106
+
107
+ Shows the usage of a command or of a tool, for example vigilbase help run or
108
+ vigilbase help website-security. Without one, shows every command.`;
73
109
  case 'tools':
74
110
  case 'list':
75
111
  return `Usage: vigilbase list [--json]`;
76
112
  case 'run':
77
113
  return `Usage: vigilbase run <tool> <target> [--json] [--via public|one] [--public]
78
114
 
79
- Public: security-headers, email-security
80
- Login: website-security, react2shell, fortibleed-check`;
115
+ Public: ${PUBLIC_TOOLS.join(', ')}
116
+ Login: ${ONE_AUTH_REQUIRED_TOOLS.join(', ')}
117
+
118
+ Run vigilbase help <tool> for a tool's usage.`;
81
119
  case 'describe':
82
120
  return `Usage: vigilbase describe <tool> [--json] [--no-skill]`;
83
121
  case 'skill':
@@ -131,6 +169,6 @@ Prefer --token-stdin with piped input to keep tokens out of command arguments.`;
131
169
  case 'email':
132
170
  return `Usage: vigilbase email <domain> [--json] [--via public|one] [--public]`;
133
171
  default:
134
- return null;
172
+ return helpForTool(command);
135
173
  }
136
174
  }
@@ -54,6 +54,8 @@ export declare class VigilbaseOneClient {
54
54
  /** Authenticated JSON transport shared by the One command clients. */
55
55
  requestJson<T extends ApiErrorBody = Record<string, unknown>>(path: string, init?: RequestInit, options?: {
56
56
  allowStatuses?: number[];
57
+ /** Whether the request names a customer outside the query string (a JSON body). */
58
+ hasCustomerScope?: boolean;
57
59
  }): Promise<T>;
58
60
  getSession(): Promise<OneSessionResponse>;
59
61
  getAccount(): Promise<Record<string, unknown>>;
@@ -6,6 +6,7 @@ import { z } from 'zod';
6
6
  import { validateOneOrigin } from './config.js';
7
7
  import { validateStripePortalUrl } from './open-url.js';
8
8
  import { parseJsonResponse, requestText } from './network.js';
9
+ import { oneErrorHint } from './one-error-hints.js';
9
10
  import { retryAfterSeconds, VigilbaseApiError } from './client.js';
10
11
  /**
11
12
  * Tools that require Vigilbase One authentication (auth-required scanners).
@@ -129,9 +130,12 @@ export class VigilbaseOneClient {
129
130
  if (errorDetails.code === 'FLAREHAWK_VIEW_AS_REQUIRED') {
130
131
  message = 'Select a customer with --customer-id <customer UUID> for security commands.';
131
132
  }
133
+ const hint = response.ok ? undefined : oneErrorHint({
134
+ method: init?.method ?? 'GET', path, status: response.status, code: errorDetails.code, hasCustomerScope: options?.hasCustomerScope,
135
+ });
132
136
  if (response.status === 401 || response.status === 403) {
133
137
  throw new VigilbaseApiError(message ||
134
- 'Authentication required. Run `vigilbase login`.', { status: response.status, body: parsed, code: errorDetails.code });
138
+ 'Authentication required. Run `vigilbase login`.', { status: response.status, body: parsed, code: errorDetails.code, hint });
135
139
  }
136
140
  if (response.status === 429) {
137
141
  throw new VigilbaseApiError(message || 'Rate limit exceeded.', {
@@ -144,7 +148,7 @@ export class VigilbaseOneClient {
144
148
  const allowed = options?.allowStatuses ?? [];
145
149
  if (!response.ok && !allowed.includes(response.status)) {
146
150
  throw new VigilbaseApiError(message ||
147
- `Request failed (HTTP ${response.status})`, { status: response.status, body: parsed, code: errorDetails.code });
151
+ `Request failed (HTTP ${response.status})`, { status: response.status, body: parsed, code: errorDetails.code, hint });
148
152
  }
149
153
  if (!z.record(z.string(), z.unknown()).safeParse(body).success) {
150
154
  throw new VigilbaseApiError('Invalid One API response. Expected a JSON object.', { status: response.status });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * CLI-authored hints for One errors the CLI recognises. They are chosen only
3
+ * from the request (method, route, whether a customer was selected), the HTTP
4
+ * status and One's own error codes. One's free-text messages are never shown:
5
+ * they can describe its configuration.
6
+ */
7
+ export declare const STAFF_CUSTOMER_SCOPE_HINT = "Vigilbase staff: add --customer-id <uuid>.";
8
+ export declare const PERSONAL_SETTINGS_HINT = "Personal settings belong to each user, so View-as (--customer-id) cannot read them.";
9
+ export declare const BROWSER_SESSION_HINT = "Invitations need a One browser session: manage them at https://one.vigilbase.com.";
10
+ export type OneErrorContext = {
11
+ method: string;
12
+ /** The One path the CLI requested, with its query string. */
13
+ path: string;
14
+ status: number;
15
+ code?: string;
16
+ /** Whether the request named a customer; defaults to a `customerId` query parameter. */
17
+ hasCustomerScope?: boolean;
18
+ };
19
+ export declare function oneErrorHint(context: OneErrorContext): string | undefined;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CLI-authored hints for One errors the CLI recognises. They are chosen only
3
+ * from the request (method, route, whether a customer was selected), the HTTP
4
+ * status and One's own error codes. One's free-text messages are never shown:
5
+ * they can describe its configuration.
6
+ */
7
+ export const STAFF_CUSTOMER_SCOPE_HINT = 'Vigilbase staff: add --customer-id <uuid>.';
8
+ export const PERSONAL_SETTINGS_HINT = 'Personal settings belong to each user, so View-as (--customer-id) cannot read them.';
9
+ export const BROWSER_SESSION_HINT = 'Invitations need a One browser session: manage them at https://one.vigilbase.com.';
10
+ /** One's security boundary codes that already explain a 403 (apps/one/src/lib/flarehawk/access.ts). */
11
+ const SECURITY_ACCESS_CODES = new Set([
12
+ 'FLAREHAWK_CUSTOMER_ACCESS_DENIED',
13
+ 'FLAREHAWK_ROLE_UNSUPPORTED',
14
+ 'FLAREHAWK_ROLE_INSUFFICIENT',
15
+ 'FLAREHAWK_ORGANIZATION_REQUIRED',
16
+ 'FLAREHAWK_VIEW_AS_REQUIRED',
17
+ ]);
18
+ const RULES = [
19
+ // Staff sessions must name the customer; One answers 400 without one.
20
+ { method: 'GET', route: /^\/api\/support\/tickets$/u, status: 400, hasCustomerScope: false, hint: STAFF_CUSTOMER_SCOPE_HINT },
21
+ { method: 'GET', route: /^\/api\/services\/cloudflare\/checkups$/u, status: 400, hasCustomerScope: false, hint: STAFF_CUSTOMER_SCOPE_HINT },
22
+ { method: 'POST', route: /^\/api\/services\/cloudflare\/checkups\/start$/u, status: 400, hasCustomerScope: false, hint: STAFF_CUSTOMER_SCOPE_HINT },
23
+ { method: 'GET', route: /^\/api\/services\/cloudflare\/workspace(?:\/[a-z-]+)?$/u, status: 400, hasCustomerScope: false, hint: STAFF_CUSTOMER_SCOPE_HINT },
24
+ // Personal security settings are per user; a View-as read is refused.
25
+ {
26
+ method: 'GET',
27
+ route: /^\/api\/flarehawk\/(?:settings\/(?:lifecycle-emails|broadcast-emails)|integrations\/grants|notifications\/preferences)$/u,
28
+ status: 403,
29
+ hasCustomerScope: true,
30
+ hint: PERSONAL_SETTINGS_HINT,
31
+ excludedCodes: SECURITY_ACCESS_CODES,
32
+ },
33
+ // Identity's management API accepts only One browser sessions, so CLI tokens can't read or send invitations.
34
+ { method: 'GET', route: /^\/api\/auth\/orgs\/invitations$/u, status: 403, hasCustomerScope: false, hint: BROWSER_SESSION_HINT, code: 'browser_session_required' },
35
+ { method: 'POST', route: /^\/api\/auth\/orgs\/invite$/u, status: 403, hasCustomerScope: false, hint: BROWSER_SESSION_HINT, code: 'browser_session_required' },
36
+ ];
37
+ export function oneErrorHint(context) {
38
+ const [pathname = '', query = ''] = context.path.split('?', 2);
39
+ const hasCustomerScope = context.hasCustomerScope ?? new URLSearchParams(query).has('customerId');
40
+ const method = context.method.toUpperCase();
41
+ return RULES.find((rule) => rule.method === method &&
42
+ rule.status === context.status &&
43
+ rule.hasCustomerScope === hasCustomerScope &&
44
+ rule.route.test(pathname) &&
45
+ (rule.code === undefined || rule.code === context.code) &&
46
+ !(context.code !== undefined && rule.excludedCodes?.has(context.code)))?.hint;
47
+ }
@@ -10,6 +10,8 @@ export declare const LOCAL_ERROR_HINTS: Record<LocalCheckupErrorCode, string>;
10
10
  * hint names both ways out.
11
11
  */
12
12
  export declare const ENV_TOKEN_AUTH_HINT = "Set CLOUDFLARE_API_TOKEN in this shell to a valid read-only Cloudflare API token (read -rs CLOUDFLARE_API_TOKEN && export CLOUDFLARE_API_TOKEN) and run the same command again, or unset it (or use --cf-auth profile) to sign in with your browser instead.";
13
+ /** SNAPSHOT_INVALID for an `inspect` or `submit` path with no file: nothing to collect again, so how to make one. */
14
+ export declare const SNAPSHOT_MISSING_HINT = "Check the file path. To collect a snapshot into a file, run `npx @vigilbase/cli cloudflare-checkup collect --out <file>`.";
13
15
  /** CF_PERMISSION_DENIED in env mode: the token lacks a read permission; the browser sign-in is the other way out. */
14
16
  export declare const ENV_TOKEN_PERMISSION_HINT = "The API token in CLOUDFLARE_API_TOKEN cannot read everything the checkup needs. Add the read permissions to it, or unset it (or use --cf-auth profile) to sign in with your browser instead, then run the same command again.";
15
17
  export declare class LocalCheckupError extends Error {