import re
from datetime import datetime

# ---------------------------------------------------------------------------------
#  Mapa con la descripción breve de cada control ENS y lo que exige
#  Para el TFG nivel Alto, nos centramos en:
#    - OP.ACC.1  – Identificación
#    - OP.ACC.2  – Requisitos de Acceso
#    - OP.ACC.4  – Gestión de Derechos de Acceso
#    - OP.ACC.5  – Mecanismos de Autenticación (Usuarios Externos)
#    - OP.ACC.6  – Mecanismos de Autenticación (Usuarios Internos)
# ---------------------------------------------------------------------------------
CONTROLES_DESCRIPCION = {
    "Op.acc.1": {
        "titulo": "OP.ACC.1 – Identificación",
        "descripcion": (
            "Este control ENS exige que cada usuario tenga identificador único "
            "y que no haya cuentas duplicadas o con shells no válidas (por ejemplo, nologin "
            "sin motivo). Permite garantizar trazabilidad y responsabilidad individual."
        )
    },
    "Op.acc.2": {
        "titulo": "OP.ACC.2 – Requisitos de Acceso",
        "descripcion": (
            "Este control verifica que los grupos privilegiados (sudo, adm, Administradores, etc.) "
            "se utilicen únicamente para cuentas autorizadas. Un exceso de usuarios con privilegios "
            "eleva el riesgo de cambios no autorizados."
        )
    },
    "Op.acc.4": {
        "titulo": "OP.ACC.4 – Gestión de Derechos de Acceso",
        "descripcion": (
            "Este control comprueba que no existan cuentas bloqueadas sin eliminar o cuentas expiradas "
            "que puedan representar vectores de ataque. También inspecciona si existen usuarios con "
            "cuentas sin contraseña o deshabilitadas sin purgar."
        )
    },
    "Op.acc.5": {
        "titulo": "OP.ACC.5 – Mecanismos de Autenticación (Usuarios Externos)",
        "descripcion": (
            "Aquí se comprueba el acceso remoto, por ejemplo SSH en Linux o RDP en Windows. "
            "Se verifica que el login remoto como root (o administrador) esté desactivado y que no "
            "se permita autenticación por contraseña si no está justificado. "
            "En resumen, asegura que usuarios externos no puedan conectarse sin controles fuertes."
        )
    },
    "Op.acc.6": {
        "titulo": "OP.ACC.6 – Mecanismos de Autenticación (Usuarios Internos)",
        "descripcion": (
            "Este control analiza las políticas de contraseña y complejidad local (por ejemplo, "
            "longitud mínima, caducidad, bloqueo tras X intentos). Previene que usuarios internos "
            "tengan contraseñas débiles o nunca expiren."
        )
    }
}

# ---------------------------------------------------------------------------------
#  Funciones auxiliares para generar recomendaciones según el tipo de hallazgo
# ---------------------------------------------------------------------------------

def recomendacion_op_acc_1(hallazgo):
    """
    Recibe una línea que empiece por 'UID duplicado:' o 'Usuario con shell sospechosa:'
    Devuelve un texto de recomendación.
    """
    if hallazgo.startswith("UID duplicado:"):
        return (
            "Existen dos o más cuentas con el mismo UID, lo cual rompe la unicidad "
            "de identificación. Recomendación: Asigne UIDs distintos a cada usuario para "
            "mantener trazabilidad."
        )
    if hallazgo.startswith("Usuario con shell sospechosa:"):
        # Extraemos el usuario / shell del texto
        # Ejemplo de línea: "Usuario con shell sospechosa: juan -> /usr/sbin/nologin"
        return (
            "El usuario tiene un shell no válido (por ejemplo '/usr/sbin/nologin' sin justificación). "
            "Recomendación: Verifique si realmente esa cuenta debe estar deshabilitada. Si es una cuenta de sistema, "
            "debería describirlo en la política; si es un usuario real, asigne shell válido o compruebe su propósito."
        )
    return "Revisar hallazgo relacionado con OP.ACC.1."

