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>
126 lines
3.7 KiB
Python
126 lines
3.7 KiB
Python
"""Tests para ping_host (CLI `ping`, estilo dict sin excepciones).
|
|
|
|
Sin red: se monkeypatchea ``subprocess.run`` en el namespace del modulo
|
|
``ping_host`` para devolver salidas fijas o lanzar excepciones controladas.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
import ping_host as ping_mod
|
|
from ping_host import ping_host
|
|
|
|
# Salida real de un ping con exito (4/4, 0% loss, linea rtt).
|
|
RAW_OK = """\
|
|
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
|
|
64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=1.10 ms
|
|
64 bytes from 1.1.1.1: icmp_seq=2 ttl=58 time=2.20 ms
|
|
64 bytes from 1.1.1.1: icmp_seq=3 ttl=58 time=2.50 ms
|
|
64 bytes from 1.1.1.1: icmp_seq=4 ttl=58 time=3.00 ms
|
|
|
|
--- 1.1.1.1 ping statistics ---
|
|
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
|
|
rtt min/avg/max/mdev = 1.1/2.2/3.3/0.4 ms
|
|
"""
|
|
|
|
# Salida real de un host con ICMP filtrado: todo perdido, sin linea rtt.
|
|
RAW_FILTERED = """\
|
|
PING blackhole.example (10.255.255.1) 56(84) bytes of data.
|
|
|
|
--- blackhole.example ping statistics ---
|
|
4 packets transmitted, 0 received, 100% packet loss, time 3070ms
|
|
"""
|
|
|
|
|
|
class _FakeProc:
|
|
"""Stand-in de CompletedProcess: solo expone stdout y returncode."""
|
|
|
|
def __init__(self, stdout: str, returncode: int = 0):
|
|
self.stdout = stdout
|
|
self.returncode = returncode
|
|
|
|
|
|
def _patch_run(monkeypatch, *, stdout=None, raises=None):
|
|
"""Sustituye subprocess.run en el modulo ping_host.
|
|
|
|
Si ``raises`` es una excepcion, la lanza al invocarse; en otro caso
|
|
devuelve un _FakeProc con el stdout dado.
|
|
"""
|
|
|
|
def fake_run(*args, **kwargs):
|
|
if raises is not None:
|
|
raise raises
|
|
return _FakeProc(stdout)
|
|
|
|
monkeypatch.setattr(ping_mod.subprocess, "run", fake_run)
|
|
|
|
|
|
def test_golden_ping_con_exito(monkeypatch):
|
|
"""Ping exitoso: parsea paquetes, perdida 0% y rtt min/avg/max."""
|
|
_patch_run(monkeypatch, stdout=RAW_OK)
|
|
|
|
result = ping_host("1.1.1.1")
|
|
|
|
assert result["status"] == "ok"
|
|
assert result["host"] == "1.1.1.1"
|
|
assert result["packets_sent"] == 4
|
|
assert result["packets_recv"] == 4
|
|
assert result["loss_pct"] == 0.0
|
|
assert result["rtt_min_ms"] == 1.1
|
|
assert result["rtt_avg_ms"] == 2.2
|
|
assert result["rtt_max_ms"] == 3.3
|
|
assert result["raw"] == RAW_OK
|
|
|
|
|
|
def test_edge_host_filtrado(monkeypatch):
|
|
"""Host inalcanzable/filtrado: status ok, 100% loss, rtt None."""
|
|
_patch_run(monkeypatch, stdout=RAW_FILTERED)
|
|
|
|
result = ping_host("blackhole.example")
|
|
|
|
assert result["status"] == "ok"
|
|
assert result["packets_sent"] == 4
|
|
assert result["packets_recv"] == 0
|
|
assert result["loss_pct"] == 100.0
|
|
assert result["rtt_avg_ms"] is None
|
|
assert result["rtt_min_ms"] is None
|
|
assert result["rtt_max_ms"] is None
|
|
|
|
|
|
def test_error_host_vacio(monkeypatch):
|
|
"""Host en blanco: status error sin invocar subprocess."""
|
|
|
|
def boom(*args, **kwargs):
|
|
raise AssertionError("subprocess.run no debe llamarse con host vacio")
|
|
|
|
monkeypatch.setattr(ping_mod.subprocess, "run", boom)
|
|
|
|
result = ping_host(" ")
|
|
assert result["status"] == "error"
|
|
assert "vacio" in result["error"]
|
|
|
|
|
|
def test_error_binario_ausente(monkeypatch):
|
|
"""ping no en PATH: status error y el mensaje menciona ping."""
|
|
_patch_run(monkeypatch, raises=FileNotFoundError())
|
|
|
|
result = ping_host("1.1.1.1")
|
|
assert result["status"] == "error"
|
|
assert "ping" in result["error"]
|
|
assert result["host"] == "1.1.1.1"
|
|
|
|
|
|
def test_error_timeout(monkeypatch):
|
|
"""Timeout duro del subprocess: status error."""
|
|
_patch_run(
|
|
monkeypatch,
|
|
raises=subprocess.TimeoutExpired(cmd=["ping"], timeout=1),
|
|
)
|
|
|
|
result = ping_host("1.1.1.1")
|
|
assert result["status"] == "error"
|
|
assert "timeout" in result["error"]
|