zeuslock-dlp-cli 0.2.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,211 @@
1
+ import { Command } from "commander";
2
+
3
+ import { requestAuthenticatedJson } from "../lib/api-client.js";
4
+ import { promptText } from "../lib/prompt.js";
5
+ import { dash, formatTable } from "../lib/table.js";
6
+
7
+ const KEY_STATUSES = ["all", "active", "revoked"];
8
+
9
+ export function createKeysCommand({
10
+ stdin = process.stdin,
11
+ stdout = process.stdout,
12
+ env = process.env
13
+ } = {}) {
14
+ const command = new Command("keys")
15
+ .description("Manage ZeusLock API keys");
16
+
17
+ command.addCommand(createKeysListCommand({ stdout, env }));
18
+ command.addCommand(createKeysCreateCommand({ stdout, env }));
19
+ command.addCommand(createKeysRevokeCommand({ stdin, stdout, env }));
20
+
21
+ return command;
22
+ }
23
+
24
+ function createKeysListCommand({ stdout, env }) {
25
+ return new Command("list")
26
+ .description("List API keys")
27
+ .option("--status <status>", "status filter: all, active, or revoked", "all")
28
+ .option("--show-secret", "include returned full key values when available")
29
+ .option("--json", "print machine-readable output")
30
+ .action(async (options) => {
31
+ const status = parseChoice(options.status, KEY_STATUSES, "--status");
32
+ const keys = normalizeApiKeys(await requestAuthenticatedJson("/api/keys", { env }));
33
+ const filtered = applyKeyFilters(keys, { status });
34
+ const output = {
35
+ total: keys.length,
36
+ matched: filtered.length,
37
+ filters: {
38
+ status
39
+ },
40
+ keys: filtered.map((key) => serializeKey(key, { showSecret: Boolean(options.showSecret) }))
41
+ };
42
+
43
+ if (options.json) {
44
+ stdout.write(`${JSON.stringify(output)}\n`);
45
+ return;
46
+ }
47
+
48
+ stdout.write(`API keys: ${output.total} total, ${output.matched} matched\n`);
49
+ if (!filtered.length) {
50
+ stdout.write("No API keys found.\n");
51
+ return;
52
+ }
53
+
54
+ stdout.write("\n");
55
+ stdout.write(`${formatKeysTable(filtered, { showSecret: Boolean(options.showSecret) })}\n`);
56
+ });
57
+ }
58
+
59
+ function createKeysCreateCommand({ stdout, env }) {
60
+ return new Command("create")
61
+ .description("Create an API key")
62
+ .requiredOption("--name <name>", "API key name")
63
+ .option("--json", "print machine-readable output")
64
+ .action(async (options) => {
65
+ const name = String(options.name || "").trim();
66
+ if (!name) {
67
+ throw new Error("--name is required.");
68
+ }
69
+
70
+ const result = await requestAuthenticatedJson("/api/keys", {
71
+ env,
72
+ method: "POST",
73
+ body: { name }
74
+ });
75
+ const created = normalizeApiKey(result);
76
+ const output = {
77
+ created: true,
78
+ key: serializeKey(created, { showSecret: true })
79
+ };
80
+
81
+ if (options.json) {
82
+ stdout.write(`${JSON.stringify(output)}\n`);
83
+ return;
84
+ }
85
+
86
+ stdout.write(`Created API key: ${created.name || name}\n`);
87
+ stdout.write(`Key ID: ${created.id || "unknown"}\n`);
88
+ stdout.write(`API key: ${created.api_key || "not returned"}\n`);
89
+ });
90
+ }
91
+
92
+ function createKeysRevokeCommand({
93
+ stdin = process.stdin,
94
+ stdout = process.stdout,
95
+ env = process.env
96
+ }) {
97
+ return new Command("revoke")
98
+ .description("Revoke one active API key")
99
+ .argument("<key_id>", "API key id from keys list")
100
+ .option("-y, --yes", "skip confirmation prompt")
101
+ .option("--json", "print machine-readable output")
102
+ .action(async (keyId, options) => {
103
+ const keys = normalizeApiKeys(await requestAuthenticatedJson("/api/keys", { env }));
104
+ const target = keys.find((key) => key.id === keyId);
105
+ if (!target) {
106
+ throw new Error(`API key not found: ${keyId}`);
107
+ }
108
+ if (!target.active) {
109
+ throw new Error("API key is already revoked. Refusing to call DELETE because the dashboard backend would permanently delete it.");
110
+ }
111
+
112
+ if (!options.yes) {
113
+ const answer = await promptText({
114
+ message: `Revoke API key ${target.name || keyId}? Type "yes" to confirm: `,
115
+ stdin,
116
+ stdout,
117
+ optionName: "--yes"
118
+ });
119
+ if (answer.toLowerCase() !== "yes") {
120
+ stdout.write("API key revocation cancelled.\n");
121
+ return;
122
+ }
123
+ }
124
+
125
+ const data = await requestAuthenticatedJson(`/api/keys/${encodeURIComponent(keyId)}`, {
126
+ env,
127
+ method: "DELETE"
128
+ });
129
+ const output = {
130
+ revoked: true,
131
+ key_id: keyId,
132
+ name: target.name || null,
133
+ message: data?.message || "API key revoked"
134
+ };
135
+
136
+ if (options.json) {
137
+ stdout.write(`${JSON.stringify(output)}\n`);
138
+ return;
139
+ }
140
+
141
+ stdout.write(`${output.message}: ${target.name || keyId}\n`);
142
+ });
143
+ }
144
+
145
+ function parseChoice(value, allowed, optionName) {
146
+ const normalized = String(value || "").trim().toLowerCase();
147
+ if (!allowed.includes(normalized)) {
148
+ throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
149
+ }
150
+ return normalized;
151
+ }
152
+
153
+ function normalizeApiKeys(data) {
154
+ const keys = Array.isArray(data) ? data : (data?.api_keys || data?.keys || []);
155
+ return keys.map(normalizeApiKey);
156
+ }
157
+
158
+ function normalizeApiKey(key = {}) {
159
+ const active = typeof key.active === "boolean"
160
+ ? key.active
161
+ : key.revoked !== true;
162
+ return {
163
+ id: key.id || "",
164
+ name: key.name || "",
165
+ api_key: key.api_key || key.key || "",
166
+ api_key_masked: key.api_key_masked || formatMaskedPrefix(key.prefix),
167
+ active,
168
+ status: active ? "active" : "revoked",
169
+ created_at: key.created_at || "",
170
+ last_used_at: key.last_used_at || key.last_used || ""
171
+ };
172
+ }
173
+
174
+ function applyKeyFilters(keys, { status }) {
175
+ if (status === "all") {
176
+ return keys;
177
+ }
178
+ return keys.filter((key) => key.status === status);
179
+ }
180
+
181
+ function serializeKey(key, { showSecret = false } = {}) {
182
+ const serialized = {
183
+ id: key.id,
184
+ name: key.name,
185
+ api_key_masked: key.api_key_masked,
186
+ status: key.status,
187
+ active: key.active,
188
+ created_at: key.created_at || null,
189
+ last_used_at: key.last_used_at || null
190
+ };
191
+
192
+ if (showSecret && key.api_key) {
193
+ serialized.api_key = key.api_key;
194
+ }
195
+
196
+ return serialized;
197
+ }
198
+
199
+ function formatKeysTable(keys, { showSecret = false } = {}) {
200
+ return formatTable(keys, [
201
+ { header: "KEY ID", value: (key) => dash(key.id) },
202
+ { header: "NAME", value: (key) => dash(key.name) },
203
+ { header: "KEY", value: (key) => showSecret && key.api_key ? key.api_key : dash(key.api_key_masked) },
204
+ { header: "STATUS", value: (key) => key.status },
205
+ { header: "CREATED", value: (key) => dash(key.created_at) }
206
+ ]);
207
+ }
208
+
209
+ function formatMaskedPrefix(prefix) {
210
+ return prefix ? `${prefix}***...****` : "";
211
+ }
@@ -0,0 +1,322 @@
1
+ import { Command } from "commander";
2
+
3
+ import { requestAuthenticatedJson } from "../lib/api-client.js";
4
+ import { dash, formatTable } from "../lib/table.js";
5
+ import { parseApiTimestamp } from "../lib/time.js";
6
+
7
+ const DAYS = [7, 30, 90];
8
+ const TRANSPORTS = ["all", "http", "sse", "stdio"];
9
+ const DIRECTIONS = ["all", "client_to_server", "server_to_client"];
10
+ const DECISIONS = ["all", "allow", "alert", "block"];
11
+ const EVENTS_LIMIT = 50;
12
+ const SERVERS_DAYS = 90;
13
+ const DISABLED_MESSAGE = "MCP monitoring is not enabled for this organization.";
14
+
15
+ export function createMcpCommand({
16
+ stdout = process.stdout,
17
+ env = process.env
18
+ } = {}) {
19
+ const command = new Command("mcp")
20
+ .description("Review MCP monitoring activity");
21
+
22
+ command.addCommand(createStatsCommand({ stdout, env }));
23
+ command.addCommand(createEventsCommand({ stdout, env }));
24
+ command.addCommand(createServersCommand({ stdout, env }));
25
+
26
+ return command;
27
+ }
28
+
29
+ function createStatsCommand({ stdout, env }) {
30
+ return new Command("stats")
31
+ .description("Show MCP monitoring statistics")
32
+ .option("--days <days>", "history window: 7, 30, or 90", "30")
33
+ .option("--json", "print machine-readable output")
34
+ .action(async (options) => {
35
+ const filters = { days: parseAllowedInt(options.days, DAYS, "--days") };
36
+ if (!await isMcpEnabled(env)) {
37
+ writeDisabled(stdout, options, filters);
38
+ return;
39
+ }
40
+
41
+ const stats = await requestAuthenticatedJson(`/api/v1/mcp/stats?days=${filters.days}`, { env });
42
+ const output = { enabled: true, filters, stats };
43
+
44
+ if (options.json) {
45
+ stdout.write(`${JSON.stringify(output)}\n`);
46
+ return;
47
+ }
48
+
49
+ stdout.write(formatMcpStats(stats, filters.days));
50
+ });
51
+ }
52
+
53
+ function createEventsCommand({ stdout, env }) {
54
+ return new Command("events")
55
+ .description("List MCP audit events")
56
+ .option("--days <days>", "history window: 7, 30, or 90", "30")
57
+ .option("--transport <transport>", "transport filter: all, http, sse, or stdio", "all")
58
+ .option("--direction <direction>", "direction filter: all, client_to_server, or server_to_client", "all")
59
+ .option("--decision <decision>", "decision filter: all, allow, alert, or block", "all")
60
+ .option("--method <method>", "MCP method filter, for example tools/call")
61
+ .option("--search <text>", "search server, tool, user, method, hostname, or path")
62
+ .option("--cursor <cursor>", "opaque next_cursor from a previous events response")
63
+ .option("--json", "print machine-readable output")
64
+ .action(async (options) => {
65
+ const filters = parseEventFilters(options);
66
+ if (!await isMcpEnabled(env)) {
67
+ writeDisabled(stdout, options, filters);
68
+ return;
69
+ }
70
+
71
+ const data = await requestAuthenticatedJson(`/api/v1/mcp/events?${buildEventsQuery(filters)}`, { env });
72
+ const events = normalizeEvents(data);
73
+ const output = {
74
+ enabled: true,
75
+ filters,
76
+ events,
77
+ next_cursor: data?.next_cursor || null
78
+ };
79
+
80
+ if (options.json) {
81
+ stdout.write(`${JSON.stringify(output)}\n`);
82
+ return;
83
+ }
84
+
85
+ stdout.write(`MCP events: ${events.length} shown (${filters.days} days)\n`);
86
+ if (output.next_cursor) {
87
+ stdout.write(`Next cursor: ${output.next_cursor}\n`);
88
+ }
89
+ if (!events.length) {
90
+ stdout.write("No MCP events found.\n");
91
+ return;
92
+ }
93
+
94
+ stdout.write("\n");
95
+ stdout.write(`${formatTable(events, [
96
+ { header: "TIME", value: (event) => dash(formatTimestamp(event.timestamp)) },
97
+ { header: "USER", value: (event) => dash(event.user) },
98
+ { header: "SERVER", value: (event) => dash(event.server_name || event.hostname) },
99
+ { header: "METHOD", value: (event) => dash(event.mcp_method || event.event_type) },
100
+ { header: "TOOL / RESOURCE", value: (event) => dash(event.tool_name || event.resource_uri) },
101
+ { header: "DIRECTION", value: (event) => dash(event.direction) },
102
+ { header: "DECISION", value: (event) => dash(event.decision) },
103
+ { header: "RISK", value: (event) => dash(event.risk_level) }
104
+ ])}\n`);
105
+ });
106
+ }
107
+
108
+ function createServersCommand({ stdout, env }) {
109
+ return new Command("servers")
110
+ .description("List discovered MCP servers")
111
+ .option("--json", "print machine-readable output")
112
+ .action(async (options) => {
113
+ const filters = { days: SERVERS_DAYS };
114
+ if (!await isMcpEnabled(env)) {
115
+ writeDisabled(stdout, options, filters);
116
+ return;
117
+ }
118
+
119
+ const data = await requestAuthenticatedJson(`/api/v1/mcp/servers?days=${SERVERS_DAYS}`, { env });
120
+ const servers = normalizeServers(data);
121
+ const output = {
122
+ enabled: true,
123
+ total: typeof data?.total === "number" ? data.total : servers.length,
124
+ filters,
125
+ servers
126
+ };
127
+
128
+ if (options.json) {
129
+ stdout.write(`${JSON.stringify(output)}\n`);
130
+ return;
131
+ }
132
+
133
+ stdout.write(`MCP servers: ${output.total} discovered (${SERVERS_DAYS} days)\n`);
134
+ if (!servers.length) {
135
+ stdout.write("No MCP servers found.\n");
136
+ return;
137
+ }
138
+
139
+ stdout.write("\n");
140
+ stdout.write(`${formatTable(servers, [
141
+ { header: "SERVER", value: (server) => dash(server.server_name) },
142
+ { header: "CLIENT", value: (server) => dash(server.client_name) },
143
+ { header: "TRANSPORT", value: (server) => dash(server.transport) },
144
+ { header: "STATUS", value: (server) => dash(server.status) },
145
+ { header: "ACTIVITY", value: (server) => numberOrZero(server.activity_count) },
146
+ { header: "LAST DECISION", value: (server) => dash(server.last_decision) },
147
+ { header: "FIRST SEEN", value: (server) => dash(formatTimestamp(server.first_seen)) },
148
+ { header: "LAST SEEN", value: (server) => dash(formatTimestamp(server.last_seen)) }
149
+ ])}\n`);
150
+ });
151
+ }
152
+
153
+ async function isMcpEnabled(env) {
154
+ const org = await requestAuthenticatedJson("/api/org", { env });
155
+ return org?.settings?.mcp_enabled !== false;
156
+ }
157
+
158
+ function writeDisabled(stdout, options, filters) {
159
+ if (options.json) {
160
+ stdout.write(`${JSON.stringify({
161
+ enabled: false,
162
+ message: DISABLED_MESSAGE,
163
+ filters
164
+ })}\n`);
165
+ return;
166
+ }
167
+ stdout.write(`${DISABLED_MESSAGE}\n`);
168
+ }
169
+
170
+ function parseEventFilters(options) {
171
+ return {
172
+ days: parseAllowedInt(options.days, DAYS, "--days"),
173
+ limit: EVENTS_LIMIT,
174
+ transport: parseOptionalChoice(options.transport, TRANSPORTS, "--transport") || "all",
175
+ direction: parseOptionalChoice(options.direction, DIRECTIONS, "--direction") || "all",
176
+ decision: parseOptionalChoice(options.decision, DECISIONS, "--decision") || "all",
177
+ mcp_method: parseMethod(options.method),
178
+ search: normalizeOptionalText(options.search),
179
+ cursor: normalizeOptionalText(options.cursor)
180
+ };
181
+ }
182
+
183
+ function buildEventsQuery(filters) {
184
+ const params = new URLSearchParams();
185
+ params.set("days", String(filters.days));
186
+ params.set("limit", String(filters.limit));
187
+ if (filters.transport !== "all") {
188
+ params.set("transport", filters.transport);
189
+ }
190
+ if (filters.direction !== "all") {
191
+ params.set("direction", filters.direction);
192
+ }
193
+ if (filters.decision !== "all") {
194
+ params.set("decision", filters.decision);
195
+ }
196
+ if (filters.mcp_method) {
197
+ params.set("mcp_method", filters.mcp_method);
198
+ }
199
+ if (filters.search) {
200
+ params.set("search", filters.search);
201
+ }
202
+ if (filters.cursor) {
203
+ params.set("cursor", filters.cursor);
204
+ }
205
+ return params.toString();
206
+ }
207
+
208
+ function parseAllowedInt(value, allowed, optionName) {
209
+ const number = Number(value);
210
+ if (!Number.isInteger(number) || !allowed.includes(number)) {
211
+ throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
212
+ }
213
+ return number;
214
+ }
215
+
216
+ function parseOptionalChoice(value, allowed, optionName) {
217
+ if (value === undefined || value === null || value === "") {
218
+ return null;
219
+ }
220
+ const normalized = String(value).trim().toLowerCase();
221
+ if (!allowed.includes(normalized)) {
222
+ throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
223
+ }
224
+ return normalized;
225
+ }
226
+
227
+ function parseMethod(value) {
228
+ const method = normalizeOptionalText(value);
229
+ if (!method || method.toLowerCase() === "all") {
230
+ return null;
231
+ }
232
+ return method;
233
+ }
234
+
235
+ function normalizeOptionalText(value) {
236
+ const text = String(value || "").trim();
237
+ return text || null;
238
+ }
239
+
240
+ function normalizeEvents(data) {
241
+ const events = Array.isArray(data) ? data : (data?.events || []);
242
+ return events.map((event) => ({
243
+ id: event.id || "",
244
+ timestamp: event.timestamp || "",
245
+ user: event.user || "",
246
+ event_type: event.event_type || "",
247
+ transport: event.transport || "",
248
+ direction: event.direction || "",
249
+ mcp_method: event.mcp_method || "",
250
+ tool_name: event.tool_name || "",
251
+ resource_uri: event.resource_uri || "",
252
+ server_name: event.server_name || "",
253
+ client_name: event.client_name || "",
254
+ decision: event.decision || "",
255
+ risk_level: event.risk_level || "",
256
+ incident_id: event.incident_id || null,
257
+ content_hash: event.content_hash || "",
258
+ byte_count: numberOrZero(event.byte_count),
259
+ hostname: event.hostname || "",
260
+ path: event.path || "",
261
+ url: event.url || "",
262
+ command: event.command || "",
263
+ source_path: event.source_path || "",
264
+ agent_id: event.agent_id || "",
265
+ platform: event.platform || ""
266
+ }));
267
+ }
268
+
269
+ function normalizeServers(data) {
270
+ const servers = Array.isArray(data) ? data : (data?.servers || []);
271
+ return servers.map((server) => ({
272
+ server_name: server.server_name || "",
273
+ client_name: server.client_name || "",
274
+ transport: server.transport || "",
275
+ endpoint: server.endpoint || "",
276
+ source_path: server.source_path || "",
277
+ agent_id: server.agent_id || "",
278
+ first_seen: server.first_seen || "",
279
+ last_seen: server.last_seen || "",
280
+ activity_count: numberOrZero(server.activity_count),
281
+ blocked_count: numberOrZero(server.blocked_count),
282
+ alerted_count: numberOrZero(server.alerted_count),
283
+ last_activity: server.last_activity || null,
284
+ last_decision: server.last_decision || "",
285
+ status: server.status || ""
286
+ }));
287
+ }
288
+
289
+ function formatMcpStats(stats, days) {
290
+ return [
291
+ `MCP stats (${days} days)`,
292
+ `Events: ${numberOrZero(stats?.total_events)}`,
293
+ `Tool calls: ${numberOrZero(stats?.tool_calls)}`,
294
+ `Blocked: ${numberOrZero(stats?.blocked)}`,
295
+ `Alerted: ${numberOrZero(stats?.alerted)}`,
296
+ `Active servers: ${numberOrZero(stats?.active_servers)}`,
297
+ `Affected users: ${numberOrZero(stats?.affected_users)}`,
298
+ formatCountMap("By transport", stats?.by_transport),
299
+ formatCountMap("By direction", stats?.by_direction),
300
+ formatCountMap("By method", stats?.by_method),
301
+ ""
302
+ ].filter((line) => line !== null).join("\n");
303
+ }
304
+
305
+ function formatCountMap(label, value) {
306
+ if (!value || typeof value !== "object" || !Object.keys(value).length) {
307
+ return null;
308
+ }
309
+ return `${label}: ${Object.entries(value).map(([key, count]) => `${key}: ${count}`).join(", ")}`;
310
+ }
311
+
312
+ function formatTimestamp(value) {
313
+ const date = parseApiTimestamp(value);
314
+ if (!date) {
315
+ return value || "";
316
+ }
317
+ return date.toISOString();
318
+ }
319
+
320
+ function numberOrZero(value) {
321
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
322
+ }