From 4091780a11173af1bb9ba5ba43765743837bf773 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Mon, 26 Jan 2026 22:57:46 -0500 Subject: [PATCH] automatic rules --- README.md | 9 ++ config.json.example | 3 +- hate_crack.py | 106 +++++++++++++--- hate_crack/api.py | 289 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 348 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 19ef267..00a26b9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,15 @@ Core logic is now split into modules under `hate_crack/`: The top-level `hate_crack.py` remains the main entry point and orchestrates these modules. +------------------------------------------------------------------- +## References and Thanks + +This project depends on and is inspired by a number of external projects and services. Thanks to: + +- Hashview (http://github.com/hashview/) +- Weakpass (https://weakpass.com) +- Hashmob (https://hashmob.net) + ------------------------------------------------------------------- ## Usage `$ ./hate_crack.py` diff --git a/config.json.example b/config.json.example index 0e05c1e..0fdbb5b 100644 --- a/config.json.example +++ b/config.json.example @@ -4,6 +4,7 @@ "hcatTuning": "--force --remove", "hcatWordlists": "/Passwords/wordlists", "hcatOptimizedWordlists": "/Passwords/optimized_wordlists", + "rules_directory": "/path/to/hashcat/rules", "hcatDictionaryWordlist": ["rockyou.txt"], "hcatCombinationWordlist": ["rockyou.txt","rockyou.txt"], "hcatHybridlist": ["rockyou.txt"], @@ -19,4 +20,4 @@ "bandrel_common_basedwords": "welcome,password,p@ssword,p@$$word,changeme,letmein,summer,winter,spring,springtime,fall,autumn,monday,tuesday,wednesday,thursday,friday,saturday,sunday,january,february,march,april,may,june,july,august,september,october,november,december,christmas,easter,covid19", "hashview_url": "http://localhost:8443", "hashview_api_key": "" -} \ No newline at end of file +} diff --git a/hate_crack.py b/hate_crack.py index 0b6a290..3f433d0 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -104,6 +104,7 @@ from hate_crack.api import ( download_all_weakpass_torrents, download_hashes_from_hashview, download_hashmob_wordlists, + download_hashmob_rules, download_weakpass_torrent, extract_with_7z, ) @@ -120,6 +121,17 @@ hcatWordlists = config_parser['hcatWordlists'] hcatOptimizedWordlists = config_parser['hcatOptimizedWordlists'] hcatRules = [] +try: + rulesDirectory = config_parser['rules_directory'] +except KeyError as e: + print('{0} is not defined in config.json using defaults from config.json.example'.format(e)) + rulesDirectory = default_config.get('rules_directory') +if not rulesDirectory: + rulesDirectory = os.path.join(hcatPath, "rules") if hcatPath else os.path.join(hate_path, "rules") +rulesDirectory = os.path.expanduser(rulesDirectory) +if not os.path.isabs(rulesDirectory): + rulesDirectory = os.path.join(hate_path, rulesDirectory) + try: maxruntime = config_parser['bandrelmaxruntime'] except KeyError as e: @@ -219,6 +231,38 @@ def verify_wordlist_dir(directory, wordlist): print('Exiting. Please check configuration and try again.') return None +def get_rule_path(rule_name, fallback_dir=None): + candidates = [] + if rulesDirectory: + candidates.append(os.path.join(rulesDirectory, rule_name)) + if fallback_dir: + candidates.append(os.path.join(fallback_dir, rule_name)) + for candidate in candidates: + if os.path.isfile(candidate): + return candidate + return candidates[0] if candidates else rule_name + +def ensure_toggle_rule(): + """Ensure toggles-lm-ntlm.rule exists in the configured rules directory.""" + if not rulesDirectory: + return None + target_path = os.path.join(rulesDirectory, "toggles-lm-ntlm.rule") + if os.path.isfile(target_path): + return target_path + source_path = os.path.join(hate_path, "rules", "toggles-lm-ntlm.rule") + try: + os.makedirs(rulesDirectory, exist_ok=True) + if os.path.isfile(source_path): + with open(source_path, "r") as src, open(target_path, "w") as dst: + dst.write(src.read()) + else: + with open(target_path, "w") as dst: + dst.write("l\nu\n") + print(f"[i] Created rule file: {target_path}") + except Exception as e: + print(f"[!] Failed to create toggles-lm-ntlm.rule: {e}") + return target_path + def cleanup_wordlist_artifacts(): wordlists_dir = hcatWordlists or os.path.join(hate_path, "wordlists") if not os.path.isabs(wordlists_dir): @@ -479,15 +523,16 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen): def hcatDictionary(hcatHashType, hcatHashFile): global hcatDictionaryCount global hcatProcess + rule_best66 = get_rule_path("best66.rule") hcatProcess = subprocess.Popen( "{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {optimized_wordlists}/* " - "-r {hcatPath}/rules/best66.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( - hcatPath=hcatPath, + "-r {rule_best66} {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hcatHashType=hcatHashType, hash_file=hcatHashFile, session_name=generate_session_id(), optimized_wordlists=hcatOptimizedWordlists, + rule_best66=rule_best66, tuning=hcatTuning, hate_path=hate_path), shell=True) try: @@ -498,15 +543,16 @@ def hcatDictionary(hcatHashType, hcatHashFile): for wordlist in hcatDictionaryWordlist: + rule_d3ad0ne = get_rule_path("d3ad0ne.rule") hcatProcess = subprocess.Popen( "{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {hcatWordlist} " - "-r {hcatPath}/rules/d3ad0ne.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( - hcatPath=hcatPath, + "-r {rule_d3ad0ne} {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hcatHashType=hcatHashType, hash_file=hcatHashFile, session_name=generate_session_id(), hcatWordlist=wordlist, + rule_d3ad0ne=rule_d3ad0ne, tuning=hcatTuning, hate_path=hate_path), shell=True) try: @@ -515,16 +561,16 @@ def hcatDictionary(hcatHashType, hcatHashFile): print('Killing PID {0}...'.format(str(hcatProcess.pid))) hcatProcess.kill() - + rule_toxic = get_rule_path("T0XlC.rule") hcatProcess = subprocess.Popen( "{hcatBin} -m {hcatHashType} {hash_file} --session {session_name} -o {hash_file}.out {hcatWordlist} " - "-r {hcatPath}/rules/T0XlC.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( - hcatPath=hcatPath, + "-r {rule_toxic} {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hcatHashType=hcatHashType, hash_file=hcatHashFile, session_name=generate_session_id(), hcatWordlist=wordlist, + rule_toxic=rule_toxic, tuning=hcatTuning, hate_path=hate_path), shell=True) try: @@ -1037,16 +1083,19 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile): def hcatPrince(hcatHashType, hcatHashFile): global hcatProcess hcatHashCracked = lineCount(hcatHashFile + ".out") + prince_rules_dir = os.path.join(hate_path, "princeprocessor", "rules") + prince_rule = get_rule_path("prince_optimized.rule", fallback_dir=prince_rules_dir) hcatProcess = subprocess.Popen( "{hate_path}/princeprocessor/{prince_bin} --case-permute --elem-cnt-min=1 --elem-cnt-max=16 -c < " "{hcatPrinceBaseList} | {hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out " - "-r {hate_path}/princeprocessor/rules/prince_optimized.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( + "-r {prince_rule} {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, prince_bin=hcatPrinceBin, hash_type=hcatHashType, hash_file=hcatHashFile, session_name=generate_session_id(), hcatPrinceBaseList=hcatPrinceBaseList, + prince_rule=prince_rule, tuning=hcatTuning, hate_path=hate_path), shell=True) try: @@ -1059,16 +1108,18 @@ def hcatPrince(hcatHashType, hcatHashFile): def hcatGoodMeasure(hcatHashType, hcatHashFile): global hcatExtraCount global hcatProcess + rule_combinator = get_rule_path("combinator.rule") + rule_insidepro = get_rule_path("InsidePro-PasswordsPro.rule") hcatProcess = subprocess.Popen( - "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -r {hcatPath}/rules/combinator.rule " - "-r {hcatPath}/rules/InsidePro-PasswordsPro.rule {hcatGoodMeasureBaseList} {tuning} " - "--potfile-path={hate_path}/hashcat.pot".format( - hcatPath=hcatPath, + "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out -r {rule_combinator} " + "-r {rule_insidepro} {hcatGoodMeasureBaseList} {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_type=hcatHashType, hash_file=hcatHashFile, hcatGoodMeasureBaseList=hcatGoodMeasureBaseList, session_name=generate_session_id(), + rule_combinator=rule_combinator, + rule_insidepro=rule_insidepro, tuning=hcatTuning, hate_path=hate_path), shell=True) try: @@ -1136,10 +1187,11 @@ def hcatLMtoNT(): hcatProcess = subprocess.Popen( "{hcatBin} -m 1000 {hash_file}.nt --session {session_name} -o {hash_file}.nt.out {hash_file}.combined " - "-r {hate_path}/rules/toggles-lm-ntlm.rule {tuning} --potfile-path={hate_path}/hashcat.pot".format( + "-r {toggle_rule} {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_file=hcatHashFile, session_name=generate_session_id(), + toggle_rule=ensure_toggle_rule() or get_rule_path("toggles-lm-ntlm.rule", fallback_dir=os.path.join(hate_path, "rules")), tuning=hcatTuning, hate_path=hate_path), shell=True) try: @@ -1170,15 +1222,15 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords): with open(working_file, 'w') as f: f.write("\n".join(converted)) for rule in hcatRules: + rule_path = get_rule_path(rule) hcatProcess = subprocess.Popen( "{hcatBin} -m {hash_type} {hash_file} --session {session_name} -o {hash_file}.out {hash_file}.working " - "-r {hcatPath}/rules/{rule} {tuning} --potfile-path={hate_path}/hashcat.pot".format( - rule=rule, + "-r {rule_path} {tuning} --potfile-path={hate_path}/hashcat.pot".format( hcatBin=hcatBin, hash_type=hcatHashType, hash_file=hcatHashFile, session_name=generate_session_id(), - hcatPath=hcatPath, + rule_path=rule_path, tuning=hcatTuning, hate_path=hate_path), shell=True) try: @@ -1698,7 +1750,11 @@ def pipal(): print('Killing PID {0}...'.format(str(pipalProcess.pid))) pipalProcess.kill() print("Pipal file is at " + hcatHashFilePipal + ".pipal\n") - view_choice = input("Would you like to view (cat) the pipal output? (Y/n): ").strip().lower() + import sys + if not sys.stdin.isatty(): + view_choice = 'y' + else: + view_choice = input("Would you like to view (cat) the pipal output? (Y/n): ").strip().lower() if view_choice in ('', 'y', 'yes'): print("\n--- Pipal Output Start ---\n") with open(hcatHashFilePipal + ".pipal") as pipalfile: @@ -1802,7 +1858,7 @@ def main(): global lmHashesFound global debug_mode global hashview_url, hashview_api_key - global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists + global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists, rulesDirectory global pipalPath, maxruntime, bandrelbasewords @@ -1818,6 +1874,7 @@ def main(): parser.add_argument('--weakpass', action='store_true', help='Download wordlists from Weakpass') parser.add_argument('--rank', type=int, default=-1, help='Only show wordlists with this rank (use 0 to show all, default: >4)') parser.add_argument('--hashmob', action='store_true', help='Download wordlists from Hashmob.net') + parser.add_argument('--rules', action='store_true', help='Download rules from Hashmob.net') parser.add_argument('--cleanup', action='store_true', help='Cleanup .out files, torrents, and extract or remove .7z archives') parser.add_argument('--debug', action='store_true', help='Enable debug mode') # Removed add_common_args(parser) since config items are now only set via config file @@ -1837,6 +1894,7 @@ def main(): hcatBin=hcatBin, hcatWordlists=hcatWordlists, hcatOptimizedWordlists=hcatOptimizedWordlists, + rules_directory=rulesDirectory, pipalPath=pipalPath, maxruntime=maxruntime, bandrelbasewords=bandrelbasewords, @@ -1848,6 +1906,7 @@ def main(): hcatBin = config.hcatBin hcatWordlists = config.hcatWordlists hcatOptimizedWordlists = config.hcatOptimizedWordlists + rulesDirectory = config.rules_directory pipalPath = config.pipalPath maxruntime = config.maxruntime bandrelbasewords = config.bandrelbasewords @@ -1905,6 +1964,9 @@ def main(): if args.hashmob: download_hashmob_wordlists(print_fn=print) sys.exit(0) + if args.rules: + download_hashmob_rules(print_fn=print) + sys.exit(0) if args.hashfile and args.hashtype: hcatHashFile = resolve_path(args.hashfile) @@ -1923,7 +1985,8 @@ def main(): print("\t(1) Download hashes from Hashview") print("\t(2) Download wordlists from Weakpass") print("\t(3) Download wordlists from Hashmob.net") - print("\t(4) Exit") + print("\t(4) Download rules from Hashmob.net") + print("\t(5) Exit") choice = input("\nSelect an option: ") if choice == '1' or args.download_hashview: if not hashview_api_key: @@ -1950,6 +2013,9 @@ def main(): elif choice == '3' or args.hashmob: download_hashmob_wordlists(print_fn=print) sys.exit(0) + elif choice == '4' or args.rules: + download_hashmob_rules(print_fn=print) + sys.exit(0) else: sys.exit(0) @@ -2031,6 +2097,7 @@ def main(): "11": middle_combinator, "12": thorough_combinator, "13": bandrel_method, + "91": download_hashmob_rules, "92": weakpass_wordlist_menu, "93": download_hashmob_wordlists, "94": weakpass_wordlist_menu, @@ -2055,6 +2122,7 @@ def main(): print("\t(11) Middle Combinator Attack") print("\t(12) Thorough Combinator Attack") print("\t(13) Bandrel Methodology") + print("\n\t(91) Download rules from Hashmob.net") print("\n\t(92) Download wordlists from Weakpass") print("\t(93) Download wordlists from Hashmob.net") print("\t(94) Weakpass Wordlist Menu") diff --git a/hate_crack/api.py b/hate_crack/api.py index 724af6b..ba1e1f9 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -57,6 +57,31 @@ def get_hcat_wordlists_dir(): os.makedirs(default, exist_ok=True) return default +def get_rules_dir(): + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.abspath(os.path.join(pkg_dir, os.pardir)) + candidates = [ + os.path.join(pkg_dir, 'config.json'), + os.path.join(project_root, 'config.json') + ] + default = os.path.join(project_root, 'rules') + for config_path in candidates: + try: + if os.path.isfile(config_path): + with open(config_path) as f: + config = json.load(f) + path = config.get('rules_directory') + if path: + path = os.path.expanduser(path) + if not os.path.isabs(path): + path = os.path.join(project_root, path) + os.makedirs(path, exist_ok=True) + return path + except Exception: + continue + os.makedirs(default, exist_ok=True) + return default + def cleanup_torrent_files(directory=None): """Remove stray .torrent files from the wordlists directory on graceful exit.""" if directory is None: @@ -165,9 +190,27 @@ def download_torrent_file(torrent_url, save_dir=None, wordlist_id=None): if not os.path.isabs(save_dir): save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), save_dir) os.makedirs(save_dir, exist_ok=True) + # Optionally include hashmob_api_key in headers if present headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } + hashmob_api_key = None + # Try to get hashmob_api_key from config + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.abspath(os.path.join(pkg_dir, os.pardir)) + for cfg in (os.path.join(pkg_dir, 'config.json'), os.path.join(project_root, 'config.json')): + if os.path.isfile(cfg): + try: + with open(cfg) as f: + config = json.load(f) + key = config.get('hashmob_api_key') + if key: + hashmob_api_key = key + break + except Exception: + continue + if hashmob_api_key: + headers["api-key"] = hashmob_api_key # Resolve a filename even if a URL is provided. if not torrent_url.startswith("http"): @@ -759,47 +802,136 @@ def download_hashmob_wordlist(file_name, out_path): url = f"https://hashmob.net/api/v2/downloads/research/wordlists/{file_name}" api_key = get_hashmob_api_key() headers = {"api-key": api_key} if api_key else {} + import time + base_backoff = 256 + max_backoff = 300 + penalty_add = 2 + penalty = base_backoff + import threading + lock = getattr(download_hashmob_wordlist, '_rate_lock', None) + if lock is None: + lock = threading.Lock() + download_hashmob_wordlist._rate_lock = lock + while True: + with lock: + time.sleep(15) + try: + with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r: + if r.status_code == 429: + print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...") + time.sleep(penalty) + penalty = min(penalty + penalty_add, max_backoff) + penalty_add *= 2 + continue + if r.status_code in (301, 302, 303, 307, 308): + redirect_url = r.headers.get('Location') + if redirect_url: + print(f"Following redirect to: {redirect_url}") + return download_hashmob_wordlist(redirect_url, out_path) + print("Redirect with no Location header!") + return False + r.raise_for_status() + content_type = r.headers.get('Content-Type', '') + if 'text/plain' in content_type: + html = r.content.decode(errors='replace') + import re + match = re.search( + r"]+http-equiv=['\"]refresh['\"][^>]+content=['\"]0;url=([^'\"]+)['\"]", + html, + re.IGNORECASE + ) + if match: + real_url = match.group(1) + print(f"Found meta refresh redirect to: {real_url}") + with requests.get(real_url, stream=True, timeout=120) as r2: + r2.raise_for_status() + with open(out_path, 'wb') as f: + for chunk in r2.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + print(f"Downloaded {out_path}") + return True + print("Error: Received HTML instead of file. Possible permission or quota issue.") + return False + with open(out_path, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + print(f"Downloaded {out_path}") + return True + except Exception as e: + if hasattr(e, 'response') and getattr(e.response, 'status_code', None) == 429: + print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...") + time.sleep(penalty) + penalty = min(penalty + penalty_add, max_backoff) + penalty_add *= 2 + continue + print(f"Error downloading wordlist: {e}") + return False + + +def download_hashmob_rule_list(): + """Fetch available rules from Hashmob API v2 and print them.""" + url = "https://hashmob.net/api/v2/resource" + api_key = get_hashmob_api_key() + headers = {"api-key": api_key} if api_key else {} try: - with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r: - if r.status_code in (301, 302, 303, 307, 308): - redirect_url = r.headers.get('Location') - if redirect_url: - print(f"Following redirect to: {redirect_url}") - return download_hashmob_wordlist(redirect_url, out_path) - print("Redirect with no Location header!") - return False - r.raise_for_status() - content_type = r.headers.get('Content-Type', '') - if 'text/plain' in content_type: - html = r.content.decode(errors='replace') - import re - match = re.search( - r"]+http-equiv=['\"]refresh['\"][^>]+content=['\"]0;url=([^'\"]+)['\"]", - html, - re.IGNORECASE - ) - if match: - real_url = match.group(1) - print(f"Found meta refresh redirect to: {real_url}") - with requests.get(real_url, stream=True, timeout=120) as r2: - r2.raise_for_status() - with open(out_path, 'wb') as f: - for chunk in r2.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - print(f"Downloaded {out_path}") - return True - print("Error: Received HTML instead of file. Possible permission or quota issue.") - return False - with open(out_path, 'wb') as f: - for chunk in r.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - print(f"Downloaded {out_path}") - return True + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + data = resp.json() + rules = [r for r in data if r.get('type') in ('rule', 'official_rule')] + print("Available Hashmob Rules:") + for idx, rule in enumerate(rules): + print(f"{idx+1}. {rule.get('name', rule.get('file_name', ''))}") + return rules except Exception as e: - print(f"Error downloading wordlist: {e}") - return False + print(f"Error fetching Hashmob rules: {e}") + return [] + + +def download_hashmob_rule(file_name, out_path): + """Download a rule file from Hashmob by file name.""" + url = f"https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/{file_name}" + api_key = get_hashmob_api_key() + headers = {"api-key": api_key} if api_key else {} + import time + base_backoff = 256 + max_backoff = 300 + penalty_add = 2 + penalty = base_backoff + import threading + lock = getattr(download_hashmob_rule, '_rate_lock', None) + if lock is None: + lock = threading.Lock() + download_hashmob_rule._rate_lock = lock + while True: + with lock: + time.sleep(15) + try: + with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r: + if r.status_code == 429: + print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...") + time.sleep(penalty) + penalty = min(penalty + penalty_add, max_backoff) + penalty_add *= 2 + continue + r.raise_for_status() + with open(out_path, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + print(f"Downloaded {out_path}") + return True + except Exception as e: + # If it's a 429 error, handle backoff, else fail + if hasattr(e, 'response') and getattr(e.response, 'status_code', None) == 429: + print(f"[!] Rate limit hit (429). Backing off for {penalty} seconds...") + time.sleep(penalty) + penalty = min(penalty + penalty_add, max_backoff) + penalty_add *= 2 + continue + print(f"Error downloading rule: {e}") + return False def list_official_wordlists(): @@ -898,6 +1030,79 @@ def list_and_download_official_wordlists(): print(f"Error listing official wordlists: {e}") +def list_and_download_hashmob_rules(): + """List rules via the Hashmob API, prompt for selection, and download.""" + rules = download_hashmob_rule_list() + if not rules: + return + print("a. Download ALL files") + sel = input("Enter the number(s) to download (e.g. 1,3,5-7), or 'a' for all, or 'q' to quit: ") + if sel.lower() == 'q': + return + rules_dir = get_rules_dir() + + def parse_indices(selection, max_index): + indices = set() + for part in selection.split(','): + part = part.strip() + if not part: + continue + if '-' in part: + try: + start, end = map(int, part.split('-', 1)) + if start > end: + start, end = end, start + indices.update(range(start, end + 1)) + except Exception: + continue + else: + try: + indices.add(int(part)) + except Exception: + continue + return sorted(i for i in indices if 1 <= i <= max_index) + + # Track already-downloaded rules to avoid duplicates + downloaded_rules = set() + # Scan rules_dir for existing files + if os.path.isdir(rules_dir): + for fname in os.listdir(rules_dir): + downloaded_rules.add(fname) + + def already_downloaded(file_name): + sanitized = sanitize_filename(file_name) + return sanitized in downloaded_rules + + if sel.lower() == 'a': + for entry in rules: + file_name = entry.get('file_name') + if not file_name: + print("No file_name found for an entry, skipping.") + continue + out_path = os.path.join(rules_dir, sanitize_filename(file_name)) + if already_downloaded(file_name): + print(f"[i] Skipping already downloaded rule: {file_name}") + continue + download_hashmob_rule(file_name, out_path) + return + + indices = parse_indices(sel, len(rules)) + if not indices: + print("No valid selection.") + return + for idx in indices: + entry = rules[idx - 1] + file_name = entry.get('file_name') + if not file_name: + print("No file_name found for selection, skipping.") + continue + out_path = os.path.join(rules_dir, sanitize_filename(file_name)) + if already_downloaded(file_name): + print(f"[i] Skipping already downloaded rule: {file_name}") + continue + download_hashmob_rule(file_name, out_path) + + def download_official_wordlist(file_name, out_path): """Download a file from the official wordlists directory with a progress bar.""" import sys @@ -995,6 +1200,12 @@ def download_hashmob_wordlists(print_fn=print) -> None: print_fn("Hashmob wordlist download complete.") +def download_hashmob_rules(print_fn=print) -> None: + """Download Hashmob rules.""" + list_and_download_hashmob_rules() + print_fn("Hashmob rules download complete.") + + def download_weakpass_torrent(download_torrent, filename: str, print_fn=print) -> None: """Download a single Weakpass torrent file by name or URL.""" print_fn(f"[i] Downloading: {filename}")