#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
=============================================================================
PortScanner - Escáner de Puertos TCP de Alta Velocidad
Mentes Consultores • Diagnóstico de Red, Servidores y ERPs
=============================================================================
Permite escanear puertos abiertos en cualquier dirección IP o dominio
tanto en redes locales (LAN: 192.168.x.x, 10.x.x.x) como en Internet.
"""

import sys
import os
import socket
import time
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed

# Habilitar soporte de colores ANSI en Windows cmd / PowerShell
if os.name == "nt":
    os.system("")

# Diccionario de servicios conocidos
SERVICIOS_COMUNES = {
    21: "FTP (Transferencia de archivos)",
    22: "SSH (Acceso remoto seguro)",
    23: "Telnet (Acceso remoto no seguro)",
    25: "SMTP (Servidor de correo saliente)",
    53: "DNS (Servidor de nombres de dominio)",
    80: "HTTP (Servidor Web)",
    110: "POP3 (Recepción de correo)",
    135: "RPC (Microsoft EPMAP)",
    139: "NetBIOS (Redes Windows)",
    143: "IMAP (Sincronización de correo)",
    443: "HTTPS (Servidor Web Seguro SSL/TLS)",
    445: "SMB / CIFS (Archivos compartidos)",
    465: "SMTPS (Envío de correo SSL)",
    587: "SMTP Submission (Envío autenticado)",
    993: "IMAPS (Correo seguro IMAP)",
    995: "POP3S (Correo seguro POP3)",
    1433: "MS SQL Server (Base de datos)",
    1521: "Oracle Database",
    2082: "cPanel (Panel de Hosting)",
    2083: "cPanel Seguro (SSL)",
    2086: "WHM (WebHost Manager)",
    2087: "WHM Seguro (SSL)",
    3000: "Node.js / React Web App",
    3306: "MySQL / MariaDB",
    3389: "RDP (Escritorio Remoto de Windows)",
    5000: "Flask / Docker Registry",
    5432: "PostgreSQL (ERPNext / MTSNext)",
    5900: "VNC (Control remoto gráfico)",
    6379: "Redis (Caché en memoria)",
    8000: "ERPNext / MTSNext / Web App",
    8069: "Odoo ERP / Global Blue Point Web",
    8080: "HTTP Alternativo / Tomcat / Proxy",
    8443: "HTTPS Alternativo / Plesk",
    8888: "Jupyter / Servicio Web",
    9000: "Portainer / SonarQube",
    27017: "MongoDB (Base de datos NoSQL)"
}

PRESETS = {
    "top": [21, 22, 25, 53, 80, 110, 143, 443, 465, 587, 993, 995, 1433, 1521, 3306, 3389, 5432, 6379, 8000, 8069, 8080, 8443],
    "web": [80, 443, 8000, 8069, 8080, 8443, 8888, 3000, 5000, 9000],
    "db": [1433, 1521, 3306, 5432, 6379, 8000, 8069, 27017],
    "remote": [22, 23, 3389, 5900, 21, 445, 2082, 2083, 2086, 2087],
    "mail": [25, 110, 143, 465, 587, 993, 995]
}


def probar_puerto(ip, puerto, timeout=1.2):
    """Verifica si un puerto TCP específico está abierto."""
    inicio = time.time()
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(timeout)
            resultado = s.connect_ex((ip, puerto))
            latencia_ms = int((time.time() - inicio) * 1000)
            if resultado == 0:
                return puerto, "ABIERTO", latencia_ms
            elif resultado in (10035, 10060, 110):  # Timeout / filtrado
                return puerto, "FILTRADO", None
            else:
                return puerto, "CERRADO", None
    except socket.timeout:
        return puerto, "FILTRADO", None
    except Exception:
        return puerto, "CERRADO", None


def parsear_puertos(cadena_puertos):
    """Convierte cadenas como '80,443,8000-8010' en una lista ordenada de enteros."""
    puertos = set()
    partes = cadena_puertos.split(",")
    for parte in partes:
        parte = parte.strip()
        if "-" in parte:
            try:
                ini, fin = map(int, parte.split("-"))
                for p in range(ini, fin + 1):
                    if 1 <= p <= 65535:
                        puertos.add(p)
            except ValueError:
                pass
        else:
            try:
                p = int(parte)
                if 1 <= p <= 65535:
                    puertos.add(p)
            except ValueError:
                pass
    return sorted(list(puertos))


def limpiar_host(host_str):
    """Limpia URLs accidentales (ej: http://, https://, barras finales, puertos)."""
    h = host_str.strip()
    if "://" in h:
        h = h.split("://")[1]
    h = h.split("/")[0]
    h = h.split("?")[0]
    if ":" in h and not h.startswith("["):
        h = h.split(":")[0]
    return h.strip()


def escanear_host(host, lista_puertos, max_threads=50, timeout=1.2):
    """Ejecuta el escaneo concurrente de puertos para el host dado."""
    host = limpiar_host(host)
    print("\n" + "=" * 72)
    print("  PortScanner • Mentes Consultores")
    print("=" * 72)
    print(f"[*] Destino objetivo: {host}")

    try:
        ip = socket.gethostbyname(host)
        print(f"[*] Dirección IP resuelta: {ip}")
    except socket.gaierror as e:
        print(f"\n[!] Error: No se pudo resolver la dirección o IP '{host}'.")
        print(f"    Detalle técnico: {e}")
        return False

    print(f"[*] Puertos a comprobar: {len(lista_puertos)} puertos")
    print(f"[*] Hilos concurrentes: {max_threads} | Timeout por puerto: {timeout}s")
    print("-" * 72)
    print(f"{'PUERTO':<12} {'ESTADO':<14} {'LATENCIA':<12} {'SERVICIO'}")
    print("-" * 72)

    abiertos = []
    cerrados_o_filtrados = 0
    t_inicio = time.time()

    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        futuros = {executor.submit(probar_puerto, ip, p, timeout): p for p in lista_puertos}
        for futuro in as_completed(futuros):
            puerto, estado, latencia = futuro.result()
            servicio = SERVICIOS_COMUNES.get(puerto, "Desconocido")
            if estado == "ABIERTO":
                abiertos.append((puerto, servicio, latencia))
                print(f"{puerto:<12} \033[92m{estado:<14}\033[0m {f'{latencia} ms':<12} {servicio}")
            else:
                cerrados_o_filtrados += 1

    t_total = time.time() - t_inicio
    print("-" * 72)
    print(f"[*] Tiempo total de escaneo: {t_total:.2f} segundos.")
    print(f"[*] Resumen: \033[92m{len(abiertos)} PUERTOS ABIERTOS\033[0m | {cerrados_o_filtrados} cerrados o protegidos por firewall.")

    if abiertos:
        print("\n\033[92m[+] DETALLE DE PUERTOS ABIERTOS EN " + host + ":\033[0m")
        for p, s, lat in sorted(abiertos, key=lambda x: x[0]):
            lat_str = f"({lat} ms)" if lat is not None else ""
            print(f"    ✓ Puerto {p:<6} -> {s} {lat_str}")

        # Alerta de seguridad si hay puertos sensibles expuestos
        riesgos = [p for p, _, _ in abiertos if p in (3389, 3306, 5432, 1433, 23, 21)]
        if riesgos:
            print("\n\033[93m[!] ADVERTENCIA DE SEGURIDAD:\033[0m")
            print(f"    Se detectaron puertos sensibles expuestos ({', '.join(map(str, riesgos))}).")
            print("    Si este equipo está en Internet público, proteja el acceso mediante VPN o listas blancas de IP.")
    else:
        print("\n[-] No se encontraron puertos abiertos en el rango seleccionado.")
        print("    (Los puertos están cerrados o un firewall descarta los paquetes).")

    print("=" * 72)
    return True


def pausa_final():
    """Mantiene la ventana abierta hasta que el usuario presione ENTER o aceptar."""
    try:
        input("\nPresione [ENTER] o escriba 'aceptar' para cerrar el programa: ")
    except (KeyboardInterrupt, EOFError):
        pass


def modo_interactivo():
    """Flujo interactivo con menú para cuando se ejecuta haciendo doble clic."""
    while True:
        print("\n" + "=" * 72)
        print("         PortScanner de Mentes Consultores (Modo Interactivo)")
        print("=" * 72)
        host = input("\nIngrese la dirección IP o dominio a escanear (ej: mentesconsultores.com.ar o 192.168.1.1)\n[O presione ENTER para salir]: ").strip()

        if not host:
            print("\nSaliendo del programa...")
            break

        print("\nSeleccione el perfil de puertos a escanear:")
        print("  1) Puertos más comunes & críticos (Top 22: Web, SSH, DB, Mail, RDP, ERP)")
        print("  2) Servidores Web & APIs (80, 443, 8000, 8069, 8080, 8443, 3000, 5000...)")
        print("  3) Bases de Datos & ERP (MySQL, PostgreSQL, SQL Server, ERPNext, Odoo)")
        print("  4) Acceso Remoto & Administración (SSH, RDP Windows 3389, VNC, cPanel)")
        print("  5) Servidores de Correo (SMTP, IMAP, POP3, SSL)")
        print("  6) Personalizado (ingresar puertos o rangos específicos, ej: 80,443,8000-8010)")

        opcion = input("\nElija una opción [1-6, por defecto 1]: ").strip()

        if opcion == "2":
            lista_puertos = PRESETS["web"]
        elif opcion == "3":
            lista_puertos = PRESETS["db"]
        elif opcion == "4":
            lista_puertos = PRESETS["remote"]
        elif opcion == "5":
            lista_puertos = PRESETS["mail"]
        elif opcion == "6":
            puertos_custom = input("Ingrese los puertos separados por coma o guión (ej: 80,443,3389,8000-8020): ").strip()
            lista_puertos = parsear_puertos(puertos_custom) if puertos_custom else PRESETS["top"]
        else:
            lista_puertos = PRESETS["top"]

        escanear_host(host, lista_puertos)

        continuar = input("\n¿Desea realizar otro escaneo de puertos? (S/N) [por defecto N para salir]: ").strip().lower()
        if continuar not in ("s", "si", "y", "yes"):
            break

    pausa_final()


def main():
    parser = argparse.ArgumentParser(description="PortScanner de Mentes Consultores")
    parser.add_argument("host", nargs="?", default="", help="Dirección IP o dominio a escanear (ej: mentesconsultores.com.ar o 192.168.1.1)")
    parser.add_argument("-p", "--ports", help="Puertos a escanear (ej: 80,443 o 8000-8020)")
    parser.add_argument("--preset", choices=["top", "web", "db", "remote", "mail"], default=None, help="Perfil de puertos predefinido")
    parser.add_argument("-t", "--timeout", type=float, default=1.2, help="Timeout en segundos por puerto (default: 1.2)")
    parser.add_argument("-w", "--workers", type=int, default=50, help="Cantidad de hilos concurrentes (default: 50)")
    parser.add_argument("--no-pause", action="store_true", help="No esperar confirmación al finalizar (para scripts automatizados)")

    args = parser.parse_args()

    # Si se ejecutó sin argumentos (por ejemplo, doble clic en Windows), iniciar modo interactivo
    if not args.host and not args.ports and not args.preset:
        try:
            modo_interactivo()
        except KeyboardInterrupt:
            print("\n\nOperación cancelada por el usuario.")
            pausa_final()
        return

    # Modo con argumentos por línea de comandos
    try:
        host = args.host
        if not host:
            host = input("Ingrese la dirección IP o dominio a escanear: ").strip()
            if not host:
                print("[!] Debe ingresar un host o dirección IP válido.")
                if not args.no_pause:
                    pausa_final()
                sys.exit(1)

        if args.ports:
            lista_puertos = parsear_puertos(args.ports)
        elif args.preset:
            lista_puertos = PRESETS.get(args.preset, PRESETS["top"])
        else:
            lista_puertos = PRESETS["top"]

        escanear_host(host, lista_puertos, max_threads=args.workers, timeout=args.timeout)

    except KeyboardInterrupt:
        print("\n\n[!] Escaneo interrumpido por el usuario.")
    except Exception as e:
        print(f"\n[!] Error inesperado: {e}")
    finally:
        if not args.no_pause:
            pausa_final()


if __name__ == "__main__":
    main()
