#!/usr/bin/env python3
"""Monitor de seguranca - sem alertas de brute force SSH/web (so voce loga)."""
import subprocess, time, re, os, json
from datetime import datetime, timezone, timedelta
from collections import defaultdict

try:
    import requests as _req
    def _post(url, payload):
        _req.post(url, json=payload, timeout=5)
    def _get_json(url):
        r = _req.get(url, timeout=4)
        return r.json() if r else None
except ImportError:
    import urllib.request, json as _json2
    def _post(url, payload):
        data = _json2.dumps(payload).encode()
        req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
        urllib.request.urlopen(req, timeout=5)
    def _get_json(url):
        try:
            resp = urllib.request.urlopen(url, timeout=4)
            return _json2.loads(resp.read().decode())
        except Exception:
            return None

WEBHOOK    = ""  # Discord webhook removido por completo do projeto
NGINX_LOG  = "/var/log/nginx/access.log"
AUTH_LOG   = "/var/log/auth.log"
ATTACK_LOG = "/var/log/gangstar_attacks.log"
STATE_FILE = "/var/www/gangstar/apk_dropper/.monitor_state.json"
HOSTNAME   = "jadbypass.my"

_last_alert: dict = {}
_banned_cache: set = set()
_geo_cache: dict = {}

# ── GEO ──────────────────────────────────────────────────────

def geo_ip(ip):
    if ip in _geo_cache:
        return _geo_cache[ip]
    if ip.startswith(('10.', '127.', '192.168.', '172.16.', '::1')):
        return {}
    try:
        d = _get_json("http://ip-api.com/json/" + ip + "?fields=status,country,countryCode,city,isp,as")
        if d and d.get('status') == 'success':
            r = {'country': d.get('country','?'), 'cc': d.get('countryCode','??'),
                 'city': d.get('city','?'), 'isp': d.get('isp','?'), 'as': d.get('as','?')}
            _geo_cache[ip] = r
            return r
    except Exception:
        pass
    return {}

def _flag(cc):
    try:
        if len(cc) == 2:
            return chr(0x1F1E6+ord(cc[0])-ord('A')) + chr(0x1F1E6+ord(cc[1])-ord('A'))
    except Exception:
        pass
    return '\U0001f310'

def geo_fields(ip):
    g = geo_ip(ip)
    if not g:
        return []
    return [
        {"name": "Pais",    "value": _flag(g['cc']) + " " + g['country'] + " (" + g['cc'] + ")", "inline": True},
        {"name": "Cidade",  "value": g['city'],                                                   "inline": True},
        {"name": "ISP/ASN", "value": (g['isp'] + " | " + g['as'])[:80],                          "inline": False},
    ]

# ── UTILS ─────────────────────────────────────────────────────

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

def now_br():
    return (datetime.now(timezone.utc) - timedelta(hours=3)).strftime('%d/%m/%Y %H:%M:%S')

def send(title, desc, color=0xef4444, fields=None):
    # Discord webhook desativado — nada é enviado para fora do servidor.
    return

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

def ban_ip(ip, motivo):
    if not ip or ip in _banned_cache:
        return
    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', 'Auto-block ' + motivo],
                       capture_output=True, timeout=5)
        send("\U0001f528 IP BANIDO AUTOMATICAMENTE",
             "**`" + ip + "`** bloqueado permanentemente no UFW.",
             color=0x7c3aed,
             fields=[{"name": "Motivo", "value": motivo, "inline": True},
                     {"name": "Quando",  "value": now_br(), "inline": True}] + geo_fields(ip))
    except Exception as e:
        print("[ban] " + str(e))

# ── STATE ─────────────────────────────────────────────────────

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

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

def _init_pos(state, key, path):
    if key not in state:
        try:
            state[key] = os.path.getsize(path)
        except Exception:
            state[key] = 0
    return state

def _read_new_lines(path, pos_key, state):
    if not os.path.exists(path):
        return []
    try:
        size = os.path.getsize(path)
        pos  = state.get(pos_key, size)
        if size < pos:
            pos = 0
        if size == pos:
            return []
        with open(path, 'r', errors='replace') as f:
            f.seek(pos)
            lines = f.readlines()
            state[pos_key] = f.tell()
        return lines
    except Exception as e:
        print("[read] " + str(e))
        return []

# ── SSH — so alerta login de IP desconhecido ─────────────────

ACCEPT_RE = re.compile(r'Accepted (?:password|publickey) for (\S+) from (\S+) port \S+ ssh2: (\S+) (\S+)')

