eb8dbf66a1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
3.8 KiB
Python
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,
|
|
}
|