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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SudoGuard — Central module for detecting permission failures and generating
|
|
3
|
+
* structured elevation prompts that instruct the AI client to ask the user
|
|
4
|
+
* for their sudo password.
|
|
5
|
+
*
|
|
6
|
+
* This module ensures that no MCP tool ever silently fails due to missing
|
|
7
|
+
* sudo privileges. Instead, failures are intercepted and converted into
|
|
8
|
+
* clear, actionable elevation prompts.
|
|
9
|
+
*
|
|
10
|
+
* ## Three Interception Layers
|
|
11
|
+
*
|
|
12
|
+
* 1. **Pre-flight** (tool-wrapper.ts): Tools with `sudo: "always"` are blocked
|
|
13
|
+
* before execution if no session is active. SudoGuard generates the prompt.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Executor** (executor.ts): After command execution, the `permissionDenied`
|
|
16
|
+
* flag on {@link CommandResult} is set when stderr/exit code match known
|
|
17
|
+
* permission-denied patterns.
|
|
18
|
+
*
|
|
19
|
+
* 3. **Post-execution** (tool-wrapper.ts): If a tool handler's response
|
|
20
|
+
* indicates a permission error (detected via output text analysis),
|
|
21
|
+
* SudoGuard wraps it with an elevation prompt.
|
|
22
|
+
*
|
|
23
|
+
* ## Usage
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { SudoGuard } from './sudo-guard.js';
|
|
27
|
+
*
|
|
28
|
+
* // Check if command output indicates permission denied
|
|
29
|
+
* if (SudoGuard.isPermissionError(result.stderr, result.exitCode)) {
|
|
30
|
+
* return SudoGuard.createElevationPrompt('firewall_iptables_add');
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @module sudo-guard
|
|
35
|
+
*/
|
|
36
|
+
import { SudoSession } from "./sudo-session.js";
|
|
37
|
+
import { lstatSync } from "node:fs";
|
|
38
|
+
// ── Permission Error Detection ───────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Patterns in stderr/stdout that indicate a permission/privilege failure.
|
|
41
|
+
* These are matched case-insensitively against combined output.
|
|
42
|
+
*
|
|
43
|
+
* Covers sudo, polkit, systemd, Docker, iptables, and general POSIX errors.
|
|
44
|
+
*/
|
|
45
|
+
const PERMISSION_ERROR_PATTERNS = [
|
|
46
|
+
// sudo-specific
|
|
47
|
+
/sudo[:\s].*password/i,
|
|
48
|
+
/sudo[:\s].*required/i,
|
|
49
|
+
/a password is required/i,
|
|
50
|
+
/sorry,?\s+try again/i,
|
|
51
|
+
/\bsudo\b.*\bnot allowed\b/i,
|
|
52
|
+
/no password.*and.*not.*(sudoers|allowed)/i,
|
|
53
|
+
// General POSIX permission errors
|
|
54
|
+
/permission denied/i,
|
|
55
|
+
/operation not permitted/i,
|
|
56
|
+
/EACCES/,
|
|
57
|
+
/EPERM/,
|
|
58
|
+
/access denied/i,
|
|
59
|
+
// Specific binary errors
|
|
60
|
+
/must be run as root/i,
|
|
61
|
+
/must be root/i,
|
|
62
|
+
/requires? root/i,
|
|
63
|
+
/requires? superuser/i,
|
|
64
|
+
/run.*as.*root/i,
|
|
65
|
+
/need to be root/i,
|
|
66
|
+
/insufficient privileges?/i,
|
|
67
|
+
/not enough privileges?/i,
|
|
68
|
+
/only root can/i,
|
|
69
|
+
/you must be root/i,
|
|
70
|
+
// iptables / nftables
|
|
71
|
+
/can't initialize iptables/i,
|
|
72
|
+
/iptables.*Permission denied/i,
|
|
73
|
+
/nft.*Operation not permitted/i,
|
|
74
|
+
// systemd / service management
|
|
75
|
+
/polkit.*authorization/i,
|
|
76
|
+
/interactive authentication required/i,
|
|
77
|
+
/access denied by.*policy/i,
|
|
78
|
+
/not privileged/i,
|
|
79
|
+
// Docker
|
|
80
|
+
/docker.*permission denied/i,
|
|
81
|
+
/connect: permission denied/i,
|
|
82
|
+
/dial.*permission denied/i,
|
|
83
|
+
// Package management
|
|
84
|
+
/are you root\?/i,
|
|
85
|
+
/unable to lock/i,
|
|
86
|
+
/could not get lock/i,
|
|
87
|
+
// auditd
|
|
88
|
+
/audit.*permission/i,
|
|
89
|
+
// File system
|
|
90
|
+
/cannot open.*permission denied/i,
|
|
91
|
+
/cannot write.*permission denied/i,
|
|
92
|
+
/read-only file system/i,
|
|
93
|
+
];
|
|
94
|
+
/**
|
|
95
|
+
* Exit codes that commonly indicate permission failures.
|
|
96
|
+
* Note: exit code alone is not sufficient — must be combined with pattern
|
|
97
|
+
* matching for reliable detection.
|
|
98
|
+
*/
|
|
99
|
+
const PERMISSION_EXIT_CODES = new Set([
|
|
100
|
+
1, // General error (common for sudo failures)
|
|
101
|
+
126, // Command invoked cannot execute (permission issue)
|
|
102
|
+
4, // iptables: resource problem (often permission)
|
|
103
|
+
77, // BSD/systemd: noperm
|
|
104
|
+
]);
|
|
105
|
+
// ── SudoGuard ────────────────────────────────────────────────────────────────
|
|
106
|
+
/**
|
|
107
|
+
* Static utility class for permission error detection and elevation prompt
|
|
108
|
+
* generation. All methods are stateless and can be called directly.
|
|
109
|
+
*/
|
|
110
|
+
export class SudoGuard {
|
|
111
|
+
/**
|
|
112
|
+
* Check whether command output (stderr and/or stdout) indicates a
|
|
113
|
+
* permission/privilege failure.
|
|
114
|
+
*
|
|
115
|
+
* Uses a combination of pattern matching against known error messages
|
|
116
|
+
* and exit code analysis. Pattern matching alone is authoritative —
|
|
117
|
+
* exit codes are used as supporting evidence only.
|
|
118
|
+
*
|
|
119
|
+
* @param output Combined stderr + stdout text to analyze
|
|
120
|
+
* @param exitCode The process exit code (optional, for confidence)
|
|
121
|
+
* @returns `true` if the output indicates a permission error
|
|
122
|
+
*/
|
|
123
|
+
static isPermissionError(output, exitCode) {
|
|
124
|
+
if (!output || output.length === 0) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
// Check patterns against combined output
|
|
128
|
+
for (const pattern of PERMISSION_ERROR_PATTERNS) {
|
|
129
|
+
if (pattern.test(output)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Exit code alone is not sufficient (too many false positives),
|
|
134
|
+
// but exit code 126 is very specific to permission issues
|
|
135
|
+
if (exitCode === 126) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create a structured MCP elevation prompt response.
|
|
142
|
+
*
|
|
143
|
+
* The response includes:
|
|
144
|
+
* - A clear human-readable message explaining what happened
|
|
145
|
+
* - Instructions to call `sudo_elevate` with the user's password
|
|
146
|
+
* - Machine-readable `_meta` for client-side automation
|
|
147
|
+
*
|
|
148
|
+
* @param toolName The tool that requires elevation
|
|
149
|
+
* @param reason Optional specific reason (from manifest or error output)
|
|
150
|
+
* @param originalError The original error message to include for context
|
|
151
|
+
*/
|
|
152
|
+
static createElevationPrompt(toolName, reason, originalError) {
|
|
153
|
+
const session = SudoSession.getInstance();
|
|
154
|
+
const status = session.getStatus();
|
|
155
|
+
const reasonText = reason ?? "This tool requires elevated (root) privileges to function.";
|
|
156
|
+
// Build the prompt message — designed to be assertive enough that
|
|
157
|
+
// AI clients will HALT their workflow and ask the user for credentials.
|
|
158
|
+
const lines = [];
|
|
159
|
+
lines.push("🛑 WORKFLOW HALTED — ELEVATED PRIVILEGES REQUIRED");
|
|
160
|
+
lines.push("═".repeat(50));
|
|
161
|
+
lines.push("");
|
|
162
|
+
lines.push(`Tool: ${toolName}`);
|
|
163
|
+
lines.push(`Reason: ${reasonText}`);
|
|
164
|
+
lines.push("");
|
|
165
|
+
if (status.elevated && status.remainingSeconds !== null && status.remainingSeconds <= 0) {
|
|
166
|
+
// Session expired
|
|
167
|
+
lines.push("⚠️ Your sudo session has expired.");
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
lines.push("⚠️ DO NOT SKIP THIS TOOL OR CONTINUE WITHOUT ELEVATION.");
|
|
171
|
+
lines.push("⚠️ You MUST ask the user for their sudo password NOW.");
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push("ACTION REQUIRED — BEFORE PROCEEDING:");
|
|
174
|
+
lines.push("─".repeat(50));
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push("1. ASK the user for their sudo password");
|
|
177
|
+
lines.push("2. CALL sudo_elevate with the password");
|
|
178
|
+
lines.push("3. RETRY this tool (and any other tools that failed)");
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push(" Tool: sudo_elevate");
|
|
181
|
+
lines.push(" Parameter: password = <user's sudo password>");
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push("Once elevated, ALL privileged tools will work automatically");
|
|
184
|
+
lines.push("for the session duration (default: 15 minutes).");
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push("Security guarantees for the user's password:");
|
|
187
|
+
lines.push(" • Stored securely in a zeroable memory buffer");
|
|
188
|
+
lines.push(" • Never logged or exposed in any output");
|
|
189
|
+
lines.push(" • Auto-expires after the configured timeout");
|
|
190
|
+
lines.push(" • Can be dropped at any time with sudo_drop");
|
|
191
|
+
if (originalError) {
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("─".repeat(50));
|
|
194
|
+
lines.push("Original error:");
|
|
195
|
+
lines.push(originalError.substring(0, 500));
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: lines.join("\n"),
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
isError: true,
|
|
205
|
+
_meta: {
|
|
206
|
+
elevationRequired: true,
|
|
207
|
+
haltWorkflow: true,
|
|
208
|
+
failedTool: toolName,
|
|
209
|
+
reason: reasonText,
|
|
210
|
+
elevationTool: "sudo_elevate",
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if a tool handler's MCP response content indicates a permission
|
|
216
|
+
* error that occurred at runtime (after pre-flight passed).
|
|
217
|
+
*
|
|
218
|
+
* This catches `conditional` sudo tools and tools where the pre-flight
|
|
219
|
+
* check passed but the actual command still failed due to permissions.
|
|
220
|
+
*
|
|
221
|
+
* Examines the `content` array of the tool's response for text content
|
|
222
|
+
* matching permission error patterns.
|
|
223
|
+
*/
|
|
224
|
+
static isResponsePermissionError(response) {
|
|
225
|
+
if (!response)
|
|
226
|
+
return false;
|
|
227
|
+
// Only check error responses
|
|
228
|
+
if (!response.isError)
|
|
229
|
+
return false;
|
|
230
|
+
const content = response.content;
|
|
231
|
+
if (!Array.isArray(content))
|
|
232
|
+
return false;
|
|
233
|
+
for (const item of content) {
|
|
234
|
+
if (typeof item === "object" &&
|
|
235
|
+
item !== null &&
|
|
236
|
+
"type" in item &&
|
|
237
|
+
item.type === "text" &&
|
|
238
|
+
"text" in item &&
|
|
239
|
+
typeof item.text === "string") {
|
|
240
|
+
const text = item.text;
|
|
241
|
+
if (SudoGuard.isPermissionError(text)) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Extract the first text content string from an MCP response.
|
|
250
|
+
* Used to pass original error context to the elevation prompt.
|
|
251
|
+
*/
|
|
252
|
+
static extractResponseText(response) {
|
|
253
|
+
if (!response)
|
|
254
|
+
return undefined;
|
|
255
|
+
const content = response.content;
|
|
256
|
+
if (!Array.isArray(content))
|
|
257
|
+
return undefined;
|
|
258
|
+
for (const item of content) {
|
|
259
|
+
if (typeof item === "object" &&
|
|
260
|
+
item !== null &&
|
|
261
|
+
"type" in item &&
|
|
262
|
+
item.type === "text" &&
|
|
263
|
+
"text" in item) {
|
|
264
|
+
return item.text;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check if the current process has an active sudo session that can
|
|
271
|
+
* be used for privileged operations.
|
|
272
|
+
*/
|
|
273
|
+
static hasActiveSession() {
|
|
274
|
+
return SudoSession.getInstance().isElevated();
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* SECURITY (CORE-006): Validate the SUDO_ASKPASS environment variable.
|
|
278
|
+
*
|
|
279
|
+
* Before trusting the SUDO_ASKPASS path, verify:
|
|
280
|
+
* 1. The file exists and is a regular file (not a symlink)
|
|
281
|
+
* 2. Ownership is root or the current user
|
|
282
|
+
* 3. Permissions are restrictive (0o700 or 0o500 — no world/group access)
|
|
283
|
+
*
|
|
284
|
+
* @returns `{ valid: true }` if safe, or `{ valid: false, reason: string }` if not
|
|
285
|
+
*/
|
|
286
|
+
static validateAskpass() {
|
|
287
|
+
const askpassPath = process.env.SUDO_ASKPASS;
|
|
288
|
+
if (!askpassPath) {
|
|
289
|
+
return { valid: true }; // Not set — nothing to validate
|
|
290
|
+
}
|
|
291
|
+
return SudoGuard.validateAskpassPath(askpassPath);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* SECURITY (CORE-016): Validate an askpass helper path.
|
|
295
|
+
*
|
|
296
|
+
* Before trusting any askpass candidate, verify:
|
|
297
|
+
* 1. The file exists and is a regular file (not a symlink)
|
|
298
|
+
* 2. Ownership is root or the current user
|
|
299
|
+
* 3. Permissions are restrictive (no group/world access)
|
|
300
|
+
*
|
|
301
|
+
* @param askpassPath Absolute path to the askpass candidate
|
|
302
|
+
* @returns `{ valid: true }` if safe, or `{ valid: false, reason: string }` if not
|
|
303
|
+
*/
|
|
304
|
+
static validateAskpassPath(askpassPath) {
|
|
305
|
+
try {
|
|
306
|
+
// 1. Check with lstat (does NOT follow symlinks)
|
|
307
|
+
const lstats = lstatSync(askpassPath);
|
|
308
|
+
if (lstats.isSymbolicLink()) {
|
|
309
|
+
return {
|
|
310
|
+
valid: false,
|
|
311
|
+
reason: `Askpass path '${askpassPath}' is a symlink. Refusing to trust it.`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (!lstats.isFile()) {
|
|
315
|
+
return {
|
|
316
|
+
valid: false,
|
|
317
|
+
reason: `Askpass path '${askpassPath}' is not a regular file.`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// 2. Verify ownership: must be root (uid 0) or the current user
|
|
321
|
+
const currentUid = process.getuid?.() ?? -1;
|
|
322
|
+
if (lstats.uid !== 0 && lstats.uid !== currentUid) {
|
|
323
|
+
return {
|
|
324
|
+
valid: false,
|
|
325
|
+
reason: `Askpass '${askpassPath}' is owned by uid ${lstats.uid}, expected root (0) or current user (${currentUid}).`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// 3. Verify permissions: no group or world access (must be 0o700 or 0o500)
|
|
329
|
+
// Extract the permission bits (lower 9 bits of mode)
|
|
330
|
+
const perms = lstats.mode & 0o777;
|
|
331
|
+
const groupWorldBits = perms & 0o077;
|
|
332
|
+
if (groupWorldBits !== 0) {
|
|
333
|
+
return {
|
|
334
|
+
valid: false,
|
|
335
|
+
reason: `Askpass '${askpassPath}' has overly permissive mode 0o${perms.toString(8)}. ` +
|
|
336
|
+
`Expected no group/world access (e.g., 0700 or 0500).`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return { valid: true };
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
343
|
+
return {
|
|
344
|
+
valid: false,
|
|
345
|
+
reason: `Failed to verify askpass '${askpassPath}': ${msg}`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SudoSession — singleton that manages elevated privilege credentials.
|
|
3
|
+
*
|
|
4
|
+
* The MCP server runs non-interactively via stdio transport, so `sudo`
|
|
5
|
+
* cannot prompt for a password through a TTY. This module stores the
|
|
6
|
+
* user's password in a zeroable Buffer and transparently provides it
|
|
7
|
+
* to `sudo -S` via stdin piping in the executor.
|
|
8
|
+
*
|
|
9
|
+
* Security features:
|
|
10
|
+
* - Password stored in a Buffer and remains as Buffer through the entire
|
|
11
|
+
* stdin pipeline (never converted to a V8 string, can be zeroed)
|
|
12
|
+
* - Auto-expires after a configurable timeout (default 15 minutes)
|
|
13
|
+
* - Explicit `drop()` zeroes the buffer immediately
|
|
14
|
+
* - Process exit handler zeroes the buffer on shutdown
|
|
15
|
+
* - Validates credentials before storing (test with `sudo -S -v`)
|
|
16
|
+
* - Never logs or exposes the password in any output
|
|
17
|
+
*
|
|
18
|
+
* Child process spawning goes through spawn-safe.ts which enforces the
|
|
19
|
+
* command allowlist and shell: false without creating circular dependencies.
|
|
20
|
+
*/
|
|
21
|
+
export interface SudoSessionStatus {
|
|
22
|
+
elevated: boolean;
|
|
23
|
+
username: string | null;
|
|
24
|
+
expiresAt: string | null;
|
|
25
|
+
remainingSeconds: number | null;
|
|
26
|
+
}
|
|
27
|
+
export declare class SudoSession {
|
|
28
|
+
/** Password stored in a Buffer so we can zero it (not interned by V8). */
|
|
29
|
+
private passwordBuf;
|
|
30
|
+
/** Username that authenticated. */
|
|
31
|
+
private username;
|
|
32
|
+
/**
|
|
33
|
+
* SECURITY (CICD-028): OS-level user ID that created this session.
|
|
34
|
+
* Used for session isolation tracking. Defaults to process.getuid().
|
|
35
|
+
*
|
|
36
|
+
* NOTE: Concurrent multi-user sessions are NOT currently supported.
|
|
37
|
+
* This server should not be used in multi-tenant environments where
|
|
38
|
+
* multiple users share the same process. Each user should run their
|
|
39
|
+
* own server instance.
|
|
40
|
+
*/
|
|
41
|
+
private sessionUserId;
|
|
42
|
+
/** Timestamp (epoch ms) when the session expires. */
|
|
43
|
+
private expiresAt;
|
|
44
|
+
/** Handle for the auto-expiry timer. */
|
|
45
|
+
private expiryTimer;
|
|
46
|
+
/** Default session timeout in milliseconds (15 min). */
|
|
47
|
+
private defaultTimeoutMs;
|
|
48
|
+
private constructor();
|
|
49
|
+
/** Get the singleton instance. */
|
|
50
|
+
static getInstance(): SudoSession;
|
|
51
|
+
/**
|
|
52
|
+
* Reset the singleton instance (for testing only).
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
static resetInstance(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Set the session timeout in milliseconds.
|
|
58
|
+
* Only affects future `elevate()` calls.
|
|
59
|
+
*/
|
|
60
|
+
setDefaultTimeout(ms: number): void;
|
|
61
|
+
/**
|
|
62
|
+
* Attempt to elevate privileges by validating the given password.
|
|
63
|
+
*
|
|
64
|
+
* Runs `sudo -S -k -v` with the password piped on stdin.
|
|
65
|
+
* `-k` invalidates cached credentials so we always test our password.
|
|
66
|
+
* `-v` validates without running a command.
|
|
67
|
+
* `-S` reads password from stdin.
|
|
68
|
+
* `-p ""` suppresses the password prompt text.
|
|
69
|
+
*
|
|
70
|
+
* @returns result indicating success or failure with error message.
|
|
71
|
+
*/
|
|
72
|
+
elevate(password: string | Buffer, timeoutMs?: number): Promise<{
|
|
73
|
+
success: boolean;
|
|
74
|
+
error?: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Returns a **copy** of the password Buffer for piping to sudo -S,
|
|
78
|
+
* or null if not elevated.
|
|
79
|
+
*
|
|
80
|
+
* The caller MUST zero the returned Buffer with `.fill(0)` after use.
|
|
81
|
+
* A copy is returned so the original can be zeroed independently via `drop()`.
|
|
82
|
+
*/
|
|
83
|
+
getPassword(): Buffer | null;
|
|
84
|
+
/** Check whether we have an active elevated session. */
|
|
85
|
+
isElevated(): boolean;
|
|
86
|
+
/** Get current session status (safe to expose via MCP). */
|
|
87
|
+
getStatus(): SudoSessionStatus;
|
|
88
|
+
/**
|
|
89
|
+
* Drop elevated privileges immediately.
|
|
90
|
+
* Zeroes the password buffer and clears all session state.
|
|
91
|
+
*/
|
|
92
|
+
drop(): void;
|
|
93
|
+
/**
|
|
94
|
+
* Extend the session timeout by the given milliseconds (or the default).
|
|
95
|
+
*/
|
|
96
|
+
extend(extraMs?: number): boolean;
|
|
97
|
+
private storePassword;
|
|
98
|
+
private isExpired;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=sudo-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sudo-session.d.ts","sourceRoot":"","sources":["../../src/core/sudo-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAsED,qBAAa,WAAW;IACtB,0EAA0E;IAC1E,OAAO,CAAC,WAAW,CAAuB;IAE1C,mCAAmC;IACnC,OAAO,CAAC,QAAQ,CAAuB;IAEvC;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa,CAAuB;IAE5C,qDAAqD;IACrD,OAAO,CAAC,SAAS,CAAuB;IAExC,wCAAwC;IACxC,OAAO,CAAC,WAAW,CAA8C;IAEjE,wDAAwD;IACxD,OAAO,CAAC,gBAAgB,CAAkB;IAE1C,OAAO;IASP,kCAAkC;IAClC,MAAM,CAAC,WAAW,IAAI,WAAW;IAOjC;;;OAGG;IACH,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMnC;;;;;;;;;;OAUG;IACG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAuE3G;;;;;;OAMG;IACH,WAAW,IAAI,MAAM,GAAG,IAAI;IAc5B,wDAAwD;IACxD,UAAU,IAAI,OAAO;IAUrB,2DAA2D;IAC3D,SAAS,IAAI,iBAAiB;IAsB9B;;;OAGG;IACH,IAAI,IAAI,IAAI;IAwBZ;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAyBjC,OAAO,CAAC,aAAa;IA8BrB,OAAO,CAAC,SAAS;CAIlB"}
|