defense-mcp-server 0.6.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.
- package/CHANGELOG.md +471 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/build/core/auto-installer.d.ts +102 -0
- package/build/core/auto-installer.d.ts.map +1 -0
- package/build/core/auto-installer.js +833 -0
- package/build/core/backup-manager.d.ts +63 -0
- package/build/core/backup-manager.d.ts.map +1 -0
- package/build/core/backup-manager.js +189 -0
- package/build/core/changelog.d.ts +75 -0
- package/build/core/changelog.d.ts.map +1 -0
- package/build/core/changelog.js +123 -0
- package/build/core/command-allowlist.d.ts +129 -0
- package/build/core/command-allowlist.d.ts.map +1 -0
- package/build/core/command-allowlist.js +849 -0
- package/build/core/config.d.ts +79 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +193 -0
- package/build/core/dependency-validator.d.ts +106 -0
- package/build/core/dependency-validator.d.ts.map +1 -0
- package/build/core/dependency-validator.js +405 -0
- package/build/core/distro-adapter.d.ts +177 -0
- package/build/core/distro-adapter.d.ts.map +1 -0
- package/build/core/distro-adapter.js +481 -0
- package/build/core/distro.d.ts +68 -0
- package/build/core/distro.d.ts.map +1 -0
- package/build/core/distro.js +457 -0
- package/build/core/encrypted-state.d.ts +76 -0
- package/build/core/encrypted-state.d.ts.map +1 -0
- package/build/core/encrypted-state.js +209 -0
- package/build/core/executor.d.ts +56 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +350 -0
- package/build/core/installer.d.ts +92 -0
- package/build/core/installer.d.ts.map +1 -0
- package/build/core/installer.js +1072 -0
- package/build/core/logger.d.ts +102 -0
- package/build/core/logger.d.ts.map +1 -0
- package/build/core/logger.js +132 -0
- package/build/core/parsers.d.ts +151 -0
- package/build/core/parsers.d.ts.map +1 -0
- package/build/core/parsers.js +479 -0
- package/build/core/policy-engine.d.ts +170 -0
- package/build/core/policy-engine.d.ts.map +1 -0
- package/build/core/policy-engine.js +656 -0
- package/build/core/preflight.d.ts +157 -0
- package/build/core/preflight.d.ts.map +1 -0
- package/build/core/preflight.js +638 -0
- package/build/core/privilege-manager.d.ts +108 -0
- package/build/core/privilege-manager.d.ts.map +1 -0
- package/build/core/privilege-manager.js +363 -0
- package/build/core/rate-limiter.d.ts +67 -0
- package/build/core/rate-limiter.d.ts.map +1 -0
- package/build/core/rate-limiter.js +129 -0
- package/build/core/rollback.d.ts +73 -0
- package/build/core/rollback.d.ts.map +1 -0
- package/build/core/rollback.js +278 -0
- package/build/core/safeguards.d.ts +58 -0
- package/build/core/safeguards.d.ts.map +1 -0
- package/build/core/safeguards.js +448 -0
- package/build/core/sanitizer.d.ts +118 -0
- package/build/core/sanitizer.d.ts.map +1 -0
- package/build/core/sanitizer.js +459 -0
- package/build/core/secure-fs.d.ts +67 -0
- package/build/core/secure-fs.d.ts.map +1 -0
- package/build/core/secure-fs.js +143 -0
- package/build/core/spawn-safe.d.ts +55 -0
- package/build/core/spawn-safe.d.ts.map +1 -0
- package/build/core/spawn-safe.js +146 -0
- package/build/core/sudo-guard.d.ts +145 -0
- package/build/core/sudo-guard.d.ts.map +1 -0
- package/build/core/sudo-guard.js +349 -0
- package/build/core/sudo-session.d.ts +100 -0
- package/build/core/sudo-session.d.ts.map +1 -0
- package/build/core/sudo-session.js +319 -0
- package/build/core/tool-dependencies.d.ts +61 -0
- package/build/core/tool-dependencies.d.ts.map +1 -0
- package/build/core/tool-dependencies.js +571 -0
- package/build/core/tool-registry.d.ts +111 -0
- package/build/core/tool-registry.d.ts.map +1 -0
- package/build/core/tool-registry.js +656 -0
- package/build/core/tool-wrapper.d.ts +73 -0
- package/build/core/tool-wrapper.d.ts.map +1 -0
- package/build/core/tool-wrapper.js +296 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +247 -0
- package/build/tools/access-control.d.ts +9 -0
- package/build/tools/access-control.d.ts.map +1 -0
- package/build/tools/access-control.js +1818 -0
- package/build/tools/api-security.d.ts +12 -0
- package/build/tools/api-security.d.ts.map +1 -0
- package/build/tools/api-security.js +901 -0
- package/build/tools/app-hardening.d.ts +11 -0
- package/build/tools/app-hardening.d.ts.map +1 -0
- package/build/tools/app-hardening.js +768 -0
- package/build/tools/backup.d.ts +8 -0
- package/build/tools/backup.d.ts.map +1 -0
- package/build/tools/backup.js +381 -0
- package/build/tools/cloud-security.d.ts +17 -0
- package/build/tools/cloud-security.d.ts.map +1 -0
- package/build/tools/cloud-security.js +739 -0
- package/build/tools/compliance.d.ts +10 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +1225 -0
- package/build/tools/container-security.d.ts +14 -0
- package/build/tools/container-security.d.ts.map +1 -0
- package/build/tools/container-security.js +788 -0
- package/build/tools/deception.d.ts +13 -0
- package/build/tools/deception.d.ts.map +1 -0
- package/build/tools/deception.js +763 -0
- package/build/tools/dns-security.d.ts +93 -0
- package/build/tools/dns-security.d.ts.map +1 -0
- package/build/tools/dns-security.js +745 -0
- package/build/tools/drift-detection.d.ts +8 -0
- package/build/tools/drift-detection.d.ts.map +1 -0
- package/build/tools/drift-detection.js +326 -0
- package/build/tools/ebpf-security.d.ts +15 -0
- package/build/tools/ebpf-security.d.ts.map +1 -0
- package/build/tools/ebpf-security.js +294 -0
- package/build/tools/encryption.d.ts +9 -0
- package/build/tools/encryption.d.ts.map +1 -0
- package/build/tools/encryption.js +1667 -0
- package/build/tools/firewall.d.ts +9 -0
- package/build/tools/firewall.d.ts.map +1 -0
- package/build/tools/firewall.js +1398 -0
- package/build/tools/hardening.d.ts +10 -0
- package/build/tools/hardening.d.ts.map +1 -0
- package/build/tools/hardening.js +2654 -0
- package/build/tools/ids.d.ts +9 -0
- package/build/tools/ids.d.ts.map +1 -0
- package/build/tools/ids.js +624 -0
- package/build/tools/incident-response.d.ts +10 -0
- package/build/tools/incident-response.d.ts.map +1 -0
- package/build/tools/incident-response.js +1180 -0
- package/build/tools/logging.d.ts +12 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +454 -0
- package/build/tools/malware.d.ts +10 -0
- package/build/tools/malware.d.ts.map +1 -0
- package/build/tools/malware.js +532 -0
- package/build/tools/meta.d.ts +11 -0
- package/build/tools/meta.d.ts.map +1 -0
- package/build/tools/meta.js +2278 -0
- package/build/tools/network-defense.d.ts +12 -0
- package/build/tools/network-defense.d.ts.map +1 -0
- package/build/tools/network-defense.js +760 -0
- package/build/tools/patch-management.d.ts +3 -0
- package/build/tools/patch-management.d.ts.map +1 -0
- package/build/tools/patch-management.js +708 -0
- package/build/tools/process-security.d.ts +12 -0
- package/build/tools/process-security.d.ts.map +1 -0
- package/build/tools/process-security.js +784 -0
- package/build/tools/reporting.d.ts +11 -0
- package/build/tools/reporting.d.ts.map +1 -0
- package/build/tools/reporting.js +559 -0
- package/build/tools/secrets.d.ts +9 -0
- package/build/tools/secrets.d.ts.map +1 -0
- package/build/tools/secrets.js +596 -0
- package/build/tools/siem-integration.d.ts +18 -0
- package/build/tools/siem-integration.d.ts.map +1 -0
- package/build/tools/siem-integration.js +754 -0
- package/build/tools/sudo-management.d.ts +18 -0
- package/build/tools/sudo-management.d.ts.map +1 -0
- package/build/tools/sudo-management.js +737 -0
- package/build/tools/supply-chain-security.d.ts +8 -0
- package/build/tools/supply-chain-security.d.ts.map +1 -0
- package/build/tools/supply-chain-security.js +256 -0
- package/build/tools/threat-intel.d.ts +22 -0
- package/build/tools/threat-intel.d.ts.map +1 -0
- package/build/tools/threat-intel.js +749 -0
- package/build/tools/vulnerability-management.d.ts +11 -0
- package/build/tools/vulnerability-management.d.ts.map +1 -0
- package/build/tools/vulnerability-management.js +667 -0
- package/build/tools/waf.d.ts +12 -0
- package/build/tools/waf.d.ts.map +1 -0
- package/build/tools/waf.js +843 -0
- package/build/tools/wireless-security.d.ts +19 -0
- package/build/tools/wireless-security.d.ts.map +1 -0
- package/build/tools/wireless-security.js +826 -0
- package/build/tools/zero-trust-network.d.ts +8 -0
- package/build/tools/zero-trust-network.d.ts.map +1 -0
- package/build/tools/zero-trust-network.js +367 -0
- package/docs/SAFEGUARDS.md +518 -0
- package/docs/TOOLS-REFERENCE.md +665 -0
- package/package.json +87 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sudo privilege management tools for Kali Defense MCP Server.
|
|
3
|
+
*
|
|
4
|
+
* Registers 6 tools: sudo_elevate, sudo_elevate_gui, sudo_status, sudo_drop,
|
|
5
|
+
* sudo_extend, preflight_batch_check.
|
|
6
|
+
*
|
|
7
|
+
* These tools manage a secure in-process sudo session so that the user
|
|
8
|
+
* only needs to provide their password once. All subsequent `sudo`
|
|
9
|
+
* commands executed by other tools transparently receive the cached
|
|
10
|
+
* credentials via stdin piping.
|
|
11
|
+
*
|
|
12
|
+
* The `preflight_batch_check` tool allows AI clients to pre-check a list
|
|
13
|
+
* of tools before executing them, so they can request sudo elevation
|
|
14
|
+
* ONCE upfront rather than failing tool-by-tool.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { spawnSafe } from "../core/spawn-safe.js";
|
|
18
|
+
import { SudoSession } from "../core/sudo-session.js";
|
|
19
|
+
import { getConfig } from "../core/config.js";
|
|
20
|
+
import { createTextContent, createErrorContent, } from "../core/parsers.js";
|
|
21
|
+
import { invalidatePreflightCaches } from '../core/tool-wrapper.js';
|
|
22
|
+
import { PreflightEngine } from '../core/preflight.js';
|
|
23
|
+
import { ToolRegistry } from '../core/tool-registry.js';
|
|
24
|
+
// ── Registration entry point ───────────────────────────────────────────────
|
|
25
|
+
export function registerSudoManagementTools(server) {
|
|
26
|
+
// ── 1. sudo_elevate ──────────────────────────────────────────────────────
|
|
27
|
+
server.tool("sudo_elevate", "Elevate privileges by providing the sudo password once. All subsequent tools that require sudo will use the cached credentials automatically. The password is stored securely in memory and auto-expires after the configured timeout.", {
|
|
28
|
+
password: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.describe("The user's sudo password. Stored securely in a zeroable buffer and never logged."),
|
|
32
|
+
timeout_minutes: z
|
|
33
|
+
.number()
|
|
34
|
+
.min(1)
|
|
35
|
+
.max(480)
|
|
36
|
+
.optional()
|
|
37
|
+
.default(15)
|
|
38
|
+
.describe("Session timeout in minutes (default: 15, max: 480). Session auto-expires after this duration."),
|
|
39
|
+
}, async ({ password, timeout_minutes }) => {
|
|
40
|
+
try {
|
|
41
|
+
const session = SudoSession.getInstance();
|
|
42
|
+
// Check if already elevated
|
|
43
|
+
if (session.isElevated()) {
|
|
44
|
+
const status = session.getStatus();
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
createTextContent(`🔓 Already elevated as '${status.username}'.\n` +
|
|
48
|
+
`Session expires at: ${status.expiresAt ?? "never"}\n` +
|
|
49
|
+
`Remaining: ${status.remainingSeconds !== null ? `${status.remainingSeconds}s` : "∞"}\n\n` +
|
|
50
|
+
`Use sudo_drop to end the current session before re-elevating.`),
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const timeoutMs = timeout_minutes * 60 * 1000;
|
|
55
|
+
// Apply config-level timeout override if set
|
|
56
|
+
const config = getConfig();
|
|
57
|
+
if (config.sudoSessionTimeout) {
|
|
58
|
+
session.setDefaultTimeout(config.sudoSessionTimeout);
|
|
59
|
+
}
|
|
60
|
+
const result = await session.elevate(password, timeoutMs);
|
|
61
|
+
if (result.success) {
|
|
62
|
+
invalidatePreflightCaches();
|
|
63
|
+
const status = session.getStatus();
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
createTextContent(`🔓 Privileges elevated successfully!\n\n` +
|
|
67
|
+
` User: ${status.username}\n` +
|
|
68
|
+
` Expires: ${status.expiresAt ?? "never (running as root)"}\n` +
|
|
69
|
+
` Timeout: ${timeout_minutes} minutes\n\n` +
|
|
70
|
+
`All tools that require sudo will now work automatically.\n` +
|
|
71
|
+
`Use sudo_status to check session state, or sudo_drop to end early.`),
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
createErrorContent(`❌ Elevation failed: ${result.error}\n\n` +
|
|
78
|
+
`Please verify:\n` +
|
|
79
|
+
` 1. The password is correct\n` +
|
|
80
|
+
` 2. Your user has sudo privileges (is in the sudoers file)\n` +
|
|
81
|
+
` 3. sudo is installed and configured`),
|
|
82
|
+
],
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
return {
|
|
89
|
+
content: [createErrorContent(`Elevation error: ${msg}`)],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// ── 1b. sudo_elevate_gui ──────────────────────────────────────────────────
|
|
95
|
+
//
|
|
96
|
+
// Secure two-phase elevation flow:
|
|
97
|
+
// Phase 1: LLM launches zenity via execute_command, password goes to temp file
|
|
98
|
+
// (password NEVER appears in terminal output or LLM context)
|
|
99
|
+
// Phase 2: LLM calls this tool which reads file → elevates → securely wipes
|
|
100
|
+
//
|
|
101
|
+
// The password is NEVER visible to the LLM at any point.
|
|
102
|
+
server.tool("sudo_elevate_gui", "Elevate privileges using a secure native GUI password dialog. Opens a system password prompt (zenity/kdialog) so the password never appears in the chat. The password goes directly from the dialog to secure memory. Preferred over sudo_elevate for interactive sessions.", {
|
|
103
|
+
timeout_minutes: z
|
|
104
|
+
.number()
|
|
105
|
+
.min(1)
|
|
106
|
+
.max(480)
|
|
107
|
+
.optional()
|
|
108
|
+
.default(15)
|
|
109
|
+
.describe("Session timeout in minutes (default: 15, max: 480). Session auto-expires after this duration."),
|
|
110
|
+
}, async ({ timeout_minutes }) => {
|
|
111
|
+
try {
|
|
112
|
+
const fs = await import("node:fs");
|
|
113
|
+
const crypto = await import("node:crypto");
|
|
114
|
+
const session = SudoSession.getInstance();
|
|
115
|
+
const SUDO_PW_FILE = "/tmp/.kali-sudo-pw";
|
|
116
|
+
// Check if already elevated
|
|
117
|
+
if (session.isElevated()) {
|
|
118
|
+
const status = session.getStatus();
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
createTextContent(`🔓 Already elevated as '${status.username}'.\n` +
|
|
122
|
+
`Session expires at: ${status.expiresAt ?? "never"}\n` +
|
|
123
|
+
`Remaining: ${status.remainingSeconds !== null ? `${status.remainingSeconds}s` : "∞"}\n\n` +
|
|
124
|
+
`Use sudo_drop to end the current session before re-elevating.`),
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Check if the password file exists (Phase 2 of the two-phase flow)
|
|
129
|
+
if (fs.existsSync(SUDO_PW_FILE)) {
|
|
130
|
+
console.error("[sudo-gui] Phase 2: Reading password from secure temp file...");
|
|
131
|
+
// Verify file ownership and permissions for safety
|
|
132
|
+
const stat = fs.statSync(SUDO_PW_FILE);
|
|
133
|
+
const mode = (stat.mode & 0o777).toString(8);
|
|
134
|
+
if (mode !== "600") {
|
|
135
|
+
// Wipe insecure file
|
|
136
|
+
try {
|
|
137
|
+
const size = stat.size || 64;
|
|
138
|
+
fs.writeFileSync(SUDO_PW_FILE, crypto.randomBytes(size));
|
|
139
|
+
fs.unlinkSync(SUDO_PW_FILE);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
console.error("[sudo-gui] Failed to wipe insecure password file");
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
createErrorContent(`❌ Security error: Password file has insecure permissions (${mode}).\n` +
|
|
147
|
+
`Expected 600. File has been securely wiped.\n` +
|
|
148
|
+
`Please run the zenity command again.`),
|
|
149
|
+
],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Read password from file
|
|
154
|
+
let password;
|
|
155
|
+
try {
|
|
156
|
+
password = fs.readFileSync(SUDO_PW_FILE, "utf-8").trim();
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
createErrorContent(`❌ Could not read password file: ${err instanceof Error ? err.message : String(err)}`),
|
|
162
|
+
],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// Securely wipe the file IMMEDIATELY (before elevation attempt)
|
|
167
|
+
try {
|
|
168
|
+
const fileSize = Buffer.byteLength(password, "utf-8") + 16;
|
|
169
|
+
fs.writeFileSync(SUDO_PW_FILE, crypto.randomBytes(fileSize));
|
|
170
|
+
fs.writeFileSync(SUDO_PW_FILE, crypto.randomBytes(fileSize)); // Double overwrite
|
|
171
|
+
fs.unlinkSync(SUDO_PW_FILE);
|
|
172
|
+
console.error("[sudo-gui] Password file securely wiped (2x random overwrite + unlink)");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
try {
|
|
176
|
+
fs.unlinkSync(SUDO_PW_FILE);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
console.error("[sudo-gui] Failed to unlink password file during wipe fallback");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!password || password.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
createErrorContent(`❌ Empty password file. Please run the zenity command again and enter your password.`),
|
|
186
|
+
],
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Elevate using the captured password
|
|
191
|
+
const timeoutMs = timeout_minutes * 60 * 1000;
|
|
192
|
+
const config = getConfig();
|
|
193
|
+
if (config.sudoSessionTimeout) {
|
|
194
|
+
session.setDefaultTimeout(config.sudoSessionTimeout);
|
|
195
|
+
}
|
|
196
|
+
const result = await session.elevate(password, timeoutMs);
|
|
197
|
+
if (result.success) {
|
|
198
|
+
invalidatePreflightCaches();
|
|
199
|
+
const status = session.getStatus();
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
createTextContent(`🔓 Privileges elevated successfully!\n\n` +
|
|
203
|
+
` User: ${status.username}\n` +
|
|
204
|
+
` Expires: ${status.expiresAt ?? "never (running as root)"}\n` +
|
|
205
|
+
` Timeout: ${timeout_minutes} minutes\n` +
|
|
206
|
+
` Method: Secure GUI dialog (password never visible to AI)\n\n` +
|
|
207
|
+
`All tools that require sudo will now work automatically.\n` +
|
|
208
|
+
`Use sudo_status to check session state, or sudo_drop to end early.`),
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
createErrorContent(`❌ Elevation failed: ${result.error}\n\n` +
|
|
215
|
+
`The password file was securely wiped. Please try again.`),
|
|
216
|
+
],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Phase 1: No password file found — instruct the LLM to launch the GUI dialog
|
|
221
|
+
// Detect which GUI tool is available for the instruction
|
|
222
|
+
const guiTool = await detectGuiPasswordTool();
|
|
223
|
+
const toolCmd = guiTool
|
|
224
|
+
? `${guiTool.command} ${guiTool.args.map(a => `'${a}'`).join(" ")}`
|
|
225
|
+
: "zenity --password --title='Kali Defense — Sudo Authentication' --width=400";
|
|
226
|
+
return {
|
|
227
|
+
content: [
|
|
228
|
+
createTextContent(`🔐 SECURE ELEVATION — Step 1 of 2\n` +
|
|
229
|
+
`${"═".repeat(50)}\n\n` +
|
|
230
|
+
`To elevate securely, run this command via execute_command:\n\n` +
|
|
231
|
+
` ${toolCmd} > /tmp/.kali-sudo-pw 2>/dev/null && chmod 600 /tmp/.kali-sudo-pw && echo "READY" || echo "CANCELLED"\n\n` +
|
|
232
|
+
`This opens a password dialog on the user's screen.\n` +
|
|
233
|
+
`The password goes DIRECTLY to a secure temp file — it\n` +
|
|
234
|
+
`NEVER appears in terminal output or the AI chat.\n\n` +
|
|
235
|
+
`After the user enters their password (output shows "READY"),\n` +
|
|
236
|
+
`call sudo_elevate_gui again to complete elevation.\n` +
|
|
237
|
+
`The tool will read the file, elevate, and securely wipe it.`),
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
243
|
+
return {
|
|
244
|
+
content: [createErrorContent(`GUI elevation error: ${msg}`)],
|
|
245
|
+
isError: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// ── 2. sudo_status ───────────────────────────────────────────────────────
|
|
250
|
+
server.tool("sudo_status", "Check the current sudo elevation status, including whether credentials are cached, the authenticated user, and remaining session time.", {}, async () => {
|
|
251
|
+
try {
|
|
252
|
+
const session = SudoSession.getInstance();
|
|
253
|
+
const status = session.getStatus();
|
|
254
|
+
if (!status.elevated) {
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
createTextContent(`🔒 Not elevated — sudo credentials are not cached.\n\n` +
|
|
258
|
+
`Use sudo_elevate to provide your password and enable\n` +
|
|
259
|
+
`transparent sudo for all defensive security tools.`),
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const sections = [];
|
|
264
|
+
sections.push("🔓 Sudo Session Active");
|
|
265
|
+
sections.push("=".repeat(40));
|
|
266
|
+
sections.push(` User: ${status.username}`);
|
|
267
|
+
sections.push(` Expires: ${status.expiresAt ?? "never (root)"}`);
|
|
268
|
+
if (status.remainingSeconds !== null) {
|
|
269
|
+
const mins = Math.floor(status.remainingSeconds / 60);
|
|
270
|
+
const secs = status.remainingSeconds % 60;
|
|
271
|
+
sections.push(` Remaining: ${mins}m ${secs}s`);
|
|
272
|
+
if (status.remainingSeconds < 120) {
|
|
273
|
+
sections.push(`\n ⚠️ Session expiring soon! Use sudo_elevate to re-authenticate.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
sections.push(` Remaining: ∞ (running as root)`);
|
|
278
|
+
}
|
|
279
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
283
|
+
return {
|
|
284
|
+
content: [createErrorContent(`Status check error: ${msg}`)],
|
|
285
|
+
isError: true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
// ── 3. sudo_drop ─────────────────────────────────────────────────────────
|
|
290
|
+
server.tool("sudo_drop", "Drop elevated privileges immediately. Zeroes the cached password from memory and invalidates the sudo session. Subsequent tools requiring sudo will fail until sudo_elevate is called again.", {}, async () => {
|
|
291
|
+
try {
|
|
292
|
+
const session = SudoSession.getInstance();
|
|
293
|
+
const wasElevated = session.isElevated();
|
|
294
|
+
const prevStatus = session.getStatus();
|
|
295
|
+
session.drop();
|
|
296
|
+
invalidatePreflightCaches();
|
|
297
|
+
if (wasElevated) {
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
createTextContent(`🔒 Privileges dropped successfully.\n\n` +
|
|
301
|
+
` Previous user: ${prevStatus.username}\n` +
|
|
302
|
+
` Password buffer: zeroed\n` +
|
|
303
|
+
` System sudo cache: invalidated\n\n` +
|
|
304
|
+
`Tools requiring sudo will now fail until sudo_elevate is called again.`),
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
content: [
|
|
310
|
+
createTextContent(`🔒 No active sudo session to drop.\n` +
|
|
311
|
+
`The system is already in an unprivileged state.`),
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
317
|
+
return {
|
|
318
|
+
content: [createErrorContent(`Drop error: ${msg}`)],
|
|
319
|
+
isError: true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
// ── 4. sudo_extend ────────────────────────────────────────────────────────
|
|
324
|
+
server.tool("sudo_extend", "Extend the current sudo session timeout. Resets the expiry timer so you don't have to re-authenticate. Requires an active sudo session (use sudo_elevate first).", {
|
|
325
|
+
minutes: z
|
|
326
|
+
.number()
|
|
327
|
+
.min(1)
|
|
328
|
+
.max(480)
|
|
329
|
+
.optional()
|
|
330
|
+
.default(15)
|
|
331
|
+
.describe("Number of minutes to extend the session by (default: 15, max: 480)."),
|
|
332
|
+
}, async ({ minutes }) => {
|
|
333
|
+
try {
|
|
334
|
+
const session = SudoSession.getInstance();
|
|
335
|
+
if (!session.isElevated()) {
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
createErrorContent(`🔒 No active sudo session to extend.\n\n` +
|
|
339
|
+
`Use sudo_elevate to provide your password and start a session first.`),
|
|
340
|
+
],
|
|
341
|
+
isError: true,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const extraMs = minutes * 60 * 1000;
|
|
345
|
+
const success = session.extend(extraMs);
|
|
346
|
+
if (success) {
|
|
347
|
+
invalidatePreflightCaches();
|
|
348
|
+
}
|
|
349
|
+
if (!success) {
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
createErrorContent(`Failed to extend sudo session. The session may have expired.\n` +
|
|
353
|
+
`Use sudo_elevate to re-authenticate.`),
|
|
354
|
+
],
|
|
355
|
+
isError: true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const status = session.getStatus();
|
|
359
|
+
return {
|
|
360
|
+
content: [
|
|
361
|
+
createTextContent(`🔓 Session extended by ${minutes} minutes.\n\n` +
|
|
362
|
+
` User: ${status.username}\n` +
|
|
363
|
+
` New expiry: ${status.expiresAt ?? "never (root)"}\n` +
|
|
364
|
+
` Remaining: ${status.remainingSeconds !== null ? `${Math.floor(status.remainingSeconds / 60)}m ${status.remainingSeconds % 60}s` : "∞"}`),
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
370
|
+
return {
|
|
371
|
+
content: [createErrorContent(`Extend error: ${msg}`)],
|
|
372
|
+
isError: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// ── 5. preflight_batch_check ──────────────────────────────────────────────
|
|
377
|
+
//
|
|
378
|
+
// Allows AI clients to pre-check a list of tools BEFORE executing them.
|
|
379
|
+
// Returns a consolidated report showing which tools will succeed, which
|
|
380
|
+
// need sudo, and which have missing dependencies — so the client can
|
|
381
|
+
// request elevation ONCE upfront instead of failing tool-by-tool.
|
|
382
|
+
server.tool("preflight_batch_check", "Pre-check multiple tools before executing them. Returns which tools are ready, which need sudo elevation, and which have missing dependencies. Call this BEFORE running a batch of audit tools so you can request sudo elevation once upfront rather than failing tool-by-tool.", {
|
|
383
|
+
tools: z
|
|
384
|
+
.array(z.string())
|
|
385
|
+
.min(1)
|
|
386
|
+
.max(100)
|
|
387
|
+
.describe("Array of tool names to pre-check (e.g., ['access_ssh_audit', 'patch_update_audit', 'harden_sysctl_audit'])"),
|
|
388
|
+
}, async ({ tools: toolNames }) => {
|
|
389
|
+
try {
|
|
390
|
+
const engine = PreflightEngine.instance();
|
|
391
|
+
const registry = ToolRegistry.instance();
|
|
392
|
+
const session = SudoSession.getInstance();
|
|
393
|
+
const results = [];
|
|
394
|
+
for (const toolName of toolNames) {
|
|
395
|
+
const manifest = registry.getManifest(toolName);
|
|
396
|
+
if (!manifest) {
|
|
397
|
+
results.push({
|
|
398
|
+
tool: toolName,
|
|
399
|
+
ready: false,
|
|
400
|
+
needsSudo: false,
|
|
401
|
+
missingDeps: [],
|
|
402
|
+
issues: [`Tool '${toolName}' not found in registry`],
|
|
403
|
+
});
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Run preflight (uses cache if available)
|
|
407
|
+
const preflight = await engine.runPreflight(toolName);
|
|
408
|
+
const missingDeps = preflight.dependencies.missing.map((d) => `${d.name} (${d.type})`);
|
|
409
|
+
const needsSudo = preflight.privileges.issues.some((i) => i.type === "sudo-required" ||
|
|
410
|
+
i.type === "sudo-unavailable" ||
|
|
411
|
+
i.type === "session-expired") ||
|
|
412
|
+
(manifest.sudo === "always" && !session.isElevated());
|
|
413
|
+
const issues = [];
|
|
414
|
+
for (const err of preflight.errors) {
|
|
415
|
+
issues.push(err);
|
|
416
|
+
}
|
|
417
|
+
results.push({
|
|
418
|
+
tool: toolName,
|
|
419
|
+
ready: preflight.passed,
|
|
420
|
+
needsSudo,
|
|
421
|
+
missingDeps,
|
|
422
|
+
sudoReason: manifest.sudoReason,
|
|
423
|
+
issues,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
// Categorize results
|
|
427
|
+
const ready = results.filter((r) => r.ready);
|
|
428
|
+
const needSudo = results.filter((r) => r.needsSudo && r.missingDeps.length === 0);
|
|
429
|
+
const needDeps = results.filter((r) => r.missingDeps.length > 0);
|
|
430
|
+
const otherFails = results.filter((r) => !r.ready && !r.needsSudo && r.missingDeps.length === 0);
|
|
431
|
+
// Build report
|
|
432
|
+
const lines = [];
|
|
433
|
+
lines.push("🔍 Pre-flight Batch Check Results");
|
|
434
|
+
lines.push("═".repeat(50));
|
|
435
|
+
lines.push(`Checked: ${toolNames.length} tools`);
|
|
436
|
+
lines.push(` ✅ Ready: ${ready.length}`);
|
|
437
|
+
lines.push(` 🔒 Need sudo: ${needSudo.length}`);
|
|
438
|
+
lines.push(` 📦 Missing deps: ${needDeps.length}`);
|
|
439
|
+
if (otherFails.length > 0) {
|
|
440
|
+
lines.push(` ❌ Other issues: ${otherFails.length}`);
|
|
441
|
+
}
|
|
442
|
+
// Section: Tools that need sudo elevation
|
|
443
|
+
if (needSudo.length > 0) {
|
|
444
|
+
lines.push("");
|
|
445
|
+
lines.push("🛑 SUDO ELEVATION REQUIRED");
|
|
446
|
+
lines.push("─".repeat(50));
|
|
447
|
+
lines.push("The following tools need sudo privileges.");
|
|
448
|
+
lines.push("Call sudo_elevate with the user's password BEFORE");
|
|
449
|
+
lines.push("executing any of these tools:");
|
|
450
|
+
lines.push("");
|
|
451
|
+
for (const r of needSudo) {
|
|
452
|
+
lines.push(` 🔒 ${r.tool}`);
|
|
453
|
+
if (r.sudoReason) {
|
|
454
|
+
lines.push(` Reason: ${r.sudoReason}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
lines.push("");
|
|
458
|
+
lines.push("→ Ask the user for their sudo password NOW,");
|
|
459
|
+
lines.push(" then call: sudo_elevate({ password: '<password>' })");
|
|
460
|
+
}
|
|
461
|
+
// Section: Missing dependencies
|
|
462
|
+
if (needDeps.length > 0) {
|
|
463
|
+
lines.push("");
|
|
464
|
+
lines.push("📦 MISSING DEPENDENCIES");
|
|
465
|
+
lines.push("─".repeat(50));
|
|
466
|
+
for (const r of needDeps) {
|
|
467
|
+
lines.push(` ❌ ${r.tool}`);
|
|
468
|
+
for (const dep of r.missingDeps) {
|
|
469
|
+
lines.push(` Missing: ${dep}`);
|
|
470
|
+
}
|
|
471
|
+
if (r.needsSudo) {
|
|
472
|
+
lines.push(` Also needs: sudo elevation`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Section: Ready tools
|
|
477
|
+
if (ready.length > 0) {
|
|
478
|
+
lines.push("");
|
|
479
|
+
lines.push("✅ READY TO EXECUTE");
|
|
480
|
+
lines.push("─".repeat(50));
|
|
481
|
+
for (const r of ready) {
|
|
482
|
+
lines.push(` ✅ ${r.tool}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Build machine-readable metadata
|
|
486
|
+
const meta = {
|
|
487
|
+
totalChecked: toolNames.length,
|
|
488
|
+
readyCount: ready.length,
|
|
489
|
+
needSudoCount: needSudo.length,
|
|
490
|
+
needDepsCount: needDeps.length,
|
|
491
|
+
needsSudoElevation: needSudo.length > 0,
|
|
492
|
+
toolsNeedingSudo: needSudo.map((r) => r.tool),
|
|
493
|
+
toolsReady: ready.map((r) => r.tool),
|
|
494
|
+
toolsMissingDeps: needDeps.map((r) => ({
|
|
495
|
+
tool: r.tool,
|
|
496
|
+
missing: r.missingDeps,
|
|
497
|
+
})),
|
|
498
|
+
};
|
|
499
|
+
if (needSudo.length > 0) {
|
|
500
|
+
meta.haltWorkflow = true;
|
|
501
|
+
meta.elevationTool = "sudo_elevate";
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
content: [createTextContent(lines.join("\n"))],
|
|
505
|
+
_meta: meta,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
510
|
+
return {
|
|
511
|
+
content: [createErrorContent(`Batch check error: ${msg}`)],
|
|
512
|
+
isError: true,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Detect which GUI password dialog tool is available.
|
|
519
|
+
* Preference order: zenity > kdialog > ssh-askpass
|
|
520
|
+
*/
|
|
521
|
+
async function detectGuiPasswordTool() {
|
|
522
|
+
const candidates = [
|
|
523
|
+
{
|
|
524
|
+
name: "zenity",
|
|
525
|
+
command: "zenity",
|
|
526
|
+
args: [
|
|
527
|
+
"--password",
|
|
528
|
+
"--title=Kali Defense — Sudo Authentication",
|
|
529
|
+
"--window-icon=dialog-password",
|
|
530
|
+
"--width=400",
|
|
531
|
+
],
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "kdialog",
|
|
535
|
+
command: "kdialog",
|
|
536
|
+
args: [
|
|
537
|
+
"--password",
|
|
538
|
+
"Kali Defense MCP Server requires sudo privileges.\nEnter your password to continue:",
|
|
539
|
+
"--title",
|
|
540
|
+
"Kali Defense — Sudo Authentication",
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
name: "ssh-askpass",
|
|
545
|
+
command: "ssh-askpass",
|
|
546
|
+
args: ["Kali Defense MCP Server requires sudo privileges. Enter password:"],
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
for (const tool of candidates) {
|
|
550
|
+
try {
|
|
551
|
+
const result = await new Promise((resolve) => {
|
|
552
|
+
const child = spawnSafe("which", [tool.command], {
|
|
553
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
554
|
+
});
|
|
555
|
+
child.on("close", (code) => resolve(code === 0));
|
|
556
|
+
child.on("error", () => resolve(false));
|
|
557
|
+
});
|
|
558
|
+
if (result)
|
|
559
|
+
return tool;
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Discover the graphical session environment by reading /proc/<pid>/environ
|
|
569
|
+
* from a known user desktop process. Falls back to the current process.env.
|
|
570
|
+
*/
|
|
571
|
+
async function getGraphicalSessionEnv() {
|
|
572
|
+
const base = { ...process.env };
|
|
573
|
+
try {
|
|
574
|
+
const { readFile } = await import("node:fs/promises");
|
|
575
|
+
const { execFileSafe } = await import("../core/spawn-safe.js");
|
|
576
|
+
// Find a PID from the user's graphical session (sddm-greeter, Xwayland, or the desktop itself)
|
|
577
|
+
const uid = process.getuid?.() ?? 1000;
|
|
578
|
+
// Get a graphical session process PID owned by the current user
|
|
579
|
+
let pid = null;
|
|
580
|
+
const candidates = ["sddm", "kwin_wayland", "plasmashell", "gnome-shell", "Xwayland", "xfce4-session"];
|
|
581
|
+
for (const proc of candidates) {
|
|
582
|
+
try {
|
|
583
|
+
const result = execFileSafe("pgrep", ["-u", String(uid), "-o", proc], { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
584
|
+
if (result) {
|
|
585
|
+
pid = result.split("\n")[0];
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!pid) {
|
|
594
|
+
console.error("[sudo-gui] No graphical session process found, using process.env");
|
|
595
|
+
return base;
|
|
596
|
+
}
|
|
597
|
+
console.error(`[sudo-gui] Reading session env from PID ${pid}`);
|
|
598
|
+
const environ = await readFile(`/proc/${pid}/environ`, "utf-8");
|
|
599
|
+
for (const entry of environ.split("\0")) {
|
|
600
|
+
const eqIdx = entry.indexOf("=");
|
|
601
|
+
if (eqIdx > 0) {
|
|
602
|
+
const key = entry.substring(0, eqIdx);
|
|
603
|
+
const val = entry.substring(eqIdx + 1);
|
|
604
|
+
// Only set missing or display-related keys
|
|
605
|
+
if (!base[key] || ["DISPLAY", "WAYLAND_DISPLAY", "XDG_RUNTIME_DIR",
|
|
606
|
+
"DBUS_SESSION_BUS_ADDRESS", "HOME", "USER", "XAUTHORITY",
|
|
607
|
+
"XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP"].includes(key)) {
|
|
608
|
+
base[key] = val;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
console.error(`[sudo-gui] Session env discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
615
|
+
}
|
|
616
|
+
return base;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Open a native GUI password dialog and return the entered password.
|
|
620
|
+
* Returns null if the user cancels the dialog.
|
|
621
|
+
* The password is captured directly in-process and never logged.
|
|
622
|
+
*
|
|
623
|
+
* Uses a temp-file approach: spawns zenity via `setsid` in a completely
|
|
624
|
+
* independent session, writing the password to a temp file. We poll
|
|
625
|
+
* asynchronously for the result to keep the Node.js event loop alive
|
|
626
|
+
* (critical — blocking the event loop kills the MCP server connection).
|
|
627
|
+
*/
|
|
628
|
+
async function openGuiPasswordDialog(tool) {
|
|
629
|
+
const fs = await import("node:fs");
|
|
630
|
+
const path = await import("node:path");
|
|
631
|
+
const crypto = await import("node:crypto");
|
|
632
|
+
// Get full graphical session environment so the dialog can display
|
|
633
|
+
const sessionEnv = await getGraphicalSessionEnv();
|
|
634
|
+
// Create a secure temp directory
|
|
635
|
+
let tmpDir;
|
|
636
|
+
try {
|
|
637
|
+
tmpDir = fs.mkdtempSync("/tmp/kali-sudo-gui-");
|
|
638
|
+
fs.chmodSync(tmpDir, 0o700);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
console.error("[sudo-gui] Failed to create temp dir");
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
const pwFile = path.join(tmpDir, "pw");
|
|
645
|
+
const doneFile = path.join(tmpDir, "done");
|
|
646
|
+
try {
|
|
647
|
+
// Write a self-contained helper script to the secure temp directory
|
|
648
|
+
// instead of passing interpolated strings to bash -c (TOOL-002 remediation).
|
|
649
|
+
const scriptPath = path.join(tmpDir, "gui-helper.sh");
|
|
650
|
+
// Build safe env export lines — only allow validated env var names
|
|
651
|
+
const envExports = ["#!/bin/sh"];
|
|
652
|
+
for (const [k, v] of Object.entries(sessionEnv)) {
|
|
653
|
+
if (v !== undefined && k !== "_" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) {
|
|
654
|
+
// Single-quote the value with proper escaping
|
|
655
|
+
envExports.push(`export ${k}='${v.replace(/'/g, "'\\''")}'`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Build command args with proper quoting — no template literal interpolation into shell
|
|
659
|
+
const quotedArgs = tool.args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
|
|
660
|
+
const scriptLines = [
|
|
661
|
+
...envExports,
|
|
662
|
+
`PW=$(setsid '${tool.command.replace(/'/g, "'\\''")}' ${quotedArgs} 2>/dev/null)`,
|
|
663
|
+
`RC=$?`,
|
|
664
|
+
`if [ $RC -eq 0 ] && [ -n "$PW" ]; then`,
|
|
665
|
+
` printf '%s' "$PW" > '${pwFile.replace(/'/g, "'\\''")}'`,
|
|
666
|
+
` chmod 600 '${pwFile.replace(/'/g, "'\\''")}'`,
|
|
667
|
+
`fi`,
|
|
668
|
+
`touch '${doneFile.replace(/'/g, "'\\''")}'`,
|
|
669
|
+
];
|
|
670
|
+
fs.writeFileSync(scriptPath, scriptLines.join("\n") + "\n", { mode: 0o700 });
|
|
671
|
+
// Launch the script directly via setsid — no bash -c with interpolated strings
|
|
672
|
+
const bg = spawnSafe("setsid", [scriptPath], {
|
|
673
|
+
stdio: "ignore",
|
|
674
|
+
detached: true,
|
|
675
|
+
env: sessionEnv,
|
|
676
|
+
});
|
|
677
|
+
bg.unref();
|
|
678
|
+
console.error("[sudo-gui] Launched password dialog, polling for result...");
|
|
679
|
+
// Poll for the done file asynchronously (non-blocking!)
|
|
680
|
+
const password = await new Promise((resolve) => {
|
|
681
|
+
let elapsed = 0;
|
|
682
|
+
const interval = setInterval(() => {
|
|
683
|
+
elapsed += 250;
|
|
684
|
+
if (elapsed > 60000) {
|
|
685
|
+
clearInterval(interval);
|
|
686
|
+
console.error("[sudo-gui] Dialog timed out after 60s");
|
|
687
|
+
resolve(null);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
// Check if done file exists
|
|
691
|
+
if (fs.existsSync(doneFile)) {
|
|
692
|
+
clearInterval(interval);
|
|
693
|
+
// Read password if it was written
|
|
694
|
+
if (fs.existsSync(pwFile)) {
|
|
695
|
+
try {
|
|
696
|
+
const pw = fs.readFileSync(pwFile, "utf-8");
|
|
697
|
+
// Zero the file on disk immediately
|
|
698
|
+
const len = Buffer.byteLength(pw, "utf-8");
|
|
699
|
+
fs.writeFileSync(pwFile, crypto.randomBytes(len));
|
|
700
|
+
fs.unlinkSync(pwFile);
|
|
701
|
+
console.error("[sudo-gui] Password captured from GUI dialog");
|
|
702
|
+
resolve(pw || null);
|
|
703
|
+
}
|
|
704
|
+
catch (err) {
|
|
705
|
+
console.error(`[sudo-gui] Read error: ${err instanceof Error ? err.message : String(err)}`);
|
|
706
|
+
resolve(null);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
console.error("[sudo-gui] Dialog cancelled (no password file)");
|
|
711
|
+
resolve(null);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}, 250);
|
|
715
|
+
});
|
|
716
|
+
return password;
|
|
717
|
+
}
|
|
718
|
+
catch (err) {
|
|
719
|
+
console.error(`[sudo-gui] Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
finally {
|
|
723
|
+
// Clean up temp dir
|
|
724
|
+
try {
|
|
725
|
+
fs.unlinkSync(pwFile);
|
|
726
|
+
}
|
|
727
|
+
catch { /* best-effort cleanup of sensitive temp file */ }
|
|
728
|
+
try {
|
|
729
|
+
fs.unlinkSync(doneFile);
|
|
730
|
+
}
|
|
731
|
+
catch { /* best-effort cleanup */ }
|
|
732
|
+
try {
|
|
733
|
+
fs.rmdirSync(tmpDir);
|
|
734
|
+
}
|
|
735
|
+
catch { /* best-effort cleanup */ }
|
|
736
|
+
}
|
|
737
|
+
}
|