Add live scan progress and graceful Ctrl-C handling

This commit is contained in:
n0mad1k
2026-04-30 14:49:57 -04:00
parent feb34ada8b
commit 615937b20b
2 changed files with 89 additions and 61 deletions
+81 -58
View File
@@ -147,77 +147,100 @@ def cmd_run(args):
total_hosts = 0 total_hosts = 0
total_matches = 0 total_matches = 0
workers = args.workers workers = args.workers
n_countries = len(countries)
with tempfile.TemporaryDirectory() as tmpdir: def progress(msg: str, end="\n"):
tmp = Path(tmpdir) print(f" {W}{msg}{RST}", end=end, flush=True)
for country in countries: try:
cc = country["code"].upper() with tempfile.TemporaryDirectory() as tmpdir:
exclude = country.get("exclude_cidrs", []) tmp = Path(tmpdir)
cidrs = get_cidrs(cc, cidr_index)
cidrs = merge_cidrs(cidrs, exclude)
if not cidrs: for idx, country in enumerate(countries, 1):
warn(f"{cc}: no CIDR ranges found, skipping") cc = country["code"].upper()
continue exclude = country.get("exclude_cidrs", [])
cidrs = get_cidrs(cc, cidr_index)
cidrs = merge_cidrs(cidrs, exclude)
if max_hosts: if not cidrs:
cidrs = cidrs[:max_hosts // 256 + 1] 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) print(f"\n{C}[{idx}/{n_countries}]{RST} {BOLD}{cc}{RST}{len(cidrs)} CIDR blocks")
hosts_found = len(set(e["ip"] for e in open_ports)) info(f"{cc}: running masscan at {rate} pps (Ctrl-C to skip country)...")
total_hosts += hosts_found
ok(f"{cc}: {hosts_found} live hosts ({len(open_ports)} open ports)")
if not open_ports: try:
continue 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 hosts_found = len(set(e["ip"] for e in open_ports))
ip_ports: dict[str, list[int]] = {} total_hosts += hosts_found
for entry in open_ports: ok(f"{cc}: {hosts_found} live hosts, {len(open_ports)} open ports found")
ip_ports.setdefault(entry["ip"], []).append(entry["port"])
info(f"{cc}: fingerprinting {hosts_found} hosts with nmap...") if not open_ports:
nmap_results: dict[str, dict] = {} continue
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] = {}
info(f"{cc}: probing and matching targets...") # Group by IP for nmap
with ThreadPoolExecutor(max_workers=workers) as ex: ip_ports: dict[str, list[int]] = {}
futs = []
for entry in open_ports: 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): total_nmap = len(ip_ports)
try: info(f"{cc}: fingerprinting {total_nmap} hosts with nmap...")
matches = fut.result() nmap_results: dict[str, dict] = {}
except Exception: nmap_done = 0
continue with ThreadPoolExecutor(max_workers=min(workers, 10)) as ex:
for match in matches: futs = {ex.submit(nmap_fingerprint, ip, ports, tmp): ip
ip = match["ip"] for ip, ports in ip_ports.items()}
port = match["port"] for fut in as_completed(futs):
nmap_info = nmap_results.get(ip, {}).get(port, {}) ip = futs[fut]
hit = { nmap_done += 1
**match, progress(f"nmap: {nmap_done}/{total_nmap}", end="\r")
"country_code": cc, try:
"nmap_service": nmap_info.get("service", ""), nmap_results[ip] = fut.result()
"nmap_banner": nmap_info.get("banner", ""), except Exception:
} nmap_results[ip] = {}
db.insert_hit(conn, run_id, hit) print()
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}")
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) db.finish_run(conn, run_id, total_hosts, total_matches)
conn.close() conn.close()
+8 -3
View File
@@ -29,14 +29,19 @@ def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Pat
f"--ports={port_str}", f"--ports={port_str}",
"-iL", str(cidr_file), "-iL", str(cidr_file),
"-oJ", str(out_file), "-oJ", str(out_file),
"--output-format", "json",
"--wait", "3", "--wait", "3",
] ]
try: try:
subprocess.run(cmd, capture_output=True, timeout=3600) proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL)
proc.wait(timeout=3600)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
pass proc.kill()
proc.wait()
except KeyboardInterrupt:
proc.kill()
proc.wait()
raise
except FileNotFoundError: except FileNotFoundError:
raise RuntimeError("masscan not found — install it first") raise RuntimeError("masscan not found — install it first")