Files
fn_registry/python/functions/cybersecurity/whois_lookup.py
T
egutierrez eb8dbf66a1 feat(infra): auto-commit con 88 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 00:16:46 +02:00

115 lines
3.8 KiB
Python

"""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/<dominio>``
(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 <code>".
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,
}