UNPKG

security-review-mcp-tool

Version:

Servidor MCP para revisão de segurança de código

382 lines (376 loc) 15.1 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { analisarSegurancaCodigo } from "./security-analyzer.js"; import { fileURLToPath } from 'url'; import { promises as fs } from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; const execPromise = promisify(exec); /** * Servidor MCP que fornece ferramentas para revisão de segurança de código. * Implementa o Model Context Protocol para integração com LLMs. */ async function iniciarServidorMCP() { // Criar instância do servidor MCP const servidor = new McpServer({ name: "Revisor de Segurança", version: "1.0.0" }); // Adicionar ferramenta para revisar segurança do código servidor.tool("revisar-seguranca-codigo", { codigo: z.string(), linguagem: z.string().optional(), nivel_detalhe: z.enum(["baixo", "medio", "alto"]).optional().default("medio"), path: z.string().optional().describe("Caminho do arquivo a ser analisado") }, async ({ codigo, linguagem, nivel_detalhe, path }) => { try { // Se tiver path, incluir no contexto da análise const contexto = path ? `Análise do arquivo: ${path}\n\n` : ''; const resultadoAnalise = await analisarSegurancaCodigo(codigo, linguagem, nivel_detalhe); return { content: [ { type: "text", text: contexto + resultadoAnalise } ] }; } catch (erro) { return { content: [ { type: "text", text: `Erro ao analisar código: ${erro.message}` } ], isError: true }; } }); // Adicionar prompt para ajudar na revisão de código servidor.prompt("revisar-codigo", { codigo: z.string(), linguagem: z.string().optional(), nivel_detalhe: z.enum(["baixo", "medio", "alto"]).optional() }, ({ codigo, linguagem, nivel_detalhe }) => ({ messages: [{ role: "user", content: { type: "text", text: `Por favor, realize uma revisão de segurança completa ${nivel_detalhe ? `com nível de detalhe ${nivel_detalhe}` : "detalhada"} do seguinte código ${linguagem ? `em ${linguagem}` : ""}:\n\n${codigo}\n\nIdentifique vulnerabilidades de segurança, potenciais falhas, e recomende melhorias para aumentar a segurança.` } }] })); // Iniciar o transporte via stdio const transporte = new StdioServerTransport(); console.log("Iniciando servidor MCP de revisão de segurança..."); await servidor.connect(transporte); console.log("Servidor MCP conectado e pronto para receber solicitações."); } /** * Obtém a lista de arquivos modificados no commit atual usando Git * ou todos os arquivos de código no diretório se não for um repositório Git */ async function obterArquivosModificados() { try { // Tentar obter arquivos usando Git try { // Verificar se estamos em um repositório Git await execPromise('git rev-parse --is-inside-work-tree'); // Obtém arquivos staged (adicionados ao commit atual) const { stdout: stagedFiles } = await execPromise('git diff --cached --name-only'); // Obtém arquivos modificados mas não staged const { stdout: modifiedFiles } = await execPromise('git diff --name-only'); // Obtém arquivos não rastreados const { stdout: untrackedFiles } = await execPromise('git ls-files --others --exclude-standard'); // Obtém arquivos adicionados recentemente (não necessariamente staged) const { stdout: recentlyAddedFiles } = await execPromise('git ls-files --modified --others --exclude-standard'); // Combina todos os arquivos e remove duplicatas const allFiles = [...new Set([ ...stagedFiles.split('\n'), ...modifiedFiles.split('\n'), ...untrackedFiles.split('\n'), ...recentlyAddedFiles.split('\n') ])].filter(file => file.trim() !== ''); console.log('Arquivos detectados via Git:', allFiles); if (allFiles.length > 0) { // Se encontrou arquivos via Git, use-os return filtrarArquivosCodigo(allFiles); } } catch (gitError) { console.log('Não foi possível obter arquivos via Git, usando modo de diretório...'); } // Se chegou aqui, não foi possível obter arquivos via Git (não é um repositório ou não há mudanças) // Usamos uma abordagem alternativa para obter todos os arquivos no diretório atual console.log('Escaneando todos os arquivos no diretório atual...'); return await obterTodosArquivosCodigo(); } catch (error) { console.error('Erro ao obter arquivos:', error.message); throw new Error('Falha ao escanear arquivos. ' + error.message); } } /** * Filtra arquivos de código de uma lista */ function filtrarArquivosCodigo(arquivos) { const codeExtensions = [ // JavaScript/TypeScript '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', // Web '.html', '.css', '.scss', '.less', // Backend '.php', '.py', '.java', '.cs', '.go', '.rb', // C e variantes '.c', '.cpp', '.h', '.hpp', // Outros '.sql', '.sh', '.bat', '.ps1', '.json', '.xml', '.yaml', '.yml' ]; const codeFiles = arquivos.filter(file => { const ext = path.extname(file).toLowerCase(); return codeExtensions.includes(ext); }); console.log('Arquivos de código filtrados:', codeFiles); return codeFiles; } /** * Escaneia recursivamente um diretório e retorna todos os arquivos de código */ async function obterTodosArquivosCodigo(dir = '.') { let arquivos = []; // Ignorar diretórios comuns que não devem ser analisados const diretoriosIgnorados = [ 'node_modules', 'dist', 'build', '.git', '.vscode', 'bin', 'obj', 'out', 'target', 'vendor' ]; try { const entradas = await fs.readdir(dir, { withFileTypes: true }); for (const entrada of entradas) { const caminho = path.join(dir, entrada.name); if (entrada.isDirectory()) { // Ignorar diretórios específicos if (diretoriosIgnorados.includes(entrada.name)) { console.log(`Pulando diretório ignorado: ${entrada.name}`); continue; } // Recursivamente buscar arquivos em subdiretórios const arquivosSubdir = await obterTodosArquivosCodigo(caminho); arquivos = [...arquivos, ...arquivosSubdir]; } else { arquivos.push(caminho); } } // Filtrar apenas arquivos de código return filtrarArquivosCodigo(arquivos); } catch (error) { console.error(`Erro ao escanear diretório ${dir}:`, error.message); return []; } } /** * Lê o conteúdo de um arquivo */ async function lerArquivo(filePath) { try { return await fs.readFile(filePath, 'utf8'); } catch (error) { console.error(`Erro ao ler arquivo ${filePath}:`, error.message); return ''; } } /** * Detecta a linguagem de programação com base na extensão do arquivo */ function detectarLinguagem(filePath) { const ext = path.extname(filePath).toLowerCase(); const linguagens = { '.js': 'javascript', '.ts': 'typescript', '.jsx': 'javascript', '.tsx': 'typescript', '.php': 'php', '.py': 'python', '.java': 'java', '.cs': 'csharp', '.go': 'go', '.rb': 'ruby', '.c': 'c', '.cpp': 'cpp', '.h': 'c', '.hpp': 'cpp' }; return linguagens[ext] || ''; } /** * Analisa a segurança de todos os arquivos modificados no commit atual */ async function analisarCommitAtual(nivelDetalhe = 'medio') { try { // Obter arquivos modificados const arquivos = await obterArquivosModificados(); if (arquivos.length === 0) { return 'Nenhum arquivo de código modificado encontrado para análise.'; } console.log(`Analisando ${arquivos.length} arquivos...`); let relatorioFinal = `# Relatório de Segurança - Commit Atual\n\n`; relatorioFinal += `Data: ${new Date().toLocaleString()}\n`; relatorioFinal += `Total de arquivos analisados: ${arquivos.length}\n\n`; relatorioFinal += `## Lista de Arquivos Analisados\n\n`; arquivos.forEach(arquivo => { relatorioFinal += `- ${arquivo}\n`; }); relatorioFinal += `\n## Resultados da Análise\n\n`; let vulnerabilidadesEncontradas = false; let errosAnalise = 0; // Analisar cada arquivo for (const arquivo of arquivos) { const conteudo = await lerArquivo(arquivo); if (!conteudo) { relatorioFinal += `\n### Arquivo: ${arquivo}\n\n`; relatorioFinal += `❌ Erro: Não foi possível ler o conteúdo do arquivo\n\n`; relatorioFinal += '---\n'; errosAnalise++; continue; } const linguagem = detectarLinguagem(arquivo); console.log(`Analisando: ${arquivo} (${linguagem || 'auto-detecção'})`); try { const resultado = await analisarSegurancaCodigo(conteudo, linguagem, nivelDetalhe); // Verifica se encontrou vulnerabilidades const temVulnerabilidades = !resultado.includes('Nenhuma vulnerabilidade encontrada'); if (temVulnerabilidades) { vulnerabilidadesEncontradas = true; } relatorioFinal += `\n### Arquivo: ${arquivo}\n\n`; relatorioFinal += resultado; relatorioFinal += '\n\n---\n'; } catch (erro) { relatorioFinal += `\n### Arquivo: ${arquivo}\n\n`; relatorioFinal += `❌ Erro ao analisar: ${erro.message}\n\n`; relatorioFinal += '---\n'; errosAnalise++; } } // Adicionar resumo ao início let resumo = ''; if (vulnerabilidadesEncontradas) { resumo = '⚠️ **ALERTA: Vulnerabilidades de segurança encontradas! Revise o relatório completo.**\n\n'; } else if (errosAnalise > 0) { resumo = `⚠️ **ATENÇÃO: ${errosAnalise} arquivos não puderam ser analisados corretamente. Revise o relatório completo.**\n\n`; } else { resumo = '✅ **Nenhuma vulnerabilidade crítica encontrada.**\n\n'; } return resumo + relatorioFinal; } catch (error) { return `Erro ao analisar commit: ${error.message}`; } } /** * Função para uso em linha de comando */ async function executarCLI() { const args = process.argv.slice(2); const comando = args[0]; if (!comando || comando === 'help' || comando === '--help' || comando === '-h') { mostrarAjuda(); return; } switch (comando) { case 'servidor': await iniciarServidorMCP(); break; case 'analisar': if (args.length < 2) { console.error('Erro: Código fonte não fornecido.'); mostrarAjuda(); process.exit(1); } const codigo = args[1]; const linguagem = args[2]; const nivelDetalhe = args[3]; try { const resultado = await analisarSegurancaCodigo(codigo, linguagem, nivelDetalhe); console.log(resultado); } catch (erro) { console.error('Erro ao analisar código:', erro.message); process.exit(1); } break; case 'git-commit': case 'analisar-dir': try { const nivelDetalhe = args[1]; const modo = comando === 'git-commit' ? 'Git' : 'Diretório'; console.log(`Iniciando análise de segurança (Modo: ${modo})...`); const resultado = await analisarCommitAtual(nivelDetalhe); console.log(resultado); // Salvar o relatório em um arquivo const dataFormatada = new Date().toISOString().replace(/[:T]/g, '-').split('.')[0]; const nomeArquivo = `relatorio-seguranca-${dataFormatada}.md`; await fs.writeFile(nomeArquivo, resultado); console.log(`Relatório salvo em: ${nomeArquivo}`); } catch (erro) { console.error(`Erro ao analisar ${comando === 'git-commit' ? 'commit' : 'diretório'}:`, erro.message); process.exit(1); } break; default: console.error(`Erro: Comando desconhecido '${comando}'`); mostrarAjuda(); process.exit(1); } } /** * Mostra a ajuda da linha de comando */ function mostrarAjuda() { console.log(` Revisor de Segurança de Código - MCP Uso: npx security-review-mcp-tool [comando] [opções] Comandos: servidor - Inicia o servidor MCP via stdio analisar - Analisa um trecho de código git-commit - Analisa arquivos modificados no commit atual (via Git) analisar-dir - Analisa todos os arquivos de código no diretório atual help - Mostra esta ajuda Exemplos: # Iniciar o servidor MCP (usado pelo Cursor) npx security-review-mcp-tool servidor # Analisar código de forma direta npx security-review-mcp-tool analisar "function teste() { eval(input); }" javascript alto # Analisar arquivos modificados via Git npx security-review-mcp-tool git-commit alto # Analisar todos os arquivos no diretório atual npx security-review-mcp-tool analisar-dir medio `); } // Verificar se o arquivo está sendo executado diretamente ou importado if (process.argv[1] === fileURLToPath(import.meta.url)) { // Executando como script executarCLI().catch((erro) => { console.error("Erro:", erro); process.exit(1); }); } else { // Sendo importado (comportamento padrão) iniciarServidorMCP().catch((erro) => { console.error("Erro ao iniciar servidor MCP:", erro); process.exit(1); }); } //# sourceMappingURL=index.js.map