def check_ssh_logins(state):
    lines = _read_new_lines(AUTH_LOG, 'auth_pos', state)
    if not lines:
        # fallback journalctl
        try:
            out = subprocess.run(
                ['journalctl', '-u', 'ssh', '--since', '1 minute ago', '--no-pager', '-o', 'short-iso'],
                capture_output=True, text=True, timeout=10).stdout
            lines = out.splitlines(keepends=True)
        except Exception:
            return

    known = set(state.get('known_ips', []))
    for line in lines:
        m = ACCEPT_RE.search(line)
        if m:
            user, ip, ktype, kfp = m.groups()
            if ip not in known and can_alert('ssh_ok_' + ip, 3600):
                send("\U0001f6aa LOGIN SSH — IP DESCONHECIDO",
                     "Novo acesso SSH detectado!\nUsuario: **`" + user + "`**",
                     color=0xf97316,
                     fields=[
                         {"name": "IP",          "value": "`" + ip + "`", "inline": True},
                         {"name": "Usuario",     "value": user,           "inline": True},
                         {"name": "Tipo chave",  "value": ktype,          "inline": True},
                         {"name": "Fingerprint", "value": kfp[:60],       "inline": False},
                         {"name": "Horario BRT", "value": now_br(),       "inline": True},
                     ] + geo_fields(ip))

# ── NGINX — scanner/recon (sem brute force) ───────────────────

SCANNER_PATTERNS = [
    (re.compile(r'(?i)(union.{1,20}select|drop.{1,10}table)'),          'SQL injection'),
    (re.compile(r'(?i)(\.\./|%2e%2e|%252e%252e)'),                      'path traversal'),
    (re.compile(r'(?i)(etc/passwd|etc/shadow|\.env|\.git/)'),           'sensitive file'),
    (re.compile(r'(?i)(eval\(|base64_decode|system\(|exec\()'),         'RCE attempt'),
    (re.compile(r'(?i)(wp-admin|wp-login|phpmyadmin|adminer)'),         'CMS probe'),
    (re.compile(r'(?i)(masscan|nikto|sqlmap|nmap|dirsearch|gobuster|nuclei|zgrab)'), 'scanner UA'),
    (re.compile(r'(?i)(xmlrpc\.php|/.git/config|/.aws/|/id_rsa|\.php$)'), 'critical path'),
    (re.compile(r'(?i)(\.env|\.sh|\.bak|\.sql|\.tar|\.zip|\.gz|\.key|\.pem)'), 'file probe'),
]

SENSITIVE_PATHS = (
    '.env', '.git', 'etc/passwd', 'wp-admin', 'wp-login', 'phpmyadmin',
    'adminer', '.aws', 'id_rsa', '.ssh', 'xmlrpc', 'backup', '.key', '.pem',
    'config.php', 'shell', 'cmd=', 'exec=', '.bak', '.sql',
)

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

_ip_4xx = defaultdict(int)

CF_PREFIXES = ('172.64.','172.65.','172.66.','172.67.','172.68.',
               '104.16.','104.17.','104.18.','104.19.','104.20.',
               '104.21.','104.22.','104.23.','104.24.','104.25.',
               '162.158.','198.41.','190.93.','188.114.')

def is_cf(ip):
    return any(ip.startswith(p) for p in CF_PREFIXES)