def recomendacion_op_acc_2(hallazgo):
    """
    Hallazgos: 'Usuario con privilegios sudo: usuarioX'
               'Usuario con privilegios elevados en grupo adm: usuarioY', etc.
    """
    # Si el hallazgo indica el admin principal, lo explicamos como algo normal.
    m = re.match(r"Usuario con privilegios.*:\s*(\S+)", hallazgo)
    if m:
        usuario = m.group(1)
        # Supongamos que el administrador normalmente es 'root' o 'Administrador';
        # no lo marcamos como crítico: 
        if usuario.lower() in ("root", "administrador", "admin"):
            return (
                f"El usuario '{usuario}' pertenece a un grupo privilegiado, lo cual es esperado "
                "para la cuenta de administrador principal. Si este es su usuario de administración, "
                "no es necesariamente un problema, pero asegúrese de que sólo estas cuentas tengan "
                "privilegios elevados. Revise otros usuarios que no deban estar en estos grupos."
            )
        else:
            return (
                f"El usuario '{usuario}' tiene privilegios elevados (por ejemplo, en 'sudo', 'adm', 'Administradores'). "
                "Recomendación: Si no es un usuario de administración justificado, revocar estos privilegios. "
                "Sólo las cuentas de administración deben pertenecer a estos grupos."
            )
    # Si no encaja con el patrón, se da una recomendación genérica
    return (
        "Se detectó un usuario en un grupo privilegiado. Verifique si esa cuenta debe tener "
        "permisos elevados y, si no es necesario, remuévala de dicho grupo."
    )

def recomendacion_op_acc_4(hallazgo):
    """
    Hallazgos: 'Cuenta bloqueada: usuarioX'
              'Usuario expirado: usuarioY (expiró el AAAA-MM-DD)'
    """
    if hallazgo.startswith("Cuenta bloqueada:"):
        return (
            "Se detectó una cuenta bloqueada que no ha sido eliminada. "
            "Recomendación: Si la cuenta ya no se usa, elimínela. De lo contrario, documente la razón "
            "por la cual está bloqueada y cuándo se procederá a su borrado definitivo."
        )
    if hallazgo.startswith("Usuario expirado:"):
        return (
            "Se detectó un usuario con cuenta expirada. Recomendación: Determine si esa cuenta aún se usa. "
            "Si no, elimínela o reactívela correctamente. "
            "Si debe seguir activa, actualice la fecha de expiración en /etc/shadow."
        )
    return "Revisar hallazgo relacionado con OP.ACC.4."

def recomendacion_op_acc_5(hallazgo):
    """
    Hallazgos Linux: 'PermitRootLogin ...', 'PasswordAuthentication ...', 'PubkeyAuthentication ...'
    Hallazgos Windows: líneas de RDP
    """
    if "PermitRootLogin" in hallazgo:
        if "activado" in hallazgo or "yes" in hallazgo.lower():
            return (
                "El inicio de sesión root vía SSH está permitido. Esto rompe el principio de menor privilegio "
                "para usuarios externos. Recomendación: En /etc/ssh/sshd_config desactive 'PermitRootLogin'."
            )
        else:
            return "SSH como root está correctamente desactivado."
    if "PasswordAuthentication" in hallazgo:
        if "activado" in hallazgo or "yes" in hallazgo.lower():
            return (
                "La autenticación por contraseña vía SSH está permitida. Esto puede facilitar ataques de fuerza bruta. "
                "Recomendación: Desactive 'PasswordAuthentication' y use autenticación por clave fuerte."
            )
        else:
            return "La autenticación por contraseña SSH está desactivada, lo cual es recomendable."
    if "PubkeyAuthentication" in hallazgo:
        if "desactivado" in hallazgo or "no" in hallazgo.lower():
            return (
                "La autenticación por clave pública está desactivada. Para ENS nivel Alto, se recomienda "
                "usar claves públicas en lugar de password. Activar 'PubkeyAuthentication yes'."
            )
        else:
            return "La autenticación por clave pública está habilitada, lo cual es correcto."
    # Para Windows, RDP:
    if "RDP está habilitado" in hallazgo or "RDP enabled" in hallazgo:
        return (
            "El acceso RDP está habilitado. ENS nivel Alto recomienda deshabilitar RDP o restringirlo "
            "mediante VPN y controles de múltiples factores. Revise la política de acceso remoto."
        )
    if "RDP está deshabilitado" in hallazgo:
        return "El acceso RDP está deshabilitado, que es lo recomendado para Entornos de Alta Seguridad."
    return "Revisar hallazgo relacionado con OP.ACC.5."

