Files
fn_registry/python/functions/cybersecurity/traceroute_host.py
T
egutierrez 935008ec3f feat(recon): grupo de reconocimiento de red + servicios + fingerprint web
Añade el capability group `recon` (dominio cybersecurity + pipelines, Python),
con la política de archivado OSINT y página madre docs/capabilities/recon.md.

Lookups y sondeo (wrappers de CLI):
- whois_lookup, rdap_lookup, dns_records, ping_host, traceroute_host, nmap_scan
- save_scan_to_osint (sink común) + recon_osint (pipeline one-shot scan+archivado)

Escaneo de puertos/servicios nativo (stdlib, sin nmap ni sudo):
- scan_tcp_ports: connect-scan TCP concurrente (open/closed/filtered)
- grab_service_banner: banner grab + identificación de servicio/versión real
- identify_port_service: puro, puerto -> servicio IANA esperado (~120 puertos)
- scan_port_services: pipeline one-shot (scan -> identify + banner por puerto abierto)

Fingerprint de tecnología web (estilo Wappalyzer), patrón pura/impura:
- fetch_http_fingerprint: GET stdlib, recoge headers/html/cookies (solo nombres)
- detect_web_tech: puro, matchea ~50 firmas regex -> tecnologías por categoría
- fingerprint_web_stack: pipeline one-shot url -> tecnologías

Todas devuelven dict {status} sin lanzar. Tests: 43 verdes, sin red externa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:12:07 +02:00

144 lines
4.9 KiB
Python

"""Trazado de la ruta de red hacia un host via el binario `traceroute` (Linux).
Funcion IMPURA: ejecuta `traceroute -m <max_hops> -w 2 <host>` como subprocess
y parsea best-effort cada hop: numero de salto, hosts (nombre + IP) y los rtt
detectados. Un hop sin respuesta ("* * *") se representa con `hosts` vacio. No
busca un parseo perfecto: captura el numero de hop y las IPs por regex. Nunca
lanza; devuelve dict de estado con `raw` siempre presente.
"""
import re
import subprocess
# Inicio de una linea de hop: numero de salto al principio.
_HOP_LINE_RE = re.compile(r"^\s*(\d+)\s+(.*)$")
# IPv4 entre parentesis o suelta.
_IP_RE = re.compile(r"\b(\d{1,3}(?:\.\d{1,3}){3})\b")
# "nombre (ip)" o "ip"
_HOST_PAREN_RE = re.compile(r"([\w.\-]+)\s+\((\d{1,3}(?:\.\d{1,3}){3})\)")
# rtt en milisegundos, p.ej "1.234 ms"
_RTT_RE = re.compile(r"([\d.]+)\s*ms")
def _parse_hop(hop_num: int, rest: str) -> dict:
"""Parsea best-effort el cuerpo de una linea de hop."""
hosts: list[dict] = []
# Caso sin respuesta: solo asteriscos.
if rest.replace("*", "").strip() == "":
return {"hop": hop_num, "hosts": []}
# rtt globales de la linea (se asocian al/los host(s) detectados).
rtts = [float(x) for x in _RTT_RE.findall(rest)]
# Hosts con formato "nombre (ip)".
paren_matches = _HOST_PAREN_RE.findall(rest)
seen_ips: set[str] = set()
for name, ip in paren_matches:
seen_ips.add(ip)
hosts.append({"name": name, "ip": ip, "rtt_ms": rtts})
# IPs sueltas no capturadas por el patron "nombre (ip)".
for ip in _IP_RE.findall(rest):
if ip not in seen_ips:
seen_ips.add(ip)
hosts.append({"name": "", "ip": ip, "rtt_ms": rtts})
# Si no detectamos ningun host pero la linea tiene contenido (raro),
# dejamos un host placeholder con el texto en name para no perder info.
if not hosts and rest.strip() and rest.strip() != "*":
hosts.append({"name": rest.strip(), "ip": "", "rtt_ms": rtts})
return {"hop": hop_num, "hosts": hosts}
def traceroute_host(host: str, max_hops: int = 30, timeout_s: int = 60) -> dict:
"""Traza la ruta de red hacia un host y parsea los hops best-effort.
Args:
host: Hostname o IP destino (ej. ``"google.com"`` o ``"1.1.1.1"``).
max_hops: Maximo numero de saltos a sondear (`traceroute -m`).
timeout_s: Timeout duro del subprocess en segundos.
Returns:
Dict de estado. En exito::
{
"status": "ok",
"host": <host>,
"hops": [
{"hop": 1, "hosts": [{"name": str, "ip": str, "rtt_ms": [float, ...]}]},
{"hop": 2, "hosts": []}, # "* * *" sin respuesta
...
],
"raw": <stdout>,
}
En fallo (binario ausente, host vacio, timeout duro)::
{"status": "error", "error": <str>, "host": <host>, "raw": <stdout|"">}
"""
if not host or not host.strip():
return {"status": "error", "error": "traceroute_host: host vacio", "host": host, "raw": ""}
host = host.strip()
try:
proc = subprocess.run(
["traceroute", "-m", str(max_hops), "-w", "2", host],
capture_output=True,
text=True,
timeout=float(timeout_s),
)
except FileNotFoundError:
return {
"status": "error",
"error": "traceroute_host: binario `traceroute` no encontrado en PATH",
"host": host,
"raw": "",
}
except subprocess.TimeoutExpired as exc:
partial = exc.stdout or ""
if isinstance(partial, bytes):
partial = partial.decode(errors="replace")
return {
"status": "error",
"error": f"traceroute_host: timeout duro del subprocess tras {timeout_s}s",
"host": host,
"raw": partial,
}
raw = proc.stdout or ""
hops: list[dict] = []
for line in raw.splitlines():
m = _HOP_LINE_RE.match(line)
if not m:
continue # cabecera "traceroute to ..." u otras lineas no-hop
hop_num = int(m.group(1))
hops.append(_parse_hop(hop_num, m.group(2)))
return {
"status": "ok",
"host": host,
"hops": hops,
"raw": raw,
}
if __name__ == "__main__":
try:
result = traceroute_host("1.1.1.1", max_hops=15, timeout_s=40)
print(result["status"])
if result["status"] == "ok":
print(f"hops detectados: {len(result['hops'])}")
for hop in result["hops"][:5]:
ips = [h["ip"] for h in hop["hosts"] if h["ip"]]
print(f" hop {hop['hop']}: {ips or '* * *'}")
print("--- raw ---")
print(result["raw"])
else:
print("error:", result.get("error"))
except Exception as exc: # smoke: tolera cualquier fallo de red sin romper
print("smoke fallo (tolerado):", exc)