Compare commits

...

3 Commits

5 changed files with 349 additions and 74 deletions

36
GEMINI.md Normal file
View File

@ -0,0 +1,36 @@
# 🤖 Instruções para Agentes Gemini
Este arquivo contém diretrizes críticas para qualquer agente de IA que interaja com este repositório.
## 🚀 Workflow de Deploy (OBRIGATÓRIO)
**NUNCA faça commit direto na branch `producao` sem antes validar a configuração.**
O repositório conta com um script de automação híbrido (`producao/scripts/deploy_pathfinder.py`) que deve ser usado para **todo e qualquer deploy**.
### Passo a Passo para Agentes:
1. **Faça suas alterações** nos arquivos de configuração (`nginx/`).
2. **Valide e Deploye** rodando o script abaixo no terminal do Windows:
```powershell
python producao/scripts/deploy_pathfinder.py sync --all
```
3. **Verifique a Saída**:
- O script fará o upload, testará a configuração (`nginx -t`) no servidor e fará o reload.
- Se houver erro, **corrija antes de prosseguir**. O script fará rollback automático no servidor, mas seu código local estará "quebrado".
4. **Commit**: Somente após o sucesso do comando acima ("Deploy Remoto Concluído com Sucesso!"), faça o commit das alterações.
## 🛠️ Comandos Úteis
- **Sincronizar Tudo (Nginx + Fail2Ban + GeoIP)**:
`python producao/scripts/deploy_pathfinder.py sync --all`
- **Deploy de Novo Site**:
`python producao/scripts/deploy_pathfinder.py site --deploy dominio.com`
- **Atualizar GeoIP Manualmente**:
`python producao/scripts/deploy_pathfinder.py geoip --update`
## ⚠️ Pontos de Atenção
- **GeoIP**: O script baixa automaticamente os bancos GeoIP se faltarem. Não precisa baixar manualmente.
- **Paramiko**: O script usa `paramiko` para SSH. Se não estiver instalado, instale com `pip install paramiko`.
- **Credenciais**: As credenciais de acesso ao servidor estão embutidas no cabeçalho do script. Não as exponha em logs públicos.

View File

@ -71,14 +71,10 @@ O Pathfinder Proxy utiliza o **ModSecurity v3** compilado sob medida para o Ngin
- **Versão Nginx**: 1.29.5 Mainline (Oficial). - **Versão Nginx**: 1.29.5 Mainline (Oficial).
- **Versão ModSec**: 3.0.14. - **Versão ModSec**: 3.0.14.
- **Regras**: OWASP Core Rule Set (CRS) v4. - **Regras**: OWASP Core Rule Set (CRS) v4 (Instalação Minimalista).
- **Plugins**: Utiliza plugins oficiais do CRS para **Nextcloud** e **WordPress**, garantindo zero falsos positivos nessas plataformas. - **Anti-Brute Force**: Proteção integrada contra força bruta em páginas de login via ModSecurity Collections (Phase 1).
- **CVE Hardening**: Regras específicas para vulnerabilidades críticas de 2024-2025: - **API Support**: Métodos **PUT, PATCH e DELETE** liberados por padrão para suporte a sistemas modernos.
- **WordPress**: Auth Bypass (Really Simple Security) e Plugin Installs maliciosos. - **Tuning**: Arquivo `modsec/app_specific_modsec_tuning.conf` centraliza exceções granulares (Zabbix, Gitea, UniFi, Veeam).
- **React/Metro**: Proteção contra RCE (React2Shell/Metro4Shell).
- **Servidores**: Mitigação de exploits em Nginx (IngressNightmare), Apache (Source Disclosure) e IIS.
- **Infra**: Bloqueio de exfiltração em PostgreSQL e bypass em FortiWeb/ScreenConnect.
- **Tuning**: Arquivo `modsec/app_specific_modsec_tuning.conf` centraliza exceções para UniFi, vCenter, Exchange, Zabbix e Veeam.
--- ---
@ -117,13 +113,33 @@ Siga este procedimento para colocar um novo sistema no ar com segurança máxima
--- ---
## ⚡ Automação de Deploy (Safe-Rollback) ## 🛠️ Automação de Deploy (Pathfinder Automator V2 - Hybrid)
O Pathfinder inclui um script robusto para evitar downtime:
- `scripts/deploy_pathfinder.py`: O Pathfinder conta com o orquestrador `scripts/deploy_pathfinder.py`, que agora funciona em modo **Híbrido (Windows Client -> Linux Server)**. Você roda o script na sua máquina local e ele faz todo o trabalho sujo.
- Faz backup datado de `/etc/nginx` e `/etc/fail2ban`.
- Sincroniza os novos arquivos da pasta temporária. ### Pré-requisitos
- Valida com `nginx -t`. - Python 3 instalado no Windows.
- **Auto-Rollback**: Se houver erro (ex: módulo Brotli faltando), ele restaura os backups originais e reinicia os serviços em milissegundos. - Biblioteca Paramiko: `pip install paramiko`
### Comandos Principais
- **`python producao/scripts/deploy_pathfinder.py sync --all`**:
- Empacota suas configs locais.
- Conecta no servidor via SSH.
- Atualiza bancos GeoIP automaticamente.
- Sincroniza configurações e recarrega o Nginx.
- **Faz Rollback Automático** se o `nginx -t` falhar.
- **`python producao/scripts/deploy_pathfinder.py site --deploy <domínio>`**:
- Sobe um novo VHost + Certificado SSL + Teste de DNS.
- **`python producao/scripts/deploy_pathfinder.py geoip --update`**:
- Força a atualização dos bancos de dados GeoIP2 (Mirror GitHub).
### 🛡️ Segurança de Operação
- **Backup & Rollback Atômico**: Cada alteração gera um `.bak`. Se `nginx -t` falhar, o script desfaz a alteração imediatamente.
- **Auditoria Syslog**: Todas as ações são registradas no syslog do servidor.
- **Validação Local**: O script retorna `Exit Code 1` no Windows se falhar no Linux, ideal para CI/CD.
- **DNS Safeguard**: O deploy de SSL só ocorre se o DNS já estiver apontando para o IP do servidor, evitando bloqueios no Let's Encrypt.
--- ---

