UNPKG

mysql-mcp-server-v1

Version:

Servidor MCP completo para MySQL Database com sistema hierárquico de configuração, operações DDL, DML, DCL, DDM, DCM, DLM, monitoramento, auditoria e gestão de ciclo de vida de dados

646 lines (527 loc) 19.2 kB
import { Logger } from './logger.js'; import mysql from 'mysql2/promise'; import { ConnectionManager } from './connection-manager.js'; export class DCMOperations { constructor(connectionManager = null) { this.logger = new Logger(); this.connectionManager = connectionManager || new ConnectionManager(); } async getConnection(connectionName = null) { try { if (this.connectionManager) { return await this.connectionManager.getConnection(connectionName); } throw new Error('Nenhuma configuração de conexão disponível'); } catch (error) { this.logger.error('Erro ao conectar com MySQL:', error); throw new Error(`Falha na conexão: ${error.message}`); } } // ===== GESTÃO DE USUÁRIOS ===== async createUser(options = {}) { const { username, password, host = '%', ifNotExists = true, connectionName = null } = options; if (!username || !password) { throw new Error('Nome de usuário e senha são obrigatórios'); } let connection; try { connection = await this.getConnection(connectionName); // Verificar se o usuário já existe if (ifNotExists) { const exists = await this.userExists(connection, username, host); if (exists) { return `⚠️ Usuário '${username}'@'${host}' já existe. Operação ignorada.`; } } const createQuery = ` CREATE USER ${ifNotExists ? 'IF NOT EXISTS ' : ''}'${username}'@'${host}' IDENTIFIED BY '${password}' `; await connection.execute(createQuery); this.logger.info(`Usuário '${username}'@'${host}' criado com sucesso`); return `✅ Usuário '${username}'@'${host}' criado com sucesso!`; } catch (error) { this.logger.error('Erro ao criar usuário:', error); throw new Error(`Erro ao criar usuário: ${error.message}`); } finally { if (connection) { await connection.end(); } } } async dropUser(options = {}) { const { username, host = '%', ifExists = true, connectionName = null } = options; if (!username) { throw new Error('Nome de usuário é obrigatório'); } let connection; try { connection = await this.getConnection(connectionName); // Verificar se o usuário existe if (ifExists) { const exists = await this.userExists(connection, username, host); if (!exists) { return `⚠️ Usuário '${username}'@'${host}' não existe. Operação ignorada.`; } } const dropQuery = `DROP USER ${ifExists ? 'IF EXISTS ' : ''}'${username}'@'${host}'`; await connection.execute(dropQuery); this.logger.info(`Usuário '${username}'@'${host}' removido com sucesso`); return `✅ Usuário '${username}'@'${host}' removido com sucesso!`; } catch (error) { this.logger.error('Erro ao remover usuário:', error); throw new Error(`Erro ao remover usuário: ${error.message}`); } finally { if (connection) { await connection.end(); } } } async listUsers(options = {}) { const { includeSystem = false, connectionName = null } = options; let connection; try { connection = await this.getConnection(connectionName); let query = ` SELECT user, host, account_locked, password_expired, password_last_changed, password_lifetime, password_reuse_time, password_require_current, password_require_current_nist FROM mysql.user `; if (!includeSystem) { query += ' WHERE user NOT IN (\'mysql.session\', \'mysql.sys\', \'mysql.infoschema\', \'root\')'; } query += ' ORDER BY user, host'; const [rows] = await connection.execute(query); let output = ''; if (rows.length === 0) { output = 'Nenhum usuário encontrado.'; } else { output += '| Usuário | Host | Conta Bloqueada | Senha Expirada | Última Alteração | Vida da Senha |\n'; output += '|---------|------|-----------------|----------------|------------------|---------------|\n'; for (const row of rows) { const lastChanged = row.password_last_changed ? new Date(row.password_last_changed).toLocaleString() : 'N/A'; const lifetime = row.password_lifetime ? `${row.password_lifetime} dias` : 'N/A'; output += `| ${row.user} | ${row.host} | ${row.account_locked} | ${row.password_expired} | ${lastChanged} | ${lifetime} |\n`; } output += `\n**Total de usuários:** ${rows.length}`; } return output; } catch (error) { this.logger.error('Erro ao listar usuários:', error); throw new Error(`Erro ao listar usuários: ${error.message}`); } finally { if (connection) { await connection.end(); } } } async changePassword(options = {}) { const { username, newPassword, host = '%', connectionName = null } = options; if (!username || !newPassword) { throw new Error('Nome de usuário e nova senha são obrigatórios'); } let connection; try { connection = await this.getConnection(connectionName); const alterQuery = ` ALTER USER '${username}'@'${host}' IDENTIFIED BY '${newPassword}' `; await connection.execute(alterQuery); this.logger.info(`Senha do usuário '${username}'@'${host}' alterada com sucesso`); return `✅ Senha do usuário '${username}'@'${host}' alterada com sucesso!`; } catch (error) { this.logger.error('Erro ao alterar senha:', error); throw new Error(`Erro ao alterar senha: ${error.message}`); } finally { if (connection) { await connection.end(); } } } // ===== GESTÃO DE PRIVILÉGIOS ===== async grantPrivileges(options = {}) { const { privileges, database = '*', table = '*', username, host = '%', withGrantOption = false, connectionName = null } = options; if (!privileges || !Array.isArray(privileges) || privileges.length === 0) { throw new Error('Lista de privilégios é obrigatória'); } if (!username) { throw new Error('Nome de usuário é obrigatório'); } let connection; try { connection = await this.getConnection(connectionName); const privilegesList = privileges.join(', '); const grantOption = withGrantOption ? ' WITH GRANT OPTION' : ''; const onObject = `${database}.${table}`; const grantQuery = ` GRANT ${privilegesList} ON ${onObject} TO '${username}'@'${host}'${grantOption} `; await connection.execute(grantQuery); this.logger.info(`Privilégios concedidos para '${username}'@'${host}' em ${onObject}`); return `✅ Privilégios ${privilegesList} concedidos para '${username}'@'${host}' em ${onObject}!`; } catch (error) { this.logger.error('Erro ao conceder privilégios:', error); throw new Error(`Erro ao conceder privilégios: ${error.message}`); } finally { if (connection) { await connection.end(); } } } async revokePrivileges(options = {}) { const { privileges, database = '*', table = '*', username, host = '%', connectionName = null } = options; if (!privileges || !Array.isArray(privileges) || privileges.length === 0) { throw new Error('Lista de privilégios é obrigatória'); } if (!username) { throw new Error('Nome de usuário é obrigatório'); } let connection; try { connection = await this.getConnection(connectionName); const privilegesList = privileges.join(', '); const onObject = `${database}.${table}`; const revokeQuery = ` REVOKE ${privilegesList} ON ${onObject} FROM '${username}'@'${host}' `; await connection.execute(revokeQuery); this.logger.info(`Privilégios revogados de '${username}'@'${host}' em ${onObject}`); return `✅ Privilégios ${privilegesList} revogados de '${username}'@'${host}' em ${onObject}!`; } catch (error) { this.logger.error('Erro ao revogar privilégios:', error); throw new Error(`Erro ao revogar privilégios: ${error.message}`); } finally { if (connection) { await connection.end(); } } } async showGrants(options = {}) { const { username, host = '%', connectionName = null } = options; if (!username) { throw new Error('Nome de usuário é obrigatório'); } let connection; try { connection = await this.getConnection(connectionName); const showQuery = `SHOW GRANTS FOR '${username}'@'${host}'`; const [rows] = await connection.execute(showQuery); let output = ''; if (rows.length === 0) { output = `Nenhum privilégio encontrado para '${username}'@'${host}'.`; } else { output += `## Privilégios de '${username}'@'${host}'\n\n`; for (const row of rows) { const grant = Object.values(row)[0]; output += `- ${grant}\n`; } } return output; } catch (error) { this.logger.error('Erro ao mostrar privilégios:', error); throw new Error(`Erro ao mostrar privilégios: ${error.message}`); } finally { if (connection) { await connection.end(); } } } async listPrivileges(options = {}) { const { database = null, table = null, connectionName = null } = options; let connection; try { connection = await this.getConnection(connectionName); let query = ` SELECT grantee, table_schema, table_name, privilege_type, is_grantable FROM information_schema.table_privileges WHERE 1=1 `; const params = []; if (database) { query += ' AND table_schema = ?'; params.push(database); } if (table) { query += ' AND table_name = ?'; params.push(table); } query += ' ORDER BY grantee, table_schema, table_name, privilege_type'; const [rows] = await connection.execute(query, params); let output = ''; if (rows.length === 0) { output = 'Nenhum privilégio encontrado.'; } else { output += '| Usuário | Database | Tabela | Privilégio | Pode Conceder |\n'; output += '|---------|----------|--------|------------|---------------|\n'; for (const row of rows) { output += `| ${row.grantee} | ${row.table_schema} | ${row.table_name} | ${row.privilege_type} | ${row.is_grantable} |\n`; } output += `\n**Total de privilégios:** ${rows.length}`; } return output; } catch (error) { this.logger.error('Erro ao listar privilégios:', error); throw new Error(`Erro ao listar privilégios: ${error.message}`); } finally { if (connection) { await connection.end(); } } } // ===== AUDITORIA E SEGURANÇA ===== async auditUserAccess(options = {}) { const { username = null, host = null, timeRange = '7d', includeFailedAttempts = true, connectionName = null } = options; let connection; try { connection = await this.getConnection(connectionName); // Converter timeRange para dias let days = 7; if (timeRange.endsWith('d')) { days = parseInt(timeRange.replace('d', '')); } else if (timeRange.endsWith('h')) { days = Math.ceil(parseInt(timeRange.replace('h', '')) / 24); } let query = ` SELECT user, host, event_timestamp, event_name, status, error_code, error_message FROM performance_schema.events_statements_summary_by_user_by_event_name WHERE event_timestamp > DATE_SUB(NOW(), INTERVAL ? DAY) `; const params = [days]; if (username) { query += ' AND user = ?'; params.push(username); } if (host) { query += ' AND host = ?'; params.push(host); } query += ' ORDER BY event_timestamp DESC LIMIT 100'; const [rows] = await connection.execute(query, params); let output = ''; if (rows.length === 0) { output = 'Nenhuma atividade encontrada no período especificado.'; } else { output += `## Auditoria de Acesso - Últimos ${days} dias\n\n`; output += '| Usuário | Host | Timestamp | Evento | Status | Código Erro |\n'; output += '|---------|------|-----------|--------|--------|-------------|\n'; for (const row of rows) { const timestamp = new Date(row.event_timestamp).toLocaleString(); const status = row.status || 'N/A'; const errorCode = row.error_code || 'N/A'; output += `| ${row.user} | ${row.host} | ${timestamp} | ${row.event_name} | ${status} | ${errorCode} |\n`; } output += `\n**Total de eventos:** ${rows.length}`; } return output; } catch (error) { this.logger.error('Erro ao auditar acesso de usuários:', error); throw new Error(`Erro ao auditar acesso de usuários: ${error.message}`); } finally { if (connection) { await connection.end(); } } } async checkPasswordPolicy(options = {}) { const { connectionName = null } = options; let connection; try { connection = await this.getConnection(connectionName); const queries = [ 'SHOW VARIABLES LIKE "validate_password%"', 'SELECT @@validate_password.policy', 'SELECT @@validate_password.length', 'SELECT @@validate_password.mixed_case_count', 'SELECT @@validate_password.number_count', 'SELECT @@validate_password.special_char_count' ]; let output = '## Política de Senhas do Sistema\n\n'; for (const query of queries) { try { const [rows] = await connection.execute(query); if (rows.length > 0) { for (const row of rows) { const key = Object.keys(row)[0]; const value = Object.values(row)[0]; output += `- **${key}:** ${value}\n`; } } } catch (error) { // Ignorar erros de variáveis não disponíveis continue; } } // Verificar usuários com senhas fracas const weakPasswordsQuery = ` SELECT user, host, password_last_changed, password_lifetime FROM mysql.user WHERE password_lifetime IS NULL OR password_lifetime = 0 OR password_last_changed < DATE_SUB(NOW(), INTERVAL 90 DAY) ORDER BY password_last_changed `; try { const [weakRows] = await connection.execute(weakPasswordsQuery); if (weakRows.length > 0) { output += '\n## ⚠️ Usuários com Senhas Fracas\n\n'; output += '| Usuário | Host | Última Alteração | Vida da Senha |\n'; output += '|---------|------|------------------|---------------|\n'; for (const row of weakRows) { const lastChanged = row.password_last_changed ? new Date(row.password_last_changed).toLocaleString() : 'N/A'; const lifetime = row.password_lifetime ? `${row.password_lifetime} dias` : 'Nunca expira'; output += `| ${row.user} | ${row.host} | ${lastChanged} | ${lifetime} |\n`; } } } catch (error) { output += `\n⚠️ Não foi possível verificar senhas fracas: ${error.message}`; } return output; } catch (error) { this.logger.error('Erro ao verificar política de senhas:', error); throw new Error(`Erro ao verificar política de senhas: ${error.message}`); } finally { if (connection) { await connection.end(); } } } // ===== MÉTODOS AUXILIARES ===== async userExists(connection, username, host) { try { const query = ` SELECT COUNT(*) FROM mysql.user WHERE user = ? AND host = ? `; const [rows] = await connection.execute(query, [username, host]); return rows[0]['COUNT(*)'] > 0; } catch (error) { return false; } } // ===== VALIDAÇÕES DE SEGURANÇA ===== validateUsername(username) { if (!username || typeof username !== 'string') { throw new Error('Nome de usuário deve ser uma string válida'); } if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(username)) { throw new Error('Nome de usuário deve conter apenas letras, números e underscore, começando com letra'); } if (username.length > 16) { throw new Error('Nome de usuário não pode exceder 16 caracteres'); } } validatePassword(password) { if (!password || typeof password !== 'string') { throw new Error('Senha deve ser uma string válida'); } if (password.length < 8) { throw new Error('Senha deve ter pelo menos 8 caracteres'); } if (password.length > 128) { throw new Error('Senha não pode exceder 128 caracteres'); } } validatePrivileges(privileges) { if (!privileges || !Array.isArray(privileges) || privileges.length === 0) { throw new Error('Lista de privilégios é obrigatória'); } const validPrivileges = [ 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'RELOAD', 'SHUTDOWN', 'PROCESS', 'FILE', 'GRANT', 'REFERENCES', 'INDEX', 'ALTER', 'SHOW DATABASES', 'SUPER', 'CREATE TEMPORARY TABLES', 'LOCK TABLES', 'EXECUTE', 'REPLICATION SLAVE', 'REPLICATION CLIENT', 'CREATE VIEW', 'SHOW VIEW', 'CREATE ROUTINE', 'ALTER ROUTINE', 'CREATE USER', 'EVENT', 'TRIGGER', 'CREATE TABLESPACE', 'ALL', 'ALL PRIVILEGES' ]; for (const privilege of privileges) { if (!validPrivileges.includes(privilege.toUpperCase())) { throw new Error(`Privilégio inválido: ${privilege}`); } } } }