#!/usr/bin/env python3
"""
RedMonitor Scanner v3.2
Escanea la red local: detecta equipos, tipos, velocidades, VLANs,
servidores de archivos improvisados y routers residenciales en red empresarial.
Requiere: Python 3.8+, nmap instalado con "Add to PATH".
Ejecutar como Administrador para mejores resultados.
"""
import subprocess
import sys
import json
import re
import socket
import platform
import ipaddress
from datetime import datetime
from xml.etree import ElementTree as ET

API_URL = "https://redmonitor.emprendistore.com/api/v1/scan"

try:
    import requests
except ImportError:
    print("  Instalando modulo 'requests'...")
    subprocess.run([sys.executable, "-m", "pip", "install", "requests", "-q"], check=True)
    import requests


# ── Tablas de clasificacion ────────────────────────────────

VENDOR_RULES = [
    # Equipos gestionados / empresariales
    (["cisco", "juniper", "extreme network"],               "switch_router",     "Switch/Router Empresarial"),
    (["mikrotik"],                                           "switch_router",     "Router MikroTik"),
    (["ubiquiti", "ruckus", "cambium", "aruba"],            "router_ap",         "AP/Router Empresarial"),
    (["tp-link", "tplink", "netgear", "asus", "linksys"],   "router_ap",         "Router/AP"),
    (["d-link"],                                             "router_ap",         "Router D-Link"),
    # Routers residenciales — alerta si están en red empresarial
    (["mercusys"],                                           "residential_router","Router Residencial (Mercusys)"),
    (["tenda"],                                              "residential_router","Router Residencial (Tenda)"),
    (["huawei"],                                             "residential_router","Router ISP/Residencial (Huawei)"),
    (["zte"],                                                "residential_router","Router ISP/Residencial (ZTE)"),
    # Impresoras
    (["brother", "canon", "epson", "xerox", "lexmark",
      "ricoh", "konica", "sharp", "kyocera"],                "printer",           "Impresora"),
    # PCs y laptops
    (["dell"],                                               "pc",                "PC/Servidor Dell"),
    (["lenovo"],                                             "pc",                "PC/Laptop Lenovo"),
    (["acer"],                                               "pc",                "PC/Laptop Acer"),
    (["intel corp", "intel(r)"],                             "pc",                "PC (Intel NIC)"),
    (["realtek"],                                            "pc",                "PC (Realtek NIC)"),
    # Celulares y tablets
    (["samsung"],                                            "mobile",            "Celular/Tablet Samsung"),
    (["xiaomi", "redmi", "poco"],                            "mobile",            "Celular Xiaomi"),
    (["motorola", "motorola mobility"],                      "mobile",            "Celular Motorola"),
    (["oneplus"],                                            "mobile",            "Celular OnePlus"),
    (["hmd global", "hmd mobile"],                           "mobile",            "Celular Nokia"),
    (["oppo"],                                               "mobile",            "Celular OPPO"),
    (["vivo mobile"],                                        "mobile",            "Celular Vivo"),
    (["realme"],                                             "mobile",            "Celular Realme"),
    (["lg electronics"],                                     "mobile",            "Celular/TV LG"),
    (["sony mobile", "sony interactive"],                    "mobile",            "Celular Sony"),
    (["wiko"],                                               "mobile",            "Celular Wiko"),
    # Otros
    (["azurewave", "espressif", "raspberry", "arduino"],     "iot",               "Dispositivo IoT"),
    (["apple"],                                              "apple",             "Apple (Mac/iPhone/iPad)"),
    (["vmware", "virtualbox", "parallels"],                  "virtual",           "Maquina Virtual"),
]

# Palabras clave de fabricantes residenciales (para is_residential_router)
RESIDENTIAL_VENDOR_KEYWORDS = ["mercusys", "tenda", "huawei", "zte"]

PORT_RULES = [
    ([9100, 515, 631],   "printer",           "Impresora"),
    ([3389, 5985],       "pc_win",            "PC Windows"),
    ([22],               "server",            "Servidor Linux"),
    ([8080, 8443],       "router_ap",         "Router/AP"),
    ([23],               "managed",           "Dispositivo Administrable (Telnet)"),
]

