Phase-accurate webrunner cost/time estimator with masscan-Tor OPSEC warnings
Estimator rewrite: - Phase-decomposed estimate_scan_hours: masscan, nmap-only, nmap-fingerprint, probe, nuclei phases each timed separately - Accepts 6 tuning params (nmap_timing, nmap_workers, nuclei_rate, nuclei_concurrency, hit_rate, use_tor) — was previously ignoring all of them - nmap-only mode fixed: was underestimated ~100x (treated nmap like packet scanning); now uses NMAP_TIME_BY_TIMING table per timing template - masscan+nuclei mode now properly modeled (was falling through to masscan-only formula, missing the nuclei phase entirely) - Tor multiplier scoped per-phase: applied to nmap/nuclei/probe only. Masscan uses raw sockets and Tor cannot proxy raw sockets — was previously applying Tor multiplier to masscan rate (incorrect) - 5min/node provisioning overhead added (matters vs Linode 1h billing min) - build_estimate_table accepts tuning dict, threads through to engine Operator-facing changes (deploy_webrunner.py): - Tuning section moved BEFORE estimate display so numbers reflect actual operator choices, not defaults - Estimate header now shows active tuning values when set - Caveats block printed below estimate explaining empirical limits (hit rate range, nuclei template variance, nmap timing tradeoffs, provisioning, billing minimums) - CRITICAL OPSEC banner displayed when Tor enabled with masscan modes: loud red warning that masscan SYN packets bypass Tor and reveal cloud node IP. Includes confirmation gate — operator must affirm they understand before proceeding. Suggests nmap-only mode for full Tor coverage if attribution risk is unacceptable Why: operator was getting estimates off by 100x for nmap-only and 0% accurate for masscan+nuclei. Tuning controls were invisible to the estimator. Tor + masscan was silently broken-by-design (raw sockets bypass proxychains) without operator awareness.
This commit is contained in:
@@ -208,8 +208,53 @@ def _get_tuning_params(scan_mode: str, default_rate: int, vars_overrides: dict)
|
||||
return tuning
|
||||
|
||||
|
||||
def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str, use_tor: bool = False):
|
||||
rows = build_estimate_table(total_ips, n_ports, providers, scan_mode, use_tor=use_tor)
|
||||
def _show_masscan_tor_warning():
|
||||
R = COLORS['RED']
|
||||
Y = COLORS['YELLOW']
|
||||
W = COLORS['WHITE']
|
||||
Z = COLORS['RESET']
|
||||
print(f"\n{R}{'█' * 70}{Z}")
|
||||
print(f"{R}█ ⚠ CRITICAL OPSEC WARNING — MASSCAN BYPASSES TOR █{Z}")
|
||||
print(f"{R}{'█' * 70}{Z}")
|
||||
print(f"{W} masscan uses raw sockets and CANNOT be tunneled through Tor or")
|
||||
print(f" proxychains. Every SYN packet sent during the masscan phase reveals")
|
||||
print(f" THIS CLOUD NODE'S IP to the targets and any monitoring along the path.{Z}")
|
||||
print()
|
||||
print(f"{Y} Tor will protect: {Z}{W}nmap fingerprinting, nuclei requests, probe banners{Z}")
|
||||
print(f"{Y} Tor will NOT protect: {Z}{R}masscan SYN scan (bypasses the proxy entirely){Z}")
|
||||
print()
|
||||
print(f"{W} If you need full Tor coverage, use 'nmap-only' mode (slow but fully")
|
||||
print(f" proxied via -sT). The masscan phase is fundamentally incompatible.{Z}\n")
|
||||
|
||||
|
||||
def _show_caveats_block(scan_mode: str, use_tor: bool):
|
||||
Y = COLORS['YELLOW']
|
||||
W = COLORS['WHITE']
|
||||
R = COLORS['RED']
|
||||
Z = COLORS['RESET']
|
||||
print(f"{Y} Caveats — empirical estimates, real scans vary ±30%:{Z}")
|
||||
print(f"{W} • Hit rate assumes ~1% of IPs have open ports on selected ports.")
|
||||
print(f" Real range: 0.5%–5% (higher for SSH/HTTP/HTTPS, lower for niche")
|
||||
print(f" ports). Override via vars file: hit_rate: 0.03{Z}")
|
||||
if scan_mode in ('masscan+nuclei',):
|
||||
print(f"{W} • Nuclei estimate assumes 5s/target (typical CVE template, 1–3 HTTP")
|
||||
print(f" requests). Heavy templates with many requests/matchers run 2–5×")
|
||||
print(f" longer.{Z}")
|
||||
if scan_mode in ('masscan+nmap', 'geo-scout', 'nmap-only'):
|
||||
print(f"{W} • nmap timing: T1=60s/host, T2=30s, T3=15s, T4=10s. Lower T values")
|
||||
print(f" evade rate-limit detection but multiply scan time accordingly.{Z}")
|
||||
if use_tor:
|
||||
print(f"{R} • Tor adds 5× latency to nmap/nuclei/probe phases. Real overhead")
|
||||
print(f" varies 3×–10× depending on circuit quality. Masscan is NOT")
|
||||
print(f" proxied — see the warning banner above.{Z}")
|
||||
print(f"{W} • Provisioning: ~5 min/node included. Add 5–10 min for first-time")
|
||||
print(f" cloud-provider auth or new region.")
|
||||
print(f" • Cost: Linode/FlokiNET bill minimum 1h per node. AWS bills per")
|
||||
print(f" second. Short scans still incur the per-provider minimum.{Z}\n")
|
||||
|
||||
|
||||
def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str, use_tor: bool = False, tuning: dict | None = None):
|
||||
rows = build_estimate_table(total_ips, n_ports, providers, scan_mode, use_tor=use_tor, tuning=tuning)
|
||||
|
||||
C = COLORS['CYAN']
|
||||
W = COLORS['WHITE']
|
||||
@@ -217,11 +262,25 @@ def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], sca
|
||||
G = COLORS['GREEN']
|
||||
R = COLORS['RESET']
|
||||
|
||||
tor_note = " [Tor: estimates reflect nmap/probe latency]" if use_tor else ""
|
||||
masscan_modes = ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei')
|
||||
if use_tor and scan_mode in masscan_modes:
|
||||
_show_masscan_tor_warning()
|
||||
|
||||
tor_note = " [Tor: estimates reflect nmap/probe latency only]" if use_tor else ""
|
||||
print(f"\n{C}{'─' * 70}{R}")
|
||||
print(f"{C} WEBRUNNER — Cost & Time Estimate{R}")
|
||||
print(f"{W} Total: {fmt_ip_count(total_ips)} IPs {n_ports} ports Mode: {scan_mode}{R}")
|
||||
print(f"{W} Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{Y}{tor_note}{R}")
|
||||
if tuning:
|
||||
bits = []
|
||||
if 'masscan_rate' in tuning:
|
||||
bits.append(f"masscan={tuning['masscan_rate']}pps")
|
||||
if 'nmap_timing' in tuning:
|
||||
bits.append(f"nmap=T{tuning['nmap_timing']}/{tuning.get('nmap_workers', 10)}w")
|
||||
if 'nuclei_rate' in tuning and scan_mode == 'masscan+nuclei':
|
||||
bits.append(f"nuclei={tuning['nuclei_rate']}rps/{tuning.get('nuclei_concurrency', 25)}c")
|
||||
if bits:
|
||||
print(f"{W} Tuning: {' · '.join(bits)}{R}")
|
||||
print(f"{C}{'─' * 70}{R}")
|
||||
print(f" {'Preset':<14} {'Nodes':>6} {'IPs/Node':>10} {'Time/Node':>12} {'Total $':>10}")
|
||||
print(f" {'─' * 56}")
|
||||
@@ -239,6 +298,7 @@ def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], sca
|
||||
|
||||
print(f"{C}{'─' * 70}{R}")
|
||||
print(f"{Y} * = recommended (Pareto-optimal: speed vs. billing minimum){R}\n")
|
||||
_show_caveats_block(scan_mode, use_tor)
|
||||
|
||||
|
||||
# ── parameter gathering ───────────────────────────────────────────────────────
|
||||
@@ -335,16 +395,23 @@ def gather_webrunner_parameters() -> dict | None:
|
||||
# Tor routing — ask before estimate so table reflects the slowdown
|
||||
tor_raw = input(f"\nRoute scans through Tor? [y/N]: ").strip().lower()
|
||||
config['use_tor'] = tor_raw in ['y', 'yes']
|
||||
if config['use_tor']:
|
||||
if scan_mode == 'masscan-only':
|
||||
print(f"{COLORS['YELLOW']} Warning: masscan uses raw sockets and bypasses proxychains — Tor has no effect in masscan-only mode.{COLORS['RESET']}")
|
||||
elif scan_mode in ('geo-scout', 'masscan+nmap'):
|
||||
print(f"{COLORS['YELLOW']} Note: masscan phase bypasses Tor (raw sockets); nmap and probe phases will use Tor.{COLORS['RESET']}")
|
||||
else:
|
||||
print(f"{COLORS['YELLOW']} Tor routing enabled — all nmap traffic will use proxychains → SOCKS5 9050.{COLORS['RESET']}")
|
||||
masscan_modes = ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei')
|
||||
if config['use_tor'] and scan_mode in masscan_modes:
|
||||
_show_masscan_tor_warning()
|
||||
confirm = input(f"{COLORS['RED']} Continue with Tor enabled, knowing masscan will leak this node's IP? [y/N]: {COLORS['RESET']}").strip().lower()
|
||||
if confirm not in ['y', 'yes']:
|
||||
print(f"{COLORS['YELLOW']} Tor disabled. To get full Tor coverage, re-run with scan mode 'nmap-only'.{COLORS['RESET']}")
|
||||
config['use_tor'] = False
|
||||
elif config['use_tor']:
|
||||
print(f"{COLORS['GREEN']} Tor routing enabled — all nmap traffic will use proxychains → SOCKS5 9050.{COLORS['RESET']}")
|
||||
|
||||
# Cost / time estimate — reflects Tor throughput impact
|
||||
_show_estimate_table(total_ips, len(ports), providers, scan_mode, use_tor=config['use_tor'])
|
||||
# Advanced tuning — gather BEFORE estimate so table reflects operator choices
|
||||
default_rate = config['masscan_rate']
|
||||
tuning = _get_tuning_params(scan_mode, default_rate, vars_overrides)
|
||||
config.update(tuning)
|
||||
|
||||
# Cost / time estimate — reflects tuning + Tor throughput impact
|
||||
_show_estimate_table(total_ips, len(ports), providers, scan_mode, use_tor=config['use_tor'], tuning=tuning)
|
||||
|
||||
# Preset selection
|
||||
print(f"{COLORS['BLUE']}Select deployment preset:{COLORS['RESET']}")
|
||||
@@ -438,11 +505,6 @@ def gather_webrunner_parameters() -> dict | None:
|
||||
if config['operator_ip']:
|
||||
print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}")
|
||||
|
||||
# Advanced tuning
|
||||
default_rate = config['masscan_rate']
|
||||
tuning = _get_tuning_params(scan_mode, default_rate, vars_overrides)
|
||||
config.update(tuning)
|
||||
|
||||
# OPSEC / teardown options
|
||||
print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user