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>
104 lines
3.0 KiB
Python
104 lines
3.0 KiB
Python
"""Tests para traceroute_host (CLI `traceroute`, estilo dict sin excepciones).
|
|
|
|
Sin red: se monkeypatchea ``subprocess.run`` en el namespace del modulo
|
|
``traceroute_host`` para devolver salidas fijas o lanzar excepciones.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
import traceroute_host as tr_mod
|
|
from traceroute_host import traceroute_host
|
|
|
|
# Salida real de traceroute: cabecera + 3 hops, uno de ellos "* * *".
|
|
RAW_OK = """\
|
|
traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 60 byte packets
|
|
1 gateway (192.168.1.1) 1.234 ms 1.111 ms 1.050 ms
|
|
2 * * *
|
|
3 one.one.one.one (1.1.1.1) 9.876 ms 9.500 ms 9.700 ms
|
|
"""
|
|
|
|
# Salida con un unico hop sin respuesta.
|
|
RAW_ALL_STARS = """\
|
|
traceroute to 10.0.0.1 (10.0.0.1), 30 hops max, 60 byte packets
|
|
1 * * *
|
|
"""
|
|
|
|
|
|
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 traceroute_host."""
|
|
|
|
def fake_run(*args, **kwargs):
|
|
if raises is not None:
|
|
raise raises
|
|
return _FakeProc(stdout)
|
|
|
|
monkeypatch.setattr(tr_mod.subprocess, "run", fake_run)
|
|
|
|
|
|
def test_golden_varios_hops(monkeypatch):
|
|
"""Traceroute con varios hops: parsea numero de hop, host, IP y rtt."""
|
|
_patch_run(monkeypatch, stdout=RAW_OK)
|
|
|
|
result = traceroute_host("1.1.1.1")
|
|
|
|
assert result["status"] == "ok"
|
|
assert result["host"] == "1.1.1.1"
|
|
assert result["raw"] == RAW_OK
|
|
|
|
hops = result["hops"]
|
|
assert [h["hop"] for h in hops] == [1, 2, 3]
|
|
|
|
# Hop 1: gateway con IP y tres rtt.
|
|
hop1 = hops[0]
|
|
assert len(hop1["hosts"]) == 1
|
|
assert hop1["hosts"][0]["name"] == "gateway"
|
|
assert hop1["hosts"][0]["ip"] == "192.168.1.1"
|
|
assert hop1["hosts"][0]["rtt_ms"] == [1.234, 1.111, 1.050]
|
|
|
|
# Hop 2: sin respuesta -> hosts vacio.
|
|
assert hops[1]["hosts"] == []
|
|
|
|
# Hop 3: destino alcanzado.
|
|
hop3 = hops[2]
|
|
assert len(hop3["hosts"]) == 1
|
|
assert hop3["hosts"][0]["name"] == "one.one.one.one"
|
|
assert hop3["hosts"][0]["ip"] == "1.1.1.1"
|
|
assert hop3["hosts"][0]["rtt_ms"] == [9.876, 9.500, 9.700]
|
|
|
|
|
|
def test_edge_hop_sin_respuesta(monkeypatch):
|
|
"""Un solo hop '* * *': hosts vacio para ese salto."""
|
|
_patch_run(monkeypatch, stdout=RAW_ALL_STARS)
|
|
|
|
result = traceroute_host("10.0.0.1")
|
|
|
|
assert result["status"] == "ok"
|
|
assert len(result["hops"]) == 1
|
|
assert result["hops"][0]["hop"] == 1
|
|
assert result["hops"][0]["hosts"] == []
|
|
|
|
|
|
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(tr_mod.subprocess, "run", boom)
|
|
|
|
result = traceroute_host(" ")
|
|
assert result["status"] == "error"
|
|
assert "vacio" in result["error"]
|