#!/usr/bin/env python3
"""
Recolector de logs de acceso para Linux — Ciberkit
========================================================

Junta en un solo fichero los logs que sirven para saber si alguien entró en el
servidor: auth.log o secure (también los rotados y comprimidos .gz) o, si el
sistema solo usa journald, las entradas de sshd, sudo, su y gestión de cuentas.
Opcionalmente añade los accesos de Nginx y Apache. El resultado se analiza en
Ciberkit -> Logs -> Analizador de logs, que no lo sube a ningún sitio.

    NO CAMBIA NADA: solo lee. Ejecútalo como root para poder leer los logs.
    Úsalo SOLO en servidores propios o con autorización.

Sin dependencias: Python 3.6+.

Ejemplos
--------
  sudo python3 recoger_logs_linux.py                 # últimos 7 días → logs-HOST.log
  sudo python3 recoger_logs_linux.py --dias 30 --web
  sudo python3 recoger_logs_linux.py --salida /tmp/accesos.log
"""

import argparse
import glob
import gzip
import os
import re
import socket
import subprocess
import sys
import time

AUTH = ["/var/log/auth.log*", "/var/log/secure*"]
WEB = ["/var/log/nginx/access.log*", "/var/log/apache2/access.log*", "/var/log/httpd/access_log*", "/var/log/httpd/*access*log*"]
JOURNAL_COMM = ["sshd", "sshd-session", "sudo", "su", "useradd", "usermod", "userdel", "gpasswd", "passwd", "chpasswd"]


def orden_rotacion(ruta):
    """auth.log.4.gz, auth.log.3.gz … auth.log: del más antiguo al más reciente."""
    m = re.search(r"\.(\d+)(\.gz)?$", ruta)
    if m:
        return (0, -int(m.group(1)))
    m = re.search(r"-(\d{8})(\.gz)?$", ruta)  # secure-20261105 (RHEL)
    if m:
        return (0, int(m.group(1)) - 99999999)
    return (1, 0)


def descubrir(patrones, raiz="", dias=7, ahora=None):
    """Ficheros que existen y se modificaron en los últimos `dias` (los rotados viejos no aportan)."""
    ahora = ahora or time.time()
    limite = ahora - dias * 86400
    vistos, grupos = set(), {}
    for p in patrones:
        for ruta in glob.glob(raiz + p):
            if ruta in vistos or not os.path.isfile(ruta):
                continue
            vistos.add(ruta)
            if os.path.getmtime(ruta) < limite:
                continue
            base = re.sub(r"(\.\d+)?(\.gz)?$|-\d{8}(\.gz)?$", "", os.path.basename(ruta))
            grupos.setdefault(os.path.join(os.path.dirname(ruta), base), []).append(ruta)
    salida = []
    for base in sorted(grupos):
        salida.extend(sorted(grupos[base], key=orden_rotacion))
    return salida


def leer(ruta):
    """Líneas de un fichero de texto o .gz (sin fallar con bytes no UTF-8)."""
    with open(ruta, "rb") as f:
        gz = f.read(2) == b"\x1f\x8b"
    abrir = gzip.open if gz else open
    with abrir(ruta, "rt", encoding="utf-8", errors="replace") as f:
        for linea in f:
            yield linea.rstrip("\n")


def journal(dias, ejecutar=subprocess.run):
    """Entradas de journald (si no hay auth.log ni secure)."""
    orden = ["journalctl", "--no-pager", "-o", "short-iso", "--since", "-{}d".format(dias)]
    for c in JOURNAL_COMM:
        orden += ["_COMM=" + c]
    try:
        r = ejecutar(orden, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300)
    except (OSError, subprocess.SubprocessError) as e:
        return None, str(e)
    if r.returncode != 0:
        return None, (r.stderr or b"").decode("utf-8", "replace").strip()[:200]
    texto = (r.stdout or b"").decode("utf-8", "replace")
    return [l for l in texto.splitlines() if l and not l.startswith("-- ")], None


def main(argv=None, raiz="", ejecutar=subprocess.run, ahora=None):
    ap = argparse.ArgumentParser(description="Junta los logs de acceso (SSH, sudo, cuentas y, opcionalmente, web) en un fichero para el analizador.")
    ap.add_argument("--dias", type=int, default=7, help="días hacia atrás (por defecto 7)")
    ap.add_argument("--web", action="store_true", help="incluir también los accesos de Nginx y Apache")
    ap.add_argument("--salida", default="logs-{}.log".format(socket.gethostname().split(".")[0] or "servidor"), help="fichero de salida")
    ap.add_argument("--max-mb", type=int, default=500, help="tamaño máximo de la salida en MB (por defecto 500)")
    a = ap.parse_args(argv)

    if hasattr(os, "geteuid") and os.geteuid() != 0 and not raiz:
        print("Aviso: sin root no se pueden leer /var/log/auth.log ni /var/log/secure.", file=sys.stderr)

    ficheros = descubrir(AUTH, raiz, a.dias, ahora)
    fuentes = []
    limite = a.max_mb * 1024 * 1024
    escrito = 0
    lineas = 0
    cortado = False
    with open(a.salida, "w", encoding="utf-8", newline="\n") as out:
        def volcar(origen, it):
            nonlocal escrito, lineas, cortado
            n = 0
            for l in it:
                if escrito >= limite:
                    cortado = True
                    break
                out.write(l + "\n")
                escrito += len(l) + 1
                n += 1
            lineas += n
            fuentes.append((origen, n))

        for f in ficheros:
            try:
                volcar(f, leer(f))
            except (OSError, EOFError) as e:
                fuentes.append((f, "error: {}".format(e.strerror if isinstance(e, OSError) else e)))
        if not ficheros:
            lj, err = journal(a.dias, ejecutar)
            if lj is None:
                fuentes.append(("journalctl", "error: {}".format(err)))
            else:
                volcar("journalctl (sshd, sudo, su, cuentas)", lj)
        if a.web:
            for f in descubrir(WEB, raiz, a.dias, ahora):
                try:
                    volcar(f, leer(f))
                except (OSError, EOFError) as e:
                    fuentes.append((f, "error: {}".format(e.strerror if isinstance(e, OSError) else e)))

    print("Fuentes:")
    for origen, n in fuentes:
        print("  {:<52} {}".format(origen, "{} líneas".format(n) if isinstance(n, int) else n))
    if cortado:
        print("Aviso: se alcanzó el máximo de {} MB; usa menos --dias o sube --max-mb.".format(a.max_mb))
    print("{} líneas guardadas en {}".format(lineas, os.path.abspath(a.salida)))
    print("Arrástralo a Ciberkit -> Logs -> Analizador de logs. No sale de tu navegador.")
    print("Ojo: contiene usuarios e IPs; no lo compartas sin revisarlo.")
    return 0 if lineas else 1


if __name__ == "__main__":
    sys.exit(main())
