diff --git a/hate_crack/api.py b/hate_crack/api.py index 8d8deef..aa450be 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -1066,7 +1066,79 @@ def download_hashmob_rule_list(): 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}" + hashmob_rule_urls = { + "nsa64.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/nsa64.rule", + "OneRuleToRuleThemAll.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/OneRuleToRuleThemAll.rule", + "pantagrule.hashorg.v6.one.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/pantagrule.hashorg.v6.one.rule", + "_NSAKEY.v2.dive.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/_NSAKEY.v2.dive.rule", + "append_digits_and_special.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/append_digits_and_special.rule", + "best64.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/best64.rule", + "blandyuk.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/blandyuk.rule", + "blandyuk_basic.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/blandyuk_basic.rule", + "combinator.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/combinator.rule", + "dive.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/dive.rule", + "fordy10k.txt": "https://www.hashmob.net/api/v2/downloads/research/rules/fordy10k.txt", + "fordy50k.txt": "https://www.hashmob.net/api/v2/downloads/research/rules/fordy50k.txt", + "FordyBigBoy.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/FordyBigBoy.rule", + "fordyv1.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/fordyv1.rule", + "Incisive-leetspeak.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Incisive-leetspeak.rule", + "InsidePro-HashManager.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/InsidePro-HashManager.rule", + "InsidePro-PasswordsPro.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/InsidePro-PasswordsPro.rule", + "Robot-Best10.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Robot-Best10.rule", + "Robot_CurrentBestRules.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Robot_CurrentBestRules.rule", + "Robot_MyFavorite.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Robot_MyFavorite.rule", + "Robot_ReverseRules.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Robot_ReverseRules.rule", + "Robot_Top1268Rules.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Robot_Top1268Rules.rule", + "Robot_toporder.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Robot_toporder.rule", + "Top1268.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Top1268.rule", + "top_1500.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/top_1500.rule", + "top_250.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/top_250.rule", + "top_3000.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/top_3000.rule", + "top_500.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/top_500.rule", + "top_5000.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/top_5000.rule", + "top_750.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/top_750.rule", + "Fordyv2.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Fordyv2.rule", + "combinator_ngram.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/combinator_ngram.rule", + "d3ad0ne.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/d3ad0ne.rule", + "generated.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/generated.rule", + "generated2.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/generated2.rule", + "leetspeak.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/leetspeak.rule", + "oscommerce.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/oscommerce.rule", + "specific.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/specific.rule", + "T0XlC-insert_00-99_1950-2050_toprules_0_F.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/T0XlC-insert_00-99_1950-2050_toprules_0_F.rule", + "T0XlC-insert_space_and_special_0_F.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/T0XlC-insert_space_and_special_0_F.rule", + "T0XlC-insert_top_100_passwords_1_G.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/T0XlC-insert_top_100_passwords_1_G.rule", + "T0XlC.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/T0XlC.rule", + "T0XlCv1.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/T0XlCv1.rule", + "toggles1.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/toggles1.rule", + "toggles2.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/toggles2.rule", + "toggles3.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/toggles3.rule", + "toggles4.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/toggles4.rule", + "toggles5.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/toggles5.rule", + "unix-ninja-leetspeak.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/unix-ninja-leetspeak.rule", + "OneRuleToRuleThemStill.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/OneRuleToRuleThemStill.rule", + "Pantacorn.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/Pantacorn.rule", + "SuperUnicorn.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/SuperUnicorn.rule", + "buka_400k.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/buka_400k.rule", + "rockyou-30000.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/rockyou-30000.rule", + "HashMob.100k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.100k.rule", + "HashMob.10k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.10k.rule", + "HashMob.150k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.150k.rule", + "HashMob.1k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.1k.rule", + "HashMob.20k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.20k.rule", + "HashMob.50k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.50k.rule", + "HashMob.5k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.5k.rule", + "HashMob.75k.rule": "https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/HashMob.75k.rule", + "HashMob._100.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/HashMob._100.rule", + "HashMob._66.rule": "https://www.hashmob.net/api/v2/downloads/research/rules/HashMob._66.rule", + } + url = hashmob_rule_urls.get(file_name) + if not url: + print(f"[i] Hashmob rule not in pinned URL list, using prefix fallback: {file_name}") + if str(file_name).startswith("HashMob"): + url = f"https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/{file_name}" + else: + url = f"https://www.hashmob.net/api/v2/downloads/research/rules/{file_name}" api_key = get_hashmob_api_key() headers = {"api-key": api_key} if api_key else {} import time diff --git a/tests/test_hashview_cli_subcommands.py b/tests/test_hashview_cli_subcommands.py new file mode 100644 index 0000000..a1b01ca --- /dev/null +++ b/tests/test_hashview_cli_subcommands.py @@ -0,0 +1,134 @@ +import os +import sys +import pytest + +import hate_crack.main as hc_main + + +class DummyHashviewAPI: + def __init__(self, base_url, api_key, debug=False): + self.base_url = base_url + self.api_key = api_key + self.debug = debug + self.calls = [] + + def upload_cracked_hashes(self, file_path, hash_type='1000'): + self.calls.append(("upload_cracked_hashes", file_path, hash_type)) + return {"msg": "Cracked hashes uploaded", "count": 2} + + def upload_wordlist_file(self, wordlist_path, wordlist_name=None): + self.calls.append(("upload_wordlist_file", wordlist_path, wordlist_name)) + return {"msg": "Wordlist uploaded", "wordlist_id": 123} + + def download_left_hashes(self, customer_id, hashfile_id, output_file=None): + self.calls.append(("download_left_hashes", customer_id, hashfile_id, output_file)) + return {"output_file": output_file or f"left_{customer_id}_{hashfile_id}.txt", "size": 10} + + def download_found_hashes(self, customer_id, hashfile_id, output_file=None): + self.calls.append(("download_found_hashes", customer_id, hashfile_id, output_file)) + return {"output_file": output_file or f"found_{customer_id}_{hashfile_id}.txt", "size": 12} + + def upload_hashfile(self, file_path, customer_id, hash_type, file_format=5, hashfile_name=None): + self.calls.append(("upload_hashfile", file_path, customer_id, hash_type, file_format, hashfile_name)) + return {"msg": "Hashfile uploaded", "hashfile_id": 456} + + def create_job(self, name, hashfile_id, customer_id, limit_recovered=False, notify_email=True): + self.calls.append(("create_job", name, hashfile_id, customer_id, limit_recovered, notify_email)) + return {"msg": "Job created", "job_id": 789} + + +@pytest.fixture +def _patch_hashview(monkeypatch): + monkeypatch.setattr(hc_main, "HashviewAPI", DummyHashviewAPI) + hc_main.hashview_api_key = "dummy" + hc_main.hashview_url = "https://hashview.example.com" + + +def _run_main_with_args(monkeypatch, args): + monkeypatch.setattr(sys, "argv", ["hate_crack.py"] + args) + with pytest.raises(SystemExit) as excinfo: + hc_main.main() + return excinfo.value.code + + +def test_hashview_cli_upload_cracked(_patch_hashview, monkeypatch, tmp_path, capsys): + cracked_file = tmp_path / "cracked.out" + cracked_file.write_text("hash:pass\n") + code = _run_main_with_args( + monkeypatch, + [ + "hashview", + "upload-cracked", + "--file", + str(cracked_file), + "--hash-type", + "1000", + ], + ) + captured = capsys.readouterr() + assert code == 0 + assert "Cracked hashes uploaded" in captured.out + + +def test_hashview_cli_upload_wordlist(_patch_hashview, monkeypatch, tmp_path, capsys): + wordlist_file = tmp_path / "wordlist.txt" + wordlist_file.write_text("password\n") + code = _run_main_with_args( + monkeypatch, + [ + "hashview", + "upload-wordlist", + "--file", + str(wordlist_file), + "--name", + "TestWordlist", + ], + ) + captured = capsys.readouterr() + assert code == 0 + assert "Wordlist uploaded" in captured.out + + +def test_hashview_cli_download_found(_patch_hashview, monkeypatch, tmp_path, capsys): + out_file = tmp_path / "found_1_2.txt" + code = _run_main_with_args( + monkeypatch, + [ + "hashview", + "download-found", + "--customer-id", + "1", + "--hashfile-id", + "2", + "--out", + str(out_file), + ], + ) + captured = capsys.readouterr() + assert code == 0 + assert "Downloaded" in captured.out + assert str(out_file) in captured.out + + +def test_hashview_cli_upload_hashfile_job(_patch_hashview, monkeypatch, tmp_path, capsys): + hashfile = tmp_path / "hashes.txt" + hashfile.write_text("hash1\n") + code = _run_main_with_args( + monkeypatch, + [ + "hashview", + "upload-hashfile-job", + "--file", + str(hashfile), + "--customer-id", + "1", + "--hash-type", + "1000", + "--job-name", + "TestJob", + ], + ) + captured = capsys.readouterr() + assert code == 0 + assert "Hashfile uploaded" in captured.out + assert "Job created" in captured.out diff --git a/tests/test_hashview_cli_subcommands_subprocess.py b/tests/test_hashview_cli_subcommands_subprocess.py new file mode 100644 index 0000000..6e99734 --- /dev/null +++ b/tests/test_hashview_cli_subcommands_subprocess.py @@ -0,0 +1,273 @@ +import json +import os +import subprocess +import sys + +import pytest + +from hate_crack.api import HashviewAPI + + +HATE_CRACK_SCRIPT = os.path.join(os.path.dirname(__file__), "..", "hate_crack.py") +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +def _config_has_hashview_key(): + config_path = os.path.join(REPO_ROOT, "config.json") + if not os.path.isfile(config_path): + return False + try: + with open(config_path, "r", encoding="utf-8") as f: + data = json.load(f) + return bool(data.get("hashview_api_key")) + except Exception: + return False + + +def _ensure_customer_one(): + api = HashviewAPI(os.environ["HASHVIEW_URL"], os.environ["HASHVIEW_API_KEY"]) + try: + customers_result = api.list_customers() + except Exception as exc: + pytest.skip(f"Unable to list customers from HASHVIEW_URL: {exc}") + customers = customers_result.get("customers", []) if isinstance(customers_result, dict) else customers_result + if not any(int(cust.get("id", 0)) == 1 for cust in customers or []): + api.create_customer("Example Customer") + return 1 + + +@pytest.mark.parametrize( + "args", + [ + ["hashview", "upload-cracked", "--file", "dummy.out", "--hash-type", "1000"], + ["hashview", "upload-wordlist", "--file", "dummy.txt", "--name", "TestWordlist"], + ["hashview", "download-left", "--customer-id", "1", "--hashfile-id", "2"], + ["hashview", "download-found", "--customer-id", "1", "--hashfile-id", "2"], + [ + "hashview", + "upload-hashfile-job", + "--file", + "dummy_hashes.txt", + "--customer-id", + "1", + "--hash-type", + "1000", + "--job-name", + "TestJob", + ], + ], +) +def test_hashview_subcommands_require_api_key(tmp_path, args): + if _config_has_hashview_key(): + pytest.skip("config.json has hashview_api_key set; skip API-key missing checks.") + + # Ensure any dummy files referenced exist to avoid confusion if the code path changes. + for idx, arg in enumerate(args): + if arg == "--file": + path = tmp_path / args[idx + 1] + path.write_text("dummy\n") + args[idx + 1] = str(path) + + cli_cmd = [sys.executable, HATE_CRACK_SCRIPT] + args + result = subprocess.run( + cli_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=REPO_ROOT, + env={**os.environ, "PYTHONUNBUFFERED": "1"}, + ) + output = result.stdout + result.stderr + assert "Hashview API key not configured" in output + assert result.returncode == 1 + + +@pytest.mark.skipif( + os.environ.get("HASHVIEW_TEST_REAL", "").lower() not in ("1", "true", "yes"), + reason="Set HASHVIEW_TEST_REAL=1 to run live Hashview subprocess tests.", +) +def test_hashview_subcommands_live_downloads(): + required = ["HASHVIEW_URL", "HASHVIEW_API_KEY", "HASHVIEW_HASHFILE_ID"] + missing = [key for key in required if not os.environ.get(key)] + if missing: + pytest.skip(f"Missing required env vars: {', '.join(missing)}") + + env = {**os.environ, "PYTHONUNBUFFERED": "1"} + base_cmd = [sys.executable, HATE_CRACK_SCRIPT, "hashview"] + customer_id = _ensure_customer_one() + + left_cmd = base_cmd + [ + "download-left", + "--customer-id", + str(customer_id), + "--hashfile-id", + os.environ["HASHVIEW_HASHFILE_ID"], + ] + left = subprocess.run( + left_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=REPO_ROOT, + env=env, + ) + left_out = left.stdout + left.stderr + assert left.returncode == 0, left_out + assert "Downloaded" in left_out + assert "left_" in left_out + + found_cmd = base_cmd + [ + "download-found", + "--customer-id", + str(customer_id), + "--hashfile-id", + os.environ["HASHVIEW_HASHFILE_ID"], + ] + found = subprocess.run( + found_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=REPO_ROOT, + env=env, + ) + found_out = found.stdout + found.stderr + assert found.returncode == 0, found_out + assert "Downloaded" in found_out + assert "found_" in found_out + + +@pytest.mark.skipif( + os.environ.get("HASHVIEW_TEST_REAL", "").lower() not in ("1", "true", "yes"), + reason="Set HASHVIEW_TEST_REAL=1 to run live Hashview subprocess tests.", +) +def test_hashview_subcommands_live_upload_hashfile_job(tmp_path): + required = ["HASHVIEW_URL", "HASHVIEW_API_KEY", "HASHVIEW_HASH_TYPE"] + missing = [key for key in required if not os.environ.get(key)] + if missing: + pytest.skip(f"Missing required env vars: {', '.join(missing)}") + + env = {**os.environ, "PYTHONUNBUFFERED": "1"} + base_cmd = [sys.executable, HATE_CRACK_SCRIPT, "hashview"] + customer_id = _ensure_customer_one() + + hash_type = os.environ["HASHVIEW_HASH_TYPE"] + hashfile = tmp_path / "hashes.txt" + # Provide a simple hash-only line; ensure HASHVIEW_HASH_TYPE matches this format. + hashfile.write_text("5f4dcc3b5aa765d61d8327deb882cf99\n") + + cmd = base_cmd + [ + "upload-hashfile-job", + "--file", + str(hashfile), + "--customer-id", + str(customer_id), + "--hash-type", + hash_type, + "--job-name", + "TestJobSubprocess", + ] + run = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=REPO_ROOT, + env=env, + ) + output = run.stdout + run.stderr + assert run.returncode == 0, output + assert ("Hashfile uploaded" in output) or ("Hashfile added" in output) + assert ("Job created" in output) or ("Failed to add job" in output) + + +@pytest.mark.skipif( + os.environ.get("HASHVIEW_TEST_REAL", "").lower() not in ("1", "true", "yes"), + reason="Set HASHVIEW_TEST_REAL=1 to run live Hashview subprocess tests.", +) +def test_hashview_subcommands_live_upload_hashfile_job_pwdump(tmp_path): + required = ["HASHVIEW_URL", "HASHVIEW_API_KEY"] + missing = [key for key in required if not os.environ.get(key)] + if missing: + pytest.skip(f"Missing required env vars: {', '.join(missing)}") + + env = {**os.environ, "PYTHONUNBUFFERED": "1"} + base_cmd = [sys.executable, HATE_CRACK_SCRIPT, "hashview"] + customer_id = _ensure_customer_one() + + hashfile = tmp_path / "hashes_pwdump.txt" + # Pwdump format: user:RID:LM:NTLM::: + hashfile.write_text( + "user:500:aad3b435b51404eeaad3b435b51404ee:5f4dcc3b5aa765d61d8327deb882cf99:::\n" + ) + + cmd = base_cmd + [ + "upload-hashfile-job", + "--file", + str(hashfile), + "--customer-id", + str(customer_id), + "--hash-type", + "1000", + "--file-format", + "0", + "--job-name", + "TestJobSubprocessPwdump", + ] + run = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=REPO_ROOT, + env=env, + ) + output = run.stdout + run.stderr + assert run.returncode == 0, output + assert ("Hashfile uploaded" in output) or ("Hashfile added" in output) + assert ("Job created" in output) or ("Failed to add job" in output) + + +@pytest.mark.skipif( + os.environ.get("HASHVIEW_TEST_REAL", "").lower() not in ("1", "true", "yes"), + reason="Set HASHVIEW_TEST_REAL=1 to run live Hashview subprocess tests.", +) +def test_hashview_subcommands_live_upload_hashfile_job_hashonly(tmp_path): + required = ["HASHVIEW_URL", "HASHVIEW_API_KEY", "HASHVIEW_HASH_TYPE"] + missing = [key for key in required if not os.environ.get(key)] + if missing: + pytest.skip(f"Missing required env vars: {', '.join(missing)}") + + env = {**os.environ, "PYTHONUNBUFFERED": "1"} + base_cmd = [sys.executable, HATE_CRACK_SCRIPT, "hashview"] + customer_id = _ensure_customer_one() + + hash_type = os.environ["HASHVIEW_HASH_TYPE"] + hashfile = tmp_path / "hashes_hashonly.txt" + hashfile.write_text("5f4dcc3b5aa765d61d8327deb882cf99\n") + + cmd = base_cmd + [ + "upload-hashfile-job", + "--file", + str(hashfile), + "--customer-id", + str(customer_id), + "--hash-type", + hash_type, + "--file-format", + "5", + "--job-name", + "TestJobSubprocessHashOnly", + ] + run = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=REPO_ROOT, + env=env, + ) + output = run.stdout + run.stderr + assert run.returncode == 0, output + assert ("Hashfile uploaded" in output) or ("Hashfile added" in output) + assert ("Job created" in output) or ("Failed to add job" in output)