import json import shutil import subprocess import tempfile import xml.etree.ElementTree as ET from pathlib import Path def check_deps() -> list[str]: missing = [] for tool in ("masscan", "nmap"): if not shutil.which(tool): missing.append(tool) return missing def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Path) -> list[dict]: if not cidrs or not ports: return [] port_str = ",".join(str(p) for p in sorted(set(ports))) cidr_file = output_dir / "targets.txt" cidr_file.write_text("\n".join(cidrs)) out_file = output_dir / "masscan.json" cmd = [ "masscan", f"--rate={rate}", f"--ports={port_str}", "-iL", str(cidr_file), "-oJ", str(out_file), "--wait", "3", ] try: proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL) proc.wait(timeout=3600) except subprocess.TimeoutExpired: proc.kill() proc.wait() except KeyboardInterrupt: proc.kill() proc.wait() raise except FileNotFoundError: raise RuntimeError("masscan not found — install it first") if not out_file.exists(): return [] raw = out_file.read_text(errors="replace").strip() if not raw or raw == "[]": return [] raw = raw.rstrip(",\n") if not raw.endswith("]"): raw += "]" if not raw.startswith("["): raw = "[" + raw try: data = json.loads(raw) except json.JSONDecodeError: return [] results = [] for entry in data: ip = entry.get("ip") for port_entry in entry.get("ports", []): port = port_entry.get("port") if ip and port: results.append({"ip": ip, "port": port}) return results def nmap_fingerprint(host: str, ports: list[int], output_dir: Path) -> dict: if not ports: return {} port_str = ",".join(str(p) for p in sorted(set(ports))) out_file = output_dir / f"nmap_{host.replace('.', '_')}.xml" cmd = [ "nmap", "-sV", "--version-intensity", "5", "-p", port_str, "-T4", "--open", "-oX", str(out_file), host, ] try: subprocess.run(cmd, capture_output=True, timeout=60) except (subprocess.TimeoutExpired, FileNotFoundError): return {} if not out_file.exists(): return {} try: tree = ET.parse(out_file) except ET.ParseError: return {} result = {} for port_el in tree.findall(".//port"): portid = int(port_el.get("portid", 0)) state = port_el.find("state") if state is None or state.get("state") != "open": continue service = port_el.find("service") info = {} if service is not None: info["service"] = service.get("name", "") info["product"] = service.get("product", "") info["version"] = service.get("version", "") info["extrainfo"] = service.get("extrainfo", "") info["banner"] = " ".join(filter(None, [ info["product"], info["version"], info["extrainfo"] ])) result[portid] = info return result