@kill-switch/cli 0.1.0
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/dist/api-client.d.ts +15 -0
- package/dist/api-client.js +46 -0
- package/dist/commands/accounts.d.ts +2 -0
- package/dist/commands/accounts.js +133 -0
- package/dist/commands/alerts.d.ts +2 -0
- package/dist/commands/alerts.js +49 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +35 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +85 -0
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +37 -0
- package/dist/commands/config-cmd.d.ts +2 -0
- package/dist/commands/config-cmd.js +72 -0
- package/dist/commands/kill.d.ts +2 -0
- package/dist/commands/kill.js +125 -0
- package/dist/commands/onboard.d.ts +24 -0
- package/dist/commands/onboard.js +340 -0
- package/dist/commands/rules.d.ts +2 -0
- package/dist/commands/rules.js +121 -0
- package/dist/commands/shield.d.ts +2 -0
- package/dist/commands/shield.js +57 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +49 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +42 -0
- package/dist/output.d.ts +18 -0
- package/dist/output.js +60 -0
- package/package.json +28 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboard Command
|
|
3
|
+
*
|
|
4
|
+
* One-command setup for connecting cloud providers, applying protection rules,
|
|
5
|
+
* and configuring alerts. Designed for both human use and AI agent automation.
|
|
6
|
+
*
|
|
7
|
+
* Human use (interactive prompts):
|
|
8
|
+
* kill-switch onboard
|
|
9
|
+
*
|
|
10
|
+
* AI agent use (non-interactive):
|
|
11
|
+
* kill-switch onboard \
|
|
12
|
+
* --provider cloudflare \
|
|
13
|
+
* --account-id YOUR_CF_ACCOUNT_ID \
|
|
14
|
+
* --token YOUR_CF_API_TOKEN \
|
|
15
|
+
* --name "Production" \
|
|
16
|
+
* --shields cost-runaway,ddos \
|
|
17
|
+
* --alert-email you@example.com
|
|
18
|
+
*
|
|
19
|
+
* Multi-provider (run multiple times or comma-separate):
|
|
20
|
+
* kill-switch onboard --provider cloudflare --token ... --account-id ...
|
|
21
|
+
* kill-switch onboard --provider aws --access-key ... --secret-key ... --region us-east-1
|
|
22
|
+
*/
|
|
23
|
+
import { apiRequest } from "../api-client.js";
|
|
24
|
+
import { outputJson, outputError } from "../output.js";
|
|
25
|
+
import { createInterface } from "readline";
|
|
26
|
+
const PROVIDER_HELP = {
|
|
27
|
+
cloudflare: {
|
|
28
|
+
name: "Cloudflare",
|
|
29
|
+
fields: "--account-id and --token",
|
|
30
|
+
howToGet: `How to get these values:
|
|
31
|
+
|
|
32
|
+
Account ID:
|
|
33
|
+
Found in your browser URL bar on any Cloudflare dashboard page:
|
|
34
|
+
https://dash.cloudflare.com/<ACCOUNT_ID>/example.com
|
|
35
|
+
Or run: curl -s -H "Authorization: Bearer TOKEN" https://api.cloudflare.com/client/v4/accounts | jq '.result[].id'
|
|
36
|
+
|
|
37
|
+
API Token (NOT Global API Key):
|
|
38
|
+
1. Go to https://dash.cloudflare.com/profile/api-tokens
|
|
39
|
+
2. Click "Create Token"
|
|
40
|
+
3. Use the "Edit Cloudflare Workers" template, or create custom with:
|
|
41
|
+
- Account > Account Analytics > Read
|
|
42
|
+
- Account > Workers Scripts > Edit
|
|
43
|
+
- Account > Workers R2 Storage > Read
|
|
44
|
+
- Account > D1 > Read
|
|
45
|
+
- Zone > Zone > Read
|
|
46
|
+
4. Copy the token (starts with a long alphanumeric string)
|
|
47
|
+
|
|
48
|
+
NOTE: The Global API Key will NOT work. You must create an API Token.`,
|
|
49
|
+
},
|
|
50
|
+
gcp: {
|
|
51
|
+
name: "Google Cloud",
|
|
52
|
+
fields: "--project-id and --service-account",
|
|
53
|
+
howToGet: `How to get these values:
|
|
54
|
+
|
|
55
|
+
Project ID:
|
|
56
|
+
Run: gcloud config get-value project
|
|
57
|
+
Or find it at: https://console.cloud.google.com/home/dashboard (project selector)
|
|
58
|
+
|
|
59
|
+
Service Account Key (JSON):
|
|
60
|
+
1. Go to https://console.cloud.google.com/iam-admin/serviceaccounts
|
|
61
|
+
2. Create a service account with "Viewer" + "Cloud Run Admin" roles
|
|
62
|
+
3. Create a JSON key: Actions > Manage Keys > Add Key > JSON
|
|
63
|
+
4. Pass the file contents: --service-account "$(cat key.json)"`,
|
|
64
|
+
},
|
|
65
|
+
aws: {
|
|
66
|
+
name: "Amazon Web Services",
|
|
67
|
+
fields: "--access-key, --secret-key, and --region",
|
|
68
|
+
howToGet: `How to get these values:
|
|
69
|
+
|
|
70
|
+
Access Key ID & Secret Access Key:
|
|
71
|
+
1. Go to https://console.aws.amazon.com/iam/home#/security_credentials
|
|
72
|
+
2. Create an access key (or use an existing IAM user with read permissions)
|
|
73
|
+
3. Copy both the Access Key ID and Secret Access Key
|
|
74
|
+
Run: aws configure get aws_access_key_id
|
|
75
|
+
|
|
76
|
+
Region:
|
|
77
|
+
Run: aws configure get region
|
|
78
|
+
Common values: us-east-1, us-west-2, eu-west-1`,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const AVAILABLE_SHIELDS = [
|
|
82
|
+
"cost-runaway", "ddos", "brute-force", "error-storm",
|
|
83
|
+
"exfiltration", "gpu-runaway", "lambda-loop", "aws-cost-runaway",
|
|
84
|
+
];
|
|
85
|
+
function ask(question) {
|
|
86
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
rl.question(question, (answer) => {
|
|
89
|
+
rl.close();
|
|
90
|
+
resolve(answer.trim());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function registerOnboardCommands(program) {
|
|
95
|
+
program
|
|
96
|
+
.command("onboard")
|
|
97
|
+
.alias("setup")
|
|
98
|
+
.description("Quick setup: connect a cloud provider, apply protection, configure alerts")
|
|
99
|
+
.option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws")
|
|
100
|
+
.option("--name <name>", "Account name (e.g., Production)")
|
|
101
|
+
.option("--token <token>", "API token (Cloudflare)")
|
|
102
|
+
.option("--account-id <id>", "Account ID (Cloudflare)")
|
|
103
|
+
.option("--project-id <id>", "Project ID (GCP)")
|
|
104
|
+
.option("--service-account <json>", "Service Account JSON (GCP)")
|
|
105
|
+
.option("--access-key <key>", "Access Key ID (AWS)")
|
|
106
|
+
.option("--secret-key <key>", "Secret Access Key (AWS)")
|
|
107
|
+
.option("--region <region>", "Region (AWS, default: us-east-1)")
|
|
108
|
+
.option("--shields <presets>", "Comma-separated shield presets to apply (default: cost-runaway)")
|
|
109
|
+
.option("--alert-email <email>", "Email address for alerts")
|
|
110
|
+
.option("--alert-discord <url>", "Discord webhook URL for alerts")
|
|
111
|
+
.option("--alert-slack <url>", "Slack webhook URL for alerts")
|
|
112
|
+
.option("--skip-shields", "Skip applying protection rules")
|
|
113
|
+
.option("--skip-alerts", "Skip setting up alerts")
|
|
114
|
+
.option("--help-provider <provider>", "Show how to get credentials for a provider")
|
|
115
|
+
.addHelpText("after", `
|
|
116
|
+
Examples:
|
|
117
|
+
|
|
118
|
+
# Interactive onboarding
|
|
119
|
+
kill-switch onboard
|
|
120
|
+
|
|
121
|
+
# AI agent / non-interactive: connect Cloudflare
|
|
122
|
+
kill-switch onboard \\
|
|
123
|
+
--provider cloudflare \\
|
|
124
|
+
--account-id 14a6fa23390363382f378b5bd4a0f849 \\
|
|
125
|
+
--token cf-api-token-here \\
|
|
126
|
+
--name "Production" \\
|
|
127
|
+
--shields cost-runaway,ddos \\
|
|
128
|
+
--alert-email you@example.com
|
|
129
|
+
|
|
130
|
+
# Show how to get Cloudflare credentials
|
|
131
|
+
kill-switch onboard --help-provider cloudflare
|
|
132
|
+
|
|
133
|
+
# Connect AWS with shields
|
|
134
|
+
kill-switch onboard \\
|
|
135
|
+
--provider aws \\
|
|
136
|
+
--access-key AKIA... \\
|
|
137
|
+
--secret-key wJalr... \\
|
|
138
|
+
--region us-east-1 \\
|
|
139
|
+
--shields aws-cost-runaway,gpu-runaway
|
|
140
|
+
|
|
141
|
+
Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
142
|
+
`)
|
|
143
|
+
.action(async (opts) => {
|
|
144
|
+
const json = program.opts().json;
|
|
145
|
+
// Help for a specific provider
|
|
146
|
+
if (opts.helpProvider) {
|
|
147
|
+
const help = PROVIDER_HELP[opts.helpProvider];
|
|
148
|
+
if (!help) {
|
|
149
|
+
outputError(`Unknown provider: ${opts.helpProvider}. Use: cloudflare, gcp, aws`, json);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
if (json) {
|
|
153
|
+
outputJson({ provider: opts.helpProvider, ...help });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.log(`\n${help.name} — required flags: ${help.fields}\n`);
|
|
157
|
+
console.log(help.howToGet);
|
|
158
|
+
console.log();
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
let provider = opts.provider;
|
|
164
|
+
let name = opts.name;
|
|
165
|
+
// Interactive mode if no provider specified
|
|
166
|
+
if (!provider) {
|
|
167
|
+
if (json) {
|
|
168
|
+
outputError("--provider is required in JSON mode. Use: cloudflare, gcp, aws", json);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
console.log("\n\u26a1 Kill Switch Onboarding\n");
|
|
172
|
+
console.log("Let's connect your cloud provider and set up cost protection.\n");
|
|
173
|
+
console.log("Available providers:");
|
|
174
|
+
console.log(" 1. cloudflare — Workers, R2, D1, Queues, Stream");
|
|
175
|
+
console.log(" 2. gcp — Cloud Run, Compute, GKE, BigQuery");
|
|
176
|
+
console.log(" 3. aws — EC2, Lambda, RDS, ECS, S3");
|
|
177
|
+
console.log();
|
|
178
|
+
const choice = await ask("Choose a provider (1/2/3 or name): ");
|
|
179
|
+
provider = { "1": "cloudflare", "2": "gcp", "3": "aws" }[choice] || choice;
|
|
180
|
+
}
|
|
181
|
+
if (!PROVIDER_HELP[provider]) {
|
|
182
|
+
outputError(`Unknown provider: ${provider}. Use: cloudflare, gcp, aws`, json);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
if (!name && !json) {
|
|
186
|
+
name = await ask("Account name (e.g., Production): ");
|
|
187
|
+
}
|
|
188
|
+
name = name || `${PROVIDER_HELP[provider].name} account`;
|
|
189
|
+
// Build credential
|
|
190
|
+
const credential = { provider };
|
|
191
|
+
if (provider === "cloudflare") {
|
|
192
|
+
let accountId = opts.accountId;
|
|
193
|
+
let token = opts.token;
|
|
194
|
+
if (!accountId && !json) {
|
|
195
|
+
console.log("\n Tip: Your Account ID is in the URL: dash.cloudflare.com/<ACCOUNT_ID>/...");
|
|
196
|
+
accountId = await ask(" Cloudflare Account ID: ");
|
|
197
|
+
}
|
|
198
|
+
if (!token && !json) {
|
|
199
|
+
console.log("\n Tip: Create an API Token (not Global Key) at:");
|
|
200
|
+
console.log(" https://dash.cloudflare.com/profile/api-tokens");
|
|
201
|
+
console.log(" Use the 'Edit Cloudflare Workers' template.\n");
|
|
202
|
+
token = await ask(" API Token: ");
|
|
203
|
+
}
|
|
204
|
+
if (!accountId || !token) {
|
|
205
|
+
outputError(`Cloudflare requires ${PROVIDER_HELP.cloudflare.fields}`, json);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
credential.accountId = accountId;
|
|
209
|
+
credential.apiToken = token;
|
|
210
|
+
}
|
|
211
|
+
else if (provider === "gcp") {
|
|
212
|
+
let projectId = opts.projectId;
|
|
213
|
+
let serviceAccount = opts.serviceAccount;
|
|
214
|
+
if (!projectId && !json) {
|
|
215
|
+
console.log("\n Tip: Run `gcloud config get-value project` to find your project ID.");
|
|
216
|
+
projectId = await ask(" GCP Project ID: ");
|
|
217
|
+
}
|
|
218
|
+
if (!serviceAccount && !json) {
|
|
219
|
+
console.log("\n Tip: Create at IAM > Service Accounts > Manage Keys > Add Key > JSON");
|
|
220
|
+
serviceAccount = await ask(" Service Account Key JSON: ");
|
|
221
|
+
}
|
|
222
|
+
if (!projectId || !serviceAccount) {
|
|
223
|
+
outputError(`GCP requires ${PROVIDER_HELP.gcp.fields}`, json);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
credential.projectId = projectId;
|
|
227
|
+
credential.serviceAccountJson = serviceAccount;
|
|
228
|
+
}
|
|
229
|
+
else if (provider === "aws") {
|
|
230
|
+
let accessKey = opts.accessKey;
|
|
231
|
+
let secretKey = opts.secretKey;
|
|
232
|
+
let region = opts.region;
|
|
233
|
+
if (!accessKey && !json) {
|
|
234
|
+
console.log("\n Tip: Find at IAM > Security Credentials, or `aws configure get aws_access_key_id`");
|
|
235
|
+
accessKey = await ask(" AWS Access Key ID: ");
|
|
236
|
+
}
|
|
237
|
+
if (!secretKey && !json) {
|
|
238
|
+
secretKey = await ask(" AWS Secret Access Key: ");
|
|
239
|
+
}
|
|
240
|
+
if (!region && !json) {
|
|
241
|
+
region = await ask(" AWS Region (default: us-east-1): ");
|
|
242
|
+
}
|
|
243
|
+
if (!accessKey || !secretKey) {
|
|
244
|
+
outputError(`AWS requires ${PROVIDER_HELP.aws.fields}`, json);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
credential.awsAccessKeyId = accessKey;
|
|
248
|
+
credential.awsSecretAccessKey = secretKey;
|
|
249
|
+
credential.awsRegion = region || "us-east-1";
|
|
250
|
+
}
|
|
251
|
+
// 1. Connect cloud account
|
|
252
|
+
if (!json)
|
|
253
|
+
console.log(`\nConnecting ${PROVIDER_HELP[provider].name}...`);
|
|
254
|
+
const account = await apiRequest("/cloud-accounts", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
body: { provider, name, credential },
|
|
257
|
+
});
|
|
258
|
+
if (!json)
|
|
259
|
+
console.log(`\u2713 Connected: ${account.name || account._id}`);
|
|
260
|
+
// 2. Apply shields
|
|
261
|
+
if (!opts.skipShields) {
|
|
262
|
+
const shieldList = opts.shields
|
|
263
|
+
? opts.shields.split(",").map((s) => s.trim())
|
|
264
|
+
: ["cost-runaway"];
|
|
265
|
+
if (!json)
|
|
266
|
+
console.log(`\nApplying ${shieldList.length} shield(s)...`);
|
|
267
|
+
for (const shield of shieldList) {
|
|
268
|
+
try {
|
|
269
|
+
await apiRequest(`/rules/presets/${shield}`, { method: "POST", body: {} });
|
|
270
|
+
if (!json)
|
|
271
|
+
console.log(` \u2713 ${shield}`);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (!json)
|
|
275
|
+
console.log(` \u2717 ${shield}: ${err.message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// 3. Set up alerts
|
|
280
|
+
if (!opts.skipAlerts) {
|
|
281
|
+
const channels = [];
|
|
282
|
+
if (opts.alertEmail) {
|
|
283
|
+
channels.push({ type: "email", name: "Email", config: { email: opts.alertEmail }, enabled: true });
|
|
284
|
+
}
|
|
285
|
+
if (opts.alertDiscord) {
|
|
286
|
+
channels.push({ type: "discord", name: "Discord", config: { webhookUrl: opts.alertDiscord }, enabled: true });
|
|
287
|
+
}
|
|
288
|
+
if (opts.alertSlack) {
|
|
289
|
+
channels.push({ type: "slack", name: "Slack", config: { webhookUrl: opts.alertSlack }, enabled: true });
|
|
290
|
+
}
|
|
291
|
+
if (channels.length === 0 && !json) {
|
|
292
|
+
const email = await ask("\nAlert email (or Enter to skip): ");
|
|
293
|
+
if (email) {
|
|
294
|
+
channels.push({ type: "email", name: "Email", config: { email }, enabled: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (channels.length > 0) {
|
|
298
|
+
if (!json)
|
|
299
|
+
console.log("Setting up alerts...");
|
|
300
|
+
try {
|
|
301
|
+
await apiRequest("/alerts/channels", { method: "PUT", body: { channels } });
|
|
302
|
+
if (!json)
|
|
303
|
+
console.log(` \u2713 ${channels.length} alert channel(s) configured`);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
if (!json)
|
|
307
|
+
console.log(` \u2717 Alerts: ${err.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// 4. Complete onboarding
|
|
312
|
+
try {
|
|
313
|
+
await apiRequest("/accounts/me", { method: "PATCH", body: { onboardingCompleted: true } });
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Non-critical
|
|
317
|
+
}
|
|
318
|
+
if (json) {
|
|
319
|
+
outputJson({
|
|
320
|
+
success: true,
|
|
321
|
+
provider,
|
|
322
|
+
accountId: account._id || account.id,
|
|
323
|
+
accountName: account.name,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
console.log(`\n\u2705 Setup complete! Kill Switch is monitoring your ${PROVIDER_HELP[provider].name} account.`);
|
|
328
|
+
console.log("\nNext steps:");
|
|
329
|
+
console.log(" kill-switch accounts list — view connected accounts");
|
|
330
|
+
console.log(" kill-switch check — run a monitoring check");
|
|
331
|
+
console.log(" kill-switch shield --list — see all available shields");
|
|
332
|
+
console.log(" kill-switch onboard --provider — add another provider\n");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
outputError(err.message, json);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { apiRequest } from "../api-client.js";
|
|
2
|
+
import { outputJson, formatTable, outputError } from "../output.js";
|
|
3
|
+
export function registerRuleCommands(program) {
|
|
4
|
+
const rules = program.command("rules").description("Manage kill switch rules");
|
|
5
|
+
rules
|
|
6
|
+
.command("list")
|
|
7
|
+
.alias("ls")
|
|
8
|
+
.description("List active rules")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const json = program.opts().json;
|
|
11
|
+
try {
|
|
12
|
+
const data = await apiRequest("/rules");
|
|
13
|
+
const list = data.rules || data;
|
|
14
|
+
if (json) {
|
|
15
|
+
outputJson(list);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
formatTable(Array.isArray(list) ? list : [], [
|
|
19
|
+
{ key: "id", header: "ID" },
|
|
20
|
+
{ key: "name", header: "Name" },
|
|
21
|
+
{ key: "trigger", header: "Trigger" },
|
|
22
|
+
{ key: "enabled", header: "Enabled" },
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
outputError(err.message, json);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
rules
|
|
32
|
+
.command("presets")
|
|
33
|
+
.description("List available preset templates")
|
|
34
|
+
.action(async () => {
|
|
35
|
+
const json = program.opts().json;
|
|
36
|
+
try {
|
|
37
|
+
const data = await apiRequest("/rules/presets", { public: true });
|
|
38
|
+
const presets = data.presets || data;
|
|
39
|
+
if (json) {
|
|
40
|
+
outputJson(presets);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
formatTable(presets, [
|
|
44
|
+
{ key: "id", header: "ID", width: 20 },
|
|
45
|
+
{ key: "name", header: "Name", width: 30 },
|
|
46
|
+
{ key: "description", header: "Description", width: 50 },
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
outputError(err.message, json);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
rules
|
|
56
|
+
.command("create <name>")
|
|
57
|
+
.description("Create a custom rule")
|
|
58
|
+
.requiredOption("--trigger <type>", "Trigger type (cost, security, api)")
|
|
59
|
+
.option("--condition <json>", "Condition JSON")
|
|
60
|
+
.option("--action <json>", "Action JSON")
|
|
61
|
+
.action(async (name, opts) => {
|
|
62
|
+
const json = program.opts().json;
|
|
63
|
+
try {
|
|
64
|
+
const body = { name, trigger: opts.trigger };
|
|
65
|
+
if (opts.condition)
|
|
66
|
+
body.conditions = JSON.parse(opts.condition);
|
|
67
|
+
if (opts.action)
|
|
68
|
+
body.actions = JSON.parse(opts.action);
|
|
69
|
+
const data = await apiRequest("/rules", { method: "POST", body });
|
|
70
|
+
if (json) {
|
|
71
|
+
outputJson(data);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log(`Rule created: ${data.id || data._id}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
outputError(err.message, json);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
rules
|
|
83
|
+
.command("delete <id>")
|
|
84
|
+
.alias("rm")
|
|
85
|
+
.description("Delete a rule")
|
|
86
|
+
.action(async (id) => {
|
|
87
|
+
const json = program.opts().json;
|
|
88
|
+
try {
|
|
89
|
+
await apiRequest(`/rules/${id}`, { method: "DELETE" });
|
|
90
|
+
if (json) {
|
|
91
|
+
outputJson({ deleted: true, id });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(`Rule ${id} deleted.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
outputError(err.message, json);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
rules
|
|
103
|
+
.command("toggle <id>")
|
|
104
|
+
.description("Enable/disable a rule")
|
|
105
|
+
.action(async (id) => {
|
|
106
|
+
const json = program.opts().json;
|
|
107
|
+
try {
|
|
108
|
+
const data = await apiRequest(`/rules/${id}/toggle`, { method: "POST" });
|
|
109
|
+
if (json) {
|
|
110
|
+
outputJson(data);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(`Rule ${id} is now ${data.enabled ? "enabled" : "disabled"}.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
outputError(err.message, json);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { apiRequest } from "../api-client.js";
|
|
2
|
+
import { outputJson, formatTable, outputError } from "../output.js";
|
|
3
|
+
const PRESETS = [
|
|
4
|
+
"ddos", "brute-force", "cost-runaway", "error-storm",
|
|
5
|
+
"exfiltration", "gpu-runaway", "lambda-loop", "aws-cost-runaway",
|
|
6
|
+
];
|
|
7
|
+
export function registerShieldCommands(program) {
|
|
8
|
+
const shield = program
|
|
9
|
+
.command("shield [preset]")
|
|
10
|
+
.description("Quick-apply a protection preset (e.g., kill-switch shield cost-runaway)")
|
|
11
|
+
.option("--list", "List available shields")
|
|
12
|
+
.action(async (preset, opts) => {
|
|
13
|
+
const json = program.opts().json;
|
|
14
|
+
if (opts.list || !preset) {
|
|
15
|
+
try {
|
|
16
|
+
const data = await apiRequest("/rules/presets", { public: true });
|
|
17
|
+
const presets = data.presets || data;
|
|
18
|
+
if (json) {
|
|
19
|
+
outputJson(presets);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log("Available shields:\n");
|
|
23
|
+
formatTable(presets, [
|
|
24
|
+
{ key: "id", header: "Shield", width: 20 },
|
|
25
|
+
{ key: "name", header: "Name", width: 30 },
|
|
26
|
+
{ key: "description", header: "Description", width: 50 },
|
|
27
|
+
]);
|
|
28
|
+
console.log("\nUsage: kill-switch shield <preset-id>");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
outputError(err.message, json);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!PRESETS.includes(preset)) {
|
|
38
|
+
outputError(`Unknown preset "${preset}". Run: kill-switch shield --list`, json);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const data = await apiRequest(`/rules/presets/${preset}`, { method: "POST" });
|
|
43
|
+
if (json) {
|
|
44
|
+
outputJson(data);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(`Shield activated: ${data.name || preset}`);
|
|
48
|
+
if (data.id)
|
|
49
|
+
console.log(`Rule ID: ${data.id}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
outputError(err.message, json);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config file management — ~/.kill-switch/config.json
|
|
3
|
+
*/
|
|
4
|
+
export interface KillSwitchConfig {
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
apiUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const CONFIG_DIR: string;
|
|
9
|
+
declare const CONFIG_FILE: string;
|
|
10
|
+
declare const DEFAULT_API_URL = "https://api.kill-switch.net";
|
|
11
|
+
export declare function loadConfig(): KillSwitchConfig;
|
|
12
|
+
export declare function saveConfig(config: KillSwitchConfig): void;
|
|
13
|
+
export declare function deleteConfig(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the API key from (in priority order):
|
|
16
|
+
* 1. KILL_SWITCH_API_KEY env var
|
|
17
|
+
* 2. --api-key flag (passed at call site)
|
|
18
|
+
* 3. Config file
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveApiKey(flagKey?: string): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the API URL.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveApiUrl(flagUrl?: string): string;
|
|
25
|
+
export { CONFIG_DIR, CONFIG_FILE, DEFAULT_API_URL };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config file management — ~/.kill-switch/config.json
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
const CONFIG_DIR = join(homedir(), ".kill-switch");
|
|
8
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
9
|
+
const DEFAULT_API_URL = "https://api.kill-switch.net";
|
|
10
|
+
export function loadConfig() {
|
|
11
|
+
try {
|
|
12
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function saveConfig(config) {
|
|
20
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
24
|
+
chmodSync(CONFIG_FILE, 0o600); // owner read/write only
|
|
25
|
+
}
|
|
26
|
+
export function deleteConfig() {
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(CONFIG_FILE, "{}\n");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the API key from (in priority order):
|
|
36
|
+
* 1. KILL_SWITCH_API_KEY env var
|
|
37
|
+
* 2. --api-key flag (passed at call site)
|
|
38
|
+
* 3. Config file
|
|
39
|
+
*/
|
|
40
|
+
export function resolveApiKey(flagKey) {
|
|
41
|
+
return process.env.KILL_SWITCH_API_KEY || flagKey || loadConfig().apiKey;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the API URL.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveApiUrl(flagUrl) {
|
|
47
|
+
return process.env.KILL_SWITCH_API_URL || flagUrl || loadConfig().apiUrl || DEFAULT_API_URL;
|
|
48
|
+
}
|
|
49
|
+
export { CONFIG_DIR, CONFIG_FILE, DEFAULT_API_URL };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kill Switch CLI
|
|
4
|
+
*
|
|
5
|
+
* Monitor cloud spending, kill runaway services from the terminal.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* kill-switch auth login --api-key ks_live_abc123
|
|
9
|
+
* kill-switch shield cost-runaway
|
|
10
|
+
* kill-switch check
|
|
11
|
+
* kill-switch accounts list --json
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kill Switch CLI
|
|
4
|
+
*
|
|
5
|
+
* Monitor cloud spending, kill runaway services from the terminal.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* kill-switch auth login --api-key ks_live_abc123
|
|
9
|
+
* kill-switch shield cost-runaway
|
|
10
|
+
* kill-switch check
|
|
11
|
+
* kill-switch accounts list --json
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import { registerAuthCommands } from "./commands/auth.js";
|
|
15
|
+
import { registerAccountCommands } from "./commands/accounts.js";
|
|
16
|
+
import { registerRuleCommands } from "./commands/rules.js";
|
|
17
|
+
import { registerShieldCommands } from "./commands/shield.js";
|
|
18
|
+
import { registerCheckCommands } from "./commands/check.js";
|
|
19
|
+
import { registerAlertCommands } from "./commands/alerts.js";
|
|
20
|
+
import { registerKillCommands } from "./commands/kill.js";
|
|
21
|
+
import { registerAnalyticsCommands } from "./commands/analytics.js";
|
|
22
|
+
import { registerConfigCommands } from "./commands/config-cmd.js";
|
|
23
|
+
import { registerOnboardCommands } from "./commands/onboard.js";
|
|
24
|
+
const program = new Command();
|
|
25
|
+
program
|
|
26
|
+
.name("kill-switch")
|
|
27
|
+
.description("Monitor cloud spending, kill runaway services, protect your infrastructure")
|
|
28
|
+
.version("0.1.0")
|
|
29
|
+
.option("--json", "Output as JSON (for automation/scripting)")
|
|
30
|
+
.option("--api-key <key>", "API key (overrides config and env)")
|
|
31
|
+
.option("--api-url <url>", "API URL (overrides config and env)");
|
|
32
|
+
registerAuthCommands(program);
|
|
33
|
+
registerAccountCommands(program);
|
|
34
|
+
registerRuleCommands(program);
|
|
35
|
+
registerShieldCommands(program);
|
|
36
|
+
registerCheckCommands(program);
|
|
37
|
+
registerAlertCommands(program);
|
|
38
|
+
registerKillCommands(program);
|
|
39
|
+
registerAnalyticsCommands(program);
|
|
40
|
+
registerConfigCommands(program);
|
|
41
|
+
registerOnboardCommands(program);
|
|
42
|
+
program.parse();
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting — tables for humans, JSON for machines
|
|
3
|
+
*/
|
|
4
|
+
export interface Column {
|
|
5
|
+
key: string;
|
|
6
|
+
header: string;
|
|
7
|
+
width?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function formatTable(rows: any[], columns: Column[]): void;
|
|
10
|
+
export declare function formatObject(obj: any, fields?: string[]): void;
|
|
11
|
+
export declare function outputJson(data: any): void;
|
|
12
|
+
export declare function outputError(message: string, json: boolean): void;
|
|
13
|
+
/**
|
|
14
|
+
* Wrap a command handler with JSON/error handling.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withOutput(fn: (opts: any) => Promise<any>, opts: {
|
|
17
|
+
json: boolean;
|
|
18
|
+
}): Promise<any>;
|