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,57 @@
|
||||
"""Tests para grab_service_banner (sin red externa; servidor TCP local fake)."""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
import threading
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from grab_service_banner import grab_service_banner
|
||||
|
||||
|
||||
class _BannerHandler(socketserver.BaseRequestHandler):
|
||||
"""Emite un banner SSH fake al conectar, como hace un servidor SSH real."""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
self.request.sendall(b"SSH-2.0-TestServer\r\n")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def test_identifica_ssh_de_banner_local():
|
||||
"""Un servidor TCP local que emite 'SSH-2.0-...' se identifica como ssh."""
|
||||
server = socketserver.TCPServer(("127.0.0.1", 0), _BannerHandler)
|
||||
# bind_and_activate por defecto ya hizo bind; tomamos el puerto efimero.
|
||||
host, port = server.server_address
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
result = grab_service_banner(host, port, timeout_s=2.0, send_probe=False)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=2.0)
|
||||
|
||||
assert result["status"] == "ok"
|
||||
assert result["service"] == "ssh"
|
||||
assert "SSH-2.0-TestServer" in result["banner"]
|
||||
assert result["product"] == "TestServer" or result["product"] # best-effort
|
||||
|
||||
|
||||
def test_host_vacio_devuelve_error():
|
||||
"""Un host vacio devuelve status error sin lanzar y sin tocar la red."""
|
||||
result = grab_service_banner("", 22)
|
||||
assert result["status"] == "error"
|
||||
assert "vacio" in result["error"]
|
||||
assert set(["status", "error", "host", "port"]).issubset(result.keys())
|
||||
|
||||
|
||||
def test_port_fuera_de_rango_devuelve_error():
|
||||
"""Un puerto fuera del rango 1..65535 devuelve status error sin conectar."""
|
||||
result = grab_service_banner("127.0.0.1", 70000)
|
||||
assert result["status"] == "error"
|
||||
assert "rango" in result["error"]
|
||||
assert result["port"] == 70000
|
||||
Reference in New Issue
Block a user