def parse_nginx(state):
    lines = _read_new_lines(NGINX_LOG, 'nginx_pos', state)
    stats = state.setdefault('stats', {})

    for line in lines:
        m = LOG_RE.match(line)
        if not m:
            continue
        ip, method, path, status, referer, ua = m.groups()
        status = int(status)
        stats['total_requests'] = stats.get('total_requests', 0) + 1

        # IP real quando vem do Cloudflare (extrai do campo CF-Connecting-IP no log detalhado)
        cf_match = re.search(r'\[([0-9a-fA-F.:]+)\]', line)
        real_ip = cf_match.group(1) if cf_match and cf_match.group(1) != '-' else ip

        # Detecta scanner
        scan_type = None
        for pattern, label in SCANNER_PATTERNS:
            if pattern.search(path) or pattern.search(ua):
                scan_type = label
                break
        if not scan_type and any(s in path.lower() for s in SENSITIVE_PATHS):
            scan_type = 'sensitive path'

        if scan_type:
            ban_ip(real_ip if real_ip != ip else ip, scan_type)
            stats['scanners'] = stats.get('scanners', 0) + 1
            alert_ip = real_ip if real_ip != ip else ip
            if can_alert('scan_' + alert_ip, 1800):
                label_ip = "`" + real_ip + "`" + (" (via CF: `" + ip + "`)" if is_cf(ip) and real_ip != ip else "")
                send("\U0001f50d SCANNER / RECON DETECTADO",
                     "Tentativa de reconhecimento bloqueada e IP banido.",
                     color=0xf59e0b,
                     fields=[
                         {"name": "IP Real",       "value": label_ip,        "inline": False},
                         {"name": "Tipo de ataque","value": scan_type,        "inline": True},
                         {"name": "Metodo",        "value": method,           "inline": True},
                         {"name": "Status",        "value": str(status),      "inline": True},
                         {"name": "Path",          "value": "`" + path[:120] + "`", "inline": False},
                         {"name": "User-Agent",    "value": ua[:120],         "inline": False},
                     ] + geo_fields(real_ip if real_ip != ip else ip))

        # Erro 500
        if status == 500 and can_alert('500_' + ip, 300):
            stats['errors_500'] = stats.get('errors_500', 0) + 1
            send("\U0001f4a5 ERRO 500 — POSSIVEL EXPLOIT",
                 "Erro interno no servidor.",
                 color=0xb91c1c,
                 fields=[
                     {"name": "IP",     "value": "`" + real_ip + "`", "inline": True},
                     {"name": "Metodo", "value": method,               "inline": True},
                     {"name": "Path",   "value": "`" + path[:100] + "`", "inline": True},
                     {"name": "UA",     "value": ua[:120],             "inline": False},
                 ] + geo_fields(real_ip if real_ip != ip else ip))

# ── ATTACK LOG — registros e logins com JSON completo ─────────

_REG_ALERTED: set = set()

def parse_attack_log(state):
    lines = _read_new_lines(ATTACK_LOG, 'attack_pos', state)

    for line in lines:
        line = line.strip()
        if not line:
            continue
        try:
            rec = json.loads(line)
        except Exception:
            continue

        ip     = rec.get('ip', '?')
        method = rec.get('method', '?')
        path   = rec.get('path', '?')
        status = rec.get('status', 0)
        ua     = rec.get('ua', '')[:150]
        body   = rec.get('body', '')
        resp   = rec.get('resp', '')

        # Novo registro (qualquer status — mostra JSON completo)
        if path == '/api/register' and method == 'POST':
            charge_key = ''
            try:
                b = json.loads(body) if body else {}
                charge_key = b.get('charge_id', b.get('reg_id', ''))[:20]
            except Exception:
                pass
            dedup_key = ip + '_reg_' + charge_key
            if dedup_key not in _REG_ALERTED:
                _REG_ALERTED.add(dedup_key)
                g = geo_fields(ip)
                body_display = body[:400] if body else '(vazio)'
                send("\U0001f4dd TENTATIVA DE REGISTRO",
                     "IP **`" + ip + "`** tentou criar conta. Status: **" + str(status) + "**",
                     color=0x22c55e if status == 200 else 0xf59e0b,
                     fields=[
                         {"name": "IP",           "value": "`" + ip + "`", "inline": True},
                         {"name": "Status",        "value": str(status),    "inline": True},
                         {"name": "JSON Enviado",
                          "value": "```json\n" + body_display + "\n```", "inline": False},
                         {"name": "User-Agent",   "value": ua,             "inline": False},
                     ] + g)

        # Login com sucesso (200) de IP novo — pode ser usuario legítimo ou invasor com senha certa
        elif path in ('/login', '/api/login') and method == 'POST' and status == 200:
            if can_alert('login_ok_' + ip, 3600):
                g = geo_fields(ip)
                try:
                    b = json.loads(body) if body else {}
                    user_field = b.get('username', b.get('user', '?'))
                except Exception:
                    user_field = '?'
                send("\U0001f511 LOGIN BEM-SUCEDIDO NO SITE",
                     "Alguem logou no site com sucesso.",
                     color=0x22c55e,
                     fields=[
                         {"name": "IP",       "value": "`" + ip + "`", "inline": True},
                         {"name": "Usuario",  "value": str(user_field), "inline": True},
                         {"name": "UA",       "value": ua,              "inline": False},
                     ] + g)

# ── FAIL2BAN — so alerta novos bans ──────────────────────────

