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 keysRoutes = require('./routes/keys');
|
||||||
const usersRoutes = require('./routes/users');
|
const usersRoutes = require('./routes/users');
|
||||||
const teamsRoutes = require("./routes/teams");
|
const teamsRoutes = require("./routes/teams");
|
||||||
|
const policiesRoutes = require('./routes/policies');
|
||||||
|
const mdmRoutes = require('./routes/mdm');
|
||||||
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/dashboard', dashboardRoutes);
|
app.use('/api/dashboard', dashboardRoutes);
|
||||||
@@ -57,6 +59,8 @@ app.use("/api/activity", activityRoutes);
|
|||||||
app.use('/api/keys', keysRoutes);
|
app.use('/api/keys', keysRoutes);
|
||||||
app.use('/api/users', usersRoutes);
|
app.use('/api/users', usersRoutes);
|
||||||
app.use("/api/teams", teamsRoutes);
|
app.use("/api/teams", teamsRoutes);
|
||||||
|
app.use('/api/policies', policiesRoutes);
|
||||||
|
app.use('/api/mdm', mdmRoutes);
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user