"""Pipeline recon_osint. One-shot que ejecuta UN escaneo de red atomico y SIEMPRE lo archiva en OSINT. Materializa la politica "todo escaneo que lancemos se guarda en osint": convierte el patron de dos llamadas (scan atomico + sink a OSINT) en una sola invocacion. Compone funciones del registry del dominio cybersecurity; no reescribe ninguna logica de escaneo ni de persistencia. Funciones del registry compuestas (importadas, no reimplementadas): whois_lookup, rdap_lookup, dns_records, ping_host, traceroute_host, nmap_scan, save_scan_to_osint """ from cybersecurity import ( whois_lookup, rdap_lookup, dns_records, ping_host, traceroute_host, nmap_scan, save_scan_to_osint, ) VALID_SCAN_TYPES = ("whois", "rdap", "dns", "ping", "traceroute", "nmap") def recon_osint( target: str, scan_type: str = "whois", save: bool = True, profile: str = "quick", record_types: list[str] | None = None, count: int = 4, max_hops: int = 30, timeout_s: int | None = None, confirm: bool = False, ) -> dict: """Ejecuta un escaneo de red atomico y lo archiva en OSINT en un solo paso. Despacha al escaneo atomico correspondiente segun ``scan_type``, captura su resultado y, si el escaneo tuvo exito y ``save`` es True, lo guarda en el vault OSINT via ``save_scan_to_osint``. Nunca lanza excepciones: cualquier fallo se refleja en la clave ``status`` del dict devuelto. Args: target: dominio, host o IP objetivo del escaneo. scan_type: tipo de escaneo, uno de whois, rdap, dns, ping, traceroute, nmap. save: si True (default) archiva el resultado en OSINT; si False solo ejecuta el escaneo. profile: perfil de nmap (solo aplica a scan_type='nmap'). Ej. quick, full-tcp, vuln. record_types: tipos de registro DNS a consultar (solo scan_type='dns'). None usa los defaults de dns_records. count: numero de paquetes ICMP (solo scan_type='ping'). max_hops: numero maximo de saltos (solo scan_type='traceroute'). timeout_s: timeout en segundos. Si se pasa, se propaga al escaneo atomico; si es None, cada atomico usa su default. confirm: confirmacion explicita para el escaneo activo de nmap (solo aplica a scan_type='nmap'). Por defecto False: nmap rechaza targets publicos/desconocidos no autorizados. Pasar True solo cuando el escaneo este autorizado. Se ignora para los demas scan_type. Returns: dict con: - status: 'ok' | 'error'. - target: el objetivo escaneado. - scan_type: el tipo de escaneo ejecutado. - scan: dict crudo devuelto por la funcion atomica. - osint: dict devuelto por save_scan_to_osint, o None si save=False. En caso de scan_type invalido devuelve {"status": "error", "stage": "validate", ...} con la lista de tipos validos. Si el escaneo falla devuelve {"status": "error", "stage": "scan", "scan": } sin intentar guardar. """ if scan_type not in VALID_SCAN_TYPES: return { "status": "error", "stage": "validate", "target": target, "scan_type": scan_type, "error": f"invalid scan_type '{scan_type}'", "valid": list(VALID_SCAN_TYPES), } # 1. Despacho al escaneo atomico correspondiente. if scan_type == "whois": kwargs = {} if timeout_s is None else {"timeout_s": timeout_s} scan = whois_lookup(target, **kwargs) elif scan_type == "rdap": kwargs = {} if timeout_s is None else {"timeout_s": timeout_s} scan = rdap_lookup(target, **kwargs) elif scan_type == "dns": kwargs = {"record_types": record_types} if timeout_s is not None: kwargs["timeout_s"] = timeout_s scan = dns_records(target, **kwargs) elif scan_type == "ping": kwargs = {"count": count} if timeout_s is not None: kwargs["timeout_s"] = timeout_s scan = ping_host(target, **kwargs) elif scan_type == "traceroute": kwargs = {"max_hops": max_hops} if timeout_s is not None: kwargs["timeout_s"] = timeout_s scan = traceroute_host(target, **kwargs) else: # nmap kwargs = {"profile": profile, "confirm": confirm} if timeout_s is not None: kwargs["timeout_s"] = timeout_s scan = nmap_scan(target, **kwargs) # 2. Si el escaneo fallo, no intentamos guardar. if scan.get("status") == "error": return {"status": "error", "stage": "scan", "scan": scan} # 3. Construye el summary segun el tipo de escaneo. if scan_type == "whois": summary = { "registrar": scan.get("registrar"), "expiry_date": scan.get("expiry_date"), } elif scan_type == "rdap": summary = { "handle": scan.get("handle"), "ldhName": scan.get("ldhName"), } elif scan_type == "dns": summary = {"records": scan.get("records")} elif scan_type == "ping": summary = { "loss_pct": scan.get("loss_pct"), "rtt_avg_ms": scan.get("rtt_avg_ms"), } elif scan_type == "traceroute": summary = {"hop_count": len(scan.get("hops") or [])} else: # nmap summary = { "open_ports": scan.get("open_ports"), "hosts_up": scan.get("hosts_up"), "profile": profile, } # 4. Archiva en OSINT si procede. osint = None if save: osint = save_scan_to_osint( target, scan_type, scan.get("raw", ""), summary=summary, tool=scan_type, ) return { "status": "ok", "target": target, "scan_type": scan_type, "scan": scan, "osint": osint, } def _parse_cli(argv: list[str]) -> dict: """Parsea los args de CLI: [scan_type] [--confirm] [--allowlist a,b,c]. Mantiene compatibilidad: sin args usa el smoke por defecto; sin --confirm, confirm=False. Devuelve un dict de kwargs para recon_osint mas la allowlist. """ positional: list[str] = [] confirm = False allowlist: list[str] | None = None i = 0 while i < len(argv): arg = argv[i] if arg == "--confirm": confirm = True elif arg == "--allowlist": if i + 1 < len(argv): allowlist = [a.strip() for a in argv[i + 1].split(",") if a.strip()] i += 1 elif arg.startswith("--allowlist="): raw = arg.split("=", 1)[1] allowlist = [a.strip() for a in raw.split(",") if a.strip()] else: positional.append(arg) i += 1 return {"positional": positional, "confirm": confirm, "allowlist": allowlist} if __name__ == "__main__": import sys parsed = _parse_cli(sys.argv[1:]) positional = parsed["positional"] target = positional[0] if len(positional) >= 1 else "example.com" scan_type = positional[1] if len(positional) >= 2 else "whois" # --confirm activa confirm directamente. --allowlist activa confirm cuando # el target esta autorizado en la lista (mismo criterio que nmap_scan). confirm = parsed["confirm"] allowlist = parsed["allowlist"] if not confirm and allowlist: t = target.strip() if any(t == a or t.endswith(a) for a in allowlist): confirm = True try: result = recon_osint( target, scan_type, confirm=confirm, ) print("status:", result.get("status")) # Para nmap rechazado por el guard, el dict trae stage=scan + scan.needs_confirm. scan = result.get("scan") or {} if scan.get("needs_confirm"): print("needs_confirm:", True) print("error:", scan.get("error")) osint = result.get("osint") or {} print("note_path:", osint.get("note_path")) print("registered:", osint.get("registered")) except Exception as exc: # smoke tolerante a red / servicios caidos print("smoke exception (tolerada):", repr(exc))