MDRS (Multi-threaded Diagnostic & Recognition Suite) is a desktop application for bulk string classification. It loads a plain-text wordlist, runs each entry through a configurable check module, and classifies results as HIT, FAIL, OTP, or ERROR in real time.
The application is built on PySide6 and ships as a self-contained standalone binary — no Python runtime or installation required on the target machine. It runs identically on Windows and macOS.
KEY FEATURES
| Feature | Details |
|---|---|
| Check modules | Config-driven, drop-in Python classes registered via REGISTRY |
| Threading | 1–20 concurrent worker threads via ThreadPoolExecutor |
| Result types | HIT, FAIL, OTP, ERROR with per-type color coding |
| Proxy support | Single proxy or rotating list, optional auth |
| Retry logic | Per-entry retries on ERROR + batch retry of all FAILs |
| Export | Hits, OTP, all results, clipboard copy |
| Pause / Resume | Non-destructive mid-run pause via threading event |
| Live stats | Hit count, fail count, errors, checks/sec, progress bar |
QUICKSTART
Get up and running in three steps:
1. Click combolist and load a .txt file of strings (one per line) 2. Select a check module from the dropdown 3. Click start
Results stream into the upper table in real time. Hits appear in the lower table. When the run completes, use the export buttons to save results.
INSTALLATION
MDRS ships as a standalone folder build — no installer, no Python required on the target machine.
WINDOWS
# Unzip the release archive MDRS-win64.zip └── MDRS/ ├── reap.exe # entry point ├── checks.py ├── worker.py └── ... (runtime libs) # Run directly — no install step MDRS\reap.exe
MACOS
# Unzip and clear quarantine unzip MDRS-macos.zip xattr -cr MDRS/ # Run ./MDRS/reap
xattr -cr MDRS/ removes the quarantine flag set by Gatekeeper. This is required for unsigned builds. The app does not make any network calls beyond what your check modules explicitly implement.
REQUIREMENTS
| Platform | Minimum OS | Arch |
|---|---|---|
| Windows | Windows 10 (build 1903+) | x86_64 |
| macOS | macOS 12 Monterey | x86_64, arm64 |
YOUR FIRST RUN
# Example wordlist: strings.txt access-production-ab123456-1234-5678-9012-000000000001 access-production-cd789012-3456-7890-1234-000000000002 access-production-ef345678-5678-9012-3456-000000000003 ... (one entry per line)
Load the file via the combolist button. The file label updates with the filename and line count. Select your check module, optionally set a parameter, then click start.
The title bar updates live: MDRS [done/total] THREADS: 20 HITS: n OTP: n
WORDLIST / COMBOLIST
MDRS accepts any UTF-8 plain-text file with one entry per line. Invalid bytes are silently ignored. Blank lines and lines consisting only of whitespace are skipped on load.
LOADING
Click the combolist button to open a file picker. Supported extensions: .txt and all files. After loading, the label below the button shows the filename and total line count.
LIMITS
There is no hard line count limit. Large files (1M+ lines) load fine but keep in mind memory usage scales linearly — each line is stored as a Python string in the self.lines list.
\n and \r characters but leading/trailing spaces are preserved. If your check module is sensitive to whitespace, pre-process your wordlist accordingly.
RELOADING
The combolist button is disabled during a run and re-enabled on completion. You can load a new file at any time when idle — the previous wordlist is replaced entirely.
CHECK MODULES
Check modules define the validation logic applied to each string in the wordlist. They are Python classes registered in checks.py via the REGISTRY dict. The UI dropdown is populated dynamically from this registry — adding a new module requires no UI changes.
STRUCTURE
# checks.py class MyCheck: name = "MY_CHECK" # internal key label = "My Check" # shown in dropdown param_hint = "enter parameter" # placeholder in param field # use "No parameter" to disable field def __init__(self, param: str): # validate param here, raise ValueError on bad input self.param = param def run(self, value: str) -> "Result": # return Result(value, "HIT"|"FAIL"|"OTP"|"ERROR", detail) ...
RESULT OBJECT
from dataclasses import dataclass @dataclass class Result: value: str # the original input string status: str # "HIT", "FAIL", "OTP", "ERROR" detail: str # human-readable info shown in table col 3
RESULT TYPES
| Status | Color | Meaning | Goes to hits table? |
|---|---|---|---|
| HIT | #00c44f | Entry satisfied the check condition | Yes |
| FAIL | #8a1a1a | Entry did not satisfy the condition | No |
| OTP | #b87800 | Secondary match (module-defined) | No |
| ERROR | #7a2020 | Exception during check execution | No |
All four result types appear in the upper (all results) table. Only HIT results appear in the lower table. FAIL entries are tracked separately for the retry fails function.
THREADING
MDRS uses a QThread + RunnerWorker architecture. The worker runs in a dedicated thread and dispatches entries to a concurrent.futures.ThreadPoolExecutor with a configurable pool size.
Results are emitted in batches back to the main thread via Qt signals, keeping the UI responsive at all thread counts. Each batch triggers a single UI update cycle rather than per-item redraws.
RECOMMENDED THREAD COUNTS
| Check Type | Recommended Threads | Reason |
|---|---|---|
| Pure local (regex, format, length) | 1–4 | CPU-bound, threads add overhead |
| DNS lookups | 8–16 | I/O-bound, benefits from concurrency |
| HTTP requests | 10–20 | High latency, maximum concurrency helps |
LEFT PANEL
The left panel contains all run configuration controls. It is 230px wide and fixed.
REGISTRY in checks.py.param_hint contains "no parameter".host:port or user:pass@host:port format.RESULTS TABLES
The right panel contains two stacked tables separated by a horizontal divider.
UPPER TABLE — ALL RESULTS
Every entry processed is shown here regardless of status. Rows are color-coded by status. The table holds a rolling window of the last 60 rows — older rows scroll off as new ones arrive.
LOWER TABLE — HITS ONLY
Only HIT results appear here. Holds the last 20 rows. Use the export hits button to save the full hit list — the table is a view window, not the complete dataset.
COLUMNS
| Column | Content |
|---|---|
| 1 (value) | The original input string from the wordlist |
| 2 (status) | HIT / FAIL / OTP / ERROR — color coded |
| 3 (detail) | Module-provided detail string (e.g. "valid format", "length 3 < min") |
STATS BAR
Displays five live counters below the tables:
PROGRESS & STATUS
The progress bar below the stats bar fills from 0% to 100% as entries are processed. It resets at the start of each run including retry runs.
The status label in the bottom-left of the results panel shows current state: ready, running..., stopping..., done in Xs - N hits, or cleared.
The window title bar mirrors key stats during a run: MDRS [done/total] THREADS: N HITS: N OTP: N
EXPORT CONTROLS
Four export actions sit in the bottom footer row of the results panel. All are disabled until a run completes and the relevant result type has at least one entry.
.txt file, one per line..txt file, one per line.value|status|detail, one per line.THREAD COUNT
The threads spinbox (1–20) sets the ThreadPoolExecutor max_workers. This value is read at run start and cannot be changed mid-run.
# worker.py — how threads_count is used with ThreadPoolExecutor(max_workers=threads_count) as pool: futures = {pool.submit(self._check_one, line): line for line in self.lines}
DELAY
Adds a time.sleep(delay_ms / 1000) call after each entry dispatch. Applied per thread, so with 20 threads and a 100ms delay the effective global rate is roughly 200 entries/sec max.
TIMEOUT
Sets the per-request network timeout passed to check modules. Check modules are responsible for honoring this value — it is passed as a parameter at run start and available via self.timeout.
detail = "timeout".
RETRIES
On ERROR, the worker retries the entry up to retries times before recording the final ERROR result. Each retry respects the configured delay.
PROXY
MDRS supports both single-proxy and proxy-list modes.
SINGLE PROXY
Enter a proxy in the left panel field and toggle proxy on:
# formats accepted
127.0.0.1:8080
192.168.1.1:3128
user:pass@10.0.0.1:8080
PROXY LIST
Click load proxies to import a .txt file of proxies, one per line. The proxy field updates to show the count of loaded proxies.
# proxies.txt — one per line
10.0.0.1:8080
10.0.0.2:8080
user:pass@10.0.0.3:3128
PROXY ROTATION
When a proxy list is loaded, the worker selects proxies in round-robin order per request. Each thread picks the next proxy from the shared list atomically.
AUTH FORMAT
user:password@host:port
# example
proxyuser:hunter2@proxy.example.com:8080
TOGGLE
The proxy off / proxy on button is a stateful toggle. Setting it to proxy off during a run takes effect on the next entry dispatch. It also calls set_proxies([]) which clears the loaded proxy list.
MODULE OVERVIEW
All check modules live in checks.py. The file exports a REGISTRY dict mapping module names to classes, and optionally a set_proxies() function used by the proxy toggle.
# checks.py structure REGISTRY = { "FORMAT_CHECK": FormatCheck, "LENGTH_RANGE": LengthRangeCheck, "REGEX_MATCH": RegexCheck, "MY_MODULE": MyModule, # add entries here } def set_proxies(proxy_list: list[str]): global _proxies _proxies = proxy_list
WRITING A MODULE
import re from dataclasses import dataclass @dataclass class Result: value: str status: str detail: str class FormatCheck: name = "FORMAT_CHECK" label = "Format Check" param_hint = "regex pattern e.g. ^access-production-" def __init__(self, param: str): if not param: raise ValueError("pattern required") self.pattern = re.compile(param) def run(self, value: str) -> Result: try: if self.pattern.search(value): return Result(value, "HIT", "pattern matched") return Result(value, "FAIL", "no match") except Exception as e: return Result(value, "ERROR", str(e))
REGISTRY
The REGISTRY dict is iterated at app startup to populate the check dropdown. Keys are internal names, values are uninstantiated classes. The UI calls cls(param_text) on run start.
PARAMETERS
Each module receives the contents of the param field as a string at instantiation. To disable the param field for a module, set param_hint to any string containing "no parameter" (case-insensitive):
param_hint = "No parameter needed" # disables the input field param_hint = "enter min:max length" # enables input with placeholder text
WORKER OVERVIEW
RunnerWorker in worker.py is a QObject moved to a QThread. It owns the ThreadPoolExecutor and is responsible for dispatching entries, collecting results, and emitting Qt signals back to the main thread.
class RunnerWorker(QObject): batch = Signal(list) # list[Result] progress = Signal(int, int) # done, total stats = Signal(int, int, int)# hits, fails, errors finished = Signal(float) # elapsed seconds
QT SIGNALS
| Signal | Payload | Frequency |
|---|---|---|
batch | list[Result] | Every N results (batched for performance) |
progress | (done: int, total: int) | With each batch |
stats | (hits, fails, errors) | With each batch |
finished | elapsed: float | Once, when run completes or is stopped |
RUN LIFECYCLE
start clicked
│
├─ validate wordlist loaded
├─ instantiate check module (raises ValueError on bad param)
├─ reset counters + clear tables
├─ create QThread + RunnerWorker
├─ connect signals
├─ thread.start()
│
│ [worker thread]
├─ ThreadPoolExecutor dispatches all entries
├─ results collected → batched → emit batch signal
├─ emit progress + stats per batch
│
├─ all futures complete (or stop() called)
├─ emit finished(elapsed)
│
└─ [main thread]
├─ re-enable buttons
├─ enable export buttons if results exist
└─ update status label + window title
PAUSE / RESUME
Pause is implemented via a threading.Event. The worker checks the event before dispatching each entry. In-flight checks (already submitted to the executor) are not interrupted — they complete normally.
# worker.py self._pause_event = threading.Event() self._pause_event.set() # set = running, clear = paused def pause(self): self._pause_event.clear() def resume(self): self._pause_event.set() # in dispatch loop self._pause_event.wait() # blocks until resumed
STOP
Calling stop() sets a _stop flag. The dispatch loop checks this flag before each entry and exits early. Already-dispatched futures are cancelled where possible; the executor is shut down with cancel_futures=True.
stopping... until all in-flight checks finish. For HTTP checks with a 10s timeout, stopping may take up to 10s.
DETAIL FIELD
The detail field of a Result is a free-form string provided by the check module. It appears in column 3 of both tables and is the value written to export files.
For HIT results, detail is typically the most useful extracted data — e.g. a validated token, matched substring, or parsed value. For FAIL and ERROR it should explain why the check failed.
# good detail strings Result(value, "HIT", "valid format — prefix matches") Result(value, "FAIL", "length 3 < minimum 8") Result(value, "OTP", "secondary pattern matched") Result(value, "ERROR", "timeout after 10s")
EXPORT FILE FORMAT
HITS / OTP FILES
# hits.txt / otp.txt — one detail string per line
valid format — prefix ok
valid format — prefix ok
secondary match
ALL RESULTS FILE
# all.txt — value|status|detail access-production-ab123456-...|HIT|valid format — prefix ok access-production-cd000000-...|FAIL|length segment mismatch access-production-ef999999-...|OTP|secondary pattern matched access-production-gh111111-...|ERROR|timeout after 10s
| as delimiter. If your detail strings contain |, split on the first two occurrences only when parsing.
RETRY FAILS
After a run completes, retry fails re-queues the internal fails_list as self.lines and calls start_run(). This is useful for:
| Scenario | How to use |
|---|---|
| Verify FAILs with a different module | Change the check dropdown, then click retry fails |
| Re-run with looser parameters | Change the param field, click retry fails |
| Re-run with proxy enabled | Enable proxy, click retry fails |
| Re-run after fixing a module bug | Edit checks.py (requires restart), then retry fails |
retry fails calls start_run() which clears all result lists including fails_list. The retry list is captured before the clear. Make sure you've exported anything you need before retrying.
BUILD & DISTRIBUTION
MDRS is compiled with Nuitka in standalone mode. The entry point is reap.py on both platforms.
WINDOWS
python -m nuitka --standalone --onefile-tempdir-spec="%TEMP%\MDRS" ^ --windows-console-mode=disable ^ --output-dir=dist ^ reap.py
MACOS
python -m nuitka --standalone \
--macos-create-app-bundle \
--output-dir=dist \
reap.py
# fix multimedia dylibs after build
install_name_tool -change @rpath/... @loader_path/... dist/reap.app/...
PLATFORM NOTES
MACOS — GATEKEEPER
xattr -cr ./MDRS/ # clear quarantine recursively
MACOS — DEVICE DETECTION
On macOS, pymobiledevice3 is used for iOS device detection via async daemon thread. The CFBundleName AppKit fix is applied at runtime to set the correct app name in the macOS menu bar.
WINDOWS — ANTIVIRUS
Nuitka standalone folder builds (not onefile) are used specifically to avoid AV heuristics that flag self-extracting archives. If your AV flags the binary, add the MDRS folder to your exclusion list.
FONT
The UI uses DejaVu Sans Mono with fallbacks to Consolas (Windows), Menlo (macOS), and Courier New. Font antialiasing is disabled for a sharper monospace rendering consistent across platforms.
PERFORMANCE TUNING
| Bottleneck | Solution |
|---|---|
| UI freezing during large runs | Results are batched — reduce batch emit frequency in worker.py if needed |
| Low throughput on local checks | Reduce threads (1–4), CPU context-switching is the bottleneck |
| High ERROR rate on HTTP checks | Increase timeout, reduce threads, add delay, enable proxy rotation |
| Memory usage on 1M+ wordlists | Split wordlist into chunks and run sequentially |
| Table rendering slow | Tables use setUpdatesEnabled(False/True) around batch inserts — already optimized |
TROUBLESHOOTING
| Problem | Cause | Fix |
|---|---|---|
| App won't open on macOS | Gatekeeper quarantine | Run xattr -cr ./MDRS/ |
| Param field disabled | Module's param_hint contains "no parameter" | Expected behavior — module takes no input |
| "Check config error" on start | Module __init__ raised ValueError | Fix the parameter value per the module's param_hint |
| All results are ERROR | Network issue, bad proxy, or module exception | Check proxy config, increase timeout, inspect module code |
| Stop button unresponsive | In-flight checks still completing | Wait — status shows "stopping..." until they finish |
| Export buttons stay disabled | No results of that type exist | Only enabled when hits/otp/all lists are non-empty |
| Retry fails does nothing | fails_list is empty | Only works if previous run had FAIL results |
| Skull animation not visible | Font rendering issue | Ensure DejaVu Sans Mono or Courier New is installed |