@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 +0 -1
- package/dist/cli.js +74 -21
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +105 -0
- package/package.json +4 -4
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|
package/dist/cli.test.js
ADDED
|
@@ -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.
|
|
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.
|
|
35
|
-
"@opensecurity/zonzon-control-plane": "^0.1.
|
|
34
|
+
"@opensecurity/zonzon-core": "^0.1.6",
|
|
35
|
+
"@opensecurity/zonzon-control-plane": "^0.1.6"
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|