@fabioforest/openclaw 3.0.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.
Files changed (43) hide show
  1. package/README.md +88 -0
  2. package/bin/openclaw.js +152 -0
  3. package/lib/channels.js +84 -0
  4. package/lib/cli/debug.js +12 -0
  5. package/lib/cli/doctor.js +266 -0
  6. package/lib/cli/init.js +145 -0
  7. package/lib/cli/orchestrate.js +43 -0
  8. package/lib/cli/status.js +128 -0
  9. package/lib/cli/update.js +122 -0
  10. package/lib/config.js +68 -0
  11. package/lib/detect.js +49 -0
  12. package/lib/ops/audit.js +156 -0
  13. package/lib/ops/enroll.js +133 -0
  14. package/lib/ops/exec.js +140 -0
  15. package/lib/ops/healthcheck.js +167 -0
  16. package/lib/ops/policy.js +153 -0
  17. package/lib/ops/transfer.js +152 -0
  18. package/lib/ops/update-safe.js +173 -0
  19. package/lib/ops/vpn.js +131 -0
  20. package/lib/security.js +48 -0
  21. package/lib/setup/config_wizard.js +186 -0
  22. package/package.json +47 -0
  23. package/templates/.agent/agents/setup-specialist.md +24 -0
  24. package/templates/.agent/agents/sysadmin-proativo.md +31 -0
  25. package/templates/.agent/hooks/pre-tool-use.js +109 -0
  26. package/templates/.agent/rules/SECURITY.md +7 -0
  27. package/templates/.agent/skills/openclaw-installation-debugger/SKILL.md +37 -0
  28. package/templates/.agent/skills/openclaw-installation-debugger/scripts/debug.js +165 -0
  29. package/templates/.agent/skills/openclaw-ops/01-openclaw-vpn-wireguard/SKILL.md +20 -0
  30. package/templates/.agent/skills/openclaw-ops/02-openclaw-enroll-host/SKILL.md +14 -0
  31. package/templates/.agent/skills/openclaw-ops/03-openclaw-policy-baseline/SKILL.md +17 -0
  32. package/templates/.agent/skills/openclaw-ops/04-openclaw-remote-exec-runbooks/SKILL.md +13 -0
  33. package/templates/.agent/skills/openclaw-ops/05-openclaw-file-transfer-safe/SKILL.md +10 -0
  34. package/templates/.agent/skills/openclaw-ops/06-openclaw-audit-logging/SKILL.md +13 -0
  35. package/templates/.agent/skills/openclaw-ops/07-openclaw-safe-update/SKILL.md +9 -0
  36. package/templates/.agent/skills/openclaw-ops/08-openclaw-healthchecks/SKILL.md +10 -0
  37. package/templates/.agent/skills/universal-setup/SKILL.md +26 -0
  38. package/templates/.agent/workflows/doctor.md +20 -0
  39. package/templates/.agent/workflows/healthcheck.md +22 -0
  40. package/templates/.agent/workflows/healthcheck.runbook.md +9 -0
  41. package/templates/.agent/workflows/restart.md +20 -0
  42. package/templates/.agent/workflows/restart.runbook.md +8 -0
  43. package/templates/.agent/workflows/setup.md +18 -0
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: Policy Baseline
5
+ *
6
+ * Define RBAC e allowlists (deny-by-default) para execução remota
7
+ * via VPN com break-glass expirável.
8
+ *
9
+ * Referência: skills/openclaw-ops/03-openclaw-policy-baseline/SKILL.md
10
+ */
11
+
12
+ const { readJsonSafe, writeJsonSafe } = require("../config");
13
+
14
+ /**
15
+ * Perfis RBAC disponíveis com suas permissões.
16
+ */
17
+ const PROFILES = {
18
+ viewer: {
19
+ description: "Somente leitura — pode consultar status e logs",
20
+ permissions: ["read:status", "read:logs", "read:config"],
21
+ },
22
+ operator: {
23
+ description: "Pode executar runbooks permitidos",
24
+ permissions: ["read:status", "read:logs", "read:config", "exec:runbook", "write:transfer"],
25
+ },
26
+ admin: {
27
+ description: "Ações elevadas com confirmação extra e auditoria",
28
+ permissions: [
29
+ "read:status", "read:logs", "read:config",
30
+ "exec:runbook", "exec:command", "write:transfer",
31
+ "write:config", "manage:hosts", "manage:policy",
32
+ ],
33
+ },
34
+ };
35
+
36
+ /**
37
+ * Verifica se um perfil tem uma determinada permissão.
38
+ * @param {string} profile — nome do perfil (viewer, operator, admin)
39
+ * @param {string} permission — permissão a verificar (ex: "exec:runbook")
40
+ * @returns {boolean}
41
+ */
42
+ function hasPermission(profile, permission) {
43
+ const p = PROFILES[profile];
44
+ if (!p) return false;
45
+ return p.permissions.includes(permission);
46
+ }
47
+
48
+ /**
49
+ * Cria uma sessão break-glass com expiração automática.
50
+ * @param {string} policyPath — caminho do arquivo de políticas
51
+ * @param {object} params
52
+ * @param {string} params.requestedBy — quem solicitou
53
+ * @param {string} params.reason — motivo do break-glass
54
+ * @param {number} [params.durationMinutes=15] — duração em minutos
55
+ * @returns {{ id: string, expiresAt: string }}
56
+ */
57
+ function createBreakGlass(policyPath, { requestedBy, reason, durationMinutes = 15 }) {
58
+ const policy = readJsonSafe(policyPath) || { breakGlass: [] };
59
+
60
+ const expiresAt = new Date(Date.now() + durationMinutes * 60 * 1000).toISOString();
61
+ const id = `bg-${Date.now()}`;
62
+
63
+ const entry = {
64
+ id,
65
+ requestedBy,
66
+ reason,
67
+ createdAt: new Date().toISOString(),
68
+ expiresAt,
69
+ active: true,
70
+ };
71
+
72
+ if (!policy.breakGlass) policy.breakGlass = [];
73
+ policy.breakGlass.push(entry);
74
+ writeJsonSafe(policyPath, policy);
75
+
76
+ return { id, expiresAt };
77
+ }
78
+
79
+ /**
80
+ * Verifica se existe um break-glass ativo e válido.
81
+ * @param {string} policyPath — caminho do arquivo de políticas
82
+ * @returns {{ active: boolean, entry?: object }}
83
+ */
84
+ function checkBreakGlass(policyPath) {
85
+ const policy = readJsonSafe(policyPath);
86
+ if (!policy || !policy.breakGlass) return { active: false };
87
+
88
+ const now = new Date();
89
+ const activeEntry = policy.breakGlass.find(
90
+ (bg) => bg.active && new Date(bg.expiresAt) > now
91
+ );
92
+
93
+ if (activeEntry) {
94
+ return { active: true, entry: activeEntry };
95
+ }
96
+
97
+ return { active: false };
98
+ }
99
+
100
+ /**
101
+ * Expira todos os break-glass vencidos.
102
+ * @param {string} policyPath — caminho do arquivo de políticas
103
+ * @returns {number} quantidade de break-glass expirados
104
+ */
105
+ function expireBreakGlass(policyPath) {
106
+ const policy = readJsonSafe(policyPath);
107
+ if (!policy || !policy.breakGlass) return 0;
108
+
109
+ const now = new Date();
110
+ let count = 0;
111
+
112
+ for (const bg of policy.breakGlass) {
113
+ if (bg.active && new Date(bg.expiresAt) <= now) {
114
+ bg.active = false;
115
+ count++;
116
+ }
117
+ }
118
+
119
+ if (count > 0) writeJsonSafe(policyPath, policy);
120
+ return count;
121
+ }
122
+
123
+ /**
124
+ * Retorna a allowlist de comandos permitidos para um perfil.
125
+ * @param {string} profile — nome do perfil
126
+ * @returns {string[]} lista de padrões de comando permitidos
127
+ */
128
+ function getAllowedCommands(profile) {
129
+ const allowlists = {
130
+ viewer: [],
131
+ operator: [
132
+ "systemctl status *",
133
+ "docker ps",
134
+ "docker compose ps",
135
+ "wg show *",
136
+ "ping *",
137
+ "cat /var/log/*",
138
+ "tail -f /var/log/*",
139
+ ],
140
+ admin: ["*"], // Admin pode tudo (com confirmação)
141
+ };
142
+
143
+ return allowlists[profile] || [];
144
+ }
145
+
146
+ module.exports = {
147
+ PROFILES,
148
+ hasPermission,
149
+ createBreakGlass,
150
+ checkBreakGlass,
151
+ expireBreakGlass,
152
+ getAllowedCommands,
153
+ };
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: File Transfer Safe
5
+ *
6
+ * Transferência de arquivos via VPN com allowlist de diretórios,
7
+ * hashing, limites de tamanho e auditoria.
8
+ *
9
+ * Referência: skills/openclaw-ops/05-openclaw-file-transfer-safe/SKILL.md
10
+ */
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const crypto = require("crypto");
15
+
16
+ // Tamanho máximo padrão: 50MB
17
+ const DEFAULT_MAX_SIZE = 50 * 1024 * 1024;
18
+
19
+ /**
20
+ * Calcula o SHA-256 de um arquivo.
21
+ * @param {string} filePath — caminho do arquivo
22
+ * @returns {string} hash hex
23
+ */
24
+ function hashFile(filePath) {
25
+ const content = fs.readFileSync(filePath);
26
+ return crypto.createHash("sha256").update(content).digest("hex");
27
+ }
28
+
29
+ /**
30
+ * Verifica se um caminho está dentro da allowlist.
31
+ * @param {string} filePath — caminho do arquivo
32
+ * @param {string[]} allowedDirs — diretórios permitidos
33
+ * @returns {{ allowed: boolean, reason?: string }}
34
+ */
35
+ function checkAllowlist(filePath, allowedDirs) {
36
+ const resolved = path.resolve(filePath);
37
+
38
+ for (const dir of allowedDirs) {
39
+ const resolvedDir = path.resolve(dir);
40
+ if (resolved.startsWith(resolvedDir + path.sep) || resolved === resolvedDir) {
41
+ return { allowed: true };
42
+ }
43
+ }
44
+
45
+ return {
46
+ allowed: false,
47
+ reason: `Caminho '${resolved}' fora da allowlist de diretórios`,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Verifica se o arquivo não excede o tamanho máximo.
53
+ * @param {string} filePath — caminho do arquivo
54
+ * @param {number} [maxSize] — tamanho máximo em bytes
55
+ * @returns {{ allowed: boolean, size: number, reason?: string }}
56
+ */
57
+ function checkFileSize(filePath, maxSize = DEFAULT_MAX_SIZE) {
58
+ const stats = fs.statSync(filePath);
59
+ if (stats.size > maxSize) {
60
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
61
+ const maxMB = (maxSize / 1024 / 1024).toFixed(2);
62
+ return {
63
+ allowed: false,
64
+ size: stats.size,
65
+ reason: `Arquivo (${sizeMB}MB) excede o limite de ${maxMB}MB`,
66
+ };
67
+ }
68
+ return { allowed: true, size: stats.size };
69
+ }
70
+
71
+ /**
72
+ * Executa transferência segura de arquivo com validações.
73
+ * @param {object} params
74
+ * @param {string} params.source — caminho do arquivo fonte
75
+ * @param {string} params.destination — caminho do destino
76
+ * @param {string[]} params.allowedDirs — diretórios permitidos
77
+ * @param {number} [params.maxSize] — tamanho máximo em bytes
78
+ * @param {string} params.operator — quem está fazendo a transferência
79
+ * @returns {{ success: boolean, auditEntry: object, error?: string }}
80
+ */
81
+ function transferFile({ source, destination, allowedDirs, maxSize, operator }) {
82
+ const auditEntry = {
83
+ action: "file.transfer",
84
+ operator,
85
+ source: path.resolve(source),
86
+ destination: path.resolve(destination),
87
+ timestamp: new Date().toISOString(),
88
+ hashBefore: null,
89
+ hashAfter: null,
90
+ size: null,
91
+ success: false,
92
+ };
93
+
94
+ // 1. Verificar se o arquivo fonte existe
95
+ if (!fs.existsSync(source)) {
96
+ auditEntry.error = "Arquivo fonte não encontrado";
97
+ return { success: false, auditEntry, error: auditEntry.error };
98
+ }
99
+
100
+ // 2. Verificar allowlist para fonte e destino
101
+ const srcCheck = checkAllowlist(source, allowedDirs);
102
+ if (!srcCheck.allowed) {
103
+ auditEntry.error = `Fonte: ${srcCheck.reason}`;
104
+ return { success: false, auditEntry, error: auditEntry.error };
105
+ }
106
+
107
+ const destCheck = checkAllowlist(destination, allowedDirs);
108
+ if (!destCheck.allowed) {
109
+ auditEntry.error = `Destino: ${destCheck.reason}`;
110
+ return { success: false, auditEntry, error: auditEntry.error };
111
+ }
112
+
113
+ // 3. Verificar tamanho
114
+ const sizeCheck = checkFileSize(source, maxSize);
115
+ if (!sizeCheck.allowed) {
116
+ auditEntry.error = sizeCheck.reason;
117
+ auditEntry.size = sizeCheck.size;
118
+ return { success: false, auditEntry, error: auditEntry.error };
119
+ }
120
+ auditEntry.size = sizeCheck.size;
121
+
122
+ // 4. Hash antes da cópia
123
+ auditEntry.hashBefore = hashFile(source);
124
+
125
+ // 5. Copiar o arquivo
126
+ const destDir = path.dirname(destination);
127
+ if (!fs.existsSync(destDir)) {
128
+ fs.mkdirSync(destDir, { recursive: true });
129
+ }
130
+ fs.copyFileSync(source, destination);
131
+
132
+ // 6. Hash depois da cópia (verificar integridade)
133
+ auditEntry.hashAfter = hashFile(destination);
134
+
135
+ if (auditEntry.hashBefore !== auditEntry.hashAfter) {
136
+ auditEntry.error = "Integridade comprometida: hash pré/pós não confere";
137
+ // Remove arquivo corrompido
138
+ fs.unlinkSync(destination);
139
+ return { success: false, auditEntry, error: auditEntry.error };
140
+ }
141
+
142
+ auditEntry.success = true;
143
+ return { success: true, auditEntry };
144
+ }
145
+
146
+ module.exports = {
147
+ hashFile,
148
+ checkAllowlist,
149
+ checkFileSize,
150
+ transferFile,
151
+ DEFAULT_MAX_SIZE,
152
+ };
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: Safe Update
5
+ *
6
+ * Atualização segura com verificação (hash/assinatura),
7
+ * canary e rollback automático.
8
+ *
9
+ * Referência: skills/openclaw-ops/07-openclaw-safe-update/SKILL.md
10
+ */
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const { execSync } = require("child_process");
15
+ const crypto = require("crypto");
16
+
17
+ /**
18
+ * Cria um snapshot de backup antes do update.
19
+ * @param {string} targetDir — diretório a fazer backup
20
+ * @param {string} backupDir — diretório de backups
21
+ * @returns {{ backupPath: string, timestamp: string }}
22
+ */
23
+ function createSnapshot(targetDir, backupDir) {
24
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
25
+ const backupPath = path.join(backupDir, `snapshot-${timestamp}`);
26
+
27
+ if (!fs.existsSync(backupDir)) {
28
+ fs.mkdirSync(backupDir, { recursive: true });
29
+ }
30
+
31
+ // Cópia recursiva do diretório
32
+ copyDirRecursive(targetDir, backupPath);
33
+
34
+ return { backupPath, timestamp };
35
+ }
36
+
37
+ /**
38
+ * Copia diretório recursivamente.
39
+ * @param {string} src — fonte
40
+ * @param {string} dest — destino
41
+ */
42
+ function copyDirRecursive(src, dest) {
43
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
44
+
45
+ const entries = fs.readdirSync(src, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const srcPath = path.join(src, entry.name);
48
+ const destPath = path.join(dest, entry.name);
49
+
50
+ if (entry.isDirectory()) {
51
+ copyDirRecursive(srcPath, destPath);
52
+ } else {
53
+ fs.copyFileSync(srcPath, destPath);
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Restaura um snapshot (rollback).
60
+ * @param {string} backupPath — caminho do backup
61
+ * @param {string} targetDir — diretório alvo para restaurar
62
+ * @returns {{ success: boolean, error?: string }}
63
+ */
64
+ function rollback(backupPath, targetDir) {
65
+ try {
66
+ if (!fs.existsSync(backupPath)) {
67
+ return { success: false, error: `Backup não encontrado: ${backupPath}` };
68
+ }
69
+
70
+ // Remove o diretório atual
71
+ fs.rmSync(targetDir, { recursive: true, force: true });
72
+
73
+ // Restaura do backup
74
+ copyDirRecursive(backupPath, targetDir);
75
+
76
+ return { success: true };
77
+ } catch (err) {
78
+ return { success: false, error: err.message };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Verifica hash de integridade de um arquivo/diretório.
84
+ * @param {string} filePath — caminho do arquivo
85
+ * @param {string} expectedHash — hash SHA-256 esperado
86
+ * @returns {{ valid: boolean, actualHash: string }}
87
+ */
88
+ function verifyHash(filePath, expectedHash) {
89
+ const content = fs.readFileSync(filePath);
90
+ const actualHash = crypto.createHash("sha256").update(content).digest("hex");
91
+ return { valid: actualHash === expectedHash, actualHash };
92
+ }
93
+
94
+ /**
95
+ * Executa um healthcheck pós-update.
96
+ * @param {object} [checks] — funções de verificação customizáveis
97
+ * @param {function} [checks.configValid] — verifica se config é válida
98
+ * @param {function} [checks.serviceRunning] — verifica se serviço está rodando
99
+ * @returns {{ healthy: boolean, results: object[] }}
100
+ */
101
+ function postUpdateHealthcheck(checks = {}) {
102
+ const results = [];
103
+
104
+ // Check 1: Configuração válida
105
+ if (checks.configValid) {
106
+ try {
107
+ const ok = checks.configValid();
108
+ results.push({ name: "config", status: ok ? "ok" : "fail" });
109
+ } catch (err) {
110
+ results.push({ name: "config", status: "fail", error: err.message });
111
+ }
112
+ }
113
+
114
+ // Check 2: Serviço rodando
115
+ if (checks.serviceRunning) {
116
+ try {
117
+ const ok = checks.serviceRunning();
118
+ results.push({ name: "service", status: ok ? "ok" : "fail" });
119
+ } catch (err) {
120
+ results.push({ name: "service", status: "fail", error: err.message });
121
+ }
122
+ }
123
+
124
+ const healthy = results.every((r) => r.status === "ok");
125
+ return { healthy, results };
126
+ }
127
+
128
+ /**
129
+ * Fluxo completo de safe update com canary e rollback.
130
+ * @param {object} params
131
+ * @param {string} params.targetDir — diretório a atualizar
132
+ * @param {string} params.backupDir — diretório de backups
133
+ * @param {function} params.applyUpdate — função que aplica o update
134
+ * @param {object} [params.healthchecks] — funções de verificação
135
+ * @returns {{ success: boolean, backup: string, error?: string }}
136
+ */
137
+ async function safeUpdate({ targetDir, backupDir, applyUpdate, healthchecks = {} }) {
138
+ // 1. Snapshot/backup
139
+ const { backupPath } = createSnapshot(targetDir, backupDir);
140
+
141
+ // 2. Aplicar update
142
+ try {
143
+ await applyUpdate();
144
+ } catch (err) {
145
+ // Rollback imediato
146
+ rollback(backupPath, targetDir);
147
+ return { success: false, backup: backupPath, error: `Update falhou: ${err.message}. Rollback aplicado.` };
148
+ }
149
+
150
+ // 3. Healthcheck pós-update
151
+ const health = postUpdateHealthcheck(healthchecks);
152
+
153
+ if (!health.healthy) {
154
+ // Rollback automático
155
+ rollback(backupPath, targetDir);
156
+ const failedChecks = health.results.filter((r) => r.status === "fail").map((r) => r.name);
157
+ return {
158
+ success: false,
159
+ backup: backupPath,
160
+ error: `Healthcheck falhou (${failedChecks.join(", ")}). Rollback aplicado.`,
161
+ };
162
+ }
163
+
164
+ return { success: true, backup: backupPath };
165
+ }
166
+
167
+ module.exports = {
168
+ createSnapshot,
169
+ rollback,
170
+ verifyHash,
171
+ postUpdateHealthcheck,
172
+ safeUpdate,
173
+ };
package/lib/ops/vpn.js ADDED
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Script operacional: VPN WireGuard
5
+ *
6
+ * Provisiona e verifica WireGuard entre VPS e hosts.
7
+ * Princípio: sem VPN, sem acesso remoto.
8
+ *
9
+ * Referência: skills/openclaw-ops/01-openclaw-vpn-wireguard/SKILL.md
10
+ */
11
+
12
+ const { execSync } = require("child_process");
13
+ const crypto = require("crypto");
14
+
15
+ /**
16
+ * Verifica se o WireGuard está instalado.
17
+ * @returns {{ installed: boolean, version?: string }}
18
+ */
19
+ function checkInstalled() {
20
+ try {
21
+ const output = execSync("wg --version", { encoding: "utf8", timeout: 5000 }).trim();
22
+ return { installed: true, version: output };
23
+ } catch {
24
+ return { installed: false };
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Verifica o status da interface WireGuard (wg0).
30
+ * @returns {{ active: boolean, peers: number, latestHandshake?: string }}
31
+ */
32
+ function getInterfaceStatus() {
33
+ try {
34
+ const output = execSync("wg show wg0", { encoding: "utf8", timeout: 5000 });
35
+ const peers = (output.match(/peer:/g) || []).length;
36
+ const handshakeMatch = output.match(/latest handshake:\s*(.+)/);
37
+
38
+ return {
39
+ active: true,
40
+ peers,
41
+ latestHandshake: handshakeMatch ? handshakeMatch[1].trim() : undefined,
42
+ };
43
+ } catch {
44
+ return { active: false, peers: 0 };
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Gera par de chaves WireGuard.
50
+ * @returns {{ privateKey: string, publicKey: string }}
51
+ */
52
+ function generateKeyPair() {
53
+ try {
54
+ const privateKey = execSync("wg genkey", { encoding: "utf8", timeout: 5000 }).trim();
55
+ const publicKey = execSync(`echo "${privateKey}" | wg pubkey`, {
56
+ encoding: "utf8",
57
+ timeout: 5000,
58
+ shell: true,
59
+ }).trim();
60
+ return { privateKey, publicKey };
61
+ } catch {
62
+ // Fallback: gera chaves com crypto (para ambientes sem wg)
63
+ const key = crypto.randomBytes(32);
64
+ return {
65
+ privateKey: key.toString("base64"),
66
+ publicKey: crypto.createHash("sha256").update(key).digest("base64"),
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Gera configuração WireGuard para a VPS (servidor).
73
+ * @param {object} params
74
+ * @param {string} params.privateKey — chave privada do servidor
75
+ * @param {string} params.listenPort — porta UDP (padrão: 51820)
76
+ * @param {string} params.address — endereço IP da VPN (padrão: 10.60.0.1/24)
77
+ * @returns {string} conteúdo do arquivo wg0.conf
78
+ */
79
+ function generateServerConfig({ privateKey, listenPort = "51820", address = "10.60.0.1/24" }) {
80
+ return `[Interface]
81
+ Address = ${address}
82
+ ListenPort = ${listenPort}
83
+ PrivateKey = ${privateKey}
84
+
85
+ # PostUp/PostDown para firewall (ajuste conforme sua interface de rede)
86
+ # PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
87
+ # PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
88
+ `;
89
+ }
90
+
91
+ /**
92
+ * Gera configuração de peer para adicionar à VPS.
93
+ * @param {object} params
94
+ * @param {string} params.publicKey — chave pública do peer
95
+ * @param {string} params.allowedIPs — IPs permitidos (ex: 10.60.0.2/32)
96
+ * @returns {string} bloco [Peer] para wg0.conf
97
+ */
98
+ function generatePeerBlock({ publicKey, allowedIPs }) {
99
+ return `
100
+ [Peer]
101
+ PublicKey = ${publicKey}
102
+ AllowedIPs = ${allowedIPs}
103
+ `;
104
+ }
105
+
106
+ /**
107
+ * Valida conectividade dentro da VPN via ping.
108
+ * @param {string} ip — IP para pingar (ex: 10.60.0.1)
109
+ * @returns {{ reachable: boolean, latency?: string }}
110
+ */
111
+ function validateConnectivity(ip) {
112
+ try {
113
+ const output = execSync(`ping -c 2 -W 3 ${ip}`, { encoding: "utf8", timeout: 10000 });
114
+ const latencyMatch = output.match(/time=(\d+\.?\d*)/);
115
+ return {
116
+ reachable: true,
117
+ latency: latencyMatch ? `${latencyMatch[1]}ms` : undefined,
118
+ };
119
+ } catch {
120
+ return { reachable: false };
121
+ }
122
+ }
123
+
124
+ module.exports = {
125
+ checkInstalled,
126
+ getInterfaceStatus,
127
+ generateKeyPair,
128
+ generateServerConfig,
129
+ generatePeerBlock,
130
+ validateConnectivity,
131
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Módulo de segurança para o OpenClaw Setup Wizard.
3
+ * Contém verificação de portas, mascaramento de segredos e geração de tokens.
4
+ *
5
+ * @module lib/security
6
+ */
7
+ const crypto = require("crypto");
8
+ const net = require("net");
9
+
10
+ /**
11
+ * Verifica se uma porta está em uso em um host específico.
12
+ * Usa timeout curto (600ms) para não travar o wizard.
13
+ * @param {string} host - Endereço do host (ex: "127.0.0.1")
14
+ * @param {number} port - Número da porta
15
+ * @returns {Promise<boolean>} true se a porta respondeu (em uso)
16
+ */
17
+ function portInUse(host, port) {
18
+ return new Promise((resolve) => {
19
+ const socket = new net.Socket();
20
+ socket.setTimeout(600);
21
+ socket.once("error", () => resolve(false));
22
+ socket.once("timeout", () => { socket.destroy(); resolve(false); });
23
+ socket.connect(port, host, () => { socket.end(); resolve(true); });
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Mascara um segredo para exibição segura (logs/console).
29
+ * Mostra apenas os 3 primeiros e 3 últimos caracteres.
30
+ * @param {string} s - String secreta a ser mascarada
31
+ * @returns {string} String mascarada (ex: "abc…xyz") ou "***" se curta
32
+ */
33
+ function mask(s) {
34
+ if (!s) return "";
35
+ if (s.length <= 6) return "***";
36
+ return s.slice(0, 3) + "…" + s.slice(-3);
37
+ }
38
+
39
+ /**
40
+ * Gera um token de autenticação seguro usando crypto.randomBytes.
41
+ * Produz 48 caracteres hexadecimais (24 bytes de entropia).
42
+ * @returns {string} Token hexadecimal de 48 caracteres
43
+ */
44
+ function generateToken() {
45
+ return crypto.randomBytes(24).toString("hex");
46
+ }
47
+
48
+ module.exports = { portInUse, mask, generateToken };