#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
detector_unificado.py

Detector Unificado ENS para Windows + Linux (versión mejorada):

– Lee JSON línea a línea desde /var/log/incidentes.json (Logstash).
– Para Windows:
    • EventID 4625 (falla de logon) → posible fuerza bruta.
    • EventID 4624 (logon exitoso) → detecta fuerza bruta con éxito tras varios fallos.
    • EventID 4720, 4722, 4728, 4732 (creación/habilitación/elevación) → alerta.
    • EventID 5152, 5156 (Firewall Drop/Allow) → alerta tráfico bloqueado/permitido.
– Para Linux (JSON via Logstash debe incluir campo “message”):
    • “authentication failure” → cuenta fallos SSH.
    • “Invalid user” → intento usuario inexistente.
    • “Accepted password” → si hubo fallos antes, fuerza bruta con éxito.
– También se monitorizan hashes de archivos críticos de Linux
  (/etc/passwd, /etc/shadow, /etc/ssh/sshd_config); si cambian, alerta.
– Cada alerta se muestra en ROJO, luego línea en blanco, luego JSON conciso.
– Todas las alertas se guardan en /var/log/incidentes_alertas.log.
"""

import json
import time
import sys
import hashlib
from datetime import datetime, timedelta
from collections import defaultdict, deque

try:
    import colorama
    from colorama import Fore, Style
except ImportError:
    sys.exit("Falta módulo 'colorama'. Ejecuta: pip install colorama")

colorama.init(autoreset=True)

# ──────────────────────────────────────────────────────────
# RUTAS Y CONSTANTES
# ──────────────────────────────────────────────────────────

INCIDENTS_JSON      = "/var/log/incidentes.json"
ALERTS_LOG          = "/var/log/incidentes_alertas.log"
FILE_INTEGRITY_LOG  = "/var/log/file_integrity.log"  # Opcional, para registrar solo integridad

WINDOW_MINUTES      = 2    # Ventana fuerza bruta Windows
FAIL_THRESHOLD      = 5

SSH_WINDOW_MINUTES  = 2    # Ventana fuerza bruta SSH
SSH_THRESHOLD       = 5

FILE_CHECK_INTERVAL = 60   # segundos entre chequeo hashes

# Archivos críticos Linux a monitorizar (ruta absoluta)
FILES_TO_MONITOR = [
    "/etc/passwd",
    "/etc/shadow",
    "/etc/ssh/sshd_config"
]

# ──────────────────────────────────────────────────────────
# VARIABLES GLOBALES
# ──────────────────────────────────────────────────────────

# Para fuerza bruta Windows: key = (usuario, ip), valor = deque(timestamps)
failed_logins       = defaultdict(deque)
# Para detectar éxito tras fallos en Windows: key misma, valor = cont_fallos
failed_count_win    = defaultdict(int)

# Para fuerza bruta SSH Linux: key = (usuario, ip), valor = deque(timestamps)
ssh_failed          = defaultdict(deque)
# Para detectar éxito tras fallos SSH: key misma, valor = cont_fallos
ssh_failed_count    = defaultdict(int)

# Hashes iniciales de archivos Linux
file_hashes         = {}

# Timestamp del último chequeo de integridad
last_file_check     = datetime.now()


# ──────────────────────────────────────────────────────────
# FUNCIONES AUXILIARES
# ──────────────────────────────────────────────────────────

def log_alert(message: str):
    """
    Imprime la alerta en rojo y la guarda en ALERTS_LOG con timestamp.
    """
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    linea = f"[{timestamp}] {message}"
    print(Fore.RED + "[ALERTA] " + linea + Style.RESET_ALL)
    try:
        with open(ALERTS_LOG, "a", encoding="utf-8") as f:
            f.write(linea + "\n")
    except Exception as e:
        print(Fore.YELLOW + f"[!] No pude escribir en {ALERTS_LOG}: {e}" + Style.RESET_ALL)

def print_json_with_timestamp(info: dict):
    """
    Añade timestamp actual a 'info' y lo imprime como JSON formateado.
    """
    info["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(Fore.CYAN + json.dumps(info, indent=2, ensure_ascii=False) + Style.RESET_ALL)

def sha256_of_file(path: str) -> str:
    """
    Calcula el hash SHA256 del archivo en 'path'.
    """
    try:
        h = hashlib.sha256()
        with open(path, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                h.update(chunk)
        return h.hexdigest()
    except Exception:
        return ""


# ──────────────────────────────────────────────────────────
# FILE INTEGRITY MONITORING (chequeo periódico)
# ──────────────────────────────────────────────────────────

def init_file_hashes():
    """
    Calcula hashes iniciales de los archivos en FILES_TO_MONITOR.
    """
    global file_hashes
    for ruta in FILES_TO_MONITOR:
        file_hashes[ruta] = sha256_of_file(ruta)

def check_file_integrity():
    """
    Recalcula hashes y, si cambia alguno, lanza alerta.
    """
    global file_hashes, last_file_check
    ahora = datetime.now()
    if (ahora - last_file_check).total_seconds() < FILE_CHECK_INTERVAL:
        return
    last_file_check = ahora

    for ruta, hash_old in file_hashes.items():
        hash_nuevo = sha256_of_file(ruta)
        if hash_nuevo and hash_nuevo != hash_old:
            # Alerta de modificación de archivo
            msg = f"Archivo crítico modificado: {ruta}"
            info = {
                "tipo": "Linux_FileModificado",
                "ruta": ruta,
                "hash_anterior": hash_old,
                "hash_nuevo": hash_nuevo
            }
            log_alert(msg)
            print()
            print_json_with_timestamp(info)
            print()
            # Actualizamos al nuevo
            file_hashes[ruta] = hash_nuevo


# ──────────────────────────────────────────────────────────
# LÓGICA DE DETECCIÓN POR EVENTO
# ──────────────────────────────────────────────────────────

def procesar_evento(parsed: dict):
    """
    Procesa un evento JSON ya parseado:
      - Windows: Eventos 4625, 4624, 4720, 4722, 4728, 4732, 5152, 5156
      - Linux: “authentication failure”, “invalid user”, “Accepted password”
    """
    # 1) EVENTOS WINDOWS
    if "winlog" in parsed:
        win   = parsed["winlog"]
        edata = win.get("event_data", {})

        try:
            event_id = int(win.get("event_id", -1))
        except:
            return

        # --- 4625: fallo de logon → posible fuerza bruta
        if event_id == 4625:
            user = edata.get("TargetUserName", "UNKNOWN")
            ip   = edata.get("IpAddress", edata.get("SourceNetworkAddress", "UNKNOWN"))
            key  = (user, ip)
            now  = datetime.now()
            dq   = failed_logins[key]
            dq.append(now)
            failed_count_win[key] += 1

            ventana = now - timedelta(minutes=WINDOW_MINUTES)
            while dq and dq[0] < ventana:
                dq.popleft()

            if len(dq) >= FAIL_THRESHOLD:
                msg = (f"{len(dq)} fallos de logon Windows para usuario '{user}' "
                       f"desde {ip} en {WINDOW_MINUTES} minutos → POSIBLE FUERZA BRUTA (4625)")
                info = {
                    "tipo":       "Windows_FuerzaBruta",
                    "event_id":   event_id,
                    "usuario":    user,
                    "origen":     ip,
                    "num_fallos": len(dq),
                    "ventana_min": WINDOW_MINUTES
                }
                log_alert(msg)
                print()
                print_json_with_timestamp(info)
                print()
                dq.clear()
                failed_count_win[key] = 0
            return

        # --- 4624: logon exitoso → posible fuerza bruta con éxito
        if event_id == 4624:
            user = edata.get("TargetUserName", "UNKNOWN")
            ip   = edata.get("IpAddress", edata.get("SourceNetworkAddress", "UNKNOWN"))
            key  = (user, ip)
            if failed_count_win.get(key, 0) >= 2:  # por ejemplo, al menos 2 fallos seguidos antes del éxito
                msg = (f"Inicio de sesión exitoso tras múltiples fallos para usuario '{user}' "
                       f"desde {ip} → POSIBLE FUERZA BRUTA CON ÉXITO (4624)")
                info = {
                    "tipo":       "Windows_FuerzaBrutaConExito",
                    "event_id":   event_id,
                    "usuario":    user,
                    "origen":     ip,
                    "num_fallos_previos": failed_count_win[key]
                }
                log_alert(msg)
                print()
                print_json_with_timestamp(info)
                print()
            # Reiniciamos contadores aunque no supere umbral
            failed_count_win[key] = 0
            failed_logins.pop(key, None)
            return

        # --- 4720: creación de cuenta local
        if event_id == 4720:
            new_user = edata.get("TargetUserName", "UNKNOWN")
            msg = f"Creación de cuenta local Windows '{new_user}' detectada → INCIDENTE (4720)."
            info = {
                "tipo":     "Windows_CreacionCuenta",
                "event_id": event_id,
                "usuario":  new_user
            }
            log_alert(msg)
            print()
            print_json_with_timestamp(info)
            print()
            return

        # --- 4722: habilitación de cuenta local
        if event_id == 4722:
            en_user = edata.get("TargetUserName", "UNKNOWN")
            msg = f"Cuenta local Windows '{en_user}' habilitada → REVISA si debe ser admin (4722)."
            info = {
                "tipo":     "Windows_HabilitacionCuenta",
                "event_id": event_id,
                "usuario":  en_user
            }
            log_alert(msg)
            print()
            print_json_with_timestamp(info)
            print()
            return

        # --- 4728: agregado a grupo GLOBAL → elevación
        if event_id == 4728:
            group = edata.get("GroupName", edata.get("TargetUserName", "UNKNOWN"))
            grp_lower = group.strip().lower()
            if "administrators" in grp_lower or "administradores" in grp_lower:
                msg = f"Miembro agregado a grupo GLOBAL Windows '{group}' → ELEVACIÓN (4728)."
                info = {
                    "tipo":     "Windows_ElevacionGlobal",
                    "event_id": event_id,
                    "grupo":    group
                }
                log_alert(msg)
                print()
                print_json_with_timestamp(info)
                print()
            return

        # --- 4732: agregado a grupo LOCAL → elevación
        if event_id == 4732:
            group = edata.get("GroupName", edata.get("TargetUserName", "UNKNOWN"))
            grp_lower = group.strip().lower()
            if "administrators" in grp_lower or "administradores" in grp_lower:
                msg = f"Miembro agregado a grupo LOCAL Windows '{group}' → ELEVACIÓN (4732)."
                info = {
                    "tipo":     "Windows_ElevacionLocal",
                    "event_id": event_id,
                    "grupo":    group
                }
                log_alert(msg)
                print()
                print_json_with_timestamp(info)
                print()
            return

        # --- 5152: Windows Firewall DROP
        if event_id == 5152:
            src_ip = edata.get("SourceAddress", "UNKNOWN")
            dst_pr = edata.get("DestinationPort", "UNKNOWN")
            msg = f"Windows Firewall BLOQUEÓ conexión desde {src_ip} a puerto {dst_pr} (5152)."
            info = {
                "tipo":          "Windows_Firewall_DROP",
                "event_id":      event_id,
                "ip_origen":     src_ip,
                "puerto_destino": dst_pr
            }
            log_alert(msg)
            print()
            print_json_with_timestamp(info)
            print()
            return

        # --- 5156: Windows Firewall ALLOW
        if event_id == 5156:
            src_ip = edata.get("SourceAddress", "UNKNOWN")
            dst_pr = edata.get("DestinationPort", "UNKNOWN")
            msg = f"Windows Firewall PERMITIÓ conexión desde {src_ip} a puerto {dst_pr} (5156)."
            info = {
                "tipo":          "Windows_Firewall_ALLOW",
                "event_id":      event_id,
                "ip_origen":     src_ip,
                "puerto_destino": dst_pr
            }
            log_alert(msg)
            print()
            print_json_with_timestamp(info)
            print()
            return

        return

    # 2) EVENTOS LINUX
    msg_text = parsed.get("message", "").lower()

    # --- 2.1) “Invalid user” (SSH)
    if "invalid user" in msg_text:
        partes  = msg_text.split()
        usuario = "UNKNOWN"
        ip_src  = "UNKNOWN"
        for idx, tok in enumerate(partes):
            if tok == "invalid" and idx+2 < len(partes) and partes[idx+1] == "user":
                usuario = partes[idx+2]
            if tok == "from" and idx+1 < len(partes):
                ip_src = partes[idx+1]
        msg = f"Intento SSH con usuario inválido '{usuario}' desde {ip_src} detectado en Linux."
        info = {
            "tipo":    "Linux_InvalidUser",
            "usuario": usuario,
            "origen":  ip_src
        }
        log_alert(msg)
        print()
        print_json_with_timestamp(info)
        print()
        return

    # --- 2.2) “authentication failure” → posible fuerza bruta SSH
    if "authentication failure" in msg_text:
        partes  = msg_text.split()
        usuario = "UNKNOWN"
        ip_src  = "UNKNOWN"
        for tok in partes:
            if tok.startswith("user="):
                usuario = tok.split("=",1)[1]
            if tok.startswith("rhost="):
                ip_src = tok.split("=",1)[1]

        key = (usuario, ip_src)
        now = datetime.now()
        dq  = ssh_failed[key]
        dq.append(now)
        ssh_failed_count[key] += 1

        ventana = now - timedelta(minutes=SSH_WINDOW_MINUTES)
        while dq and dq[0] < ventana:
            dq.popleft()

        if len(dq) >= SSH_THRESHOLD:
            msg = (f"{len(dq)} fallos de autenticación SSH Linux para usuario '{usuario}' "
                   f"desde {ip_src} en {SSH_WINDOW_MINUTES} minutos → POSIBLE FUERZA BRUTA SSH")
            info = {
                "tipo":       "Linux_FuerzaBrutaSSH",
                "usuario":    usuario,
                "origen":     ip_src,
                "num_fallos": len(dq),
                "ventana_min": SSH_WINDOW_MINUTES
            }
            log_alert(msg)
            print()
            print_json_with_timestamp(info)
            print()
            dq.clear()
            ssh_failed_count[key] = 0
        return

    # --- 2.3) “Accepted password” → éxito tras fallos SSH
    if "accepted password" in msg_text:
        partes  = msg_text.split()
        usuario = "UNKNOWN"
        ip_src  = "UNKNOWN"
        for tok in partes:
            if tok.startswith("user="):
                usuario = tok.split("=",1)[1]
            if tok.startswith("rhost="):
                ip_src = tok.split("=",1)[1]

        key = (usuario, ip_src)
        if ssh_failed_count.get(key, 0) >= 2:
            msg = (f"Inicio SSH exitoso tras múltiples fallos para usuario '{usuario}' "
                   f"desde {ip_src} → POSIBLE FUERZA BRUTA CON ÉXITO SSH.")
            info = {
                "tipo":                "Linux_FuerzaBrutaConExitoSSH",
                "usuario":             usuario,
                "origen":              ip_src,
                "num_fallos_previos":  ssh_failed_count[key]
            }
            log_alert(msg)
            print()
            print_json_with_timestamp(info)
            print()
        ssh_failed_count[key] = 0
        ssh_failed.pop(key, None)
        return

    return


# ──────────────────────────────────────────────────────────
# BUCLE PRINCIPAL: “tail -f” de INCIDENTS_JSON + chequeo integridad
# ──────────────────────────────────────────────────────────

def main():
    # Inicializar hashes de integridad
    init_file_hashes()

    # 1) Asegurar que exista INCIDENTS_JSON
    try:
        open(INCIDENTS_JSON, "a").close()
    except Exception as e:
        sys.exit(f"No pude crear/abrir {INCIDENTS_JSON}: {e}")

    # 2) Abrir en modo lectura
    try:
        f = open(INCIDENTS_JSON, "r", encoding="utf-8")
    except Exception as e:
        sys.exit(f"No pude abrir {INCIDENTS_JSON}: {e}")

    # 3) Colocar puntero al final para ignorar eventos previos
    f.seek(0, 2)

    print(Fore.GREEN + "[*] Iniciando Detector Unificado (JSON)…\n" + Style.RESET_ALL +
          "Pulsa Ctrl+C para detener.\n")

    try:
        while True:
            # Chequeo periódico de integridad de archivos Linux
            check_file_integrity()

            linea = f.readline()
            if not linea:
                time.sleep(0.2)
                continue

            linea = linea.strip()
            if not linea:
                continue

            # Parsear JSON
            try:
                parsed = json.loads(linea)
            except Exception as ex:
                print(Fore.YELLOW + f"[!] Línea no JSON válida: {ex}" + Style.RESET_ALL)
                continue

            # Procesar el evento (Windows o Linux)
            try:
                procesar_evento(parsed)
            except Exception as ex:
                print(Fore.YELLOW + f"[!] Error al procesar evento: {ex}" + Style.RESET_ALL)

    except KeyboardInterrupt:
        print(Fore.YELLOW + "\n[!] Detector detenido por usuario." + Style.RESET_ALL)
        sys.exit(0)
    except Exception as e:
        sys.exit(f"[ERROR FATAL] {e}")


if __name__ == "__main__":
    main()