security-review-mcp-tool
Version:
Servidor MCP para revisão de segurança de código
382 lines (376 loc) • 15.1 kB
JavaScript
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