@opensecurity/zonzon-cli 0.1.4 → 0.1.5

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.js CHANGED
@@ -4,10 +4,15 @@ import * as path from "path";
4
4
  import * as os from "os";
5
5
  import { parseArgs } from "util";
6
6
  import { randomBytes } from "crypto";
7
+ import { createRequire } from "module";
7
8
  import { DevDnsServer, DnsHandler, HttpHandler, SniProxyService, validateServerConfig, audit } from "@opensecurity/zonzon-core";
8
9
  import { ControlPlane } from "@opensecurity/zonzon-control-plane";
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require("../package.json");
12
+ const CLI_VERSION = pkg.version;
9
13
  const CONFIG_DIR = path.join(os.homedir(), ".zonzon");
10
14
  const DEFAULT_CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
15
+ const DEFAULT_SOCK_PATH = path.join(os.tmpdir(), "zonzon-cp.sock");
11
16
  function ensureConfigDir() {
12
17
  if (!fs.existsSync(CONFIG_DIR)) {
13
18
  fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
@@ -58,7 +63,7 @@ function setDeepValue(obj, pathStr, value) {
58
63
  }
59
64
  function printUsage() {
60
65
  console.log(`
61
- zonzon core engine (v0.1.0)
66
+ zonzon core engine (v${CLI_VERSION})
62
67
  Usage: zonzon <command> [options]
63
68
 
64
69
  Commands:
@@ -74,12 +79,18 @@ Config Commands:
74
79
 
75
80
  Global Options:
76
81
  --config, -c Override path to configuration file (default: ~/.zonzon/config.json)
82
+ --json Output CLI command results in pure JSON format
77
83
  `);
78
84
  process.exit(0);
79
85
  }
80
- async function handleInit(configPath) {
86
+ async function handleInit(configPath, isJson) {
81
87
  if (fs.existsSync(configPath)) {
82
- audit.error(`Configuration already exists at ${configPath}`);
88
+ if (isJson) {
89
+ console.log(JSON.stringify({ success: false, error: "Configuration already exists", path: configPath }));
90
+ }
91
+ else {
92
+ audit.error(`Configuration already exists at ${configPath}`);
93
+ }
83
94
  process.exit(1);
84
95
  }
85
96
  const defaultConf = {
@@ -91,7 +102,7 @@ async function handleInit(configPath) {
91
102
  tcpIdleTimeoutMs: 30000,
92
103
  controlPlane: {
93
104
  enabled: true,
94
- port: 8080
105
+ socketPath: DEFAULT_SOCK_PATH
95
106
  },
96
107
  firewall: {
97
108
  defaultPolicy: "deny",
@@ -100,33 +111,58 @@ async function handleInit(configPath) {
100
111
  hosts: {}
101
112
  };
102
113
  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.`);
114
+ if (isJson) {
115
+ console.log(JSON.stringify({ success: true, action: "init", path: configPath, config: defaultConf }));
116
+ }
117
+ else {
118
+ audit.system(`Initialized secure default configuration at ${configPath}`);
119
+ audit.system(`Security Notice: Default HTTP/HTTPS ports mapped to 80/443.`);
120
+ audit.system(`If executing within a non-root sandbox, mutate config.json to unprivileged ports (e.g. 8080/8443) to prevent EACCES binding faults.`);
121
+ }
106
122
  process.exit(0);
107
123
  }
108
- async function handleConfig(configPath, args) {
124
+ async function handleConfig(configPath, args, isJson) {
109
125
  const subCmd = args[0];
110
126
  if (subCmd === "view") {
111
127
  if (!fs.existsSync(configPath)) {
112
- audit.error(`No configuration found at ${configPath}. Run 'zonzon init' first.`);
128
+ if (isJson) {
129
+ console.log(JSON.stringify({ success: false, error: "No configuration found. Run init first." }));
130
+ }
131
+ else {
132
+ audit.error(`No configuration found at ${configPath}. Run 'zonzon init' first.`);
133
+ }
113
134
  process.exit(1);
114
135
  }
115
136
  const fileContents = fs.readFileSync(configPath, "utf8");
116
- console.log(fileContents);
137
+ if (isJson) {
138
+ console.log(JSON.stringify(JSON.parse(fileContents)));
139
+ }
140
+ else {
141
+ console.log(fileContents);
142
+ }
117
143
  process.exit(0);
118
144
  }
119
145
  if (subCmd === "set") {
120
146
  const key = args[1];
121
147
  const value = args[2];
122
148
  if (!key || value === undefined) {
123
- audit.error("Usage: zonzon config set <key> <value>");
149
+ if (isJson) {
150
+ console.log(JSON.stringify({ success: false, error: "Missing key or value" }));
151
+ }
152
+ else {
153
+ audit.error("Usage: zonzon config set <key> <value>");
154
+ }
124
155
  process.exit(1);
125
156
  }
126
157
  const currentConfig = loadConfig(configPath);
127
158
  setDeepValue(currentConfig, key, value);
128
159
  saveConfig(configPath, currentConfig);
129
- audit.system(`Updated configuration: ${key} = ${value}`);
160
+ if (isJson) {
161
+ console.log(JSON.stringify({ success: true, action: "set", key, value }));
162
+ }
163
+ else {
164
+ audit.system(`Updated configuration: ${key} = ${value}`);
165
+ }
130
166
  process.exit(0);
131
167
  }
132
168
  printUsage();
@@ -202,11 +238,14 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
202
238
  }
203
239
  const blindIndexSalt = randomBytes(16).toString("hex");
204
240
  const cpPort = config.controlPlane?.port || 8080;
241
+ const socketPath = config.controlPlane?.socketPath || DEFAULT_SOCK_PATH;
205
242
  controlPlane = new ControlPlane({
206
243
  port: cpPort,
244
+ socketPath: socketPath,
207
245
  apiKey: activeApiKey,
208
246
  blindIndexSalt: blindIndexSalt,
209
247
  initialConfig: config,
248
+ configFilePath: configPath
210
249
  });
211
250
  controlPlane.subscribe(async (newConfig) => {
212
251
  audit.system("Applying dynamic configuration update from Control Plane...");
@@ -223,12 +262,22 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
223
262
  }
224
263
  }
225
264
  audit.system("Initialization complete. Awaiting connections...");
265
+ let shuttingDown = false;
226
266
  const shutdown = async () => {
227
- audit.system("Initiating graceful shutdown sequence...");
267
+ if (shuttingDown)
268
+ return;
269
+ shuttingDown = true;
270
+ audit.system("SIGINT/SIGTERM received. Initiating graceful connection draining sequence (10s bounds)...");
271
+ const forceExit = setTimeout(() => {
272
+ audit.error("Graceful drain timeout exceeded. Forcing engine termination.");
273
+ process.exit(1);
274
+ }, 10000);
228
275
  await daemon.stop();
229
276
  if (controlPlane) {
230
277
  await controlPlane.stop();
231
278
  }
279
+ clearTimeout(forceExit);
280
+ audit.system("All boundaries offline. Process terminating cleanly.");
232
281
  process.exit(0);
233
282
  };
234
283
  process.on("SIGINT", shutdown);
@@ -248,6 +297,10 @@ async function main() {
248
297
  "cp-port": {
249
298
  type: "string",
250
299
  },
300
+ json: {
301
+ type: "boolean",
302
+ default: false,
303
+ }
251
304
  },
252
305
  strict: false,
253
306
  allowPositionals: true
@@ -256,12 +309,13 @@ async function main() {
256
309
  const configPath = values.config
257
310
  ? path.resolve(process.cwd(), values.config)
258
311
  : DEFAULT_CONFIG_PATH;
312
+ const isJson = values.json;
259
313
  switch (command) {
260
314
  case "init":
261
- await handleInit(configPath);
315
+ await handleInit(configPath, isJson);
262
316
  break;
263
317
  case "config":
264
- await handleConfig(configPath, positionals.slice(1));
318
+ await handleConfig(configPath, positionals.slice(1), isJson);
265
319
  break;
266
320
  case "start":
267
321
  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.5",
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.5",
35
+ "@opensecurity/zonzon-control-plane": "^0.1.5"
36
36
  }
37
- }
37
+ }