# SMB — puertos que identifican un servidor de archivos improvisado
SMB_PORTS = {139, 445}

DEVICE_ICONS = {
    "switch_router":     "🌐",
    "router_ap":         "📡",
    "residential_router":"⚠️",
    "printer":           "🖨",
    "pc":                "💻",
    "pc_win":            "💻",
    "file_server":       "🗄",
    "server":            "🖥",
    "iot":               "🔌",
    "apple":             "🍎",
    "mobile":            "📱",
    "virtual":           "🖥",
    "managed":           "🔧",
    "unknown":           "❓",
}


# ── Clasificacion de dispositivos ──────────────────────────

def classify_device(vendor="", open_ports=None, hostname=""):
    if open_ports is None:
        open_ports = []
    vendor_l   = vendor.lower()
    hostname_l = hostname.lower()

    # HP puede ser impresora o PC — decidir por puertos
    if any(x in vendor_l for x in ["hewlett", "hp inc", "hewlett packard"]):
        if any(p in open_ports for p in [9100, 515, 631]):
            return "printer", "Impresora HP"
        return "pc", "PC/Laptop HP"

    for keywords, dtype, label in VENDOR_RULES:
        if any(kw in vendor_l for kw in keywords):
            # Residential router ya tiene su tipo correcto
            if dtype == "residential_router":
                return dtype, label
            # Override: PC con puertos SMB abiertos = servidor de archivos improvisado
            if dtype in ("pc", "pc_win") and SMB_PORTS.intersection(open_ports):
                return "file_server", "Servidor de Archivos (PC compartida)"
            return dtype, label

    for ports, dtype, label in PORT_RULES:
        if any(p in open_ports for p in ports):
            return dtype, label

    # Override final: cualquier dispositivo con SMB abierto que no sea switch/router
    if SMB_PORTS.intersection(open_ports):
        return "file_server", "Servidor de Archivos (PC compartida)"

    # Por hostname
    for kw in ["printer", "print", "mfp", "copier"]:
        if kw in hostname_l:
            return "printer", "Impresora"
    for kw in ["router", "gateway", "gw", "fw", "firewall", "ap-"]:
        if kw in hostname_l:
            return "router_ap", "Router/Firewall"
    for kw in ["server", "srv", "nas", "dc-", "dc1"]:
        if kw in hostname_l:
            return "server", "Servidor"

    return "unknown", "Desconocido"


def is_residential_router(vendor, device_type):
    vendor_l = vendor.lower()
    if device_type == "residential_router":
        return True
    if device_type in ("router_ap", "switch_router"):
        return any(kw in vendor_l for kw in RESIDENTIAL_VENDOR_KEYWORDS)
    return False


# ── Velocidades ────────────────────────────────────────────

def parse_speed_bps(s):
    s = str(s).lower().strip()
    m = re.match(r"(\d+\.?\d*)\s*(g|m|k)?bps?", s)
    if not m:
        return 0
    val = float(m.group(1))
    unit = m.group(2) or ""
    return int(val * {"g": 1e9, "m": 1e6, "k": 1e3}.get(unit, 1))


def classify_speed(bps):
    if bps >= 1_000_000_000:
        return "gigabit", "1 Gbps"
    if bps >= 100_000_000:
        return "100m", "100 Mbps"
    if bps >= 10_000_000:
        return "10m", "10 Mbps"
    if bps > 0:
        return "other", f"{bps // 1_000_000} Mbps"
    return "unknown", "Desconocido"


# ── NIC propio ─────────────────────────────────────────────

def get_own_nics():
    nics = []
    if platform.system() != "Windows":
        return nics
    try:
        cmd = [
            "powershell", "-NoProfile", "-Command",
            "Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | "
            "Select-Object Name, LinkSpeed, MacAddress | ConvertTo-Json -Compress"
        ]
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        data = json.loads(r.stdout.strip())
        if isinstance(data, dict):
            data = [data]
        for a in data:
            spd = parse_speed_bps(a.get("LinkSpeed", "0"))
            sc, sl = classify_speed(spd)
            nics.append({
                "name":        a.get("Name", ""),
                "mac":         a.get("MacAddress", "").replace("-", ":"),
                "speed_bps":   spd,
                "speed_label": sl,
                "speed_class": sc,
            })
    except Exception:
        pass
    return nics


