UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

528 lines (527 loc) 22.2 kB
import { exec } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs'; import * as path from 'path'; import logger from '../../../logger.js'; import { UnifiedSecurityConfigManager } from '../../vibe-task-manager/security/unified-security-config.js'; import { SemgrepRuleGenerator } from './semgrepRuleGenerator.js'; import { resolveImport } from '../utils/importResolver.js'; const execAsync = promisify(exec); export class SemgrepAdapter { allowedDir; outputDir; securityManager; ruleGenerator; cache = new Map(); tempFiles = []; constructor(allowedDir, outputDir) { this.allowedDir = allowedDir; this.outputDir = outputDir; this.securityManager = UnifiedSecurityConfigManager.getInstance(); this.ruleGenerator = new SemgrepRuleGenerator(); } async analyzeImports(filePath, options) { try { const cacheKey = `${filePath}:${JSON.stringify(options)}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const validation = this.securityManager.validatePathSecurity(filePath, { operation: 'read' }); if (!validation.isValid) { logger.warn({ filePath, error: validation.error }, 'File path validation failed'); return []; } const rules = this.ruleGenerator.generateImportRules(); const rulesFile = path.join(this.outputDir, `semgrep-rules-${Date.now()}.yaml`); this.tempFiles.push(rulesFile); await this.ruleGenerator.writeRulesToFile(rules, rulesFile); const command = this.buildSemgrepCommand(filePath, rulesFile, options); const { stdout } = await execAsync(command); const result = JSON.parse(stdout); const imports = this.convertToImportInfo(result, filePath, options); await fs.promises.unlink(rulesFile); const fileIndex = this.tempFiles.indexOf(rulesFile); if (fileIndex !== -1) { this.tempFiles.splice(fileIndex, 1); } this.cache.set(cacheKey, imports); return imports; } catch (error) { logger.error({ err: error, filePath }, 'Error analyzing imports with Semgrep'); return []; } } buildSemgrepCommand(filePath, rulesFile, options) { const baseCommand = 'npx @semgrep/semgrep'; let command = `${baseCommand} --json --config ${rulesFile}`; if (options.timeout) { command += ` --timeout ${options.timeout}`; } if (options.maxMemory) { command += ` --max-memory ${options.maxMemory}`; } if (options.excludePatterns && options.excludePatterns.length > 0) { options.excludePatterns.forEach(pattern => { command += ` --exclude ${pattern}`; }); } command += ` "${filePath}"`; return command; } convertToImportInfo(result, filePath, options) { const imports = []; const importMap = new Map(); try { if (!result.results || !Array.isArray(result.results)) { return imports; } for (const match of result.results) { try { const importPath = this.extractImportPath(match); if (!importPath) { continue; } const resolvedPath = this.resolveImportPath(importPath, filePath, options.projectRoot || this.allowedDir); const importInfo = this.createImportInfo(match, importPath, resolvedPath); const key = importInfo.path; if (importMap.has(key)) { const existing = importMap.get(key); if (importInfo.importedItems && importInfo.importedItems.length > 0) { if (!existing.importedItems) { existing.importedItems = []; } for (const item of importInfo.importedItems) { if (!existing.importedItems.some(i => i.name === item.name)) { existing.importedItems.push(item); } } } existing.metadata = { ...existing.metadata, ...importInfo.metadata }; } else { importMap.set(key, importInfo); } } catch (error) { logger.warn({ err: error, match }, 'Error processing Semgrep match'); } } return Array.from(importMap.values()); } catch (error) { logger.error({ err: error, filePath }, 'Error converting Semgrep results to ImportInfo'); return imports; } } extractImportPath(match) { try { const text = match.extra.lines; if (match.check_id.startsWith('js-')) { const regex = /(from|require)\s*['"](.*?)['"]/; const match = text.match(regex); return match ? match[2] : null; } else if (match.check_id.startsWith('python-')) { if (match.check_id === 'python-from-import') { const regex = /from\s+(.*?)\s+import/; const match = text.match(regex); return match ? match[1] : null; } else { const regex = /import\s+(.*?)($|\s+as)/; const match = text.match(regex); return match ? match[1] : null; } } else if (match.check_id.startsWith('java-')) { const regex = /import\s+(?:static\s+)?(.*?)(?:\.\w+)?;/; const match = text.match(regex); return match ? match[1] : null; } else if (match.check_id.startsWith('cpp-')) { const regex = /#include\s*[<"](.*?)[>"]/; const match = text.match(regex); return match ? match[1] : null; } else if (match.check_id.startsWith('ruby-')) { const regex = /(?:require|require_relative|load)\s*['"](.*?)['"]/; const match = text.match(regex); return match ? match[1] : null; } else if (match.check_id.startsWith('go-')) { const regex = /import\s+(?:\w+\s+)?["]([^"]+)["]/; const match = text.match(regex); return match ? match[1] : null; } else if (match.check_id.startsWith('php-')) { if (match.check_id.includes('use')) { const regex = /use\s+(.*?)(?:\s+as\s+|;)/; const match = text.match(regex); return match ? match[1] : null; } else { const regex = /(?:require|include)(?:_once)?\s*['"](.*?)['"]/; const match = text.match(regex); return match ? match[1] : null; } } return null; } catch (error) { logger.warn({ err: error, match }, 'Error extracting import path from Semgrep match'); return null; } } resolveImportPath(importPath, filePath, projectRoot) { try { const resolved = resolveImport(importPath, { projectRoot, fromFile: filePath, language: path.extname(filePath).slice(1), expandSecurityBoundary: true }); return resolved; } catch (error) { logger.debug({ err: error, importPath, filePath }, 'Error resolving import path'); return importPath; } } createImportInfo(match, importPath, resolvedPath) { const metadata = match.extra.metadata || {}; const isCore = metadata.isCore || this.isCorePath(importPath, match.check_id); const isExternalPackage = !isCore && !metadata.isRelative && !resolvedPath.includes(this.allowedDir); const importedItems = this.extractImportedItems(match); const importInfo = { path: resolvedPath || importPath, importedItems, isCore, isExternalPackage, isDynamic: metadata.isDynamic || false, moduleSystem: metadata.moduleSystem || this.getModuleSystem(match.check_id), metadata: { originalPath: importPath, importType: metadata.importType || match.check_id, isRelative: metadata.isRelative || importPath.startsWith('.'), matchedPattern: match.extra.lines } }; return importInfo; } extractImportedItems(match) { const items = []; try { const text = match.extra.lines; if (match.check_id.startsWith('js-')) { if (match.check_id === 'js-import-default') { const regex = /import\s+(\w+)\s+from/; const m = text.match(regex); if (m) { items.push({ name: m[1], isDefault: true, isNamespace: false, nodeText: text }); } } else if (match.check_id === 'js-import-named') { const regex = /import\s+{(.*?)}\s+from/; const m = text.match(regex); if (m) { const namedImports = m[1].split(',').map(s => s.trim()); for (const namedImport of namedImports) { const [name, alias] = namedImport.split(' as ').map(s => s.trim()); items.push({ name: alias || name, alias: alias ? name : undefined, isDefault: false, isNamespace: false, nodeText: text }); } } } else if (match.check_id === 'js-import-namespace') { const regex = /import\s+\*\s+as\s+(\w+)\s+from/; const m = text.match(regex); if (m) { items.push({ name: m[1], isDefault: false, isNamespace: true, nodeText: text }); } } } else if (match.check_id.startsWith('python-')) { if (match.check_id === 'python-import') { const regex = /import\s+(\w+)/; const m = text.match(regex); if (m) { items.push({ name: m[1], isDefault: false, isNamespace: false, nodeText: text }); } } else if (match.check_id === 'python-from-import') { const regex = /from\s+.*?\s+import\s+(.*?)$/; const m = text.match(regex); if (m) { const namedImports = m[1].split(',').map(s => s.trim()); for (const namedImport of namedImports) { const [name, alias] = namedImport.split(' as ').map(s => s.trim()); items.push({ name: alias || name, alias: alias ? name : undefined, isDefault: false, isNamespace: false, nodeText: text }); } } } else if (match.check_id === 'python-import-as') { const regex = /import\s+(\w+)\s+as\s+(\w+)/; const m = text.match(regex); if (m) { items.push({ name: m[2], alias: m[1], isDefault: false, isNamespace: false, nodeText: text }); } } } else if (match.check_id.startsWith('java-')) { if (match.check_id === 'java-import') { const regex = /import\s+.*?\.(\w+);/; const m = text.match(regex); if (m) { items.push({ name: m[1], isDefault: false, isNamespace: false, nodeText: text }); } } else if (match.check_id === 'java-import-static') { const regex = /import\s+static\s+.*?\.(\w+)\.(\w+);/; const m = text.match(regex); if (m) { items.push({ name: m[2], isDefault: false, isNamespace: false, isStatic: true, nodeText: text }); } } } else if (match.check_id.startsWith('cpp-')) { const regex = /#include\s*[<"]([^>"]+)[>"]/; const m = text.match(regex); if (m) { const headerName = m[1]; const baseName = path.basename(headerName, path.extname(headerName)); items.push({ name: baseName, isDefault: false, isNamespace: false, nodeText: text }); } } else if (match.check_id.startsWith('ruby-')) { const regex = /(?:require|require_relative|load)\s*['"]([^'"]+)['"]/; const m = text.match(regex); if (m) { const moduleName = m[1]; const baseName = path.basename(moduleName, path.extname(moduleName)); items.push({ name: baseName, isDefault: false, isNamespace: false, nodeText: text }); } } else if (match.check_id.startsWith('go-')) { if (match.check_id === 'go-import-single') { const regex = /import\s+["']([^"']+)["']/; const m = text.match(regex); if (m) { const packagePath = m[1]; const packageName = path.basename(packagePath); items.push({ name: packageName, isDefault: false, isNamespace: false, nodeText: text }); } } else if (match.check_id === 'go-import-alias') { const regex = /import\s+(\w+)\s+["']([^"']+)["']/; const m = text.match(regex); if (m) { items.push({ name: m[1], isDefault: false, isNamespace: false, nodeText: text }); } } } else if (match.check_id.startsWith('php-')) { if (match.check_id.includes('use')) { const regex = /use\s+(.*?)(?:\s+as\s+(\w+))?;/; const m = text.match(regex); if (m) { const namespace = m[1]; const alias = m[2]; const name = namespace.split('\\').pop() || ''; items.push({ name: alias || name, alias: alias ? name : undefined, isDefault: false, isNamespace: false, nodeText: text }); } } else { const regex = /(?:require|include)(?:_once)?\s*['"]([^'"]+)['"]/; const m = text.match(regex); if (m) { const filePath = m[1]; const baseName = path.basename(filePath, path.extname(filePath)); items.push({ name: baseName, isDefault: false, isNamespace: false, nodeText: text }); } } } } catch (error) { logger.warn({ err: error, match }, 'Error extracting imported items from Semgrep match'); } return items; } isCorePath(importPath, checkId) { if (checkId.startsWith('js-')) { const nodeBuiltins = [ 'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib' ]; return nodeBuiltins.includes(importPath); } else if (checkId.startsWith('python-')) { const pythonBuiltins = [ 'abc', 'argparse', 'asyncio', 'collections', 'copy', 'datetime', 'functools', 'glob', 'io', 'itertools', 'json', 'logging', 'math', 'os', 'pathlib', 're', 'shutil', 'sys', 'time', 'typing', 'uuid' ]; return pythonBuiltins.includes(importPath.split('.')[0]); } else if (checkId.startsWith('java-')) { return importPath.startsWith('java.') || importPath.startsWith('javax.') || importPath.startsWith('sun.') || importPath.startsWith('com.sun.'); } else if (checkId.startsWith('cpp-')) { return checkId === 'cpp-include-system'; } else if (checkId.startsWith('ruby-')) { const rubyBuiltins = [ 'abbrev', 'base64', 'benchmark', 'bigdecimal', 'cgi', 'csv', 'date', 'digest', 'fileutils', 'find', 'forwardable', 'io', 'json', 'logger', 'net', 'open-uri', 'optparse', 'pathname', 'pp', 'set', 'shellwords', 'stringio', 'strscan', 'tempfile', 'time', 'timeout', 'uri', 'yaml', 'zlib' ]; return rubyBuiltins.includes(importPath); } else if (checkId.startsWith('go-')) { const goBuiltins = [ 'bufio', 'bytes', 'context', 'crypto', 'database', 'encoding', 'errors', 'flag', 'fmt', 'io', 'log', 'math', 'net', 'os', 'path', 'reflect', 'regexp', 'runtime', 'sort', 'strconv', 'strings', 'sync', 'syscall', 'time', 'unicode' ]; return goBuiltins.some(pkg => importPath === pkg || importPath.startsWith(pkg + '/')); } else if (checkId.startsWith('php-')) { return false; } return false; } getModuleSystem(checkId) { if (checkId.startsWith('js-')) { if (checkId === 'js-require') { return 'commonjs'; } else { return 'esm'; } } else if (checkId.startsWith('python-')) { return 'python'; } else if (checkId.startsWith('java-')) { return 'java'; } else if (checkId.startsWith('cpp-')) { return 'cpp'; } else if (checkId.startsWith('ruby-')) { return 'ruby'; } else if (checkId.startsWith('go-')) { return 'go'; } else if (checkId.startsWith('php-')) { return 'php'; } return 'unknown'; } dispose() { this.cache.clear(); if (this.tempFiles && this.tempFiles.length > 0) { this.tempFiles.forEach(file => { try { if (fs.existsSync(file)) { fs.unlinkSync(file); logger.debug({ file }, 'Deleted temporary file during SemgrepAdapter disposal'); } } catch (error) { logger.warn({ file, error }, 'Failed to delete temporary file during SemgrepAdapter disposal'); } }); this.tempFiles = []; } logger.debug('SemgrepAdapter disposed'); } }