#!/usr/bin/env python3
"""Monitor de seguranca - alertas Discord para qualquer ataque detectado."""
import subprocess, time, re, os, json
from datetime import datetime, timezone
from collections import defaultdict
try:
    import requests
except ImportError:
    import urllib.request
    class requests:
        @staticmethod
        def post(url, json=None, timeout=5):
            data = __import__('json').dumps(json).encode()
            req = __import__('urllib.request').request.Request(
                url, data=data, headers={'Content-Type': 'application/json'})
            __import__('urllib.request').request.urlopen(req, timeout=timeout)

WEBHOOK  = "https://discord.com/api/webhooks/1526113125022040084/0uw-g4-GVYE6pfHXQNLiBoDRbd0AekHzrZx30BknKe_FjRadiOMVFVdUElrIgWhyzp2h"
NGINX_LOG   = "/var/log/nginx/access.log"
STATE_FILE  = "/var/www/gangstar/apk_dropper/.monitor_state.json"

# Cooldowns (segundos) - evita spam no Discord
COOLDOWN_SCANNER   = 1800   # 30 min por IP scanner
COOLDOWN_SSH_FAIL  = 120    # 2 min por IP brute-force SSH
COOLDOWN_SSH_LOGIN = 3600   # 1h por IP desconhecido logado
COOLDOWN_BAN       = 60     # 1 min por IP banido
COOLDOWN_WEB_BRUTE = 600    # 10 min por IP brute-force web
COOLDOWN_500       = 300    # 5 min por IP com erro 500

_last_alert: dict = {}
ip_fail_count: dict = defaultdict(int)

def now_utc():
    return datetime.now(timezone.utc).isoformat()

def send(title, desc, color=0xef4444, fields=None):
    try:
        payload = {"embeds": [{"title": title, "description": desc, "color": color,
                   "timestamp": now_utc(), "fields": fields or []}]}
        requests.post(WEBHOOK, json=payload, timeout=5)
    except Exception as e:
        print(f"[Discord erro] {e}")

def can_alert(key, cooldown):
    now = time.time()
    if now - _last_alert.get(key, 0) < cooldown:
        return False
    _last_alert[key] = now
    return True


_banned_cache: set = set()

def ban_ip(ip, motivo):
    """Bane IP no UFW permanentemente (idempotente)."""
    if not ip or ip in _banned_cache:
        return
    # ignora IPs privados/locais
    if ip.startswith(('10.', '127.', '192.168.', '172.16.', '::1')):
        return
    _banned_cache.add(ip)
    try:
        subprocess.run(['ufw', 'deny', 'from', ip, 'to', 'any',
                        'comment', f'Auto-block {motivo}'],
                       capture_output=True, timeout=5)
        send("IP BLOQUEADO AUTOMATICAMENTE",
             f"**`{ip}`** banido no UFW ({motivo}). Ban permanente.",
             color=0x7c3aed,
             fields=[{"name": "Motivo", "value": motivo, "inline": True}])
    except Exception as e:
        print(f"[ban erro] {e}")

def load_state():
    try:
        with open(STATE_FILE) as f:
            return json.load(f)
    except:
        return {}

def save_state(state):
    try:
        with open(STATE_FILE, 'w') as f:
            json.dump(state, f)
    except:
        pass

def init_nginx_pos(state):
    """Na primeira execucao, comecar do fim do log (ignorar historico)."""
    if 'nginx_pos' not in state:
        try:
            state['nginx_pos'] = os.path.getsize(NGINX_LOG)
        except:
            state['nginx_pos'] = 0
    return state

# ── SSH / FAIL2BAN ──────────────────────────────────────────