# ── Traceroute ─────────────────────────────────────────────

def run_traceroute(target="8.8.8.8"):
    """Traza la ruta hacia internet y extrae saltos con latencias."""
    hops = []
    if platform.system() == "Windows":
        cmd = ["tracert", "-d", "-h", "15", "-w", "1000", target]
    else:
        cmd = ["traceroute", "-n", "-m", "15", "-w", "2", target]
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=90, errors="replace")
        for line in r.stdout.splitlines():
            m = re.match(r'^\s*(\d+)', line)
            if not m:
                continue
            hop_num = int(m.group(1))
            # IP al final de la linea
            m_ip = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*$', line)
            ip = m_ip.group(1) if m_ip else "*"
            # Primera latencia (<1 ms o N ms)
            m_lat = re.search(r'[<]?(\d+)\s+ms', line)
            latency = float(m_lat.group(1)) if m_lat else None
            hops.append({"hop": hop_num, "ip": ip, "latency_ms": latency})
    except Exception:
        pass

    first_ip  = hops[0]["ip"] if hops else None
    first_priv = False
    if first_ip and first_ip != "*":
        try:
            first_priv = ipaddress.ip_address(first_ip).is_private
        except Exception:
            pass

    return {
        "target":              target,
        "hops":                hops,
        "total_hops":          len(hops),
        "first_hop_ip":        first_ip,
        "first_hop_private":   first_priv,
    }


# ── nmap helpers ───────────────────────────────────────────

def run_nmap(args, timeout=180):
    cmd = ["nmap", "-oX", "-"] + args
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return r.stdout
    except subprocess.TimeoutExpired:
        print("  [!] Timeout — resultados parciales.")
        return None
    except FileNotFoundError:
        print("\n  [ERROR] nmap no encontrado. Instalar desde https://nmap.org")
        sys.exit(1)


def get_elem(table, key):
    for el in table.findall("elem"):
        if el.get("key") == key:
            return (el.text or "").strip()
    return ""


# ── Parsers nmap XML ───────────────────────────────────────

def parse_arp_scan(xml_str):
    devices = []
    try:
        root = ET.fromstring(xml_str)
    except ET.ParseError:
        return devices

    for host in root.findall("host"):
        if host.find("status").get("state") != "up":
            continue
        ip = mac = vendor = hostname = None
        for addr in host.findall("address"):
            if addr.get("addrtype") == "ipv4":
                ip = addr.get("addr")
            elif addr.get("addrtype") == "mac":
                mac = addr.get("addr")
                vendor = addr.get("vendor", "")
        hn_el = host.find("hostnames")
        if hn_el is not None:
            first = hn_el.find("hostname")
            if first is not None:
                hostname = first.get("name", "")
        if ip:
            dtype, dlabel = classify_device(vendor or "", [], hostname or "")
            devices.append({
                "ip":                   ip,
                "mac":                  mac or "N/A",
                "vendor":               vendor or "Desconocido",
                "hostname":             hostname or "",
                "device_type":          dtype,
                "device_label":         dlabel,
                "open_ports":           [],
                "interfaces":           [],
                "vlans":                [],
                "snmp":                 False,
                "speed_class":          "unknown",
                "speed_label":          "Desconocido",
                "speed_bps":            0,
                "is_residential_router": is_residential_router(vendor or "", dtype),
                "has_smb":              False,
            })
    return devices


