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/README.md +570 -0
- package/bin/zeuslock.js +9 -0
- package/package.json +33 -0
- package/src/cli.js +78 -0
- package/src/commands/agents.js +211 -0
- package/src/commands/anonymize.js +83 -0
- package/src/commands/auth.js +337 -0
- package/src/commands/deploy.js +515 -0
- package/src/commands/extensions.js +73 -0
- package/src/commands/hook.js +221 -0
- package/src/commands/incidents.js +436 -0
- package/src/commands/keys.js +211 -0
- package/src/commands/mcp.js +322 -0
- package/src/commands/rules.js +432 -0
- package/src/commands/scan.js +178 -0
- package/src/commands/shadow-ai.js +255 -0
- package/src/commands/siem.js +241 -0
- package/src/commands/status.js +170 -0
- package/src/commands/tokens.js +293 -0
- package/src/commands/users.js +255 -0
- package/src/commands/whoami.js +43 -0
- package/src/lib/api-client.js +308 -0
- package/src/lib/api-key-store.js +84 -0
- package/src/lib/auth-store.js +123 -0
- package/src/lib/cli-token.js +22 -0
- package/src/lib/command-token.js +15 -0
- package/src/lib/config.js +27 -0
- package/src/lib/dlp-scan.js +146 -0
- package/src/lib/package-info.js +11 -0
- package/src/lib/prompt.js +55 -0
- package/src/lib/siem-cursors.js +64 -0
- package/src/lib/table.js +30 -0
- package/src/lib/time.js +33 -0
- package/src/lib/version.js +24 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
requestAuthenticatedJson,
|
|
7
|
+
resolveApiUrlForRequest
|
|
8
|
+
} from "../lib/api-client.js";
|
|
9
|
+
import { dash, formatTable } from "../lib/table.js";
|
|
10
|
+
import { parseApiTimestamp } from "../lib/time.js";
|
|
11
|
+
|
|
12
|
+
const ARTIFACT_SETS = ["all", "extension", "agent"];
|
|
13
|
+
const BROWSERS = ["all", "chrome", "edge"];
|
|
14
|
+
const VERIFY_PLATFORMS = ["windows", "macos", "linux"];
|
|
15
|
+
const VERIFY_STATUSES = ["online", "offline", "any"];
|
|
16
|
+
const BUSINESS_PLANS = new Set(["business", "enterprise"]);
|
|
17
|
+
const EXTENSION_ID = "hgooghpcnalhpjbemnnmdoabfjhchoip";
|
|
18
|
+
const EXTENSION_UPDATE_URL = "https://clients2.google.com/service/update2/crx";
|
|
19
|
+
const AGENT_ONLINE_WINDOW_MS = 5 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
export function createDeployCommand({
|
|
22
|
+
stdout = process.stdout,
|
|
23
|
+
env = process.env
|
|
24
|
+
} = {}) {
|
|
25
|
+
const command = new Command("deploy")
|
|
26
|
+
.description("Generate and verify ZeusLock deployment artifacts");
|
|
27
|
+
|
|
28
|
+
command.addCommand(createGpoConfigCommand({ stdout, env }));
|
|
29
|
+
command.addCommand(createAgentVerifyCommand({ stdout, env }));
|
|
30
|
+
|
|
31
|
+
return command;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createGpoConfigCommand({ stdout, env }) {
|
|
35
|
+
return new Command("gpo-config")
|
|
36
|
+
.description("Generate dashboard-supported GPO and agent deployment artifacts")
|
|
37
|
+
.option("--artifact <set>", "artifact set: all, extension, or agent", "all")
|
|
38
|
+
.option("--browser <browser>", "extension browser policy: chrome, edge, or all", "all")
|
|
39
|
+
.option("--output <dir>", "output directory", "zeuslock-deploy")
|
|
40
|
+
.option("--api-key <key>", "full organization API key to embed")
|
|
41
|
+
.option("--key-id <id>", "existing API key id from keys list")
|
|
42
|
+
.option("--create-key-name <name>", "create and use a new API key with this name")
|
|
43
|
+
.option("--json", "print machine-readable output")
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
const artifactSet = parseChoice(options.artifact, ARTIFACT_SETS, "--artifact");
|
|
46
|
+
const browser = parseChoice(options.browser, BROWSERS, "--browser");
|
|
47
|
+
const requested = {
|
|
48
|
+
extension: includesArtifact(artifactSet, "extension"),
|
|
49
|
+
agent: includesArtifact(artifactSet, "agent")
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const org = await requestAuthenticatedJson("/api/org", { env });
|
|
53
|
+
if (requested.extension && !BUSINESS_PLANS.has(String(org?.plan || "free").toLowerCase())) {
|
|
54
|
+
throw new Error("Extension GPO deployment is only available on Business and Enterprise plans.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const apiUrl = await resolveApiUrlForRequest(env);
|
|
58
|
+
validateApiUrl(apiUrl);
|
|
59
|
+
const selectedKey = await resolveDeploymentApiKey(options, { env });
|
|
60
|
+
const outputDir = path.resolve(options.output || "zeuslock-deploy");
|
|
61
|
+
const artifacts = [];
|
|
62
|
+
|
|
63
|
+
if (requested.extension) {
|
|
64
|
+
artifacts.push(...await writeExtensionArtifacts({
|
|
65
|
+
outputDir,
|
|
66
|
+
browser,
|
|
67
|
+
apiUrl,
|
|
68
|
+
apiKey: selectedKey.api_key
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (requested.agent) {
|
|
73
|
+
artifacts.push(...await writeAgentArtifacts({
|
|
74
|
+
outputDir,
|
|
75
|
+
apiUrl,
|
|
76
|
+
apiKey: selectedKey.api_key
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const summary = {
|
|
81
|
+
schema_version: "zeuslock.deploy.v1",
|
|
82
|
+
generated_at: new Date().toISOString(),
|
|
83
|
+
api_url: apiUrl,
|
|
84
|
+
org: {
|
|
85
|
+
org_id: org?.org_id || org?.id || null,
|
|
86
|
+
name: org?.name || null,
|
|
87
|
+
plan: org?.plan || null
|
|
88
|
+
},
|
|
89
|
+
api_key: {
|
|
90
|
+
id: selectedKey.id || null,
|
|
91
|
+
name: selectedKey.name || null,
|
|
92
|
+
api_key_masked: selectedKey.api_key_masked || maskSecret(selectedKey.api_key),
|
|
93
|
+
source: selectedKey.source
|
|
94
|
+
},
|
|
95
|
+
requested: {
|
|
96
|
+
artifact: artifactSet,
|
|
97
|
+
browser
|
|
98
|
+
},
|
|
99
|
+
skipped: skippedArtifacts({ requested, browser }),
|
|
100
|
+
artifacts
|
|
101
|
+
};
|
|
102
|
+
const summaryArtifact = await writeArtifact(
|
|
103
|
+
outputDir,
|
|
104
|
+
"deployment-summary.json",
|
|
105
|
+
`${JSON.stringify(summary, null, 2)}\n`,
|
|
106
|
+
"summary"
|
|
107
|
+
);
|
|
108
|
+
const output = {
|
|
109
|
+
...summary,
|
|
110
|
+
output_dir: outputDir,
|
|
111
|
+
artifacts: [...artifacts, summaryArtifact]
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (options.json) {
|
|
115
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
stdout.write(`Generated deployment artifacts in ${outputDir}\n`);
|
|
120
|
+
stdout.write(`API URL: ${apiUrl}\n`);
|
|
121
|
+
stdout.write(`API key: ${output.api_key.api_key_masked || "masked"}\n`);
|
|
122
|
+
stdout.write("\n");
|
|
123
|
+
stdout.write(`${formatTable(output.artifacts, [
|
|
124
|
+
{ header: "TYPE", value: (artifact) => artifact.type },
|
|
125
|
+
{ header: "PATH", value: (artifact) => artifact.path }
|
|
126
|
+
])}\n`);
|
|
127
|
+
|
|
128
|
+
if (output.skipped.length) {
|
|
129
|
+
stdout.write("\nSkipped:\n");
|
|
130
|
+
for (const skipped of output.skipped) {
|
|
131
|
+
stdout.write(`- ${skipped.feature}: ${skipped.reason}\n`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createAgentVerifyCommand({ stdout, env }) {
|
|
138
|
+
return new Command("agent-verify")
|
|
139
|
+
.description("Verify agent enrollment and heartbeat state from the dashboard backend")
|
|
140
|
+
.option("--agent-id <id>", "exact agent id")
|
|
141
|
+
.option("--hostname <name>", "exact hostname, case-insensitive")
|
|
142
|
+
.option("--platform <platform>", "platform filter: windows, macos, or linux")
|
|
143
|
+
.option("--status <status>", "expected status: online, offline, or any", "online")
|
|
144
|
+
.option("--json", "print machine-readable output")
|
|
145
|
+
.action(async (options) => {
|
|
146
|
+
const platform = options.platform
|
|
147
|
+
? parseChoice(options.platform, VERIFY_PLATFORMS, "--platform")
|
|
148
|
+
: null;
|
|
149
|
+
const expectedStatus = parseChoice(options.status, VERIFY_STATUSES, "--status");
|
|
150
|
+
const data = await requestAuthenticatedJson("/api/agents", { env });
|
|
151
|
+
const agents = normalizeAgents(Array.isArray(data) ? data : (data?.agents || []));
|
|
152
|
+
const filters = {
|
|
153
|
+
agent_id: options.agentId || null,
|
|
154
|
+
hostname: options.hostname || null,
|
|
155
|
+
platform,
|
|
156
|
+
status: expectedStatus
|
|
157
|
+
};
|
|
158
|
+
const matchingIdentity = agents.filter((agent) => matchesAgentIdentity(agent, filters));
|
|
159
|
+
const matchingStatus = expectedStatus === "any"
|
|
160
|
+
? matchingIdentity
|
|
161
|
+
: matchingIdentity.filter((agent) => agent.status === expectedStatus);
|
|
162
|
+
const outcome = buildVerifyOutcome({
|
|
163
|
+
total: typeof data?.total === "number" ? data.total : agents.length,
|
|
164
|
+
onlineCount: typeof data?.online_count === "number"
|
|
165
|
+
? data.online_count
|
|
166
|
+
: agents.filter((agent) => agent.is_online).length,
|
|
167
|
+
offlineCount: typeof data?.offline_count === "number"
|
|
168
|
+
? data.offline_count
|
|
169
|
+
: agents.filter((agent) => !agent.is_online).length,
|
|
170
|
+
filters,
|
|
171
|
+
matchingIdentity,
|
|
172
|
+
matchingStatus,
|
|
173
|
+
expectedStatus
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (options.json) {
|
|
177
|
+
stdout.write(`${JSON.stringify(outcome)}\n`);
|
|
178
|
+
if (!outcome.verified) {
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
stdout.write(`Agent verification: ${outcome.status}\n`);
|
|
185
|
+
stdout.write(`Matched agents: ${outcome.matched}\n`);
|
|
186
|
+
stdout.write(`Fleet: ${outcome.total} total, ${outcome.online_count} online, ${outcome.offline_count} offline\n`);
|
|
187
|
+
|
|
188
|
+
if (!outcome.agents.length) {
|
|
189
|
+
stdout.write("No matching agents found.\n");
|
|
190
|
+
if (!outcome.verified) {
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
stdout.write("\n");
|
|
197
|
+
stdout.write(`${formatTable(outcome.agents, [
|
|
198
|
+
{ header: "AGENT ID", value: (agent) => dash(agent.agent_id) },
|
|
199
|
+
{ header: "HOSTNAME", value: (agent) => dash(agent.hostname) },
|
|
200
|
+
{ header: "PLATFORM", value: (agent) => dash(agent.platform) },
|
|
201
|
+
{ header: "VERSION", value: (agent) => dash(agent.version) },
|
|
202
|
+
{ header: "STATUS", value: (agent) => agent.status },
|
|
203
|
+
{ header: "LAST HEARTBEAT", value: (agent) => dash(agent.last_seen) }
|
|
204
|
+
])}\n`);
|
|
205
|
+
if (!outcome.verified) {
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseChoice(value, allowed, optionName) {
|
|
212
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
213
|
+
if (!allowed.includes(normalized)) {
|
|
214
|
+
throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
|
|
215
|
+
}
|
|
216
|
+
return normalized;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function includesArtifact(artifactSet, artifact) {
|
|
220
|
+
return artifactSet === "all" || artifactSet === artifact;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateApiUrl(apiUrl) {
|
|
224
|
+
try {
|
|
225
|
+
new URL(apiUrl);
|
|
226
|
+
} catch {
|
|
227
|
+
throw new Error(`Invalid API URL: ${apiUrl}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function resolveDeploymentApiKey(options, { env }) {
|
|
232
|
+
const provided = String(options.apiKey || "").trim();
|
|
233
|
+
if (provided) {
|
|
234
|
+
return {
|
|
235
|
+
id: "",
|
|
236
|
+
name: "",
|
|
237
|
+
api_key: provided,
|
|
238
|
+
api_key_masked: maskSecret(provided),
|
|
239
|
+
active: true,
|
|
240
|
+
source: "provided"
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const createKeyName = String(options.createKeyName || "").trim();
|
|
245
|
+
if (createKeyName) {
|
|
246
|
+
const created = normalizeApiKey(await requestAuthenticatedJson("/api/keys", {
|
|
247
|
+
env,
|
|
248
|
+
method: "POST",
|
|
249
|
+
body: { name: createKeyName }
|
|
250
|
+
}));
|
|
251
|
+
if (!created.api_key) {
|
|
252
|
+
throw new Error("Created API key response did not include the full key.");
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
...created,
|
|
256
|
+
source: "created"
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const keys = normalizeApiKeys(await requestAuthenticatedJson("/api/keys", { env }));
|
|
261
|
+
const keyId = String(options.keyId || "").trim();
|
|
262
|
+
if (keyId) {
|
|
263
|
+
const selected = keys.find((key) => key.id === keyId);
|
|
264
|
+
if (!selected) {
|
|
265
|
+
throw new Error(`API key not found: ${keyId}`);
|
|
266
|
+
}
|
|
267
|
+
if (!selected.active) {
|
|
268
|
+
throw new Error(`API key is revoked: ${keyId}`);
|
|
269
|
+
}
|
|
270
|
+
if (!selected.api_key) {
|
|
271
|
+
throw new Error(`API key ${keyId} does not include a decryptable full key. Provide --api-key or create a new key with --create-key-name.`);
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
...selected,
|
|
275
|
+
source: "existing"
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const activeDecryptable = keys.filter((key) => key.active && key.api_key);
|
|
280
|
+
if (activeDecryptable.length === 1) {
|
|
281
|
+
return {
|
|
282
|
+
...activeDecryptable[0],
|
|
283
|
+
source: "existing"
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (activeDecryptable.length > 1) {
|
|
287
|
+
throw new Error("Multiple active API keys are available. Provide --key-id, --api-key, or --create-key-name.");
|
|
288
|
+
}
|
|
289
|
+
throw new Error("No active decryptable API key is available. Provide --api-key or create a new key with --create-key-name.");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function normalizeApiKeys(data) {
|
|
293
|
+
const keys = Array.isArray(data) ? data : (data?.api_keys || data?.keys || []);
|
|
294
|
+
return keys.map(normalizeApiKey);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeApiKey(key = {}) {
|
|
298
|
+
const active = typeof key.active === "boolean"
|
|
299
|
+
? key.active
|
|
300
|
+
: key.revoked !== true;
|
|
301
|
+
return {
|
|
302
|
+
id: key.id || "",
|
|
303
|
+
name: key.name || "",
|
|
304
|
+
api_key: key.api_key || key.key || "",
|
|
305
|
+
api_key_masked: key.api_key_masked || formatMaskedPrefix(key.prefix),
|
|
306
|
+
active,
|
|
307
|
+
status: active ? "active" : "revoked"
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatMaskedPrefix(prefix) {
|
|
312
|
+
return prefix ? `${prefix}***...****` : "";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function writeExtensionArtifacts({ outputDir, browser, apiUrl, apiKey }) {
|
|
316
|
+
const artifacts = [];
|
|
317
|
+
const browsers = browser === "all" ? ["chrome", "edge"] : [browser];
|
|
318
|
+
for (const target of browsers) {
|
|
319
|
+
artifacts.push(await writeArtifact(
|
|
320
|
+
outputDir,
|
|
321
|
+
`extension/${target}-extension-forcelist.txt`,
|
|
322
|
+
`${EXTENSION_ID};${EXTENSION_UPDATE_URL}\n`,
|
|
323
|
+
"extension"
|
|
324
|
+
));
|
|
325
|
+
artifacts.push(await writeArtifact(
|
|
326
|
+
outputDir,
|
|
327
|
+
`extension/${target}-extension-settings.json`,
|
|
328
|
+
`${JSON.stringify(buildExtensionSettings({ apiUrl, apiKey }), null, 2)}\n`,
|
|
329
|
+
"extension"
|
|
330
|
+
));
|
|
331
|
+
}
|
|
332
|
+
return artifacts;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function buildExtensionSettings({ apiUrl, apiKey }) {
|
|
336
|
+
const apiHost = new URL(apiUrl).host;
|
|
337
|
+
return {
|
|
338
|
+
[EXTENSION_ID]: {
|
|
339
|
+
installation_mode: "force_installed",
|
|
340
|
+
runtime_allowed_hosts: [`*://${apiHost}`],
|
|
341
|
+
runtime_blocked_hosts: [],
|
|
342
|
+
configuration: {
|
|
343
|
+
api_url: apiUrl,
|
|
344
|
+
api_key: apiKey,
|
|
345
|
+
monitor_clipboard: true,
|
|
346
|
+
monitor_ai_apps: true,
|
|
347
|
+
show_notifications: true
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function writeAgentArtifacts({ outputDir, apiUrl, apiKey }) {
|
|
354
|
+
return [
|
|
355
|
+
await writeArtifact(
|
|
356
|
+
outputDir,
|
|
357
|
+
"agent/windows-agent-policy.reg",
|
|
358
|
+
buildWindowsAgentRegistry({ apiUrl, apiKey }),
|
|
359
|
+
"agent"
|
|
360
|
+
),
|
|
361
|
+
await writeArtifact(
|
|
362
|
+
outputDir,
|
|
363
|
+
"agent/agent-config.json",
|
|
364
|
+
`${JSON.stringify(buildAgentConfig({ apiUrl, apiKey }), null, 2)}\n`,
|
|
365
|
+
"agent"
|
|
366
|
+
)
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildWindowsAgentRegistry({ apiUrl, apiKey }) {
|
|
371
|
+
return [
|
|
372
|
+
"Windows Registry Editor Version 5.00",
|
|
373
|
+
"",
|
|
374
|
+
"[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\ZeusLock]",
|
|
375
|
+
`"ServerUrl"="${escapeRegString(apiUrl)}"`,
|
|
376
|
+
`"LicenseKey"="${escapeRegString(apiKey)}"`,
|
|
377
|
+
""
|
|
378
|
+
].join("\r\n");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function buildAgentConfig({ apiUrl, apiKey }) {
|
|
382
|
+
return {
|
|
383
|
+
ServerUrl: apiUrl,
|
|
384
|
+
LicenseKey: apiKey,
|
|
385
|
+
apiUrl,
|
|
386
|
+
apiKey
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function escapeRegString(value) {
|
|
391
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function writeArtifact(outputDir, relativePath, content, type) {
|
|
395
|
+
const fullPath = path.join(outputDir, relativePath);
|
|
396
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
397
|
+
await writeFile(fullPath, content, "utf8");
|
|
398
|
+
return {
|
|
399
|
+
type,
|
|
400
|
+
path: fullPath
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function skippedArtifacts({ requested, browser }) {
|
|
405
|
+
const skipped = [];
|
|
406
|
+
skipped.push({
|
|
407
|
+
feature: "proxy .reg artifact",
|
|
408
|
+
reason: "Not implemented in the dashboard or backend; skipped."
|
|
409
|
+
});
|
|
410
|
+
if (!requested.extension) {
|
|
411
|
+
skipped.push({
|
|
412
|
+
feature: "extension deployment artifacts",
|
|
413
|
+
reason: "Not requested by --artifact."
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (!requested.agent) {
|
|
417
|
+
skipped.push({
|
|
418
|
+
feature: "agent deployment artifacts",
|
|
419
|
+
reason: "Not requested by --artifact."
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (requested.extension && browser !== "all") {
|
|
423
|
+
skipped.push({
|
|
424
|
+
feature: `${browser === "chrome" ? "Edge" : "Chrome"} extension artifacts`,
|
|
425
|
+
reason: "Not requested by --browser."
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return skipped;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function maskSecret(value) {
|
|
432
|
+
const secret = String(value || "");
|
|
433
|
+
if (!secret) {
|
|
434
|
+
return "";
|
|
435
|
+
}
|
|
436
|
+
if (secret.length <= 10) {
|
|
437
|
+
return `${secret.slice(0, 3)}***`;
|
|
438
|
+
}
|
|
439
|
+
return `${secret.slice(0, 6)}***...${secret.slice(-4)}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizeAgents(agents) {
|
|
443
|
+
return agents.map((agent) => {
|
|
444
|
+
const online = computeOnline(agent);
|
|
445
|
+
return {
|
|
446
|
+
agent_id: agent.agent_id || agent.id || "",
|
|
447
|
+
hostname: agent.hostname || "",
|
|
448
|
+
platform: String(agent.platform || "").toLowerCase(),
|
|
449
|
+
version: agent.version || "",
|
|
450
|
+
username: agent.username || agent.user || agent.user_email || "",
|
|
451
|
+
last_seen: agent.last_seen || "",
|
|
452
|
+
first_seen: agent.first_seen || "",
|
|
453
|
+
is_online: online,
|
|
454
|
+
status: online ? "online" : "offline"
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function computeOnline(agent, now = new Date()) {
|
|
460
|
+
if (typeof agent?.is_online === "boolean") {
|
|
461
|
+
return agent.is_online;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const lastSeen = parseApiTimestamp(agent?.last_seen);
|
|
465
|
+
if (!lastSeen) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return now.getTime() - lastSeen.getTime() < AGENT_ONLINE_WINDOW_MS;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function matchesAgentIdentity(agent, filters) {
|
|
473
|
+
if (filters.agent_id && agent.agent_id !== filters.agent_id) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
if (filters.hostname && agent.hostname.toLowerCase() !== String(filters.hostname).toLowerCase()) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
if (filters.platform && agent.platform !== filters.platform) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function buildVerifyOutcome({
|
|
486
|
+
total,
|
|
487
|
+
onlineCount,
|
|
488
|
+
offlineCount,
|
|
489
|
+
filters,
|
|
490
|
+
matchingIdentity,
|
|
491
|
+
matchingStatus,
|
|
492
|
+
expectedStatus
|
|
493
|
+
}) {
|
|
494
|
+
let status = "not_found";
|
|
495
|
+
let verified = false;
|
|
496
|
+
if (matchingIdentity.length && matchingStatus.length) {
|
|
497
|
+
status = "verified";
|
|
498
|
+
verified = true;
|
|
499
|
+
} else if (matchingIdentity.length && expectedStatus === "online") {
|
|
500
|
+
status = "offline";
|
|
501
|
+
} else if (matchingIdentity.length && expectedStatus === "offline") {
|
|
502
|
+
status = "online";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
verified,
|
|
507
|
+
status,
|
|
508
|
+
total,
|
|
509
|
+
online_count: onlineCount,
|
|
510
|
+
offline_count: offlineCount,
|
|
511
|
+
matched: matchingStatus.length || matchingIdentity.length,
|
|
512
|
+
filters,
|
|
513
|
+
agents: matchingStatus.length ? matchingStatus : matchingIdentity
|
|
514
|
+
};
|
|
515
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
import { requestAuthenticatedJson } from "../lib/api-client.js";
|
|
4
|
+
import { dash, formatTable } from "../lib/table.js";
|
|
5
|
+
|
|
6
|
+
export function createExtensionsCommand({
|
|
7
|
+
stdout = process.stdout,
|
|
8
|
+
env = process.env
|
|
9
|
+
} = {}) {
|
|
10
|
+
const command = new Command("extensions")
|
|
11
|
+
.description("Review browser extension deployment status");
|
|
12
|
+
|
|
13
|
+
command.addCommand(createExtensionsStatusCommand({ stdout, env }));
|
|
14
|
+
|
|
15
|
+
return command;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createExtensionsStatusCommand({ stdout = process.stdout, env = process.env } = {}) {
|
|
19
|
+
return new Command("status")
|
|
20
|
+
.description("Show active browser extensions")
|
|
21
|
+
.option("--json", "print machine-readable output")
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
const data = await requestAuthenticatedJson("/api/extensions", { env });
|
|
24
|
+
const extensions = normalizeExtensions(Array.isArray(data) ? data : (data.extensions || []));
|
|
25
|
+
const output = {
|
|
26
|
+
active_count: typeof data?.active_count === "number" ? data.active_count : extensions.length,
|
|
27
|
+
active_users: typeof data?.active_users === "number" ? data.active_users : countUniqueUsers(extensions),
|
|
28
|
+
last_minutes: typeof data?.last_minutes === "number" ? data.last_minutes : 30,
|
|
29
|
+
extensions
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (options.json) {
|
|
33
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
stdout.write(`Active extensions: ${output.active_count}\n`);
|
|
38
|
+
stdout.write(`Active users: ${output.active_users}\n`);
|
|
39
|
+
stdout.write(`Window: last ${output.last_minutes} minutes\n`);
|
|
40
|
+
|
|
41
|
+
if (!extensions.length) {
|
|
42
|
+
stdout.write("No active extensions found.\n");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stdout.write("\n");
|
|
47
|
+
stdout.write(`${formatTable(extensions, [
|
|
48
|
+
{ header: "USER EMAIL", value: (extension) => dash(extension.user_email) },
|
|
49
|
+
{ header: "BROWSER", value: (extension) => dash(extension.browser) },
|
|
50
|
+
{ header: "VERSION", value: (extension) => dash(extension.version) },
|
|
51
|
+
{ header: "LAST ACTIVITY", value: (extension) => dash(extension.last_activity) },
|
|
52
|
+
{ header: "STATUS", value: (extension) => extension.status }
|
|
53
|
+
])}\n`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeExtensions(extensions) {
|
|
58
|
+
return extensions.map((extension) => {
|
|
59
|
+
const isActive = extension.is_active !== false;
|
|
60
|
+
return {
|
|
61
|
+
extension_id: extension.extension_id || extension.id || "",
|
|
62
|
+
user_email: extension.user_email || "",
|
|
63
|
+
browser: extension.browser || "",
|
|
64
|
+
version: extension.version || extension.extension_version || "",
|
|
65
|
+
last_activity: extension.timestamp || extension.last_seen || "",
|
|
66
|
+
status: isActive ? "active" : "inactive"
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function countUniqueUsers(extensions) {
|
|
72
|
+
return new Set(extensions.map((extension) => extension.user_email).filter(Boolean)).size;
|
|
73
|
+
}
|