feat: Sistema MDM completo implementado

- Rotas API para gerenciamento de políticas (/api/policies)
- Rotas MDM para cliente (/api/mdm)
- Tabelas PostgreSQL para políticas e execuções
- Cliente Python com executor MDM (CLIENT_MDM.py)
- Suporte a 8 tipos de políticas:
  * Windows Update (forçar atualizações)
  * Instalar/Desinstalar Software
  * Scripts PowerShell
  * Modificar Registro
  * Reiniciar dispositivos
  * Limpeza de sistema
  * Configurar Firewall
- Templates pré-configurados
- Histórico de execuções
- Documentação completa (SISTEMA_MDM.md)
- Exemplo de integração

Sistema similar ao JumpCloud MDM, permitindo gerenciamento remoto
completo de dispositivos Windows.
This commit is contained in:
Sérgio Corrêa
2025-11-16 23:12:30 +00:00
parent 6086c13be7
commit f32eee53f0
8 changed files with 2458 additions and 0 deletions

250
backend/routes/mdm.js Normal file
View File

@@ -0,0 +1,250 @@
// Rotas MDM para cliente Windows buscar e executar comandos
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// Cliente busca comandos pendentes
router.post('/commands/poll', async (req, res) => {
try {
const { device_id } = req.body;
if (!device_id) {
return res.status(400).json({ success: false, message: 'device_id é obrigatório' });
}
// Buscar comandos pendentes ordenados por prioridade
const result = await pool.query(`
SELECT
pc.*,
p.name as policy_name,
p.description as policy_description
FROM policy_commands pc
INNER JOIN policies p ON pc.policy_id = p.id
WHERE pc.device_id = $1
AND pc.status IN ('pending', 'sent')
AND pc.retry_count < pc.max_retries
ORDER BY pc.priority DESC, pc.created_at ASC
LIMIT 10
`, [device_id]);
if (result.rows.length > 0) {
// Marcar comandos como 'sent'
const commandIds = result.rows.map(r => r.id);
await pool.query(`
UPDATE policy_commands
SET status = 'sent', sent_at = NOW()
WHERE id = ANY($1) AND status = 'pending'
`, [commandIds]);
console.log(`📤 Enviando ${result.rows.length} comando(s) para ${device_id}`);
}
res.json({
success: true,
commands: result.rows.map(cmd => ({
command_id: cmd.id,
policy_id: cmd.policy_id,
policy_name: cmd.policy_name,
policy_description: cmd.policy_description,
command_type: cmd.command_type,
command_data: cmd.command_data,
priority: cmd.priority
}))
});
} catch (error) {
console.error('Erro ao buscar comandos:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Cliente reporta início de execução
router.post('/commands/:id/start', async (req, res) => {
try {
const { id } = req.params;
await pool.query(`
UPDATE policy_commands
SET status = 'executing'
WHERE id = $1
`, [id]);
res.json({ success: true, message: 'Execução iniciada' });
} catch (error) {
console.error('Erro ao iniciar execução:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Cliente reporta resultado da execução
router.post('/commands/:id/result', async (req, res) => {
try {
const { id } = req.params;
const {
status, // 'success' ou 'failed'
result,
error_message,
duration_seconds
} = req.body;
// Buscar informações do comando
const commandResult = await pool.query(`
SELECT device_id, policy_id
FROM policy_commands
WHERE id = $1
`, [id]);
if (commandResult.rows.length === 0) {
return res.status(404).json({ success: false, message: 'Comando não encontrado' });
}
const { device_id, policy_id } = commandResult.rows[0];
// Atualizar comando
if (status === 'success') {
await pool.query(`
UPDATE policy_commands
SET status = $1,
completed_at = NOW(),
result = $2
WHERE id = $3
`, [status, JSON.stringify(result), id]);
} else {
// Se falhou, incrementar retry_count
await pool.query(`
UPDATE policy_commands
SET status = $1,
completed_at = NOW(),
result = $2,
error_message = $3,
retry_count = retry_count + 1
WHERE id = $4
`, [status, JSON.stringify(result), error_message, id]);
}
// Registrar no histórico
await pool.query(`
INSERT INTO policy_executions (
device_id,
policy_id,
command_id,
status,
duration_seconds,
result,
error_message
) VALUES ($1, $2, $3, $4, $5, $6, $7)
`, [
device_id,
policy_id,
id,
status,
duration_seconds,
JSON.stringify(result),
error_message
]);
console.log(`${status === 'success' ? '✅' : '❌'} Comando ${id} executado: ${status}`);
res.json({ success: true, message: 'Resultado registrado' });
} catch (error) {
console.error('Erro ao registrar resultado:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Buscar políticas aplicadas ao dispositivo
router.get('/policies/:device_id', async (req, res) => {
try {
const { device_id } = req.params;
const result = await pool.query(`
SELECT
p.*,
dp.assigned_at,
pe.last_execution,
pe.last_status
FROM policies p
INNER JOIN device_policies dp ON p.id = dp.policy_id
LEFT JOIN LATERAL (
SELECT
executed_at as last_execution,
status as last_status
FROM policy_executions
WHERE device_id = $1 AND policy_id = p.id
ORDER BY executed_at DESC
LIMIT 1
) pe ON true
WHERE dp.device_id = $1 AND p.enabled = true
ORDER BY p.priority DESC, p.name
`, [device_id]);
res.json({ success: true, policies: result.rows });
} catch (error) {
console.error('Erro ao buscar políticas:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Listar templates de políticas disponíveis
router.get('/templates', async (req, res) => {
try {
const { category } = req.query;
let query = 'SELECT * FROM policy_templates';
const params = [];
if (category) {
query += ' WHERE category = $1';
params.push(category);
}
query += ' ORDER BY category, name';
const result = await pool.query(query, params);
res.json({ success: true, templates: result.rows });
} catch (error) {
console.error('Erro ao buscar templates:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Estatísticas de comandos por dispositivo
router.get('/stats/:device_id', async (req, res) => {
try {
const { device_id } = req.params;
const stats = await pool.query(`
SELECT
COUNT(*) FILTER (WHERE status = 'pending') as pending,
COUNT(*) FILTER (WHERE status = 'sent') as sent,
COUNT(*) FILTER (WHERE status = 'executing') as executing,
COUNT(*) FILTER (WHERE status = 'success') as success,
COUNT(*) FILTER (WHERE status = 'failed') as failed
FROM policy_commands
WHERE device_id = $1
AND created_at > NOW() - INTERVAL '7 days'
`, [device_id]);
const recentExecutions = await pool.query(`
SELECT
status,
COUNT(*) as count
FROM policy_executions
WHERE device_id = $1
AND executed_at > NOW() - INTERVAL '30 days'
GROUP BY status
`, [device_id]);
res.json({
success: true,
commands: stats.rows[0],
recent_executions: recentExecutions.rows
});
} catch (error) {
console.error('Erro ao buscar estatísticas:', error);
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;

314
backend/routes/policies.js Normal file
View File

@@ -0,0 +1,314 @@
// Rotas para gerenciamento de políticas MDM
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// Listar todas as políticas
router.get('/', async (req, res) => {
try {
const result = await pool.query(`
SELECT
p.*,
COUNT(DISTINCT dp.device_id) as devices_count,
COUNT(DISTINCT pe.id) FILTER (WHERE pe.status = 'success') as success_count,
COUNT(DISTINCT pe.id) FILTER (WHERE pe.status = 'failed') as failed_count
FROM policies p
LEFT JOIN device_policies dp ON p.id = dp.policy_id
LEFT JOIN policy_executions pe ON p.id = pe.policy_id
GROUP BY p.id
ORDER BY p.created_at DESC
`);
res.json({ success: true, policies: result.rows });
} catch (error) {
console.error('Erro ao listar políticas:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Criar nova política
router.post('/', async (req, res) => {
try {
const {
name,
description,
type,
config,
enabled,
schedule,
priority
} = req.body;
const result = await pool.query(`
INSERT INTO policies (
name, description, type, config, enabled, schedule, priority
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`, [name, description, type, JSON.stringify(config), enabled, schedule, priority || 5]);
console.log(`✅ Política criada: ${name} (${type})`);
res.json({ success: true, policy: result.rows[0] });
} catch (error) {
console.error('Erro ao criar política:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Atualizar política
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const {
name,
description,
type,
config,
enabled,
schedule,
priority
} = req.body;
const result = await pool.query(`
UPDATE policies
SET name = $1,
description = $2,
type = $3,
config = $4,
enabled = $5,
schedule = $6,
priority = $7,
updated_at = NOW()
WHERE id = $8
RETURNING *
`, [name, description, type, JSON.stringify(config), enabled, schedule, priority, id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: 'Política não encontrada' });
}
console.log(`✅ Política atualizada: ${name}`);
res.json({ success: true, policy: result.rows[0] });
} catch (error) {
console.error('Erro ao atualizar política:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Deletar política
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// Remover associações com dispositivos
await pool.query('DELETE FROM device_policies WHERE policy_id = $1', [id]);
// Remover política
const result = await pool.query('DELETE FROM policies WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: 'Política não encontrada' });
}
console.log(`✅ Política deletada: ${result.rows[0].name}`);
res.json({ success: true, message: 'Política deletada com sucesso' });
} catch (error) {
console.error('Erro ao deletar política:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Associar política a dispositivos
router.post('/:id/devices', async (req, res) => {
try {
const { id } = req.params;
const { device_ids } = req.body; // Array de device_ids
if (!Array.isArray(device_ids) || device_ids.length === 0) {
return res.status(400).json({ success: false, message: 'device_ids deve ser um array não vazio' });
}
// Inserir associações
const values = device_ids.map(device_id => `('${device_id}', ${id})`).join(',');
await pool.query(`
INSERT INTO device_policies (device_id, policy_id)
VALUES ${values}
ON CONFLICT (device_id, policy_id) DO NOTHING
`);
console.log(`✅ Política ${id} associada a ${device_ids.length} dispositivo(s)`);
res.json({ success: true, message: `Política aplicada a ${device_ids.length} dispositivo(s)` });
} catch (error) {
console.error('Erro ao associar política:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Remover política de dispositivos
router.delete('/:id/devices', async (req, res) => {
try {
const { id } = req.params;
const { device_ids } = req.body;
if (!Array.isArray(device_ids) || device_ids.length === 0) {
return res.status(400).json({ success: false, message: 'device_ids deve ser um array não vazio' });
}
const result = await pool.query(`
DELETE FROM device_policies
WHERE policy_id = $1 AND device_id = ANY($2)
`, [id, device_ids]);
console.log(`✅ Política ${id} removida de ${result.rowCount} dispositivo(s)`);
res.json({ success: true, message: `Política removida de ${result.rowCount} dispositivo(s)` });
} catch (error) {
console.error('Erro ao remover política:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Executar política agora (forçar execução imediata)
router.post('/:id/execute', async (req, res) => {
try {
const { id } = req.params;
const { device_ids } = req.body; // Se não fornecido, executa em todos os dispositivos com a política
// Buscar política
const policyResult = await pool.query('SELECT * FROM policies WHERE id = $1', [id]);
if (policyResult.rows.length === 0) {
return res.status(404).json({ success: false, message: 'Política não encontrada' });
}
const policy = policyResult.rows[0];
// Buscar dispositivos
let devicesQuery;
if (device_ids && device_ids.length > 0) {
devicesQuery = await pool.query(`
SELECT DISTINCT d.device_id, d.device_name
FROM devices d
WHERE d.device_id = ANY($1) AND d.status = 'online'
`, [device_ids]);
} else {
devicesQuery = await pool.query(`
SELECT DISTINCT d.device_id, d.device_name
FROM devices d
INNER JOIN device_policies dp ON d.device_id = dp.device_id
WHERE dp.policy_id = $1 AND d.status = 'online'
`, [id]);
}
if (devicesQuery.rows.length === 0) {
return res.json({
success: false,
message: 'Nenhum dispositivo online encontrado para executar a política'
});
}
// Criar comandos de execução para cada dispositivo
const commands = [];
for (const device of devicesQuery.rows) {
const commandResult = await pool.query(`
INSERT INTO policy_commands (
device_id,
policy_id,
command_type,
command_data,
status
) VALUES ($1, $2, $3, $4, 'pending')
RETURNING id
`, [
device.device_id,
id,
policy.type,
JSON.stringify(policy.config)
]);
commands.push({
command_id: commandResult.rows[0].id,
device_id: device.device_id,
device_name: device.device_name
});
}
console.log(`✅ Política ${policy.name} enfileirada para ${commands.length} dispositivo(s)`);
res.json({
success: true,
message: `Política enfileirada para ${commands.length} dispositivo(s)`,
commands
});
} catch (error) {
console.error('Erro ao executar política:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Ver histórico de execuções de uma política
router.get('/:id/executions', async (req, res) => {
try {
const { id } = req.params;
const { limit = 50, offset = 0 } = req.query;
const result = await pool.query(`
SELECT
pe.*,
d.device_name,
d.hostname
FROM policy_executions pe
INNER JOIN devices d ON pe.device_id = d.device_id
WHERE pe.policy_id = $1
ORDER BY pe.executed_at DESC
LIMIT $2 OFFSET $3
`, [id, limit, offset]);
const countResult = await pool.query(
'SELECT COUNT(*) FROM policy_executions WHERE policy_id = $1',
[id]
);
res.json({
success: true,
executions: result.rows,
total: parseInt(countResult.rows[0].count)
});
} catch (error) {
console.error('Erro ao buscar execuções:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// Ver dispositivos com a política aplicada
router.get('/:id/devices', async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT
d.*,
dp.assigned_at,
pe.last_execution,
pe.last_status
FROM devices d
INNER JOIN device_policies dp ON d.device_id = dp.device_id
LEFT JOIN LATERAL (
SELECT
executed_at as last_execution,
status as last_status
FROM policy_executions
WHERE device_id = d.device_id AND policy_id = $1
ORDER BY executed_at DESC
LIMIT 1
) pe ON true
WHERE dp.policy_id = $1
ORDER BY d.device_name
`, [id]);
res.json({ success: true, devices: result.rows });
} catch (error) {
console.error('Erro ao buscar dispositivos:', error);
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;