@kill-switch/cli 0.2.0 → 0.3.1
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 +86 -46
- package/dist/commands/check.d.ts +2 -1
- package/dist/commands/check.js +28 -15
- package/dist/commands/config-cmd.js +20 -5
- 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 +244 -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/config.d.ts +4 -1
- package/dist/config.js +19 -2
- 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",
|
|
@@ -92,26 +75,119 @@ const PROVIDER_HELP = {
|
|
|
92
75
|
- Read access to pods, serverless endpoints, and network volumes
|
|
93
76
|
- Write access if you want auto-kill actions (stop/terminate pods, scale endpoints)`,
|
|
94
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
|
+
},
|
|
95
178
|
};
|
|
96
179
|
const AVAILABLE_SHIELDS = [
|
|
97
180
|
"cost-runaway", "ddos", "brute-force", "error-storm",
|
|
98
181
|
"exfiltration", "gpu-runaway", "lambda-loop", "aws-cost-runaway",
|
|
99
182
|
];
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
rl.question(question, (answer) => {
|
|
104
|
-
rl.close();
|
|
105
|
-
resolve(answer.trim());
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
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) {
|
|
110
186
|
program
|
|
111
187
|
.command("onboard")
|
|
112
188
|
.alias("setup")
|
|
113
189
|
.description("Quick setup: connect a cloud provider, apply protection, configure alerts")
|
|
114
|
-
.option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws")
|
|
190
|
+
.option("--provider <provider>", "Cloud provider: cloudflare, gcp, aws, runpod, neo4j, mongodb")
|
|
115
191
|
.option("--name <name>", "Account name (e.g., Production)")
|
|
116
192
|
.option("--token <token>", "API token (Cloudflare)")
|
|
117
193
|
.option("--account-id <id>", "Account ID (Cloudflare)")
|
|
@@ -121,7 +197,18 @@ export function registerOnboardCommands(program) {
|
|
|
121
197
|
.option("--secret-key <key>", "Secret Access Key (AWS)")
|
|
122
198
|
.option("--region <region>", "Region (AWS, default: us-east-1)")
|
|
123
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)")
|
|
124
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)")
|
|
125
212
|
.option("--alert-email <email>", "Email address for alerts")
|
|
126
213
|
.option("--alert-discord <url>", "Discord webhook URL for alerts")
|
|
127
214
|
.option("--alert-slack <url>", "Slack webhook URL for alerts")
|
|
@@ -134,14 +221,14 @@ Examples:
|
|
|
134
221
|
# Interactive onboarding
|
|
135
222
|
kill-switch onboard
|
|
136
223
|
|
|
137
|
-
# AI agent / non-interactive: connect Cloudflare
|
|
224
|
+
# AI agent / non-interactive: connect Cloudflare with PagerDuty
|
|
138
225
|
kill-switch onboard \\
|
|
139
226
|
--provider cloudflare \\
|
|
140
227
|
--account-id 14a6fa23390363382f378b5bd4a0f849 \\
|
|
141
228
|
--token cf-api-token-here \\
|
|
142
229
|
--name "Production" \\
|
|
143
230
|
--shields cost-runaway,ddos \\
|
|
144
|
-
--alert-
|
|
231
|
+
--alert-pagerduty YOUR_ROUTING_KEY
|
|
145
232
|
|
|
146
233
|
# Show how to get Cloudflare credentials
|
|
147
234
|
kill-switch onboard --help-provider cloudflare
|
|
@@ -162,7 +249,7 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
162
249
|
if (opts.helpProvider) {
|
|
163
250
|
const help = PROVIDER_HELP[opts.helpProvider];
|
|
164
251
|
if (!help) {
|
|
165
|
-
outputError(`Unknown provider: ${opts.helpProvider}. Use:
|
|
252
|
+
outputError(`Unknown provider: ${opts.helpProvider}. Use: ${ONBOARDABLE_LIST}`, json);
|
|
166
253
|
process.exit(1);
|
|
167
254
|
}
|
|
168
255
|
if (json) {
|
|
@@ -176,12 +263,13 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
176
263
|
return;
|
|
177
264
|
}
|
|
178
265
|
try {
|
|
266
|
+
const client = createClient();
|
|
179
267
|
let provider = opts.provider;
|
|
180
268
|
let name = opts.name;
|
|
181
269
|
// Interactive mode if no provider specified
|
|
182
270
|
if (!provider) {
|
|
183
271
|
if (json) {
|
|
184
|
-
outputError(
|
|
272
|
+
outputError(`--provider is required in JSON mode. Use: ${ONBOARDABLE_LIST}`, json);
|
|
185
273
|
process.exit(1);
|
|
186
274
|
}
|
|
187
275
|
console.log("\n\u26a1 Kill Switch Onboarding\n");
|
|
@@ -191,12 +279,14 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
191
279
|
console.log(" 2. gcp — Cloud Run, Compute, GKE, BigQuery");
|
|
192
280
|
console.log(" 3. aws — EC2, Lambda, RDS, ECS, S3");
|
|
193
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");
|
|
194
284
|
console.log();
|
|
195
|
-
const choice = await ask("Choose a provider (1
|
|
196
|
-
provider = { "1": "cloudflare", "2": "gcp", "3": "aws", "4": "runpod" }[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;
|
|
197
287
|
}
|
|
198
288
|
if (!PROVIDER_HELP[provider]) {
|
|
199
|
-
outputError(`Unknown provider: ${provider}. Use:
|
|
289
|
+
outputError(`Unknown provider: ${provider}. Use: ${ONBOARDABLE_LIST}`, json);
|
|
200
290
|
process.exit(1);
|
|
201
291
|
}
|
|
202
292
|
if (!name && !json) {
|
|
@@ -277,15 +367,103 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
277
367
|
}
|
|
278
368
|
credential.runpodApiKey = apiKey;
|
|
279
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
|
+
}
|
|
280
457
|
// 1. Connect cloud account
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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,
|
|
286
463
|
});
|
|
464
|
+
s?.stop();
|
|
287
465
|
if (!json)
|
|
288
|
-
|
|
466
|
+
success(`Connected: ${account.name || account.id}`);
|
|
289
467
|
// 2. Apply shields
|
|
290
468
|
if (!opts.skipShields) {
|
|
291
469
|
const shieldList = opts.shields
|
|
@@ -295,19 +473,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
295
473
|
console.log(`\nApplying ${shieldList.length} shield(s)...`);
|
|
296
474
|
for (const shield of shieldList) {
|
|
297
475
|
try {
|
|
298
|
-
await
|
|
476
|
+
await client.rules.applyPreset(shield);
|
|
299
477
|
if (!json)
|
|
300
|
-
|
|
478
|
+
success(` ${shield}`);
|
|
301
479
|
}
|
|
302
480
|
catch (err) {
|
|
303
481
|
if (!json)
|
|
304
|
-
|
|
482
|
+
fail(` ${shield}: ${err.message}`);
|
|
305
483
|
}
|
|
306
484
|
}
|
|
307
485
|
}
|
|
308
486
|
// 3. Set up alerts
|
|
309
487
|
if (!opts.skipAlerts) {
|
|
310
488
|
const channels = [];
|
|
489
|
+
if (opts.alertPagerduty) {
|
|
490
|
+
channels.push({ type: "pagerduty", name: "PagerDuty", config: { routingKey: opts.alertPagerduty }, enabled: true });
|
|
491
|
+
}
|
|
311
492
|
if (opts.alertEmail) {
|
|
312
493
|
channels.push({ type: "email", name: "Email", config: { email: opts.alertEmail }, enabled: true });
|
|
313
494
|
}
|
|
@@ -318,28 +499,30 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
318
499
|
channels.push({ type: "slack", name: "Slack", config: { webhookUrl: opts.alertSlack }, enabled: true });
|
|
319
500
|
}
|
|
320
501
|
if (channels.length === 0 && !json) {
|
|
321
|
-
const
|
|
322
|
-
if (
|
|
323
|
-
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 });
|
|
324
505
|
}
|
|
325
506
|
}
|
|
326
507
|
if (channels.length > 0) {
|
|
327
508
|
if (!json)
|
|
328
509
|
console.log("Setting up alerts...");
|
|
329
510
|
try {
|
|
330
|
-
|
|
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]);
|
|
331
514
|
if (!json)
|
|
332
|
-
|
|
515
|
+
success(` ${channels.length} alert channel(s) configured`);
|
|
333
516
|
}
|
|
334
517
|
catch (err) {
|
|
335
518
|
if (!json)
|
|
336
|
-
|
|
519
|
+
fail(` Alerts: ${err.message}`);
|
|
337
520
|
}
|
|
338
521
|
}
|
|
339
522
|
}
|
|
340
523
|
// 4. Complete onboarding
|
|
341
524
|
try {
|
|
342
|
-
await
|
|
525
|
+
await client.account.update({ onboardingCompleted: true });
|
|
343
526
|
}
|
|
344
527
|
catch {
|
|
345
528
|
// Non-critical
|
|
@@ -348,22 +531,22 @@ Available shields: ${AVAILABLE_SHIELDS.join(", ")}
|
|
|
348
531
|
outputJson({
|
|
349
532
|
success: true,
|
|
350
533
|
provider,
|
|
351
|
-
accountId: account.
|
|
534
|
+
accountId: account.id,
|
|
352
535
|
accountName: account.name,
|
|
353
536
|
});
|
|
354
537
|
}
|
|
355
538
|
else {
|
|
356
|
-
console.log(`\
|
|
539
|
+
console.log(`\nSetup complete! Kill Switch is monitoring your ${PROVIDER_HELP[provider].name} account.`);
|
|
357
540
|
console.log("\nNext steps:");
|
|
358
|
-
console.log(" kill-switch
|
|
359
|
-
console.log(" kill-switch
|
|
360
|
-
console.log(" kill-switch
|
|
361
|
-
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");
|
|
362
546
|
}
|
|
363
547
|
}
|
|
364
548
|
catch (err) {
|
|
365
|
-
|
|
366
|
-
process.exit(1);
|
|
549
|
+
handleError(err, json);
|
|
367
550
|
}
|
|
368
551
|
});
|
|
369
552
|
}
|