def check_ssh_attacks(state):
    try:
        result = subprocess.run(
            ['journalctl', '-u', 'ssh', '--since', '1 minute ago', '--no-pager', '-o', 'short-iso'],
            capture_output=True, text=True, timeout=10
        ).stdout
    except:
        return

    fail_re = re.compile(r'Failed password for (?:invalid user )?(\S+) from (\S+) port')
    for m in fail_re.finditer(result):
        user, ip = m.groups()
        state.setdefault('ssh_fails', {})
        state['ssh_fails'][ip] = state['ssh_fails'].get(ip, 0) + 1
        count = state['ssh_fails'][ip]
        if count % 5 == 1 and can_alert(f'ssh_{ip}', COOLDOWN_SSH_FAIL):
            send(
                "ATAQUE SSH - Brute Force",
                f"IP **`{ip}`** atacando SSH por forca bruta.",
                color=0xef4444,
                fields=[
                    {"name": "Alvo", "value": user, "inline": True},
                    {"name": "Tentativas", "value": str(count), "inline": True},
                ]
            )

    known = set(state.get('known_ips', ['187.57.7.248', '177.62.106.234']))
    accept_re = re.compile(r'Accepted (?:password|publickey) for (\S+) from (\S+) port')
    for m in accept_re.finditer(result):
        user, ip = m.groups()
        if ip not in known and can_alert(f'ssh_ok_{ip}', COOLDOWN_SSH_LOGIN):
            send(
                "LOGIN SSH - IP DESCONHECIDO",
                f"Acesso SSH de IP nao reconhecido!",
                color=0xf97316,
                fields=[
                    {"name": "IP", "value": ip, "inline": True},
                    {"name": "Usuario", "value": user, "inline": True}
                ]
            )

def check_fail2ban(state):
    try:
        out = subprocess.run(
            ['fail2ban-client', 'status', 'sshd'],
            capture_output=True, text=True, timeout=10
        ).stdout
    except:
        return

    m = re.search(r'Banned IP list:\s+(.*)', out)
    if not m:
        return
    current = [ip.strip() for ip in m.group(1).split() if ip.strip()]
    prev = state.get('banned_ips', [])
    new_bans = [ip for ip in current if ip not in prev]
    state['banned_ips'] = current

    for ip in new_bans:
        if can_alert(f'ban_{ip}', COOLDOWN_BAN):
            send(
                "IP BANIDO AUTOMATICAMENTE",
                f"Fail2Ban bloqueou **`{ip}`** por brute force SSH.",
                color=0x7c3aed,
                fields=[
                    {"name": "IP", "value": ip, "inline": True},
                    {"name": "Total banidos", "value": str(len(current)), "inline": True},
                    {"name": "Duracao", "value": "24 horas", "inline": True}
                ]
            )

# ── NGINX WEB ATTACKS ────────────────────────────────────────

SCANNER_RE = [
    re.compile(r'(?i)(\.php$|\.asp|\.jsp|\.cgi)'),
    re.compile(r'(?i)(union.{1,20}select|drop.{1,10}table)'),
    re.compile(r'(?i)(\.\./|%2e%2e|%252e%252e)'),
    re.compile(r'(?i)(etc/passwd|etc/shadow|\.env|\.git/)'),
    re.compile(r'(?i)(eval\(|base64_decode|system\(|exec\()'),
    re.compile(r'(?i)(wp-admin|wp-login|phpmyadmin|adminer)'),
    re.compile(r'(?i)(phpunit|laravel|drupal|joomla)'),
    re.compile(r'(?i)(masscan|nikto|sqlmap|nmap|dirsearch|gobuster|nuclei|zgrab|libredtail|nmap-http)'),
]

LOG_RE = re.compile(
    r'(\S+) - - \[[^\]]+\] "(\S+) ([^"]+) HTTP[^"]*" (\d+) \d+ "[^"]*" "([^"]*)"'
)

