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.
Files changed (186) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/build/core/auto-installer.d.ts +102 -0
  5. package/build/core/auto-installer.d.ts.map +1 -0
  6. package/build/core/auto-installer.js +833 -0
  7. package/build/core/backup-manager.d.ts +63 -0
  8. package/build/core/backup-manager.d.ts.map +1 -0
  9. package/build/core/backup-manager.js +189 -0
  10. package/build/core/changelog.d.ts +75 -0
  11. package/build/core/changelog.d.ts.map +1 -0
  12. package/build/core/changelog.js +123 -0
  13. package/build/core/command-allowlist.d.ts +129 -0
  14. package/build/core/command-allowlist.d.ts.map +1 -0
  15. package/build/core/command-allowlist.js +849 -0
  16. package/build/core/config.d.ts +79 -0
  17. package/build/core/config.d.ts.map +1 -0
  18. package/build/core/config.js +193 -0
  19. package/build/core/dependency-validator.d.ts +106 -0
  20. package/build/core/dependency-validator.d.ts.map +1 -0
  21. package/build/core/dependency-validator.js +405 -0
  22. package/build/core/distro-adapter.d.ts +177 -0
  23. package/build/core/distro-adapter.d.ts.map +1 -0
  24. package/build/core/distro-adapter.js +481 -0
  25. package/build/core/distro.d.ts +68 -0
  26. package/build/core/distro.d.ts.map +1 -0
  27. package/build/core/distro.js +457 -0
  28. package/build/core/encrypted-state.d.ts +76 -0
  29. package/build/core/encrypted-state.d.ts.map +1 -0
  30. package/build/core/encrypted-state.js +209 -0
  31. package/build/core/executor.d.ts +56 -0
  32. package/build/core/executor.d.ts.map +1 -0
  33. package/build/core/executor.js +350 -0
  34. package/build/core/installer.d.ts +92 -0
  35. package/build/core/installer.d.ts.map +1 -0
  36. package/build/core/installer.js +1072 -0
  37. package/build/core/logger.d.ts +102 -0
  38. package/build/core/logger.d.ts.map +1 -0
  39. package/build/core/logger.js +132 -0
  40. package/build/core/parsers.d.ts +151 -0
  41. package/build/core/parsers.d.ts.map +1 -0
  42. package/build/core/parsers.js +479 -0
  43. package/build/core/policy-engine.d.ts +170 -0
  44. package/build/core/policy-engine.d.ts.map +1 -0
  45. package/build/core/policy-engine.js +656 -0
  46. package/build/core/preflight.d.ts +157 -0
  47. package/build/core/preflight.d.ts.map +1 -0
  48. package/build/core/preflight.js +638 -0
  49. package/build/core/privilege-manager.d.ts +108 -0
  50. package/build/core/privilege-manager.d.ts.map +1 -0
  51. package/build/core/privilege-manager.js +363 -0
  52. package/build/core/rate-limiter.d.ts +67 -0
  53. package/build/core/rate-limiter.d.ts.map +1 -0
  54. package/build/core/rate-limiter.js +129 -0
  55. package/build/core/rollback.d.ts +73 -0
  56. package/build/core/rollback.d.ts.map +1 -0
  57. package/build/core/rollback.js +278 -0
  58. package/build/core/safeguards.d.ts +58 -0
  59. package/build/core/safeguards.d.ts.map +1 -0
  60. package/build/core/safeguards.js +448 -0
  61. package/build/core/sanitizer.d.ts +118 -0
  62. package/build/core/sanitizer.d.ts.map +1 -0
  63. package/build/core/sanitizer.js +459 -0
  64. package/build/core/secure-fs.d.ts +67 -0
  65. package/build/core/secure-fs.d.ts.map +1 -0
  66. package/build/core/secure-fs.js +143 -0
  67. package/build/core/spawn-safe.d.ts +55 -0
  68. package/build/core/spawn-safe.d.ts.map +1 -0
  69. package/build/core/spawn-safe.js +146 -0
  70. package/build/core/sudo-guard.d.ts +145 -0
  71. package/build/core/sudo-guard.d.ts.map +1 -0
  72. package/build/core/sudo-guard.js +349 -0
  73. package/build/core/sudo-session.d.ts +100 -0
  74. package/build/core/sudo-session.d.ts.map +1 -0
  75. package/build/core/sudo-session.js +319 -0
  76. package/build/core/tool-dependencies.d.ts +61 -0
  77. package/build/core/tool-dependencies.d.ts.map +1 -0
  78. package/build/core/tool-dependencies.js +571 -0
  79. package/build/core/tool-registry.d.ts +111 -0
  80. package/build/core/tool-registry.d.ts.map +1 -0
  81. package/build/core/tool-registry.js +656 -0
  82. package/build/core/tool-wrapper.d.ts +73 -0
  83. package/build/core/tool-wrapper.d.ts.map +1 -0
  84. package/build/core/tool-wrapper.js +296 -0
  85. package/build/index.d.ts +3 -0
  86. package/build/index.d.ts.map +1 -0
  87. package/build/index.js +247 -0
  88. package/build/tools/access-control.d.ts +9 -0
  89. package/build/tools/access-control.d.ts.map +1 -0
  90. package/build/tools/access-control.js +1818 -0
  91. package/build/tools/api-security.d.ts +12 -0
  92. package/build/tools/api-security.d.ts.map +1 -0
  93. package/build/tools/api-security.js +901 -0
  94. package/build/tools/app-hardening.d.ts +11 -0
  95. package/build/tools/app-hardening.d.ts.map +1 -0
  96. package/build/tools/app-hardening.js +768 -0
  97. package/build/tools/backup.d.ts +8 -0
  98. package/build/tools/backup.d.ts.map +1 -0
  99. package/build/tools/backup.js +381 -0
  100. package/build/tools/cloud-security.d.ts +17 -0
  101. package/build/tools/cloud-security.d.ts.map +1 -0
  102. package/build/tools/cloud-security.js +739 -0
  103. package/build/tools/compliance.d.ts +10 -0
  104. package/build/tools/compliance.d.ts.map +1 -0
  105. package/build/tools/compliance.js +1225 -0
  106. package/build/tools/container-security.d.ts +14 -0
  107. package/build/tools/container-security.d.ts.map +1 -0
  108. package/build/tools/container-security.js +788 -0
  109. package/build/tools/deception.d.ts +13 -0
  110. package/build/tools/deception.d.ts.map +1 -0
  111. package/build/tools/deception.js +763 -0
  112. package/build/tools/dns-security.d.ts +93 -0
  113. package/build/tools/dns-security.d.ts.map +1 -0
  114. package/build/tools/dns-security.js +745 -0
  115. package/build/tools/drift-detection.d.ts +8 -0
  116. package/build/tools/drift-detection.d.ts.map +1 -0
  117. package/build/tools/drift-detection.js +326 -0
  118. package/build/tools/ebpf-security.d.ts +15 -0
  119. package/build/tools/ebpf-security.d.ts.map +1 -0
  120. package/build/tools/ebpf-security.js +294 -0
  121. package/build/tools/encryption.d.ts +9 -0
  122. package/build/tools/encryption.d.ts.map +1 -0
  123. package/build/tools/encryption.js +1667 -0
  124. package/build/tools/firewall.d.ts +9 -0
  125. package/build/tools/firewall.d.ts.map +1 -0
  126. package/build/tools/firewall.js +1398 -0
  127. package/build/tools/hardening.d.ts +10 -0
  128. package/build/tools/hardening.d.ts.map +1 -0
  129. package/build/tools/hardening.js +2654 -0
  130. package/build/tools/ids.d.ts +9 -0
  131. package/build/tools/ids.d.ts.map +1 -0
  132. package/build/tools/ids.js +624 -0
  133. package/build/tools/incident-response.d.ts +10 -0
  134. package/build/tools/incident-response.d.ts.map +1 -0
  135. package/build/tools/incident-response.js +1180 -0
  136. package/build/tools/logging.d.ts +12 -0
  137. package/build/tools/logging.d.ts.map +1 -0
  138. package/build/tools/logging.js +454 -0
  139. package/build/tools/malware.d.ts +10 -0
  140. package/build/tools/malware.d.ts.map +1 -0
  141. package/build/tools/malware.js +532 -0
  142. package/build/tools/meta.d.ts +11 -0
  143. package/build/tools/meta.d.ts.map +1 -0
  144. package/build/tools/meta.js +2278 -0
  145. package/build/tools/network-defense.d.ts +12 -0
  146. package/build/tools/network-defense.d.ts.map +1 -0
  147. package/build/tools/network-defense.js +760 -0
  148. package/build/tools/patch-management.d.ts +3 -0
  149. package/build/tools/patch-management.d.ts.map +1 -0
  150. package/build/tools/patch-management.js +708 -0
  151. package/build/tools/process-security.d.ts +12 -0
  152. package/build/tools/process-security.d.ts.map +1 -0
  153. package/build/tools/process-security.js +784 -0
  154. package/build/tools/reporting.d.ts +11 -0
  155. package/build/tools/reporting.d.ts.map +1 -0
  156. package/build/tools/reporting.js +559 -0
  157. package/build/tools/secrets.d.ts +9 -0
  158. package/build/tools/secrets.d.ts.map +1 -0
  159. package/build/tools/secrets.js +596 -0
  160. package/build/tools/siem-integration.d.ts +18 -0
  161. package/build/tools/siem-integration.d.ts.map +1 -0
  162. package/build/tools/siem-integration.js +754 -0
  163. package/build/tools/sudo-management.d.ts +18 -0
  164. package/build/tools/sudo-management.d.ts.map +1 -0
  165. package/build/tools/sudo-management.js +737 -0
  166. package/build/tools/supply-chain-security.d.ts +8 -0
  167. package/build/tools/supply-chain-security.d.ts.map +1 -0
  168. package/build/tools/supply-chain-security.js +256 -0
  169. package/build/tools/threat-intel.d.ts +22 -0
  170. package/build/tools/threat-intel.d.ts.map +1 -0
  171. package/build/tools/threat-intel.js +749 -0
  172. package/build/tools/vulnerability-management.d.ts +11 -0
  173. package/build/tools/vulnerability-management.d.ts.map +1 -0
  174. package/build/tools/vulnerability-management.js +667 -0
  175. package/build/tools/waf.d.ts +12 -0
  176. package/build/tools/waf.d.ts.map +1 -0
  177. package/build/tools/waf.js +843 -0
  178. package/build/tools/wireless-security.d.ts +19 -0
  179. package/build/tools/wireless-security.d.ts.map +1 -0
  180. package/build/tools/wireless-security.js +826 -0
  181. package/build/tools/zero-trust-network.d.ts +8 -0
  182. package/build/tools/zero-trust-network.d.ts.map +1 -0
  183. package/build/tools/zero-trust-network.js +367 -0
  184. package/docs/SAFEGUARDS.md +518 -0
  185. package/docs/TOOLS-REFERENCE.md +665 -0
  186. 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
+ }