Skip to main content
Glama
ooples

MCP Console Automation Server

PSExecProtocol.ts22 kB
import { spawn, ChildProcess } from 'child_process'; import { BaseProtocol } from '../core/BaseProtocol.js'; import { ConsoleSession, SessionOptions, ConsoleType, ConsoleOutput, } from '../types/index.js'; import { ProtocolCapabilities, SessionState, ErrorContext, ProtocolHealthStatus, ErrorRecoveryResult, ResourceUsage, } from '../core/IProtocol.js'; // PSExec Protocol connection options interface PSExecConnectionOptions extends SessionOptions { hostname: string; username?: string; password?: string; domain?: string; acceptEula?: boolean; copyToRemote?: boolean; runInteractive?: boolean; runAsSystem?: boolean; runAsCurrentUser?: boolean; runWithProfile?: boolean; runLowPrivilege?: boolean; runElevated?: boolean; workingDirectory?: string; remoteDirectory?: string; priority?: | 'low' | 'belownormal' | 'normal' | 'abovenormal' | 'high' | 'realtime'; affinity?: number; timeout?: number; waitForExit?: boolean; hideWindow?: boolean; noLogo?: boolean; verbose?: boolean; copyExclude?: string[]; copyInclude?: string[]; serviceAccount?: string; servicePassword?: string; sessionId?: number; enableSSL?: boolean; port?: number; ipVersion?: 'v4' | 'v6' | 'auto'; connectionRetries?: number; retryDelay?: number; enableDebug?: boolean; logLevel?: 'quiet' | 'normal' | 'verbose' | 'debug'; environment?: Record<string, string>; inputFile?: string; outputFile?: string; errorFile?: string; processName?: string; killOnTimeout?: boolean; forceKill?: boolean; createNoWindow?: boolean; useCredSSP?: boolean; useKerberos?: boolean; useNTLM?: boolean; encryptionLevel?: 'none' | 'low' | 'high' | 'fips'; compressionLevel?: number; bufferSize?: number; enableKeepAlive?: boolean; keepAliveInterval?: number; maxConnectionAttempts?: number; connectionTimeout?: number; executionTimeout?: number; enableAuditing?: boolean; auditLogPath?: string; enableEventLog?: boolean; eventLogSource?: string; enableWMI?: boolean; wmiNamespace?: string; enablePowerShell?: boolean; powerShellVersion?: string; runspace?: string; enableClr?: boolean; clrVersion?: string; assemblies?: string[]; modules?: string[]; snapins?: string[]; enableProxy?: boolean; proxyServer?: string; proxyPort?: number; proxyUsername?: string; proxyPassword?: string; enableFirewall?: boolean; firewallProfile?: 'domain' | 'private' | 'public' | 'all'; customPSExecPath?: string; customFlags?: string[]; } /** * PSExec Protocol Implementation * * Provides PSExec remote execution capabilities for Windows and cross-platform systems * Supports comprehensive PSExec functionality including remote command execution, * file copying, service management, system administration, and enterprise security features */ export class PSExecProtocol extends BaseProtocol { public readonly type: ConsoleType = 'psexec'; public readonly capabilities: ProtocolCapabilities; private psexecProcesses = new Map<string, ChildProcess>(); // Compatibility property for old ProtocolFactory interface public get healthStatus(): ProtocolHealthStatus { return { isHealthy: this.isInitialized, lastChecked: new Date(), errors: [], warnings: [], metrics: { activeSessions: this.sessions.size, totalSessions: this.sessions.size, averageLatency: 0, successRate: 100, uptime: 0, }, dependencies: {}, }; } constructor() { super('psexec'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: true, supportsX11Forwarding: false, supportsPortForwarding: false, supportsAuthentication: true, supportsEncryption: true, supportsCompression: true, supportsMultiplexing: false, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: true, supportsCustomEnvironment: true, supportsWorkingDirectory: true, supportsSignals: true, supportsResizing: false, supportsPTY: false, maxConcurrentSessions: 10, // PSExec can handle multiple remote sessions defaultTimeout: 120000, // Remote operations can take longer supportedEncodings: ['utf-8', 'utf-16', 'ascii'], supportedAuthMethods: ['ntlm', 'kerberos', 'credSSP'], platformSupport: { windows: true, // Primary PSExec platform linux: true, // Via Wine or alternatives macos: true, // Via Wine or alternatives freebsd: true, }, }; } async initialize(): Promise<void> { if (this.isInitialized) return; try { // Check if PSExec is available await this.checkPSExecAvailability(); this.isInitialized = true; this.logger.info('PSExec protocol initialized with enterprise features'); } catch (error: any) { this.logger.error('Failed to initialize PSExec protocol', error); throw error; } } async createSession(options: SessionOptions): Promise<ConsoleSession> { const sessionId = `psexec-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; return await this.createSessionWithTypeDetection(sessionId, options); } async dispose(): Promise<void> { await this.cleanup(); } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const fullCommand = args && args.length > 0 ? `${command} ${args.join(' ')}` : command; await this.sendInput(sessionId, fullCommand + '\r\n'); } async sendInput(sessionId: string, input: string): Promise<void> { const psexecProcess = this.psexecProcesses.get(sessionId); const session = this.sessions.get(sessionId); if (!psexecProcess || !psexecProcess.stdin || !session) { throw new Error(`No active PSExec session: ${sessionId}`); } psexecProcess.stdin.write(input); session.lastActivity = new Date(); this.emit('input-sent', { sessionId, input, timestamp: new Date(), }); this.logger.debug( `Sent input to PSExec session ${sessionId}: ${input.substring(0, 100)}` ); } async closeSession(sessionId: string): Promise<void> { try { const psexecProcess = this.psexecProcesses.get(sessionId); if (psexecProcess) { // Try graceful shutdown first psexecProcess.kill('SIGTERM'); // Force kill after timeout setTimeout(() => { if (psexecProcess && !psexecProcess.killed) { psexecProcess.kill('SIGKILL'); } }, 10000); this.psexecProcesses.delete(sessionId); } // Clean up base protocol session this.sessions.delete(sessionId); this.logger.info(`PSExec session ${sessionId} closed`); this.emit('session-closed', sessionId); } catch (error) { this.logger.error(`Error closing PSExec session ${sessionId}:`, error); throw error; } } async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const psexecOptions = options as PSExecConnectionOptions; // Validate required PSExec parameters if (!psexecOptions.hostname) { throw new Error('Hostname is required for PSExec protocol'); } // Build PSExec command const psexecCommand = this.buildPSExecCommand(psexecOptions); // Spawn PSExec process const psexecProcess = spawn(psexecCommand[0], psexecCommand.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(psexecOptions), ...options.env, }, }); // Set up output handling psexecProcess.stdout?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); psexecProcess.stderr?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); psexecProcess.on('error', (error) => { this.logger.error( `PSExec process error for session ${sessionId}:`, error ); this.emit('session-error', { sessionId, error }); }); psexecProcess.on('close', (code) => { this.logger.info( `PSExec process closed for session ${sessionId} with code ${code}` ); this.markSessionComplete(sessionId, code || 0); }); // Store the process this.psexecProcesses.set(sessionId, psexecProcess); // Create session object const session: ConsoleSession = { id: sessionId, command: psexecCommand[0], args: psexecCommand.slice(1), cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(psexecOptions), ...options.env, }, createdAt: new Date(), lastActivity: new Date(), status: 'running', type: this.type, streaming: options.streaming, executionState: 'idle', activeCommands: new Map(), pid: psexecProcess.pid, }; this.sessions.set(sessionId, session); this.logger.info( `PSExec session ${sessionId} created for host ${psexecOptions.hostname}` ); this.emit('session-created', { sessionId, type: 'psexec', session }); return session; } // Override getOutput to satisfy old ProtocolFactory interface (returns string) async getOutput(sessionId: string, since?: Date): Promise<any> { const outputs = await super.getOutput(sessionId, since); return outputs.map((output) => output.data).join(''); } // Missing IProtocol methods for compatibility getAllSessions(): ConsoleSession[] { return Array.from(this.sessions.values()); } getActiveSessions(): ConsoleSession[] { return Array.from(this.sessions.values()).filter( (session) => session.status === 'running' ); } getSessionCount(): number { return this.sessions.size; } async getSessionState(sessionId: string): Promise<SessionState> { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } return { sessionId, status: session.status, isOneShot: false, // PSExec sessions can be persistent for interactive use isPersistent: true, createdAt: session.createdAt, lastActivity: session.lastActivity, pid: session.pid, metadata: {}, }; } async handleError( error: Error, context: ErrorContext ): Promise<ErrorRecoveryResult> { this.logger.error( `Error in PSExec session ${context.sessionId}: ${error.message}` ); return { recovered: false, strategy: 'none', attempts: 0, duration: 0, error: error.message, }; } async recoverSession(sessionId: string): Promise<boolean> { const psexecProcess = this.psexecProcesses.get(sessionId); return (psexecProcess && !psexecProcess.killed) || false; } getResourceUsage(): ResourceUsage { const memUsage = process.memoryUsage(); const cpuUsage = process.cpuUsage(); return { memory: { used: memUsage.heapUsed, available: memUsage.heapTotal, peak: memUsage.heapTotal, }, cpu: { usage: cpuUsage.user + cpuUsage.system, load: [0, 0, 0], }, network: { bytesIn: 0, bytesOut: 0, connectionsActive: this.psexecProcesses.size, }, storage: { bytesRead: 0, bytesWritten: 0, }, sessions: { active: this.sessions.size, total: this.sessions.size, peak: this.sessions.size, }, }; } async getHealthStatus(): Promise<ProtocolHealthStatus> { const baseStatus = await super.getHealthStatus(); try { await this.checkPSExecAvailability(); return { ...baseStatus, dependencies: { psexec: { available: true }, }, }; } catch (error) { return { ...baseStatus, isHealthy: false, errors: [...baseStatus.errors, `PSExec not available: ${error}`], dependencies: { psexec: { available: false }, }, }; } } private async checkPSExecAvailability(): Promise<void> { return new Promise((resolve, reject) => { const testProcess = spawn('psexec', ['/accepteula', '-h'], { stdio: 'pipe', }); testProcess.on('close', (code) => { if (code === 0 || code === 1) { // PSExec returns 1 for help resolve(); } else { reject( new Error( 'PSExec tool not found. Please install PsTools from Microsoft Sysinternals.' ) ); } }); testProcess.on('error', () => { reject( new Error( 'PSExec tool not found. Please install PsTools from Microsoft Sysinternals.' ) ); }); }); } private buildPSExecCommand(options: PSExecConnectionOptions): string[] { const command = []; // PSExec executable if (options.customPSExecPath) { command.push(options.customPSExecPath); } else { command.push('psexec'); } // Accept EULA automatically if (options.acceptEula !== false) { command.push('/accepteula'); } // Target hostname command.push(`\\\\${options.hostname}`); // Authentication if (options.username) { command.push('-u', options.username); } if (options.password) { command.push('-p', options.password); } if (options.domain) { command.push('-d', options.domain); } // Execution options if (options.runInteractive) { command.push('-i'); if (options.sessionId !== undefined) { command.push('-i', options.sessionId.toString()); } } if (options.runAsSystem) { command.push('-s'); } if (options.runAsCurrentUser) { command.push('-e'); } if (options.runWithProfile) { command.push('-l'); } if (options.runLowPrivilege) { command.push('-x'); } if (options.runElevated) { command.push('-h'); } // Working directory if (options.workingDirectory) { command.push('-w', options.workingDirectory); } // Process priority if (options.priority) { const priorityMap = { low: '-low', belownormal: '-belownormal', normal: '-normal', abovenormal: '-abovenormal', high: '-high', realtime: '-realtime', }; command.push(priorityMap[options.priority]); } // Processor affinity if (options.affinity !== undefined) { command.push('-a', options.affinity.toString()); } // Copy options if (options.copyToRemote) { command.push('-c'); } if (options.copyExclude && options.copyExclude.length > 0) { command.push('-csrc', options.copyExclude.join(',')); } if (options.copyInclude && options.copyInclude.length > 0) { command.push('-clist', options.copyInclude.join(',')); } // File redirection if (options.inputFile) { command.push('<', options.inputFile); } if (options.outputFile) { command.push('>', options.outputFile); } if (options.errorFile) { command.push('2>', options.errorFile); } // Timeout if (options.timeout) { command.push('-n', options.timeout.toString()); } // Service account if (options.serviceAccount) { command.push('-r', options.serviceAccount); if (options.servicePassword) { command.push('-rp', options.servicePassword); } } // SSL/TLS if (options.enableSSL) { command.push('-encrypt'); } // Port if (options.port) { command.push('-port', options.port.toString()); } // IP version if (options.ipVersion === 'v4') { command.push('-4'); } else if (options.ipVersion === 'v6') { command.push('-6'); } // Authentication protocols if (options.useCredSSP) { command.push('-credssp'); } if (options.useKerberos) { command.push('-k'); } if (options.useNTLM) { command.push('-ntlm'); } // Window options if (options.hideWindow) { command.push('-b'); } if (options.createNoWindow) { command.push('-nowindow'); } // Verbosity if (options.verbose) { command.push('-v'); } if (options.noLogo) { command.push('-nologo'); } // Debug options if (options.enableDebug) { command.push('-debug'); } // Process management if (options.waitForExit) { command.push('-w'); } if (options.killOnTimeout) { command.push('-k'); } if (options.forceKill) { command.push('-f'); } // PowerShell integration if (options.enablePowerShell) { command.push('-powershell'); if (options.powerShellVersion) { command.push('-psversion', options.powerShellVersion); } } // CLR integration if (options.enableClr) { command.push('-clr'); if (options.clrVersion) { command.push('-clrversion', options.clrVersion); } } // WMI integration if (options.enableWMI) { command.push('-wmi'); if (options.wmiNamespace) { command.push('-wminamespace', options.wmiNamespace); } } // Proxy settings if (options.enableProxy && options.proxyServer) { command.push( '-proxy', `${options.proxyServer}:${options.proxyPort || 8080}` ); if (options.proxyUsername) { command.push('-proxyuser', options.proxyUsername); if (options.proxyPassword) { command.push('-proxypass', options.proxyPassword); } } } // Custom flags if (options.customFlags && options.customFlags.length > 0) { command.push(...options.customFlags); } // Command and arguments if (options.command) { command.push(options.command); } if (options.args) { command.push(...options.args); } return command; } private buildEnvironment( options: PSExecConnectionOptions ): Record<string, string> { const env: Record<string, string> = {}; // PSExec environment variables if (options.timeout) { env.PSEXEC_TIMEOUT = options.timeout.toString(); } if (options.connectionRetries) { env.PSEXEC_RETRIES = options.connectionRetries.toString(); } if (options.retryDelay) { env.PSEXEC_RETRY_DELAY = options.retryDelay.toString(); } // Logging and debugging if (options.logLevel) { env.PSEXEC_LOG_LEVEL = options.logLevel; } if (options.enableDebug) { env.PSEXEC_DEBUG = '1'; } if (options.enableAuditing && options.auditLogPath) { env.PSEXEC_AUDIT_LOG = options.auditLogPath; } if (options.enableEventLog) { env.PSEXEC_EVENT_LOG = '1'; if (options.eventLogSource) { env.PSEXEC_EVENT_SOURCE = options.eventLogSource; } } // Security settings if (options.encryptionLevel) { env.PSEXEC_ENCRYPTION = options.encryptionLevel; } if (options.compressionLevel !== undefined) { env.PSEXEC_COMPRESSION = options.compressionLevel.toString(); } // Network settings if (options.bufferSize) { env.PSEXEC_BUFFER_SIZE = options.bufferSize.toString(); } if (options.enableKeepAlive) { env.PSEXEC_KEEP_ALIVE = '1'; if (options.keepAliveInterval) { env.PSEXEC_KEEP_ALIVE_INTERVAL = options.keepAliveInterval.toString(); } } if (options.maxConnectionAttempts) { env.PSEXEC_MAX_ATTEMPTS = options.maxConnectionAttempts.toString(); } if (options.connectionTimeout) { env.PSEXEC_CONNECT_TIMEOUT = options.connectionTimeout.toString(); } if (options.executionTimeout) { env.PSEXEC_EXEC_TIMEOUT = options.executionTimeout.toString(); } // PowerShell settings if (options.enablePowerShell) { env.PSEXEC_POWERSHELL = '1'; if (options.runspace) { env.PSEXEC_RUNSPACE = options.runspace; } } // Module and assembly loading if (options.modules && options.modules.length > 0) { env.PSEXEC_MODULES = options.modules.join(';'); } if (options.assemblies && options.assemblies.length > 0) { env.PSEXEC_ASSEMBLIES = options.assemblies.join(';'); } if (options.snapins && options.snapins.length > 0) { env.PSEXEC_SNAPINS = options.snapins.join(';'); } // Firewall settings if (options.enableFirewall) { env.PSEXEC_FIREWALL = '1'; if (options.firewallProfile) { env.PSEXEC_FIREWALL_PROFILE = options.firewallProfile; } } // Custom environment variables if (options.environment) { Object.assign(env, options.environment); } return env; } async cleanup(): Promise<void> { this.logger.info('Cleaning up PSExec protocol'); // Close all PSExec processes for (const [sessionId, process] of Array.from(this.psexecProcesses)) { try { process.kill(); } catch (error) { this.logger.error( `Error killing PSExec process for session ${sessionId}:`, error ); } } // Clear all data this.psexecProcesses.clear(); // Call parent cleanup await super.cleanup(); } } export default PSExecProtocol;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server