def check_fail2ban(state):
    try:
        out = subprocess.run(['fail2ban-client', 'status', 'sshd'],
                             capture_output=True, text=True, timeout=10).stdout
    except Exception:
        return
    m = re.search(r'Currently banned:\s+(\d+)', out)
    total = int(m.group(1)) if m else 0
    m2 = re.search(r'Banned IP list:\s+(.*)', out)
    current = [x.strip() for x in m2.group(1).split() if x.strip()] if m2 else []
    prev = state.get('banned_ips', [])
    new_bans = [ip for ip in current if ip not in prev]
    state['banned_ips'] = current
    state.setdefault('stats', {})['f2b_total'] = total

    # Alerta agrupado: se muitos banidos de uma vez, manda so 1 mensagem
    if new_bans and can_alert('f2b_batch', 300):
        sample = new_bans[:5]
        ips_text = '\n'.join('`' + ip + '`' for ip in sample)
        if len(new_bans) > 5:
            ips_text += '\n... e mais ' + str(len(new_bans) - 5)
        send("\U0001f6ab FAIL2BAN — " + str(len(new_bans)) + " IP(s) BANIDO(s)",
             "Fail2Ban bloqueou IPs por brute force SSH.",
             color=0x7c3aed,
             fields=[
                 {"name": "IPs banidos agora", "value": ips_text,   "inline": False},
                 {"name": "Total banidos",      "value": str(total), "inline": True},
             ])

# ── RESUMO DIARIO ─────────────────────────────────────────────

def check_daily_summary(state):
    now = datetime.now(timezone.utc)
    today = now.strftime('%Y-%m-%d')
    if state.get('last_summary_day') == today or now.hour != 0:
        return
    stats = state.get('stats', {})
    top = sorted(state.get('ssh_fails', {}).items(), key=lambda x: -x[1])[:5]
    top_text = '\n'.join('`' + ip + '`: ' + str(c) for ip, c in top) or 'Nenhum'
    send("\U0001f4ca RESUMO DIARIO",
         "Relatorio de seguranca do dia **" + now.strftime('%d/%m/%Y') + "**",
         color=0x3b82f6,
         fields=[
             {"name": "Requisicoes Web",    "value": str(stats.get('total_requests', 0)), "inline": True},
             {"name": "Scanners/Recon",     "value": str(stats.get('scanners', 0)),       "inline": True},
             {"name": "Erros 500",          "value": str(stats.get('errors_500', 0)),     "inline": True},
             {"name": "IPs banidos F2B",    "value": str(stats.get('f2b_total', 0)),      "inline": True},
             {"name": "Top Atacantes SSH",  "value": top_text,                             "inline": False},
         ])
    state['stats'] = {k: 0 for k in stats}
    state['last_summary_day'] = today

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

def main():
    print("[Monitor] Iniciando...")
    state = load_state()
    _init_pos(state, 'nginx_pos',  NGINX_LOG)
    _init_pos(state, 'auth_pos',   AUTH_LOG)
    _init_pos(state, 'attack_pos', ATTACK_LOG)
    save_state(state)

    try:
        pub_ip = _get_json('https://api.ipify.org?format=json')
        pub_ip = pub_ip.get('ip','167.148.161.78') if isinstance(pub_ip, dict) else '167.148.161.78'
    except Exception:
        pub_ip = '167.148.161.78'

    send("✅ MONITOR REINICIADO",
         "Sistema ativo. Alertando apenas o que importa.",
         color=0x22c55e,
         fields=[
             {"name": "Host",     "value": HOSTNAME,  "inline": True},
             {"name": "IP",       "value": pub_ip,    "inline": True},
             {"name": "Horario",  "value": now_br(),  "inline": True},
             {"name": "Alertas ativos", "value":
                 "Scanner / Recon web\n"
                 "IP auto-banido (UFW + Fail2Ban)\n"
                 "Login SSH de IP desconhecido\n"
                 "Tentativa de registro (JSON completo)\n"
                 "Login bem-sucedido no site\n"
                 "Erro 500 / exploit\n"
                 "Resumo diario 00:00 UTC",
              "inline": False},
         ])

    tick = 0
    while True:
        try:
            check_ssh_logins(state)
            parse_nginx(state)
            parse_attack_log(state)
            if tick % 2 == 0:
                check_fail2ban(state)
            if tick % 6 == 0:
                check_daily_summary(state)
            save_state(state)
            tick += 1
        except Exception as e:
            print("[Erro] " + str(e))
        time.sleep(30)

if __name__ == '__main__':
    main()
