Compare commits
3 Commits
42a9ea5582
...
dba24f08bc
| Author | SHA1 | Date |
|---|---|---|
|
|
dba24f08bc | |
|
|
b0b9485b1a | |
|
|
7af7fa0ec7 |
|
|
@ -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.
|
||||||
46
README.md
46
README.md
|
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue