"""Recoleccion OSINT pasiva de datos de registro de dominio via RDAP. Funcion IMPURA: consulta el servicio RDAP publico (reemplazo moderno de WHOIS, sobre HTTP/JSON) y normaliza la respuesta. Es OSINT pasivo: no toca al dominio objetivo, solo el directorio RDAP publico. """ import os import sys sys.path.insert( 0, os.path.join(os.path.dirname(__file__), "..", "..", "functions") ) from infra.http_get_json import http_get_json # noqa: E402 def _events_by_action(raw: dict) -> dict: """Indexa la lista RDAP ``events`` por ``eventAction`` -> ``eventDate``.""" out: dict = {} for event in raw.get("events", []) or []: action = event.get("eventAction") date = event.get("eventDate") if action and date: out[action] = date return out def _extract_registrar(raw: dict) -> str | None: """Busca la entidad con rol ``registrar`` y devuelve su nombre vCard.""" for entity in raw.get("entities", []) or []: roles = entity.get("roles", []) or [] if "registrar" not in roles: continue vcard = entity.get("vcardArray") if isinstance(vcard, list) and len(vcard) == 2: for field in vcard[1]: if isinstance(field, list) and field and field[0] == "fn": return field[3] return entity.get("handle") return None def _extract_nameservers(raw: dict) -> list: """Extrae los ldhName de los nameservers RDAP, ordenados.""" servers = [] for ns in raw.get("nameservers", []) or []: name = ns.get("ldhName") if name: servers.append(name.lower()) return sorted(set(servers)) def whois_lookup(dominio: str, timeout_s: float = 15.0) -> dict: """Consulta RDAP de un dominio y normaliza la informacion de registro. Usa ``http_get_json`` del registry contra ``https://rdap.org/domain/`` (rdap.org redirige al servidor RDAP autoritativo del TLD). Normaliza registrar, fechas (creacion / expiracion / ultimo cambio), nameservers, estados y entidades, e incluye la respuesta cruda en ``raw``. Args: dominio: Dominio a consultar (ej. ``"organic-machine.com"``). timeout_s: Segundos maximo de espera de la peticion HTTP (default 15). Returns: Dict normalizado con claves: ``found`` (bool), ``registrar``, ``creation_date``, ``expiration_date``, ``last_changed``, ``nameservers`` (lista), ``status`` (lista), ``entities`` (lista de roles/handles) y ``raw`` (respuesta RDAP completa). Si el dominio no existe (HTTP 404) devuelve ``{"found": False}``. Raises: RuntimeError: Si el dominio esta vacio o la peticion falla por una razon distinta de 404. """ if not dominio or not dominio.strip(): raise RuntimeError("whois_lookup: dominio vacio") url = f"https://rdap.org/domain/{dominio.strip()}" try: raw = http_get_json(url, timeout=timeout_s) except RuntimeError as e: # http_get_json envuelve los HTTPError como "HTTP ". if "HTTP 404" in str(e): return {"found": False} raise if not isinstance(raw, dict): raise RuntimeError( f"whois_lookup: respuesta RDAP inesperada (tipo {type(raw).__name__})" ) events = _events_by_action(raw) entities = [ { "handle": ent.get("handle"), "roles": ent.get("roles", []) or [], } for ent in raw.get("entities", []) or [] ] return { "found": True, "registrar": _extract_registrar(raw), "creation_date": events.get("registration"), "expiration_date": events.get("expiration"), "last_changed": events.get("last changed"), "nameservers": _extract_nameservers(raw), "status": raw.get("status", []) or [], "entities": entities, "raw": raw, }