def parse_port_scan(xml_str, devices_by_ip):
    try:
        root = ET.fromstring(xml_str)
    except ET.ParseError:
        return
    for host in root.findall("host"):
        ip = None
        for addr in host.findall("address"):
            if addr.get("addrtype") == "ipv4":
                ip = addr.get("addr")
                break
        if not ip or ip not in devices_by_ip:
            continue
        dev = devices_by_ip[ip]
        ports_el = host.find("ports")
        if ports_el is None:
            continue
        open_ports = []
        for port in ports_el.findall("port"):
            state = port.find("state")
            if state is not None and state.get("state") == "open":
                try:
                    open_ports.append(int(port.get("portid", 0)))
                except ValueError:
                    pass
        dev["open_ports"] = open_ports
        dev["has_smb"]    = bool(SMB_PORTS.intersection(open_ports))
        # Reclasificar con informacion de puertos
        dtype, dlabel = classify_device(dev["vendor"], open_ports, dev["hostname"])
        dev["device_type"]           = dtype
        dev["device_label"]          = dlabel
        dev["is_residential_router"] = is_residential_router(dev["vendor"], dtype)


def parse_snmp_scan(xml_str, devices_by_ip):
    try:
        root = ET.fromstring(xml_str)
    except ET.ParseError:
        return
    for host in root.findall("host"):
        ip = None
        for addr in host.findall("address"):
            if addr.get("addrtype") == "ipv4":
                ip = addr.get("addr")
                break
        if not ip or ip not in devices_by_ip:
            continue
        dev = devices_by_ip[ip]
        ports_el = host.find("ports")
        if ports_el is None:
            continue
        for port in ports_el.findall("port"):
            for script in port.findall("script"):
                sid    = script.get("id", "")
                output = script.get("output", "")

                if sid == "snmp-interfaces":
                    dev["snmp"] = True
                    ifaces = []
                    outer = script.find("table")
                    if outer is not None:
                        for iface_tbl in outer.findall("table"):
                            name      = iface_tbl.get("key", "if")
                            speed_str = get_elem(iface_tbl, "Speed")
                            itype     = get_elem(iface_tbl, "Type")
                            status    = get_elem(iface_tbl, "Status")
                            spd_bps   = parse_speed_bps(speed_str)
                            ifaces.append({
                                "name":        name,
                                "speed_label": speed_str or "N/A",
                                "speed_bps":   spd_bps,
                                "type":        itype,
                                "status":      status,
                            })
                    if not ifaces:
                        ifaces = _parse_interfaces_text(output)

                    dev["interfaces"] = ifaces
                    eth = [i["speed_bps"] for i in ifaces
                           if "ethernet" in i.get("type", "").lower() and i["speed_bps"] > 0]
                    if not eth:
                        eth = [i["speed_bps"] for i in ifaces if i["speed_bps"] > 0]
                    if eth:
                        bps = max(eth)
                        dev["speed_bps"]   = bps
                        dev["speed_class"], dev["speed_label"] = classify_speed(bps)

                elif sid == "snmp-vlans":
                    dev["snmp"] = True
                    vlans = []
                    outer = script.find("table")
                    if outer is not None:
                        for vtbl in outer.findall("table"):
                            vid   = get_elem(vtbl, "vlanId") or vtbl.get("key", "")
                            vname = get_elem(vtbl, "vlanName")
                            try:
                                vlans.append({"id": int(vid), "name": vname})
                            except ValueError:
                                pass
                    if not vlans:
                        vlans = _parse_vlans_text(output)
                    dev["vlans"] = vlans


def _parse_interfaces_text(text):
    ifaces, cur = [], None
    for raw in text.split("\n"):
        line = raw.strip()
        if not line:
            if cur:
                ifaces.append(cur)
                cur = None
            continue
        if not raw.startswith("  ") and not raw.startswith("\t"):
            if cur:
                ifaces.append(cur)
            cur = {"name": line, "speed_bps": 0, "speed_label": "N/A", "type": "", "status": ""}
            continue
        if cur is None:
            cur = {"name": "", "speed_bps": 0, "speed_label": "N/A", "type": "", "status": ""}
        if "Speed:" in line:
            v = line.split("Speed:", 1)[1].strip()
            cur["speed_label"] = v
            cur["speed_bps"]   = parse_speed_bps(v)
        elif "Type:" in line:
            cur["type"] = line.split("Type:", 1)[1].strip()
        elif "Status:" in line:
            cur["status"] = line.split("Status:", 1)[1].strip()
    if cur:
        ifaces.append(cur)
    return ifaces


