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.
package/src/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ import { createRequire } from "node:module";
2
+ import { Command } from "commander";
3
+
4
+ import { createAgentsCommand } from "./commands/agents.js";
5
+ import { createAnonymizeCommand } from "./commands/anonymize.js";
6
+ import { createAuthCommand } from "./commands/auth.js";
7
+ import { createDeployCommand } from "./commands/deploy.js";
8
+ import { createExtensionsCommand } from "./commands/extensions.js";
9
+ import { createHookCommand } from "./commands/hook.js";
10
+ import { createIncidentsCommand } from "./commands/incidents.js";
11
+ import { createKeysCommand } from "./commands/keys.js";
12
+ import { createMcpCommand } from "./commands/mcp.js";
13
+ import { createRulesCommand } from "./commands/rules.js";
14
+ import { createScanCommand } from "./commands/scan.js";
15
+ import { createShadowAiCommand } from "./commands/shadow-ai.js";
16
+ import { createSiemCommand } from "./commands/siem.js";
17
+ import { createStatusCommand } from "./commands/status.js";
18
+ import { createTokensCommand } from "./commands/tokens.js";
19
+ import { createUsersCommand } from "./commands/users.js";
20
+ import { createWhoamiCommand } from "./commands/whoami.js";
21
+ import { assertCliAccessToken } from "./lib/cli-token.js";
22
+ import { setCommandToken } from "./lib/command-token.js";
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const packageJson = require("../package.json");
26
+
27
+ export function buildProgram({
28
+ stdin = process.stdin,
29
+ stdout = process.stdout,
30
+ stderr = process.stderr,
31
+ env = process.env
32
+ } = {}) {
33
+ const program = new Command();
34
+
35
+ program
36
+ .name("zeuslock")
37
+ .description("ZeusLock command line")
38
+ .version(packageJson.version)
39
+ .option("--token <zlu_token>", "personal CLI access token for one command invocation")
40
+ .configureOutput({
41
+ writeOut: (text) => stdout.write(text),
42
+ writeErr: (text) => stderr.write(text),
43
+ outputError: (text, write) => write(text)
44
+ });
45
+
46
+ program.hook("preAction", (thisCommand) => {
47
+ const token = String(thisCommand.opts().token || "").trim();
48
+ if (token) {
49
+ assertCliAccessToken(token, "--token");
50
+ }
51
+ setCommandToken(env, token);
52
+ });
53
+
54
+ program.addCommand(createAuthCommand({ stdin, stdout, stderr, env }));
55
+ program.addCommand(createScanCommand({ stdin, stdout, env }));
56
+ program.addCommand(createAnonymizeCommand({ stdout, env }));
57
+ program.addCommand(createHookCommand({ stdout, env }));
58
+ program.addCommand(createDeployCommand({ stdout, env }));
59
+ program.addCommand(createAgentsCommand({ stdin, stdout, env }));
60
+ program.addCommand(createExtensionsCommand({ stdout, env }));
61
+ program.addCommand(createIncidentsCommand({ stdout, env }));
62
+ program.addCommand(createUsersCommand({ stdin, stdout, env }));
63
+ program.addCommand(createKeysCommand({ stdin, stdout, env }));
64
+ program.addCommand(createTokensCommand({ stdin, stdout, env }));
65
+ program.addCommand(createRulesCommand({ stdout, env }));
66
+ program.addCommand(createSiemCommand({ stdout, env }));
67
+ program.addCommand(createShadowAiCommand({ stdout, env }));
68
+ program.addCommand(createMcpCommand({ stdout, env }));
69
+ program.addCommand(createWhoamiCommand({ stdout, stderr, env }));
70
+ program.addCommand(createStatusCommand({ stdout, env }));
71
+
72
+ return program;
73
+ }
74
+
75
+ export async function run(argv = process.argv) {
76
+ const program = buildProgram();
77
+ await program.parseAsync(argv);
78
+ }
@@ -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
+ import { parseApiTimestamp } from "../lib/time.js";
7
+ import { compareVersions } from "../lib/version.js";
8
+
9
+ const AGENT_ONLINE_WINDOW_MS = 5 * 60 * 1000;
10
+ const STATUS_FILTERS = ["all", "online", "offline"];
11
+
12
+ export function createAgentsCommand({
13
+ stdin = process.stdin,
14
+ stdout = process.stdout,
15
+ env = process.env
16
+ } = {}) {
17
+ const command = new Command("agents")
18
+ .description("Review and manage desktop agents");
19
+
20
+ command.addCommand(createAgentsListCommand({ stdout, env }));
21
+ command.addCommand(createAgentsRevokeCommand({ stdin, stdout, env }));
22
+
23
+ return command;
24
+ }
25
+
26
+ function createAgentsListCommand({ stdout = process.stdout, env = process.env } = {}) {
27
+ return new Command("list")
28
+ .description("List desktop agents")
29
+ .option("--status <status>", "status filter: all, online, or offline", "all")
30
+ .option("--search <text>", "search hostname or platform")
31
+ .option("--json", "print machine-readable output")
32
+ .action(async (options) => {
33
+ const status = parseStatus(options.status);
34
+ const agentsData = await requestAuthenticatedJson("/api/agents", { env });
35
+ const latestVersion = await fetchLatestAgentVersion({ env });
36
+ const agents = normalizeAgents(
37
+ Array.isArray(agentsData) ? agentsData : (agentsData.agents || []),
38
+ latestVersion
39
+ );
40
+
41
+ const filtered = agents.filter((agent) =>
42
+ matchesAgentFilters(agent, {
43
+ status,
44
+ search: options.search
45
+ })
46
+ );
47
+
48
+ const output = {
49
+ total: typeof agentsData?.total === "number" ? agentsData.total : agents.length,
50
+ online_count: typeof agentsData?.online_count === "number"
51
+ ? agentsData.online_count
52
+ : agents.filter((agent) => agent.is_online).length,
53
+ offline_count: typeof agentsData?.offline_count === "number"
54
+ ? agentsData.offline_count
55
+ : agents.filter((agent) => !agent.is_online).length,
56
+ latest_version: latestVersion || null,
57
+ filters: {
58
+ status,
59
+ search: options.search || null
60
+ },
61
+ agents: filtered
62
+ };
63
+
64
+ if (options.json) {
65
+ stdout.write(`${JSON.stringify(output)}\n`);
66
+ return;
67
+ }
68
+
69
+ stdout.write(`Agents: ${output.total} total, ${output.online_count} online, ${output.offline_count} offline\n`);
70
+ if (latestVersion) {
71
+ stdout.write(`Latest published version: ${latestVersion}\n`);
72
+ }
73
+ if (!filtered.length) {
74
+ stdout.write("No agents found.\n");
75
+ return;
76
+ }
77
+
78
+ stdout.write("\n");
79
+ stdout.write(`${formatTable(filtered, [
80
+ { header: "AGENT ID", value: (agent) => dash(agent.agent_id) },
81
+ { header: "HOSTNAME", value: (agent) => dash(agent.hostname) },
82
+ { header: "PLATFORM", value: (agent) => dash(agent.platform) },
83
+ { header: "VERSION", value: (agent) => dash(agent.version) },
84
+ { header: "UPDATE", value: (agent) => agent.update_available ? "yes" : "-" },
85
+ { header: "STATUS", value: (agent) => agent.status },
86
+ { header: "USER", value: (agent) => dash(agent.username) },
87
+ { header: "LAST HEARTBEAT", value: (agent) => dash(agent.last_seen) }
88
+ ])}\n`);
89
+ });
90
+ }
91
+
92
+ function createAgentsRevokeCommand({
93
+ stdin = process.stdin,
94
+ stdout = process.stdout,
95
+ env = process.env
96
+ } = {}) {
97
+ return new Command("revoke")
98
+ .description("Revoke a desktop agent")
99
+ .argument("<agent_id>", "agent ID from agents list")
100
+ .option("-y, --yes", "skip confirmation prompt")
101
+ .option("--json", "print machine-readable output")
102
+ .action(async (agentId, options) => {
103
+ if (!options.yes) {
104
+ const answer = await promptText({
105
+ message: `Revoke agent ${agentId}? Type "yes" to confirm: `,
106
+ stdin,
107
+ stdout,
108
+ optionName: "--yes"
109
+ });
110
+ if (answer.toLowerCase() !== "yes") {
111
+ stdout.write("Revocation cancelled.\n");
112
+ return;
113
+ }
114
+ }
115
+
116
+ const data = await requestAuthenticatedJson(`/api/agents/${encodeURIComponent(agentId)}`, {
117
+ env,
118
+ method: "DELETE"
119
+ });
120
+ const output = {
121
+ agent_id: agentId,
122
+ ...data
123
+ };
124
+
125
+ if (options.json) {
126
+ stdout.write(`${JSON.stringify(output)}\n`);
127
+ return;
128
+ }
129
+
130
+ stdout.write(`${output.message || "Agent revoked"}: ${agentId}\n`);
131
+ });
132
+ }
133
+
134
+ function parseStatus(value) {
135
+ const status = String(value || "all").trim().toLowerCase();
136
+ if (!STATUS_FILTERS.includes(status)) {
137
+ throw new Error(`Invalid --status. Allowed values: ${STATUS_FILTERS.join(", ")}.`);
138
+ }
139
+ return status;
140
+ }
141
+
142
+ async function fetchLatestAgentVersion({ env }) {
143
+ try {
144
+ const downloadsData = await requestAuthenticatedJson("/api/v1/agents/downloads", { env });
145
+ return downloadsData?.version || "";
146
+ } catch {
147
+ return "";
148
+ }
149
+ }
150
+
151
+ function normalizeAgents(agents, latestVersion = "") {
152
+ return agents.map((agent) => {
153
+ const online = computeOnline(agent);
154
+ const version = agent.version || "";
155
+ return {
156
+ agent_id: agent.agent_id || agent.id || "",
157
+ hostname: agent.hostname || "",
158
+ platform: agent.platform || "",
159
+ version,
160
+ username: agent.username || agent.user || agent.user_email || "",
161
+ last_seen: agent.last_seen || "",
162
+ first_seen: agent.first_seen || "",
163
+ is_online: online,
164
+ status: online ? "online" : "offline",
165
+ update_available: isOutdated(version, latestVersion),
166
+ alerts_today: numberOrZero(agent.alerts_today),
167
+ blocks_today: numberOrZero(agent.blocks_today),
168
+ anonymizations_today: numberOrZero(agent.anonymizations_today)
169
+ };
170
+ });
171
+ }
172
+
173
+ function matchesAgentFilters(agent, { status, search = "" }) {
174
+ if (status === "online" && !agent.is_online) {
175
+ return false;
176
+ }
177
+ if (status === "offline" && agent.is_online) {
178
+ return false;
179
+ }
180
+
181
+ const query = String(search || "").trim().toLowerCase();
182
+ if (query) {
183
+ return (
184
+ agent.hostname.toLowerCase().includes(query) ||
185
+ agent.platform.toLowerCase().includes(query)
186
+ );
187
+ }
188
+
189
+ return true;
190
+ }
191
+
192
+ function computeOnline(agent, now = new Date()) {
193
+ if (typeof agent?.is_online === "boolean") {
194
+ return agent.is_online;
195
+ }
196
+
197
+ const lastSeen = parseApiTimestamp(agent?.last_seen);
198
+ if (!lastSeen) {
199
+ return false;
200
+ }
201
+
202
+ return now.getTime() - lastSeen.getTime() < AGENT_ONLINE_WINDOW_MS;
203
+ }
204
+
205
+ function isOutdated(version, latestVersion) {
206
+ return Boolean(version && latestVersion && compareVersions(version, latestVersion) < 0);
207
+ }
208
+
209
+ function numberOrZero(value) {
210
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
211
+ }
@@ -0,0 +1,83 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Command } from "commander";
4
+
5
+ import { requestApiKeyJson } from "../lib/api-client.js";
6
+
7
+ export function createAnonymizeCommand({
8
+ stdout = process.stdout,
9
+ env = process.env
10
+ } = {}) {
11
+ return new Command("anonymize")
12
+ .description("Anonymize a text file before sending it to an LLM")
13
+ .argument("<file>", "text file to anonymize")
14
+ .option("--api-key <key>", "organization API key; defaults to saved key or ZEUSLOCK_API_KEY")
15
+ .option("--output <path>", "write anonymized text to a file instead of stdout")
16
+ .option("--include-sensitive-map", "include original-to-masked substitutions in JSON output")
17
+ .option("--json", "print machine-readable output")
18
+ .action(async (file, options) => {
19
+ const filePath = path.resolve(file);
20
+ const text = await readFile(filePath, "utf8");
21
+ const result = await requestApiKeyJson("/api/v1/anonymize", {
22
+ env,
23
+ apiKey: options.apiKey,
24
+ method: "POST",
25
+ body: { text }
26
+ });
27
+ const anonymizedText = String(result?.anonymized_text ?? result?.masked ?? "");
28
+ const anonymizationCount = Array.isArray(result?.anonymizations)
29
+ ? result.anonymizations.length
30
+ : 0;
31
+
32
+ if (options.output) {
33
+ await writeFile(path.resolve(options.output), anonymizedText, "utf8");
34
+ }
35
+
36
+ if (options.json) {
37
+ stdout.write(`${JSON.stringify(buildJsonOutput(result, {
38
+ file: filePath,
39
+ output: options.output ? path.resolve(options.output) : null,
40
+ includeSensitiveMap: Boolean(options.includeSensitiveMap),
41
+ anonymizedText,
42
+ anonymizationCount
43
+ }))}\n`);
44
+ return;
45
+ }
46
+
47
+ if (options.output) {
48
+ stdout.write(`Anonymized ${anonymizationCount} value(s) to ${path.resolve(options.output)}\n`);
49
+ return;
50
+ }
51
+
52
+ stdout.write(anonymizedText);
53
+ if (!anonymizedText.endsWith("\n")) {
54
+ stdout.write("\n");
55
+ }
56
+ });
57
+ }
58
+
59
+ function buildJsonOutput(result, {
60
+ file,
61
+ output,
62
+ includeSensitiveMap,
63
+ anonymizedText,
64
+ anonymizationCount
65
+ }) {
66
+ const base = {
67
+ file,
68
+ output,
69
+ anonymized_text: anonymizedText,
70
+ anonymization_count: anonymizationCount
71
+ };
72
+
73
+ if (includeSensitiveMap) {
74
+ return {
75
+ ...base,
76
+ original: result?.original ?? "",
77
+ masked: result?.masked ?? anonymizedText,
78
+ anonymizations: Array.isArray(result?.anonymizations) ? result.anonymizations : []
79
+ };
80
+ }
81
+
82
+ return base;
83
+ }