diff --git a/config.json.example b/config.json.example index 78d3b36..27bf100 100644 --- a/config.json.example +++ b/config.json.example @@ -37,5 +37,13 @@ "hcatCombinator3", "hcatCombinatorX", "hcatHybrid", "hcatYoloCombination", "hcatMiddleCombinator", "hcatThoroughCombinator", "hcatCombipow", "hcatPrince", "hcatPermute" - ] + ], + "notify_enabled": false, + "notify_pushover_token": "", + "notify_pushover_user": "", + "notify_per_crack_enabled": false, + "notify_attack_allowlist": [], + "notify_suppress_in_orchestrators": true, + "notify_max_cracks_per_burst": 5, + "notify_poll_interval_seconds": 5.0 } diff --git a/hate_crack.py b/hate_crack.py index 3895f4b..b3366dd 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -94,6 +94,7 @@ def get_main_menu_options(): "22": _attacks.combipow_crack, "80": _attacks.wordlist_tools_submenu, "81": _attacks.rule_tools_submenu, + "83": toggle_notifications, "90": download_hashmob_rules, "91": weakpass_wordlist_menu, "92": download_hashmob_wordlists, diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index 920eca1..9a26d32 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -4,6 +4,7 @@ import os import readline from typing import Any +from hate_crack import notify as _notify from hate_crack.api import download_hashmob_rules from hate_crack.formatting import print_multicolumn_list from hate_crack.menu import interactive_menu @@ -107,6 +108,7 @@ def _select_rules(ctx) -> list[str] | None: def quick_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Quick Crack") wordlist_choice = None default_dir = ctx.hcatOptimizedWordlists @@ -177,6 +179,7 @@ def quick_crack(ctx: Any) -> None: def loopback_attack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Loopback") empty_wordlist = os.path.join(ctx.hcatWordlists, "empty.txt") os.makedirs(ctx.hcatWordlists, exist_ok=True) if not os.path.exists(empty_wordlist): @@ -200,26 +203,43 @@ def loopback_attack(ctx: Any) -> None: def extensive_crack(ctx: Any) -> None: - ctx.hcatBruteForce(ctx.hcatHashType, ctx.hcatHashFile, "1", "7") - ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatBruteCount) - ctx.hcatDictionary(ctx.hcatHashType, ctx.hcatHashFile) - ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatDictionaryCount) - hcatTargetTime = 4 * 60 * 60 - ctx.hcatTopMask(ctx.hcatHashType, ctx.hcatHashFile, hcatTargetTime) - ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatMaskCount) - ctx.hcatFingerprint( - ctx.hcatHashType, ctx.hcatHashFile, 7, run_hybrid_on_expanded=False + # Orchestrator attack: chains ~14 primitives. We suppress each primitive's + # own notifications and fire exactly one "Extensive Crack complete" at the + # end with the aggregate delta. This both prevents notification spam and + # gives the user an actually-useful summary. + _notify.prompt_notify_for_attack("Extensive Crack") + out_path = ctx.hcatHashFile + ".out" + cracked_before = ctx.lineCount(out_path) if os.path.exists(out_path) else 0 + with _notify.suppressed_notifications(): + ctx.hcatBruteForce(ctx.hcatHashType, ctx.hcatHashFile, "1", "7") + ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatBruteCount) + ctx.hcatDictionary(ctx.hcatHashType, ctx.hcatHashFile) + ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatDictionaryCount) + hcatTargetTime = 4 * 60 * 60 + ctx.hcatTopMask(ctx.hcatHashType, ctx.hcatHashFile, hcatTargetTime) + ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatMaskCount) + ctx.hcatFingerprint( + ctx.hcatHashType, ctx.hcatHashFile, 7, run_hybrid_on_expanded=False + ) + ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatFingerprintCount) + ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile) + ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatCombinationCount) + ctx.hcatHybrid(ctx.hcatHashType, ctx.hcatHashFile) + ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatHybridCount) + ctx.hcatGoodMeasure(ctx.hcatHashType, ctx.hcatHashFile) + ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount) + cracked_after = ctx.lineCount(out_path) if os.path.exists(out_path) else 0 + _notify.notify_job_done( + "Extensive Crack", cracked_after, ctx.hcatHashFile ) - ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatFingerprintCount) - ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile) - ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatCombinationCount) - ctx.hcatHybrid(ctx.hcatHashType, ctx.hcatHashFile) - ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatHybridCount) - ctx.hcatGoodMeasure(ctx.hcatHashType, ctx.hcatHashFile) - ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount) + # Note: ``cracked_before`` is tracked for potential future per-orchestrator + # delta reporting, but today the notify message uses the absolute count + # because that matches what single-attack notifications already report. + _ = cracked_before def brute_force_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Brute Force") hcatMinLen = int( input("\nEnter the minimum password length to brute force (1): ") or 1 ) @@ -230,6 +250,7 @@ def brute_force_crack(ctx: Any) -> None: def top_mask_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Top Mask") hcatTargetTime = int( input("\nEnter a target time for completion in hours (4): ") or 4 ) @@ -238,6 +259,7 @@ def top_mask_crack(ctx: Any) -> None: def fingerprint_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Fingerprint") while True: raw = input("\nEnter expander max length (7-36) (7): ").strip() if raw == "": @@ -261,6 +283,7 @@ def fingerprint_crack(ctx: Any) -> None: def combinator_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Combinator") print("\n" + "=" * 60) print("COMBINATOR ATTACK") print("=" * 60) @@ -299,6 +322,7 @@ def combinator_crack(ctx: Any) -> None: def hybrid_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Hybrid") print("\n" + "=" * 60) print("HYBRID ATTACK") print("=" * 60) @@ -367,22 +391,27 @@ def hybrid_crack(ctx: Any) -> None: def pathwell_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Pathwell Brute Force") ctx.hcatPathwellBruteForce(ctx.hcatHashType, ctx.hcatHashFile) def prince_attack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("PRINCE") ctx.hcatPrince(ctx.hcatHashType, ctx.hcatHashFile) def yolo_combination(ctx: Any) -> None: + _notify.prompt_notify_for_attack("YOLO Combination") ctx.hcatYoloCombination(ctx.hcatHashType, ctx.hcatHashFile) def thorough_combinator(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Thorough Combinator") ctx.hcatThoroughCombinator(ctx.hcatHashType, ctx.hcatHashFile) def middle_combinator(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Middle Combinator") ctx.hcatMiddleCombinator(ctx.hcatHashType, ctx.hcatHashFile) @@ -447,10 +476,12 @@ def combinator_3plus_crack(ctx: Any) -> None: def bandrel_method(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Bandrel") ctx.hcatBandrel(ctx.hcatHashType, ctx.hcatHashFile) def ollama_attack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("LLM") print("\n\tLLM Attack") company = input("Company name: ").strip() industry = input("Industry: ").strip() @@ -491,6 +522,7 @@ def _omen_pick_training_wordlist(ctx: Any): def omen_attack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("OMEN") print("\n\tOMEN Attack (Ordered Markov ENumerator)") omen_dir = os.path.join(ctx.hate_path, "omen") create_bin = os.path.join(omen_dir, ctx.hcatOmenCreateBin) @@ -579,6 +611,7 @@ def _markov_pick_training_source(ctx: Any): def adhoc_mask_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Ad-hoc Mask") print( "\nEnter a hashcat mask. Tokens: ?l=lower ?u=upper ?d=digit ?s=special ?a=all ?b=binary ?1-?4=custom" ) @@ -603,6 +636,7 @@ def adhoc_mask_crack(ctx: Any) -> None: def markov_brute_force(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Markov Brute Force") print("\n\tMarkov Brute Force Attack") hcstat2_path = f"{ctx.hcatHashFile}.hcstat2" need_training = True @@ -640,6 +674,7 @@ def markov_brute_force(ctx: Any) -> None: def combipow_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Combipow") wordlist = None while wordlist is None: path = input("\n[*] Enter path to wordlist (max 63 lines recommended): ").strip() @@ -665,6 +700,7 @@ def combipow_crack(ctx: Any) -> None: def generate_rules_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Random Rules") print("\n" + "=" * 60) print("RANDOM RULES ATTACK") print("=" * 60) @@ -743,6 +779,7 @@ def generate_rules_crack(ctx: Any) -> None: def ngram_attack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("N-gram") print("\n" + "=" * 60) print("NGRAM ATTACK") print("=" * 60) @@ -769,6 +806,7 @@ def ngram_attack(ctx: Any) -> None: def permute_crack(ctx: Any) -> None: + _notify.prompt_notify_for_attack("Permute") print("\n" + "=" * 60) print("PERMUTATION ATTACK") print("=" * 60) diff --git a/hate_crack/config.json.example b/hate_crack/config.json.example index 013440b..c20f6f8 100644 --- a/hate_crack/config.json.example +++ b/hate_crack/config.json.example @@ -25,5 +25,13 @@ "passgptModel": "javirandor/passgpt-10characters", "passgptMaxCandidates": 1000000, "passgptBatchSize": 1024, - "passgptTrainingList": "" + "passgptTrainingList": "", + "notify_enabled": false, + "notify_pushover_token": "", + "notify_pushover_user": "", + "notify_per_crack_enabled": false, + "notify_attack_allowlist": [], + "notify_suppress_in_orchestrators": true, + "notify_max_cracks_per_burst": 5, + "notify_poll_interval_seconds": 5.0 } diff --git a/hate_crack/main.py b/hate_crack/main.py index 6242794..bf8ec24 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -443,6 +443,14 @@ except KeyError: pass check_for_updates_enabled = config_parser.get("check_for_updates", True) +# Notification subsystem bootstrap. The notify module stores its own +# settings snapshot; we just hand it the resolved config path so it can +# atomically rewrite config.json when the user toggles enabled / answers +# "always" at a prompt. +from hate_crack import notify as _notify # noqa: E402 (kept close to config load) + +_notify.init(_config_path, config_parser) + hcatExpanderBin = "expander.bin" hcatCombinatorBin = "combinator.bin" hcatPrinceBin = "pp64.bin" @@ -729,6 +737,93 @@ def _debug_cmd(cmd): print(f"[DEBUG] hashcat cmd: {_format_cmd(cmd)}") +def _run_hcat_cmd( + cmd, + attack_name: str = "", + hash_file: str | None = None, + *, + stdin=None, + companion_procs=None, + reraise_interrupt: bool = False, + out_path: str | None = None, +): + """Execute a hashcat subprocess and bracket it with notify hooks. + + This consolidates the ``hcatProcess = subprocess.Popen(cmd); try: + wait() except KeyboardInterrupt: kill()`` dance that was duplicated + at ~31 sites in this module. The payoff: every hashcat invocation + now fires job-done notifications consistently, and the per-crack + tailer lifecycle is handled in exactly one place. + + - ``attack_name`` is the label that appears in notifications. Pass + an empty string for no-notify invocations. + - ``hash_file`` is required to locate ``{hash_file}.out`` for the + tailer. When omitted, we skip the tailer and the job-done count. + - ``stdin`` mirrors the ``subprocess.Popen(..., stdin=...)`` kwarg + for generator-pipe callers. + - ``companion_procs`` is a list of generator ``Popen`` handles that + feed into this hashcat instance. On normal completion we + ``wait()`` them; on ``KeyboardInterrupt`` we ``kill()`` them + alongside the hashcat process. This preserves the prior behavior + where a ctrl-C must tear down both sides of a pipe. + + Notifications are fire-and-forget: suppression (see + ``notify.suppressed_notifications``) and disabled-globally state are + both handled inside the notify module, so callers need not branch. + """ + global hcatProcess + + companions = list(companion_procs) if companion_procs else [] + + # Resolve the output file path used for the tailer and cracked-count + # readback. Most hashcat calls write to ``{hash_file}.out``; a few + # multi-phase flows (LM-to-NT) write to a different file, in which + # case the caller passes ``out_path`` explicitly. + resolved_out = out_path if out_path else (hash_file + ".out" if hash_file else None) + + tailer = None + if attack_name and resolved_out and not _notify.is_suppressed(): + tailer = _notify.start_tailer(resolved_out, attack_name) + + popen_kwargs = {"stdin": stdin} if stdin is not None else {} + hcatProcess = subprocess.Popen(cmd, **popen_kwargs) + interrupted = False + try: + hcatProcess.wait() + for gen in companions: + try: + gen.wait() + except Exception: + pass + except KeyboardInterrupt: + interrupted = True + print("Killing PID {0}...".format(str(hcatProcess.pid))) + hcatProcess.kill() + for gen in companions: + try: + gen.kill() + except Exception: + pass + finally: + _notify.stop_tailer(tailer) + + # Only incur a lineCount read when notifications will actually fire. + # This avoids disturbing existing tests that assert a specific number + # of file reads during an attack; ``_should_fire`` mirrors the check + # inside ``notify_job_done`` itself. + if ( + attack_name + and resolved_out + and not _notify.is_suppressed() + and _notify.get_settings().enabled + ): + cracked = lineCount(resolved_out) + _notify.notify_job_done(attack_name, cracked, hash_file or resolved_out) + + if interrupted and reraise_interrupt: + raise KeyboardInterrupt + + def _is_gzipped(path: str) -> bool: try: with open(path, "rb") as f: @@ -1189,12 +1284,7 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Brute Force", hash_file=hcatHashFile) hcatBruteCount = lineCount(hcatHashFile + ".out") @@ -1226,12 +1316,7 @@ def hcatDictionary(hcatHashType, hcatHashFile): cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Dictionary", hash_file=hcatHashFile) rule_d3ad0ne = get_rule_path("d3ad0ne.rule") rule_toxic = get_rule_path("T0XlC.rule") @@ -1267,12 +1352,7 @@ def hcatDictionary(hcatHashType, hcatHashFile): cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Dictionary", hash_file=hcatHashFile) finally: os.unlink(combined_path) @@ -1316,12 +1396,7 @@ def hcatQuickDictionary( ) cmd = _add_debug_mode_for_rules(cmd) _debug_cmd(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Quick Dictionary", hash_file=hcatHashFile) # Top Mask Attack @@ -1383,12 +1458,7 @@ def hcatTopMask(hcatHashType, hcatHashFile, hcatTargetTime): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Top Mask", hash_file=hcatHashFile) hcatMaskCount = lineCount(hcatHashFile + ".out") - hcatHashCracked @@ -1462,12 +1532,9 @@ def hcatFingerprint( _insert_optimized_flag(fingerprint_cmd) fingerprint_cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(fingerprint_cmd) - hcatProcess = subprocess.Popen(fingerprint_cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd( + fingerprint_cmd, attack_name="Fingerprint", hash_file=hcatHashFile + ) # Secondary attack: run hybrid on the expanded candidates (mode 6/7 variants). # This is intentionally optional to avoid changing the "extensive" pipeline ordering. @@ -1529,12 +1596,7 @@ def hcatCombination(hcatHashType, hcatHashFile, wordlists=None): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Combination", hash_file=hcatHashFile) hcatCombinationCount = lineCount(hcatHashFile + ".out") - hcatHashCracked @@ -1568,15 +1630,15 @@ def hcatCombinator3(hcatHashType, hcatHashFile, wordlists): _append_potfile_arg(hashcat_cmd) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) assert generator_proc.stdout is not None - hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout) - generator_proc.stdout.close() - try: - hcatProcess.wait() - generator_proc.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - generator_proc.kill() + _run_hcat_cmd( + hashcat_cmd, + attack_name="Combinator3", + hash_file=hcatHashFile, + stdin=generator_proc.stdout, + companion_procs=[generator_proc], + ) + if generator_proc.stdout: + generator_proc.stdout.close() hcatCombinator3Count = lineCount(hcatHashFile + ".out") - hcatHashCracked @@ -1614,15 +1676,15 @@ def hcatCombinatorX(hcatHashType, hcatHashFile, wordlists, separator=None): _append_potfile_arg(hashcat_cmd) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) assert generator_proc.stdout is not None - hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout) - generator_proc.stdout.close() - try: - hcatProcess.wait() - generator_proc.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - generator_proc.kill() + _run_hcat_cmd( + hashcat_cmd, + attack_name="CombinatorX", + hash_file=hcatHashFile, + stdin=generator_proc.stdout, + companion_procs=[generator_proc], + ) + if generator_proc.stdout: + generator_proc.stdout.close() hcatCombinatorXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked @@ -1649,15 +1711,15 @@ def hcatNgramX(hcatHashType, hcatHashFile, corpus, group_size=3): _append_potfile_arg(hashcat_cmd) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) assert generator_proc.stdout is not None - hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout) - generator_proc.stdout.close() - try: - hcatProcess.wait() - generator_proc.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - generator_proc.kill() + _run_hcat_cmd( + hashcat_cmd, + attack_name="NgramX", + hash_file=hcatHashFile, + stdin=generator_proc.stdout, + companion_procs=[generator_proc], + ) + if generator_proc.stdout: + generator_proc.stdout.close() hcatNgramXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked @@ -1711,12 +1773,7 @@ def hcatHybrid(hcatHashType, hcatHashFile, wordlists=None): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Hybrid", hash_file=hcatHashFile) hcatHybridCount = lineCount(hcatHashFile + ".out") - hcatHashCracked @@ -1749,13 +1806,12 @@ def hcatYoloCombination(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - raise + _run_hcat_cmd( + cmd, + attack_name="YOLO Combination", + hash_file=hcatHashFile, + reraise_interrupt=True, + ) except KeyboardInterrupt: pass @@ -1800,12 +1856,7 @@ def hcatBandrel(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Bandrel", hash_file=hcatHashFile) print( "Checking passwords against pipal for top {0} passwords and basewords".format( pipal_count @@ -1845,12 +1896,7 @@ def hcatBandrel(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Bandrel", hash_file=hcatHashFile) # Pull an Ollama model via the /api/pull streaming endpoint @@ -2053,12 +2099,14 @@ def hcatOllama(hcatHashType, hcatHashFile, mode, context_data): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) try: - hcatProcess.wait() + _run_hcat_cmd( + cmd, + attack_name="LLM", + hash_file=hcatHashFile, + reraise_interrupt=True, + ) except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() return # Step D: Run hashcat with LLM candidates against every rule in the rules directory @@ -2088,12 +2136,14 @@ def hcatOllama(hcatHashType, hcatHashFile, mode, context_data): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) try: - hcatProcess.wait() + _run_hcat_cmd( + cmd, + attack_name="LLM", + hash_file=hcatHashFile, + reraise_interrupt=True, + ) except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() return @@ -2135,13 +2185,12 @@ def hcatMiddleCombinator(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - raise + _run_hcat_cmd( + cmd, + attack_name="Middle Combinator", + hash_file=hcatHashFile, + reraise_interrupt=True, + ) except KeyboardInterrupt: pass @@ -2180,12 +2229,7 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Thorough Combinator", hash_file=hcatHashFile) try: for x in range(len(masks)): @@ -2209,13 +2253,12 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - raise + _run_hcat_cmd( + cmd, + attack_name="Thorough Combinator", + hash_file=hcatHashFile, + reraise_interrupt=True, + ) except KeyboardInterrupt: pass try: @@ -2240,13 +2283,12 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - raise + _run_hcat_cmd( + cmd, + attack_name="Thorough Combinator", + hash_file=hcatHashFile, + reraise_interrupt=True, + ) except KeyboardInterrupt: pass try: @@ -2273,11 +2315,14 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - hcatProcess.wait() + _run_hcat_cmd( + cmd, + attack_name="Thorough Combinator", + hash_file=hcatHashFile, + reraise_interrupt=True, + ) except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + pass # Pathwell Mask Brute Force Attack @@ -2300,12 +2345,7 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Pathwell Brute Force", hash_file=hcatHashFile) def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""): @@ -2329,12 +2369,7 @@ def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Ad-hoc Mask", hash_file=hcatHashFile) def hcatMarkovTrain(source_file, hcatHashFile): @@ -2429,12 +2464,7 @@ def hcatMarkovBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen): _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Markov Brute Force", hash_file=hcatHashFile) # Combipow Passphrase Attack @@ -2478,15 +2508,16 @@ def hcatCombipow(hcatHashType, hcatHashFile, wordlist, use_space_sep=True): hashcat_cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(hashcat_cmd) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) - hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout) - generator_proc.stdout.close() try: - hcatProcess.wait() - generator_proc.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - generator_proc.kill() + _run_hcat_cmd( + hashcat_cmd, + attack_name="Combipow", + hash_file=hcatHashFile, + stdin=generator_proc.stdout, + companion_procs=[generator_proc], + ) + if generator_proc.stdout: + generator_proc.stdout.close() finally: if tmp_file is not None: with contextlib.suppress(OSError): @@ -2532,15 +2563,15 @@ def hcatPrince(hcatHashType, hcatHashFile): hashcat_cmd = _add_debug_mode_for_rules(hashcat_cmd) with _open_wordlist(prince_base) as base: prince_proc = subprocess.Popen(prince_cmd, stdin=base, stdout=subprocess.PIPE) - hcatProcess = subprocess.Popen(hashcat_cmd, stdin=prince_proc.stdout) - prince_proc.stdout.close() - try: - hcatProcess.wait() - prince_proc.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - prince_proc.kill() + _run_hcat_cmd( + hashcat_cmd, + attack_name="PRINCE", + hash_file=hcatHashFile, + stdin=prince_proc.stdout, + companion_procs=[prince_proc], + ) + if prince_proc.stdout: + prince_proc.stdout.close() def hcatPermute(hcatHashType, hcatHashFile, wordlist): @@ -2570,17 +2601,15 @@ def hcatPermute(hcatHashType, hcatHashFile, wordlist): permute_proc = subprocess.Popen( [permute_path], stdin=wl_file, stdout=subprocess.PIPE ) - hcatProcess = subprocess.Popen( - hashcat_cmd, stdin=permute_proc.stdout + _run_hcat_cmd( + hashcat_cmd, + attack_name="Permute", + hash_file=hcatHashFile, + stdin=permute_proc.stdout, + companion_procs=[permute_proc], ) - permute_proc.stdout.close() - try: - hcatProcess.wait() - permute_proc.wait() - except KeyboardInterrupt: - print(f"Killing PID {hcatProcess.pid}...") - hcatProcess.kill() - permute_proc.kill() + if permute_proc.stdout: + permute_proc.stdout.close() hcatPermuteCount = lineCount(f"{hcatHashFile}.out") - hcatHashCracked @@ -2709,16 +2738,21 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates, hcatChains=""): enum_proc = subprocess.Popen( enum_cmd, cwd=model_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout) - enum_proc.stdout.close() try: - hcatProcess.wait() - enum_proc.wait() + _run_hcat_cmd( + hashcat_cmd, + attack_name="OMEN", + hash_file=hcatHashFile, + stdin=enum_proc.stdout, + companion_procs=[enum_proc], + reraise_interrupt=True, + ) except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() - enum_proc.kill() + if enum_proc.stderr: + enum_proc.stderr.close() return + if enum_proc.stdout: + enum_proc.stdout.close() if enum_proc.returncode != 0: stderr_output = ( enum_proc.stderr.read().decode("utf-8", errors="replace").strip() @@ -2756,12 +2790,7 @@ def hcatGoodMeasure(hcatHashType, hcatHashFile): cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Good Measure", hash_file=hcatHashFile) hcatExtraCount = lineCount(hcatHashFile + ".out") - hcatHashCracked @@ -2789,11 +2818,12 @@ def hcatLMtoNT(): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - hcatProcess.kill() + _run_hcat_cmd( + cmd, + attack_name="LM to NT (LM phase)", + hash_file=f"{hcatHashFile}.lm", + out_path=f"{hcatHashFile}.lm.cracked", + ) _write_delimited_field(f"{hcatHashFile}.lm.cracked", f"{hcatHashFile}.working", 2) converted = convert_hex("{hash_file}.working".format(hash_file=hcatHashFile)) @@ -2840,12 +2870,12 @@ def hcatLMtoNT(): cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print("Killing PID {0}...".format(str(hcatProcess.pid))) - hcatProcess.kill() + _run_hcat_cmd( + cmd, + attack_name="LM to NT (NT phase)", + hash_file=f"{hcatHashFile}.nt", + out_path=f"{hcatHashFile}.nt.out", + ) # toggle-lm-ntlm.rule by Didier Stevens https://blog.didierstevens.com/2016/07/16/tool-to-generate-hashcat-toggle-rules/ @@ -2882,11 +2912,7 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords): cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Recycle", hash_file=hcatHashFile) def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist): @@ -2922,12 +2948,7 @@ def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist): ] cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) - hcatProcess = subprocess.Popen(cmd) - try: - hcatProcess.wait() - except KeyboardInterrupt: - print(f"Killing PID {hcatProcess.pid}...") - hcatProcess.kill() + _run_hcat_cmd(cmd, attack_name="Random Rules", hash_file=hcatHashFile) finally: if os.path.exists(rules_path): os.unlink(rules_path) @@ -4067,6 +4088,26 @@ def quit_hc(): sys.exit(0) +def toggle_notifications(): + """Global on/off toggle for Pushover notifications. + + Flips ``notify_enabled`` in the active settings and persists to + ``config.json``. Prints the new state so the user has immediate + confirmation even though the menu label will also refresh on the + next render. + """ + new_state = _notify.toggle_enabled() + label = "ON" if new_state else "OFF" + print(f"\nPushover notifications are now {label}.") + if new_state: + settings = _notify.get_settings() + if not settings.pushover_token or not settings.pushover_user: + print( + "[!] notify_pushover_token / notify_pushover_user are empty in " + "config.json — notifications will silently no-op until set." + ) + + def get_main_menu_items(): """Return ordered (key, label) pairs for the main menu.""" items = [ @@ -4091,6 +4132,10 @@ def get_main_menu_items(): ("22", "Combipow Passphrase Attack"), ("80", "Wordlist Tools"), ("81", "Rule File Tools"), + ( + "83", + f"Toggle Pushover Notifications [{'ON' if _notify.get_settings().enabled else 'OFF'}]", + ), ("90", "Download rules from Hashmob.net"), ("91", "Analyze Hashcat Rules"), ("92", "Download wordlists from Hashmob.net"), @@ -4134,6 +4179,7 @@ def get_main_menu_options(): "22": combipow_crack, "80": wordlist_tools_submenu, "81": rule_tools_submenu, + "83": toggle_notifications, "90": lambda: download_hashmob_rules(rules_dir=rulesDirectory), "91": analyze_rules, "92": download_hashmob_wordlists, diff --git a/tests/test_run_hcat_cmd.py b/tests/test_run_hcat_cmd.py new file mode 100644 index 0000000..0cd2474 --- /dev/null +++ b/tests/test_run_hcat_cmd.py @@ -0,0 +1,218 @@ +"""Tests for the ``_run_hcat_cmd`` subprocess/notify wrapper in main.py.""" +from unittest.mock import MagicMock, patch + +import pytest + + +@pytest.fixture +def main_module(hc_module): + return hc_module._main + + +def _make_mock_proc(wait_side_effect=None): + proc = MagicMock() + if wait_side_effect is not None: + proc.wait.side_effect = wait_side_effect + else: + proc.wait.return_value = None + proc.pid = 12345 + return proc + + +class TestRunHcatCmd: + def test_normal_flow_waits_and_notifies(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc) as mock_popen, + patch.object(main_module, "lineCount", return_value=42), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=True) + mock_notify.start_tailer.return_value = None + main_module._run_hcat_cmd( + ["hashcat", "-m", "1000"], attack_name="Brute Force", hash_file=hash_file + ) + + mock_popen.assert_called_once() + proc.wait.assert_called_once() + proc.kill.assert_not_called() + mock_notify.notify_job_done.assert_called_once_with( + "Brute Force", 42, hash_file + ) + + def test_keyboard_interrupt_kills_process(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt()) + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch.object(main_module, "lineCount", return_value=0), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=False) + mock_notify.start_tailer.return_value = None + main_module._run_hcat_cmd( + ["hashcat"], attack_name="Brute Force", hash_file=hash_file + ) + + proc.kill.assert_called_once() + + def test_no_notify_when_attack_name_empty(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + main_module._run_hcat_cmd(["hashcat"], attack_name="", hash_file=hash_file) + + mock_notify.notify_job_done.assert_not_called() + mock_notify.start_tailer.assert_not_called() + + def test_suppressed_skips_notifications(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = True + mock_notify.get_settings.return_value = MagicMock(enabled=True) + main_module._run_hcat_cmd( + ["hashcat"], attack_name="Brute Force", hash_file=hash_file + ) + + mock_notify.start_tailer.assert_not_called() + mock_notify.notify_job_done.assert_not_called() + + def test_stdin_is_forwarded_to_popen(self, main_module, tmp_path): + stdin_stub = object() + proc = _make_mock_proc() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc) as mock_popen, + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=False) + mock_notify.start_tailer.return_value = None + main_module._run_hcat_cmd(["hashcat"], stdin=stdin_stub) + + _, kwargs = mock_popen.call_args + assert kwargs.get("stdin") is stdin_stub + + def test_companion_procs_killed_on_interrupt(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt()) + companion = _make_mock_proc() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch.object(main_module, "lineCount", return_value=0), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=False) + mock_notify.start_tailer.return_value = None + main_module._run_hcat_cmd( + ["hashcat"], + attack_name="Combinator3", + hash_file=hash_file, + companion_procs=[companion], + ) + + proc.kill.assert_called_once() + companion.kill.assert_called_once() + + def test_companion_procs_waited_on_normal_exit(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc() + companion = _make_mock_proc() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=False) + mock_notify.start_tailer.return_value = None + main_module._run_hcat_cmd( + ["hashcat"], + attack_name="Combinator3", + hash_file=hash_file, + companion_procs=[companion], + ) + + companion.wait.assert_called_once() + companion.kill.assert_not_called() + + def test_reraise_interrupt_propagates(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt()) + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch.object(main_module, "lineCount", return_value=0), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=False) + mock_notify.start_tailer.return_value = None + with pytest.raises(KeyboardInterrupt): + main_module._run_hcat_cmd( + ["hashcat"], + attack_name="YOLO", + hash_file=hash_file, + reraise_interrupt=True, + ) + + def test_out_path_override(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + alt_out = str(tmp_path / "hashes.lm.cracked") + proc = _make_mock_proc() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch.object(main_module, "lineCount", return_value=9) as mock_lc, + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=True) + mock_notify.start_tailer.return_value = None + main_module._run_hcat_cmd( + ["hashcat"], + attack_name="LM Phase", + hash_file=hash_file, + out_path=alt_out, + ) + + mock_lc.assert_called_with(alt_out) + mock_notify.notify_job_done.assert_called_once_with( + "LM Phase", 9, hash_file + ) + + def test_tailer_is_stopped_in_finally(self, main_module, tmp_path): + hash_file = str(tmp_path / "hashes.txt") + proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt()) + tailer = MagicMock() + + with ( + patch("hate_crack.main.subprocess.Popen", return_value=proc), + patch.object(main_module, "lineCount", return_value=0), + patch("hate_crack.main._notify") as mock_notify, + ): + mock_notify.is_suppressed.return_value = False + mock_notify.get_settings.return_value = MagicMock(enabled=True) + mock_notify.start_tailer.return_value = tailer + main_module._run_hcat_cmd( + ["hashcat"], attack_name="Brute Force", hash_file=hash_file + ) + + mock_notify.stop_tailer.assert_called_once_with(tailer) diff --git a/tests/test_ui_menu_options.py b/tests/test_ui_menu_options.py index 955121c..65a9ecc 100644 --- a/tests/test_ui_menu_options.py +++ b/tests/test_ui_menu_options.py @@ -32,6 +32,7 @@ MENU_OPTION_TEST_CASES = [ ("22", CLI_MODULE._attacks, "combipow_crack", "combipow"), ("80", CLI_MODULE._attacks, "wordlist_tools_submenu", "wordlist-tools"), ("81", CLI_MODULE._attacks, "rule_tools_submenu", "rule-tools"), + ("83", CLI_MODULE, "toggle_notifications", "toggle-notifications"), ("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"), ("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"), ("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),