@opensecurity/zonzon-cli 0.1.4 → 0.1.6

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/dist/cli.d.ts CHANGED
@@ -1,2 +1 @@
1
- #!/usr/bin/env node
2
1
  export {};
package/dist/cli.js CHANGED
@@ -1,13 +1,17 @@
1
- #!/usr/bin/env node
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import * as os from "os";
5
- import { parseArgs } from "util";
6
- import { randomBytes } from "crypto";
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { parseArgs } from "node:util";
5
+ import { randomBytes } from "node:crypto";
6
+ import { createRequire } from "node:module";
7
7
  import { DevDnsServer, DnsHandler, HttpHandler, SniProxyService, validateServerConfig, audit } from "@opensecurity/zonzon-core";
8
8
  import { ControlPlane } from "@opensecurity/zonzon-control-plane";
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require("../package.json");
11
+ const CLI_VERSION = pkg.version;
9
12
  const CONFIG_DIR = path.join(os.homedir(), ".zonzon");
10
13
  const DEFAULT_CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
14
+ const DEFAULT_SOCK_PATH = path.join(os.tmpdir(), "zonzon-cp.sock");
11
15
  function ensureConfigDir() {
12
16
  if (!fs.existsSync(CONFIG_DIR)) {
13
17
  fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
@@ -58,7 +62,7 @@ function setDeepValue(obj, pathStr, value) {
58
62
  }
59
63
  function printUsage() {
60
64
  console.log(`
61
- zonzon core engine (v0.1.0)
65
+ zonzon core engine (v${CLI_VERSION})
62
66
  Usage: zonzon <command> [options]
63
67
 
64
68
  Commands:
@@ -74,12 +78,18 @@ Config Commands:
74
78
 
75
79
  Global Options:
76
80
  --config, -c Override path to configuration file (default: ~/.zonzon/config.json)
81
+ --json Output CLI command results in pure JSON format
77
82
  `);
78
83
  process.exit(0);
79
84
  }
80
- async function handleInit(configPath) {
85
+ async function handleInit(configPath, isJson) {
81
86
  if (fs.existsSync(configPath)) {
82
- audit.error(`Configuration already exists at ${configPath}`);
87
+ if (isJson) {
88
+ console.log(JSON.stringify({ success: false, error: "Configuration already exists", path: configPath }));
89
+ }
90
+ else {
91
+ audit.error(`Configuration already exists at ${configPath}`);
92
+ }
83
93
  process.exit(1);
84
94
  }
85
95
  const defaultConf = {
@@ -91,7 +101,7 @@ async function handleInit(configPath) {
91
101
  tcpIdleTimeoutMs: 30000,
92
102
  controlPlane: {
93
103
  enabled: true,
94
- port: 8080
104
+ socketPath: DEFAULT_SOCK_PATH
95
105
  },
96
106
  firewall: {
97
107
  defaultPolicy: "deny",
@@ -100,33 +110,58 @@ async function handleInit(configPath) {
100
110
  hosts: {}
101
111
  };
102
112
  saveConfig(configPath, defaultConf);
103
- audit.system(`Initialized secure default configuration at ${configPath}`);
104
- audit.system(`Security Notice: Default HTTP/HTTPS ports mapped to 80/443.`);
105
- audit.system(`If executing within a non-root sandbox, mutate config.json to unprivileged ports (e.g. 8080/8443) to prevent EACCES binding faults.`);
113
+ if (isJson) {
114
+ console.log(JSON.stringify({ success: true, action: "init", path: configPath, config: defaultConf }));
115
+ }
116
+ else {
117
+ audit.system(`Initialized secure default configuration at ${configPath}`);
118
+ audit.system(`Security Notice: Default HTTP/HTTPS ports mapped to 80/443.`);
119
+ audit.system(`If executing within a non-root sandbox, mutate config.json to unprivileged ports (e.g. 8080/8443) to prevent EACCES binding faults.`);
120
+ }
106
121
  process.exit(0);
107
122
  }
108
- async function handleConfig(configPath, args) {
123
+ async function handleConfig(configPath, args, isJson) {
109
124
  const subCmd = args[0];
110
125
  if (subCmd === "view") {
111
126
  if (!fs.existsSync(configPath)) {
112
- audit.error(`No configuration found at ${configPath}. Run 'zonzon init' first.`);
127
+ if (isJson) {
128
+ console.log(JSON.stringify({ success: false, error: "No configuration found. Run init first." }));
129
+ }
130
+ else {
131
+ audit.error(`No configuration found at ${configPath}. Run 'zonzon init' first.`);
132
+ }
113
133
  process.exit(1);
114
134
  }
115
135
  const fileContents = fs.readFileSync(configPath, "utf8");
116
- console.log(fileContents);
136
+ if (isJson) {
137
+ console.log(JSON.stringify(JSON.parse(fileContents)));
138
+ }
139
+ else {
140
+ console.log(fileContents);
141
+ }
117
142
  process.exit(0);
118
143
  }
119
144
  if (subCmd === "set") {
120
145
  const key = args[1];
121
146
  const value = args[2];
122
147
  if (!key || value === undefined) {
123
- audit.error("Usage: zonzon config set <key> <value>");
148
+ if (isJson) {
149
+ console.log(JSON.stringify({ success: false, error: "Missing key or value" }));
150
+ }
151
+ else {
152
+ audit.error("Usage: zonzon config set <key> <value>");
153
+ }
124
154
  process.exit(1);
125
155
  }
126
156
  const currentConfig = loadConfig(configPath);
127
157
  setDeepValue(currentConfig, key, value);
128
158
  saveConfig(configPath, currentConfig);
129
- audit.system(`Updated configuration: ${key} = ${value}`);
159
+ if (isJson) {
160
+ console.log(JSON.stringify({ success: true, action: "set", key, value }));
161
+ }
162
+ else {
163
+ audit.system(`Updated configuration: ${key} = ${value}`);
164
+ }
130
165
  process.exit(0);
131
166
  }
132
167
  printUsage();
@@ -202,11 +237,14 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
202
237
  }
203
238
  const blindIndexSalt = randomBytes(16).toString("hex");
204
239
  const cpPort = config.controlPlane?.port || 8080;
240
+ const socketPath = config.controlPlane?.socketPath || DEFAULT_SOCK_PATH;
205
241
  controlPlane = new ControlPlane({
206
242
  port: cpPort,
243
+ socketPath: socketPath,
207
244
  apiKey: activeApiKey,
208
245
  blindIndexSalt: blindIndexSalt,
209
246
  initialConfig: config,
247
+ configFilePath: configPath
210
248
  });
211
249
  controlPlane.subscribe(async (newConfig) => {
212
250
  audit.system("Applying dynamic configuration update from Control Plane...");
@@ -223,12 +261,22 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
223
261
  }
224
262
  }
225
263
  audit.system("Initialization complete. Awaiting connections...");
264
+ let shuttingDown = false;
226
265
  const shutdown = async () => {
227
- audit.system("Initiating graceful shutdown sequence...");
266
+ if (shuttingDown)
267
+ return;
268
+ shuttingDown = true;
269
+ audit.system("SIGINT/SIGTERM received. Initiating graceful connection draining sequence (10s bounds)...");
270
+ const forceExit = setTimeout(() => {
271
+ audit.error("Graceful drain timeout exceeded. Forcing engine termination.");
272
+ process.exit(1);
273
+ }, 10000);
228
274
  await daemon.stop();
229
275
  if (controlPlane) {
230
276
  await controlPlane.stop();
231
277
  }
278
+ clearTimeout(forceExit);
279
+ audit.system("All boundaries offline. Process terminating cleanly.");
232
280
  process.exit(0);
233
281
  };
234
282
  process.on("SIGINT", shutdown);
@@ -248,6 +296,10 @@ async function main() {
248
296
  "cp-port": {
249
297
  type: "string",
250
298
  },
299
+ json: {
300
+ type: "boolean",
301
+ default: false,
302
+ }
251
303
  },
252
304
  strict: false,
253
305
  allowPositionals: true
@@ -256,12 +308,13 @@ async function main() {
256
308
  const configPath = values.config
257
309
  ? path.resolve(process.cwd(), values.config)
258
310
  : DEFAULT_CONFIG_PATH;
311
+ const isJson = values.json;
259
312
  switch (command) {
260
313
  case "init":
261
- await handleInit(configPath);
314
+ await handleInit(configPath, isJson);
262
315
  break;
263
316
  case "config":
264
- await handleConfig(configPath, positionals.slice(1));
317
+ await handleConfig(configPath, positionals.slice(1), isJson);
265
318
  break;
266
319
  case "start":
267
320
  await startEngine(configPath, values.port, values["cp-port"]);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert";
3
+ import { exec } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { promises as fs } from "node:fs";
6
+ import * as path from "node:path";
7
+ import * as os from "node:os";
8
+ const execAsync = promisify(exec);
9
+ const PROJECT_ROOT = process.cwd();
10
+ const CLI_PATH = path.resolve(PROJECT_ROOT, "packages/cli/src/cli.ts");
11
+ async function runCli(args) {
12
+ try {
13
+ const { stdout, stderr } = await execAsync(`npx tsx ${CLI_PATH} ${args.join(" ")}`);
14
+ return { stdout, stderr, code: 0 };
15
+ }
16
+ catch (error) {
17
+ return { stdout: error.stdout || "", stderr: error.stderr || "", code: error.code || 1 };
18
+ }
19
+ }
20
+ describe("CLI Integration Tests", () => {
21
+ let tempDir;
22
+ let testConfigPath;
23
+ before(async () => {
24
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zonzon-cli-test-"));
25
+ testConfigPath = path.join(tempDir, "config.json");
26
+ });
27
+ after(async () => {
28
+ await fs.rm(tempDir, { recursive: true, force: true });
29
+ });
30
+ it("fails to run without a command and prints usage", async () => {
31
+ const { stdout, code } = await runCli([]);
32
+ assert.strictEqual(code, 0);
33
+ assert.ok(stdout.includes("Usage: zonzon <command>"));
34
+ });
35
+ it("initializes a new configuration file", async () => {
36
+ const { stdout, code } = await runCli(["init", "--config", testConfigPath, "--json"]);
37
+ assert.strictEqual(code, 0);
38
+ const output = JSON.parse(stdout);
39
+ assert.strictEqual(output.success, true);
40
+ assert.strictEqual(output.action, "init");
41
+ const fileContent = await fs.readFile(testConfigPath, "utf8");
42
+ const config = JSON.parse(fileContent);
43
+ assert.strictEqual(config.port, 53);
44
+ assert.strictEqual(config.controlPlane.enabled, true);
45
+ });
46
+ it("refuses to overwrite an existing configuration", async () => {
47
+ const { stdout, code } = await runCli(["init", "--config", testConfigPath, "--json"]);
48
+ assert.strictEqual(code, 1);
49
+ const output = JSON.parse(stdout);
50
+ assert.strictEqual(output.success, false);
51
+ assert.ok(output.error.includes("already exists"));
52
+ });
53
+ it("views the current configuration", async () => {
54
+ const { stdout, code } = await runCli(["config", "view", "--config", testConfigPath, "--json"]);
55
+ assert.strictEqual(code, 0);
56
+ const config = JSON.parse(stdout);
57
+ assert.strictEqual(config.port, 53);
58
+ assert.strictEqual(config.httpPort, 80);
59
+ });
60
+ it("mutates a top level configuration value using numeric casting", async () => {
61
+ const { stdout, code } = await runCli(["config", "set", "port", "5353", "--config", testConfigPath, "--json"]);
62
+ assert.strictEqual(code, 0);
63
+ const output = JSON.parse(stdout);
64
+ assert.strictEqual(output.success, true);
65
+ const { stdout: viewOut } = await runCli(["config", "view", "--config", testConfigPath, "--json"]);
66
+ const config = JSON.parse(viewOut);
67
+ assert.strictEqual(config.port, 5353);
68
+ });
69
+ it("mutates a nested configuration value using boolean casting", async () => {
70
+ const { code } = await runCli(["config", "set", "controlPlane.enabled", "false", "--config", testConfigPath, "--json"]);
71
+ assert.strictEqual(code, 0);
72
+ const fileContent = await fs.readFile(testConfigPath, "utf8");
73
+ const config = JSON.parse(fileContent);
74
+ assert.strictEqual(config.controlPlane.enabled, false);
75
+ });
76
+ it("mutates a nested configuration value with standard strings", async () => {
77
+ const { code } = await runCli(["config", "set", "fallbackDns", "8.8.8.8", "--config", testConfigPath, "--json"]);
78
+ assert.strictEqual(code, 0);
79
+ const fileContent = await fs.readFile(testConfigPath, "utf8");
80
+ const config = JSON.parse(fileContent);
81
+ assert.strictEqual(config.fallbackDns, "8.8.8.8");
82
+ });
83
+ it("creates nested objects natively if they do not exist", async () => {
84
+ const { code } = await runCli(["config", "set", "firewall.allowlist_ips.0", "10.0.0.1", "--config", testConfigPath, "--json"]);
85
+ assert.strictEqual(code, 0);
86
+ const fileContent = await fs.readFile(testConfigPath, "utf8");
87
+ const config = JSON.parse(fileContent);
88
+ assert.strictEqual(config.firewall.allowlist_ips[0], "10.0.0.1");
89
+ });
90
+ it("fails to view configuration if file is missing", async () => {
91
+ const missingPath = path.join(tempDir, "missing.json");
92
+ const { stdout, code } = await runCli(["config", "view", "--config", missingPath, "--json"]);
93
+ assert.strictEqual(code, 1);
94
+ const output = JSON.parse(stdout);
95
+ assert.strictEqual(output.success, false);
96
+ assert.ok(output.error.includes("No configuration found"));
97
+ });
98
+ it("fails to set configuration if parameters are missing", async () => {
99
+ const { stdout, code } = await runCli(["config", "set", "port", "--config", testConfigPath, "--json"]);
100
+ assert.strictEqual(code, 1);
101
+ const output = JSON.parse(stdout);
102
+ assert.strictEqual(output.success, false);
103
+ assert.ok(output.error.includes("Missing key or value"));
104
+ });
105
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensecurity/zonzon-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "cli interface for zonzon",
5
5
  "type": "module",
6
6
  "author": "Lucian BLETAN <neuraluc@gmail.com>",
@@ -31,7 +31,7 @@
31
31
  "dev:watch": "NODE_OPTIONS=--disable-warning=DEP0205 tsx watch src/cli.ts start --config ../../config/hosts.json"
32
32
  },
33
33
  "dependencies": {
34
- "@opensecurity/zonzon-core": "^0.1.4",
35
- "@opensecurity/zonzon-control-plane": "^0.1.4"
34
+ "@opensecurity/zonzon-core": "^0.1.6",
35
+ "@opensecurity/zonzon-control-plane": "^0.1.6"
36
36
  }
37
- }
37
+ }