def recomendacion_op_acc_6(hallazgo):
    """
    Hallazgos Linux: PASS_MAX_DAYS, PASS_MIN_DAYS, PASS_MIN_LEN, PASS_WARN_AGE, pam_pwquality, etc.
    Hallazgos Windows: líneas 'PASS_MAX_DAYS = X', 'PASS_MIN_DAYS = Y', 'INTENTOS_FALLIDOS = Z'
    """
    # Linux: ej. "PASS_MAX_DAYS = 42 (valor esperado: 90)"
    m_max = re.match(r"PASS_MAX_DAYS\s*=\s*(\d+).*esperado:\s*(\d+)", hallazgo)
    if m_max:
        actual = int(m_max.group(1))
        esperado = int(m_max.group(2))
        return (
            f"La política de caducidad de contraseñas es de {actual} días, pero se recomienda "
            f"un máximo de {esperado} para nivel Alto. Aumente PASS_MAX_DAYS a >= {esperado}."
        )
    m_min = re.match(r"PASS_MIN_DAYS\s*=\s*(\d+).*esperado:\s*(\d+)", hallazgo)
    if m_min:
        actual = int(m_min.group(1))
        esperado = int(m_min.group(2))
        return (
            f"Se permite cambiar contraseña cada {actual} días, pero ENS recomienda mínimo {esperado} días. "
            f"Establezca PASS_MIN_DAYS a >= {esperado} para evitar cambios frecuentes que puedan debilitar seguridad."
        )
    m_len = re.match(r"minlen=(\d+)", hallazgo)
    if m_len:
        longitud = int(m_len.group(1))
        if longitud < 8:
            return (
                f"La longitud mínima de contraseña está configurada en {longitud}, pero se recomienda al menos 8. "
                "Ajuste minlen>=8 en '/etc/pam.d/common-password'."
            )
        else:
            return f"Longitud de contraseña mínima = {longitud}, correcto para ENS Alto."
    m_fail = re.match(r"INTENTOS_FALLIDOS\s*=\s*([0-9]+).*esperado:\s*([0-9]+)", hallazgo)
    if m_fail:
        actual = m_fail.group(1)
        esperado = m_fail.group(2)
        if actual == "Nunca":
            return (
                "No se limita el número de intentos fallidos de contraseña, lo cual facilita ataques de fuerza bruta. "
                f"Se recomienda bloquear tras {esperado} intentos. Configure la política de bloqueo de cuenta."
            )
        return (
            f"El bloqueo de cuenta tras {actual} intentos está configurado, pero se recomienda un límite de {esperado}. "
            "Revise la configuración de políticas de bloqueo."
        )
    if "pam_pwquality" in hallazgo or "pam_cracklib" in hallazgo:
        return (
            "Se detectó que no se usan módulos de calidad (pam_pwquality o pam_cracklib), "
            "lo cual debilita la verificación de complejidad de contraseñas. "
            "Instale y configure pam_pwquality o pam_cracklib en '/etc/pam.d/common-password'."
        )
    return "Revisar hallazgo relacionado con OP.ACC.6."

# ---------------------------------------------------------------------------------
#  Función principal: detecta si es un informe Linux o Windows y extrae por secciones
# ---------------------------------------------------------------------------------

