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>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user