935008ec3f
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>
131 lines
4.2 KiB
Python
131 lines
4.2 KiB
Python
"""Sondeo de disponibilidad ICMP de un host via el binario `ping` (Linux).
|
|
|
|
Funcion IMPURA: ejecuta `ping -c <count> -w <timeout_s> <host>` como subprocess
|
|
y parsea la salida (resumen de paquetes y linea rtt). Un host inalcanzable o
|
|
con ICMP filtrado NO es error: se reporta `status:"ok"` con `loss_pct=100` y
|
|
rtt None. Solo es error si el binario falla, el host esta vacio o hay timeout
|
|
duro del subprocess. El campo `raw` siempre esta presente.
|
|
"""
|
|
|
|
import re
|
|
import subprocess
|
|
|
|
_LOSS_RE = re.compile(r"([\d.]+)%\s+packet\s+loss")
|
|
_TX_RX_RE = re.compile(r"(\d+)\s+packets\s+transmitted,\s+(\d+)\s+(?:packets\s+)?received")
|
|
_RTT_RE = re.compile(
|
|
r"(?:rtt|round-trip)\s+min/avg/max(?:/mdev)?\s*=\s*"
|
|
r"([\d.]+)/([\d.]+)/([\d.]+)"
|
|
)
|
|
|
|
|
|
def ping_host(host: str, count: int = 4, timeout_s: int = 30) -> dict:
|
|
"""Hace ping ICMP a un host y parsea el resumen de paquetes y latencia.
|
|
|
|
Args:
|
|
host: Hostname o IP a sondear (ej. ``"1.1.1.1"`` o ``"google.com"``).
|
|
count: Numero de echo requests a enviar (`ping -c`).
|
|
timeout_s: Deadline total del comando ping (`ping -w`) y a la vez el
|
|
timeout duro del subprocess (este ultimo con +5s de margen).
|
|
|
|
Returns:
|
|
Dict de estado. En exito (incluido host inalcanzable)::
|
|
|
|
{
|
|
"status": "ok",
|
|
"host": <host>,
|
|
"packets_sent": <int>,
|
|
"packets_recv": <int>,
|
|
"loss_pct": <float>,
|
|
"rtt_avg_ms": <float|None>,
|
|
"rtt_min_ms": <float|None>,
|
|
"rtt_max_ms": <float|None>,
|
|
"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": "ping_host: host vacio", "host": host, "raw": ""}
|
|
|
|
host = host.strip()
|
|
hard_timeout = float(timeout_s) + 5.0
|
|
|
|
try:
|
|
proc = subprocess.run(
|
|
["ping", "-c", str(count), "-w", str(timeout_s), host],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=hard_timeout,
|
|
)
|
|
except FileNotFoundError:
|
|
return {
|
|
"status": "error",
|
|
"error": "ping_host: binario `ping` no encontrado en PATH (paquete iputils-ping)",
|
|
"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"ping_host: timeout duro del subprocess tras {hard_timeout}s",
|
|
"host": host,
|
|
"raw": partial,
|
|
}
|
|
|
|
raw = proc.stdout or ""
|
|
|
|
packets_sent: int | None = None
|
|
packets_recv: int | None = None
|
|
loss_pct: float = 100.0
|
|
rtt_min = rtt_avg = rtt_max = None
|
|
|
|
m_txrx = _TX_RX_RE.search(raw)
|
|
if m_txrx:
|
|
packets_sent = int(m_txrx.group(1))
|
|
packets_recv = int(m_txrx.group(2))
|
|
|
|
m_loss = _LOSS_RE.search(raw)
|
|
if m_loss:
|
|
loss_pct = float(m_loss.group(1))
|
|
|
|
m_rtt = _RTT_RE.search(raw)
|
|
if m_rtt:
|
|
rtt_min = float(m_rtt.group(1))
|
|
rtt_avg = float(m_rtt.group(2))
|
|
rtt_max = float(m_rtt.group(3))
|
|
|
|
return {
|
|
"status": "ok",
|
|
"host": host,
|
|
"packets_sent": packets_sent,
|
|
"packets_recv": packets_recv,
|
|
"loss_pct": loss_pct,
|
|
"rtt_avg_ms": rtt_avg,
|
|
"rtt_min_ms": rtt_min,
|
|
"rtt_max_ms": rtt_max,
|
|
"raw": raw,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
result = ping_host("1.1.1.1", count=3, timeout_s=10)
|
|
print(result["status"])
|
|
if result["status"] == "ok":
|
|
print(
|
|
f"loss={result['loss_pct']}% "
|
|
f"recv={result['packets_recv']}/{result['packets_sent']} "
|
|
f"avg={result['rtt_avg_ms']}ms"
|
|
)
|
|
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)
|