@jango-blockchained/hoox-cli 0.5.2 → 0.5.6
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/bin/hoox.js +3 -4
- package/dist/index.js +32545 -0
- package/package.json +10 -15
- package/src/commands/check/check-command.test.ts +0 -476
- package/src/commands/check/check-command.ts +0 -1087
- package/src/commands/check/index.ts +0 -10
- package/src/commands/check/prerequisites-command.test.ts +0 -19
- package/src/commands/check/prerequisites-command.ts +0 -97
- package/src/commands/check/types.ts +0 -103
- package/src/commands/clone/clone-command.test.ts +0 -440
- package/src/commands/clone/clone-command.ts +0 -431
- package/src/commands/clone/index.ts +0 -1
- package/src/commands/config/config-command.test.ts +0 -583
- package/src/commands/config/config-command.ts +0 -896
- package/src/commands/config/env-command.test.ts +0 -43
- package/src/commands/config/env-command.ts +0 -292
- package/src/commands/config/index.ts +0 -3
- package/src/commands/config/kv-command.test.ts +0 -14
- package/src/commands/config/kv-command.ts +0 -290
- package/src/commands/dashboard/dashboard-command.test.ts +0 -47
- package/src/commands/dashboard/dashboard-command.ts +0 -130
- package/src/commands/dashboard/index.ts +0 -1
- package/src/commands/db/db-command.test.ts +0 -21
- package/src/commands/db/db-command.ts +0 -311
- package/src/commands/db/index.ts +0 -1
- package/src/commands/deploy/deploy-command.test.ts +0 -324
- package/src/commands/deploy/deploy-command.ts +0 -1054
- package/src/commands/deploy/index.ts +0 -2
- package/src/commands/deploy/telegram-service.ts +0 -76
- package/src/commands/deploy/types.ts +0 -34
- package/src/commands/dev/dev-command.test.ts +0 -405
- package/src/commands/dev/dev-command.ts +0 -398
- package/src/commands/dev/index.ts +0 -1
- package/src/commands/disclaimer/disclaimer-command.ts +0 -21
- package/src/commands/disclaimer/index.ts +0 -1
- package/src/commands/infra/index.ts +0 -5
- package/src/commands/infra/infra-command.test.ts +0 -719
- package/src/commands/infra/infra-command.ts +0 -974
- package/src/commands/infra/types.ts +0 -23
- package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
- package/src/commands/init/cli-provisioner.ts +0 -71
- package/src/commands/init/index.ts +0 -1
- package/src/commands/init/init-command.test.ts +0 -854
- package/src/commands/init/init-command.ts +0 -597
- package/src/commands/init/types.ts +0 -23
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/logs/logs-command.test.ts +0 -293
- package/src/commands/logs/logs-command.ts +0 -452
- package/src/commands/monitor/index.ts +0 -2
- package/src/commands/monitor/monitor-command.test.ts +0 -271
- package/src/commands/monitor/monitor-command.ts +0 -269
- package/src/commands/monitor/monitor-service.ts +0 -65
- package/src/commands/monitor/types.ts +0 -13
- package/src/commands/repair/index.ts +0 -2
- package/src/commands/repair/repair-command.test.ts +0 -243
- package/src/commands/repair/repair-command.ts +0 -220
- package/src/commands/repair/repair-service.ts +0 -164
- package/src/commands/repair/types.ts +0 -13
- package/src/commands/schema/index.ts +0 -1
- package/src/commands/schema/schema-command.ts +0 -137
- package/src/commands/test/index.ts +0 -2
- package/src/commands/test/test-command.test.ts +0 -318
- package/src/commands/test/test-command.ts +0 -406
- package/src/commands/tui/index.ts +0 -1
- package/src/commands/tui/tui-command.ts +0 -87
- package/src/commands/update/index.ts +0 -1
- package/src/commands/update/update-command.ts +0 -109
- package/src/commands/waf/index.ts +0 -2
- package/src/commands/waf/types.ts +0 -48
- package/src/commands/waf/waf-command.test.ts +0 -506
- package/src/commands/waf/waf-command.ts +0 -545
- package/src/index.ts +0 -261
- package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
- package/src/services/cloudflare/cloudflare-service.ts +0 -433
- package/src/services/cloudflare/index.ts +0 -2
- package/src/services/cloudflare/types.ts +0 -30
- package/src/services/config/config-service.test.ts +0 -395
- package/src/services/config/config-service.ts +0 -202
- package/src/services/config/index.ts +0 -9
- package/src/services/config/types.ts +0 -66
- package/src/services/db/db-service.test.ts +0 -63
- package/src/services/db/db-service.ts +0 -131
- package/src/services/db/index.ts +0 -1
- package/src/services/docker/docker-service.ts +0 -155
- package/src/services/docker/index.ts +0 -1
- package/src/services/env/env-service.test.ts +0 -259
- package/src/services/env/env-service.ts +0 -372
- package/src/services/env/index.ts +0 -1
- package/src/services/kv/index.ts +0 -1
- package/src/services/kv/kv-sync-service.test.ts +0 -38
- package/src/services/kv/kv-sync-service.ts +0 -243
- package/src/services/prerequisites/index.ts +0 -1
- package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
- package/src/services/prerequisites/prerequisites-service.ts +0 -377
- package/src/services/prerequisites/types.ts +0 -48
- package/src/services/schema/index.ts +0 -1
- package/src/services/schema/schema-service.ts +0 -99
- package/src/services/secrets/index.ts +0 -11
- package/src/services/secrets/secrets-service.test.ts +0 -502
- package/src/services/secrets/secrets-service.ts +0 -287
- package/src/services/secrets/types.ts +0 -50
- package/src/services/update/index.ts +0 -2
- package/src/services/update/update-service.test.ts +0 -76
- package/src/services/update/update-service.ts +0 -193
- package/src/ui/banner.ts +0 -141
- package/src/ui/index.ts +0 -8
- package/src/ui/menu.ts +0 -490
- package/src/utils/error-handler.ts +0 -62
- package/src/utils/errors.test.ts +0 -69
- package/src/utils/errors.ts +0 -24
- package/src/utils/formatters.test.ts +0 -180
- package/src/utils/formatters.ts +0 -269
- package/src/utils/git.ts +0 -134
- package/src/utils/theme.ts +0 -92
|
@@ -1,545 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `hoox waf` command — Cloudflare WAF (Web Application Firewall) management.
|
|
3
|
-
*
|
|
4
|
-
* Subcommands:
|
|
5
|
-
* waf status Show WAF status for the zone
|
|
6
|
-
* waf rules list List all active firewall rules
|
|
7
|
-
* waf rules add <type> <value> Add a WAF rule
|
|
8
|
-
* waf rules remove <id> Remove a WAF rule by ID
|
|
9
|
-
* waf mode enable Enable WAF protection
|
|
10
|
-
* waf mode disable Disable WAF protection
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { Command } from "commander";
|
|
14
|
-
import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
|
|
15
|
-
import type { WranglerResult } from "../../services/cloudflare/types.js";
|
|
16
|
-
import { CLIError, ExitCode } from "../../utils/errors.js";
|
|
17
|
-
import { formatSuccess, formatTable } from "../../utils/formatters.js";
|
|
18
|
-
import { withErrorHandling } from "../../utils/error-handler.js";
|
|
19
|
-
import { theme, icons } from "../../utils/theme.js";
|
|
20
|
-
import type { FormatOptions } from "../../utils/formatters.js";
|
|
21
|
-
import type { WafStatus, WafRule, WafRuleInput } from "./types.js";
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Cloudflare API helpers
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
const CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Reads the Cloudflare API token and account ID from environment variables.
|
|
31
|
-
* Required for direct REST API calls (wrangler doesn't have native WAF commands).
|
|
32
|
-
*/
|
|
33
|
-
function getCredentials(): { token: string; accountId: string } {
|
|
34
|
-
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
35
|
-
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
36
|
-
if (!token) {
|
|
37
|
-
throw new CLIError(
|
|
38
|
-
"CLOUDFLARE_API_TOKEN environment variable is not set. Set it or run `wrangler login`.",
|
|
39
|
-
ExitCode.ERROR
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (!accountId) {
|
|
43
|
-
throw new CLIError(
|
|
44
|
-
"CLOUDFLARE_ACCOUNT_ID environment variable is not set.",
|
|
45
|
-
ExitCode.ERROR
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return { token, accountId };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Makes an authenticated request to the Cloudflare REST API.
|
|
53
|
-
* Returns a WranglerResult-style discriminated union for consistent error handling.
|
|
54
|
-
*/
|
|
55
|
-
async function cfApi<T>(
|
|
56
|
-
method: "GET" | "POST" | "PATCH" | "DELETE",
|
|
57
|
-
path: string,
|
|
58
|
-
body?: unknown
|
|
59
|
-
): Promise<WranglerResult<T>> {
|
|
60
|
-
const { token } = getCredentials();
|
|
61
|
-
const url = `${CF_API_BASE}${path}`;
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch(url, {
|
|
65
|
-
method,
|
|
66
|
-
headers: {
|
|
67
|
-
Authorization: `Bearer ${token}`,
|
|
68
|
-
"Content-Type": "application/json",
|
|
69
|
-
},
|
|
70
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const json = (await response.json()) as {
|
|
74
|
-
success: boolean;
|
|
75
|
-
result: T;
|
|
76
|
-
errors: Array<{ message: string }>;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
if (!response.ok || !json.success) {
|
|
80
|
-
const errorMsg =
|
|
81
|
-
json.errors?.map((e) => e.message).join("; ") ||
|
|
82
|
-
`HTTP ${response.status}`;
|
|
83
|
-
return { ok: false, error: errorMsg };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return { ok: true, value: json.result };
|
|
87
|
-
} catch (err) {
|
|
88
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
89
|
-
return { ok: false, error: `Cloudflare API request failed: ${message}` };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// WAF API operations
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
interface CfWafSetting {
|
|
98
|
-
id: string;
|
|
99
|
-
value: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
interface CfFirewallRule {
|
|
103
|
-
id: string;
|
|
104
|
-
description: string;
|
|
105
|
-
action: string;
|
|
106
|
-
filter: { id: string; expression: string };
|
|
107
|
-
created_on: string;
|
|
108
|
-
modified_on: string;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface CfAnalyticsOverview {
|
|
112
|
-
totals: {
|
|
113
|
-
threats: number;
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Builds expression from rule type and value.
|
|
119
|
-
* ip-allowlist → (ip.src eq {value})
|
|
120
|
-
* ip-blocklist → (ip.src eq {value})
|
|
121
|
-
* rate-limit → handled as custom with rate-limit expression
|
|
122
|
-
* custom → uses value as raw expression
|
|
123
|
-
*/
|
|
124
|
-
function buildExpression(type: WafRuleInput["type"], value: string): string {
|
|
125
|
-
switch (type) {
|
|
126
|
-
case "ip-allowlist":
|
|
127
|
-
return `(ip.src eq ${value})`;
|
|
128
|
-
case "ip-blocklist":
|
|
129
|
-
return `(ip.src eq ${value})`;
|
|
130
|
-
case "rate-limit":
|
|
131
|
-
// Rate limiting is configured via a separate API; here we build a
|
|
132
|
-
// basic rate-limit expression that can be extended.
|
|
133
|
-
return `(http.request.uri.path contains "/")`;
|
|
134
|
-
case "custom":
|
|
135
|
-
return value;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Maps rule type and value to a Cloudflare firewall rule action.
|
|
141
|
-
*/
|
|
142
|
-
function mapAction(type: WafRuleInput["type"]): string {
|
|
143
|
-
switch (type) {
|
|
144
|
-
case "ip-allowlist":
|
|
145
|
-
return "allow";
|
|
146
|
-
case "ip-blocklist":
|
|
147
|
-
return "block";
|
|
148
|
-
case "rate-limit":
|
|
149
|
-
return "managed_challenge";
|
|
150
|
-
case "custom":
|
|
151
|
-
return "block";
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Fetches the Cloudflare zone to operate on. Uses CloudflareService.zonesList()
|
|
157
|
-
* and picks the first zone. Falls back to env CLOUDFLARE_ZONE_ID.
|
|
158
|
-
*/
|
|
159
|
-
async function resolveZone(
|
|
160
|
-
cf: CloudflareService
|
|
161
|
-
): Promise<{ id: string; name: string }> {
|
|
162
|
-
// Try explicit env var first
|
|
163
|
-
if (process.env.CLOUDFLARE_ZONE_ID) {
|
|
164
|
-
return {
|
|
165
|
-
id: process.env.CLOUDFLARE_ZONE_ID,
|
|
166
|
-
name: process.env.CLOUDFLARE_ZONE_ID,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const result = await cf.zonesList();
|
|
171
|
-
if (!result.ok) {
|
|
172
|
-
throw new CLIError(`Failed to list zones: ${result.error}`, ExitCode.ERROR);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// wrangler zones list outputs lines like "zone-name (zone-id)"
|
|
176
|
-
const lines = result.value.split("\n").filter(Boolean);
|
|
177
|
-
if (lines.length === 0) {
|
|
178
|
-
throw new CLIError(
|
|
179
|
-
"No zones found. Set CLOUDFLARE_ZONE_ID environment variable.",
|
|
180
|
-
ExitCode.ERROR
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Parse first zone: "domain.com (abc123...)"
|
|
185
|
-
const match = lines[0].match(/^(.+?)\s+\((.+?)\)\s*$/);
|
|
186
|
-
if (match) {
|
|
187
|
-
return { name: match[1], id: match[2] };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Fallback: use the first line as ID
|
|
191
|
-
return { name: lines[0], id: lines[0] };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
// Command action: waf status
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
|
|
198
|
-
async function handleStatus(opts: FormatOptions): Promise<void> {
|
|
199
|
-
const cf = new CloudflareService();
|
|
200
|
-
|
|
201
|
-
const zone = await resolveZone(cf);
|
|
202
|
-
|
|
203
|
-
// Fetch WAF setting
|
|
204
|
-
const wafResult = await cfApi<CfWafSetting>(
|
|
205
|
-
"GET",
|
|
206
|
-
`/zones/${zone.id}/settings/waf`
|
|
207
|
-
);
|
|
208
|
-
if (!wafResult.ok) {
|
|
209
|
-
throw new CLIError(
|
|
210
|
-
`Failed to get WAF status: ${wafResult.error}`,
|
|
211
|
-
ExitCode.ERROR
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Fetch firewall rules count
|
|
216
|
-
const rulesResult = await cfApi<CfFirewallRule[]>(
|
|
217
|
-
"GET",
|
|
218
|
-
`/zones/${zone.id}/firewall/rules?per_page=50`
|
|
219
|
-
);
|
|
220
|
-
const activeRulesCount = rulesResult.ok ? rulesResult.value.length : 0;
|
|
221
|
-
|
|
222
|
-
// Fetch recent blocks (last 24h analytics overview)
|
|
223
|
-
let recentBlocks = 0;
|
|
224
|
-
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
225
|
-
const analyticsResult = await cfApi<CfAnalyticsOverview>(
|
|
226
|
-
"GET",
|
|
227
|
-
`/zones/${zone.id}/analytics/dashboard?since=${encodeURIComponent(since)}&continuous=true`
|
|
228
|
-
);
|
|
229
|
-
if (analyticsResult.ok) {
|
|
230
|
-
recentBlocks = analyticsResult.value.totals?.threats ?? 0;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const status: WafStatus = {
|
|
234
|
-
enabled: wafResult.value.value === "on",
|
|
235
|
-
mode: wafResult.value.value,
|
|
236
|
-
activeRulesCount,
|
|
237
|
-
recentBlocks,
|
|
238
|
-
zoneId: zone.id,
|
|
239
|
-
zoneName: zone.name,
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
if (opts.json) {
|
|
243
|
-
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
process.stdout.write(theme.heading(`\nWAF status for ${zone.name}\n\n`));
|
|
248
|
-
|
|
249
|
-
const colorFn = status.enabled ? theme.success : theme.error;
|
|
250
|
-
process.stdout.write(
|
|
251
|
-
` Status: ${colorFn(status.enabled ? `${icons.success} ENABLED` : `${icons.error} DISABLED`)}\n`
|
|
252
|
-
);
|
|
253
|
-
process.stdout.write(` Mode: ${status.mode}\n`);
|
|
254
|
-
process.stdout.write(` Rules: ${status.activeRulesCount} active\n`);
|
|
255
|
-
process.stdout.write(` Blocks: ${status.recentBlocks} (last 24h)\n`);
|
|
256
|
-
process.stdout.write(` Zone ID: ${theme.dim(zone.id)}\n\n`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// Command action: waf rules list
|
|
261
|
-
// ---------------------------------------------------------------------------
|
|
262
|
-
|
|
263
|
-
async function handleRulesList(opts: FormatOptions): Promise<void> {
|
|
264
|
-
const cf = new CloudflareService();
|
|
265
|
-
const zone = await resolveZone(cf);
|
|
266
|
-
|
|
267
|
-
const result = await cfApi<CfFirewallRule[]>(
|
|
268
|
-
"GET",
|
|
269
|
-
`/zones/${zone.id}/firewall/rules?per_page=50`
|
|
270
|
-
);
|
|
271
|
-
if (!result.ok) {
|
|
272
|
-
throw new CLIError(
|
|
273
|
-
`Failed to list firewall rules: ${result.error}`,
|
|
274
|
-
ExitCode.ERROR
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const rules: WafRule[] = result.value.map((r) => ({
|
|
279
|
-
id: r.id,
|
|
280
|
-
description: r.description,
|
|
281
|
-
mode: r.action,
|
|
282
|
-
expression: r.filter?.expression ?? "(unknown)",
|
|
283
|
-
created_at: r.created_on,
|
|
284
|
-
updated_at: r.modified_on,
|
|
285
|
-
}));
|
|
286
|
-
|
|
287
|
-
if (opts.json) {
|
|
288
|
-
process.stdout.write(JSON.stringify(rules, null, 2) + "\n");
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (rules.length === 0) {
|
|
293
|
-
process.stdout.write(
|
|
294
|
-
`${theme.dim(`${icons.info} No firewall rules configured.\n`)}`
|
|
295
|
-
);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
process.stdout.write(theme.heading(`\nWAF rules for ${zone.name}\n\n`));
|
|
300
|
-
|
|
301
|
-
const rows = rules.map((r) => ({
|
|
302
|
-
ID: r.id.substring(0, 8) + "...",
|
|
303
|
-
Description: r.description || "(no description)",
|
|
304
|
-
Mode: r.mode,
|
|
305
|
-
Expression:
|
|
306
|
-
r.expression.length > 40
|
|
307
|
-
? r.expression.substring(0, 37) + "..."
|
|
308
|
-
: r.expression,
|
|
309
|
-
}));
|
|
310
|
-
|
|
311
|
-
formatTable(rows, opts);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// ---------------------------------------------------------------------------
|
|
315
|
-
// Command action: waf rules add
|
|
316
|
-
// ---------------------------------------------------------------------------
|
|
317
|
-
|
|
318
|
-
async function handleRulesAdd(
|
|
319
|
-
type: string,
|
|
320
|
-
value: string,
|
|
321
|
-
opts: FormatOptions
|
|
322
|
-
): Promise<void> {
|
|
323
|
-
const validTypes = ["ip-allowlist", "ip-blocklist", "rate-limit", "custom"];
|
|
324
|
-
if (!validTypes.includes(type)) {
|
|
325
|
-
throw new CLIError(
|
|
326
|
-
`Invalid rule type "${type}". Must be one of: ${validTypes.join(", ")}`,
|
|
327
|
-
ExitCode.INVALID_USAGE
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const ruleInput: WafRuleInput = {
|
|
332
|
-
type: type as WafRuleInput["type"],
|
|
333
|
-
value,
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const cf = new CloudflareService();
|
|
337
|
-
const zone = await resolveZone(cf);
|
|
338
|
-
|
|
339
|
-
const expression = buildExpression(ruleInput.type, value);
|
|
340
|
-
const action = mapAction(ruleInput.type);
|
|
341
|
-
|
|
342
|
-
const body = {
|
|
343
|
-
description: ruleInput.description ?? `${ruleInput.type} rule for ${value}`,
|
|
344
|
-
action,
|
|
345
|
-
filter: { expression },
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
// Create filter first, then rule referencing the filter
|
|
349
|
-
const filterResult = await cfApi<{ id: string }>(
|
|
350
|
-
"POST",
|
|
351
|
-
`/zones/${zone.id}/filters`,
|
|
352
|
-
{ expression }
|
|
353
|
-
);
|
|
354
|
-
if (!filterResult.ok) {
|
|
355
|
-
throw new CLIError(
|
|
356
|
-
`Failed to create filter: ${filterResult.error}`,
|
|
357
|
-
ExitCode.ERROR
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const ruleBody = {
|
|
362
|
-
description: body.description,
|
|
363
|
-
action,
|
|
364
|
-
filter: { id: filterResult.value.id },
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const ruleResult = await cfApi<CfFirewallRule>(
|
|
368
|
-
"POST",
|
|
369
|
-
`/zones/${zone.id}/firewall/rules`,
|
|
370
|
-
[ruleBody] // Cloudflare expects an array of rules
|
|
371
|
-
);
|
|
372
|
-
if (!ruleResult.ok) {
|
|
373
|
-
throw new CLIError(
|
|
374
|
-
`Failed to create firewall rule: ${ruleResult.error}`,
|
|
375
|
-
ExitCode.ERROR
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Cloudflare returns an array of created rules
|
|
380
|
-
const created = Array.isArray(ruleResult.value)
|
|
381
|
-
? (ruleResult.value[0] as unknown as CfFirewallRule)
|
|
382
|
-
: (ruleResult.value as unknown as CfFirewallRule);
|
|
383
|
-
|
|
384
|
-
formatSuccess(
|
|
385
|
-
`WAF rule added (${created.id}) — ${created.action} for "${value}"`,
|
|
386
|
-
opts
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
|
-
// Command action: waf rules remove
|
|
392
|
-
// ---------------------------------------------------------------------------
|
|
393
|
-
|
|
394
|
-
async function handleRulesRemove(
|
|
395
|
-
ruleId: string,
|
|
396
|
-
opts: FormatOptions
|
|
397
|
-
): Promise<void> {
|
|
398
|
-
const cf = new CloudflareService();
|
|
399
|
-
const zone = await resolveZone(cf);
|
|
400
|
-
|
|
401
|
-
const result = await cfApi<{ id: string }>(
|
|
402
|
-
"DELETE",
|
|
403
|
-
`/zones/${zone.id}/firewall/rules/${ruleId}`
|
|
404
|
-
);
|
|
405
|
-
if (!result.ok) {
|
|
406
|
-
throw new CLIError(
|
|
407
|
-
`Failed to remove firewall rule: ${result.error}`,
|
|
408
|
-
ExitCode.ERROR
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
formatSuccess(`WAF rule ${ruleId} removed`, opts);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ---------------------------------------------------------------------------
|
|
416
|
-
// Command action: waf mode enable / disable
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
|
|
419
|
-
async function handleMode(
|
|
420
|
-
mode: "on" | "off",
|
|
421
|
-
opts: FormatOptions
|
|
422
|
-
): Promise<void> {
|
|
423
|
-
const cf = new CloudflareService();
|
|
424
|
-
const zone = await resolveZone(cf);
|
|
425
|
-
|
|
426
|
-
const result = await cfApi<CfWafSetting>(
|
|
427
|
-
"PATCH",
|
|
428
|
-
`/zones/${zone.id}/settings/waf`,
|
|
429
|
-
{ value: mode }
|
|
430
|
-
);
|
|
431
|
-
if (!result.ok) {
|
|
432
|
-
throw new CLIError(
|
|
433
|
-
`Failed to ${mode === "on" ? "enable" : "disable"} WAF: ${result.error}`,
|
|
434
|
-
ExitCode.ERROR
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const label = mode === "on" ? "enabled" : "disabled";
|
|
439
|
-
formatSuccess(`WAF ${label} on zone ${zone.name}`, opts);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ---------------------------------------------------------------------------
|
|
443
|
-
// Command registration
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Registers the `hoox waf` command and all subcommands on the given
|
|
448
|
-
* Commander.js program instance.
|
|
449
|
-
*/
|
|
450
|
-
export function registerWafCommand(program: Command): void {
|
|
451
|
-
const waf = program
|
|
452
|
-
.command("waf")
|
|
453
|
-
.description("Manage Cloudflare WAF (Web Application Firewall)");
|
|
454
|
-
|
|
455
|
-
// -- waf status ------------------------------------------------------------
|
|
456
|
-
waf
|
|
457
|
-
.command("status")
|
|
458
|
-
.description(
|
|
459
|
-
"Show WAF status (enabled/disabled, active rules, recent blocks)"
|
|
460
|
-
)
|
|
461
|
-
.action(
|
|
462
|
-
withErrorHandling(
|
|
463
|
-
async (_, cmd: Command) => {
|
|
464
|
-
const opts: FormatOptions = cmd.parent!.parent!.opts();
|
|
465
|
-
await handleStatus(opts);
|
|
466
|
-
},
|
|
467
|
-
{ service: "waf" }
|
|
468
|
-
)
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
// -- waf rules -------------------------------------------------------------
|
|
472
|
-
const rules = waf.command("rules").description("Manage WAF firewall rules");
|
|
473
|
-
|
|
474
|
-
rules
|
|
475
|
-
.command("list")
|
|
476
|
-
.description("List all WAF firewall rules")
|
|
477
|
-
.action(
|
|
478
|
-
withErrorHandling(
|
|
479
|
-
async (_, cmd: Command) => {
|
|
480
|
-
const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
|
|
481
|
-
await handleRulesList(opts);
|
|
482
|
-
},
|
|
483
|
-
{ service: "waf" }
|
|
484
|
-
)
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
rules
|
|
488
|
-
.command("add <type> <value>")
|
|
489
|
-
.description(
|
|
490
|
-
"Add a WAF rule. Types: ip-allowlist, ip-blocklist, rate-limit, custom"
|
|
491
|
-
)
|
|
492
|
-
.action(
|
|
493
|
-
withErrorHandling(
|
|
494
|
-
async (type: string, value: string, _, cmd: Command) => {
|
|
495
|
-
const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
|
|
496
|
-
await handleRulesAdd(type, value, opts);
|
|
497
|
-
},
|
|
498
|
-
{ service: "waf" }
|
|
499
|
-
)
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
rules
|
|
503
|
-
.command("remove <ruleId>")
|
|
504
|
-
.description("Remove a WAF rule by ID")
|
|
505
|
-
.action(
|
|
506
|
-
withErrorHandling(
|
|
507
|
-
async (ruleId: string, _, cmd: Command) => {
|
|
508
|
-
const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
|
|
509
|
-
await handleRulesRemove(ruleId, opts);
|
|
510
|
-
},
|
|
511
|
-
{ service: "waf" }
|
|
512
|
-
)
|
|
513
|
-
);
|
|
514
|
-
|
|
515
|
-
// -- waf mode --------------------------------------------------------------
|
|
516
|
-
const mode = waf
|
|
517
|
-
.command("mode")
|
|
518
|
-
.description("Enable or disable WAF protection");
|
|
519
|
-
|
|
520
|
-
mode
|
|
521
|
-
.command("enable")
|
|
522
|
-
.description("Enable WAF protection on the zone")
|
|
523
|
-
.action(
|
|
524
|
-
withErrorHandling(
|
|
525
|
-
async (_, cmd: Command) => {
|
|
526
|
-
const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
|
|
527
|
-
await handleMode("on", opts);
|
|
528
|
-
},
|
|
529
|
-
{ service: "waf" }
|
|
530
|
-
)
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
mode
|
|
534
|
-
.command("disable")
|
|
535
|
-
.description("Disable WAF protection on the zone")
|
|
536
|
-
.action(
|
|
537
|
-
withErrorHandling(
|
|
538
|
-
async (_, cmd: Command) => {
|
|
539
|
-
const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
|
|
540
|
-
await handleMode("off", opts);
|
|
541
|
-
},
|
|
542
|
-
{ service: "waf" }
|
|
543
|
-
)
|
|
544
|
-
);
|
|
545
|
-
}
|