@opensecurity/zonzon-cli 0.1.3 → 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 +125 -44
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +105 -0
- package/package.json +5 -5
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 (
|
|
66
|
+
zonzon core engine (v${CLI_VERSION})
|
|
62
67
|
Usage: zonzon <command> [options]
|
|
63
68
|
|
|
64
69
|
Commands:
|
|
@@ -74,22 +79,30 @@ 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
|
-
|
|
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 = {
|
|
86
97
|
port: 53,
|
|
98
|
+
httpPort: 80,
|
|
99
|
+
httpsPort: 443,
|
|
87
100
|
fallbackDns: "1.1.1.1",
|
|
88
101
|
maxTcpConnections: 100,
|
|
89
102
|
tcpIdleTimeoutMs: 30000,
|
|
90
103
|
controlPlane: {
|
|
91
104
|
enabled: true,
|
|
92
|
-
|
|
105
|
+
socketPath: DEFAULT_SOCK_PATH
|
|
93
106
|
},
|
|
94
107
|
firewall: {
|
|
95
108
|
defaultPolicy: "deny",
|
|
@@ -98,35 +111,101 @@ async function handleInit(configPath) {
|
|
|
98
111
|
hosts: {}
|
|
99
112
|
};
|
|
100
113
|
saveConfig(configPath, defaultConf);
|
|
101
|
-
|
|
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
|
+
}
|
|
102
122
|
process.exit(0);
|
|
103
123
|
}
|
|
104
|
-
async function handleConfig(configPath, args) {
|
|
124
|
+
async function handleConfig(configPath, args, isJson) {
|
|
105
125
|
const subCmd = args[0];
|
|
106
126
|
if (subCmd === "view") {
|
|
107
127
|
if (!fs.existsSync(configPath)) {
|
|
108
|
-
|
|
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
|
+
}
|
|
109
134
|
process.exit(1);
|
|
110
135
|
}
|
|
111
136
|
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
112
|
-
|
|
137
|
+
if (isJson) {
|
|
138
|
+
console.log(JSON.stringify(JSON.parse(fileContents)));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.log(fileContents);
|
|
142
|
+
}
|
|
113
143
|
process.exit(0);
|
|
114
144
|
}
|
|
115
145
|
if (subCmd === "set") {
|
|
116
146
|
const key = args[1];
|
|
117
147
|
const value = args[2];
|
|
118
148
|
if (!key || value === undefined) {
|
|
119
|
-
|
|
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
|
+
}
|
|
120
155
|
process.exit(1);
|
|
121
156
|
}
|
|
122
157
|
const currentConfig = loadConfig(configPath);
|
|
123
158
|
setDeepValue(currentConfig, key, value);
|
|
124
159
|
saveConfig(configPath, currentConfig);
|
|
125
|
-
|
|
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
|
+
}
|
|
126
166
|
process.exit(0);
|
|
127
167
|
}
|
|
128
168
|
printUsage();
|
|
129
169
|
}
|
|
170
|
+
class ZonzonDaemon {
|
|
171
|
+
dnsHandler = null;
|
|
172
|
+
httpHandler = null;
|
|
173
|
+
sniProxy = null;
|
|
174
|
+
async start(config) {
|
|
175
|
+
try {
|
|
176
|
+
const dnsServer = new DevDnsServer(config);
|
|
177
|
+
this.dnsHandler = new DnsHandler(dnsServer, config);
|
|
178
|
+
await this.dnsHandler.start();
|
|
179
|
+
audit.system(`DNS Listener actively enforcing Zero-Trust boundaries on port ${config.port}`);
|
|
180
|
+
this.httpHandler = new HttpHandler(dnsServer, config, config.httpPort ?? 80);
|
|
181
|
+
await this.httpHandler.start();
|
|
182
|
+
audit.system(`HTTP L7 Sandbox Router active on port ${config.httpPort ?? 80}`);
|
|
183
|
+
this.sniProxy = new SniProxyService(config, config.httpsPort ?? 443);
|
|
184
|
+
await this.sniProxy.start();
|
|
185
|
+
audit.system(`SNI Proxy active on port ${config.httpsPort ?? 443}`);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
audit.error(`Fatal bind error during initialization: ${err.message}`);
|
|
189
|
+
await this.stop();
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async stop() {
|
|
194
|
+
if (this.dnsHandler) {
|
|
195
|
+
await this.dnsHandler.stop();
|
|
196
|
+
this.dnsHandler = null;
|
|
197
|
+
}
|
|
198
|
+
if (this.httpHandler) {
|
|
199
|
+
await this.httpHandler.stop();
|
|
200
|
+
this.httpHandler = null;
|
|
201
|
+
}
|
|
202
|
+
if (this.sniProxy) {
|
|
203
|
+
await this.sniProxy.stop();
|
|
204
|
+
this.sniProxy = null;
|
|
205
|
+
}
|
|
206
|
+
audit.system("Subsystems halted. Sockets closed.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
130
209
|
async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
131
210
|
const rawConfig = loadConfig(configPath);
|
|
132
211
|
if (portOverride) {
|
|
@@ -145,10 +224,8 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
|
145
224
|
audit.error(`Configuration Schema Violation: ${err.message}`);
|
|
146
225
|
process.exit(1);
|
|
147
226
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
const httpHandler = new HttpHandler(dnsServer, config, 80);
|
|
151
|
-
const sniProxy = new SniProxyService(config, 443);
|
|
227
|
+
const daemon = new ZonzonDaemon();
|
|
228
|
+
await daemon.start(config);
|
|
152
229
|
const isCpEnabled = config.controlPlane?.enabled !== false;
|
|
153
230
|
let controlPlane = null;
|
|
154
231
|
let isEphemeralKey = false;
|
|
@@ -161,51 +238,50 @@ async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
|
161
238
|
}
|
|
162
239
|
const blindIndexSalt = randomBytes(16).toString("hex");
|
|
163
240
|
const cpPort = config.controlPlane?.port || 8080;
|
|
241
|
+
const socketPath = config.controlPlane?.socketPath || DEFAULT_SOCK_PATH;
|
|
164
242
|
controlPlane = new ControlPlane({
|
|
165
243
|
port: cpPort,
|
|
244
|
+
socketPath: socketPath,
|
|
166
245
|
apiKey: activeApiKey,
|
|
167
246
|
blindIndexSalt: blindIndexSalt,
|
|
168
247
|
initialConfig: config,
|
|
248
|
+
configFilePath: configPath
|
|
169
249
|
});
|
|
170
|
-
controlPlane.subscribe((newConfig) => {
|
|
250
|
+
controlPlane.subscribe(async (newConfig) => {
|
|
171
251
|
audit.system("Applying dynamic configuration update from Control Plane...");
|
|
252
|
+
await daemon.stop();
|
|
253
|
+
await daemon.start(newConfig);
|
|
172
254
|
});
|
|
255
|
+
await controlPlane.start();
|
|
256
|
+
if (isEphemeralKey) {
|
|
257
|
+
audit.system(`[SECURITY] Generated Ephemeral API Key for this session: ${activeApiKey}`);
|
|
258
|
+
audit.system(`[SECURITY] Do not lose this key. It will not be shown again.`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
audit.system(`[SECURITY] Control Plane using static API Key from configuration.`);
|
|
262
|
+
}
|
|
173
263
|
}
|
|
264
|
+
audit.system("Initialization complete. Awaiting connections...");
|
|
265
|
+
let shuttingDown = false;
|
|
174
266
|
const shutdown = async () => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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);
|
|
275
|
+
await daemon.stop();
|
|
179
276
|
if (controlPlane) {
|
|
180
277
|
await controlPlane.stop();
|
|
181
278
|
}
|
|
279
|
+
clearTimeout(forceExit);
|
|
280
|
+
audit.system("All boundaries offline. Process terminating cleanly.");
|
|
182
281
|
process.exit(0);
|
|
183
282
|
};
|
|
184
283
|
process.on("SIGINT", shutdown);
|
|
185
284
|
process.on("SIGTERM", shutdown);
|
|
186
|
-
try {
|
|
187
|
-
await dnsHandler.start();
|
|
188
|
-
audit.system(`DNS Listener actively enforcing Zero-Trust boundaries on port ${config.port}`);
|
|
189
|
-
await httpHandler.start();
|
|
190
|
-
audit.system(`HTTP L7 Sandbox Router active on port 80`);
|
|
191
|
-
await sniProxy.start();
|
|
192
|
-
audit.system(`SNI Proxy active on port 443`);
|
|
193
|
-
if (controlPlane) {
|
|
194
|
-
await controlPlane.start();
|
|
195
|
-
if (isEphemeralKey) {
|
|
196
|
-
audit.system(`[SECURITY] Generated Ephemeral API Key for this session: ${activeApiKey}`);
|
|
197
|
-
audit.system(`[SECURITY] Do not lose this key. It will not be shown again.`);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
audit.system(`[SECURITY] Control Plane using static API Key from configuration.`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
audit.system("Initialization complete. Awaiting connections...");
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
audit.error(`Fatal bind error during initialization: ${err.message}`);
|
|
207
|
-
await shutdown();
|
|
208
|
-
}
|
|
209
285
|
}
|
|
210
286
|
async function main() {
|
|
211
287
|
const { values, positionals } = parseArgs({
|
|
@@ -221,6 +297,10 @@ async function main() {
|
|
|
221
297
|
"cp-port": {
|
|
222
298
|
type: "string",
|
|
223
299
|
},
|
|
300
|
+
json: {
|
|
301
|
+
type: "boolean",
|
|
302
|
+
default: false,
|
|
303
|
+
}
|
|
224
304
|
},
|
|
225
305
|
strict: false,
|
|
226
306
|
allowPositionals: true
|
|
@@ -229,12 +309,13 @@ async function main() {
|
|
|
229
309
|
const configPath = values.config
|
|
230
310
|
? path.resolve(process.cwd(), values.config)
|
|
231
311
|
: DEFAULT_CONFIG_PATH;
|
|
312
|
+
const isJson = values.json;
|
|
232
313
|
switch (command) {
|
|
233
314
|
case "init":
|
|
234
|
-
await handleInit(configPath);
|
|
315
|
+
await handleInit(configPath, isJson);
|
|
235
316
|
break;
|
|
236
317
|
case "config":
|
|
237
|
-
await handleConfig(configPath, positionals.slice(1));
|
|
318
|
+
await handleConfig(configPath, positionals.slice(1), isJson);
|
|
238
319
|
break;
|
|
239
320
|
case "start":
|
|
240
321
|
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.5",
|
|
4
4
|
"description": "cli interface for zonzon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Lucian BLETAN <neuraluc@gmail.com>",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsc -b",
|
|
30
30
|
"start": "node dist/cli.js",
|
|
31
|
-
"dev": "tsx watch src/cli.ts"
|
|
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.5",
|
|
35
|
+
"@opensecurity/zonzon-control-plane": "^0.1.5"
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|