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:
281
backend/create_mdm_tables.sql
Normal file
281
backend/create_mdm_tables.sql
Normal file
@@ -0,0 +1,281 @@
|
||||
-- Tabelas para Sistema MDM (Mobile Device Management)
|
||||
|
||||
-- Tabela de políticas
|
||||
CREATE TABLE IF NOT EXISTS policies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
type VARCHAR(50) NOT NULL, -- 'windows_update', 'software_install', 'registry', 'script', 'security', etc.
|
||||
config JSONB NOT NULL DEFAULT '{}', -- Configuração específica da política
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
schedule VARCHAR(100), -- Cron expression ou 'immediate'
|
||||
priority INTEGER DEFAULT 5, -- 1-10, 10 = mais alta
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices para policies
|
||||
CREATE INDEX IF NOT EXISTS idx_policies_type ON policies(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_policies_enabled ON policies(enabled);
|
||||
|
||||
-- Tabela de associação: dispositivos x políticas
|
||||
CREATE TABLE IF NOT EXISTS device_policies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id VARCHAR(255) NOT NULL,
|
||||
policy_id INTEGER NOT NULL,
|
||||
assigned_at TIMESTAMP DEFAULT NOW(),
|
||||
FOREIGN KEY (device_id) REFERENCES devices(device_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE,
|
||||
UNIQUE(device_id, policy_id)
|
||||
);
|
||||
|
||||
-- Índices para device_policies
|
||||
CREATE INDEX IF NOT EXISTS idx_device_policies_device ON device_policies(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_device_policies_policy ON device_policies(policy_id);
|
||||
|
||||
-- Tabela de comandos de política (fila de execução)
|
||||
CREATE TABLE IF NOT EXISTS policy_commands (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id VARCHAR(255) NOT NULL,
|
||||
policy_id INTEGER NOT NULL,
|
||||
command_type VARCHAR(50) NOT NULL,
|
||||
command_data JSONB NOT NULL DEFAULT '{}',
|
||||
status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'sent', 'executing', 'success', 'failed', 'cancelled'
|
||||
priority INTEGER DEFAULT 5,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
sent_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
result JSONB,
|
||||
error_message TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
max_retries INTEGER DEFAULT 3,
|
||||
FOREIGN KEY (device_id) REFERENCES devices(device_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Índices para policy_commands
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_commands_device ON policy_commands(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_commands_status ON policy_commands(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_commands_created ON policy_commands(created_at DESC);
|
||||
|
||||
-- Tabela de histórico de execução de políticas
|
||||
CREATE TABLE IF NOT EXISTS policy_executions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id VARCHAR(255) NOT NULL,
|
||||
policy_id INTEGER NOT NULL,
|
||||
command_id INTEGER,
|
||||
status VARCHAR(20) NOT NULL, -- 'success', 'failed', 'partial'
|
||||
executed_at TIMESTAMP DEFAULT NOW(),
|
||||
duration_seconds INTEGER,
|
||||
result JSONB,
|
||||
error_message TEXT,
|
||||
details TEXT,
|
||||
FOREIGN KEY (device_id) REFERENCES devices(device_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (command_id) REFERENCES policy_commands(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Índices para policy_executions
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_executions_device ON policy_executions(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_executions_policy ON policy_executions(policy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_executions_executed ON policy_executions(executed_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_executions_status ON policy_executions(status);
|
||||
|
||||
-- Tabela de templates de políticas (pré-configuradas)
|
||||
CREATE TABLE IF NOT EXISTS policy_templates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
category VARCHAR(50), -- 'security', 'updates', 'software', 'configuration', etc.
|
||||
type VARCHAR(50) NOT NULL,
|
||||
default_config JSONB NOT NULL DEFAULT '{}',
|
||||
icon VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices para policy_templates
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_templates_category ON policy_templates(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_templates_type ON policy_templates(type);
|
||||
|
||||
-- Inserir templates padrão de políticas
|
||||
INSERT INTO policy_templates (name, description, category, type, default_config, icon) VALUES
|
||||
(
|
||||
'Forçar Windows Update',
|
||||
'Força a verificação e instalação de atualizações do Windows',
|
||||
'updates',
|
||||
'windows_update',
|
||||
'{
|
||||
"action": "force_check_and_install",
|
||||
"reboot_if_required": true,
|
||||
"reboot_delay_minutes": 30,
|
||||
"include_optional": false,
|
||||
"max_wait_hours": 24
|
||||
}',
|
||||
'update'
|
||||
),
|
||||
(
|
||||
'Instalar Software',
|
||||
'Instala software via URL de download ou repositório',
|
||||
'software',
|
||||
'software_install',
|
||||
'{
|
||||
"software_name": "",
|
||||
"download_url": "",
|
||||
"install_args": "/S /SILENT",
|
||||
"verify_install": true,
|
||||
"uninstall_first": false
|
||||
}',
|
||||
'download'
|
||||
),
|
||||
(
|
||||
'Executar Script PowerShell',
|
||||
'Executa um script PowerShell personalizado',
|
||||
'configuration',
|
||||
'powershell_script',
|
||||
'{
|
||||
"script": "",
|
||||
"run_as_admin": true,
|
||||
"timeout_minutes": 30,
|
||||
"execution_policy": "Bypass"
|
||||
}',
|
||||
'terminal'
|
||||
),
|
||||
(
|
||||
'Configurar Registro do Windows',
|
||||
'Modifica chaves do registro do Windows',
|
||||
'configuration',
|
||||
'registry',
|
||||
'{
|
||||
"hive": "HKLM",
|
||||
"key": "",
|
||||
"value_name": "",
|
||||
"value_data": "",
|
||||
"value_type": "String",
|
||||
"create_if_missing": true
|
||||
}',
|
||||
'settings'
|
||||
),
|
||||
(
|
||||
'Reiniciar Dispositivo',
|
||||
'Reinicia o dispositivo Windows',
|
||||
'maintenance',
|
||||
'reboot',
|
||||
'{
|
||||
"delay_minutes": 5,
|
||||
"force": false,
|
||||
"message": "O sistema será reiniciado em breve"
|
||||
}',
|
||||
'refresh'
|
||||
),
|
||||
(
|
||||
'Limpar Arquivos Temporários',
|
||||
'Remove arquivos temporários e cache para liberar espaço',
|
||||
'maintenance',
|
||||
'cleanup',
|
||||
'{
|
||||
"clear_temp": true,
|
||||
"clear_windows_temp": true,
|
||||
"clear_browser_cache": true,
|
||||
"clear_recycle_bin": true,
|
||||
"min_age_days": 7
|
||||
}',
|
||||
'trash'
|
||||
),
|
||||
(
|
||||
'Configurar Firewall',
|
||||
'Adiciona ou modifica regras do Windows Firewall',
|
||||
'security',
|
||||
'firewall',
|
||||
'{
|
||||
"action": "add_rule",
|
||||
"rule_name": "",
|
||||
"direction": "inbound",
|
||||
"protocol": "TCP",
|
||||
"port": "",
|
||||
"action_type": "allow"
|
||||
}',
|
||||
'shield'
|
||||
),
|
||||
(
|
||||
'Desinstalar Software',
|
||||
'Remove software instalado no dispositivo',
|
||||
'software',
|
||||
'software_uninstall',
|
||||
'{
|
||||
"software_name": "",
|
||||
"silent": true,
|
||||
"force": false
|
||||
}',
|
||||
'delete'
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- View para resumo de políticas por dispositivo
|
||||
CREATE OR REPLACE VIEW device_policies_summary AS
|
||||
SELECT
|
||||
d.device_id,
|
||||
d.device_name,
|
||||
COUNT(DISTINCT dp.policy_id) as total_policies,
|
||||
COUNT(DISTINCT CASE WHEN p.enabled = true THEN dp.policy_id END) as active_policies,
|
||||
MAX(pe.executed_at) as last_execution,
|
||||
COUNT(DISTINCT CASE WHEN pe.status = 'success' THEN pe.id END) as successful_executions,
|
||||
COUNT(DISTINCT CASE WHEN pe.status = 'failed' THEN pe.id END) as failed_executions
|
||||
FROM devices d
|
||||
LEFT JOIN device_policies dp ON d.device_id = dp.device_id
|
||||
LEFT JOIN policies p ON dp.policy_id = p.id
|
||||
LEFT JOIN policy_executions pe ON d.device_id = pe.device_id
|
||||
GROUP BY d.device_id, d.device_name;
|
||||
|
||||
-- View para comandos pendentes por dispositivo
|
||||
CREATE OR REPLACE VIEW pending_commands_by_device AS
|
||||
SELECT
|
||||
device_id,
|
||||
COUNT(*) as pending_count,
|
||||
MIN(created_at) as oldest_command,
|
||||
MAX(priority) as highest_priority
|
||||
FROM policy_commands
|
||||
WHERE status IN ('pending', 'sent')
|
||||
GROUP BY device_id;
|
||||
|
||||
-- Comentários nas tabelas
|
||||
COMMENT ON TABLE policies IS 'Políticas MDM que podem ser aplicadas aos dispositivos';
|
||||
COMMENT ON TABLE device_policies IS 'Associação entre dispositivos e políticas';
|
||||
COMMENT ON TABLE policy_commands IS 'Fila de comandos de política a serem executados';
|
||||
COMMENT ON TABLE policy_executions IS 'Histórico de execução de políticas';
|
||||
COMMENT ON TABLE policy_templates IS 'Templates pré-configurados de políticas comuns';
|
||||
|
||||
COMMENT ON COLUMN policies.config IS 'Configuração JSON específica de cada tipo de política';
|
||||
COMMENT ON COLUMN policies.schedule IS 'Quando executar: immediate, cron expression, ou NULL para manual';
|
||||
COMMENT ON COLUMN policy_commands.status IS 'Status do comando: pending, sent, executing, success, failed, cancelled';
|
||||
COMMENT ON COLUMN policy_commands.retry_count IS 'Número de tentativas de execução';
|
||||
|
||||
-- Função para limpar execuções antigas (manter apenas últimos 90 dias)
|
||||
CREATE OR REPLACE FUNCTION cleanup_old_policy_executions()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
deleted_count INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM policy_executions
|
||||
WHERE executed_at < NOW() - INTERVAL '90 days';
|
||||
|
||||
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
||||
RETURN deleted_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger para atualizar updated_at em policies
|
||||
CREATE OR REPLACE FUNCTION update_policies_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_policies_timestamp
|
||||
BEFORE UPDATE ON policies
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_policies_timestamp();
|
||||
|
||||
COMMENT ON FUNCTION cleanup_old_policy_executions IS 'Remove execuções de políticas com mais de 90 dias';
|
||||
|
||||
250
backend/routes/mdm.js
Normal file
250
backend/routes/mdm.js
Normal 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
314
backend/routes/policies.js
Normal 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;
|
||||
|
||||
@@ -48,6 +48,8 @@ const activityRoutes = require('./routes/activity');
|
||||
const keysRoutes = require('./routes/keys');
|
||||
const usersRoutes = require('./routes/users');
|
||||
const teamsRoutes = require("./routes/teams");
|
||||
const policiesRoutes = require('./routes/policies');
|
||||
const mdmRoutes = require('./routes/mdm');
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/dashboard', dashboardRoutes);
|
||||
@@ -57,6 +59,8 @@ app.use("/api/activity", activityRoutes);
|
||||
app.use('/api/keys', keysRoutes);
|
||||
app.use('/api/users', usersRoutes);
|
||||
app.use("/api/teams", teamsRoutes);
|
||||
app.use('/api/policies', policiesRoutes);
|
||||
app.use('/api/mdm', mdmRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user