diff --git a/geo-scout.py b/geo-scout.py index 9f31939..b110baf 100644 --- a/geo-scout.py +++ b/geo-scout.py @@ -147,77 +147,100 @@ def cmd_run(args): total_hosts = 0 total_matches = 0 workers = args.workers + n_countries = len(countries) - with tempfile.TemporaryDirectory() as tmpdir: - tmp = Path(tmpdir) + def progress(msg: str, end="\n"): + print(f" {W}{msg}{RST}", end=end, flush=True) - for country in countries: - cc = country["code"].upper() - exclude = country.get("exclude_cidrs", []) - cidrs = get_cidrs(cc, cidr_index) - cidrs = merge_cidrs(cidrs, exclude) + try: + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) - if not cidrs: - warn(f"{cc}: no CIDR ranges found, skipping") - continue + for idx, country in enumerate(countries, 1): + cc = country["code"].upper() + exclude = country.get("exclude_cidrs", []) + cidrs = get_cidrs(cc, cidr_index) + cidrs = merge_cidrs(cidrs, exclude) - if max_hosts: - cidrs = cidrs[:max_hosts // 256 + 1] + if not cidrs: + warn(f"{cc}: no CIDR ranges found, skipping") + continue - info(f"{BOLD}{cc}{RST}: {len(cidrs)} CIDR blocks → masscan...") + if max_hosts: + cidrs = cidrs[:max_hosts // 256 + 1] - open_ports = masscan_sweep(cidrs, all_ports, rate, tmp) - hosts_found = len(set(e["ip"] for e in open_ports)) - total_hosts += hosts_found - ok(f"{cc}: {hosts_found} live hosts ({len(open_ports)} open ports)") + print(f"\n{C}[{idx}/{n_countries}]{RST} {BOLD}{cc}{RST} — {len(cidrs)} CIDR blocks") + info(f"{cc}: running masscan at {rate} pps (Ctrl-C to skip country)...") - if not open_ports: - continue + try: + open_ports = masscan_sweep(cidrs, all_ports, rate, tmp) + except KeyboardInterrupt: + print() + warn(f"{cc}: scan interrupted — skipping to next country") + continue - # Group by IP for nmap - ip_ports: dict[str, list[int]] = {} - for entry in open_ports: - ip_ports.setdefault(entry["ip"], []).append(entry["port"]) + hosts_found = len(set(e["ip"] for e in open_ports)) + total_hosts += hosts_found + ok(f"{cc}: {hosts_found} live hosts, {len(open_ports)} open ports found") - info(f"{cc}: fingerprinting {hosts_found} hosts with nmap...") - nmap_results: dict[str, dict] = {} - with ThreadPoolExecutor(max_workers=min(workers, 10)) as ex: - futs = {ex.submit(nmap_fingerprint, ip, ports, tmp): ip - for ip, ports in ip_ports.items()} - for fut in as_completed(futs): - ip = futs[fut] - try: - nmap_results[ip] = fut.result() - except Exception: - nmap_results[ip] = {} + if not open_ports: + continue - info(f"{cc}: probing and matching targets...") - with ThreadPoolExecutor(max_workers=workers) as ex: - futs = [] + # Group by IP for nmap + ip_ports: dict[str, list[int]] = {} for entry in open_ports: - futs.append(ex.submit(probe_host, entry["ip"], entry["port"], targets)) + ip_ports.setdefault(entry["ip"], []).append(entry["port"]) - for fut in as_completed(futs): - try: - matches = fut.result() - except Exception: - continue - for match in matches: - ip = match["ip"] - port = match["port"] - nmap_info = nmap_results.get(ip, {}).get(port, {}) - hit = { - **match, - "country_code": cc, - "nmap_service": nmap_info.get("service", ""), - "nmap_banner": nmap_info.get("banner", ""), - } - db.insert_hit(conn, run_id, hit) - total_matches += 1 - ver = f" [{match['version']}]" if match.get("version") else "" - print(f" {G}HIT{RST} {BOLD}{ip}:{port}{RST} {Y}{match['target_name']}{RST}{ver} [{match['confidence']}] {cc}") + total_nmap = len(ip_ports) + info(f"{cc}: fingerprinting {total_nmap} hosts with nmap...") + nmap_results: dict[str, dict] = {} + nmap_done = 0 + with ThreadPoolExecutor(max_workers=min(workers, 10)) as ex: + futs = {ex.submit(nmap_fingerprint, ip, ports, tmp): ip + for ip, ports in ip_ports.items()} + for fut in as_completed(futs): + ip = futs[fut] + nmap_done += 1 + progress(f"nmap: {nmap_done}/{total_nmap}", end="\r") + try: + nmap_results[ip] = fut.result() + except Exception: + nmap_results[ip] = {} + print() - print() + total_probes = len(open_ports) + info(f"{cc}: probing {total_probes} open ports for target matches...") + probe_done = 0 + with ThreadPoolExecutor(max_workers=workers) as ex: + futs = [ex.submit(probe_host, entry["ip"], entry["port"], targets) + for entry in open_ports] + + for fut in as_completed(futs): + probe_done += 1 + progress(f"probe: {probe_done}/{total_probes}", end="\r") + try: + matches = fut.result() + except Exception: + continue + for match in matches: + ip = match["ip"] + port = match["port"] + nmap_info = nmap_results.get(ip, {}).get(port, {}) + hit = { + **match, + "country_code": cc, + "nmap_service": nmap_info.get("service", ""), + "nmap_banner": nmap_info.get("banner", ""), + } + db.insert_hit(conn, run_id, hit) + total_matches += 1 + ver = f" [{match['version']}]" if match.get("version") else "" + print(f"\r {G}HIT{RST} {BOLD}{ip}:{port}{RST} {Y}{match['target_name']}{RST}{ver} [{match['confidence']}] {cc}") + print() + + except KeyboardInterrupt: + print() + warn("Scan aborted by user") db.finish_run(conn, run_id, total_hosts, total_matches) conn.close() diff --git a/lib/scanner.py b/lib/scanner.py index 13fb533..0911f9b 100644 --- a/lib/scanner.py +++ b/lib/scanner.py @@ -29,14 +29,19 @@ def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Pat f"--ports={port_str}", "-iL", str(cidr_file), "-oJ", str(out_file), - "--output-format", "json", "--wait", "3", ] try: - subprocess.run(cmd, capture_output=True, timeout=3600) + proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL) + proc.wait(timeout=3600) except subprocess.TimeoutExpired: - pass + proc.kill() + proc.wait() + except KeyboardInterrupt: + proc.kill() + proc.wait() + raise except FileNotFoundError: raise RuntimeError("masscan not found — install it first")