@chainpatrol/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.
@@ -0,0 +1,139 @@
1
+ import {
2
+ createApiClient
3
+ } from "./chunk-H7UKKLCV.js";
4
+ import {
5
+ ErrorDisplay,
6
+ Spinner
7
+ } from "./chunk-JCMWDZYY.js";
8
+ import {
9
+ AuthCorruptedError,
10
+ AuthExpiredError,
11
+ AuthNotLoggedInError
12
+ } from "./chunk-EEG7T6WT.js";
13
+ import "./chunk-U73SABXK.js";
14
+
15
+ // src/commands/configs/list.tsx
16
+ import { useState, useEffect } from "react";
17
+ import { Text, Box, useApp } from "ink";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ var shouldAutoExit = process.env.NODE_ENV !== "test";
20
+ function statusColor(status) {
21
+ switch (status) {
22
+ case "enabled":
23
+ return "green";
24
+ case "disabled":
25
+ return "red";
26
+ default:
27
+ return "yellow";
28
+ }
29
+ }
30
+ function formatConfigValue(key, value) {
31
+ if (value === null || value === void 0 || value === "") return "";
32
+ if (typeof value === "boolean") return value ? "yes" : "no";
33
+ return String(value);
34
+ }
35
+ function ConfigsList({ org, apiClient }) {
36
+ const { exit } = useApp();
37
+ const [state, setState] = useState({ phase: "loading" });
38
+ useEffect(() => {
39
+ const fetchConfigs = async () => {
40
+ try {
41
+ const client = apiClient ?? createApiClient();
42
+ const sources = await client.listDetectionConfigs(org);
43
+ setState({ phase: "loaded", sources });
44
+ } catch (err) {
45
+ if (err instanceof AuthNotLoggedInError || err instanceof AuthCorruptedError) {
46
+ setState({ phase: "error", message: err.message });
47
+ } else if (err instanceof AuthExpiredError) {
48
+ setState({ phase: "error", message: err.message });
49
+ } else {
50
+ const message = err instanceof Error ? err.message : "Failed to fetch detection configs";
51
+ if (message.includes("FORBIDDEN") || message.includes("403")) {
52
+ setState({
53
+ phase: "error",
54
+ message: "You don't have permission to view configs for this organization."
55
+ });
56
+ } else {
57
+ setState({ phase: "error", message });
58
+ }
59
+ }
60
+ if (shouldAutoExit) {
61
+ setTimeout(() => exit(), 100);
62
+ }
63
+ }
64
+ };
65
+ fetchConfigs();
66
+ }, []);
67
+ switch (state.phase) {
68
+ case "loading":
69
+ return /* @__PURE__ */ jsx(Spinner, { label: "Fetching detection configs..." });
70
+ case "error":
71
+ return /* @__PURE__ */ jsx(ErrorDisplay, { message: state.message });
72
+ case "loaded": {
73
+ if (shouldAutoExit) {
74
+ setTimeout(() => exit(), 100);
75
+ }
76
+ const configured = state.sources.filter((s) => s.configs.length > 0);
77
+ const globalOnly = state.sources.filter(
78
+ (s) => s.scope === "global" && s.configs.length === 0
79
+ );
80
+ const notConfigured = state.sources.filter(
81
+ (s) => s.scope !== "global" && s.configs.length === 0
82
+ );
83
+ const totalConfigs = state.sources.reduce((sum, s) => sum + s.configs.length, 0);
84
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
85
+ configured.map((source) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
86
+ /* @__PURE__ */ jsx(Text, { bold: true, children: source.source }),
87
+ source.configs.map((cfg) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [
88
+ /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
89
+ /* @__PURE__ */ jsx(Text, { color: statusColor(cfg.status), children: cfg.status }),
90
+ cfg.title ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: cfg.title }) : null,
91
+ cfg.cron ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
92
+ "cron: ",
93
+ cfg.cron
94
+ ] }) : null
95
+ ] }),
96
+ Object.keys(cfg.config).length > 0 ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginLeft: 2, children: Object.entries(cfg.config).map(([key, value]) => {
97
+ const formatted = formatConfigValue(key, value);
98
+ if (!formatted) return null;
99
+ return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
100
+ key,
101
+ ": ",
102
+ formatted
103
+ ] }, key);
104
+ }) }) : null
105
+ ] }, cfg.id))
106
+ ] }, source.source)),
107
+ globalOnly.length > 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
108
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "blue", children: [
109
+ "Global (",
110
+ globalOnly.length,
111
+ "):"
112
+ ] }),
113
+ /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: globalOnly.map((s) => s.source).join(", ") }) })
114
+ ] }) : null,
115
+ notConfigured.length > 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
116
+ /* @__PURE__ */ jsxs(Text, { bold: true, dimColor: true, children: [
117
+ "Not configured (",
118
+ notConfigured.length,
119
+ "):"
120
+ ] }),
121
+ /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: notConfigured.map((s) => s.source).join(", ") }) })
122
+ ] }) : null,
123
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
124
+ "\n",
125
+ state.sources.length,
126
+ " source(s), ",
127
+ totalConfigs,
128
+ " config(s) across",
129
+ " ",
130
+ configured.length,
131
+ " active source(s)"
132
+ ] })
133
+ ] });
134
+ }
135
+ }
136
+ }
137
+ export {
138
+ ConfigsList as default
139
+ };
@@ -0,0 +1,24 @@
1
+ import {
2
+ createApiClient
3
+ } from "./chunk-H7UKKLCV.js";
4
+ import "./chunk-EEG7T6WT.js";
5
+ import "./chunk-U73SABXK.js";
6
+
7
+ // src/commands/configs/list-json.ts
8
+ async function listConfigsJson({
9
+ org,
10
+ apiClient
11
+ }) {
12
+ try {
13
+ const client = apiClient ?? createApiClient();
14
+ const configs = await client.listDetectionConfigs(org);
15
+ console.log(JSON.stringify(configs, null, 2));
16
+ } catch (err) {
17
+ const message = err instanceof Error ? err.message : "Failed to fetch configs";
18
+ console.error(JSON.stringify({ error: message }));
19
+ process.exit(1);
20
+ }
21
+ }
22
+ export {
23
+ listConfigsJson
24
+ };
@@ -0,0 +1,162 @@
1
+ import {
2
+ ErrorDisplay,
3
+ Spinner
4
+ } from "./chunk-JCMWDZYY.js";
5
+ import {
6
+ fetchUserEmail,
7
+ getCredentials,
8
+ isLoggedIn,
9
+ pollForToken,
10
+ requestDeviceCode,
11
+ storeCredentials
12
+ } from "./chunk-EEG7T6WT.js";
13
+ import "./chunk-U73SABXK.js";
14
+
15
+ // src/commands/login.tsx
16
+ import { useState, useEffect } from "react";
17
+ import { Text, Box, useApp } from "ink";
18
+ import open from "open";
19
+ import { jsx, jsxs } from "react/jsx-runtime";
20
+ function Login() {
21
+ const { exit } = useApp();
22
+ const [state, setState] = useState({ phase: "checking" });
23
+ useEffect(() => {
24
+ if (isLoggedIn()) {
25
+ const creds = getCredentials();
26
+ if (creds.email) {
27
+ setState({ phase: "already-logged-in", email: creds.email });
28
+ setTimeout(() => exit(), 100);
29
+ } else {
30
+ fetchUserEmail(creds.accessToken).then((email) => {
31
+ if (email) {
32
+ storeCredentials({ ...creds, email });
33
+ }
34
+ setState({ phase: "already-logged-in", email: email ?? "" });
35
+ setTimeout(() => exit(), 100);
36
+ }).catch(() => {
37
+ setState({ phase: "already-logged-in", email: "" });
38
+ setTimeout(() => exit(), 100);
39
+ });
40
+ }
41
+ return;
42
+ }
43
+ setState({ phase: "requesting-code" });
44
+ requestDeviceCode().then((deviceCode) => {
45
+ setState({ phase: "waiting-for-approval", deviceCode });
46
+ const uri = deviceCode.verification_uri_complete ?? deviceCode.verification_uri;
47
+ open(uri).catch(() => {
48
+ });
49
+ }).catch((err) => {
50
+ setState({
51
+ phase: "error",
52
+ message: err instanceof Error ? err.message : "Failed to request device code"
53
+ });
54
+ setTimeout(() => exit(), 100);
55
+ });
56
+ }, []);
57
+ useEffect(() => {
58
+ if (state.phase !== "waiting-for-approval") return;
59
+ let interval = state.deviceCode.interval * 1e3;
60
+ let cancelled = false;
61
+ const poll = async () => {
62
+ while (!cancelled) {
63
+ await new Promise((r) => setTimeout(r, interval));
64
+ if (cancelled) break;
65
+ const result = await pollForToken(state.deviceCode.device_code);
66
+ switch (result.status) {
67
+ case "success": {
68
+ const expiresAt = new Date(
69
+ Date.now() + result.expiresIn * 1e3
70
+ ).toISOString();
71
+ const email = await fetchUserEmail(result.accessToken);
72
+ const creds = {
73
+ accessToken: result.accessToken,
74
+ expiresAt,
75
+ ...email ? { email } : {}
76
+ };
77
+ storeCredentials(creds);
78
+ setState({ phase: "success", email: email ?? "" });
79
+ setTimeout(() => exit(), 500);
80
+ return;
81
+ }
82
+ case "pending":
83
+ continue;
84
+ case "slow_down":
85
+ interval += result.addSeconds * 1e3;
86
+ continue;
87
+ case "expired":
88
+ setState({
89
+ phase: "error",
90
+ message: "Code expired. Run `chainpatrol login` again."
91
+ });
92
+ setTimeout(() => exit(), 100);
93
+ return;
94
+ case "denied":
95
+ setState({ phase: "error", message: "Authorization denied." });
96
+ setTimeout(() => exit(), 100);
97
+ return;
98
+ case "error":
99
+ setState({ phase: "error", message: result.message });
100
+ setTimeout(() => exit(), 100);
101
+ return;
102
+ }
103
+ }
104
+ };
105
+ poll().catch((err) => {
106
+ if (!cancelled) {
107
+ setState({
108
+ phase: "error",
109
+ message: err instanceof Error ? `Cannot reach ChainPatrol API. ${err.message}` : "Network error during authentication."
110
+ });
111
+ setTimeout(() => exit(), 100);
112
+ }
113
+ });
114
+ return () => {
115
+ cancelled = true;
116
+ };
117
+ }, [state.phase === "waiting-for-approval" ? state.deviceCode.device_code : null]);
118
+ switch (state.phase) {
119
+ case "checking":
120
+ return /* @__PURE__ */ jsx(Spinner, { label: "Checking authentication status..." });
121
+ case "already-logged-in":
122
+ return /* @__PURE__ */ jsxs(Text, { children: [
123
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "\u2713" }),
124
+ /* @__PURE__ */ jsx(Text, { children: " Already logged in" }),
125
+ state.email ? /* @__PURE__ */ jsxs(Text, { children: [
126
+ " ",
127
+ "as ",
128
+ /* @__PURE__ */ jsx(Text, { bold: true, children: state.email })
129
+ ] }) : null
130
+ ] });
131
+ case "requesting-code":
132
+ return /* @__PURE__ */ jsx(Spinner, { label: "Requesting device code..." });
133
+ case "waiting-for-approval":
134
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
135
+ /* @__PURE__ */ jsxs(Box, { children: [
136
+ /* @__PURE__ */ jsx(Text, { children: "Your code is: " }),
137
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: state.deviceCode.user_code.length === 8 ? `${state.deviceCode.user_code.slice(0, 4)}-${state.deviceCode.user_code.slice(4)}` : state.deviceCode.user_code })
138
+ ] }),
139
+ /* @__PURE__ */ jsxs(Text, { children: [
140
+ "Open this URL in your browser:",
141
+ " ",
142
+ /* @__PURE__ */ jsx(Text, { color: "blue", underline: true, children: state.deviceCode.verification_uri })
143
+ ] }),
144
+ /* @__PURE__ */ jsx(Spinner, { label: "Waiting for approval..." })
145
+ ] });
146
+ case "success":
147
+ return /* @__PURE__ */ jsxs(Text, { children: [
148
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "\u2713" }),
149
+ /* @__PURE__ */ jsx(Text, { children: " Logged in successfully" }),
150
+ state.email ? /* @__PURE__ */ jsxs(Text, { children: [
151
+ " ",
152
+ "as ",
153
+ /* @__PURE__ */ jsx(Text, { bold: true, children: state.email })
154
+ ] }) : null
155
+ ] });
156
+ case "error":
157
+ return /* @__PURE__ */ jsx(ErrorDisplay, { message: state.message });
158
+ }
159
+ }
160
+ export {
161
+ Login as default
162
+ };
@@ -0,0 +1,71 @@
1
+ import {
2
+ fetchUserEmail,
3
+ getCredentials,
4
+ isLoggedIn,
5
+ pollForToken,
6
+ requestDeviceCode,
7
+ storeCredentials
8
+ } from "./chunk-EEG7T6WT.js";
9
+ import "./chunk-U73SABXK.js";
10
+
11
+ // src/commands/login-json.ts
12
+ async function loginJson() {
13
+ if (isLoggedIn()) {
14
+ const creds = getCredentials();
15
+ console.log(
16
+ JSON.stringify({ status: "already_logged_in", email: creds.email ?? null })
17
+ );
18
+ return;
19
+ }
20
+ const deviceCode = await requestDeviceCode();
21
+ console.error(
22
+ JSON.stringify({
23
+ action: "open_url",
24
+ user_code: deviceCode.user_code,
25
+ verification_uri: deviceCode.verification_uri,
26
+ verification_uri_complete: deviceCode.verification_uri_complete ?? null,
27
+ expires_in: deviceCode.expires_in
28
+ })
29
+ );
30
+ let interval = deviceCode.interval * 1e3;
31
+ while (true) {
32
+ await new Promise((r) => setTimeout(r, interval));
33
+ const result = await pollForToken(deviceCode.device_code);
34
+ switch (result.status) {
35
+ case "success": {
36
+ const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
37
+ const email = await fetchUserEmail(result.accessToken);
38
+ const creds = {
39
+ accessToken: result.accessToken,
40
+ expiresAt,
41
+ ...email ? { email } : {}
42
+ };
43
+ storeCredentials(creds);
44
+ console.log(JSON.stringify({ status: "success", email: email ?? null }));
45
+ return;
46
+ }
47
+ case "pending":
48
+ continue;
49
+ case "slow_down":
50
+ interval += result.addSeconds * 1e3;
51
+ continue;
52
+ case "expired":
53
+ console.error(
54
+ JSON.stringify({ error: "Code expired. Run `chainpatrol login` again." })
55
+ );
56
+ process.exit(1);
57
+ break;
58
+ case "denied":
59
+ console.error(JSON.stringify({ error: "Authorization denied." }));
60
+ process.exit(1);
61
+ break;
62
+ case "error":
63
+ console.error(JSON.stringify({ error: result.message }));
64
+ process.exit(1);
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ export {
70
+ loginJson
71
+ };
@@ -0,0 +1,25 @@
1
+ import {
2
+ clearCredentials,
3
+ isLoggedIn
4
+ } from "./chunk-EEG7T6WT.js";
5
+ import "./chunk-U73SABXK.js";
6
+
7
+ // src/commands/logout.tsx
8
+ import { Text, useApp } from "ink";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ function Logout() {
11
+ const { exit } = useApp();
12
+ if (!isLoggedIn()) {
13
+ setTimeout(() => exit(), 100);
14
+ return /* @__PURE__ */ jsx(Text, { children: "Not currently logged in." });
15
+ }
16
+ clearCredentials();
17
+ setTimeout(() => exit(), 100);
18
+ return /* @__PURE__ */ jsxs(Text, { children: [
19
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "\u2713" }),
20
+ /* @__PURE__ */ jsx(Text, { children: " Logged out successfully." })
21
+ ] });
22
+ }
23
+ export {
24
+ Logout as default
25
+ };
@@ -0,0 +1,18 @@
1
+ import {
2
+ clearCredentials,
3
+ isLoggedIn
4
+ } from "./chunk-EEG7T6WT.js";
5
+ import "./chunk-U73SABXK.js";
6
+
7
+ // src/commands/logout-json.ts
8
+ async function logoutJson() {
9
+ if (!isLoggedIn()) {
10
+ console.log(JSON.stringify({ status: "not_logged_in" }));
11
+ return;
12
+ }
13
+ clearCredentials();
14
+ console.log(JSON.stringify({ status: "logged_out" }));
15
+ }
16
+ export {
17
+ logoutJson
18
+ };
@@ -0,0 +1,112 @@
1
+ import {
2
+ CliExitError,
3
+ ExitCode
4
+ } from "./chunk-E2LAMILJ.js";
5
+ import {
6
+ printOutput,
7
+ toCsvRows
8
+ } from "./chunk-VFT3TD3E.js";
9
+ import {
10
+ createApiClient
11
+ } from "./chunk-H7UKKLCV.js";
12
+ import "./chunk-EEG7T6WT.js";
13
+ import "./chunk-U73SABXK.js";
14
+
15
+ // src/commands/detections/run.ts
16
+ async function runDetectionsRun(options) {
17
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
18
+ if (options.dryRun) {
19
+ const payload = {
20
+ slug: options.org,
21
+ configId: options.configId,
22
+ source: options.source,
23
+ includeDisabled: options.includeDisabled ?? false
24
+ };
25
+ printOutput({
26
+ outputFormat,
27
+ json: {
28
+ dryRun: true,
29
+ mutation: "detection.configs.run",
30
+ payload,
31
+ explanation: options.explain ? "This command triggers on-demand execution for the selected configs." : void 0
32
+ },
33
+ markdown: [
34
+ "# Dry Run: Detection Run",
35
+ "",
36
+ `- Org: ${payload.slug}`,
37
+ `- Config ID: ${payload.configId ?? "all"}`,
38
+ `- Source: ${payload.source ?? "all"}`,
39
+ `- Include disabled: ${payload.includeDisabled}`
40
+ ].join("\n"),
41
+ human: () => {
42
+ console.log("Dry run only. No detection configs were executed.");
43
+ console.log(
44
+ `Would run configs for org=${payload.slug} configId=${payload.configId ?? "all"} source=${payload.source ?? "all"}`
45
+ );
46
+ }
47
+ });
48
+ return;
49
+ }
50
+ const client = options.apiClient ?? createApiClient();
51
+ const result = await client.runDetectionConfigs({
52
+ slug: options.org,
53
+ configId: options.configId,
54
+ source: options.source,
55
+ includeDisabled: options.includeDisabled ?? false
56
+ });
57
+ const csv = toCsvRows(
58
+ result.results.map((item) => ({
59
+ configId: item.configId,
60
+ source: item.source,
61
+ ok: item.ok,
62
+ assetsCount: item.assetsCount,
63
+ message: item.message
64
+ }))
65
+ );
66
+ const markdown = [
67
+ `# Detection Run (${options.org})`,
68
+ "",
69
+ `- Ran: ${result.ranCount}`,
70
+ `- Success: ${result.successCount}`,
71
+ `- Failed: ${result.failedCount}`,
72
+ "",
73
+ ...result.results.map(
74
+ (item) => `- ${item.ok ? "OK" : "FAIL"} [${item.source}] #${item.configId} assets=${item.assetsCount}${item.message ? ` (${item.message})` : ""}`
75
+ )
76
+ ].join("\n");
77
+ printOutput({
78
+ outputFormat,
79
+ json: {
80
+ ...result,
81
+ explanation: options.explain ? {
82
+ checkType: "detection_run",
83
+ failureCondition: "failedCount > 0"
84
+ } : void 0
85
+ },
86
+ markdown,
87
+ csv,
88
+ human: () => {
89
+ console.log(
90
+ `Ran ${result.ranCount} config(s): ${result.successCount} success, ${result.failedCount} failed`
91
+ );
92
+ for (const item of result.results) {
93
+ const icon = item.ok ? "\u2713" : "\u2717";
94
+ console.log(
95
+ `${icon} [${item.source}] config ${item.configId} assets=${item.assetsCount}${item.message ? ` message=${item.message}` : ""}`
96
+ );
97
+ }
98
+ if (options.explain) {
99
+ console.log("Run is marked failed when at least one config run returns not ok.");
100
+ }
101
+ }
102
+ });
103
+ if (result.failedCount > 0) {
104
+ throw new CliExitError(
105
+ "One or more detection config runs failed.",
106
+ ExitCode.MUTATION_PARTIAL_FAILURE
107
+ );
108
+ }
109
+ }
110
+ export {
111
+ runDetectionsRun
112
+ };
@@ -0,0 +1,34 @@
1
+ import {
2
+ getPresetDefinition,
3
+ runPreset
4
+ } from "./chunk-D2QGXYXZ.js";
5
+ import {
6
+ CliExitError,
7
+ ExitCode
8
+ } from "./chunk-E2LAMILJ.js";
9
+ import "./chunk-VFT3TD3E.js";
10
+ import "./chunk-H7UKKLCV.js";
11
+ import "./chunk-EEG7T6WT.js";
12
+ import "./chunk-U73SABXK.js";
13
+
14
+ // src/commands/presets/run.ts
15
+ async function runPresetsRun({
16
+ presetId,
17
+ org,
18
+ outputFormat,
19
+ explain
20
+ }) {
21
+ const preset = getPresetDefinition(presetId);
22
+ if (!preset) {
23
+ throw new CliExitError(`Unknown preset '${presetId}'.`, ExitCode.USAGE);
24
+ }
25
+ await runPreset({
26
+ presetId: preset.id,
27
+ org,
28
+ outputFormat,
29
+ explain
30
+ });
31
+ }
32
+ export {
33
+ runPresetsRun
34
+ };