def _parse_vlans_text(text):
    vlans, seen = [], set()
    for line in text.split("\n"):
        m = re.search(r"VLAN\s+(\d+)[:\s]+(.+)", line, re.IGNORECASE)
        if m:
            vid = int(m.group(1))
            if vid not in seen:
                vlans.append({"id": vid, "name": m.group(2).strip()})
                seen.add(vid)
    return vlans


# ── Subredes ───────────────────────────────────────────────

def build_subnets(devices):
    subnets = {}
    for dev in devices:
        parts = dev["ip"].split(".")
        key = f"{parts[0]}.{parts[1]}.{parts[2]}.0/24"
        if key not in subnets:
            subnets[key] = []
        subnets[key].append(dev["ip"])
    return [{"subnet": k, "ips": v, "count": len(v)} for k, v in sorted(subnets.items())]


# ── Main ───────────────────────────────────────────────────

def main():
    print()
    print("=" * 58)
    print("  RedMonitor Scanner v3.2")
    print("  Ejecutar como Administrador para resultados completos")
    print("=" * 58)
    print()

    # Nombre del cliente
    nombre_cliente = input("  Nombre del cliente (empresa a diagnosticar): ").strip()
    if not nombre_cliente:
        nombre_cliente = "Sin cliente"

    print()

    # IP propia
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        my_ip = s.getsockname()[0]
    finally:
        s.close()
    parts = my_ip.split(".")
    auto_range = f"{parts[0]}.{parts[1]}.{parts[2]}.0/24"

    # NICs propios
    own_nics = get_own_nics()
    print("  Adaptadores de red detectados:")
    if own_nics:
        for n in own_nics:
            print(f"    {n['name']:<20} {n['speed_label']:<12}  {n['mac']}")
    else:
        print("    (No disponible en este sistema)")

    print()
    print(f"  Cliente        : {nombre_cliente}")
    print(f"  IP del scanner : {my_ip}")
    print(f"  Rango sugerido : {auto_range}")
    print()

    rango = input(f"  Rango a escanear [{auto_range}]: ").strip() or auto_range

    print()
    print(f"[1/5] Descubriendo dispositivos en {rango}...")
    xml_arp = run_nmap(["-sn", rango], timeout=120)
    if not xml_arp:
        print("  [ERROR] ARP scan fallo.")
        input("\n  Enter para salir...")
        sys.exit(1)

    devices = parse_arp_scan(xml_arp)
    print(f"      {len(devices)} dispositivos encontrados.")

    print()
    print("[2/5] Identificando tipos de dispositivos (puertos)...")
    # Puertos: servicios comunes + SMB (139, 445) para detectar servidores de archivos improvisados
    xml_ports = run_nmap(
        ["-p", "22,23,80,139,443,445,3389,5985,8080,8443,9100", "--open", rango],
        timeout=120,
    )
    if xml_ports:
        dmap = {d["ip"]: d for d in devices}
        parse_port_scan(xml_ports, dmap)
    print("      Hecho.")

    print()
    print("[3/5] Escaneando velocidades SNMP e interfaces...")
    print("      (Puede tardar 3-8 minutos. Aguarda...)")
    xml_snmp = run_nmap(
        ["-sU", "-p", "161", "--open", "--script", "snmp-interfaces,snmp-vlans", rango],
        timeout=480,
    )
    if xml_snmp:
        dmap = {d["ip"]: d for d in devices}
        parse_snmp_scan(xml_snmp, dmap)

    print()
    print("[4/5] Trazando ruta hacia internet (8.8.8.8)...")
    print("      (Puede tardar hasta 60 segundos. Aguarda...)")
    traceroute_data = run_traceroute("8.8.8.8")
    if traceroute_data["hops"]:
        print(f"      {traceroute_data['total_hops']} saltos detectados:")
        for h in traceroute_data["hops"][:8]:
            lat_str = f"{int(h['latency_ms'])} ms" if h.get("latency_ms") is not None else "*"
            print(f"      Salto {h['hop']:2d}: {h['ip']:<20} {lat_str}")
        if traceroute_data["total_hops"] > 8:
            print(f"      ... y {traceroute_data['total_hops'] - 8} saltos mas")
    else:
        print("      No se pudo trazar la ruta.")

    # VLANs globales
    all_vlans: dict[int, dict] = {}
    for dev in devices:
        for vlan in dev.get("vlans", []):
            vid = vlan["id"]
            if vid not in all_vlans:
                all_vlans[vid] = {"id": vid, "name": vlan.get("name", ""), "found_on": []}
            all_vlans[vid]["found_on"].append(dev["ip"])

    # Contadores
    gigabit     = sum(1 for d in devices if d["speed_class"] == "gigabit")
    fast        = sum(1 for d in devices if d["speed_class"] == "100m")
    slow        = sum(1 for d in devices if d["speed_class"] == "10m")
    unknown     = sum(1 for d in devices if d["speed_class"] == "unknown")
    file_srvs   = sum(1 for d in devices if d["device_type"] == "file_server")
    res_routers = sum(1 for d in devices if d.get("is_residential_router"))

    subnets = build_subnets(devices)

    payload = {
        "timestamp":        datetime.now().isoformat(),
        "network":          rango,
        "scanner_ip":       my_ip,
        "nombre_cliente":   nombre_cliente,
        "own_nics":         own_nics,
        "total":            len(devices),
        "gigabit":          gigabit,
        "fast_ethernet":    fast,
        "slow_ethernet":    slow,
        "unknown":          unknown,
        "file_servers":     file_srvs,
        "residential_routers": res_routers,
        "subnets":          subnets,
        "devices":          devices,
        "vlans":            list(all_vlans.values()),
        "traceroute":       traceroute_data,
    }

    print()
    print("[5/5] Enviando resultados al dashboard...")
    try:
        r = requests.post(API_URL, json=payload, timeout=20)
        if r.status_code == 200:
            data = r.json()
            score = data.get("score", "N/A")
            print(f"  Enviado.  Scan ID: {data.get('scan_id','?')}  |  Puntaje: {score}/100")
            print("  Dashboard -> https://redmonitor.emprendistore.com")
        else:
            print(f"  [!] Error HTTP {r.status_code}: {r.text[:200]}")
    except Exception as e:
        print(f"  [!] Sin conexion: {e}")

    # Resumen
    print()
    print("=" * 58)
    print("  RESUMEN")
    print("=" * 58)
    print(f"  Total dispositivos      : {len(devices)}")
    print(f"  Gigabit (1 Gbps)        : {gigabit}")
    print(f"  Fast (100 Mbps)         : {fast}")
    print(f"  Slow (10 Mbps)          : {slow}")
    print(f"  Sin SNMP                : {unknown}")
    print(f"  Redes diferentes        : {len(subnets)}")
    print(f"  VLANs detectadas        : {len(all_vlans)}")
    if file_srvs:
        print(f"  *** Serv. archivos PC   : {file_srvs}  <-- REVISAR")
    if res_routers:
        print(f"  *** Routers residenciales: {res_routers}  <-- REVISAR")

    if len(subnets) > 1:
        print()
        print("  Redes encontradas:")
        for sn in subnets:
            print(f"    {sn['subnet']:<20} {sn['count']} equipos")

    print()
    print("  Dispositivos:")
    for dev in sorted(devices, key=lambda d: [int(x) for x in d["ip"].split(".")]):
        icon = DEVICE_ICONS.get(dev["device_type"], "?")
        flags = ""
        if dev.get("has_smb"):
            flags += " [SMB]"
        if dev.get("is_residential_router"):
            flags += " [RESIDENCIAL]"
        print(f"    {icon} {dev['ip']:<16} {dev['speed_label']:<12} {dev['device_label']:<28}{flags}")

    print()
    input("  Presiona Enter para salir...")


if __name__ == "__main__":
    main()
