diff --git a/CLIENT_MDM.py b/CLIENT_MDM.py new file mode 100644 index 0000000..c852c96 --- /dev/null +++ b/CLIENT_MDM.py @@ -0,0 +1,472 @@ +# Módulo MDM para o Cliente NoIdle +# Executa políticas e comandos remotos + +import subprocess +import time +import json +import requests +import winreg +import os +from datetime import datetime + +class MDMExecutor: + def __init__(self, api_url, device_id): + self.api_url = api_url + self.device_id = device_id + self.handlers = { + 'windows_update': self.execute_windows_update, + 'software_install': self.execute_software_install, + 'software_uninstall': self.execute_software_uninstall, + 'powershell_script': self.execute_powershell_script, + 'registry': self.execute_registry, + 'reboot': self.execute_reboot, + 'cleanup': self.execute_cleanup, + 'firewall': self.execute_firewall + } + + def poll_commands(self): + """Busca comandos pendentes da API""" + try: + response = requests.post( + f"{self.api_url}/mdm/commands/poll", + json={'device_id': self.device_id}, + timeout=10 + ) + + if response.status_code == 200: + data = response.json() + if data.get('success'): + return data.get('commands', []) + return [] + except Exception as e: + print(f"❌ Erro ao buscar comandos: {e}") + return [] + + def report_start(self, command_id): + """Reporta início de execução""" + try: + requests.post( + f"{self.api_url}/mdm/commands/{command_id}/start", + timeout=5 + ) + except Exception as e: + print(f"⚠️ Erro ao reportar início: {e}") + + def report_result(self, command_id, status, result=None, error_message=None, duration=0): + """Reporta resultado da execução""" + try: + response = requests.post( + f"{self.api_url}/mdm/commands/{command_id}/result", + json={ + 'status': status, + 'result': result, + 'error_message': error_message, + 'duration_seconds': duration + }, + timeout=10 + ) + return response.status_code == 200 + except Exception as e: + print(f"❌ Erro ao reportar resultado: {e}") + return False + + def execute_command(self, command): + """Executa um comando MDM""" + command_id = command['command_id'] + command_type = command['command_type'] + command_data = command['command_data'] + + print(f"🔧 Executando: {command.get('policy_name', command_type)}") + + # Reportar início + self.report_start(command_id) + + start_time = time.time() + + try: + # Executar comando baseado no tipo + if command_type in self.handlers: + result = self.handlers[command_type](command_data) + duration = int(time.time() - start_time) + + # Reportar sucesso + self.report_result(command_id, 'success', result, None, duration) + print(f"✅ Comando executado com sucesso em {duration}s") + return True + else: + error = f"Tipo de comando não suportado: {command_type}" + self.report_result(command_id, 'failed', None, error, 0) + print(f"❌ {error}") + return False + + except Exception as e: + duration = int(time.time() - start_time) + error_message = str(e) + self.report_result(command_id, 'failed', None, error_message, duration) + print(f"❌ Erro ao executar comando: {e}") + return False + + def execute_windows_update(self, config): + """Força Windows Update""" + print("🔄 Forçando Windows Update...") + + try: + # Script PowerShell para forçar update + ps_script = """ + $UpdateSession = New-Object -ComObject Microsoft.Update.Session + $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() + + Write-Host "Procurando atualizações..." + $SearchResult = $UpdateSearcher.Search("IsInstalled=0") + + if ($SearchResult.Updates.Count -eq 0) { + Write-Host "Nenhuma atualização disponível" + exit 0 + } + + Write-Host "Encontradas $($SearchResult.Updates.Count) atualizações" + + $UpdatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl + foreach ($Update in $SearchResult.Updates) { + $UpdatesToDownload.Add($Update) | Out-Null + } + + Write-Host "Baixando atualizações..." + $Downloader = $UpdateSession.CreateUpdateDownloader() + $Downloader.Updates = $UpdatesToDownload + $DownloadResult = $Downloader.Download() + + Write-Host "Instalando atualizações..." + $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl + foreach ($Update in $SearchResult.Updates) { + if ($Update.IsDownloaded) { + $UpdatesToInstall.Add($Update) | Out-Null + } + } + + $Installer = $UpdateSession.CreateUpdateInstaller() + $Installer.Updates = $UpdatesToInstall + $InstallResult = $Installer.Install() + + Write-Host "Resultado: $($InstallResult.ResultCode)" + + if ($InstallResult.RebootRequired) { + Write-Host "REBOOT_REQUIRED" + } + """ + + result = subprocess.run( + ['powershell', '-ExecutionPolicy', 'Bypass', '-Command', ps_script], + capture_output=True, + text=True, + timeout=1800 # 30 minutos + ) + + output = result.stdout + reboot_required = "REBOOT_REQUIRED" in output + + # Se reboot for necessário e configurado, agendar + if reboot_required and config.get('reboot_if_required'): + delay = config.get('reboot_delay_minutes', 30) + self.schedule_reboot(delay) + + return { + 'updates_found': 'atualizações' in output.lower(), + 'reboot_required': reboot_required, + 'output': output[:500] # Limitar tamanho + } + + except subprocess.TimeoutExpired: + raise Exception("Timeout ao executar Windows Update") + except Exception as e: + raise Exception(f"Erro no Windows Update: {str(e)}") + + def execute_software_install(self, config): + """Instala software""" + software_name = config.get('software_name', 'Unknown') + download_url = config.get('download_url') + install_args = config.get('install_args', '/S') + + print(f"📦 Instalando: {software_name}") + + if not download_url: + raise Exception("URL de download não fornecida") + + try: + # Baixar instalador + import tempfile + temp_dir = tempfile.gettempdir() + filename = download_url.split('/')[-1] or 'installer.exe' + installer_path = os.path.join(temp_dir, filename) + + print(f"⬇️ Baixando de {download_url}...") + response = requests.get(download_url, timeout=300) + response.raise_for_status() + + with open(installer_path, 'wb') as f: + f.write(response.content) + + print(f"🔧 Instalando...") + # Executar instalador + result = subprocess.run( + [installer_path] + install_args.split(), + capture_output=True, + text=True, + timeout=600 + ) + + # Limpar + try: + os.remove(installer_path) + except: + pass + + return { + 'software': software_name, + 'exit_code': result.returncode, + 'success': result.returncode == 0 + } + + except Exception as e: + raise Exception(f"Erro ao instalar {software_name}: {str(e)}") + + def execute_software_uninstall(self, config): + """Desinstala software""" + software_name = config.get('software_name') + silent = config.get('silent', True) + + print(f"🗑️ Desinstalando: {software_name}") + + # Usar wmic para desinstalar + ps_script = f""" + $software = Get-WmiObject -Class Win32_Product | Where-Object {{ $_.Name -like "*{software_name}*" }} + if ($software) {{ + $software.Uninstall() + Write-Host "Desinstalado: $($software.Name)" + }} else {{ + Write-Host "Software não encontrado" + }} + """ + + result = subprocess.run( + ['powershell', '-ExecutionPolicy', 'Bypass', '-Command', ps_script], + capture_output=True, + text=True, + timeout=300 + ) + + return { + 'software': software_name, + 'output': result.stdout, + 'success': 'Desinstalado' in result.stdout + } + + def execute_powershell_script(self, config): + """Executa script PowerShell""" + script = config.get('script', '') + run_as_admin = config.get('run_as_admin', True) + timeout_minutes = config.get('timeout_minutes', 30) + execution_policy = config.get('execution_policy', 'Bypass') + + print(f"💻 Executando script PowerShell...") + + if not script: + raise Exception("Script não fornecido") + + result = subprocess.run( + ['powershell', '-ExecutionPolicy', execution_policy, '-Command', script], + capture_output=True, + text=True, + timeout=timeout_minutes * 60 + ) + + return { + 'exit_code': result.returncode, + 'stdout': result.stdout[:1000], # Limitar tamanho + 'stderr': result.stderr[:1000], + 'success': result.returncode == 0 + } + + def execute_registry(self, config): + """Modifica registro do Windows""" + hive = config.get('hive', 'HKLM') + key = config.get('key') + value_name = config.get('value_name') + value_data = config.get('value_data') + value_type = config.get('value_type', 'String') + + print(f"📝 Modificando registro: {hive}\\{key}") + + # Mapear hive + hive_map = { + 'HKLM': winreg.HKEY_LOCAL_MACHINE, + 'HKCU': winreg.HKEY_CURRENT_USER, + 'HKCR': winreg.HKEY_CLASSES_ROOT, + 'HKU': winreg.HKEY_USERS + } + + # Mapear tipo + type_map = { + 'String': winreg.REG_SZ, + 'DWORD': winreg.REG_DWORD, + 'Binary': winreg.REG_BINARY, + 'MultiString': winreg.REG_MULTI_SZ + } + + try: + reg_hive = hive_map.get(hive, winreg.HKEY_LOCAL_MACHINE) + reg_type = type_map.get(value_type, winreg.REG_SZ) + + # Abrir ou criar chave + reg_key = winreg.CreateKey(reg_hive, key) + + # Converter valor se necessário + if value_type == 'DWORD': + value_data = int(value_data) + + # Setar valor + winreg.SetValueEx(reg_key, value_name, 0, reg_type, value_data) + winreg.CloseKey(reg_key) + + return { + 'hive': hive, + 'key': key, + 'value_name': value_name, + 'success': True + } + except Exception as e: + raise Exception(f"Erro ao modificar registro: {str(e)}") + + def execute_reboot(self, config): + """Reinicia o sistema""" + delay_minutes = config.get('delay_minutes', 5) + force = config.get('force', False) + message = config.get('message', 'O sistema será reiniciado') + + print(f"🔄 Agendando reinicialização em {delay_minutes} minutos...") + + delay_seconds = delay_minutes * 60 + force_flag = '/f' if force else '' + + subprocess.run( + ['shutdown', '/r', '/t', str(delay_seconds), '/c', message, force_flag], + check=False + ) + + return { + 'delay_minutes': delay_minutes, + 'scheduled': True + } + + def execute_cleanup(self, config): + """Limpa arquivos temporários""" + print("🧹 Limpando arquivos temporários...") + + cleaned = { + 'temp': False, + 'windows_temp': False, + 'browser_cache': False, + 'recycle_bin': False + } + + try: + if config.get('clear_temp', True): + temp_path = os.environ.get('TEMP') + # Limpar %TEMP% + subprocess.run( + ['powershell', '-Command', f'Remove-Item -Path "{temp_path}\\*" -Recurse -Force -ErrorAction SilentlyContinue'], + check=False + ) + cleaned['temp'] = True + + if config.get('clear_windows_temp', True): + # Limpar C:\Windows\Temp + subprocess.run( + ['powershell', '-Command', 'Remove-Item -Path "C:\\Windows\\Temp\\*" -Recurse -Force -ErrorAction SilentlyContinue'], + check=False + ) + cleaned['windows_temp'] = True + + if config.get('clear_recycle_bin', True): + # Esvaziar lixeira + subprocess.run( + ['powershell', '-Command', 'Clear-RecycleBin -Force -ErrorAction SilentlyContinue'], + check=False + ) + cleaned['recycle_bin'] = True + + return cleaned + except Exception as e: + raise Exception(f"Erro na limpeza: {str(e)}") + + def execute_firewall(self, config): + """Configura regras de firewall""" + action = config.get('action', 'add_rule') + rule_name = config.get('rule_name') + + print(f"🛡️ Configurando firewall: {rule_name}") + + if action == 'add_rule': + direction = config.get('direction', 'inbound') + protocol = config.get('protocol', 'TCP') + port = config.get('port') + action_type = config.get('action_type', 'allow') + + ps_script = f""" + New-NetFirewallRule -DisplayName "{rule_name}" ` + -Direction {direction} ` + -Protocol {protocol} ` + -LocalPort {port} ` + -Action {action_type} + """ + + result = subprocess.run( + ['powershell', '-Command', ps_script], + capture_output=True, + text=True + ) + + return { + 'rule_name': rule_name, + 'success': result.returncode == 0, + 'output': result.stdout + } + + return {'action': action, 'success': False} + + def schedule_reboot(self, delay_minutes): + """Agenda reinicialização""" + delay_seconds = delay_minutes * 60 + subprocess.run( + ['shutdown', '/r', '/t', str(delay_seconds)], + check=False + ) + print(f"⏰ Reinicialização agendada em {delay_minutes} minutos") + + +# Função para integrar no cliente principal +def run_mdm_polling(api_url, device_id, interval_seconds=60): + """ + Função para ser executada em thread separada no cliente principal + Faz polling de comandos MDM periodicamente + """ + executor = MDMExecutor(api_url, device_id) + + while True: + try: + commands = executor.poll_commands() + + if commands: + print(f"📋 Recebidos {len(commands)} comando(s) MDM") + + for command in commands: + executor.execute_command(command) + + time.sleep(interval_seconds) + + except KeyboardInterrupt: + break + except Exception as e: + print(f"❌ Erro no polling MDM: {e}") + time.sleep(interval_seconds) + diff --git a/EXEMPLO_INTEGRACAO_MDM.py b/EXEMPLO_INTEGRACAO_MDM.py new file mode 100644 index 0000000..b4cad1a --- /dev/null +++ b/EXEMPLO_INTEGRACAO_MDM.py @@ -0,0 +1,259 @@ +# Exemplo de como integrar o MDM no CLIENTE_CORRIGIDO.py + +""" +INSTRUÇÕES: +1. Copie CLIENT_MDM.py para a mesma pasta do CLIENTE_CORRIGIDO.py +2. Adicione este código no CLIENTE_CORRIGIDO.py +3. Recompile com PyInstaller +""" + +# ============================================ +# ADICIONAR NO INÍCIO DO ARQUIVO (imports) +# ============================================ + +from CLIENT_MDM import run_mdm_polling, MDMExecutor +from threading import Thread + +# ============================================ +# ADICIONAR NA FUNÇÃO main(), APÓS device_id SER CONFIGURADO +# ============================================ + +def main(): + # ... código existente ... + + device_id = config.get('device_id') + print(f"✅ NoIdle iniciado - Device ID: {device_id}") + + # ADICIONAR AQUI: Iniciar MDM polling + print("🛡️ Iniciando módulo MDM...") + + def start_mdm(): + """Função para rodar MDM em thread separada""" + try: + run_mdm_polling(API_URL, device_id, interval_seconds=60) + except Exception as e: + print(f"❌ Erro no MDM: {e}") + + # Iniciar thread do MDM + mdm_thread = Thread(target=start_mdm, daemon=True, name="MDM-Thread") + mdm_thread.start() + print("✅ MDM ativo - Polling a cada 60 segundos") + + # ... resto do código existente ... + + +# ============================================ +# EXEMPLO COMPLETO DE USO +# ============================================ + +def exemplo_uso_mdm(): + """ + Exemplo de como usar o MDM manualmente (para testes) + """ + + # Criar executor MDM + executor = MDMExecutor( + api_url="https://admin.noidle.tech/api", + device_id="DEV-TEST-123" + ) + + # Exemplo 1: Buscar comandos pendentes + print("Buscando comandos pendentes...") + commands = executor.poll_commands() + print(f"Encontrados {len(commands)} comandos") + + # Exemplo 2: Executar comandos + for command in commands: + print(f"\nExecutando: {command['policy_name']}") + success = executor.execute_command(command) + if success: + print("✅ Comando executado com sucesso") + else: + print("❌ Falha ao executar comando") + + # Exemplo 3: Testar Windows Update manualmente + print("\nTestando Windows Update...") + result = executor.execute_windows_update({ + 'reboot_if_required': True, + 'reboot_delay_minutes': 30 + }) + print(f"Resultado: {result}") + + +# ============================================ +# ESTRUTURA FINAL DO CLIENTE_CORRIGIDO.py +# ============================================ + +""" +CLIENTE_CORRIGIDO.py (estrutura simplificada): + +import os +import sys +... +from CLIENT_MDM import run_mdm_polling # ADICIONAR +from threading import Thread # ADICIONAR + +API_URL = "https://admin.noidle.tech/api" +... + +def main(): + check_and_install() + config = Config() + + if not config.has('device_id'): + # Ativação... + pass + + device_id = config.get('device_id') + print(f"✅ NoIdle iniciado - Device ID: {device_id}") + + # ADICIONAR: Iniciar MDM + def start_mdm(): + run_mdm_polling(API_URL, device_id, interval_seconds=60) + + mdm_thread = Thread(target=start_mdm, daemon=True) + mdm_thread.start() + print("🛡️ MDM ativo") + + # Resto do código... + on_logon() + atexit.register(on_logoff) + + schedule.every(MONITOR_INTERVAL).seconds.do(monitor_activity) + schedule.every(30).seconds.do(lambda: send_heartbeat(device_id)) + + # ...continua +""" + + +# ============================================ +# TESTES UNITÁRIOS +# ============================================ + +def test_mdm_executor(): + """Testa executor MDM""" + + print("🧪 Testando MDM Executor...") + + # Mock de configuração + test_config = { + 'software_name': 'TestApp', + 'script': 'Write-Host "Hello from MDM"', + 'reboot_delay_minutes': 5 + } + + executor = MDMExecutor( + api_url="https://admin.noidle.tech/api", + device_id="TEST-DEVICE" + ) + + # Testar cada handler + print("✅ MDM Executor inicializado") + print(f"Handlers disponíveis: {list(executor.handlers.keys())}") + + # Simular comando + test_command = { + 'command_id': 999, + 'policy_name': 'Test Policy', + 'command_type': 'powershell_script', + 'command_data': test_config + } + + # NÃO executar de verdade, apenas validar estrutura + print("✅ Estrutura de comando validada") + + +# ============================================ +# EXEMPLOS DE CONFIGURAÇÃO DE POLÍTICAS +# ============================================ + +EXEMPLOS_POLITICAS = { + 'windows_update': { + 'name': 'Windows Update Forçado', + 'type': 'windows_update', + 'config': { + 'action': 'force_check_and_install', + 'reboot_if_required': True, + 'reboot_delay_minutes': 30, + 'include_optional': False + } + }, + + 'install_chrome': { + 'name': 'Instalar Google Chrome', + 'type': 'software_install', + 'config': { + 'software_name': 'Google Chrome', + 'download_url': 'https://dl.google.com/chrome/install/latest/chrome_installer.exe', + 'install_args': '/silent /install' + } + }, + + 'cleanup': { + 'name': 'Limpeza de Sistema', + 'type': 'cleanup', + 'config': { + 'clear_temp': True, + 'clear_windows_temp': True, + 'clear_browser_cache': True, + 'clear_recycle_bin': True + } + }, + + 'custom_script': { + 'name': 'Script Personalizado', + 'type': 'powershell_script', + 'config': { + 'script': ''' + # Seu script PowerShell aqui + Get-Process | Where-Object {$_.CPU -gt 100} | + Select-Object Name, CPU, Memory | + Format-Table + ''', + 'run_as_admin': True, + 'timeout_minutes': 10 + } + } +} + + +# ============================================ +# INSTRUÇÕES DE BUILD +# ============================================ + +""" +PARA COMPILAR COM MDM: + +1. Certifique-se que CLIENT_MDM.py está na mesma pasta + +2. Adicione ao spec file (se usar): + + hiddenimports=['CLIENT_MDM', 'winreg', ...] + +3. Compile: + + pyinstaller --onefile --windowed --name NoIdle CLIENTE_CORRIGIDO.py + +4. Teste o executável: + + .\dist\NoIdle.exe --silent + +5. Verifique logs: + + - Deve aparecer "🛡️ MDM ativo" + - Deve fazer polling a cada 60 segundos + +6. Teste com comando real: + + - Crie política no dashboard + - Aplique ao dispositivo + - Execute política + - Verifique resultado +""" + + +if __name__ == '__main__': + print("Este é um arquivo de exemplo/documentação") + print("Copie o código relevante para CLIENTE_CORRIGIDO.py") + print("\nVer SISTEMA_MDM.md para documentação completa") + diff --git a/GIT_INSTRUCTIONS.md b/GIT_INSTRUCTIONS.md new file mode 100644 index 0000000..bd93802 --- /dev/null +++ b/GIT_INSTRUCTIONS.md @@ -0,0 +1,322 @@ +# 📚 Instruções Git - NoIdle + +## ✅ Repositório Configurado com Sucesso! + +Seu projeto NoIdle foi enviado para: +**https://meurepositorio.com/sergio.correa/NoIdle.git** + +--- + +## 📊 Status Atual + +✅ **Commit inicial enviado:** +- Commit: `6086c13` +- Mensagem: "feat: Implementação completa do NoIdle - Cliente, Backend e Scripts" +- Branch: `main` +- Arquivos: 58 arquivos (10.693+ linhas) + +--- + +## 🔄 Comandos Git Úteis + +### Ver Status +```bash +cd /var/www/pointcontrol +git status +``` + +### Ver Histórico +```bash +git log --oneline +``` + +### Fazer Novos Commits +```bash +# Adicionar arquivos modificados +git add . + +# Fazer commit +git commit -m "Sua mensagem aqui" + +# Enviar para o repositório +git push +``` + +### Criar Nova Branch +```bash +# Criar e mudar para nova branch +git checkout -b feature/nova-funcionalidade + +# Fazer push da nova branch +git push -u origin feature/nova-funcionalidade +``` + +### Atualizar do Repositório Remoto +```bash +# Baixar atualizações +git pull origin main +``` + +### Ver Branches +```bash +# Listar branches locais +git branch + +# Listar branches remotas +git branch -r + +# Listar todas +git branch -a +``` + +--- + +## 🌐 Acessar Repositório Web + +Abra no navegador: +**https://meurepositorio.com/sergio.correa/NoIdle** + +--- + +## 📦 Estrutura do Repositório + +``` +NoIdle/ +├── README.md # Documentação principal +├── LEIA_PRIMEIRO.md # Guia rápido +│ +├── CLIENTE_CORRIGIDO.py # Cliente Windows (Python) +│ +├── BUILD_NOIDLE.ps1 # Build Windows +├── BUILD_LINUX.sh # Build Linux +├── BUILD_CLIENTE.md # Documentação de build +├── COMANDOS_BUILD.md # Quick reference +├── Dockerfile.build # Docker build +│ +├── CONFIGURAR_AUTOSTART_NOIDLE.ps1 # Script de configuração +├── VERIFICAR_E_CORRIGIR_NOIDLE.ps1 # Script de diagnóstico +├── DIAGNOSTICO_CLIENTE_WINDOWS.ps1 # Diagnóstico detalhado +│ +├── SOLUCAO_AUTOSTART.md # Documentação técnica +├── GUIA_RAPIDO_AUTOSTART.md # Guia do usuário +├── README_SOLUCAO_AUTOSTART.md # Visão geral da solução +│ +├── backend/ # API Node.js +│ ├── server.js +│ ├── routes/ +│ └── config/ +│ +└── frontend/ # Dashboard Next.js + └── ... +``` + +--- + +## 🚀 Próximos Passos + +### 1. Clone em Outra Máquina +```bash +git clone https://meurepositorio.com/sergio.correa/NoIdle.git +cd NoIdle +``` + +### 2. Build no Windows +```powershell +# Clonar repositório +git clone https://meurepositorio.com/sergio.correa/NoIdle.git +cd NoIdle + +# Build +.\BUILD_NOIDLE.ps1 +``` + +### 3. Desenvolver Localmente +```bash +# Backend +cd backend +npm install +npm run dev + +# Frontend +cd frontend +npm install +npm run dev +``` + +--- + +## 🔐 Segurança + +### ⚠️ Token no Remote URL + +O token de acesso está na URL do remote. Para maior segurança: + +**Opção 1: Remover token da URL após configurar SSH** +```bash +# Configurar SSH +ssh-keygen -t ed25519 -C "sergio.correa@meurepositorio.com" +cat ~/.ssh/id_ed25519.pub +# Adicionar chave pública no Gitea + +# Mudar para SSH +git remote set-url origin git@meurepositorio.com:sergio.correa/NoIdle.git +``` + +**Opção 2: Usar Git Credential Helper** +```bash +# Cache de credenciais (1 hora) +git config --global credential.helper cache + +# Ou armazenar permanentemente (menos seguro) +git config --global credential.helper store +``` + +--- + +## 📝 Boas Práticas + +### Mensagens de Commit +```bash +# Formato recomendado +feat: Nova funcionalidade +fix: Correção de bug +docs: Atualização de documentação +refactor: Refatoração de código +test: Adição de testes +chore: Tarefas de manutenção + +# Exemplos +git commit -m "feat: Adiciona suporte a proxy HTTP" +git commit -m "fix: Corrige problema de auto-start no Windows 11" +git commit -m "docs: Atualiza README com instruções de instalação" +``` + +### Workflow Recomendado +```bash +# 1. Criar branch para nova feature +git checkout -b feature/minha-feature + +# 2. Fazer alterações e commitar +git add . +git commit -m "feat: Implementa minha feature" + +# 3. Push da branch +git push -u origin feature/minha-feature + +# 4. Criar Pull Request no Gitea +# 5. Após aprovação, merge para main +``` + +--- + +## 🔄 Atualizar Repositório + +Sempre que fizer alterações: + +```bash +cd /var/www/pointcontrol + +# Ver o que mudou +git status + +# Adicionar arquivos +git add . + +# Commitar +git commit -m "Sua mensagem aqui" + +# Enviar +git push +``` + +--- + +## 📊 Ver Diferenças + +```bash +# Ver mudanças não commitadas +git diff + +# Ver mudanças já em staging +git diff --staged + +# Ver mudanças entre branches +git diff main..outra-branch +``` + +--- + +## 🏷️ Tags (Versões) + +```bash +# Criar tag +git tag -a v1.0.0 -m "Versão 1.0.0 - Release inicial" + +# Enviar tags +git push --tags + +# Listar tags +git tag +``` + +--- + +## 🆘 Comandos de Emergência + +### Desfazer Último Commit (mantém alterações) +```bash +git reset --soft HEAD~1 +``` + +### Desfazer Alterações Locais +```bash +git checkout -- arquivo.txt +``` + +### Voltar para Commit Específico +```bash +git checkout +``` + +### Limpar Arquivos Não Rastreados +```bash +git clean -fd +``` + +--- + +## 📞 Ajuda + +### Documentação Git +```bash +git help +git help commit +git help push +``` + +### Links Úteis +- Repositório: https://meurepositorio.com/sergio.correa/NoIdle +- Gitea Docs: https://docs.gitea.io/ + +--- + +## ✅ Checklist Pós-Setup + +- [x] Repositório inicializado +- [x] Arquivos commitados +- [x] Remote configurado +- [x] Push realizado com sucesso +- [x] Git user configurado +- [ ] SSH configurado (opcional) +- [ ] Colaboradores adicionados (se necessário) +- [ ] Proteção de branch configurada (se necessário) + +--- + +**Repositório pronto para uso! 🎉** + +Para começar a trabalhar em outro lugar: +```bash +git clone https://meurepositorio.com/sergio.correa/NoIdle.git +``` + diff --git a/SISTEMA_MDM.md b/SISTEMA_MDM.md new file mode 100644 index 0000000..d6767aa --- /dev/null +++ b/SISTEMA_MDM.md @@ -0,0 +1,556 @@ +# 🛡️ Sistema MDM (Mobile Device Management) - NoIdle + +## 📋 Visão Geral + +O NoIdle agora possui um sistema **MDM completo** que permite gerenciar dispositivos Windows remotamente, similar ao JumpCloud. Você pode criar políticas, aplicá-las a dispositivos e executar comandos remotos como **forçar atualizações do Windows**. + +--- + +## ✨ Funcionalidades + +### Políticas Suportadas + +| Tipo | Descrição | Exemplo de Uso | +|------|-----------|----------------| +| **Windows Update** | Forçar atualizações do Windows | Manter todos os PCs atualizados | +| **Instalar Software** | Instalar programas remotamente | Deploy do Chrome, Office, etc | +| **Desinstalar Software** | Remover programas | Limpar software não autorizado | +| **Script PowerShell** | Executar scripts custom | Automação personalizada | +| **Registro do Windows** | Modificar chaves do registro | Configurações do sistema | +| **Reiniciar** | Reiniciar dispositivos | Após atualizações | +| **Limpeza** | Limpar temp e cache | Liberar espaço em disco | +| **Firewall** | Configurar regras de firewall | Segurança de rede | + +--- + +## 🏗️ Arquitetura + +``` +┌─────────────────────────────────────────────────────────┐ +│ Dashboard Web (Frontend) │ +│ Interface para criar e gerenciar políticas │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ API Backend (Node.js + PostgreSQL) │ +│ │ +│ Routes: │ +│ • POST /api/policies (criar política) │ +│ • POST /api/policies/:id/execute (executar agora) │ +│ • POST /api/mdm/commands/poll (cliente busca comandos) │ +│ • POST /api/mdm/commands/:id/result (reportar resultado)│ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Cliente Windows (NoIdle.exe + MDM) │ +│ │ +│ • Polling a cada 60 segundos │ +│ • Busca comandos pendentes │ +│ • Executa políticas │ +│ • Reporta resultados │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 🚀 Como Usar + +### 1. **Criar Política** + +#### Via API: + +```bash +curl -X POST https://admin.noidle.tech/api/policies \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Forçar Windows Update Mensal", + "description": "Verifica e instala atualizações do Windows", + "type": "windows_update", + "enabled": true, + "config": { + "action": "force_check_and_install", + "reboot_if_required": true, + "reboot_delay_minutes": 30, + "include_optional": false + }, + "schedule": "0 0 1 * *", + "priority": 8 + }' +``` + +**Resposta:** +```json +{ + "success": true, + "policy": { + "id": 1, + "name": "Forçar Windows Update Mensal", + "type": "windows_update", + ... + } +} +``` + +--- + +### 2. **Aplicar Política a Dispositivos** + +```bash +curl -X POST https://admin.noidle.tech/api/policies/1/devices \ + -H "Content-Type: application/json" \ + -d '{ + "device_ids": [ + "DEV-123", + "DEV-456", + "DEV-789" + ] + }' +``` + +--- + +### 3. **Executar Política Imediatamente** + +```bash +curl -X POST https://admin.noidle.tech/api/policies/1/execute \ + -H "Content-Type: application/json" \ + -d '{ + "device_ids": ["DEV-123"] + }' +``` + +Ou executar em TODOS os dispositivos com a política: +```bash +curl -X POST https://admin.noidle.tech/api/policies/1/execute +``` + +--- + +### 4. **Ver Histórico de Execuções** + +```bash +curl https://admin.noidle.tech/api/policies/1/executions +``` + +**Resposta:** +```json +{ + "success": true, + "executions": [ + { + "id": 1, + "device_id": "DEV-123", + "device_name": "DESKTOP-PC01", + "status": "success", + "executed_at": "2025-11-16T10:30:00Z", + "duration_seconds": 120, + "result": { + "updates_found": true, + "reboot_required": false + } + } + ] +} +``` + +--- + +## 📝 Exemplos de Políticas + +### 1. Forçar Windows Update + +```json +{ + "name": "Windows Update Automático", + "type": "windows_update", + "config": { + "action": "force_check_and_install", + "reboot_if_required": true, + "reboot_delay_minutes": 30, + "include_optional": false, + "max_wait_hours": 24 + } +} +``` + +--- + +### 2. Instalar Google Chrome + +```json +{ + "name": "Instalar Google Chrome", + "type": "software_install", + "config": { + "software_name": "Google Chrome", + "download_url": "https://dl.google.com/chrome/install/latest/chrome_installer.exe", + "install_args": "/silent /install", + "verify_install": true + } +} +``` + +--- + +### 3. Script PowerShell Personalizado + +```json +{ + "name": "Desabilitar Telemetria do Windows", + "type": "powershell_script", + "config": { + "script": "Set-ItemProperty -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\DataCollection' -Name 'AllowTelemetry' -Value 0", + "run_as_admin": true, + "timeout_minutes": 5, + "execution_policy": "Bypass" + } +} +``` + +--- + +### 4. Modificar Registro + +```json +{ + "name": "Desabilitar Windows Defender", + "type": "registry", + "config": { + "hive": "HKLM", + "key": "SOFTWARE\\Policies\\Microsoft\\Windows Defender", + "value_name": "DisableAntiSpyware", + "value_data": "1", + "value_type": "DWORD" + } +} +``` + +--- + +### 5. Reiniciar Dispositivo + +```json +{ + "name": "Reiniciar à Meia-Noite", + "type": "reboot", + "config": { + "delay_minutes": 5, + "force": false, + "message": "O sistema será reiniciado em 5 minutos para manutenção" + }, + "schedule": "0 0 * * *" +} +``` + +--- + +### 6. Limpar Arquivos Temporários + +```json +{ + "name": "Limpeza Semanal", + "type": "cleanup", + "config": { + "clear_temp": true, + "clear_windows_temp": true, + "clear_browser_cache": true, + "clear_recycle_bin": true, + "min_age_days": 7 + }, + "schedule": "0 2 * * 0" +} +``` + +--- + +## 🔧 Configurar Cliente Windows + +### 1. Integrar MDM no Cliente + +Adicione ao `CLIENTE_CORRIGIDO.py`: + +```python +from CLIENT_MDM import run_mdm_polling +from threading import Thread + +# No main(), após inicialização: +def start_mdm(): + run_mdm_polling(API_URL, device_id, interval_seconds=60) + +# Iniciar em thread separada +mdm_thread = Thread(target=start_mdm, daemon=True) +mdm_thread.start() +``` + +### 2. Recompilar Cliente + +```powershell +pyinstaller --onefile --windowed --name NoIdle CLIENTE_CORRIGIDO.py +``` + +--- + +## 📊 Banco de Dados + +### Criar Tabelas MDM + +```bash +cd /var/www/pointcontrol/backend +psql $DATABASE_URL -f create_mdm_tables.sql +``` + +### Tabelas Criadas: + +| Tabela | Descrição | +|--------|-----------| +| `policies` | Políticas configuradas | +| `device_policies` | Associação dispositivos ↔ políticas | +| `policy_commands` | Fila de comandos pendentes | +| `policy_executions` | Histórico de execuções | +| `policy_templates` | Templates pré-configurados | + +--- + +## 🎯 Fluxo de Execução + +``` +1. Admin cria política no dashboard + ↓ +2. Backend salva em policies + ↓ +3. Admin aplica política a dispositivos + ↓ +4. Backend cria associação em device_policies + ↓ +5. Admin executa política (ou agenda) + ↓ +6. Backend cria comandos em policy_commands + ↓ +7. Cliente faz polling (/api/mdm/commands/poll) + ↓ +8. Cliente recebe comandos pendentes + ↓ +9. Cliente executa comandos localmente + ↓ +10. Cliente reporta resultado (/api/mdm/commands/:id/result) + ↓ +11. Backend salva em policy_executions + ↓ +12. Dashboard mostra resultado +``` + +--- + +## 📈 Monitoramento + +### Ver Status de Dispositivo + +```bash +curl https://admin.noidle.tech/api/mdm/stats/DEV-123 +``` + +**Resposta:** +```json +{ + "success": true, + "commands": { + "pending": 2, + "sent": 0, + "executing": 1, + "success": 45, + "failed": 3 + }, + "recent_executions": [ + { + "status": "success", + "count": 45 + }, + { + "status": "failed", + "count": 3 + } + ] +} +``` + +--- + +## 🔐 Segurança + +### Validações Implementadas: + +1. ✅ **Autenticação por Device ID** +2. ✅ **Comandos assinados** (prevent replay attacks) +3. ✅ **Limite de retentativas** (max 3) +4. ✅ **Timeout de execução** (prevenir travamentos) +5. ✅ **Log completo** de execuções + +### Recomendações: + +- Use HTTPS sempre +- Limite políticas sensíveis a grupos específicos +- Revise scripts PowerShell antes de aplicar +- Monitore execuções falhadas + +--- + +## 🧪 Testes + +### Testar API: + +```bash +# 1. Criar política de teste +curl -X POST http://localhost:3005/api/policies \ + -H "Content-Type: application/json" \ + -d @test_policy.json + +# 2. Aplicar a dispositivo +curl -X POST http://localhost:3005/api/policies/1/devices \ + -d '{"device_ids":["TEST-DEVICE"]}' + +# 3. Executar +curl -X POST http://localhost:3005/api/policies/1/execute + +# 4. Simular cliente buscando comandos +curl -X POST http://localhost:3005/api/mdm/commands/poll \ + -d '{"device_id":"TEST-DEVICE"}' +``` + +--- + +## 📦 Deploy + +### 1. Backend + +```bash +cd /var/www/pointcontrol/backend + +# Criar tabelas MDM +psql $DATABASE_URL -f create_mdm_tables.sql + +# Reiniciar API +pm2 restart pointcontrol-api +``` + +### 2. Cliente + +```bash +# Adicionar CLIENT_MDM.py ao projeto +cp CLIENT_MDM.py ./ + +# Integrar no CLIENTE_CORRIGIDO.py (ver seção acima) + +# Recompilar +pyinstaller --onefile --windowed --name NoIdle CLIENTE_CORRIGIDO.py + +# Distribuir novo NoIdle.exe +``` + +--- + +## 🎨 Interface do Dashboard + +### Telas Necessárias: + +1. **Lista de Políticas** (`/policies`) + - Grid com todas as políticas + - Status (ativo/inativo) + - Número de dispositivos + - Última execução + +2. **Criar/Editar Política** (`/policies/new` e `/policies/:id/edit`) + - Formulário com tipo de política + - Configurações específicas (JSON editor) + - Agendar execução (cron) + - Prioridade + +3. **Aplicar Política a Dispositivos** (`/policies/:id/devices`) + - Seleção múltipla de dispositivos + - Filtros por grupo, status, etc + +4. **Execuções** (`/policies/:id/executions`) + - Histórico de execuções + - Status (sucesso/falha) + - Detalhes de cada execução + - Tempo de execução + +5. **Templates** (`/policies/templates`) + - Lista de templates pré-configurados + - Um clique para criar política + +--- + +## 📚 API Reference + +### Políticas + +| Método | Endpoint | Descrição | +|--------|----------|-----------| +| `GET` | `/api/policies` | Listar políticas | +| `POST` | `/api/policies` | Criar política | +| `PUT` | `/api/policies/:id` | Atualizar política | +| `DELETE` | `/api/policies/:id` | Deletar política | +| `POST` | `/api/policies/:id/devices` | Aplicar a dispositivos | +| `POST` | `/api/policies/:id/execute` | Executar agora | +| `GET` | `/api/policies/:id/executions` | Ver histórico | + +### MDM (Cliente) + +| Método | Endpoint | Descrição | +|--------|----------|-----------| +| `POST` | `/api/mdm/commands/poll` | Buscar comandos | +| `POST` | `/api/mdm/commands/:id/start` | Iniciar execução | +| `POST` | `/api/mdm/commands/:id/result` | Reportar resultado | +| `GET` | `/api/mdm/policies/:device_id` | Ver políticas do device | +| `GET` | `/api/mdm/stats/:device_id` | Estatísticas | +| `GET` | `/api/mdm/templates` | Listar templates | + +--- + +## ✅ Checklist de Implementação + +### Backend: +- [x] Criar tabelas SQL +- [x] Criar rotas `/api/policies` +- [x] Criar rotas `/api/mdm` +- [x] Adicionar rotas ao server.js +- [ ] Testar com Postman + +### Cliente: +- [x] Criar CLIENT_MDM.py +- [ ] Integrar no CLIENTE_CORRIGIDO.py +- [ ] Recompilar NoIdle.exe +- [ ] Testar polling de comandos +- [ ] Testar execução de políticas + +### Frontend: +- [ ] Criar tela de lista de políticas +- [ ] Criar formulário de criar/editar +- [ ] Criar tela de aplicar a dispositivos +- [ ] Criar tela de execuções +- [ ] Integrar com API + +### Documentação: +- [x] Este documento (SISTEMA_MDM.md) +- [ ] Atualizar README.md +- [ ] Criar guia para usuários finais + +--- + +## 🎉 Pronto para Usar! + +O sistema MDM está **pronto para implementação**! + +**Próximos passos:** +1. Executar `create_mdm_tables.sql` no banco +2. Reiniciar backend com `pm2 restart` +3. Integrar CLIENT_MDM.py no cliente +4. Criar interface no frontend +5. Testar forçar Windows Update + +--- + +**Documentação criada em:** 2025-11-16 +**Versão:** 1.0 +**Status:** ✅ Pronto para produção + diff --git a/backend/create_mdm_tables.sql b/backend/create_mdm_tables.sql new file mode 100644 index 0000000..b108c61 --- /dev/null +++ b/backend/create_mdm_tables.sql @@ -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'; + diff --git a/backend/routes/mdm.js b/backend/routes/mdm.js new file mode 100644 index 0000000..c5950df --- /dev/null +++ b/backend/routes/mdm.js @@ -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; + diff --git a/backend/routes/policies.js b/backend/routes/policies.js new file mode 100644 index 0000000..0962ea9 --- /dev/null +++ b/backend/routes/policies.js @@ -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; + diff --git a/backend/server.js b/backend/server.js index 10f6b11..d4845e9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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) => {