@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.
- package/CLOUDFLARE.md +8 -8
- package/COMMANDS.md +77 -15
- package/LOCAL.md +6 -3
- package/ONE.md +4 -3
- package/README.md +34 -21
- package/SECURITY.md +4 -2
- package/dist/auth/device-auth.d.ts +3 -0
- package/dist/auth/device-auth.js +12 -3
- package/dist/cli.js +6 -3
- package/dist/commands/account.js +1 -1
- package/dist/commands/auth.js +2 -1
- package/dist/commands/cloudflare.d.ts +1 -1
- package/dist/commands/cloudflare.js +8 -3
- package/dist/commands/legacy-validation.d.ts +7 -1
- package/dist/commands/legacy-validation.js +15 -2
- package/dist/commands/local.js +2 -1
- package/dist/commands/one.js +33 -6
- package/dist/commands/public-tools.js +25 -19
- package/dist/commands/runs.js +1 -1
- package/dist/commands/security.d.ts +1 -1
- package/dist/commands/security.js +34 -22
- package/dist/lib/client.d.ts +14 -0
- package/dist/lib/client.js +20 -4
- package/dist/lib/cloudflare-client.js +1 -1
- package/dist/lib/format.d.ts +4 -0
- package/dist/lib/format.js +38 -14
- package/dist/lib/help.d.ts +3 -0
- package/dist/lib/help.js +43 -5
- package/dist/lib/one-client.d.ts +4 -0
- package/dist/lib/one-client.js +81 -10
- package/dist/lib/one-error-hints.d.ts +19 -0
- package/dist/lib/one-error-hints.js +47 -0
- package/dist/local/cloudflare-checkup/errors.d.ts +2 -0
- package/dist/local/cloudflare-checkup/errors.js +2 -0
- package/dist/local/cloudflare-checkup/snapshot-file.js +5 -3
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { assertCommandFlags } from './helpers.js';
|
|
3
|
-
|
|
4
|
-
|
|
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;
|
package/dist/commands/local.js
CHANGED
|
@@ -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
|
-
|
|
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'));
|
package/dist/commands/one.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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', {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
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}
|
|
239
|
+
publicApi: checkResult(publicResult, `${ctx.config.publicBaseUrl}${PUBLIC_TOOLS_PATH}`),
|
|
234
240
|
one: checkResult(oneResult, oneCheckUrl),
|
|
235
241
|
signIn: checkResult(signInResult),
|
|
236
242
|
};
|
package/dist/commands/runs.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
|
|
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/${
|
|
132
|
-
remediation: [`/assets/${
|
|
133
|
-
datasets: [`/fabric/assets/${
|
|
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'
|
|
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,
|
|
256
|
-
|
|
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
|
|
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";
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -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.
|
package/dist/lib/client.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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)
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/format.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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);
|