View File

@ -57,6 +57,11 @@ http {
open_file_cache_min_uses 2; open_file_cache_min_uses 2;
open_file_cache_errors on; open_file_cache_errors on;
# --- HTTP/2 Hardening (CVE-2025-8671: MadeYouReset Mitigation) ---
http2_max_concurrent_streams 64;
http2_idle_timeout 3m;
keepalive_requests 500;
# 2. Conexões & Timeouts # 2. Conexões & Timeouts
reset_timedout_connection on; reset_timedout_connection on;
client_body_timeout 12s; client_body_timeout 12s;

View File

@ -77,25 +77,23 @@ map $request_uri $is_suspicious_uri {
"~*/wp-content/uploads/.*\.php" 1; # Bloqueio de execução de PHP em uploads "~*/wp-content/uploads/.*\.php" 1; # Bloqueio de execução de PHP em uploads
"~*(/wp-includes/|/wp-content/plugins/.*\.txt|/wp-content/themes/.*\.txt)" 1; "~*(/wp-includes/|/wp-content/plugins/.*\.txt|/wp-content/themes/.*\.txt)" 1;
# CVE-Specific Exploits (2024-2025) # CVE-Specific Exploits (2024-2026)
"~*/reallysimplessl/v1/two_fa/skip_onboarding" 1; # CVE-2024-10924 (Auth Bypass) "~*/reallysimplessl/v1/two_fa/skip_onboarding" 1; # CVE-2024-10924 (Auth Bypass)
"~*(/gutenkit/v1/install-active-plugin|/cleantalk-antispam/v1/perform)" 1; # CVE-2024-9234 / CVE-2024-10781 "~*(/gutenkit/v1/install-active-plugin|/cleantalk-antispam/v1/perform)" 1; # CVE-2024-9234 / CVE-2024-10781
"~*(/open-url|/open-stack-frame)" 1; # CVE-2025-11953 (Metro4Shell) "~*(/open-url|/open-stack-frame)" 1; # CVE-2025-11953 (Metro4Shell)
"~*/api/fabric/device/status" 1; # CVE-2025-25257 (FortiWeb RCE) "~*/api/fabric/device/status" 1; # CVE-2025-25257 (FortiWeb RCE - Legacy)
"~*/api/v2\.0/cmdb/system/admin" 1; # CVE-2025-64446 (FortiWeb Traversal)
"~*\/ajax\/" 1; # CVE-2025-40551 (SolarWinds Evasion)
"~*/SetupWizard\.aspx" 1; # CVE-2024-1709 (ScreenConnect Bypass) "~*/SetupWizard\.aspx" 1; # CVE-2024-1709 (ScreenConnect Bypass)
"~*cgi-bin/fwbcgi" 1; # Fortinet CGI signature
"~*display=filestore.*&action=testconnection" 1; # CVE-2025-64328 (FreePBX)
# Server-Specific CVEs (Nginx/Apache/IIS) # Server-Specific CVEs (Nginx/Apache/IIS)
"~*/AdmissionReview" 1; # CVE-2025-1974 (Ingress-Nginx) "~*/AdmissionReview" 1; # CVE-2025-1974 (Ingress-Nginx)
"~*(/_vti_bin/|/MSOffice/|/WebDAV/)" 1; # IIS/WebDAV Probes "~*(/_vti_bin/|/MSOffice/|/WebDAV/)" 1; # IIS/WebDAV Probes
"~*/Cityworks/.*(Common|Config)/" 1; # CVE-2025-0994 (Cityworks on IIS) "~*/Cityworks/.*(Common|Config)/" 1; # CVE-2025-0994 (Cityworks on IIS)
"~*(\.php/.*AddType|RewriteRule.*\[E=)" 1; # CVE-2024-40725 (Apache Source Disclosure) "~*(\.php/.*AddType|RewriteRule.*\[E=)" 1; # CVE-2024-40725 (Apache Source Disclosure)
"~*\.php$" 1; # General PHP probing (e.g. CVE-2025-0108 PAN-OS)
# Framework Debugging & Admin Endpoints (Fast-Fail)
"~*(/_ignition/|/_profiler/|/_telescope/|/actuator/|/eureka/|/api-docs)" 1;
"~*(/phpmyadmin|/wp-admin/setup-config\.php|/rails/info/properties)" 1;
# Webshells e Exploracao Ativa Conhecida
"~*(/shell\.php|/cmd\.php|/eval-stdin\.php|/xmlrpc\.php|/setup\.php|/install\.php)" 1;
} }
# --- Pathfinder Deep Inspect Payload Map --- # --- Pathfinder Deep Inspect Payload Map ---
@ -127,8 +125,9 @@ map $args $is_malicious_payload {
# 5. Path Traversal & LFI # 5. Path Traversal & LFI
"~*(\.\./|\.\.\\|/etc/passwd|/etc/shadow|boot\.ini|/windows/win\.ini)" 1; "~*(\.\./|\.\.\\|/etc/passwd|/etc/shadow|boot\.ini|/windows/win\.ini)" 1;
# 6. PHP & Remote Execution / Binary Probes # 6. PHP & Remote Execution / Binary Probes / Command Injection (n8n/SolarWinds/FreePBX)
"~*(<\?php|base64_decode|system\(|shell_exec|proc_open)" 1; "~*(<\?php|base64_decode|system\(|shell_exec|proc_open|exec\()" 1;
"~*(child_process|spawn|eval\(|require\(|constructor|fs\.readFile|process\.env)" 1;
"~*(\\x00|\\x03|\\xE0|\\x83|\\xF8)" 1; # Binary probes / Buffer overflow patterns "~*(\\x00|\\x03|\\xE0|\\x83|\\xF8)" 1; # Binary probes / Buffer overflow patterns
} }
@ -157,10 +156,26 @@ map $http_user_agent $is_protocol_violation {
# 3. Geographic Risk (Requires GeoIP2 .mmdb files) # 3. Geographic Risk (Requires GeoIP2 .mmdb files)
map $geoip2_data_country_code $is_high_risk_country { map $geoip2_data_country_code $is_high_risk_country {
default 0; default 0;
"CN" 1; # China "CU" 1; # Cuba
"RU" 1; # Russia "SY" 1; # Syria
"KP" 1; # North Korea }
"IR" 1; # Iran
# --- NOVO: Detecção de Cabeçalhos Suspeitos (CVE-2025-55182 / React2Shell) ---
map $http_next_action $react_attack_1 {
default 0;
"~*(`|\$|\(|\)|<|>|\{|}|;|\|)" 1;
"~*(child_process|exec|spawn|eval|require)" 1;
}
map $http_rsc_action_id $react_attack_2 {
default 0;
"~*(`|\$|\(|\)|<|>|\{|}|;|\|)" 1;
"~*(child_process|exec|spawn|eval|require)" 1;
}
map $react_attack_1$react_attack_2 $is_suspicious_header {
"00" 0;
default 1;
} }
# --- Pathfinder Security Decision Engine (PSDE) --- # --- Pathfinder Security Decision Engine (PSDE) ---
@ -171,15 +186,16 @@ map $request_method $is_suspicious_method {
~*(TRACE|TRACK|CONNECT|DEBUG) 1; ~*(TRACE|TRACK|CONNECT|DEBUG) 1;
} }
# 2. Security Scoring System (7-Vector Combinatorial Matrix) # 2. Security Scoring System (8-Vector Combinatorial Matrix)
# Ordem: [Bot][URI][Method][Payload][Geo][Protocol][Referer] # Ordem: [Bot][URI][Method][Payload][Geo][Protocol][Referer][Header]
map $is_bad_bot$is_suspicious_uri$is_suspicious_method$is_malicious_payload$is_high_risk_country$is_protocol_violation$is_spam_referer $security_score { map $is_bad_bot$is_suspicious_uri$is_suspicious_method$is_malicious_payload$is_high_risk_country$is_protocol_violation$is_spam_referer$is_suspicious_header $security_score {
"0000000" 0; # Saudável "00000000" 0; # Saudável
# --- BLOQUEIO CRÍTICO (Score 3) --- # --- BLOQUEIO CRÍTICO (Score 3) ---
"~*...1..." 3; # Qualquer Payload "~*...1...." 3; # Qualquer Payload
"~*......1" 3; # Qualquer Referer Spam "~*......1." 3; # Qualquer Referer Spam
"~*[1-9]{3,}" 3; # Qualquer 3 ou mais vetores em simultâneo (Regex para detectar 3 ou mais '1's) "~*.......1" 3; # Qualquer Cabeçalho Malicioso (React2Shell/etc)
"~*[1-9]{3,}" 3; # Qualquer 3 ou mais vetores em simultâneo
"~*11[1-9]...." 3; # Bot + URI + Método "~*11[1-9]...." 3; # Bot + URI + Método
"~*11...[1-9]." 3; # Bot + URI + Protocolo "~*11...[1-9]." 3; # Bot + URI + Protocolo
"~*1.1.1.." 3; # Bot + Método + Geo "~*1.1.1.." 3; # Bot + Método + Geo

View File

@ -4,29 +4,53 @@ import sys
import argparse import argparse
import subprocess import subprocess
import socket import socket
import syslog
import shutil import shutil
import platform
import tarfile
import time
from datetime import datetime from datetime import datetime
# Tenta importar syslog (Apenas Linux)
try:
import syslog
SYSLOG_AVAILABLE = True
except ImportError:
SYSLOG_AVAILABLE = False
# Tenta importar paramiko (Apenas para Cliente Windows)
try:
import paramiko
PARAMIKO_AVAILABLE = True
except ImportError:
PARAMIKO_AVAILABLE = False
# ============================================================================== # ==============================================================================
# CONFIGURAÇÕES TÉCNICAS # CONFIGURAÇÕES TÉCNICAS
# ============================================================================== # ==============================================================================
# Credenciais
SERVER_HOST = "172.17.0.253"
SERVER_USER = "itguys"
PASSWORD = "vR7Ag$Pk" PASSWORD = "vR7Ag$Pk"
# Caminhos (Linux Server)
NGINX_CONF_DIR = "/etc/nginx" NGINX_CONF_DIR = "/etc/nginx"
NGINX_CONF_BACKUP = "/etc/nginx.bak"
FAIL2BAN_CONF_DIR = "/etc/fail2ban" FAIL2BAN_CONF_DIR = "/etc/fail2ban"
TMP_SYNC_BASE = "/tmp/pathfinder_sync" TMP_SYNC_BASE = "/tmp/pathfinder_sync"
LOG_DIR = "/var/log/nginx" LOG_DIR = "/var/log/nginx"
# Endereço IP Público do Host de Produção (para validação DNS) # Endereço IP Público do Host de Produção (para validação DNS)
HOST_PUBLIC_IP = "" HOST_PUBLIC_IP = "177.104.182.28"
# ============================================================================== # ==============================================================================
# UTILITÁRIOS DE SISTEMA E AUDITORIA # UTILITÁRIOS DE SISTEMA E AUDITORIA (SERVER-SIDE)
# ============================================================================== # ==============================================================================
def log_syslog(task, function, details=""): def log_syslog(task, function, details=""):
"""Registra a ação no Syslog para auditoria.""" """Registra a ação no Syslog para auditoria (Apenas Linux)."""
if not SYSLOG_AVAILABLE:
print(f"[*] [SYSLOG_MOCK] {task} | {function} | {details}")
return
try: try:
hostname = socket.gethostname() hostname = socket.gethostname()
remote_ip = os.environ.get('SSH_CLIENT', 'localhost').split()[0] remote_ip = os.environ.get('SSH_CLIENT', 'localhost').split()[0]
@ -59,7 +83,7 @@ def check_nginx():
return rc == 0, err return rc == 0, err
# ============================================================================== # ==============================================================================
# LÓGICA DE BACKUP E ROLLBACK ATÔMICO # LÓGICA DE BACKUP E ROLLBACK ATÔMICO (SERVER-SIDE)
# ============================================================================== # ==============================================================================
BACKUP_MAP = {} # Rastreia arquivos alterados para rollback BACKUP_MAP = {} # Rastreia arquivos alterados para rollback
@ -88,7 +112,7 @@ def rollback_all():
check_nginx() check_nginx()
# ============================================================================== # ==============================================================================
# AUXILIARES DE REDE (DNS/IP/SSL) # AUXILIARES DE REDE (DNS/IP/SSL) (SERVER-SIDE)
# ============================================================================== # ==============================================================================
def get_public_ip(): def get_public_ip():
@ -103,14 +127,7 @@ def get_public_ip():
HOST_PUBLIC_IP = response.read().decode('utf-8') HOST_PUBLIC_IP = response.read().decode('utf-8')
return HOST_PUBLIC_IP return HOST_PUBLIC_IP
except: except:
try: return "127.0.0.1"
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
HOST_PUBLIC_IP = s.getsockname()[0]
s.close()
return HOST_PUBLIC_IP
except:
return "127.0.0.1"
def validate_dns(domain): def validate_dns(domain):
"""Verifica se o domínio aponta para este host.""" """Verifica se o domínio aponta para este host."""
@ -145,21 +162,147 @@ def setup_ssl(domain):
return False return False
# ============================================================================== # ==============================================================================
# FUNCIONALIDADES DO SCRIPT # CLIENTE WINDOWS (REMOTE DEPLOY)
# ==============================================================================
def create_tarball(source_dirs, output_filename="deploy_package.tar.gz"):
"""Cria um tar.gz dos diretórios selecionados."""
print(f"[*] Criando pacote de deploy: {output_filename}")
with tarfile.open(output_filename, "w:gz") as tar:
for folder in source_dirs:
if os.path.exists(folder):
print(f" - Adicionando: {folder}")
tar.add(folder, arcname=os.path.basename(folder))
else:
print(f"[!] AVISO: Pasta não encontrada: {folder}")
def windows_deploy(args):
"""Orquestra o deploy remoto a partir do Windows."""
if not PARAMIKO_AVAILABLE:
print("[!] Erro: Biblioteca 'paramiko' não instalada.")
print(" Execute: pip install paramiko")
sys.exit(1)
print("="*60)
print(f"🚀 PATHFINDER REMOTE DEPLOYER - Target: {SERVER_HOST}")
print("="*60)
# 1. Definir caminhos locais relativos à raiz do projeto
# Assume que o script está sendo rodado da raiz ou de producao/scripts
base_dir = os.getcwd()
if os.path.basename(base_dir) == "scripts":
base_dir = os.path.dirname(os.path.dirname(base_dir)) # Sobe para raiz
elif os.path.basename(base_dir) == "producao":
base_dir = os.path.dirname(base_dir) # Sobe para raiz
prod_dir = os.path.join(base_dir, "producao")
nginx_dir = os.path.join(prod_dir, "nginx")
scripts_dir = os.path.join(prod_dir, "scripts")
if not os.path.exists(nginx_dir):
print(f"[!] Erro: Não encontrei a pasta 'nginx' em {prod_dir}")
sys.exit(1)
# 2. Empacotar arquivos
tar_name = "pathfinder_deploy.tar.gz"
create_tarball([nginx_dir, scripts_dir], tar_name)
# 3. Conexão SSH
print(f"[*] Conectando em {SERVER_HOST} como {SERVER_USER}...")
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(SERVER_HOST, username=SERVER_USER, password=PASSWORD)
# 4. Upload SFTP
print(f"[*] Uploading {tar_name} para /tmp/...")
sftp = ssh.open_sftp()
sftp.put(tar_name, f"/tmp/{tar_name}")
sftp.close()
# 5. Execução Remota
# Reconstrói os argumentos passados para o script local
remote_args = " ".join(sys.argv[1:])
if not remote_args:
remote_args = "--help"
print(f"[*] Executando remotamente: deploy_pathfinder.py {remote_args}")
print("-" * 60)
# Sequência de comandos remotos (Redirecionando erro para arquivo)
remote_cmd = (
f"bash -c 'cd /tmp && "
f"tar -xzf {tar_name} && "
f"sudo -S -p \"\" python3 scripts/deploy_pathfinder.py {remote_args} > /tmp/deployment_error.log 2>&1'"
)
stdin, stdout, stderr = ssh.exec_command(remote_cmd, get_pty=False)
# Envia senha para o sudo se solicitado
stdin.write(PASSWORD + "\n")
stdin.flush()
# Stream de saída (stdout)
while True:
if stdout.channel.recv_ready():
output = stdout.channel.recv(1024).decode('utf-8', errors='replace')
print(output, end="")
if stdout.channel.exit_status_ready() and not stdout.channel.recv_ready():
break
time.sleep(0.1)
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0:
print(f"\n❌ Falha no Deploy Remoto (Exit Code: {exit_status})")
print("--- ERRO DETALHADO DO SERVIDOR ---")
# Ler arquivo de log remoto
_, err_stdout, _ = ssh.exec_command("cat /tmp/deployment_error.log")
print(err_stdout.read().decode('utf-8'))
print("----------------------------------")
else:
print(f"\n✅ Deploy Remoto Concluído com Sucesso!")
ssh.close()
# Limpeza local
if os.path.exists(tar_name):
os.remove(tar_name)
except Exception as e:
print(f"\n[!] Erro fatal na conexão SSH: {e}")
sys.exit(1)
# ==============================================================================
# FUNCIONALIDADES DO SCRIPT (SERVER-SIDE LÓGICA)
# ============================================================================== # ==============================================================================
def sync_all(): def sync_all():
"""Sincronização completa (legado).""" """Sincronização completa (legado)."""
log_syslog("SYNC", "sync_all", "Sincronização total de Nginx e Fail2Ban") log_syslog("SYNC", "sync_all", "Sincronização total de Nginx e Fail2Ban")
backup_file(NGINX_CONF_DIR) backup_file(NGINX_CONF_DIR)
backup_file(FAIL2BAN_CONF_DIR)
src_nginx = os.path.join(TMP_SYNC_BASE, "nginx", ".") # Ajuste: No modo remoto, os arquivos são descompactados em /tmp/nginx diretamente
run_sudo(['cp', '-rf', src_nginx, NGINX_CONF_DIR]) # Se rodar manual, assume TMP_SYNC_BASE antigo
# Verifica onde estão os arquivos fontes (Prioridade para o pacote descompactado em /tmp)
src_nginx = "/tmp/nginx"
if not os.path.exists(src_nginx):
src_nginx = os.path.join(TMP_SYNC_BASE, "nginx")
if not os.path.exists(src_nginx):
print(f"[!] Erro: Diretório fonte não encontrado em /tmp/nginx ou {TMP_SYNC_BASE}")
return False
print(f"[*] Sincronizando de: {src_nginx}")
run_sudo(['cp', '-rf', os.path.join(src_nginx, "."), NGINX_CONF_DIR])
# Atualiza GeoIP se solicitado (ou sempre no sync --all)
update_geoip_db()
ok, err = check_nginx() ok, err = check_nginx()
if not ok: if not ok:
print(f"[!] Erro na configuração: {err}") print(f"[!] Erro na configuração (Nginx -t falhou): {err}")
rollback_all() rollback_all()
return False return False
@ -167,9 +310,56 @@ def sync_all():
print("[+] Sincronização total concluída com sucesso.") print("[+] Sincronização total concluída com sucesso.")
return True return True
def update_geoip_db():
"""Baixa os bancos de dados GeoIP mais recentes (Mirror GitHub)."""
# URLs de Mirror Confiáveis (P3TERX ou similar)
GEOIP_URLS = {
"GeoLite2-Country.mmdb": "https://git.io/GeoLite2-Country.mmdb",
"GeoLite2-City.mmdb": "https://git.io/GeoLite2-City.mmdb"
}
GEOIP_DIR = "/usr/share/GeoIP"
print("[*] Verificando bancos de dados GeoIP2...")
if not os.path.exists(GEOIP_DIR):
run_sudo(['mkdir', '-p', GEOIP_DIR])
import urllib.request
for db_name, url in GEOIP_URLS.items():
target = os.path.join(GEOIP_DIR, db_name)
# Lógica simplificada: Baixar sempre no sync --all para garantir atualização
# Para produção real, poderia checar headers ETag/Last-Modified, mas o mirror redireciona.
print(f" - Baixando {db_name}...")
try:
# Baixa para /tmp primeiro
tmp_target = os.path.join("/tmp", db_name)
# Usar curl é mais robusto em ambientes restritos que o urllib do python
# Segue redirecionamentos (-L) e falha em erro (-f)
rc, out, err = run_sudo(['curl', '-L', '-f', '-o', tmp_target, url])
if rc == 0:
run_sudo(['mv', '-f', tmp_target, target])
run_sudo(['chmod', '644', target])
print(f" [OK] Atualizado: {target}")
else:
print(f" [!] Falha no download de {db_name}: {err}")
except Exception as e:
print(f" [!] Erro ao atualizar GeoIP: {e}")
def sync_item(relative_path): def sync_item(relative_path):
"""Sincroniza um arquivo ou diretório (ex: snippets/ ou modsec/).""" """Sincroniza um arquivo ou diretório."""
src = os.path.join(TMP_SYNC_BASE, "nginx", relative_path) # Definição do source (Dual mode: /tmp/nginx direto ou TMP_SYNC_BASE)
src_base_1 = "/tmp/nginx"
src_base_2 = os.path.join(TMP_SYNC_BASE, "nginx")
src = os.path.join(src_base_1, relative_path)
if not os.path.exists(src):
src = os.path.join(src_base_2, relative_path)
dst = os.path.join(NGINX_CONF_DIR, relative_path) dst = os.path.join(NGINX_CONF_DIR, relative_path)
if not os.path.exists(src): if not os.path.exists(src):
@ -177,11 +367,8 @@ def sync_item(relative_path):
return False return False
log_syslog("SYNC_ITEM", "sync_item", f"Sincronizando {relative_path}") log_syslog("SYNC_ITEM", "sync_item", f"Sincronizando {relative_path}")
# Backup recursivo se for diretório ou arquivo
backup_file(dst) backup_file(dst)
# Usa -rf para suportar diretórios (como modsec/)
if os.path.isdir(src): if os.path.isdir(src):
run_sudo(['cp', '-rf', os.path.join(src, '.'), dst]) run_sudo(['cp', '-rf', os.path.join(src, '.'), dst])
else: else:
@ -189,7 +376,7 @@ def sync_item(relative_path):
ok, err = check_nginx() ok, err = check_nginx()
if not ok: if not ok:
print(f"[!] Falha na validação após sincronizar {relative_path}. Revertendo...") print(f"[!] Falha na validação. Revertendo...")
rollback_all() rollback_all()
return False return False
@ -199,11 +386,15 @@ def sync_item(relative_path):
def site_deploy(domain): def site_deploy(domain):
"""Deploy completo de um novo site.""" """Deploy completo de um novo site."""
src_vhost = os.path.join(TMP_SYNC_BASE, "nginx", "conf.d", f"{domain}.conf") # Busca fonte
src_vhost = os.path.join("/tmp/nginx/conf.d", f"{domain}.conf")
if not os.path.exists(src_vhost):
src_vhost = os.path.join(TMP_SYNC_BASE, "nginx", "conf.d", f"{domain}.conf")
dst_vhost = os.path.join(NGINX_CONF_DIR, "conf.d", f"{domain}.conf") dst_vhost = os.path.join(NGINX_CONF_DIR, "conf.d", f"{domain}.conf")
if not os.path.exists(src_vhost): if not os.path.exists(src_vhost):
print(f"[!] Arquivo de VHost não encontrado em: {src_vhost}") print(f"[!] Arquivo de VHost não encontrado: {src_vhost}")
return False return False
log_syslog("SITE_DEPLOY", "site_deploy", f"Iniciando deploy de {domain}") log_syslog("SITE_DEPLOY", "site_deploy", f"Iniciando deploy de {domain}")
@ -220,8 +411,8 @@ def site_deploy(domain):
dns_ok, domain_ip = validate_dns(domain) dns_ok, domain_ip = validate_dns(domain)
if not dns_ok: if not dns_ok:
print(f"[!] AVISO: DNS de {domain} ({domain_ip}) não aponta para este host ({get_public_ip()}).") print(f"[!] AVISO: DNS de {domain} ({domain_ip}) incorreto.")
print("[!] SSL Certbot será pulado. Rode 'site --update' após corrigir o DNS.") print("[!] SSL Certbot será pulado.")
return True return True
setup_ssl(domain) setup_ssl(domain)
@ -240,18 +431,14 @@ def site_update(domain):
def site_remove(domain): def site_remove(domain):
"""Remove site, SSL e Logs.""" """Remove site, SSL e Logs."""
log_syslog("SITE_REMOVE", "site_remove", f"Removendo site {domain}") log_syslog("SITE_REMOVE", "site_remove", f"Removendo site {domain}")
# 1. Nginx Config
vhost = os.path.join(NGINX_CONF_DIR, "conf.d", f"{domain}.conf") vhost = os.path.join(NGINX_CONF_DIR, "conf.d", f"{domain}.conf")
if os.path.exists(vhost): if os.path.exists(vhost):
backup_file(vhost) backup_file(vhost)
run_sudo(['rm', '-f', vhost]) run_sudo(['rm', '-f', vhost])
# 2. SSL Certbot
print(f"[*] Removendo certificados para {domain}...") print(f"[*] Removendo certificados para {domain}...")
run_sudo(['certbot', 'delete', '--cert-name', domain]) run_sudo(['certbot', 'delete', '--cert-name', domain])
# 3. Logs (Atuais e GZ)
print(f"[*] Limpando logs de {domain}...") print(f"[*] Limpando logs de {domain}...")
run_sudo(['bash', '-c', f"rm -f {LOG_DIR}/{domain}*"]) run_sudo(['bash', '-c', f"rm -f {LOG_DIR}/{domain}*"])
@ -267,6 +454,13 @@ def site_remove(domain):
# ============================================================================== # ==============================================================================
def main(): def main():
# Detecta SO para decidir modo de operação
if platform.system() == "Windows":
# Modo Cliente (Windows)
windows_deploy(sys.argv)
return
# Modo Servidor (Linux)
parser = argparse.ArgumentParser(description="Pathfinder Automator V2 - Nginx/SSL Orchestration") parser = argparse.ArgumentParser(description="Pathfinder Automator V2 - Nginx/SSL Orchestration")
subparsers = parser.add_subparsers(dest="command", help="Comando a executar") subparsers = parser.add_subparsers(dest="command", help="Comando a executar")
@ -279,6 +473,9 @@ def main():
site_parser.add_argument("--update", type=str, help="Atualizar site existente (Domínio)") site_parser.add_argument("--update", type=str, help="Atualizar site existente (Domínio)")
site_parser.add_argument("--remove", type=str, help="Remover site completamente (Domínio)") site_parser.add_argument("--remove", type=str, help="Remover site completamente (Domínio)")
geoip_parser = subparsers.add_parser("geoip", help="Gerenciamento de GeoIP")
geoip_parser.add_argument("--update", action="store_true", help="Baixar/Atualizar bancos GeoIP")
args = parser.parse_args() args = parser.parse_args()
if args.command == "sync": if args.command == "sync":
@ -290,9 +487,14 @@ def main():
if args.deploy: site_deploy(args.deploy) if args.deploy: site_deploy(args.deploy)
elif args.update: site_update(args.update) elif args.update: site_update(args.update)
elif args.remove: site_remove(args.remove) elif args.remove: site_remove(args.remove)
elif args.command == "geoip":
if args.update:
update_geoip_db()
if __name__ == "__main__": if __name__ == "__main__":
if os.getuid() == 0: try:
print("[!] Não execute diretamente como root. Use um usuário com sudo.") main()
except KeyboardInterrupt:
print("\n[!] Operação cancelada pelo usuário.")
sys.exit(1) sys.exit(1)
main()