@vigilbase/cli 0.2.2 → 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 +1 -1
- package/COMMANDS.md +30 -11
- package/LOCAL.md +2 -2
- package/ONE.md +1 -1
- package/README.md +12 -9
- package/SECURITY.md +1 -1
- package/dist/auth/device-auth.d.ts +3 -0
- package/dist/auth/device-auth.js +12 -3
- package/dist/commands/public-tools.js +14 -8
- package/dist/lib/client.d.ts +9 -0
- package/dist/lib/client.js +13 -2
- package/dist/lib/format.d.ts +4 -0
- package/dist/lib/format.js +38 -14
- package/dist/lib/one-client.d.ts +2 -0
- package/dist/lib/one-client.js +77 -10
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/CLOUDFLARE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`vigilbase cf` and `vigilbase cloudflare` use the authenticated Vigilbase One API. Run `vigilbase login` first (signing in isn't supported on Windows yet). Your organization, permissions, customer links, and allowed zones determine access. The CLI never requires a Cloudflare API key.
|
|
4
4
|
|
|
5
|
-
`cf checkup` runs on One's servers. `cloudflare-checkup` (also `local cloudflare-checkup`) runs on your machine with Cloudflare's own `cf` CLI and needs no Vigilbase login: one command, `npx @vigilbase/cli cloudflare-checkup`, verifies your work email with a code, installs cf if needed, signs in read-only, collects and uploads after you confirm. It works while vigilbase.com has local checkups switched on; otherwise it says so before asking for your email and points you to the checkup in your browser at https://vigilbase.com/tools/cloudflare-checkup (see [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
5
|
+
`cf checkup` runs on One's servers. `cloudflare-checkup` (also `local cloudflare-checkup`) runs on your machine with Cloudflare's own `cf` CLI and needs no Vigilbase login: one command, `npx @vigilbase/cli cloudflare-checkup`, verifies your work email with a code, installs cf if needed, signs in read-only, collects and uploads after you confirm. It works while vigilbase.com has local checkups switched on; otherwise it says so before asking for your email and points you to the checkup in your browser at https://vigilbase.com/tools/cloudflare-checkup (see [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md)).
|
|
6
6
|
|
|
7
7
|
Customer provider access requires an `active` or `trialing` Cloudflare service
|
|
8
8
|
entitlement for the selected account. Historical/inactive rows and legacy account
|
package/COMMANDS.md
CHANGED
|
@@ -10,11 +10,11 @@ below omit `--json`, which every command supports.
|
|
|
10
10
|
|
|
11
11
|
| Area | Complete command and API reference |
|
|
12
12
|
| --- | --- |
|
|
13
|
-
| Cloudflare | [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
14
|
-
| Security | [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
15
|
-
| Customers, organizations, monitoring, support | [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
16
|
-
| Local Cloudflare Checkup (cf on your machine) | [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
17
|
-
| Sign-in, credentials, configuration, troubleshooting | [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
13
|
+
| Cloudflare | [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md) |
|
|
14
|
+
| Security | [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/SECURITY.md) |
|
|
15
|
+
| Customers, organizations, monitoring, support | [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/ONE.md) |
|
|
16
|
+
| Local Cloudflare Checkup (cf on your machine) | [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md) |
|
|
17
|
+
| Sign-in, credentials, configuration, troubleshooting | [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/README.md) |
|
|
18
18
|
|
|
19
19
|
The references list the supported operations, filters, response and pagination
|
|
20
20
|
behaviour, API endpoints and access requirements of the CLI.
|
|
@@ -141,7 +141,19 @@ credentials file is there.
|
|
|
141
141
|
When a public scan fails and vigilbase.com explains why with a known error
|
|
142
142
|
code, the CLI prints `Error: <message> (<code>)` (JSON: `error`, `code`,
|
|
143
143
|
`status`), with the message cleaned to one line of at most 300 characters.
|
|
144
|
-
Otherwise it prints
|
|
144
|
+
Otherwise it prints a message of its own, never the server's text.
|
|
145
|
+
|
|
146
|
+
When a scan run through Vigilbase One fails (One answers 422), the CLI prints
|
|
147
|
+
`Error: The scan couldn't be completed. Try again later.` and exits 1. JSON
|
|
148
|
+
has `success: false`, that `error`, `status: 422` and the `toolRunId` One
|
|
149
|
+
recorded, for `vigilbase runs get <toolRunId>`.
|
|
150
|
+
|
|
151
|
+
When the scanner ran but couldn't assess the target, for example because the
|
|
152
|
+
site answered with a bot verification challenge or an error page, the CLI
|
|
153
|
+
prints `✗ <tool> · <target> · n/a` and the scanner's reason on the next line,
|
|
154
|
+
cleaned to one line of at most 300 characters, and exits 1. JSON has
|
|
155
|
+
`success: false`, that reason as `error`, `grade: null`, the scanner's
|
|
156
|
+
`result`, `status: 422`, `code: "scan_unassessed"` and the `toolRunId`.
|
|
145
157
|
|
|
146
158
|
## Account and billing
|
|
147
159
|
|
|
@@ -158,7 +170,7 @@ Otherwise it prints `Request failed (HTTP <status>).`
|
|
|
158
170
|
| `billing` | `summary`, `show`, `status` | GET `/api/account/billing` |
|
|
159
171
|
| `billing portal` | `open`; `--no-open` prints the portal URL without opening a browser | POST `/api/billing/portal` |
|
|
160
172
|
|
|
161
|
-
Invitations need a One browser session: Identity's management API doesn't accept CLI sign-ins, so `account invites` and `account invites create` answer 403 (`browser_session_required`)
|
|
173
|
+
Invitations need a One browser session: Identity's management API doesn't accept CLI sign-ins, so `account invites` and `account invites create` answer 403 (`browser_session_required`): the CLI prints `You don't have access to this.` and points you to https://one.vigilbase.com.
|
|
162
174
|
|
|
163
175
|
Creating an invitation sends it; `billing portal` starts a Stripe portal
|
|
164
176
|
session. Both are explicit actions. Billing data comes from One's Stripe
|
|
@@ -226,14 +238,21 @@ the uploaded data and the error codes.
|
|
|
226
238
|
|
|
227
239
|
Exit codes: **0** success, **1** validation, API, network or sign-in failure,
|
|
228
240
|
**2** HTTP 429 rate limiting. A completed scan whose result is unsuccessful
|
|
229
|
-
also exits 1.
|
|
241
|
+
also exits 1. So does a scan the scanner could not grade (result `status`
|
|
242
|
+
`"error"`, no grade): it prints `✗ <tool> · <target> · n/a` and the scanner's
|
|
243
|
+
message, cleaned to one line of at most 300 characters, and JSON has
|
|
244
|
+
`success: false` with that message as `error`. A signed-out `whoami` is not a
|
|
245
|
+
failure, and a healthy `doctor`
|
|
230
246
|
does not need a sign-in. API error JSON keeps the status, code and retry delay
|
|
231
247
|
when available.
|
|
232
248
|
|
|
233
249
|
Vigilbase One's error messages are never shown, because they can describe
|
|
234
|
-
One's configuration
|
|
235
|
-
|
|
236
|
-
|
|
250
|
+
One's configuration. The CLI prints its own message for the status instead:
|
|
251
|
+
``Authentication required. Run `vigilbase login`.`` (401),
|
|
252
|
+
`You don't have access to this.` (403), `Rate limit exceeded.` (429) and
|
|
253
|
+
`Request failed (HTTP <status>).` otherwise. When it recognises the failure
|
|
254
|
+
from the route, the status and One's error code, it adds a hint of its own
|
|
255
|
+
(`Hint: …` in text, `hint` in JSON):
|
|
237
256
|
|
|
238
257
|
| One answers | Hint |
|
|
239
258
|
| --- | --- |
|
package/LOCAL.md
CHANGED
|
@@ -15,7 +15,7 @@ before it asks for your email; when local mode is off it stops with
|
|
|
15
15
|
your browser at <https://vigilbase.com/tools/cloudflare-checkup>.
|
|
16
16
|
|
|
17
17
|
`vigilbase cf checkup` runs on Vigilbase One's servers with stored credentials
|
|
18
|
-
(see [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
18
|
+
(see [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md));
|
|
19
19
|
`vigilbase cloudflare-checkup` runs on your machine and needs no Vigilbase
|
|
20
20
|
login.
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ of the CLI (0.1.x) is running instead: `npx` prefers an installed global
|
|
|
34
34
|
update the global install with `npm install -g @vigilbase/cli@latest`. With
|
|
35
35
|
Yarn 4.15+ or pnpm 11+, `yarn dlx` and `pnpm dlx` run an older release for a
|
|
36
36
|
day after each release; use `npx` (see
|
|
37
|
-
[Troubleshooting](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
37
|
+
[Troubleshooting](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/README.md#troubleshooting)).
|
|
38
38
|
|
|
39
39
|
The command does everything, in this order, and stops at the first problem
|
|
40
40
|
before any slower step:
|
package/ONE.md
CHANGED
|
@@ -95,4 +95,4 @@ vigilbase support tickets [--all] [--limit 1..100] [--customer-id <uuid>] [--jso
|
|
|
95
95
|
|
|
96
96
|
## Related commands and validation
|
|
97
97
|
|
|
98
|
-
See [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
98
|
+
See [README.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/README.md) for sign-in and configuration, [COMMANDS.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/COMMANDS.md) for account, billing, public scanners, run history and Watchdog, [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md) for `cf`/`cloudflare`, and [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/SECURITY.md) for security commands. The read operations above do not replace the existing explicitly confirmed write commands.
|
package/README.md
CHANGED
|
@@ -28,11 +28,11 @@ supported on Windows yet; public tools and `cloudflare-checkup` still work there
|
|
|
28
28
|
|
|
29
29
|
| Reference | Covers |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| [COMMANDS.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
32
|
-
| [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
33
|
-
| [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
34
|
-
| [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
35
|
-
| [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
31
|
+
| [COMMANDS.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/COMMANDS.md) | Core commands, aliases, global options, API mapping, sign-in, output |
|
|
32
|
+
| [CLOUDFLARE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/CLOUDFLARE.md) | Accounts, zones, DNS, WAF, analytics, Zero Trust, Workers/Pages, audit, checkups |
|
|
33
|
+
| [SECURITY.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/SECURITY.md) | Incidents, logs, intelligence, analytics, assets, connections, policies, notifications, reports |
|
|
34
|
+
| [ONE.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/ONE.md) | Customer records, billing, reports, investigations, organizations, monitoring, support |
|
|
35
|
+
| [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md) | `cloudflare-checkup`: verifies your work email, collects your Cloudflare configuration on your machine with Cloudflare's `cf` and uploads it after you confirm |
|
|
36
36
|
|
|
37
37
|
These files ship with the package. Built-in help: `vigilbase <command> --help`
|
|
38
38
|
or `vigilbase help <command>`; `vigilbase help <tool>` shows how to run a tool.
|
|
@@ -143,7 +143,7 @@ read-only Cloudflare API token instead. When you are done:
|
|
|
143
143
|
npx @vigilbase/cli cloudflare-checkup logout # removes the read-only cf login and the cf it installed
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
See [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.
|
|
146
|
+
See [LOCAL.md](https://app.unpkg.com/@vigilbase/cli@0.2.3/files/LOCAL.md) for
|
|
147
147
|
every step, what is uploaded and the error codes.
|
|
148
148
|
|
|
149
149
|
## Public tools and sign-in
|
|
@@ -191,9 +191,12 @@ fails and vigilbase.com explains why, the CLI shows that reason:
|
|
|
191
191
|
`Error: <message> (<code>)` in text, and `error` plus `code` in JSON. The
|
|
192
192
|
reason is shown only when vigilbase.com sends it as a known error code, and it
|
|
193
193
|
is cleaned to one line of at most 300 characters. Anything else, and every
|
|
194
|
-
Vigilbase One error, prints
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
Vigilbase One error, prints a message the CLI wrote (for example
|
|
195
|
+
`You don't have access to this.` or `Request failed (HTTP <status>).`), never
|
|
196
|
+
the server's text. The exception is a One scan the scanner couldn't assess
|
|
197
|
+
(for example a bot verification challenge): the CLI prints the scanner's
|
|
198
|
+
reason, cleaned the same way. Hints (`Hint: …`, `hint` in JSON) are always written by the
|
|
199
|
+
CLI; COMMANDS.md lists the One errors that get one.
|
|
197
200
|
|
|
198
201
|
Commands use named One APIs and One's own authorization. They never take
|
|
199
202
|
Cloudflare or other provider secrets, delegated identity claims or an arbitrary
|
package/SECURITY.md
CHANGED
|
@@ -99,7 +99,7 @@ Provider events map `--provider` to `provider_id`, `--dataset` to `dataset_id`,
|
|
|
99
99
|
| `settings broadcast-emails` | None | `/settings/broadcast-emails` |
|
|
100
100
|
| `settings applications` | None | `/integrations/grants` |
|
|
101
101
|
|
|
102
|
-
Intel lookup input is trimmed and limited to 512 characters; the server validates the indicator and applies lookup quota. Personal settings show existing preferences and authorized application grants. They do not change preferences or revoke grants. Personal settings and `notifications preferences` belong to each user: with `--customer-id` (View-as), a refusal prints `
|
|
102
|
+
Intel lookup input is trimmed and limited to 512 characters; the server validates the indicator and applies lookup quota. Personal settings show existing preferences and authorized application grants. They do not change preferences or revoke grants. Personal settings and `notifications preferences` belong to each user: with `--customer-id` (View-as), a refusal prints `You don't have access to this.` with the hint `Personal settings belong to each user, so View-as (--customer-id) cannot read them.` Reports are fetched on demand; this CLI does not persist report or billing records.
|
|
103
103
|
|
|
104
104
|
## Limits and pagination
|
|
105
105
|
|
|
@@ -14,10 +14,13 @@ export declare class DeviceAuthError extends Error {
|
|
|
14
14
|
readonly code?: string;
|
|
15
15
|
readonly status: number;
|
|
16
16
|
readonly body: unknown;
|
|
17
|
+
/** The message is the CLI's own text, never the server's (see VigilbaseApiError). */
|
|
18
|
+
readonly isCliAuthored: boolean;
|
|
17
19
|
constructor(message: string, options: {
|
|
18
20
|
status: number;
|
|
19
21
|
code?: string;
|
|
20
22
|
body?: unknown;
|
|
23
|
+
isCliAuthored?: boolean;
|
|
21
24
|
});
|
|
22
25
|
}
|
|
23
26
|
export declare function requestDeviceCode(config: CliConfig, options?: {
|
package/dist/auth/device-auth.js
CHANGED
|
@@ -8,12 +8,15 @@ export class DeviceAuthError extends Error {
|
|
|
8
8
|
code;
|
|
9
9
|
status;
|
|
10
10
|
body;
|
|
11
|
+
/** The message is the CLI's own text, never the server's (see VigilbaseApiError). */
|
|
12
|
+
isCliAuthored;
|
|
11
13
|
constructor(message, options) {
|
|
12
14
|
super(message);
|
|
13
15
|
this.name = 'DeviceAuthError';
|
|
14
16
|
this.status = options.status;
|
|
15
17
|
this.code = options.code;
|
|
16
18
|
this.body = options.body;
|
|
19
|
+
this.isCliAuthored = options.isCliAuthored ?? false;
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
function authBase(config) {
|
|
@@ -139,7 +142,7 @@ export async function pollDeviceToken(config, deviceCode) {
|
|
|
139
142
|
},
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
|
-
throw new DeviceAuthError(`Device token request failed (HTTP ${status})`, { status, code, body: errorBody });
|
|
145
|
+
throw new DeviceAuthError(`Device token request failed (HTTP ${status})`, { status, code, body: errorBody, isCliAuthored: true });
|
|
143
146
|
}
|
|
144
147
|
export async function sleep(ms, signal) {
|
|
145
148
|
await new Promise((resolve, reject) => {
|
|
@@ -198,14 +201,19 @@ export async function waitForDeviceAuthorization(config, device, options) {
|
|
|
198
201
|
consecutiveTransient += 1;
|
|
199
202
|
lastTransientStatus = result.httpStatus;
|
|
200
203
|
if (consecutiveTransient >= maxTransient) {
|
|
204
|
+
// After an HTTP error the description is the server's, so only a network failure's
|
|
205
|
+
// own (CLI-authored) description joins the message.
|
|
206
|
+
const detail = (!lastTransientStatus && result.error.error_description) ||
|
|
207
|
+
'Retry `vigilbase login` once One is healthy.';
|
|
201
208
|
throw new DeviceAuthError(`Lost contact with device authorization on ${config.oneBaseUrl} while waiting for approval` +
|
|
202
209
|
(lastTransientStatus
|
|
203
210
|
? ` (HTTP ${lastTransientStatus})`
|
|
204
211
|
: '') +
|
|
205
|
-
`. ${
|
|
212
|
+
`. ${detail}`, {
|
|
206
213
|
status: lastTransientStatus,
|
|
207
214
|
code: 'unreachable',
|
|
208
215
|
body: result.error,
|
|
216
|
+
isCliAuthored: true,
|
|
209
217
|
});
|
|
210
218
|
}
|
|
211
219
|
break;
|
|
@@ -214,9 +222,10 @@ export async function waitForDeviceAuthorization(config, device, options) {
|
|
|
214
222
|
status: 403,
|
|
215
223
|
code: 'access_denied',
|
|
216
224
|
body: result.error,
|
|
225
|
+
isCliAuthored: true,
|
|
217
226
|
});
|
|
218
227
|
case 'expired':
|
|
219
|
-
throw new DeviceAuthError('The device code expired. Run `vigilbase login` again.', { status: 400, code: 'expired_token', body: result.error });
|
|
228
|
+
throw new DeviceAuthError('The device code expired. Run `vigilbase login` again.', { status: 400, code: 'expired_token', body: result.error, isCliAuthored: true });
|
|
220
229
|
default: {
|
|
221
230
|
const _exhaustive = result;
|
|
222
231
|
throw new DeviceAuthError(`Unexpected poll result: ${JSON.stringify(_exhaustive)}`, {
|
|
@@ -4,7 +4,7 @@ import { NetworkError } from '../lib/network.js';
|
|
|
4
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`.';
|
|
@@ -99,21 +99,27 @@ 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
125
|
validateArguments(ctx, 1, ['isNoSkill'], 'vigilbase describe <tool>');
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export declare class VigilbaseApiError extends Error {
|
|
|
9
9
|
readonly reason?: string;
|
|
10
10
|
/** CLI-authored remediation, never server text (see one-error-hints.ts). */
|
|
11
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;
|
|
12
19
|
constructor(message: string, options: {
|
|
13
20
|
status: number;
|
|
14
21
|
body?: unknown;
|
|
@@ -16,6 +23,8 @@ export declare class VigilbaseApiError extends Error {
|
|
|
16
23
|
code?: string;
|
|
17
24
|
reason?: string;
|
|
18
25
|
hint?: string;
|
|
26
|
+
isCliAuthored?: boolean;
|
|
27
|
+
toolRunId?: string;
|
|
19
28
|
});
|
|
20
29
|
}
|
|
21
30
|
export declare function retryAfterSeconds(response: Response, body: unknown): number | undefined;
|
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';
|
|
@@ -12,6 +12,13 @@ export class VigilbaseApiError extends Error {
|
|
|
12
12
|
reason;
|
|
13
13
|
/** CLI-authored remediation, never server text (see one-error-hints.ts). */
|
|
14
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;
|
|
15
22
|
constructor(message, options) {
|
|
16
23
|
super(message);
|
|
17
24
|
this.name = 'VigilbaseApiError';
|
|
@@ -21,6 +28,8 @@ export class VigilbaseApiError extends Error {
|
|
|
21
28
|
this.code = options.code;
|
|
22
29
|
this.reason = options.reason;
|
|
23
30
|
this.hint = options.hint;
|
|
31
|
+
this.isCliAuthored = options.isCliAuthored ?? false;
|
|
32
|
+
this.toolRunId = options.toolRunId;
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
35
|
export function retryAfterSeconds(response, body) {
|
|
@@ -61,6 +70,7 @@ function problemReason(response, body) {
|
|
|
61
70
|
const message = singleLine(problem.data.error.message);
|
|
62
71
|
return message ? { code: code.data, message } : undefined;
|
|
63
72
|
}
|
|
73
|
+
/** `fallback` is the CLI's own message, used when com gave no reason it accepts. */
|
|
64
74
|
function publicApiError(fallback, response, body) {
|
|
65
75
|
const reason = problemReason(response, body);
|
|
66
76
|
return new VigilbaseApiError(reason?.message ?? fallback, {
|
|
@@ -69,6 +79,7 @@ function publicApiError(fallback, response, body) {
|
|
|
69
79
|
retryAfterSec: retryAfterSeconds(response, body),
|
|
70
80
|
code: reason?.code,
|
|
71
81
|
reason: reason?.message,
|
|
82
|
+
isCliAuthored: !reason,
|
|
72
83
|
});
|
|
73
84
|
}
|
|
74
85
|
/** The versioned public tools API. The unversioned /api/agent/tools aliases sunset on 2027-09-01. */
|
|
@@ -131,7 +142,7 @@ export class VigilbaseClient {
|
|
|
131
142
|
throw new VigilbaseApiError('Invalid tool execution response. Check the API URL or retry later.', { status: response.status });
|
|
132
143
|
}
|
|
133
144
|
if (!parsed.success) {
|
|
134
|
-
throw publicApiError(
|
|
145
|
+
throw publicApiError(SCAN_FAILED_MESSAGE, response, body);
|
|
135
146
|
}
|
|
136
147
|
return parsed;
|
|
137
148
|
}
|
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);
|
package/dist/lib/one-client.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { CliConfig } from './config.js';
|
|
|
7
7
|
export type OneToolExecuteResponse = {
|
|
8
8
|
success: boolean;
|
|
9
9
|
error?: string;
|
|
10
|
+
/** `scan_unassessed`: the scanner ran but could not assess the target. */
|
|
11
|
+
code?: string;
|
|
10
12
|
data?: {
|
|
11
13
|
toolRunId?: string;
|
|
12
14
|
status?: string;
|
package/dist/lib/one-client.js
CHANGED
|
@@ -7,6 +7,7 @@ import { validateOneOrigin } from './config.js';
|
|
|
7
7
|
import { validateStripePortalUrl } from './open-url.js';
|
|
8
8
|
import { parseJsonResponse, requestText } from './network.js';
|
|
9
9
|
import { oneErrorHint } from './one-error-hints.js';
|
|
10
|
+
import { SCAN_FAILED_MESSAGE } from './format.js';
|
|
10
11
|
import { retryAfterSeconds, VigilbaseApiError } from './client.js';
|
|
11
12
|
/**
|
|
12
13
|
* Tools that require Vigilbase One authentication (auth-required scanners).
|
|
@@ -133,9 +134,12 @@ export class VigilbaseOneClient {
|
|
|
133
134
|
const hint = response.ok ? undefined : oneErrorHint({
|
|
134
135
|
method: init?.method ?? 'GET', path, status: response.status, code: errorDetails.code, hasCustomerScope: options?.hasCustomerScope,
|
|
135
136
|
});
|
|
137
|
+
// Every message below is the CLI's own; One's error text stays in the body, which is never printed.
|
|
136
138
|
if (response.status === 401 || response.status === 403) {
|
|
137
|
-
throw new VigilbaseApiError(message ||
|
|
138
|
-
'Authentication required. Run `vigilbase login`.'
|
|
139
|
+
throw new VigilbaseApiError(message || (response.status === 401
|
|
140
|
+
? 'Authentication required. Run `vigilbase login`.'
|
|
141
|
+
// Signed in, but One refuses this user or this request: logging in again won't help.
|
|
142
|
+
: "You don't have access to this."), { status: response.status, body: parsed, code: errorDetails.code, hint, isCliAuthored: true });
|
|
139
143
|
}
|
|
140
144
|
if (response.status === 429) {
|
|
141
145
|
throw new VigilbaseApiError(message || 'Rate limit exceeded.', {
|
|
@@ -143,18 +147,21 @@ export class VigilbaseOneClient {
|
|
|
143
147
|
body: parsed,
|
|
144
148
|
retryAfterSec: retryAfterSeconds(response, body),
|
|
145
149
|
code: errorDetails.code,
|
|
150
|
+
isCliAuthored: true,
|
|
146
151
|
});
|
|
147
152
|
}
|
|
148
153
|
const allowed = options?.allowStatuses ?? [];
|
|
149
154
|
if (!response.ok && !allowed.includes(response.status)) {
|
|
150
155
|
throw new VigilbaseApiError(message ||
|
|
151
|
-
`Request failed (HTTP ${response.status})
|
|
156
|
+
`Request failed (HTTP ${response.status}).`, { status: response.status, body: parsed, code: errorDetails.code, hint, isCliAuthored: true });
|
|
152
157
|
}
|
|
153
158
|
if (!z.record(z.string(), z.unknown()).safeParse(body).success) {
|
|
154
159
|
throw new VigilbaseApiError('Invalid One API response. Expected a JSON object.', { status: response.status });
|
|
155
160
|
}
|
|
156
161
|
if (parsed.success === false || ('ok' in parsed && parsed.ok === false)) {
|
|
157
|
-
throw new VigilbaseApiError('One API reported an unsuccessful operation.', {
|
|
162
|
+
throw new VigilbaseApiError('One API reported an unsuccessful operation.', {
|
|
163
|
+
status: response.status, body: parsed, code: errorDetails.code, isCliAuthored: true,
|
|
164
|
+
});
|
|
158
165
|
}
|
|
159
166
|
return parsed;
|
|
160
167
|
}
|
|
@@ -188,7 +195,7 @@ export class VigilbaseOneClient {
|
|
|
188
195
|
}
|
|
189
196
|
const { response, body } = await this.request('/api/auth/get-session');
|
|
190
197
|
if (!response.ok) {
|
|
191
|
-
throw new VigilbaseApiError(`Failed to load One session (HTTP ${response.status})`, { status: response.status, body });
|
|
198
|
+
throw new VigilbaseApiError(`Failed to load One session (HTTP ${response.status})`, { status: response.status, body, isCliAuthored: true });
|
|
192
199
|
}
|
|
193
200
|
const schema = z.object({
|
|
194
201
|
session: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
@@ -283,10 +290,70 @@ export class VigilbaseOneClient {
|
|
|
283
290
|
return this.requestJson(`/api/tool-runs/${encodeURIComponent(id)}`);
|
|
284
291
|
}
|
|
285
292
|
async executeTool(toolName, target) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
293
|
+
try {
|
|
294
|
+
return await this.requestJson(`/api/tools/${encodeURIComponent(toolName)}/execute`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({ target }),
|
|
298
|
+
}, { allowStatuses: [422] });
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
const unassessed = unassessedScan(error);
|
|
302
|
+
if (unassessed)
|
|
303
|
+
return unassessed;
|
|
304
|
+
throw failedScanError(error) ?? error;
|
|
305
|
+
}
|
|
291
306
|
}
|
|
292
307
|
}
|
|
308
|
+
/** One's answer when the scan itself failed, for example when com ran out of memory. */
|
|
309
|
+
const failedScanSchema = z.object({
|
|
310
|
+
success: z.literal(false),
|
|
311
|
+
data: z.object({ status: z.literal('failed'), toolRunId: z.unknown().optional() }).passthrough(),
|
|
312
|
+
}).passthrough();
|
|
313
|
+
const unassessedScanSchema = z.object({
|
|
314
|
+
code: z.literal('scan_unassessed'),
|
|
315
|
+
data: z.object({
|
|
316
|
+
toolRunId: z.unknown().optional(),
|
|
317
|
+
result: z.object({ status: z.literal('error'), message: z.string() }).passthrough(),
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
/**
|
|
321
|
+
* A scan the scanner ran but could not assess (a bot verification challenge, an
|
|
322
|
+
* error page). The result carries the scanner's message, which the formatter
|
|
323
|
+
* prints bounded like any scanner result; One's error text stays unused.
|
|
324
|
+
*/
|
|
325
|
+
function unassessedScan(error) {
|
|
326
|
+
if (!(error instanceof VigilbaseApiError) || error.status !== 422)
|
|
327
|
+
return undefined;
|
|
328
|
+
const unassessed = unassessedScanSchema.safeParse(error.body);
|
|
329
|
+
if (!unassessed.success)
|
|
330
|
+
return undefined;
|
|
331
|
+
const toolRunId = z.string().uuid().safeParse(unassessed.data.data.toolRunId);
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
code: 'scan_unassessed',
|
|
335
|
+
data: {
|
|
336
|
+
toolRunId: toolRunId.success ? toolRunId.data : undefined,
|
|
337
|
+
status: 'failed',
|
|
338
|
+
grade: null,
|
|
339
|
+
gradeScore: null,
|
|
340
|
+
result: unassessed.data.data.result,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** com's copy for a 422 failed scan, with the run One recorded; One's error text stays hidden. */
|
|
345
|
+
function failedScanError(error) {
|
|
346
|
+
if (!(error instanceof VigilbaseApiError) || error.status !== 422)
|
|
347
|
+
return undefined;
|
|
348
|
+
const failed = failedScanSchema.safeParse(error.body);
|
|
349
|
+
if (!failed.success)
|
|
350
|
+
return undefined;
|
|
351
|
+
const toolRunId = z.string().uuid().safeParse(failed.data.data.toolRunId);
|
|
352
|
+
return new VigilbaseApiError(SCAN_FAILED_MESSAGE, {
|
|
353
|
+
status: 422,
|
|
354
|
+
body: error.body,
|
|
355
|
+
code: error.code,
|
|
356
|
+
isCliAuthored: true,
|
|
357
|
+
toolRunId: toolRunId.success ? toolRunId.data : undefined,
|
|
358
|
+
});
|
|
359
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export type AgentToolExecuteResponse = {
|
|
|
53
53
|
error?: AgentApiError | string | null;
|
|
54
54
|
uiUrl?: string;
|
|
55
55
|
skillUrl?: string;
|
|
56
|
+
/** A One scan the scanner could not assess: One's status and code, and the run it recorded. */
|
|
57
|
+
status?: number;
|
|
58
|
+
code?: string;
|
|
59
|
+
toolRunId?: string;
|
|
56
60
|
retryAfterSec?: number;
|
|
57
61
|
toolsUrl?: string;
|
|
58
62
|
skillsIndex?: string;
|