UNPKG

@tb.p/terminai

Version:

MCP (Model Context Protocol) server for secure SSH remote command execution. Enables AI assistants like Claude, Cursor, and VS Code to execute commands on remote servers via SSH with command validation, history tracking, and web-based configuration UI.

115 lines (100 loc) 3.4 kB
import { minimatch } from 'minimatch'; function matchesPattern(command, pattern) { if (pattern === '*') { return true; } // Check if pattern is a regex (format: /pattern/ or /pattern/flags) const regexMatch = pattern.match(/^\/(.+)\/([gimsuy]*)$/); if (regexMatch) { try { const regexPattern = regexMatch[1]; const flags = regexMatch[2] || ''; const regex = new RegExp(regexPattern, flags); return regex.test(command); } catch (error) { // If regex is invalid, fall back to glob matching console.warn(`Invalid regex pattern: ${pattern}, falling back to glob matching`); } } // Try glob pattern matching first (for file path patterns) const globMatch = minimatch(command, pattern, { nocase: false }); if (globMatch) { return true; } // If glob doesn't match and pattern contains wildcards, try substring matching // This handles patterns like *root* that should match "cd /root" if (pattern.includes('*')) { // Convert glob pattern to regex for substring matching // Escape special regex chars except * const escaped = pattern .replace(/[.+?^${}()|[\]\\]/g, '\\$&') .replace(/\*/g, '.*'); try { const regex = new RegExp(`^${escaped}$`); return regex.test(command); } catch (error) { // If regex conversion fails, fall back to simple substring check const simplePattern = pattern.replace(/\*/g, ''); return command.includes(simplePattern); } } // Exact match if no wildcards return command === pattern; } function checkAgainstRules(command, rules) { for (const rule of rules) { if (matchesPattern(command, rule)) { return true; } } return false; } export function validateCommand(command, connectionName, config) { const connection = config.connections[connectionName]; if (!connection) { return { allowed: false, reason: `Connection '${connectionName}' not found` }; } // Order of priority (least to highest): // 1. Global Denied (broadest restrictions) // 2. Global Allowed (can override global denied) // 3. Connection Denied (more specific restrictions) // 4. Connection Allowed (most specific, can override everything) // // We check in reverse order (highest to least) so higher priority rules override lower ones // 4. Connection Allowed (highest priority - checked first) if (checkAgainstRules(command, connection.allowedCommands)) { return { allowed: true, reason: `Command allowed by connection-specific allow rule` }; } // 3. Connection Denied (overrides global rules) if (checkAgainstRules(command, connection.disallowedCommands)) { return { allowed: false, reason: `Command denied by connection-specific disallow rule` }; } // 2. Global Allowed (can override global denied) if (checkAgainstRules(command, config.global.allowedCommands)) { return { allowed: true, reason: `Command allowed by global allow rule` }; } // 1. Global Denied (least priority - checked last) if (checkAgainstRules(command, config.global.disallowedCommands)) { return { allowed: false, reason: `Command denied by global disallow rule` }; } // Default allow if nothing matches return { allowed: true, reason: `Command allowed (default allow - no matching rules)` }; }