@kill-switch/cli 0.1.1 → 0.3.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/README.md +49 -4
- package/dist/commands/accounts.d.ts +2 -1
- package/dist/commands/accounts.js +44 -27
- package/dist/commands/activity.d.ts +3 -0
- package/dist/commands/activity.js +80 -0
- package/dist/commands/agent-guard.d.ts +10 -0
- package/dist/commands/agent-guard.js +175 -0
- package/dist/commands/alerts.d.ts +2 -1
- package/dist/commands/alerts.js +112 -12
- package/dist/commands/analytics.d.ts +2 -1
- package/dist/commands/analytics.js +36 -14
- package/dist/commands/auth.d.ts +2 -1
- package/dist/commands/auth.js +127 -14
- package/dist/commands/check.d.ts +2 -1
- package/dist/commands/check.js +28 -15
- package/dist/commands/kill.d.ts +2 -1
- package/dist/commands/kill.js +52 -37
- package/dist/commands/onboard.d.ts +2 -17
- package/dist/commands/onboard.js +273 -61
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.js +192 -0
- package/dist/commands/providers.d.ts +3 -0
- package/dist/commands/providers.js +82 -0
- package/dist/commands/rules.d.ts +2 -1
- package/dist/commands/rules.js +51 -28
- package/dist/commands/shield.d.ts +2 -1
- package/dist/commands/shield.js +36 -17
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +100 -0
- package/dist/commands/watch.d.ts +3 -0
- package/dist/commands/watch.js +68 -0
- package/dist/device-flow.d.ts +33 -0
- package/dist/device-flow.js +91 -0
- package/dist/index.js +38 -11
- package/dist/output.d.ts +26 -4
- package/dist/output.js +101 -12
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +24 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +29 -7
package/dist/commands/kill.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export function registerKillCommands(program) {
|
|
1
|
+
import { outputJson, formatTable, formatObject, handleError, spinner, success, warn, colors as c } from "../output.js";
|
|
2
|
+
import { confirm } from "../prompts.js";
|
|
3
|
+
export function registerKillCommands(program, createClient) {
|
|
4
4
|
const kill = program.command("kill").description("Database kill switch sequences");
|
|
5
5
|
kill
|
|
6
6
|
.command("init")
|
|
@@ -8,25 +8,32 @@ export function registerKillCommands(program) {
|
|
|
8
8
|
.requiredOption("--credential-id <id>", "Stored credential ID")
|
|
9
9
|
.requiredOption("--trigger <reason>", "Kill trigger reason")
|
|
10
10
|
.action(async (opts) => {
|
|
11
|
-
const json = program.opts()
|
|
11
|
+
const { json, yes } = program.opts();
|
|
12
12
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const ok = await confirm(`${c.red("WARNING:")} This will start a database kill sequence. Continue?`, { yes, json });
|
|
14
|
+
if (!ok) {
|
|
15
|
+
console.log("Aborted.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const s = json ? null : spinner("Initiating kill sequence...").start();
|
|
19
|
+
const client = createClient();
|
|
20
|
+
const data = await client.database.initiate({
|
|
21
|
+
credentialId: opts.credentialId,
|
|
22
|
+
trigger: opts.trigger,
|
|
16
23
|
});
|
|
24
|
+
s?.stop();
|
|
17
25
|
if (json) {
|
|
18
26
|
outputJson(data);
|
|
19
27
|
}
|
|
20
28
|
else {
|
|
21
|
-
|
|
22
|
-
console.log(`Status: ${data.status}`);
|
|
23
|
-
console.log(`Steps: ${data.steps?.map((s) => s.action).join("
|
|
24
|
-
console.log(`\
|
|
29
|
+
warn(`Kill sequence initiated: ${c.bold(data.id)}`);
|
|
30
|
+
console.log(` Status: ${data.status}`);
|
|
31
|
+
console.log(` Steps: ${data.steps?.map((s) => s.action).join(" → ")}`);
|
|
32
|
+
console.log(`\n Advance: ${c.dim(`ks kill advance ${data.id} --credential-id ${opts.credentialId}`)}`);
|
|
25
33
|
}
|
|
26
34
|
}
|
|
27
35
|
catch (err) {
|
|
28
|
-
|
|
29
|
-
process.exit(1);
|
|
36
|
+
handleError(err, json);
|
|
30
37
|
}
|
|
31
38
|
});
|
|
32
39
|
kill
|
|
@@ -35,8 +42,9 @@ export function registerKillCommands(program) {
|
|
|
35
42
|
.action(async (id) => {
|
|
36
43
|
const json = program.opts().json;
|
|
37
44
|
try {
|
|
45
|
+
const client = createClient();
|
|
38
46
|
if (id) {
|
|
39
|
-
const data = await
|
|
47
|
+
const data = await client.database.get(id);
|
|
40
48
|
if (json) {
|
|
41
49
|
outputJson(data);
|
|
42
50
|
}
|
|
@@ -53,13 +61,12 @@ export function registerKillCommands(program) {
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
else {
|
|
56
|
-
const
|
|
57
|
-
const sequences = data.sequences || data;
|
|
64
|
+
const sequences = await client.database.list();
|
|
58
65
|
if (json) {
|
|
59
66
|
outputJson(sequences);
|
|
60
67
|
}
|
|
61
68
|
else {
|
|
62
|
-
formatTable(
|
|
69
|
+
formatTable(sequences, [
|
|
63
70
|
{ key: "id", header: "Sequence ID" },
|
|
64
71
|
{ key: "status", header: "Status" },
|
|
65
72
|
{ key: "currentStep", header: "Step" },
|
|
@@ -68,8 +75,7 @@ export function registerKillCommands(program) {
|
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
catch (err) {
|
|
71
|
-
|
|
72
|
-
process.exit(1);
|
|
78
|
+
handleError(err, json);
|
|
73
79
|
}
|
|
74
80
|
});
|
|
75
81
|
kill
|
|
@@ -78,48 +84,57 @@ export function registerKillCommands(program) {
|
|
|
78
84
|
.requiredOption("--credential-id <credId>", "Stored credential ID")
|
|
79
85
|
.option("--human-approval", "Confirm human approval (required for nuke step)")
|
|
80
86
|
.action(async (id, opts) => {
|
|
81
|
-
const json = program.opts()
|
|
87
|
+
const { json, yes } = program.opts();
|
|
82
88
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const ok = await confirm(`Execute the next step in kill sequence ${id}?`, { yes, json });
|
|
90
|
+
if (!ok) {
|
|
91
|
+
console.log("Aborted.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const s = json ? null : spinner("Executing step...").start();
|
|
95
|
+
const client = createClient();
|
|
96
|
+
const data = await client.database.advance(id, {
|
|
97
|
+
credentialId: opts.credentialId,
|
|
98
|
+
humanApproval: opts.humanApproval || false,
|
|
89
99
|
});
|
|
100
|
+
s?.stop();
|
|
90
101
|
if (json) {
|
|
91
102
|
outputJson(data);
|
|
92
103
|
}
|
|
93
104
|
else {
|
|
94
|
-
|
|
95
|
-
console.log(`Status: ${data.status}`);
|
|
105
|
+
success(`Step executed: ${data.steps?.[data.currentStep - 1]?.action || "?"}`);
|
|
106
|
+
console.log(` Status: ${data.status}`);
|
|
96
107
|
if (data.status !== "completed") {
|
|
97
|
-
console.log(`Next:
|
|
108
|
+
console.log(` Next: ${c.dim(`ks kill advance ${id} --credential-id ${opts.credentialId}`)}`);
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
catch (err) {
|
|
102
|
-
|
|
103
|
-
process.exit(1);
|
|
113
|
+
handleError(err, json);
|
|
104
114
|
}
|
|
105
115
|
});
|
|
106
116
|
kill
|
|
107
117
|
.command("abort <id>")
|
|
108
118
|
.description("Abort a kill sequence")
|
|
109
119
|
.action(async (id) => {
|
|
110
|
-
const json = program.opts()
|
|
120
|
+
const { json, yes } = program.opts();
|
|
111
121
|
try {
|
|
112
|
-
const
|
|
122
|
+
const ok = await confirm(`Abort kill sequence ${id}?`, { yes, json });
|
|
123
|
+
if (!ok) {
|
|
124
|
+
console.log("Aborted.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const client = createClient();
|
|
128
|
+
await client.database.abort(id);
|
|
113
129
|
if (json) {
|
|
114
|
-
outputJson(
|
|
130
|
+
outputJson({ aborted: true, id });
|
|
115
131
|
}
|
|
116
132
|
else {
|
|
117
|
-
|
|
133
|
+
success(`Kill sequence ${id} aborted.`);
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
136
|
catch (err) {
|
|
121
|
-
|
|
122
|
-
process.exit(1);
|
|
137
|
+
handleError(err, json);
|
|
123
138
|
}
|
|
124
139
|
});
|
|
125
140
|
}
|
|
@@ -3,22 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One-command setup for connecting cloud providers, applying protection rules,
|
|
5
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
6
|
*/
|
|
23
7
|
import { Command } from "commander";
|
|
24
|
-
|
|
8
|
+
import type { ClientFactory } from "../types.js";
|
|
9
|
+
export declare function registerOnboardCommands(program: Command, createClient: ClientFactory): void;
|
package/dist/commands/onboard.js
CHANGED
|
@@ -3,26 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One-command setup for connecting cloud providers, applying protection rules,
|
|
5
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
6
|
*/
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { createInterface } from "readline";
|
|
7
|
+
import { outputJson, outputError, handleError, spinner, success, fail } from "../output.js";
|
|
8
|
+
import { ask } from "../prompts.js";
|
|
26
9
|
const PROVIDER_HELP = {
|
|
27
10
|
cloudflare: {
|
|
28
11
|
name: "Cloudflare",
|
|
@@ -77,26 +60,134 @@ const PROVIDER_HELP = {
|
|
|
77
60
|
Run: aws configure get region
|
|
78
61
|
Common values: us-east-1, us-west-2, eu-west-1`,
|
|
79
62
|
},
|
|
63
|
+
runpod: {
|
|
64
|
+
name: "RunPod",
|
|
65
|
+
fields: "--runpod-api-key",
|
|
66
|
+
howToGet: `How to get this value:
|
|
67
|
+
|
|
68
|
+
API Key:
|
|
69
|
+
1. Go to https://www.runpod.io/console/user/settings
|
|
70
|
+
2. Scroll to "API Keys" section
|
|
71
|
+
3. Click "Create API Key" (or copy an existing one)
|
|
72
|
+
4. The key starts with a long alphanumeric string
|
|
73
|
+
|
|
74
|
+
Required permissions:
|
|
75
|
+
- Read access to pods, serverless endpoints, and network volumes
|
|
76
|
+
- Write access if you want auto-kill actions (stop/terminate pods, scale endpoints)`,
|
|
77
|
+
},
|
|
78
|
+
redis: {
|
|
79
|
+
name: "Redis",
|
|
80
|
+
fields: "--redis-url (self-hosted) or --redis-cloud-key + --redis-cloud-secret + --subscription-id",
|
|
81
|
+
howToGet: `Redis supports three deployment types:
|
|
82
|
+
|
|
83
|
+
Self-hosted Redis:
|
|
84
|
+
Provide a connection URL: --redis-url redis://user:pass@host:6379
|
|
85
|
+
|
|
86
|
+
Redis Cloud:
|
|
87
|
+
1. Go to https://app.redislabs.com/#/account/api-keys
|
|
88
|
+
2. Create an API key pair (Account Key + Secret Key)
|
|
89
|
+
3. Find your subscription ID in the console
|
|
90
|
+
Use: --redis-cloud-key KEY --redis-cloud-secret SECRET --subscription-id ID
|
|
91
|
+
|
|
92
|
+
AWS ElastiCache:
|
|
93
|
+
Use AWS credentials + cluster ID:
|
|
94
|
+
--access-key AKIA... --secret-key ... --region us-east-1 --cluster-id my-cluster`,
|
|
95
|
+
},
|
|
96
|
+
mongodb: {
|
|
97
|
+
name: "MongoDB",
|
|
98
|
+
fields: "--mongodb-uri (self-hosted) or --atlas-public-key + --atlas-private-key + --atlas-project-id",
|
|
99
|
+
howToGet: `MongoDB supports two deployment types:
|
|
100
|
+
|
|
101
|
+
MongoDB Atlas:
|
|
102
|
+
1. Go to Organization > Access Manager > API Keys
|
|
103
|
+
2. Create a key with "Project Read Only" + "Project Cluster Manager" roles
|
|
104
|
+
Use: --atlas-public-key PUB --atlas-private-key PRIV --atlas-project-id PROJ --atlas-cluster-name Cluster0
|
|
105
|
+
|
|
106
|
+
Self-hosted MongoDB:
|
|
107
|
+
Provide a URI: --mongodb-uri mongodb+srv://user:pass@host/db`,
|
|
108
|
+
},
|
|
109
|
+
openai: {
|
|
110
|
+
name: "OpenAI",
|
|
111
|
+
fields: "--openai-api-key",
|
|
112
|
+
howToGet: ` 1. Go to https://platform.openai.com/api-keys
|
|
113
|
+
2. Create a new API key
|
|
114
|
+
3. Copy the key (starts with sk-)
|
|
115
|
+
Optional: --openai-org-id (from Organization Settings)`,
|
|
116
|
+
},
|
|
117
|
+
anthropic: {
|
|
118
|
+
name: "Anthropic",
|
|
119
|
+
fields: "--anthropic-api-key",
|
|
120
|
+
howToGet: ` 1. Go to https://console.anthropic.com/settings/keys
|
|
121
|
+
2. Create a new API key
|
|
122
|
+
3. Copy the key (starts with sk-ant-)
|
|
123
|
+
Optional: --anthropic-workspace-id`,
|
|
124
|
+
},
|
|
125
|
+
xai: {
|
|
126
|
+
name: "xAI (Grok)",
|
|
127
|
+
fields: "--xai-api-key",
|
|
128
|
+
howToGet: ` 1. Go to https://console.x.ai/api-keys
|
|
129
|
+
2. Create a new API key
|
|
130
|
+
3. Copy the key`,
|
|
131
|
+
},
|
|
132
|
+
replicate: {
|
|
133
|
+
name: "Replicate",
|
|
134
|
+
fields: "--replicate-api-token",
|
|
135
|
+
howToGet: ` 1. Go to https://replicate.com/account/api-tokens
|
|
136
|
+
2. Create a new token
|
|
137
|
+
3. Copy the token (starts with r8_)`,
|
|
138
|
+
},
|
|
139
|
+
snowflake: {
|
|
140
|
+
name: "Snowflake",
|
|
141
|
+
fields: "--snowflake-account + --snowflake-username + --snowflake-password",
|
|
142
|
+
howToGet: ` Account: Found in your Snowflake URL (https://<account>.snowflakecomputing.com)
|
|
143
|
+
Username/Password: Your Snowflake login credentials
|
|
144
|
+
Optional: --warehouse COMPUTE_WH --role ACCOUNTADMIN`,
|
|
145
|
+
},
|
|
146
|
+
vercel: {
|
|
147
|
+
name: "Vercel",
|
|
148
|
+
fields: "--vercel-api-token",
|
|
149
|
+
howToGet: ` 1. Go to https://vercel.com/account/tokens
|
|
150
|
+
2. Create a new token with appropriate scope
|
|
151
|
+
3. Copy the token
|
|
152
|
+
Optional: --vercel-team-id (from Team Settings)`,
|
|
153
|
+
},
|
|
154
|
+
datadog: {
|
|
155
|
+
name: "Datadog",
|
|
156
|
+
fields: "--datadog-api-key + --datadog-application-key",
|
|
157
|
+
howToGet: ` API Key: Organization Settings > API Keys
|
|
158
|
+
Application Key: Organization Settings > Application Keys
|
|
159
|
+
Optional: --datadog-site us|eu (default: us)`,
|
|
160
|
+
},
|
|
161
|
+
neo4j: {
|
|
162
|
+
name: "Neo4j Aura",
|
|
163
|
+
fields: "--neo4j-client-id + --neo4j-client-secret",
|
|
164
|
+
howToGet: `How to get these values:
|
|
165
|
+
|
|
166
|
+
API Credentials (OAuth2 client credentials):
|
|
167
|
+
1. Go to https://console.neo4j.io/
|
|
168
|
+
2. Click your account menu (top-right) > API Credentials
|
|
169
|
+
3. Click "Create API Credentials"
|
|
170
|
+
4. Choose the "Tenant Admin" role for full monitoring + kill switch actions
|
|
171
|
+
5. Copy both the Client ID and Client Secret
|
|
172
|
+
|
|
173
|
+
Instance ID (optional):
|
|
174
|
+
Found in the console URL or the instance card:
|
|
175
|
+
https://console.neo4j.io/d/<INSTANCE_ID>/overview
|
|
176
|
+
Or omit to monitor all instances in your tenant.`,
|
|
177
|
+
},
|
|
80
178
|
};
|
|
81
179
|
const AVAILABLE_SHIELDS = [
|
|
82
180
|
"cost-runaway", "ddos", "brute-force", "error-storm",
|
|
83
181
|
"exfiltration", "gpu-runaway", "lambda-loop", "aws-cost-runaway",
|
|
84
182
|
];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
rl.question(question, (answer) => {
|
|
89
|
-
rl.close();
|
|
90
|
-
resolve(answer.trim());
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
export function registerOnboardCommands(program) {
|
|
183
|
+
const ONBOARDABLE_PROVIDERS = ["cloudflare", "gcp", "aws", "runpod", "neo4j", "mongodb"];
|
|
184
|
+
const ONBOARDABLE_LIST = ONBOARDABLE_PROVIDERS.join(", ");
|
|
185
|
+
export function registerOnboardCommands(program, createClient) {
|
|
95
186
|
program
|
|
96
187
|
.command("onboard")
|
|
97
188
|
.alias("setup")
|
|
98
189
|
.description("Quick setup: connect a cloud provider, apply protection, configure alerts")
|
|
99
|
-
.option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws")
|
|
190
|
+
.option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws, runpod, neo4j, mongodb")
|
|
100
191
|
.option("--name <name>", "Account name (e.g., Production)")
|
|
101
192
|
.option("--token <token>", "API token (Cloudflare)")
|
|
102
193
|
.option("--account-id <id>", "Account ID (Cloudflare)")
|
|
@@ -105,7 +196,19 @@ export function registerOnboardCommands(program) {
|
|
|
105
196
|
.option("--access-key <key>", "Access Key ID (AWS)")
|
|
106
197
|
.option("--secret-key <key>", "Secret Access Key (AWS)")
|
|
107
198
|
.option("--region <region>", "Region (AWS, default: us-east-1)")
|
|
199
|
+
.option("--runpod-api-key <key>", "API Key (RunPod)")
|
|
200
|
+
.option("--neo4j-client-id <id>", "Client ID (Neo4j Aura)")
|
|
201
|
+
.option("--neo4j-client-secret <secret>", "Client Secret (Neo4j Aura)")
|
|
202
|
+
.option("--neo4j-instance-id <id>", "Instance ID (Neo4j Aura, optional)")
|
|
203
|
+
.option("--mongodb-subtype <type>", "MongoDB sub-type: atlas | self-hosted (inferred if omitted)")
|
|
204
|
+
.option("--atlas-public-key <key>", "Public key (MongoDB Atlas API)")
|
|
205
|
+
.option("--atlas-private-key <key>", "Private key (MongoDB Atlas API)")
|
|
206
|
+
.option("--atlas-project-id <id>", "Project ID (MongoDB Atlas)")
|
|
207
|
+
.option("--atlas-cluster-name <name>", "Cluster name (MongoDB Atlas, optional)")
|
|
208
|
+
.option("--mongodb-uri <uri>", "Connection URI (self-hosted MongoDB)")
|
|
209
|
+
.option("--mongodb-database <name>", "Database name (self-hosted MongoDB, optional)")
|
|
108
210
|
.option("--shields <presets>", "Comma-separated shield presets to apply (default: cost-runaway)")
|
|
211
|
+
.option("--alert-pagerduty <key>", "PagerDuty Events API v2 routing key (recommended)")
|
|
109
212
|
.option("--alert-email <email>", "Email address for alerts")
|
|
110
213
|
.option("--alert-discord <url>", "Discord webhook URL for alerts")
|
|
111
214
|
.option("--alert-slack <url>", "Slack webhook URL for alerts")
|
|
@@ -118,14 +221,14 @@ Examples:
|
|
|
118
221
|
# Interactive onboarding
|
|
119
222
|
kill-switch onboard
|
|
120
223
|
|
|
121
|
-
# AI agent / non-interactive: connect Cloudflare
|
|
224
|
+
# AI agent / non-interactive: connect Cloudflare with PagerDuty
|
|
122
225
|
kill-switch onboard \\
|
|
123
226
|
--provider cloudflare \\
|
|
124
227
|
--account-id 14a6fa23390363382f378b5bd4a0f849 \\
|
|
125
228
|
--token cf-api-token-here \\
|
|
126
229
|
--name "Production" \\
|
|
127
230
|
--shields cost-runaway,ddos \\
|
|
128
|
-
--alert-
|
|
231
|
+
--alert-pagerduty YOUR_ROUTING_KEY
|
|
129
232
|
|
|
130
233
|
# Show how to get Cloudflare credentials
|
|
131
234
|
kill-switch onboard --help-provider cloudflare
|
|
@@ -146,7 +249,7 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
146
249
|
if (opts.helpProvider) {
|
|
147
250
|
const help = PROVIDER_HELP[opts.helpProvider];
|
|
148
251
|
if (!help) {
|
|
149
|
-
outputError(`Unknown provider: ${opts.helpProvider}. Use:
|
|
252
|
+
outputError(`Unknown provider: ${opts.helpProvider}. Use: ${ONBOARDABLE_LIST}`, json);
|
|
150
253
|
process.exit(1);
|
|
151
254
|
}
|
|
152
255
|
if (json) {
|
|
@@ -160,12 +263,13 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
160
263
|
return;
|
|
161
264
|
}
|
|
162
265
|
try {
|
|
266
|
+
const client = createClient();
|
|
163
267
|
let provider = opts.provider;
|
|
164
268
|
let name = opts.name;
|
|
165
269
|
// Interactive mode if no provider specified
|
|
166
270
|
if (!provider) {
|
|
167
271
|
if (json) {
|
|
168
|
-
outputError(
|
|
272
|
+
outputError(`--provider is required in JSON mode. Use: ${ONBOARDABLE_LIST}`, json);
|
|
169
273
|
process.exit(1);
|
|
170
274
|
}
|
|
171
275
|
console.log("\n\u26a1 Kill Switch Onboarding\n");
|
|
@@ -174,12 +278,15 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
174
278
|
console.log(" 1. cloudflare — Workers, R2, D1, Queues, Stream");
|
|
175
279
|
console.log(" 2. gcp — Cloud Run, Compute, GKE, BigQuery");
|
|
176
280
|
console.log(" 3. aws — EC2, Lambda, RDS, ECS, S3");
|
|
281
|
+
console.log(" 4. runpod — GPU Pods, Serverless Endpoints, Network Volumes");
|
|
282
|
+
console.log(" 5. neo4j — Neo4j Aura graph databases");
|
|
283
|
+
console.log(" 6. mongodb — MongoDB Atlas, self-hosted MongoDB");
|
|
177
284
|
console.log();
|
|
178
|
-
const choice = await ask("Choose a provider (1
|
|
179
|
-
provider = { "1": "cloudflare", "2": "gcp", "3": "aws" }[choice] || choice;
|
|
285
|
+
const choice = await ask("Choose a provider (1-6 or name): ");
|
|
286
|
+
provider = { "1": "cloudflare", "2": "gcp", "3": "aws", "4": "runpod", "5": "neo4j", "6": "mongodb" }[choice] || choice;
|
|
180
287
|
}
|
|
181
288
|
if (!PROVIDER_HELP[provider]) {
|
|
182
|
-
outputError(`Unknown provider: ${provider}. Use:
|
|
289
|
+
outputError(`Unknown provider: ${provider}. Use: ${ONBOARDABLE_LIST}`, json);
|
|
183
290
|
process.exit(1);
|
|
184
291
|
}
|
|
185
292
|
if (!name && !json) {
|
|
@@ -248,15 +355,115 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
248
355
|
credential.awsSecretAccessKey = secretKey;
|
|
249
356
|
credential.awsRegion = region || "us-east-1";
|
|
250
357
|
}
|
|
358
|
+
else if (provider === "runpod") {
|
|
359
|
+
let apiKey = opts.runpodApiKey;
|
|
360
|
+
if (!apiKey && !json) {
|
|
361
|
+
console.log("\n Tip: Create an API Key at https://www.runpod.io/console/user/settings");
|
|
362
|
+
apiKey = await ask(" RunPod API Key: ");
|
|
363
|
+
}
|
|
364
|
+
if (!apiKey) {
|
|
365
|
+
outputError(`RunPod requires ${PROVIDER_HELP.runpod.fields}`, json);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
credential.runpodApiKey = apiKey;
|
|
369
|
+
}
|
|
370
|
+
else if (provider === "neo4j") {
|
|
371
|
+
let neo4jClientId = opts.neo4jClientId || process.env.NEO4J_CLIENT_ID;
|
|
372
|
+
let neo4jClientSecret = opts.neo4jClientSecret || process.env.NEO4J_CLIENT_SECRET;
|
|
373
|
+
let neo4jInstanceId = opts.neo4jInstanceId || process.env.NEO4J_INSTANCE_ID;
|
|
374
|
+
if (!neo4jClientId && !json) {
|
|
375
|
+
console.log("\n Tip: Create API Credentials at https://console.neo4j.io/ > Account > API Credentials");
|
|
376
|
+
neo4jClientId = await ask(" Neo4j Client ID: ");
|
|
377
|
+
}
|
|
378
|
+
if (!neo4jClientSecret && !json) {
|
|
379
|
+
neo4jClientSecret = await ask(" Neo4j Client Secret: ");
|
|
380
|
+
}
|
|
381
|
+
if (!neo4jInstanceId && !json) {
|
|
382
|
+
neo4jInstanceId = await ask(" Instance ID (Enter to monitor all): ");
|
|
383
|
+
}
|
|
384
|
+
if (!neo4jClientId || !neo4jClientSecret) {
|
|
385
|
+
outputError(`Neo4j requires ${PROVIDER_HELP.neo4j.fields}`, json);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
credential.neo4jClientId = neo4jClientId;
|
|
389
|
+
credential.neo4jClientSecret = neo4jClientSecret;
|
|
390
|
+
if (neo4jInstanceId)
|
|
391
|
+
credential.neo4jInstanceId = neo4jInstanceId;
|
|
392
|
+
}
|
|
393
|
+
else if (provider === "mongodb") {
|
|
394
|
+
const hasAtlasFlags = !!(opts.atlasPublicKey || opts.atlasPrivateKey || opts.atlasProjectId);
|
|
395
|
+
const hasSelfHostedFlag = !!opts.mongodbUri;
|
|
396
|
+
if (hasAtlasFlags && hasSelfHostedFlag) {
|
|
397
|
+
outputError("MongoDB: pass either Atlas keys (--atlas-public-key/--atlas-private-key/--atlas-project-id) or --mongodb-uri, not both.", json);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
let subType = opts.mongodbSubtype === "atlas" || opts.mongodbSubtype === "self-hosted"
|
|
401
|
+
? opts.mongodbSubtype
|
|
402
|
+
: hasAtlasFlags ? "atlas" : hasSelfHostedFlag ? "self-hosted" : undefined;
|
|
403
|
+
if (!subType && !json) {
|
|
404
|
+
const answer = (await ask("MongoDB type — [a]tlas or [s]elf-hosted? ")).toLowerCase();
|
|
405
|
+
subType = answer.startsWith("s") ? "self-hosted" : "atlas";
|
|
406
|
+
}
|
|
407
|
+
if (!subType) {
|
|
408
|
+
outputError("MongoDB requires --mongodb-subtype atlas|self-hosted (or provide credentials inferring the type)", json);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
credential.mongodbSubType = subType;
|
|
412
|
+
if (subType === "atlas") {
|
|
413
|
+
let publicKey = opts.atlasPublicKey;
|
|
414
|
+
let privateKey = opts.atlasPrivateKey;
|
|
415
|
+
let projectId = opts.atlasProjectId;
|
|
416
|
+
let clusterName = opts.atlasClusterName;
|
|
417
|
+
if (!publicKey && !json) {
|
|
418
|
+
console.log("\n Tip: Create at Atlas > Access Manager > API Keys (Project-level)");
|
|
419
|
+
console.log(" Roles: Project Read Only + Project Cluster Manager");
|
|
420
|
+
publicKey = await ask(" Atlas Public Key: ");
|
|
421
|
+
}
|
|
422
|
+
if (!privateKey && !json) {
|
|
423
|
+
privateKey = await ask(" Atlas Private Key: ");
|
|
424
|
+
}
|
|
425
|
+
if (!projectId && !json) {
|
|
426
|
+
console.log("\n Tip: Project ID is in the URL: cloud.mongodb.com/v2/<PROJECT_ID>");
|
|
427
|
+
projectId = await ask(" Atlas Project ID: ");
|
|
428
|
+
}
|
|
429
|
+
if (!clusterName && !json) {
|
|
430
|
+
clusterName = await ask(" Cluster name (Enter to monitor first cluster in project): ");
|
|
431
|
+
}
|
|
432
|
+
if (!publicKey || !privateKey || !projectId) {
|
|
433
|
+
outputError(`MongoDB Atlas requires ${PROVIDER_HELP.mongodb.fields}`, json);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
credential.atlasPublicKey = publicKey;
|
|
437
|
+
credential.atlasPrivateKey = privateKey;
|
|
438
|
+
credential.atlasProjectId = projectId;
|
|
439
|
+
if (clusterName)
|
|
440
|
+
credential.atlasClusterName = clusterName;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
let uri = opts.mongodbUri;
|
|
444
|
+
if (!uri && !json) {
|
|
445
|
+
console.log("\n Tip: Format mongodb+srv://user:pass@host/db or mongodb://...");
|
|
446
|
+
uri = await ask(" MongoDB Connection URI: ");
|
|
447
|
+
}
|
|
448
|
+
if (!uri) {
|
|
449
|
+
outputError("MongoDB self-hosted requires --mongodb-uri", json);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
credential.mongodbUri = uri;
|
|
453
|
+
if (opts.mongodbDatabase)
|
|
454
|
+
credential.mongodbDatabaseName = opts.mongodbDatabase;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
251
457
|
// 1. Connect cloud account
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
458
|
+
const s = json ? null : spinner(`Connecting ${PROVIDER_HELP[provider].name}...`).start();
|
|
459
|
+
const account = await client.accounts.create({
|
|
460
|
+
provider: provider,
|
|
461
|
+
name,
|
|
462
|
+
credential: credential,
|
|
257
463
|
});
|
|
464
|
+
s?.stop();
|
|
258
465
|
if (!json)
|
|
259
|
-
|
|
466
|
+
success(`Connected: ${account.name || account.id}`);
|
|
260
467
|
// 2. Apply shields
|
|
261
468
|
if (!opts.skipShields) {
|
|
262
469
|
const shieldList = opts.shields
|
|
@@ -266,19 +473,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
266
473
|
console.log(`\nApplying ${shieldList.length} shield(s)...`);
|
|
267
474
|
for (const shield of shieldList) {
|
|
268
475
|
try {
|
|
269
|
-
await
|
|
476
|
+
await client.rules.applyPreset(shield);
|
|
270
477
|
if (!json)
|
|
271
|
-
|
|
478
|
+
success(` ${shield}`);
|
|
272
479
|
}
|
|
273
480
|
catch (err) {
|
|
274
481
|
if (!json)
|
|
275
|
-
|
|
482
|
+
fail(` ${shield}: ${err.message}`);
|
|
276
483
|
}
|
|
277
484
|
}
|
|
278
485
|
}
|
|
279
486
|
// 3. Set up alerts
|
|
280
487
|
if (!opts.skipAlerts) {
|
|
281
488
|
const channels = [];
|
|
489
|
+
if (opts.alertPagerduty) {
|
|
490
|
+
channels.push({ type: "pagerduty", name: "PagerDuty", config: { routingKey: opts.alertPagerduty }, enabled: true });
|
|
491
|
+
}
|
|
282
492
|
if (opts.alertEmail) {
|
|
283
493
|
channels.push({ type: "email", name: "Email", config: { email: opts.alertEmail }, enabled: true });
|
|
284
494
|
}
|
|
@@ -289,28 +499,30 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
289
499
|
channels.push({ type: "slack", name: "Slack", config: { webhookUrl: opts.alertSlack }, enabled: true });
|
|
290
500
|
}
|
|
291
501
|
if (channels.length === 0 && !json) {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
channels.push({ type: "
|
|
502
|
+
const pdKey = await ask("\nPagerDuty routing key (or Enter to skip): ");
|
|
503
|
+
if (pdKey) {
|
|
504
|
+
channels.push({ type: "pagerduty", name: "PagerDuty", config: { routingKey: pdKey }, enabled: true });
|
|
295
505
|
}
|
|
296
506
|
}
|
|
297
507
|
if (channels.length > 0) {
|
|
298
508
|
if (!json)
|
|
299
509
|
console.log("Setting up alerts...");
|
|
300
510
|
try {
|
|
301
|
-
|
|
511
|
+
// Append to existing channels — re-running onboard must not wipe prior integrations.
|
|
512
|
+
const existing = await client.alerts.channels().catch(() => []);
|
|
513
|
+
await client.alerts.updateChannels([...existing, ...channels]);
|
|
302
514
|
if (!json)
|
|
303
|
-
|
|
515
|
+
success(` ${channels.length} alert channel(s) configured`);
|
|
304
516
|
}
|
|
305
517
|
catch (err) {
|
|
306
518
|
if (!json)
|
|
307
|
-
|
|
519
|
+
fail(` Alerts: ${err.message}`);
|
|
308
520
|
}
|
|
309
521
|
}
|
|
310
522
|
}
|
|
311
523
|
// 4. Complete onboarding
|
|
312
524
|
try {
|
|
313
|
-
await
|
|
525
|
+
await client.account.update({ onboardingCompleted: true });
|
|
314
526
|
}
|
|
315
527
|
catch {
|
|
316
528
|
// Non-critical
|
|
@@ -319,22 +531,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
319
531
|
outputJson({
|
|
320
532
|
success: true,
|
|
321
533
|
provider,
|
|
322
|
-
accountId: account.
|
|
534
|
+
accountId: account.id,
|
|
323
535
|
accountName: account.name,
|
|
324
536
|
});
|
|
325
537
|
}
|
|
326
538
|
else {
|
|
327
|
-
console.log(`\
|
|
539
|
+
console.log(`\nSetup complete! Kill Switch is monitoring your ${PROVIDER_HELP[provider].name} account.`);
|
|
328
540
|
console.log("\nNext steps:");
|
|
329
|
-
console.log(" kill-switch
|
|
330
|
-
console.log(" kill-switch
|
|
331
|
-
console.log(" kill-switch
|
|
332
|
-
console.log(" kill-switch
|
|
541
|
+
console.log(" kill-switch check — run a monitoring check");
|
|
542
|
+
console.log(" kill-switch alerts add --type pagerduty --routing-key KEY — set up on-call alerts");
|
|
543
|
+
console.log(" kill-switch alerts test — verify alerts work");
|
|
544
|
+
console.log(" kill-switch accounts list — view connected accounts");
|
|
545
|
+
console.log(" kill-switch onboard --provider <p> — add another provider\n");
|
|
333
546
|
}
|
|
334
547
|
}
|
|
335
548
|
catch (err) {
|
|
336
|
-
|
|
337
|
-
process.exit(1);
|
|
549
|
+
handleError(err, json);
|
|
338
550
|
}
|
|
339
551
|
});
|
|
340
552
|
}
|