def parse_nginx(state):
    if not os.path.exists(NGINX_LOG):
        return

    try:
        size = os.path.getsize(NGINX_LOG)
        pos  = state.get('nginx_pos', size)  # default: fim do arquivo
        if size < pos:
            pos = 0  # log rotacionou
        if size == pos:
            return
        with open(NGINX_LOG, 'r', errors='replace') as f:
            f.seek(pos)
            lines = f.readlines()
            state['nginx_pos'] = f.tell()
    except:
        return

    for line in lines:
        m = LOG_RE.match(line)
        if not m:
            continue
        ip, method, path, status, ua = m.groups()
        status = int(status)

        is_scan = any(r.search(line) for r in SCANNER_RE)

        # Path sensivel = recon (ban imediato, sem esperar 20 erros)
        _SENSITIVE = ('.env', '.git', 'etc/passwd', 'etc/shadow', 'wp-admin',
                      'wp-login', 'phpmyadmin', 'adminer', '.aws', 'config.php',
                      'config.json', 'backup', 'id_rsa', '.ssh')
        is_sensitive = any(s in path.lower() for s in _SENSITIVE)

        # BAN IMEDIATO: scanner detectado OU tentativa de path sensivel
        if is_scan or is_sensitive:
            ban_ip(ip, 'scanner' if is_scan else 'recon path sensivel')

        # Contar erros 4xx por IP
        if 400 <= status < 500:
            ip_fail_count[ip] += 1
        else:
            ip_fail_count[ip] = max(0, ip_fail_count[ip] - 1)

        # Scanner detectado
        if is_scan and can_alert(f'scan_{ip}', COOLDOWN_SCANNER):
            send(
                "SCANNER WEB DETECTADO",
                f"IP **`{ip}`** esta fazendo scan/reconhecimento.",
                color=0xf59e0b,
                fields=[
                    {"name": "IP",     "value": ip,           "inline": True},
                    {"name": "Status", "value": str(status),  "inline": True},
                    {"name": "Metodo", "value": method,       "inline": True},
                    {"name": "Path",   "value": path[:100],   "inline": False},
                    {"name": "UA",     "value": ua[:120],     "inline": False},
                ]
            )

        # Brute force web (muitos 4xx)
        if ip_fail_count[ip] >= 20 and can_alert(f'webbrute_{ip}', COOLDOWN_WEB_BRUTE):
            send(
                "BRUTE FORCE WEB",
                f"IP **`{ip}`** fez **{ip_fail_count[ip]}** erros seguidos no site.",
                color=0xef4444,
                fields=[
                    {"name": "IP",        "value": ip,                      "inline": True},
                    {"name": "Erros 4xx", "value": str(ip_fail_count[ip]),  "inline": True},
                    {"name": "Ultimo",    "value": path[:80],                "inline": False},
                ]
            )
            try:
                subprocess.run(['ufw', 'deny', 'from', ip, 'to', 'any',
                                'comment', 'Auto-block web brute'],
                               capture_output=True, timeout=5)
                send("IP BLOQUEADO AUTOMATICAMENTE",
                     f"**`{ip}`** banido no UFW por brute force web.",
                     color=0x7c3aed)
            except:
                pass

        # Erro 500
        if status == 500 and can_alert(f'500_{ip}', COOLDOWN_500):
            send(
                "ERRO 500 INTERNO",
                f"Erro no servidor — possivel tentativa de exploit.",
                color=0xb91c1c,
                fields=[
                    {"name": "IP",     "value": ip,          "inline": True},
                    {"name": "Path",   "value": path[:100],  "inline": True},
                    {"name": "Metodo", "value": method,      "inline": True},
                ]
            )

# ── MAIN ─────────────────────────────────────────────────────

def main():
    print("[Monitor] Iniciando...")
    state = load_state()
    state = init_nginx_pos(state)
    save_state(state)
    send(
        "MONITOR REINICIADO",
        "Sistema de alertas de seguranca ativo. Monitorando apenas eventos novos.",
        color=0x22c55e,
        fields=[{"name": "Host", "value": "jadbypass.my", "inline": True}]
    )
    tick = 0
    while True:
        try:
            check_ssh_attacks(state)
            parse_nginx(state)
            if tick % 2 == 0:  # a cada 60s
                check_fail2ban(state)
            save_state(state)
            tick += 1
        except Exception as e:
            print(f"[Erro] {e}")
        time.sleep(30)

if __name__ == '__main__':
    main()
