"""Tests para nmap_scan (wrapper nmap, estilo dict sin excepciones). SIN red: nunca ejecuta nmap real. subprocess.run se monkeypatchea para que el guard y el parseo de XML se prueben de forma determinista y offline. """ import os import subprocess import sys import tempfile sys.path.insert(0, os.path.dirname(__file__)) from nmap_scan import _parse_xml, _target_is_private, nmap_scan # XML fixture minimo de nmap: un host up con el puerto 22/tcp open (ssh). NMAP_XML = """\
""" def _make_fake_run(write_xml: bool = True, returncode: int = 0): """Devuelve un fake de subprocess.run que escribe el XML fixture en la ruta que sigue a '-oX' en el comando y devuelve un CompletedProcess.""" def fake_run(cmd, *args, **kwargs): if write_xml: idx = cmd.index("-oX") xml_path = cmd[idx + 1] with open(xml_path, "w", encoding="utf-8") as fh: fh.write(NMAP_XML) return subprocess.CompletedProcess( args=cmd, returncode=returncode, stdout="raw nmap output", stderr="" ) return fake_run def _fail_if_called(*args, **kwargs): """subprocess.run que falla el test si se invoca (el guard NO debe ejecutar nmap).""" raise AssertionError("subprocess.run no debe llamarse cuando el guard rechaza el target") # --- 1. _parse_xml: golden --------------------------------------------------- def test_parse_xml_extrae_puertos_abiertos_y_hosts_up(): """Escribe el XML fixture a un tmp y comprueba el parseo.""" fd, xml_path = tempfile.mkstemp(suffix=".xml") os.close(fd) try: with open(xml_path, "w", encoding="utf-8") as fh: fh.write(NMAP_XML) open_ports, hosts_up, host_status = _parse_xml(xml_path) assert hosts_up == ["192.168.1.10"] assert host_status == "up" # Solo el puerto 22 esta open; el 80 esta closed y se descarta. assert len(open_ports) == 1 p = open_ports[0] assert p["port"] == 22 assert p["proto"] == "tcp" assert p["state"] == "open" assert p["service"] == "ssh" assert p["product"] == "OpenSSH" assert p["version"] == "8.9" finally: os.remove(xml_path) # --- 2. Guard error path: publico sin confirm no ejecuta nmap ---------------- def test_guard_publico_sin_confirm_rechaza_y_no_ejecuta(monkeypatch): """8.8.8.8 (publico) sin confirm -> error + needs_confirm, sin tocar subprocess.""" monkeypatch.setattr(subprocess, "run", _fail_if_called) result = nmap_scan("8.8.8.8") assert result["status"] == "error" assert result["needs_confirm"] is True assert "8.8.8.8" in result["error"] # --- 3. Guard privado OK: procede y parsea ----------------------------------- def test_guard_privado_procede_y_parsea(monkeypatch): """192.168.1.10 (privado) sin confirm -> procede; XML parseado.""" monkeypatch.setattr(subprocess, "run", _make_fake_run()) result = nmap_scan("192.168.1.10") assert result["status"] == "ok" assert result["hosts_up"] == ["192.168.1.10"] assert len(result["open_ports"]) == 1 assert result["open_ports"][0]["port"] == 22 assert result["raw"] == "raw nmap output" # --- 4. Guard confirm=True sobre publico procede ----------------------------- def test_guard_confirm_true_sobre_publico_procede(monkeypatch): """8.8.8.8 (publico) con confirm=True -> procede.""" monkeypatch.setattr(subprocess, "run", _make_fake_run()) result = nmap_scan("8.8.8.8", confirm=True) assert result["status"] == "ok" assert len(result["open_ports"]) == 1 # --- 5. Guard allowlist: target autorizado procede --------------------------- def test_guard_allowlist_procede(monkeypatch): """scanme.nmap.org en allowlist -> procede sin confirm.""" monkeypatch.setattr(subprocess, "run", _make_fake_run()) result = nmap_scan("scanme.nmap.org", allowlist=["scanme.nmap.org"]) assert result["status"] == "ok" # --- 6. Errores de validacion ------------------------------------------------ def test_perfil_invalido_devuelve_error(monkeypatch): """Un perfil no listado -> status error, sin ejecutar nmap.""" monkeypatch.setattr(subprocess, "run", _fail_if_called) result = nmap_scan("192.168.1.10", profile="noexiste") assert result["status"] == "error" assert "invalido" in result["error"] def test_target_vacio_devuelve_error(monkeypatch): """Target vacio -> status error, sin ejecutar nmap.""" monkeypatch.setattr(subprocess, "run", _fail_if_called) result = nmap_scan("") assert result["status"] == "error" assert "vacio" in result["error"] # --- 7. _target_is_private: clasificacion ------------------------------------ def test_target_is_private_clasifica(): """Privados/local -> True; publico -> False; hostname publico -> None.""" assert _target_is_private("10.0.0.1") is True assert _target_is_private("127.0.0.1") is True assert _target_is_private("192.168.0.0/24") is True assert _target_is_private("8.8.8.8") is False assert _target_is_private("localhost") is True assert _target_is_private("foo.local") is True assert _target_is_private("example.com") is None