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,130 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user