def parsear_informe_linux(filepath):
    """
    Lee el archivo de salida generado por auditor_linux.sh y devuelve un dict:
      {
        "Op.acc.1": [ { "hallazgo": "...", "recomendacion": "..." }, ... ],
        "Op.acc.2": [ ... ],
        "Op.acc.4": [ ... ],
        "Op.acc.5": [ ... ],
        "Op.acc.6": [ ... ]
      }
    Si en un control no hubo vulnerabilidades, la lista estará vacía.
    """
    # Inicializar estructura vacía
    resultado = {
        "Op.acc.1": [],
        "Op.acc.2": [],
        "Op.acc.4": [],
        "Op.acc.5": [],
        "Op.acc.6": []
    }

    # Leer todo el contenido
    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
        lineas = f.readlines()

    # Variables de control para saber en qué sección estamos
    seccion_actual = None

    for linea in lineas:
        linea = linea.strip()

        # Identificar comienzo de sección por etiquetas exactas:
        if linea.startswith("Control Op.acc.1"):
            seccion_actual = "Op.acc.1"
            continue
        if linea.startswith("Control Op.acc.2"):
            seccion_actual = "Op.acc.2"
            continue
        if linea.startswith("Control Op.acc.4"):
            seccion_actual = "Op.acc.4"
            continue
        if linea.startswith("Control Op.acc.5"):
            seccion_actual = "Op.acc.5"
            continue
        if linea.startswith("Control Op.acc.6"):
            seccion_actual = "Op.acc.6"
            continue

        # Si no estamos dentro de ninguna sección, saltar
        if seccion_actual is None:
            continue

        # Omite líneas vacías o las que NO contengan “UID duplicado:”, “Usuario con shell sospechosa:”,
        # “Usuario con privilegios sudo:”, “Usuario con privilegios elevados en grupo”, “Cuenta bloqueada:”,
        # “Usuario expirado:”, “PermitRootLogin...”, “PasswordAuthentication...”, “PubkeyAuthentication...”, “PASS_*”,
        # “pam_pwquality” o “minlen=...”. Estas contienen la información necesaria para diagnosticar
        if any([
            linea.startswith("UID duplicado:"),
            linea.startswith("Usuario con shell sospechosa:"),
            linea.startswith("Usuario con privilegios sudo:"),
            re.match(r"Usuario con privilegios elevados en grupo", linea),
            linea.startswith("Cuenta bloqueada:"),
            linea.startswith("Usuario expirado:"),
            "PermitRootLogin" in linea,
            "PasswordAuthentication" in linea,
            "PubkeyAuthentication" in linea,
            "PASS_MAX_DAYS" in linea,
            "PASS_MIN_DAYS" in linea,
            "minlen=" in linea,
            "pam_pwquality" in linea or "pam_cracklib" in linea,
            "INTENTOS_FALLIDOS" in linea
        ]):
            # Construir dict con hallazgo y recomendación
            if seccion_actual == "Op.acc.1":
                rec = recomendacion_op_acc_1(linea)
            elif seccion_actual == "Op.acc.2":
                rec = recomendacion_op_acc_2(linea)
            elif seccion_actual == "Op.acc.4":
                rec = recomendacion_op_acc_4(linea)
            elif seccion_actual == "Op.acc.5":
                rec = recomendacion_op_acc_5(linea)
            elif seccion_actual == "Op.acc.6":
                rec = recomendacion_op_acc_6(linea)
            else:
                rec = "Revisar hallazgo."

            resultado[seccion_actual].append({
                "hallazgo": linea,
                "recomendacion": rec
            })

    return resultado


