@vigilbase/cli 0.2.2 → 0.2.4
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 +2 -2
- package/COMMANDS.md +128 -42
- package/LOCAL.md +99 -38
- package/ONE.md +1 -1
- package/README.md +217 -66
- package/SECURITY.md +1 -1
- package/dist/auth/acl.d.ts +6 -0
- package/dist/auth/acl.js +45 -0
- package/dist/auth/device-auth.d.ts +3 -0
- package/dist/auth/device-auth.js +13 -3
- package/dist/auth/provider.js +4 -4
- package/dist/auth/refresh.d.ts +5 -4
- package/dist/auth/refresh.js +82 -41
- package/dist/auth/store.d.ts +25 -0
- package/dist/auth/store.js +91 -11
- package/dist/bin.js +13 -1
- package/dist/cli.js +205 -81
- package/dist/commands/account.js +11 -7
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.js +75 -24
- package/dist/commands/billing.js +2 -2
- package/dist/commands/cloudflare.d.ts +1 -1
- package/dist/commands/cloudflare.js +31 -17
- package/dist/commands/helpers.js +3 -3
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +2 -1
- package/dist/commands/legacy-validation.d.ts +20 -0
- package/dist/commands/legacy-validation.js +77 -3
- package/dist/commands/local.js +186 -65
- package/dist/commands/one.js +16 -13
- package/dist/commands/public-tools.js +66 -35
- package/dist/commands/runs.js +5 -5
- package/dist/commands/security.d.ts +1 -1
- package/dist/commands/security.js +41 -37
- package/dist/commands/watchdog.js +5 -5
- package/dist/lib/banner.d.ts +5 -11
- package/dist/lib/banner.js +11 -31
- package/dist/lib/client.d.ts +9 -0
- package/dist/lib/client.js +31 -2
- package/dist/lib/cloudflare-client.d.ts +2 -2
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +77 -19
- package/dist/lib/format.d.ts +5 -1
- package/dist/lib/format.js +110 -35
- package/dist/lib/help.js +57 -6
- package/dist/lib/network.d.ts +12 -3
- package/dist/lib/network.js +73 -9
- package/dist/lib/one-client.d.ts +2 -0
- package/dist/lib/one-client.js +86 -12
- package/dist/lib/open-url.js +2 -1
- package/dist/lib/platform.d.ts +2 -0
- package/dist/lib/platform.js +5 -0
- package/dist/lib/progress.d.ts +18 -3
- package/dist/lib/progress.js +72 -10
- package/dist/lib/system-trust.d.ts +2 -0
- package/dist/lib/system-trust.js +6 -0
- package/dist/lib/tui.d.ts +1 -1
- package/dist/lib/tui.js +5 -7
- package/dist/local/cloudflare-checkup/cf/child-env.js +2 -4
- package/dist/local/cloudflare-checkup/cf/install.d.ts +4 -2
- package/dist/local/cloudflare-checkup/cf/install.js +55 -24
- package/dist/local/cloudflare-checkup/cf/locate.d.ts +2 -0
- package/dist/local/cloudflare-checkup/cf/locate.js +10 -1
- package/dist/local/cloudflare-checkup/cf/run-cf.d.ts +18 -3
- package/dist/local/cloudflare-checkup/cf/run-cf.js +75 -17
- package/dist/local/cloudflare-checkup/contract.d.ts +16 -9
- package/dist/local/cloudflare-checkup/contract.js +80 -14
- package/dist/local/cloudflare-checkup/errors.d.ts +34 -1
- package/dist/local/cloudflare-checkup/errors.js +36 -5
- package/dist/local/cloudflare-checkup/output.d.ts +6 -0
- package/dist/local/cloudflare-checkup/output.js +9 -0
- package/dist/local/cloudflare-checkup/snapshot-file.d.ts +6 -2
- package/dist/local/cloudflare-checkup/snapshot-file.js +25 -11
- package/dist/local/cloudflare-checkup/submit-client.js +7 -1
- package/dist/types.d.ts +6 -1
- package/esm-only.cjs +3 -0
- package/package.json +7 -4
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { toolNameSchema, validateArguments } from './legacy-validation.js';
|
|
1
|
+
import { InvalidArgumentError, targetValue, toolNameValue, validateArguments } from './legacy-validation.js';
|
|
3
2
|
import { NetworkError } from '../lib/network.js';
|
|
4
3
|
import { PUBLIC_TOOLS_PATH, VigilbaseApiError } from '../lib/client.js';
|
|
5
4
|
import { getOneAccessToken, getOptionalOneAccessToken, hasUsableSignIn, readCredentials } from '../auth/index.js';
|
|
6
|
-
import { SignInUnsupportedError } from '../auth/store.js';
|
|
7
|
-
import { formatExecuteResult, formatToolDescription, formatToolsList, } from '../lib/format.js';
|
|
5
|
+
import { CredentialsPathError, NotSignedInError, SignInUnsupportedError } from '../auth/store.js';
|
|
6
|
+
import { formatExecuteResult, formatToolDescription, formatToolsList, isSuccessfulScan, } from '../lib/format.js';
|
|
8
7
|
import { isOneAuthRequiredTool, isOneExecutableTool, ONE_AUTH_REQUIRED_TOOLS, resolveToolDefinition, VigilbaseOneClient, } from '../lib/one-client.js';
|
|
9
8
|
import { EXIT, ok, outputJson, withSpinner, } from './helpers.js';
|
|
10
|
-
|
|
9
|
+
function unknownTool(toolName) {
|
|
10
|
+
return new InvalidArgumentError(`Unknown tool "${toolName}". Run \`vigilbase list\` for the allowlist.`);
|
|
11
|
+
}
|
|
11
12
|
/**
|
|
12
13
|
* For --via one and auth-required tools only: any sign-in problem fails. Specific errors keep their
|
|
13
14
|
* message; those that don't already point to `vigilbase login` get it as their hint.
|
|
@@ -18,14 +19,15 @@ async function requireOneSignIn(config) {
|
|
|
18
19
|
accessToken = await getOneAccessToken(config);
|
|
19
20
|
}
|
|
20
21
|
catch (error) {
|
|
21
|
-
// Windows cannot sign in at all, so pointing to
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Windows cannot sign in at all, and login stops on an unsafe credentials path too, so pointing to it would mislead.
|
|
23
|
+
const isLoginBlocked = error instanceof SignInUnsupportedError || error instanceof CredentialsPathError;
|
|
24
|
+
if (error instanceof Error && !isLoginBlocked && !error.message.includes('`vigilbase login`')) {
|
|
25
|
+
Object.assign(error, { hint: new NotSignedInError().message });
|
|
24
26
|
}
|
|
25
27
|
throw error;
|
|
26
28
|
}
|
|
27
29
|
if (!accessToken)
|
|
28
|
-
throw new
|
|
30
|
+
throw new NotSignedInError();
|
|
29
31
|
return accessToken;
|
|
30
32
|
}
|
|
31
33
|
/** Public tools work without login. Auth-required tools need Vigilbase One. */
|
|
@@ -39,6 +41,7 @@ export const handleList = async (ctx) => {
|
|
|
39
41
|
...catalog,
|
|
40
42
|
authenticated,
|
|
41
43
|
authRequiredTools: [...ONE_AUTH_REQUIRED_TOOLS],
|
|
44
|
+
// Signed out, nothing runs through One.
|
|
42
45
|
oneExecutableTools: authenticated
|
|
43
46
|
? [
|
|
44
47
|
...new Set([
|
|
@@ -46,7 +49,7 @@ export const handleList = async (ctx) => {
|
|
|
46
49
|
...ONE_AUTH_REQUIRED_TOOLS,
|
|
47
50
|
]),
|
|
48
51
|
]
|
|
49
|
-
:
|
|
52
|
+
: [],
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
else {
|
|
@@ -78,17 +81,23 @@ export const handleRun = async (ctx) => {
|
|
|
78
81
|
}
|
|
79
82
|
if (!toolName || !target)
|
|
80
83
|
throw new Error(`Usage: ${usage}`);
|
|
81
|
-
toolName =
|
|
82
|
-
target =
|
|
84
|
+
toolName = toolNameValue(toolName);
|
|
85
|
+
target = targetValue(target);
|
|
86
|
+
// Known tools are those One can run: the public ones and the One-only ones.
|
|
87
|
+
if (!isOneExecutableTool(toolName))
|
|
88
|
+
throw unknownTool(toolName);
|
|
83
89
|
const via = ctx.values.via;
|
|
84
90
|
if (ctx.values.isPublic && via === 'one') {
|
|
85
91
|
throw new Error('--public cannot be combined with --via one.');
|
|
86
92
|
}
|
|
87
|
-
if (via && via !== 'one' && via !== 'public') {
|
|
88
|
-
throw new
|
|
93
|
+
if (via !== undefined && via !== 'one' && via !== 'public') {
|
|
94
|
+
throw new InvalidArgumentError('--via must be one of: public, one.');
|
|
89
95
|
}
|
|
90
96
|
const forcePublic = Boolean(ctx.values.isPublic) || via === 'public';
|
|
91
97
|
const forceOne = via === 'one';
|
|
98
|
+
if (forcePublic && isOneAuthRequiredTool(toolName)) {
|
|
99
|
+
throw new Error(`${toolName} runs only through Vigilbase One: drop ${ctx.values.isPublic ? '--public' : '--via public'} and run \`vigilbase login\`.`);
|
|
100
|
+
}
|
|
92
101
|
// Public tools stay free unless forced onto One. Auth-required tools need login.
|
|
93
102
|
// The public route is anonymous and never reads the credential store.
|
|
94
103
|
const useOne = !forcePublic && (forceOne || isOneAuthRequiredTool(toolName));
|
|
@@ -99,33 +108,38 @@ export const handleRun = async (ctx) => {
|
|
|
99
108
|
}
|
|
100
109
|
const oneClient = new VigilbaseOneClient(ctx.config, accessToken, true);
|
|
101
110
|
const oneResult = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => oneClient.executeTool(toolName, target));
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
const unassessed = oneResult.code === 'scan_unassessed';
|
|
112
|
+
const toolRunId = oneResult.data?.toolRunId;
|
|
113
|
+
const result = {
|
|
114
|
+
success: oneResult.success === true,
|
|
104
115
|
toolName,
|
|
105
116
|
target,
|
|
106
117
|
grade: oneResult.data?.grade ?? null,
|
|
107
118
|
gradeScore: oneResult.data?.gradeScore ?? null,
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
// Only a completed scan, or one the scanner could not assess, carries the scanner's result.
|
|
120
|
+
result: oneResult.success === true || unassessed ? oneResult.data?.result ?? null : null,
|
|
121
|
+
// One's error text is server text; a failed scan prints the CLI's own message.
|
|
122
|
+
error: null,
|
|
110
123
|
uiUrl: `${ctx.config.oneBaseUrl}/dashboard/tools/${toolName}`,
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
...(unassessed ? { status: 422, code: 'scan_unassessed', ...(toolRunId ? { toolRunId } : {}) } : {}),
|
|
125
|
+
};
|
|
126
|
+
formatExecuteResult(result, ctx.mode, ctx.writer);
|
|
127
|
+
return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
|
|
113
128
|
}
|
|
114
129
|
const result = await withSpinner(ctx, `Running ${toolName} against ${target}`, () => ctx.publicClient.executeTool(toolName, target));
|
|
115
130
|
formatExecuteResult(result, ctx.mode, ctx.writer);
|
|
116
|
-
return result
|
|
131
|
+
return isSuccessfulScan(result) ? ok() : EXIT.ERROR;
|
|
117
132
|
};
|
|
118
133
|
export const handleDescribe = async (ctx) => {
|
|
119
134
|
validateArguments(ctx, 1, ['isNoSkill'], 'vigilbase describe <tool>');
|
|
120
|
-
|
|
135
|
+
toolNameValue(ctx.rest[0]);
|
|
121
136
|
const toolName = ctx.rest[0];
|
|
122
137
|
if (!toolName)
|
|
123
138
|
throw new Error('Usage: vigilbase describe <tool>');
|
|
124
139
|
const catalog = await withSpinner(ctx, 'Loading tool catalog', () => ctx.publicClient.listTools());
|
|
125
140
|
const tool = resolveToolDefinition(toolName, catalog.tools, ctx.config.oneBaseUrl);
|
|
126
|
-
if (!tool)
|
|
127
|
-
throw
|
|
128
|
-
}
|
|
141
|
+
if (!tool)
|
|
142
|
+
throw unknownTool(toolName);
|
|
129
143
|
let skillMarkdown;
|
|
130
144
|
if (!ctx.values.isNoSkill) {
|
|
131
145
|
try {
|
|
@@ -140,18 +154,18 @@ export const handleDescribe = async (ctx) => {
|
|
|
140
154
|
};
|
|
141
155
|
export const handleSkill = async (ctx) => {
|
|
142
156
|
validateArguments(ctx, 1, [], 'vigilbase skill <tool>');
|
|
143
|
-
|
|
157
|
+
toolNameValue(ctx.rest[0]);
|
|
144
158
|
const toolName = ctx.rest[0];
|
|
145
159
|
if (!toolName)
|
|
146
160
|
throw new Error('Usage: vigilbase skill <tool>');
|
|
147
161
|
const catalog = await withSpinner(ctx, 'Loading tool catalog', () => ctx.publicClient.listTools());
|
|
148
162
|
const definition = resolveToolDefinition(toolName, catalog.tools, ctx.config.oneBaseUrl);
|
|
149
|
-
if (!definition)
|
|
150
|
-
throw
|
|
151
|
-
}
|
|
163
|
+
if (!definition)
|
|
164
|
+
throw unknownTool(toolName);
|
|
152
165
|
const skillMarkdown = await withSpinner(ctx, 'Fetching skill docs', () => ctx.publicClient.fetchSkillMarkdown(definition.skillUrl));
|
|
153
166
|
if (ctx.mode === 'json') {
|
|
154
167
|
outputJson(ctx, {
|
|
168
|
+
success: true,
|
|
155
169
|
toolName: definition.toolName,
|
|
156
170
|
skillUrl: definition.skillUrl,
|
|
157
171
|
content: skillMarkdown,
|
|
@@ -162,18 +176,35 @@ export const handleSkill = async (ctx) => {
|
|
|
162
176
|
}
|
|
163
177
|
return ok();
|
|
164
178
|
};
|
|
179
|
+
/** The badge route's domain rule (apps/com/app/api/badge/[domain]/route.ts). */
|
|
180
|
+
const BADGE_DOMAIN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
181
|
+
/** The hostname the badge route accepts: a URL gives its hostname; case, a trailing dot and www. don't matter. */
|
|
182
|
+
function badgeDomain(value) {
|
|
183
|
+
let domain = value.trim();
|
|
184
|
+
try {
|
|
185
|
+
const url = new URL(domain);
|
|
186
|
+
if (url.protocol === 'https:' || url.protocol === 'http:')
|
|
187
|
+
domain = url.hostname;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Not a URL: a bare domain.
|
|
191
|
+
}
|
|
192
|
+
domain = domain.toLowerCase().replace(/\.$/, '').replace(/^www\./, '');
|
|
193
|
+
if (domain.length > 253 || !BADGE_DOMAIN.test(domain)) {
|
|
194
|
+
throw new InvalidArgumentError('Enter a domain such as vigilbase.com. Usage: vigilbase badge <domain>');
|
|
195
|
+
}
|
|
196
|
+
return domain;
|
|
197
|
+
}
|
|
165
198
|
export const handleBadge = async (ctx) => {
|
|
166
199
|
validateArguments(ctx, 1, ['style'], 'vigilbase badge <domain>');
|
|
167
|
-
const domain = ctx.rest[0];
|
|
168
|
-
if (!domain)
|
|
169
|
-
throw new Error('Usage: vigilbase badge <domain>');
|
|
200
|
+
const domain = badgeDomain(ctx.rest[0] ?? '');
|
|
170
201
|
const styleRaw = ctx.values.style ?? 'default';
|
|
171
202
|
if (styleRaw !== 'default' && styleRaw !== 'compact' && styleRaw !== 'mini') {
|
|
172
|
-
throw new
|
|
203
|
+
throw new InvalidArgumentError('--style must be one of: default, compact, mini.');
|
|
173
204
|
}
|
|
174
205
|
const url = ctx.publicClient.badgeUrl(domain, styleRaw);
|
|
175
206
|
if (ctx.mode === 'json') {
|
|
176
|
-
outputJson(ctx, { domain, style: styleRaw, url });
|
|
207
|
+
outputJson(ctx, { success: true, domain, style: styleRaw, url });
|
|
177
208
|
}
|
|
178
209
|
else {
|
|
179
210
|
ctx.writer.stdout(url);
|
|
@@ -240,7 +271,7 @@ export const handleDoctor = async (ctx) => {
|
|
|
240
271
|
const payload = {
|
|
241
272
|
ok: isOk,
|
|
242
273
|
success: isOk,
|
|
243
|
-
...(firstFailure ? { error: firstFailure.error, status: firstFailure.status } : {}),
|
|
274
|
+
...(firstFailure ? { error: firstFailure.error, status: firstFailure.status, ...(firstFailure.code ? { code: firstFailure.code } : {}) } : {}),
|
|
244
275
|
publicBaseUrl: ctx.config.publicBaseUrl,
|
|
245
276
|
oneBaseUrl: ctx.config.oneBaseUrl,
|
|
246
277
|
clientId: ctx.config.clientId,
|
package/dist/commands/runs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { limitValue,
|
|
2
|
+
import { limitValue, parseArgument, toolNameValue, unknownSubcommand, validateArguments } from './legacy-validation.js';
|
|
3
3
|
import { asArray, asRecord, ok, outputJson, requireOneClient, withSpinner, } from './helpers.js';
|
|
4
4
|
export const handleRuns = async (ctx) => {
|
|
5
5
|
const subcommand = (ctx.rest[0] ?? 'list').toLowerCase();
|
|
@@ -7,16 +7,16 @@ export const handleRuns = async (ctx) => {
|
|
|
7
7
|
if (subcommand === 'list' || subcommand === 'ls') {
|
|
8
8
|
validateArguments(ctx, ctx.rest[0] === undefined ? 0 : 1, ['tool', 'limit']);
|
|
9
9
|
limit = limitValue(ctx.values.limit, 50, 200);
|
|
10
|
-
toolName =
|
|
10
|
+
toolName = ctx.values.tool === undefined ? undefined : toolNameValue(ctx.values.tool);
|
|
11
11
|
}
|
|
12
12
|
else if (subcommand === 'get' || subcommand === 'show') {
|
|
13
13
|
validateArguments(ctx, 2, [], `vigilbase runs ${subcommand} <UUID>`);
|
|
14
|
-
z.string().uuid()
|
|
14
|
+
parseArgument(z.string().uuid(), ctx.rest[1], 'A valid UUID is required. Copy the full run ID from `vigilbase runs list`.');
|
|
15
15
|
}
|
|
16
16
|
else if (z.string().uuid().safeParse(subcommand).success)
|
|
17
17
|
validateArguments(ctx, 1);
|
|
18
18
|
else
|
|
19
|
-
throw
|
|
19
|
+
throw unknownSubcommand('runs', ctx.rest[0] ?? '');
|
|
20
20
|
const one = await requireOneClient(ctx);
|
|
21
21
|
if (subcommand === 'list' || subcommand === 'ls') {
|
|
22
22
|
const payload = await withSpinner(ctx, 'Loading tool runs', () => one.listToolRuns({
|
|
@@ -36,7 +36,7 @@ export const handleRuns = async (ctx) => {
|
|
|
36
36
|
const run = asRecord(raw);
|
|
37
37
|
if (!run)
|
|
38
38
|
continue;
|
|
39
|
-
ctx.writer.stdout(`${String(run.id)
|
|
39
|
+
ctx.writer.stdout(`${String(run.id)} ${String(run.tool_name).padEnd(18)} ${String(run.status).padEnd(10)} ${String(run.grade ?? '-')} ${String(run.target)}`);
|
|
40
40
|
}
|
|
41
41
|
return ok();
|
|
42
42
|
}
|
|
@@ -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
|
|
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\n --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 --start --end --limit --cursor --sort\n --asset-id --provider --dataset\n --search --frs-min\n audit --limit --cursor\n --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>\n analytics <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>.\nInternal View-as: --customer-id <One customer UUID>.\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for endpoints, supported values, aliases and pagination.\n";
|
|
@@ -1,22 +1,20 @@
|
|
|
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
|
+
import { InvalidArgumentError, inRange, isoDateTime, missingArguments, oneOf, parseArgument, textValue, unknownSubcommand } from './legacy-validation.js';
|
|
5
5
|
const statuses = ['open', 'active', 'acknowledged', 'investigating', 'mitigating', 'mitigated', 'resolved', 'false_positive'];
|
|
6
6
|
const updateStatuses = ['acknowledged', 'mitigating', 'resolved', 'false_positive'];
|
|
7
7
|
function textFlag(ctx, flag) {
|
|
8
8
|
const value = ctx.values[flag];
|
|
9
|
-
|
|
10
|
-
return undefined;
|
|
11
|
-
return z.string().trim().min(1, `--${flag} must not be empty.`).max(2048).parse(value);
|
|
9
|
+
return value === undefined ? undefined : textValue(`--${flag}`, String(value), 2048);
|
|
12
10
|
}
|
|
13
11
|
function integer(ctx, flag, max, fallback) {
|
|
14
12
|
const value = textFlag(ctx, flag);
|
|
15
13
|
if (value === undefined)
|
|
16
14
|
return fallback;
|
|
17
15
|
if (!/^\d+$/.test(value))
|
|
18
|
-
throw new
|
|
19
|
-
return
|
|
16
|
+
throw new InvalidArgumentError(`--${flag} must be an integer.`);
|
|
17
|
+
return inRange(`--${flag}`, Number(value), ['limit', 'hours', 'min-weight'].includes(flag) ? 1 : 0, max);
|
|
20
18
|
}
|
|
21
19
|
function allowed(ctx, flags) {
|
|
22
20
|
assertCommandFlags(ctx, [...flags, 'customer-id']);
|
|
@@ -28,6 +26,10 @@ function arity(ctx, length, usage) {
|
|
|
28
26
|
if (ctx.rest.length > length)
|
|
29
27
|
throw new Error('Unexpected security arguments. Run `vigilbase security --help` for usage.');
|
|
30
28
|
}
|
|
29
|
+
/** The entry of a command table named by user input; never an inherited Object.prototype member. */
|
|
30
|
+
function own(table, name) {
|
|
31
|
+
return name !== undefined && Object.hasOwn(table, name) ? table[name] : undefined;
|
|
32
|
+
}
|
|
31
33
|
function uuid(value) {
|
|
32
34
|
const parsed = z.string().uuid().safeParse(value);
|
|
33
35
|
if (!parsed.success)
|
|
@@ -44,8 +46,8 @@ function extendedSecurityRequest(ctx, scope) {
|
|
|
44
46
|
if (Boolean(start) !== Boolean(end))
|
|
45
47
|
throw new Error('--start and --end must be supplied together as ISO timestamps.');
|
|
46
48
|
if (start && end) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
isoDateTime('--start', start);
|
|
50
|
+
isoDateTime('--end', end);
|
|
49
51
|
if (Date.parse(start) >= Date.parse(end))
|
|
50
52
|
throw new Error('--start must be before --end.');
|
|
51
53
|
if (ctx.values.hours !== undefined)
|
|
@@ -79,9 +81,9 @@ function extendedSecurityRequest(ctx, scope) {
|
|
|
79
81
|
throw new Error('--search must be at most 200 characters for provider events.');
|
|
80
82
|
return request('/analytics/provider-events', 'providerEvents', { provider_id: provider, dataset_id: dataset, start: query.start, end: query.end, asset_ids: query.asset_id, search, limit: integer(ctx, 'limit', 500, 200) });
|
|
81
83
|
}
|
|
82
|
-
const view = views
|
|
84
|
+
const view = own(views, operation);
|
|
83
85
|
if (!view)
|
|
84
|
-
throw
|
|
86
|
+
throw unknownSubcommand('security analytics', operation);
|
|
85
87
|
const query = window(operation === 'overview' ? ['metrics'] : []);
|
|
86
88
|
if (operation === 'overview') {
|
|
87
89
|
const metrics = textFlag(ctx, 'metrics') ?? 'hourly,by_domain,by_dataset';
|
|
@@ -94,26 +96,28 @@ function extendedSecurityRequest(ctx, scope) {
|
|
|
94
96
|
}
|
|
95
97
|
if (group === 'zero-trust') {
|
|
96
98
|
const views = { 'device-health': 'deviceHealth', 'app-tests': 'appTests', gateway: 'gateway', access: 'access', sessions: 'sessions', 'device-posture': 'devicePosture' };
|
|
97
|
-
|
|
99
|
+
const view = own(views, sub);
|
|
100
|
+
if (!sub || !view)
|
|
98
101
|
throw new Error('Choose a zero-trust command: device-health, app-tests, gateway, access, sessions, device-posture.');
|
|
99
102
|
arity(ctx, sub === 'gateway' ? 3 : 2, 'zero-trust gateway <dns|http|network>');
|
|
100
103
|
const query = window();
|
|
101
104
|
if (sub === 'gateway')
|
|
102
|
-
query.surface =
|
|
103
|
-
return request(`/zero-trust/${sub}`,
|
|
105
|
+
query.surface = oneOf('Gateway surface', ['dns', 'http', 'network'], id);
|
|
106
|
+
return request(`/zero-trust/${sub}`, view, query);
|
|
104
107
|
}
|
|
105
108
|
if (group === 'cloudflare-account') {
|
|
106
109
|
const operation = sub ?? 'inventory';
|
|
107
110
|
const views = { inventory: 'accountInventory', audit: 'accountAudit', ingestion: 'accountIngestion', posture: 'accountPosture' };
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
const view = own(views, operation);
|
|
112
|
+
if (!view)
|
|
113
|
+
throw unknownSubcommand('security cloudflare-account', operation);
|
|
110
114
|
arity(ctx, sub === undefined ? 1 : 2);
|
|
111
115
|
let query = {};
|
|
112
116
|
if (operation === 'inventory')
|
|
113
117
|
allowed(ctx, []);
|
|
114
118
|
else
|
|
115
119
|
query = window();
|
|
116
|
-
return request(`/cloudflare-account/${operation}`,
|
|
120
|
+
return request(`/cloudflare-account/${operation}`, view, query);
|
|
117
121
|
}
|
|
118
122
|
if (group === 'providers') {
|
|
119
123
|
arity(ctx, 1);
|
|
@@ -123,9 +127,9 @@ function extendedSecurityRequest(ctx, scope) {
|
|
|
123
127
|
if (group === 'notifications') {
|
|
124
128
|
const operation = sub ?? 'list';
|
|
125
129
|
const routes = { list: ['/notifications', 'notifications'], settings: ['/notifications/settings', 'notificationSettings'], preferences: ['/notifications/preferences', 'preferences'], 'slack-channels': ['/notifications/slack/channels', 'slackChannels'] };
|
|
126
|
-
const route = routes
|
|
130
|
+
const route = own(routes, operation);
|
|
127
131
|
if (!route)
|
|
128
|
-
throw
|
|
132
|
+
throw unknownSubcommand('security notifications', operation);
|
|
129
133
|
arity(ctx, sub === undefined ? 1 : 2);
|
|
130
134
|
allowed(ctx, operation === 'list' ? ['limit'] : []);
|
|
131
135
|
return request(...route, operation === 'list' ? { limit: integer(ctx, 'limit', 100, 100) } : {});
|
|
@@ -136,9 +140,9 @@ function extendedSecurityRequest(ctx, scope) {
|
|
|
136
140
|
remediation: (assetId) => [`/assets/${assetId}/remediation-settings`, 'assetRemediation'], autonomy: (assetId) => [`/assets/${assetId}/vigil-autonomy`, 'autonomy'],
|
|
137
141
|
datasets: (assetId) => [`/fabric/assets/${assetId}/datasets`, 'datasets'], graph: (assetId) => [`/fabric/assets/${assetId}/graph`, 'graph'],
|
|
138
142
|
};
|
|
139
|
-
const route =
|
|
143
|
+
const route = own(routes, sub);
|
|
140
144
|
if (!route)
|
|
141
|
-
throw
|
|
145
|
+
throw unknownSubcommand('security assets', sub);
|
|
142
146
|
arity(ctx, 3, `assets ${sub} <asset-id>`);
|
|
143
147
|
if (sub !== 'graph')
|
|
144
148
|
allowed(ctx, []);
|
|
@@ -174,12 +178,12 @@ function extendedSecurityRequest(ctx, scope) {
|
|
|
174
178
|
return request(`/alerts/${uuid(id)}`, 'alert');
|
|
175
179
|
}
|
|
176
180
|
if (sub !== undefined && sub !== 'list')
|
|
177
|
-
throw
|
|
181
|
+
throw unknownSubcommand('security alerts', sub);
|
|
178
182
|
arity(ctx, sub === undefined ? 1 : 2);
|
|
179
183
|
allowed(ctx, ['limit', 'asset-id', 'start']);
|
|
180
184
|
const since = textFlag(ctx, 'start');
|
|
181
185
|
if (since)
|
|
182
|
-
|
|
186
|
+
isoDateTime('--start', since);
|
|
183
187
|
return request('/alerts', 'alerts', { limit: integer(ctx, 'limit', 100, 100), asset_id: ctx.values['asset-id'] !== undefined ? uuid(ctx.values['asset-id']) : undefined, since });
|
|
184
188
|
}
|
|
185
189
|
if (group === 'incidents' && (sub === 'fabric' || sub === 'share-links')) {
|
|
@@ -190,7 +194,7 @@ function extendedSecurityRequest(ctx, scope) {
|
|
|
190
194
|
}
|
|
191
195
|
if (group === 'settings') {
|
|
192
196
|
const routes = { 'lifecycle-emails': ['/settings/lifecycle-emails', 'lifecycleEmails'], 'broadcast-emails': ['/settings/broadcast-emails', 'broadcastEmails'], applications: ['/integrations/grants', 'applications'] };
|
|
193
|
-
const route =
|
|
197
|
+
const route = own(routes, sub);
|
|
194
198
|
if (!route)
|
|
195
199
|
throw new Error('Choose settings lifecycle-emails, broadcast-emails, or applications.');
|
|
196
200
|
arity(ctx, 2);
|
|
@@ -228,13 +232,13 @@ export function buildSecurityRequest(ctx) {
|
|
|
228
232
|
arity(ctx, 4, 'incidents update <id> <status>');
|
|
229
233
|
allowed(ctx, ['note', 'isConfirm']);
|
|
230
234
|
const id = uuid(ctx.rest[2]);
|
|
231
|
-
const status =
|
|
235
|
+
const status = oneOf('Status', updateStatuses, ctx.rest[3]);
|
|
232
236
|
if (!ctx.values.isConfirm)
|
|
233
237
|
throw new Error('Updating an incident requires --confirm. Your One permissions still apply.');
|
|
234
238
|
return { ...request(`/incidents/${id}`, 'update'), body: { status, ...(ctx.values.note !== undefined ? { note: textFlag(ctx, 'note') } : {}) } };
|
|
235
239
|
}
|
|
236
240
|
if (command !== 'list' && command !== 'ls')
|
|
237
|
-
throw
|
|
241
|
+
throw unknownSubcommand(`security ${group}`, command);
|
|
238
242
|
arity(ctx, ctx.rest[1] === undefined ? 1 : 2);
|
|
239
243
|
if (group === 'connections') {
|
|
240
244
|
allowed(ctx, []);
|
|
@@ -247,11 +251,11 @@ export function buildSecurityRequest(ctx) {
|
|
|
247
251
|
allowed(ctx, ['limit', 'offset', 'status', 'severity', 'attack-category', 'order-by', 'order-direction']);
|
|
248
252
|
return request('/incidents', 'incidents', {
|
|
249
253
|
limit: integer(ctx, 'limit', 100, 50), offset: integer(ctx, 'offset', 1_000_000),
|
|
250
|
-
status:
|
|
251
|
-
severity:
|
|
254
|
+
status: oneOf('--status', statuses, textFlag(ctx, 'status')),
|
|
255
|
+
severity: oneOf('--severity', ['critical', 'high', 'medium', 'low'], textFlag(ctx, 'severity')),
|
|
252
256
|
attack_category: textFlag(ctx, 'attack-category'),
|
|
253
|
-
order_by:
|
|
254
|
-
order_direction:
|
|
257
|
+
order_by: oneOf('--order-by', ['last_alert_at', 'first_alert_at', 'created_at', 'severity_rank', 'combined_score', 'alert_count'], textFlag(ctx, 'order-by')),
|
|
258
|
+
order_direction: oneOf('--order-direction', ['asc', 'desc'], textFlag(ctx, 'order-direction')),
|
|
255
259
|
});
|
|
256
260
|
}
|
|
257
261
|
if (group === 'assets') {
|
|
@@ -274,8 +278,8 @@ export function buildSecurityRequest(ctx) {
|
|
|
274
278
|
if (Boolean(start) !== Boolean(end))
|
|
275
279
|
throw new Error('--start and --end must be supplied together as ISO timestamps.');
|
|
276
280
|
if (start && end) {
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
isoDateTime('--start', start);
|
|
282
|
+
isoDateTime('--end', end);
|
|
279
283
|
if (Date.parse(start) >= Date.parse(end))
|
|
280
284
|
throw new Error('--start must be before --end.');
|
|
281
285
|
}
|
|
@@ -283,7 +287,7 @@ export function buildSecurityRequest(ctx) {
|
|
|
283
287
|
limit: integer(ctx, 'limit', 1000, 100), asset_id: ctx.values['asset-id'] !== undefined ? uuid(ctx.values['asset-id']) : undefined,
|
|
284
288
|
provider: textFlag(ctx, 'provider'), dataset: textFlag(ctx, 'dataset'), search: textFlag(ctx, 'search'),
|
|
285
289
|
start_time: start, end_time: end, cursor: textFlag(ctx, 'cursor'),
|
|
286
|
-
sort:
|
|
290
|
+
sort: oneOf('--sort', ['ts', 'frs', 'frs_desc'], textFlag(ctx, 'sort')),
|
|
287
291
|
frs_min: integer(ctx, 'frs-min', 100),
|
|
288
292
|
});
|
|
289
293
|
}
|
|
@@ -292,7 +296,7 @@ export function buildSecurityRequest(ctx) {
|
|
|
292
296
|
allowed(ctx, ['limit', 'cursor', 'action-prefix', 'outcome']);
|
|
293
297
|
return request('/vigilbase-one/audit-events', 'audit', {
|
|
294
298
|
limit: integer(ctx, 'limit', 100, 50), cursor: textFlag(ctx, 'cursor'), action_prefix: textFlag(ctx, 'action-prefix'),
|
|
295
|
-
outcome:
|
|
299
|
+
outcome: oneOf('--outcome', ['success', 'failure', 'denied'], textFlag(ctx, 'outcome')),
|
|
296
300
|
});
|
|
297
301
|
}
|
|
298
302
|
if (group === 'intel') {
|
|
@@ -302,11 +306,11 @@ export function buildSecurityRequest(ctx) {
|
|
|
302
306
|
return request('/intel/quota', 'quota');
|
|
303
307
|
}
|
|
304
308
|
arity(ctx, 3, 'intel <ip|asn|cidr|ja3|ja4> <query>');
|
|
305
|
-
const type =
|
|
306
|
-
const q = z.string().trim().min(1).max(512)
|
|
309
|
+
const type = oneOf('Lookup type', ['ip', 'asn', 'cidr', 'ja3', 'ja4'], command);
|
|
310
|
+
const q = parseArgument(z.string().trim().min(1).max(512), ctx.rest[2], 'The intel query must be 1-512 characters.');
|
|
307
311
|
return request('/intel/lookup', 'intel', { q, type });
|
|
308
312
|
}
|
|
309
|
-
throw
|
|
313
|
+
throw unknownSubcommand('security', group);
|
|
310
314
|
}
|
|
311
315
|
function cell(value) {
|
|
312
316
|
// Security evidence may contain attacker-controlled text; keep each terminal row literal.
|
|
@@ -399,4 +403,4 @@ export const handleSecurity = async (ctx) => {
|
|
|
399
403
|
display(ctx, request, payload);
|
|
400
404
|
return ok();
|
|
401
405
|
};
|
|
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
|
|
406
|
+
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\n --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 --start --end --limit --cursor --sort\n --asset-id --provider --dataset\n --search --frs-min\n audit --limit --cursor\n --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>\n analytics <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>.\nInternal View-as: --customer-id <One customer UUID>.\nAll operations read data except confirmed incident status updates.\nUpdate status: acknowledged, mitigating, resolved, false_positive.\nSee SECURITY.md for endpoints, supported values, aliases and pagination.\n";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { limitValue, validateArguments } from './legacy-validation.js';
|
|
2
|
+
import { limitValue, parseArgument, unknownSubcommand, validateArguments } from './legacy-validation.js';
|
|
3
3
|
import { asArray, asRecord, ok, outputJson, requireOneClient, withSpinner, } from './helpers.js';
|
|
4
4
|
export const handleWatchdog = async (ctx) => {
|
|
5
5
|
const subcommand = (ctx.rest[0] ?? 'list').toLowerCase();
|
|
@@ -7,12 +7,12 @@ export const handleWatchdog = async (ctx) => {
|
|
|
7
7
|
if (subcommand === 'scans' || subcommand === 'scan') {
|
|
8
8
|
validateArguments(ctx, 1, ['target-id', 'limit']);
|
|
9
9
|
limit = limitValue(ctx.values.limit, 20, 100);
|
|
10
|
-
targetId = z.string().uuid().optional()
|
|
10
|
+
targetId = parseArgument(z.string().uuid().optional(), ctx.values['target-id'], '--target-id must be a UUID. Copy the full ID from its list command.');
|
|
11
11
|
}
|
|
12
12
|
else if (['list', 'targets', 'status'].includes(subcommand))
|
|
13
13
|
validateArguments(ctx, ctx.rest[0] === undefined ? 0 : 1);
|
|
14
14
|
else
|
|
15
|
-
throw
|
|
15
|
+
throw unknownSubcommand('watchdog', ctx.rest[0] ?? '');
|
|
16
16
|
const one = await requireOneClient(ctx);
|
|
17
17
|
if (subcommand === 'list' || subcommand === 'targets' || subcommand === 'status') {
|
|
18
18
|
const payload = await withSpinner(ctx, 'Loading watchdog targets', () => one.getWatchdogTargets());
|
|
@@ -29,7 +29,7 @@ export const handleWatchdog = async (ctx) => {
|
|
|
29
29
|
const target = asRecord(raw);
|
|
30
30
|
if (!target)
|
|
31
31
|
continue;
|
|
32
|
-
ctx.writer.stdout(`${String(target.domain).padEnd(28)} ${String(target.uptimeStatus || '-')}${typeof target.uptime24h === 'number'
|
|
32
|
+
ctx.writer.stdout(`${String(target.id ?? '-')} ${String(target.domain).padEnd(28)} ${String(target.uptimeStatus || '-')}${typeof target.uptime24h === 'number'
|
|
33
33
|
? ` 24h=${Math.round(target.uptime24h * 100) / 100}`
|
|
34
34
|
: ''}`);
|
|
35
35
|
}
|
|
@@ -53,7 +53,7 @@ export const handleWatchdog = async (ctx) => {
|
|
|
53
53
|
const scan = asRecord(raw);
|
|
54
54
|
if (!scan)
|
|
55
55
|
continue;
|
|
56
|
-
ctx.writer.stdout(`${String(scan.id)
|
|
56
|
+
ctx.writer.stdout(`${String(scan.id)} ${String(scan.status || '-').padEnd(10)} ${String(scan.scanType || '-')} ${String(scan.createdAt || '')}`);
|
|
57
57
|
}
|
|
58
58
|
return ok();
|
|
59
59
|
}
|
package/dist/lib/banner.d.ts
CHANGED
|
@@ -2,27 +2,21 @@
|
|
|
2
2
|
* Compact Vigilbase flare mark + wordmark for human CLI surfaces.
|
|
3
3
|
* Static strings only — no I/O, no deps, skipped entirely in --json mode.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* Compact square Vigilbase flare (inverted V).
|
|
7
|
-
* Left wing (full block) = terminal default fg; right wing (medium shade) = brand yellow.
|
|
8
|
-
*/
|
|
9
|
-
/** Compact flare used on help/version (keeps the first viewport short). */
|
|
5
|
+
/** Compact square Vigilbase flare (inverted V), used on help/version (keeps the first viewport short). */
|
|
10
6
|
export declare const VIGILBASE_ASCII_MARK: string;
|
|
11
7
|
export type BannerOptions = {
|
|
12
8
|
/** Include version line under the mark. */
|
|
13
9
|
version?: string;
|
|
14
10
|
/** One-line tagline. */
|
|
15
11
|
tagline?: string;
|
|
16
|
-
/** Force ANSI on/off. Default: auto from TTY + env. */
|
|
17
|
-
useColor?: boolean;
|
|
18
12
|
};
|
|
13
|
+
/** Whether the checkup terminal UI (tui.ts) may colour this stream. */
|
|
19
14
|
export declare function shouldUseColor(stream: {
|
|
20
15
|
isTTY?: boolean;
|
|
21
16
|
} | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
22
17
|
export declare function shouldShowBanner(mode?: 'text' | 'json', env?: NodeJS.ProcessEnv): boolean;
|
|
23
18
|
/**
|
|
24
|
-
* Banner composition (flare + wordmark + tagline).
|
|
19
|
+
* Banner composition (flare + wordmark + tagline). Plain text: the writer strips terminal
|
|
20
|
+
* sequences from every line, so the banner carries no colour.
|
|
25
21
|
*/
|
|
26
|
-
export declare function renderBanner(options?: BannerOptions
|
|
27
|
-
isTTY?: boolean;
|
|
28
|
-
} | undefined, env?: NodeJS.ProcessEnv): string;
|
|
22
|
+
export declare function renderBanner(options?: BannerOptions): string;
|