diff --git a/geo-scout.py b/geo-scout.py index f9578d6..7d7820e 100644 --- a/geo-scout.py +++ b/geo-scout.py @@ -4,6 +4,7 @@ geo-scout — geo-targeted network scanner masscan for port discovery, nmap for fingerprinting, probe engine for matching """ +import ipaddress import json import os import sys @@ -90,6 +91,33 @@ def collect_all_ports(targets: list[dict]) -> list[int]: return sorted(ports) +def count_ips(cidrs: list[str]) -> int: + total = 0 + for c in cidrs: + try: + total += ipaddress.ip_network(c, strict=False).num_addresses + except ValueError: + pass + return total + + +def fmt_ip_count(n: int) -> str: + if n >= 1_000_000: + return f"{n/1_000_000:.1f}M" + if n >= 1_000: + return f"{n/1_000:.0f}K" + return str(n) + + +def fmt_eta(total_ips: int, n_ports: int, rate: int) -> str: + if rate <= 0: + return "?" + secs = int(total_ips * n_ports / rate) + m, s = divmod(secs, 60) + h, m = divmod(m, 60) + return f"{h}h {m:02d}m" if h else f"{m}m {s:02d}s" + + # ── Probe worker ────────────────────────────────────────────────────────────── def probe_host(ip: str, port: int, targets: list[dict]) -> list[dict]: matches = [] @@ -183,8 +211,10 @@ def cmd_run(args): if max_hosts: cidrs = cidrs[:max_hosts // 256 + 1] - 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 once = skip country, twice = abort all)...") + total_ips = count_ips(cidrs) + eta = fmt_eta(total_ips, len(all_ports), rate) + print(f"\n{C}[{idx}/{n_countries}]{RST} {BOLD}{cc}{RST} — {len(cidrs)} CIDR blocks {W}({fmt_ip_count(total_ips)} IPs ~{eta} at {rate} pps){RST}") + info(f"{cc}: running masscan (Ctrl-C once = skip country, twice = abort all)...") try: open_ports = masscan_sweep(cidrs, all_ports, rate, tmp) diff --git a/lib/scanner.py b/lib/scanner.py index 0911f9b..5f3eb6c 100644 --- a/lib/scanner.py +++ b/lib/scanner.py @@ -1,7 +1,8 @@ import json import shutil import subprocess -import tempfile +import threading +import time import xml.etree.ElementTree as ET from pathlib import Path @@ -14,6 +15,12 @@ def check_deps() -> list[str]: return missing +def _fmt_elapsed(seconds: int) -> str: + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return f"{h}h{m:02d}m{s:02d}s" if h else f"{m}m{s:02d}s" + + def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Path) -> list[dict]: if not cidrs or not ports: return [] @@ -32,8 +39,26 @@ def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Pat "--wait", "3", ] + stop = threading.Event() + start = time.time() + + def _progress(): + while not stop.is_set(): + found = 0 + if out_file.exists(): + try: + found = out_file.read_text(errors="replace").count('"ip"') + except Exception: + pass + elapsed = _fmt_elapsed(int(time.time() - start)) + print(f"\r [{elapsed}] masscan running — {found} hosts found so far ", end="", flush=True) + stop.wait(timeout=4) + + t = threading.Thread(target=_progress, daemon=True) + t.start() + try: - proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL) + proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) proc.wait(timeout=3600) except subprocess.TimeoutExpired: proc.kill() @@ -41,10 +66,20 @@ def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Pat except KeyboardInterrupt: proc.kill() proc.wait() + stop.set() + t.join(timeout=2) + print() raise except FileNotFoundError: + stop.set() + t.join(timeout=2) + print() raise RuntimeError("masscan not found — install it first") + stop.set() + t.join(timeout=2) + print() + if not out_file.exists(): return []