def parsear_informe_windows(filepath):
    """
    Similar a la versión Linux, pero adaptado al formato PowerShell transcript.
    Extrae:
      - OP.ACC.1: 'Usuario con contraseña que nunca expira: X'
      - OP.ACC.2: 'Usuario con privilegios en grupo Administradores – X', etc.
      - OP.ACC.4: 'Cuenta deshabilitada: X'
                 'No hay cuentas locales expiradas.' -> no hay hallazgos
      - OP.ACC.5: 'RDP está deshabilitado' o 'Verificar si el acceso remoto (RDP) está habilitado'
      - OP.ACC.6: 'PASS_MAX_DAYS = 42 (esperado: 90)', 'PASS_MIN_DAYS = 0 (esperado: 1)',
                 'INTENTOS_FALLIDOS = Nunca (esperado: 5)'
    Devuelve el mismo dict de controles con lista de hallazgos + recomendación.
    """
    resultado = {
        "Op.acc.1": [],
        "Op.acc.2": [],
        "Op.acc.4": [],
        "Op.acc.5": [],
        "Op.acc.6": []
    }

    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
        lineas = f.readlines()

    seccion_actual = None

    for linea in lineas:
        linea = linea.strip()

        # Del transcript de PowerShell, buscamos los encabezados exactos:
        if linea.startswith("Control Op.acc.1"):
            seccion_actual = "Op.acc.1"
            continue
        if linea.startswith("Control Op.acc.2"):
            seccion_actual = "Op.acc.2"
            continue
        if linea.startswith("Control Op.acc.4"):
            seccion_actual = "Op.acc.4"
            continue
        if linea.startswith("Control Op.acc.5"):
            seccion_actual = "Op.acc.5"
            continue
        if linea.startswith("Control Op.acc.6"):
            seccion_actual = "Op.acc.6"
            continue

        if seccion_actual is None:
            continue

        # Distinguir hallazgos por sección:
        if seccion_actual == "Op.acc.1":
            # Ejemplo: "Usuario con contraseña que nunca expira: dagon"
            if linea.startswith("Usuario con contraseña que nunca expira:"):
                rec = (
                    "El usuario tiene la contraseña configurada para nunca expirar. "
                    "Recomendación: Ajuste la expiración de contraseña para cumplir 'PASS_MAX_DAYS'."
                )
                resultado["Op.acc.1"].append({"hallazgo": linea, "recomendacion": rec})
            continue

        if seccion_actual == "Op.acc.2":
            # Ejemplo: "Usuario con privilegios en grupo Administradores – LAPTOP-IOLVTGOJ\Administrador"
            if linea.startswith("Usuario con privilegios"):
                rec = recomendacion_op_acc_2(linea)
                resultado["Op.acc.2"].append({"hallazgo": linea, "recomendacion": rec})
            # Pueden aparecer errores de PowerShell sobre grupos inexistentes: ignoramos
            continue

        if seccion_actual == "Op.acc.4":
            # Ejemplo: "Cuenta deshabilitada: Administrador"
            if linea.startswith("Cuenta deshabilitada:"):
                rec = recomendacion_op_acc_4(linea)
                resultado["Op.acc.4"].append({"hallazgo": linea, "recomendacion": rec})
            # "No hay cuentas locales expiradas." -> no se añade a la lista (cumple)
            continue

        if seccion_actual == "Op.acc.5":
            # Ejemplo: "RDP está deshabilitado (bloqueando acceso remoto)."
            if "RDP" in linea:
                rec = recomendacion_op_acc_5(linea)
                resultado["Op.acc.5"].append({"hallazgo": linea, "recomendacion": rec})
            continue

        if seccion_actual == "Op.acc.6":
            # Ejemplos: "PASS_MAX_DAYS = 42 (esperado: 90)"
            #           "PASS_MIN_DAYS = 0 (esperado: 1)"
            #           "INTENTOS_FALLIDOS = Nunca (esperado: 5)"
            if any([
                linea.startswith("PASS_MAX_DAYS"),
                linea.startswith("PASS_MIN_DAYS"),
                linea.startswith("INTENTOS_FALLIDOS")
            ]):
                rec = recomendacion_op_acc_6(linea)
                resultado["Op.acc.6"].append({"hallazgo": linea, "recomendacion": rec})
            continue

    return resultado


def parsear_informe(filepath):
    """
    Detecta por contenido si el informe es de Linux (busca 'Control Op.acc.1 – Identificación')
    o de Windows (busca 'PowerShell transcript' o patrones típicos). Llama a la función 
    correspondiente y devuelve el resultado.
    """
    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
        primeras_lineas = "".join([next(f) for _ in range(10)])  # leemos las primeras 10 líneas

    # Si aparece 'PowerShell transcript' es Windows
    if "PowerShell transcript" in primeras_lineas:
        return parsear_informe_windows(filepath)
    # Si aparece 'Control Op.acc.1' con formato de texto bruto, es Linux
    if "Control Op.acc.1" in primeras_lineas:
        return parsear_informe_linux(filepath)
    # Si no reconocemos, devolvemos dict vacío (todos vacíos significa: no se pudo parsear)
    return {
        "Op.acc.1": [],
        "Op.acc.2": [],
        "Op.acc.4": [],
        "Op.acc.5": [],
        "Op.acc.6": []
    }
