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:
472
CLIENT_MDM.py
Normal file
472
CLIENT_MDM.py
Normal file
@@ -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)
|
||||
|
||||
259
EXEMPLO_INTEGRACAO_MDM.py
Normal file
259
EXEMPLO_INTEGRACAO_MDM.py
Normal file
@@ -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")
|
||||
|
||||
322
GIT_INSTRUCTIONS.md
Normal file
322
GIT_INSTRUCTIONS.md
Normal file
@@ -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 <hash-do-commit>
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
556
SISTEMA_MDM.md
Normal file
556
SISTEMA_MDM.md
Normal file
@@ -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
|
||||
|
||||
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