Files
fn_registry/python/functions/cybersecurity/fetch_http_fingerprint_test.py
T
egutierrez 935008ec3f 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>
2026-06-14 15:12:07 +02:00

109 lines
3.7 KiB
Python

"""Tests para fetch_http_fingerprint.
Levanta un http.server.HTTPServer local en 127.0.0.1 en un puerto efimero,
servido por un thread, con headers fake (Server, X-Powered-By, Set-Cookie) y
un HTML con <title>Hola</title>. NO toca red externa.
"""
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from fetch_http_fingerprint import fetch_http_fingerprint
_HTML = b"<!doctype html><html><head><title>Hola</title></head><body>ok</body></html>"
class _FakeHandler(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802 (firma de BaseHTTPRequestHandler)
self.send_response(200)
self.send_header("Server", "TestServer/1.0")
self.send_header("X-Powered-By", "PHP/8.1")
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Set-Cookie", "PHPSESSID=secret_value_no_capturar; Path=/")
self.end_headers()
self.wfile.write(_HTML)
def log_message(self, *args): # silencia el logging del server en los tests
pass
def _start_server():
server = HTTPServer(("127.0.0.1", 0), _FakeHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
return server, thread
def test_status_ok_y_status_code_200():
server, thread = _start_server()
try:
port = server.server_address[1]
res = fetch_http_fingerprint(f"http://127.0.0.1:{port}/")
assert res["status"] == "ok", res
assert res["status_code"] == 200, res["status_code"]
finally:
server.shutdown()
thread.join(timeout=2)
def test_headers_normalizados_lowercase():
server, thread = _start_server()
try:
port = server.server_address[1]
res = fetch_http_fingerprint(f"http://127.0.0.1:{port}/")
assert res["headers"]["server"] == "TestServer/1.0", res["headers"]
assert res["server"] == "TestServer/1.0", res["server"]
assert res["headers"]["x-powered-by"] == "PHP/8.1", res["headers"]
finally:
server.shutdown()
thread.join(timeout=2)
def test_cookies_solo_nombres_no_valores():
server, thread = _start_server()
try:
port = server.server_address[1]
res = fetch_http_fingerprint(f"http://127.0.0.1:{port}/")
assert "PHPSESSID" in res["cookies"], res["cookies"]
# El valor sensible NUNCA debe aparecer en la salida.
assert "secret_value_no_capturar" not in res["raw"], "valor de cookie filtrado en raw"
assert all("=" not in c for c in res["cookies"]), res["cookies"]
finally:
server.shutdown()
thread.join(timeout=2)
def test_title_extraido():
server, thread = _start_server()
try:
port = server.server_address[1]
res = fetch_http_fingerprint(f"http://127.0.0.1:{port}/")
assert res["title"] == "Hola", res["title"]
assert res["html_len"] == len(_HTML), res["html_len"]
finally:
server.shutdown()
thread.join(timeout=2)
def test_url_vacia_devuelve_error():
res = fetch_http_fingerprint("")
assert res["status"] == "error", res
assert "url vacia" in res["error"], res["error"]
def test_host_inexistente_devuelve_error_sin_lanzar():
# Puerto cerrado en loopback: conexion rechazada, debe devolver error, no lanzar.
res = fetch_http_fingerprint("http://127.0.0.1:1/")
assert res["status"] == "error", res
assert res["url"] == "http://127.0.0.1:1/", res
if __name__ == "__main__":
test_status_ok_y_status_code_200()
test_headers_normalizados_lowercase()
test_cookies_solo_nombres_no_valores()
test_title_extraido()
test_url_vacia_devuelve_error()
test_host_inexistente_devuelve_error_sin_lanzar()
print("all tests passed")