@eladtest/mcp
Version:
MCP server for shellfirm - provides interactive command validation with captcha
207 lines (181 loc) • 7.2 kB
text/typescript
import { exec } from 'child_process';
import { promisify } from 'util';
import { validateSplitCommandWithOptions } from './shellfirm-wasm.js';
import { BrowserChallenge, type ChallengeData } from './browser-challenge.js';
import type { ChallengeType } from './types.js';
import { log as mcpLog, toErrorObject } from './logger.js';
const execAsync = promisify(exec);
/**
* Command interceptor that forces ALL commands through MCP validation
*/
export class CommandInterceptor {
/**
* Intercept and validate command before execution
*/
static async interceptCommand(
command: string,
workingDirectory?: string,
challengeType: ChallengeType = 'confirm',
allowedSeverities?: string[],
environment?: Record<string, string>,
propagateProcessEnv: boolean = true
): Promise<{
allowed: boolean;
output?: string;
error?: string;
message: string;
}> {
void mcpLog('debug', 'interceptor', { message: 'Intercepting command', command });
try {
// Use WASM-based validation with options for proper severity filtering
const validationOptions = {
allowed_severities: allowedSeverities || [],
deny_pattern_ids: [], // Could be extended to support denied patterns
};
const validationResult = await validateSplitCommandWithOptions(command, validationOptions);
void mcpLog('debug', 'interceptor', {
message: 'WASM validation result',
should_challenge: validationResult.should_challenge,
should_deny: validationResult.should_deny,
matches: validationResult.matches.map(match => match.id)
});
if (!validationResult.should_challenge) {
// Safe command - execute directly
void mcpLog('info', 'interceptor', { message: 'Command is safe, executing' });
return await this.executeCommand(command, workingDirectory, environment, propagateProcessEnv);
}
// Command denied completely
if (validationResult.should_deny) {
void mcpLog('warning', 'interceptor', { message: 'Command denied by security policy' });
const descriptions = validationResult.matches.map(check => check.description).join(', ');
return {
allowed: false,
message: `Shellfirm MCP: Command denied by security policy. Reasons: ${descriptions}`,
error: 'Security policy violation'
};
}
// Risky command - require browser-based challenge verification
const descriptions = validationResult.matches.map(check => check.description).join(', ');
void mcpLog('notice', 'interceptor', { message: 'Risky command detected', patterns: descriptions });
// Check if this is a Block challenge - if so, block immediately
if (challengeType === 'block') {
void mcpLog('warning', 'interceptor', { message: 'Command blocked by security policy (Block challenge type)' });
return {
allowed: false,
message: `Shellfirm MCP: Command blocked by security policy. This command cannot be executed. Reasons: ${descriptions}`,
error: 'Command blocked by security policy'
};
}
void mcpLog('notice', 'interceptor', { message: 'Opening browser challenge for user verification' });
// Prepare challenge data
const challengeData: ChallengeData = {
command,
patterns: validationResult.matches.map(check => check.description),
severity: this.getHighestSeverity(validationResult.matches),
matches: validationResult.matches.map(check => ({
id: check.id,
severity: check.severity,
description: check.description
}))
};
try {
// Show browser challenge
const challengeResult = await BrowserChallenge.showChallenge(
challengeType,
challengeData,
60000 // 60 second timeout
);
if (challengeResult.approved) {
void mcpLog('info', 'interceptor', { message: 'User approved command through browser challenge' });
// User approved - execute the command
return await this.executeCommand(command, workingDirectory, environment, propagateProcessEnv);
} else {
void mcpLog('warning', 'interceptor', { message: 'User denied command or challenge failed' });
return {
allowed: false,
message: `Shellfirm MCP: Command denied by user. ${challengeResult.error || 'User chose not to approve the command.'}`,
error: 'User denial or challenge failure'
};
}
} catch (challengeError) {
void mcpLog('error', 'interceptor', { message: 'Browser challenge system error', error: toErrorObject(challengeError) });
return {
allowed: false,
message: `Shellfirm MCP: Challenge system error. Command blocked for security. Error: ${challengeError instanceof Error ? challengeError.message : 'Unknown error'}`,
error: 'Challenge system failure'
};
}
} catch (error) {
void mcpLog('error', 'interceptor', { message: 'Error in command interception', error: toErrorObject(error) });
return {
allowed: false,
message: `Command blocked due to error: ${error instanceof Error ? error.message : 'Unknown error'}`,
error: 'Interception error'
};
}
}
/**
* Get the highest severity level from a list of matches
*/
private static getHighestSeverity(matches: Array<{ severity?: string }>): string {
const severityOrder = ['low', 'medium', 'high', 'critical'];
let highestSeverity = 'medium';
for (const match of matches) {
const severity = match.severity || 'medium';
if (severityOrder.indexOf(severity) > severityOrder.indexOf(highestSeverity)) {
highestSeverity = severity;
}
}
return highestSeverity;
}
/**
* Execute command after validation
*/
private static async executeCommand(
command: string,
workingDirectory?: string,
environment?: Record<string, string>,
propagateProcessEnv: boolean = true
): Promise<{
allowed: boolean;
output?: string;
error?: string;
message: string;
}> {
try {
const options: { cwd?: string; env?: Record<string, string> } = {};
if (workingDirectory) {
options.cwd = workingDirectory;
}
// Configure environment propagation behavior
if (propagateProcessEnv) {
// Merge clean process.env with provided environment (if any)
const cleanProcessEnv = Object.fromEntries(
Object.entries(process.env).filter(([_, value]) => value !== undefined)
) as Record<string, string>;
options.env = { ...cleanProcessEnv, ...(environment || {}) };
} else {
// Do not propagate current process env; use only provided env (or empty)
options.env = { ...(environment || {}) };
}
// Clean the command by removing any trailing whitespace/newlines
const cleanCommand = command.trim();
const { stdout, stderr } = await execAsync(cleanCommand, options);
return {
allowed: true,
output: stdout.toString(),
error: stderr ? stderr.toString() : undefined,
message: 'Command executed successfully'
};
} catch (execError) {
const errorMessage = execError instanceof Error ? execError.message : 'Unknown execution error';
void mcpLog('error', 'interceptor', { message: 'Command execution failed', error: errorMessage });
return {
allowed: true, // Command was allowed but failed execution
output: '',
error: errorMessage,
message: 'Command was allowed but execution failed'
};
}
}
}