mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-03-12 21:23:05 -07:00
test: add 150 tests for attacks, wrappers, utils, api, and proxy
- tests/test_attacks_behavior.py: 29 tests for attack handler logic (loopback, extensive, top_mask, combinator, hybrid, ollama, simple pass-throughs) - tests/test_hashcat_wrappers.py: 33 tests for hashcat subprocess wrappers (brute force, quick dict, combination, hybrid, prince, recycle, good measure, etc.) - tests/test_main_utils.py: 44 tests for utility functions (_append_potfile_arg, generate_session_id, _ensure_hashfile_in_cwd, _run_hashcat_show, _dedup_netntlm_by_username, path resolution, cleanup) - tests/test_api_downloads.py: 25 tests for api.py functions (sanitize_filename, check_7z, potfile config, hashmob key, extract_with_7z, download) - tests/test_proxy.py: 18 tests for root module proxy mechanism (__getattr__, _sync_globals_to_main, _sync_callables_to_main, symbol re-export) Also fix combinator_crack to abort gracefully when hcatCombinationWordlist is a single string (only 1 wordlist configured) instead of crashing with IndexError.
This commit is contained in:
@@ -389,6 +389,12 @@ def combinator_crack(ctx: Any) -> None:
|
||||
ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists[:2]
|
||||
]
|
||||
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Combinator attack requires 2 wordlists but only 1 is configured.")
|
||||
print("Set hcatCombinationWordlist to a list of 2 paths in config.json.")
|
||||
print("Aborting combinator attack.")
|
||||
return
|
||||
|
||||
print("\nStarting combinator attack with 2 wordlists:")
|
||||
print(f" Wordlist 1: {wordlists[0]}")
|
||||
print(f" Wordlist 2: {wordlists[1]}")
|
||||
|
||||
227
tests/test_api_downloads.py
Normal file
227
tests/test_api_downloads.py
Normal file
@@ -0,0 +1,227 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from hate_crack.api import (
|
||||
check_7z,
|
||||
check_transmission_cli,
|
||||
download_hashmob_wordlist,
|
||||
extract_with_7z,
|
||||
get_hashmob_api_key,
|
||||
get_hcat_potfile_args,
|
||||
get_hcat_potfile_path,
|
||||
sanitize_filename,
|
||||
)
|
||||
|
||||
|
||||
class TestSanitizeFilename:
|
||||
def test_normal_filename_unchanged(self):
|
||||
assert sanitize_filename("rockyou.txt") == "rockyou.txt"
|
||||
|
||||
def test_spaces_become_underscores(self):
|
||||
assert sanitize_filename("my file.txt") == "my_file.txt"
|
||||
|
||||
def test_path_separators_removed(self):
|
||||
# Dots are kept; slashes are removed. "../../etc/passwd" has 4 dots, 2 slashes.
|
||||
assert sanitize_filename("../../etc/passwd") == "....etcpasswd"
|
||||
|
||||
def test_empty_string(self):
|
||||
assert sanitize_filename("") == ""
|
||||
|
||||
def test_mixed_case_preserved(self):
|
||||
assert sanitize_filename("RockYou.txt") == "RockYou.txt"
|
||||
|
||||
|
||||
class TestCheck7z:
|
||||
def test_returns_true_when_found(self, capsys):
|
||||
with patch("shutil.which", return_value="/usr/bin/7z"):
|
||||
result = check_7z()
|
||||
assert result is True
|
||||
|
||||
def test_returns_false_when_missing(self, capsys):
|
||||
with patch("shutil.which", return_value=None):
|
||||
result = check_7z()
|
||||
assert result is False
|
||||
captured = capsys.readouterr()
|
||||
assert "7z" in captured.out
|
||||
|
||||
|
||||
class TestCheckTransmissionCli:
|
||||
def test_returns_true_when_found(self):
|
||||
with patch("shutil.which", return_value="/usr/bin/transmission-cli"):
|
||||
result = check_transmission_cli()
|
||||
assert result is True
|
||||
|
||||
def test_returns_false_when_missing(self, capsys):
|
||||
with patch("shutil.which", return_value=None):
|
||||
result = check_transmission_cli()
|
||||
assert result is False
|
||||
captured = capsys.readouterr()
|
||||
assert "transmission-cli" in captured.out
|
||||
|
||||
|
||||
class TestGetHcatPotfilePath:
|
||||
def test_returns_config_value_when_set(self, tmp_path):
|
||||
config_data = {"hcatPotfilePath": "/custom/hashcat.potfile"}
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
with patch("hate_crack.api._resolve_config_path", return_value=str(config_file)):
|
||||
result = get_hcat_potfile_path()
|
||||
assert result == "/custom/hashcat.potfile"
|
||||
|
||||
def test_returns_default_when_key_missing(self, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({}))
|
||||
with patch("hate_crack.api._resolve_config_path", return_value=str(config_file)):
|
||||
result = get_hcat_potfile_path()
|
||||
assert result == os.path.expanduser("~/.hashcat/hashcat.potfile")
|
||||
|
||||
def test_returns_default_when_no_config(self):
|
||||
with patch("hate_crack.api._resolve_config_path", return_value=None):
|
||||
result = get_hcat_potfile_path()
|
||||
assert result == os.path.expanduser("~/.hashcat/hashcat.potfile")
|
||||
|
||||
def test_expands_tilde_in_config_value(self, tmp_path):
|
||||
config_data = {"hcatPotfilePath": "~/.custom/hashcat.potfile"}
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
with patch("hate_crack.api._resolve_config_path", return_value=str(config_file)):
|
||||
result = get_hcat_potfile_path()
|
||||
assert result == os.path.expanduser("~/.custom/hashcat.potfile")
|
||||
assert "~" not in result
|
||||
|
||||
|
||||
class TestGetHcatPotfileArgs:
|
||||
def test_returns_list_with_potfile_arg(self):
|
||||
with patch("hate_crack.api.get_hcat_potfile_path", return_value="/some/path/hashcat.potfile"):
|
||||
result = get_hcat_potfile_args()
|
||||
assert result == ["--potfile-path=/some/path/hashcat.potfile"]
|
||||
|
||||
def test_returns_non_empty_list_by_default(self):
|
||||
# Default path always resolves to something (expanduser never returns empty)
|
||||
with patch("hate_crack.api._resolve_config_path", return_value=None):
|
||||
result = get_hcat_potfile_args()
|
||||
assert len(result) == 1
|
||||
assert result[0].startswith("--potfile-path=")
|
||||
|
||||
|
||||
class TestGetHashmobApiKey:
|
||||
def test_returns_key_from_config(self, tmp_path):
|
||||
config_data = {"hashmob_api_key": "abc123secret"}
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
config_path = str(config_file)
|
||||
# Patch isfile so the function sees our config file as the pkg_dir config,
|
||||
# and patch open so reads come from it.
|
||||
with patch("hate_crack.api.os.path.isfile", side_effect=lambda p: p == config_path), \
|
||||
patch("hate_crack.api.os.path.dirname", return_value=str(tmp_path)), \
|
||||
patch("hate_crack.api.os.path.abspath", side_effect=lambda p: p):
|
||||
result = get_hashmob_api_key()
|
||||
assert result == "abc123secret"
|
||||
|
||||
def test_returns_none_when_missing(self, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(json.dumps({}))
|
||||
config_path = str(config_file)
|
||||
with patch("hate_crack.api.os.path.isfile", side_effect=lambda p: p == config_path), \
|
||||
patch("hate_crack.api.os.path.dirname", return_value=str(tmp_path)), \
|
||||
patch("hate_crack.api.os.path.abspath", side_effect=lambda p: p):
|
||||
result = get_hashmob_api_key()
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_when_no_config(self):
|
||||
with patch("hate_crack.api.os.path.isfile", return_value=False):
|
||||
result = get_hashmob_api_key()
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestExtractWith7z:
|
||||
def _make_run_result(self, returncode=0):
|
||||
result = MagicMock()
|
||||
result.returncode = returncode
|
||||
result.stdout = ""
|
||||
result.stderr = ""
|
||||
return result
|
||||
|
||||
def test_returns_false_when_not_installed(self, tmp_path, capsys):
|
||||
with patch("hate_crack.api.shutil.which", return_value=None):
|
||||
archive = tmp_path / "test.7z"
|
||||
archive.write_text("fake archive data")
|
||||
result = extract_with_7z(str(archive), str(tmp_path))
|
||||
assert result is False
|
||||
captured = capsys.readouterr()
|
||||
assert "7z" in captured.out
|
||||
|
||||
def test_returns_true_on_success(self, tmp_path):
|
||||
archive = tmp_path / "test.7z"
|
||||
archive.write_text("fake archive data")
|
||||
mock_result = self._make_run_result(returncode=0)
|
||||
with patch("hate_crack.api.shutil.which", return_value="/usr/bin/7z"), \
|
||||
patch("subprocess.run", return_value=mock_result):
|
||||
result = extract_with_7z(str(archive), str(tmp_path), remove_archive=False)
|
||||
assert result is True
|
||||
|
||||
def test_returns_false_on_failure(self, tmp_path):
|
||||
archive = tmp_path / "test.7z"
|
||||
archive.write_text("fake archive data")
|
||||
mock_result = self._make_run_result(returncode=1)
|
||||
with patch("hate_crack.api.shutil.which", return_value="/usr/bin/7z"), \
|
||||
patch("subprocess.run", return_value=mock_result):
|
||||
result = extract_with_7z(str(archive), str(tmp_path))
|
||||
assert result is False
|
||||
|
||||
def test_removes_archive_on_success(self, tmp_path):
|
||||
archive = tmp_path / "test.7z"
|
||||
archive.write_text("fake archive data")
|
||||
mock_result = self._make_run_result(returncode=0)
|
||||
with patch("hate_crack.api.shutil.which", return_value="/usr/bin/7z"), \
|
||||
patch("subprocess.run", return_value=mock_result):
|
||||
result = extract_with_7z(str(archive), str(tmp_path), remove_archive=True)
|
||||
assert result is True
|
||||
assert not archive.exists()
|
||||
|
||||
def test_keeps_archive_when_remove_false(self, tmp_path):
|
||||
archive = tmp_path / "test.7z"
|
||||
archive.write_text("fake archive data")
|
||||
mock_result = self._make_run_result(returncode=0)
|
||||
with patch("hate_crack.api.shutil.which", return_value="/usr/bin/7z"), \
|
||||
patch("subprocess.run", return_value=mock_result):
|
||||
result = extract_with_7z(str(archive), str(tmp_path), remove_archive=False)
|
||||
assert result is True
|
||||
assert archive.exists()
|
||||
|
||||
|
||||
class TestDownloadHashmobWordlist:
|
||||
def _make_mock_response(self, status_code=200, content=b"wordlist data"):
|
||||
mock_response = MagicMock()
|
||||
mock_response.__enter__ = lambda s: mock_response
|
||||
mock_response.__exit__ = MagicMock(return_value=False)
|
||||
mock_response.status_code = status_code
|
||||
mock_response.headers = {"Content-Type": "application/octet-stream"}
|
||||
mock_response.iter_content.return_value = [content]
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
return mock_response
|
||||
|
||||
def test_successful_download(self, tmp_path):
|
||||
mock_response = self._make_mock_response(status_code=200, content=b"wordlist data")
|
||||
out = tmp_path / "test.txt"
|
||||
with patch("hate_crack.api.requests.get", return_value=mock_response), \
|
||||
patch("hate_crack.api.time.sleep"):
|
||||
result = download_hashmob_wordlist("test.txt", str(out))
|
||||
assert result is True
|
||||
assert out.exists()
|
||||
assert out.read_bytes() == b"wordlist data"
|
||||
|
||||
def test_404_returns_false(self, tmp_path):
|
||||
import requests as req
|
||||
|
||||
mock_response = self._make_mock_response(status_code=404)
|
||||
mock_response.raise_for_status.side_effect = req.exceptions.HTTPError(
|
||||
response=MagicMock(status_code=404)
|
||||
)
|
||||
out = tmp_path / "test.txt"
|
||||
with patch("hate_crack.api.requests.get", return_value=mock_response), \
|
||||
patch("hate_crack.api.time.sleep"):
|
||||
result = download_hashmob_wordlist("test.txt", str(out))
|
||||
assert result is False
|
||||
368
tests/test_attacks_behavior.py
Normal file
368
tests/test_attacks_behavior.py
Normal file
@@ -0,0 +1,368 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from hate_crack.attacks import (
|
||||
bandrel_method,
|
||||
combinator_crack,
|
||||
extensive_crack,
|
||||
hybrid_crack,
|
||||
loopback_attack,
|
||||
middle_combinator,
|
||||
ollama_attack,
|
||||
pathwell_crack,
|
||||
prince_attack,
|
||||
thorough_combinator,
|
||||
top_mask_crack,
|
||||
yolo_combination,
|
||||
)
|
||||
|
||||
|
||||
def _make_ctx(hash_type: str = "1000", hash_file: str = "/tmp/hashes.txt") -> MagicMock:
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
return ctx
|
||||
|
||||
|
||||
class TestLoopbackAttack:
|
||||
def test_no_rules_proceeds_without_rules(self, tmp_path: Path) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
||||
ctx.rulesDirectory = str(tmp_path / "rules")
|
||||
os.makedirs(ctx.rulesDirectory, exist_ok=True)
|
||||
|
||||
# No rule files in directory -> prompts for download -> user says "n"
|
||||
# Then rule_choice becomes ["0"] via the "no rules" branch
|
||||
with (
|
||||
patch("hate_crack.attacks.download_hashmob_rules"),
|
||||
patch("builtins.input", side_effect=["n", "0"]),
|
||||
):
|
||||
loopback_attack(ctx)
|
||||
|
||||
ctx.hcatQuickDictionary.assert_called_once()
|
||||
call_kwargs = ctx.hcatQuickDictionary.call_args
|
||||
assert call_kwargs.kwargs.get("loopback") is True
|
||||
|
||||
def test_with_rule_file_calls_with_rule(self, tmp_path: Path) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
||||
ctx.rulesDirectory = str(tmp_path / "rules")
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
(rules_dir / "best66.rule").write_text("")
|
||||
|
||||
with patch("builtins.input", return_value="1"):
|
||||
loopback_attack(ctx)
|
||||
|
||||
ctx.hcatQuickDictionary.assert_called_once()
|
||||
call_args = ctx.hcatQuickDictionary.call_args
|
||||
assert call_args.kwargs.get("loopback") is True
|
||||
# Third positional arg is the rule chain string
|
||||
assert "best66.rule" in call_args[0][2]
|
||||
|
||||
def test_rule_99_returns_without_calling(self, tmp_path: Path) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
||||
ctx.rulesDirectory = str(tmp_path / "rules")
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
(rules_dir / "best66.rule").write_text("")
|
||||
|
||||
with patch("builtins.input", return_value="99"):
|
||||
loopback_attack(ctx)
|
||||
|
||||
ctx.hcatQuickDictionary.assert_not_called()
|
||||
|
||||
def test_creates_empty_wordlist_if_missing(self, tmp_path: Path) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
||||
ctx.rulesDirectory = str(tmp_path / "rules")
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
(rules_dir / "best66.rule").write_text("")
|
||||
|
||||
empty_txt = tmp_path / "wordlists" / "empty.txt"
|
||||
assert not empty_txt.exists()
|
||||
|
||||
with patch("builtins.input", return_value="1"):
|
||||
loopback_attack(ctx)
|
||||
|
||||
assert empty_txt.exists()
|
||||
|
||||
def test_empty_wordlist_passed_to_hcatQuickDictionary(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
||||
ctx.rulesDirectory = str(tmp_path / "rules")
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
(rules_dir / "best66.rule").write_text("")
|
||||
|
||||
with patch("builtins.input", return_value="1"):
|
||||
loopback_attack(ctx)
|
||||
|
||||
call_args = ctx.hcatQuickDictionary.call_args
|
||||
# Fourth positional arg is the empty wordlist path
|
||||
empty_wordlist_arg = call_args[0][3]
|
||||
assert empty_wordlist_arg.endswith("empty.txt")
|
||||
|
||||
|
||||
class TestExtensiveCrack:
|
||||
def test_calls_all_attack_methods(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
extensive_crack(ctx)
|
||||
|
||||
ctx.hcatBruteForce.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile, "1", "7")
|
||||
ctx.hcatDictionary.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
ctx.hcatTopMask.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile, 4 * 60 * 60)
|
||||
ctx.hcatFingerprint.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, 7, run_hybrid_on_expanded=False
|
||||
)
|
||||
ctx.hcatCombination.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
ctx.hcatHybrid.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
ctx.hcatGoodMeasure.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
def test_calls_recycle_after_each_attack(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
extensive_crack(ctx)
|
||||
|
||||
# extensive_crack calls hcatRecycle after: brute, dictionary, mask,
|
||||
# fingerprint, combination, hybrid, and once more at the end (hcatExtraCount)
|
||||
assert ctx.hcatRecycle.call_count == 7
|
||||
ctx.hcatRecycle.assert_any_call(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatBruteCount)
|
||||
ctx.hcatRecycle.assert_any_call(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatDictionaryCount
|
||||
)
|
||||
ctx.hcatRecycle.assert_any_call(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatMaskCount)
|
||||
ctx.hcatRecycle.assert_any_call(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatFingerprintCount
|
||||
)
|
||||
ctx.hcatRecycle.assert_any_call(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatCombinationCount
|
||||
)
|
||||
ctx.hcatRecycle.assert_any_call(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatHybridCount)
|
||||
ctx.hcatRecycle.assert_any_call(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount)
|
||||
|
||||
|
||||
class TestTopMaskCrack:
|
||||
def test_default_time_uses_four_hours(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
top_mask_crack(ctx)
|
||||
|
||||
ctx.hcatTopMask.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile, 4 * 60 * 60)
|
||||
|
||||
def test_custom_time_converts_hours_to_seconds(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
with patch("builtins.input", return_value="2"):
|
||||
top_mask_crack(ctx)
|
||||
|
||||
ctx.hcatTopMask.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile, 2 * 60 * 60)
|
||||
|
||||
def test_one_hour_input(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
with patch("builtins.input", return_value="1"):
|
||||
top_mask_crack(ctx)
|
||||
|
||||
ctx.hcatTopMask.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile, 1 * 60 * 60)
|
||||
|
||||
|
||||
class TestCombinatorCrack:
|
||||
def test_default_list_wordlist_calls_hcatCombination(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatCombinationWordlist = ["/wl/rockyou.txt", "/wl/passwords.txt"]
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
combinator_crack(ctx)
|
||||
|
||||
ctx.hcatCombination.assert_called_once_with(
|
||||
ctx.hcatHashType,
|
||||
ctx.hcatHashFile,
|
||||
["/wl/rockyou.txt", "/wl/passwords.txt"],
|
||||
)
|
||||
|
||||
def test_default_single_string_wordlist_aborts_gracefully(self, capsys) -> None:
|
||||
# When hcatCombinationWordlist is a plain string (one wordlist), the code
|
||||
# wraps it in a list giving only 1 item - the handler should abort with a
|
||||
# clear message instead of crashing with IndexError.
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatCombinationWordlist = "/wl/rockyou.txt"
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
|
||||
with patch("builtins.input", return_value="y"):
|
||||
combinator_crack(ctx)
|
||||
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
captured = capsys.readouterr()
|
||||
assert "Aborting combinator attack" in captured.out
|
||||
|
||||
def test_resolve_wordlist_path_called_for_each_wordlist(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatCombinationWordlist = ["/wl/a.txt", "/wl/b.txt"]
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
combinator_crack(ctx)
|
||||
|
||||
assert ctx._resolve_wordlist_path.call_count == 2
|
||||
ctx._resolve_wordlist_path.assert_any_call("/wl/a.txt", ctx.hcatWordlists)
|
||||
ctx._resolve_wordlist_path.assert_any_call("/wl/b.txt", ctx.hcatWordlists)
|
||||
|
||||
def test_uses_only_first_two_wordlists(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatCombinationWordlist = ["/wl/a.txt", "/wl/b.txt", "/wl/c.txt"]
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
combinator_crack(ctx)
|
||||
|
||||
call_wordlists = ctx.hcatCombination.call_args[0][2]
|
||||
assert len(call_wordlists) == 2
|
||||
assert "/wl/c.txt" not in call_wordlists
|
||||
|
||||
|
||||
class TestHybridCrack:
|
||||
def test_default_list_wordlist_calls_hcatHybrid(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatHybridlist = ["/wl/rockyou.txt"]
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
hybrid_crack(ctx)
|
||||
|
||||
ctx.hcatHybrid.assert_called_once_with(
|
||||
ctx.hcatHashType,
|
||||
ctx.hcatHashFile,
|
||||
["/wl/rockyou.txt"],
|
||||
)
|
||||
|
||||
def test_default_string_wordlist_wraps_in_list(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatHybridlist = "/wl/rockyou.txt"
|
||||
ctx._resolve_wordlist_path.side_effect = lambda wl, _: wl
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
hybrid_crack(ctx)
|
||||
|
||||
ctx.hcatHybrid.assert_called_once()
|
||||
call_wordlists = ctx.hcatHybrid.call_args[0][2]
|
||||
assert "/wl/rockyou.txt" in call_wordlists
|
||||
|
||||
def test_decline_default_aborts_when_no_selection(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
ctx.select_file_with_autocomplete.return_value = None
|
||||
|
||||
with patch("builtins.input", return_value="n"):
|
||||
hybrid_crack(ctx)
|
||||
|
||||
ctx.hcatHybrid.assert_not_called()
|
||||
|
||||
|
||||
class TestSimpleAttacks:
|
||||
def test_pathwell_crack(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
pathwell_crack(ctx)
|
||||
|
||||
ctx.hcatPathwellBruteForce.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
def test_prince_attack(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
prince_attack(ctx)
|
||||
|
||||
ctx.hcatPrince.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
def test_yolo_combination(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
yolo_combination(ctx)
|
||||
|
||||
ctx.hcatYoloCombination.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
def test_thorough_combinator(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
thorough_combinator(ctx)
|
||||
|
||||
ctx.hcatThoroughCombinator.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
def test_middle_combinator(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
middle_combinator(ctx)
|
||||
|
||||
ctx.hcatMiddleCombinator.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
def test_bandrel_method(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
bandrel_method(ctx)
|
||||
|
||||
ctx.hcatBandrel.assert_called_once_with(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
def test_pathwell_crack_passes_hash_type_and_file(self) -> None:
|
||||
ctx = _make_ctx(hash_type="500", hash_file="/data/hashes.hash")
|
||||
|
||||
pathwell_crack(ctx)
|
||||
|
||||
ctx.hcatPathwellBruteForce.assert_called_once_with("500", "/data/hashes.hash")
|
||||
|
||||
def test_prince_attack_passes_hash_type_and_file(self) -> None:
|
||||
ctx = _make_ctx(hash_type="500", hash_file="/data/hashes.hash")
|
||||
|
||||
prince_attack(ctx)
|
||||
|
||||
ctx.hcatPrince.assert_called_once_with("500", "/data/hashes.hash")
|
||||
|
||||
|
||||
class TestOllamaAttack:
|
||||
def test_calls_hcatOllama_with_context(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
with patch("builtins.input", side_effect=["ACME", "tech", "NYC"]):
|
||||
ollama_attack(ctx)
|
||||
|
||||
ctx.hcatOllama.assert_called_once_with(
|
||||
ctx.hcatHashType,
|
||||
ctx.hcatHashFile,
|
||||
"target",
|
||||
{"company": "ACME", "industry": "tech", "location": "NYC"},
|
||||
)
|
||||
|
||||
def test_passes_hash_type_and_file(self) -> None:
|
||||
ctx = _make_ctx(hash_type="1800", hash_file="/tmp/sha512.txt")
|
||||
|
||||
with patch("builtins.input", side_effect=["Corp", "finance", "London"]):
|
||||
ollama_attack(ctx)
|
||||
|
||||
call_args = ctx.hcatOllama.call_args[0]
|
||||
assert call_args[0] == "1800"
|
||||
assert call_args[1] == "/tmp/sha512.txt"
|
||||
|
||||
def test_strips_whitespace_from_inputs(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
with patch("builtins.input", side_effect=[" ACME ", " tech ", " NYC "]):
|
||||
ollama_attack(ctx)
|
||||
|
||||
target_info = ctx.hcatOllama.call_args[0][3]
|
||||
assert target_info["company"] == "ACME"
|
||||
assert target_info["industry"] == "tech"
|
||||
assert target_info["location"] == "NYC"
|
||||
|
||||
def test_target_string_is_literal_target(self) -> None:
|
||||
ctx = _make_ctx()
|
||||
|
||||
with patch("builtins.input", side_effect=["X", "Y", "Z"]):
|
||||
ollama_attack(ctx)
|
||||
|
||||
assert ctx.hcatOllama.call_args[0][2] == "target"
|
||||
624
tests/test_hashcat_wrappers.py
Normal file
624
tests/test_hashcat_wrappers.py
Normal file
@@ -0,0 +1,624 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_module(hc_module):
|
||||
"""Return the underlying hate_crack.main module for direct patching."""
|
||||
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
|
||||
|
||||
|
||||
def _common_patches(main_module, tmp_path):
|
||||
"""Return a list of patch context managers for the most common globals."""
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
return [
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatTuning", ""),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
patch.object(main_module, "hcatHashFile", hash_file, create=True),
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"),
|
||||
]
|
||||
|
||||
|
||||
class TestHcatBruteForce:
|
||||
def test_contains_attack_mode_3(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch.object(main_module, "lineCount", return_value=0):
|
||||
main_module.hcatBruteForce("1000", hash_file, 1, 7)
|
||||
|
||||
mock_popen.assert_called_once()
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "-a" in cmd
|
||||
assert "3" in cmd
|
||||
|
||||
def test_increment_flags(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch.object(main_module, "lineCount", return_value=0):
|
||||
main_module.hcatBruteForce("1000", hash_file, 3, 9)
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
|
||||
assert "--increment" in cmd
|
||||
assert "--increment-min=3" in cmd
|
||||
assert "--increment-max=9" in cmd
|
||||
|
||||
def test_keyboard_interrupt_kills_process(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc), \
|
||||
patch.object(main_module, "lineCount", return_value=0):
|
||||
main_module.hcatBruteForce("1000", hash_file, 1, 7)
|
||||
|
||||
mock_proc.kill.assert_called_once()
|
||||
|
||||
def test_hash_type_and_file_in_cmd(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch.object(main_module, "lineCount", return_value=0):
|
||||
main_module.hcatBruteForce("500", hash_file, 1, 8)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "-m" in cmd
|
||||
assert "500" in cmd
|
||||
assert hash_file in cmd
|
||||
|
||||
|
||||
class TestHcatQuickDictionary:
|
||||
def test_wordlist_added_to_cmd(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wordlist = str(tmp_path / "words.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main._debug_cmd"):
|
||||
main_module.hcatQuickDictionary("1000", hash_file, "", wordlist)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert wordlist in cmd
|
||||
|
||||
def test_loopback_flag_added(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wordlist = str(tmp_path / "words.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main._debug_cmd"):
|
||||
main_module.hcatQuickDictionary("1000", hash_file, "", wordlist, loopback=True)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "--loopback" in cmd
|
||||
|
||||
def test_no_loopback_by_default(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wordlist = str(tmp_path / "words.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main._debug_cmd"):
|
||||
main_module.hcatQuickDictionary("1000", hash_file, "", wordlist)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "--loopback" not in cmd
|
||||
|
||||
def test_chains_added_when_provided(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wordlist = str(tmp_path / "words.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main._debug_cmd"):
|
||||
main_module.hcatQuickDictionary(
|
||||
"1000", hash_file, "-r /fake/rule.rule", wordlist
|
||||
)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "-r" in cmd
|
||||
assert "/fake/rule.rule" in cmd
|
||||
|
||||
def test_list_of_wordlists_all_added(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl1 = str(tmp_path / "words1.txt")
|
||||
wl2 = str(tmp_path / "words2.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main._debug_cmd"):
|
||||
main_module.hcatQuickDictionary("1000", hash_file, "", [wl1, wl2])
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert wl1 in cmd
|
||||
assert wl2 in cmd
|
||||
|
||||
|
||||
class TestHcatCombination:
|
||||
def test_contains_attack_mode_1(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl1 = tmp_path / "words1.txt"
|
||||
wl2 = tmp_path / "words2.txt"
|
||||
wl1.write_text("word1\n")
|
||||
wl2.write_text("word2\n")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True):
|
||||
main_module.hcatCombination(
|
||||
"1000", hash_file, wordlists=[str(wl1), str(wl2)]
|
||||
)
|
||||
|
||||
mock_popen.assert_called_once()
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "-a" in cmd
|
||||
assert "1" in cmd
|
||||
|
||||
def test_aborts_with_fewer_than_two_wordlists(self, main_module, tmp_path, capsys):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl1 = tmp_path / "words1.txt"
|
||||
wl1.write_text("word1\n")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatCombination("1000", hash_file, wordlists=[str(wl1)])
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
captured = capsys.readouterr()
|
||||
assert "requires at least 2" in captured.out
|
||||
|
||||
|
||||
class TestHcatHybrid:
|
||||
def test_contains_attack_mode_6_or_7(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl = tmp_path / "words.txt"
|
||||
wl.write_text("word\n")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True):
|
||||
main_module.hcatHybrid("1000", hash_file, wordlists=[str(wl)])
|
||||
|
||||
assert mock_popen.call_count >= 1
|
||||
all_cmds = [c[0][0] for c in mock_popen.call_args_list]
|
||||
modes_used = set()
|
||||
for cmd in all_cmds:
|
||||
if "-a" in cmd:
|
||||
idx = cmd.index("-a")
|
||||
modes_used.add(cmd[idx + 1])
|
||||
assert modes_used & {"6", "7"}, f"Expected mode 6 or 7 in cmds, got modes: {modes_used}"
|
||||
|
||||
def test_aborts_when_no_valid_wordlists(self, main_module, tmp_path, capsys):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatHybrid(
|
||||
"1000", hash_file, wordlists=["/nonexistent/wordlist.txt"]
|
||||
)
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
captured = capsys.readouterr()
|
||||
assert "No valid wordlists" in captured.out
|
||||
|
||||
|
||||
class TestHcatPathwellBruteForce:
|
||||
def test_popen_called(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatPathwellBruteForce("1000", hash_file)
|
||||
|
||||
mock_popen.assert_called_once()
|
||||
|
||||
def test_attack_mode_3_in_cmd(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatPathwellBruteForce("1000", hash_file)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "-a" in cmd
|
||||
assert "3" in cmd
|
||||
|
||||
def test_pathwell_mask_in_cmd(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatPathwellBruteForce("1000", hash_file)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert any("pathwell.hcmask" in arg for arg in cmd)
|
||||
|
||||
|
||||
class TestHcatPrince:
|
||||
def test_two_popen_calls_for_pipe(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
prince_base = tmp_path / "prince_base.txt"
|
||||
prince_base.write_text("password\n")
|
||||
prince_dir = tmp_path / "princeprocessor"
|
||||
prince_dir.mkdir()
|
||||
(prince_dir / "pp64.bin").touch()
|
||||
|
||||
mock_prince_proc = MagicMock()
|
||||
mock_prince_proc.stdout = MagicMock()
|
||||
mock_prince_proc.wait.return_value = None
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 12345
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "hcatPrinceBin", "pp64.bin"), \
|
||||
patch.object(main_module, "hcatPrinceBaseList", [str(prince_base)]), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "get_rule_path", return_value="/fake/prince_optimized.rule"), \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_prince_proc, mock_hashcat_proc]
|
||||
main_module.hcatPrince("1000", hash_file)
|
||||
|
||||
assert mock_popen.call_count == 2
|
||||
|
||||
def test_prince_binary_first_call(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
prince_base = tmp_path / "prince_base.txt"
|
||||
prince_base.write_text("password\n")
|
||||
prince_dir = tmp_path / "princeprocessor"
|
||||
prince_dir.mkdir()
|
||||
pp_bin = prince_dir / "pp64.bin"
|
||||
pp_bin.touch()
|
||||
|
||||
mock_prince_proc = MagicMock()
|
||||
mock_prince_proc.stdout = MagicMock()
|
||||
mock_prince_proc.wait.return_value = None
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_hashcat_proc.pid = 12345
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "hcatPrinceBin", "pp64.bin"), \
|
||||
patch.object(main_module, "hcatPrinceBaseList", [str(prince_base)]), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "get_rule_path", return_value="/fake/prince_optimized.rule"), \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_popen.side_effect = [mock_prince_proc, mock_hashcat_proc]
|
||||
main_module.hcatPrince("1000", hash_file)
|
||||
|
||||
prince_cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert str(pp_bin) in prince_cmd
|
||||
|
||||
def test_aborts_when_prince_base_missing(self, main_module, tmp_path, capsys):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), \
|
||||
patch.object(main_module, "hcatPrinceBin", "pp64.bin"), \
|
||||
patch.object(main_module, "hcatPrinceBaseList", ["/nonexistent/base.txt"]), \
|
||||
patch.object(main_module, "get_rule_path", return_value="/fake/rule.rule"), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
main_module.hcatPrince("1000", hash_file)
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
captured = capsys.readouterr()
|
||||
assert "not found" in captured.out
|
||||
|
||||
|
||||
class TestHcatRecycle:
|
||||
def test_skipped_when_count_zero(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
|
||||
with patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
main_module.hcatRecycle("1000", hash_file, 0)
|
||||
|
||||
mock_popen.assert_not_called()
|
||||
|
||||
def test_popen_called_when_count_nonzero(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
out_file = tmp_path / "hashes.txt.out"
|
||||
out_file.write_text("hash1:password1\nhash2:password2\n")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatRules", ["best66.rule"]), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "get_rule_path", return_value="/fake/best66.rule"), \
|
||||
patch("hate_crack.main._write_delimited_field"), \
|
||||
patch("hate_crack.main.convert_hex", return_value=["password1", "password2"]), \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("builtins.open", create=True) as mock_open, \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
mock_open.return_value.__enter__ = MagicMock(return_value=MagicMock())
|
||||
mock_open.return_value.__exit__ = MagicMock(return_value=False)
|
||||
main_module.hcatRecycle("1000", hash_file, 5)
|
||||
|
||||
assert mock_popen.call_count >= 1
|
||||
|
||||
def test_rule_path_in_cmd_when_count_nonzero(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatRules", ["best66.rule"]), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "get_rule_path", return_value="/fake/best66.rule"), \
|
||||
patch("hate_crack.main._write_delimited_field"), \
|
||||
patch("hate_crack.main.convert_hex", return_value=["pass1"]), \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("builtins.open", create=True) as mock_open, \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
mock_open.return_value.__enter__ = MagicMock(return_value=MagicMock())
|
||||
mock_open.return_value.__exit__ = MagicMock(return_value=False)
|
||||
main_module.hcatRecycle("1000", hash_file, 3)
|
||||
|
||||
cmd = mock_popen.call_args[0][0]
|
||||
assert "-r" in cmd
|
||||
assert "/fake/best66.rule" in cmd
|
||||
|
||||
|
||||
class TestHcatGoodMeasure:
|
||||
def test_popen_called_at_least_once(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatGoodMeasureBaseList", "/fake/base.txt"), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "get_rule_path", return_value="/fake/rule.rule"), \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True):
|
||||
main_module.hcatGoodMeasure("1000", hash_file)
|
||||
|
||||
assert mock_popen.call_count >= 1
|
||||
|
||||
def test_hash_type_in_cmd(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatGoodMeasureBaseList", "/fake/base.txt"), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch.object(main_module, "get_rule_path", return_value="/fake/rule.rule"), \
|
||||
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
|
||||
patch.object(main_module, "lineCount", return_value=0), \
|
||||
patch.object(main_module, "hcatHashCracked", 0, create=True):
|
||||
main_module.hcatGoodMeasure("500", hash_file)
|
||||
|
||||
cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "-m" in cmd
|
||||
assert "500" in cmd
|
||||
|
||||
|
||||
class TestHcatMiddleCombinator:
|
||||
def test_popen_called_at_least_once(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatMiddleCombinatorMasks", ["!", "1"]), \
|
||||
patch.object(main_module, "hcatMiddleBaseList", "/fake/base.txt"), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatMiddleCombinator("1000", hash_file)
|
||||
|
||||
assert mock_popen.call_count >= 1
|
||||
|
||||
def test_attack_mode_1_in_cmd(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatMiddleCombinatorMasks", ["!"]), \
|
||||
patch.object(main_module, "hcatMiddleBaseList", "/fake/base.txt"), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatMiddleCombinator("1000", hash_file)
|
||||
|
||||
cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "-a" in cmd
|
||||
assert "1" in cmd
|
||||
|
||||
|
||||
class TestHcatThoroughCombinator:
|
||||
def test_popen_called_at_least_once(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
mock_proc = _make_mock_proc()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatThoroughCombinatorMasks", ["!"]), \
|
||||
patch.object(main_module, "hcatThoroughBaseList", "/fake/base.txt"), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatThoroughCombinator("1000", hash_file)
|
||||
|
||||
assert mock_popen.call_count >= 1
|
||||
|
||||
|
||||
class TestHcatYoloCombination:
|
||||
def test_popen_called_once_then_keyboard_interrupt(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl_dir = tmp_path / "wordlists"
|
||||
wl_dir.mkdir()
|
||||
(wl_dir / "words.txt").write_text("word\n")
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.pid = 12345
|
||||
mock_proc.wait.side_effect = KeyboardInterrupt()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatWordlists", str(wl_dir)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatYoloCombination("1000", hash_file)
|
||||
|
||||
assert mock_popen.call_count >= 1
|
||||
mock_proc.kill.assert_called()
|
||||
|
||||
def test_attack_mode_1_in_cmd(self, main_module, tmp_path):
|
||||
hash_file = str(tmp_path / "hashes.txt")
|
||||
wl_dir = tmp_path / "wordlists"
|
||||
wl_dir.mkdir()
|
||||
(wl_dir / "words.txt").write_text("word\n")
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.pid = 12345
|
||||
mock_proc.wait.side_effect = KeyboardInterrupt()
|
||||
|
||||
with patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatWordlists", str(wl_dir)), \
|
||||
patch.object(main_module, "generate_session_id", return_value="test_session"), \
|
||||
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
|
||||
main_module.hcatYoloCombination("1000", hash_file)
|
||||
|
||||
cmd = mock_popen.call_args_list[0][0][0]
|
||||
assert "-a" in cmd
|
||||
assert "1" in cmd
|
||||
|
||||
|
||||
class TestAppendPotfileArg:
|
||||
def test_appends_when_potfile_set(self, main_module):
|
||||
cmd = ["hashcat", "-m", "1000"]
|
||||
with patch.object(main_module, "hcatPotfilePath", "/fake/hashcat.pot"):
|
||||
main_module._append_potfile_arg(cmd)
|
||||
assert "--potfile-path=/fake/hashcat.pot" in cmd
|
||||
|
||||
def test_no_append_when_empty(self, main_module):
|
||||
cmd = ["hashcat", "-m", "1000"]
|
||||
with patch.object(main_module, "hcatPotfilePath", ""):
|
||||
main_module._append_potfile_arg(cmd)
|
||||
assert not any(arg.startswith("--potfile-path") for arg in cmd)
|
||||
|
||||
def test_no_append_when_use_potfile_false(self, main_module):
|
||||
cmd = ["hashcat", "-m", "1000"]
|
||||
with patch.object(main_module, "hcatPotfilePath", "/fake/hashcat.pot"):
|
||||
main_module._append_potfile_arg(cmd, use_potfile_path=False)
|
||||
assert not any(arg.startswith("--potfile-path") for arg in cmd)
|
||||
|
||||
def test_explicit_potfile_path_overrides_global(self, main_module):
|
||||
cmd = ["hashcat", "-m", "1000"]
|
||||
with patch.object(main_module, "hcatPotfilePath", "/global/hashcat.pot"):
|
||||
main_module._append_potfile_arg(cmd, potfile_path="/override/custom.pot")
|
||||
assert "--potfile-path=/override/custom.pot" in cmd
|
||||
assert "--potfile-path=/global/hashcat.pot" not in cmd
|
||||
480
tests/test_main_utils.py
Normal file
480
tests/test_main_utils.py
Normal file
@@ -0,0 +1,480 @@
|
||||
"""Tests for utility functions in hate_crack/main.py."""
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hate_crack.main import _dedup_netntlm_by_username
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_module(hc_module):
|
||||
return hc_module._main
|
||||
|
||||
|
||||
class TestAppendPotfileArg:
|
||||
def test_appends_potfile_path(self, main_module):
|
||||
with patch.object(main_module, "hcatPotfilePath", "/some/path"):
|
||||
cmd = []
|
||||
main_module._append_potfile_arg(cmd)
|
||||
assert "--potfile-path=/some/path" in cmd
|
||||
|
||||
def test_no_append_empty_potfile(self, main_module):
|
||||
with patch.object(main_module, "hcatPotfilePath", ""):
|
||||
cmd = []
|
||||
main_module._append_potfile_arg(cmd)
|
||||
assert cmd == []
|
||||
|
||||
def test_disabled_by_flag(self, main_module):
|
||||
with patch.object(main_module, "hcatPotfilePath", "/some/path"):
|
||||
cmd = []
|
||||
main_module._append_potfile_arg(cmd, use_potfile_path=False)
|
||||
assert cmd == []
|
||||
|
||||
def test_explicit_potfile_overrides_global(self, main_module):
|
||||
with patch.object(main_module, "hcatPotfilePath", "/global/path"):
|
||||
cmd = []
|
||||
main_module._append_potfile_arg(cmd, potfile_path="/custom/path")
|
||||
assert "--potfile-path=/custom/path" in cmd
|
||||
assert "--potfile-path=/global/path" not in cmd
|
||||
|
||||
def test_explicit_potfile_when_global_empty(self, main_module):
|
||||
with patch.object(main_module, "hcatPotfilePath", ""):
|
||||
cmd = []
|
||||
main_module._append_potfile_arg(cmd, potfile_path="/explicit/path")
|
||||
assert "--potfile-path=/explicit/path" in cmd
|
||||
|
||||
|
||||
class TestGenerateSessionId:
|
||||
def test_basic_filename(self, main_module):
|
||||
with patch("hate_crack.main.hcatHashFile", "/tmp/myfile.txt", create=True):
|
||||
result = main_module.generate_session_id()
|
||||
assert result == "myfile"
|
||||
|
||||
def test_with_hyphens_and_underscores(self, main_module):
|
||||
with patch("hate_crack.main.hcatHashFile", "/path/to/my-file_v2.txt", create=True):
|
||||
result = main_module.generate_session_id()
|
||||
assert result == "my-file_v2"
|
||||
|
||||
def test_dots_replaced(self, main_module):
|
||||
with patch("hate_crack.main.hcatHashFile", "/tmp/file.with.dots.txt", create=True):
|
||||
result = main_module.generate_session_id()
|
||||
assert result == "file_with_dots"
|
||||
|
||||
def test_spaces_replaced(self, main_module):
|
||||
with patch("hate_crack.main.hcatHashFile", "/tmp/file with spaces.txt", create=True):
|
||||
result = main_module.generate_session_id()
|
||||
assert result == "file_with_spaces"
|
||||
|
||||
def test_returns_nonempty_string(self, main_module):
|
||||
with patch("hate_crack.main.hcatHashFile", "/tmp/somefile.txt", create=True):
|
||||
result = main_module.generate_session_id()
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_only_safe_chars(self, main_module):
|
||||
with patch("hate_crack.main.hcatHashFile", "/tmp/f!le@na#me.txt", create=True):
|
||||
result = main_module.generate_session_id()
|
||||
import re
|
||||
assert re.fullmatch(r"[a-zA-Z0-9_-]+", result) is not None
|
||||
|
||||
|
||||
class TestEnsureHashfileInCwd:
|
||||
def test_none_returns_none(self, main_module):
|
||||
result = main_module._ensure_hashfile_in_cwd(None)
|
||||
assert result is None
|
||||
|
||||
def test_empty_string_returns_empty(self, main_module):
|
||||
result = main_module._ensure_hashfile_in_cwd("")
|
||||
assert result == ""
|
||||
|
||||
def test_relative_path_unchanged(self, main_module):
|
||||
result = main_module._ensure_hashfile_in_cwd("relative/path.txt")
|
||||
assert result == "relative/path.txt"
|
||||
|
||||
def test_already_in_cwd(self, main_module, tmp_path):
|
||||
target = tmp_path / "hashfile.txt"
|
||||
target.write_text("hashes")
|
||||
with patch("os.getcwd", return_value=str(tmp_path)):
|
||||
result = main_module._ensure_hashfile_in_cwd(str(target))
|
||||
assert result == str(target)
|
||||
|
||||
def test_different_dir_existing_file_in_cwd(self, main_module, tmp_path):
|
||||
# File exists in a different directory
|
||||
other_dir = tmp_path / "other"
|
||||
other_dir.mkdir()
|
||||
source_file = other_dir / "hashes.txt"
|
||||
source_file.write_text("hashes")
|
||||
|
||||
# A file with the same name already exists in cwd
|
||||
cwd_dir = tmp_path / "cwd"
|
||||
cwd_dir.mkdir()
|
||||
cwd_copy = cwd_dir / "hashes.txt"
|
||||
cwd_copy.write_text("cwd version")
|
||||
|
||||
with patch("os.getcwd", return_value=str(cwd_dir)):
|
||||
result = main_module._ensure_hashfile_in_cwd(str(source_file))
|
||||
assert result == str(cwd_copy)
|
||||
|
||||
def test_different_dir_creates_symlink(self, main_module, tmp_path):
|
||||
# Source file in a different directory, nothing in cwd
|
||||
other_dir = tmp_path / "other"
|
||||
other_dir.mkdir()
|
||||
source_file = other_dir / "hashes.txt"
|
||||
source_file.write_text("hashes")
|
||||
|
||||
cwd_dir = tmp_path / "cwd"
|
||||
cwd_dir.mkdir()
|
||||
|
||||
with patch("os.getcwd", return_value=str(cwd_dir)):
|
||||
result = main_module._ensure_hashfile_in_cwd(str(source_file))
|
||||
|
||||
expected = str(cwd_dir / "hashes.txt")
|
||||
assert result == expected
|
||||
assert os.path.exists(expected)
|
||||
|
||||
|
||||
class TestRunHashcatShow:
|
||||
def _make_mock_result(self, stdout_bytes):
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = stdout_bytes
|
||||
return mock_result
|
||||
|
||||
def test_show_flag_present(self, main_module, tmp_path):
|
||||
mock_result = self._make_mock_result(b"")
|
||||
output = tmp_path / "out.txt"
|
||||
captured_cmd = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured_cmd.extend(cmd)
|
||||
return mock_result
|
||||
|
||||
with (
|
||||
patch("hate_crack.main.subprocess.run", side_effect=fake_run),
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
):
|
||||
main_module._run_hashcat_show("1000", "/tmp/h.txt", str(output))
|
||||
|
||||
assert "--show" in captured_cmd
|
||||
|
||||
def test_valid_lines_written(self, main_module, tmp_path):
|
||||
stdout = b"abc123:password\ndeadbeef:hunter2\n"
|
||||
mock_result = self._make_mock_result(stdout)
|
||||
output = tmp_path / "out.txt"
|
||||
|
||||
with (
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result),
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
):
|
||||
main_module._run_hashcat_show("1000", "/tmp/h.txt", str(output))
|
||||
|
||||
lines = output.read_text().splitlines()
|
||||
assert "abc123:password" in lines
|
||||
assert "deadbeef:hunter2" in lines
|
||||
|
||||
def test_hash_parsing_error_excluded(self, main_module, tmp_path):
|
||||
stdout = b"abc123:password\nHash parsing error: bad line\n"
|
||||
mock_result = self._make_mock_result(stdout)
|
||||
output = tmp_path / "out.txt"
|
||||
|
||||
with (
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result),
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
):
|
||||
main_module._run_hashcat_show("1000", "/tmp/h.txt", str(output))
|
||||
|
||||
content = output.read_text()
|
||||
assert "Hash parsing error" not in content
|
||||
assert "abc123:password" in content
|
||||
|
||||
def test_star_prefix_excluded(self, main_module, tmp_path):
|
||||
stdout = b"abc123:password\n* Device #1: ...\n"
|
||||
mock_result = self._make_mock_result(stdout)
|
||||
output = tmp_path / "out.txt"
|
||||
|
||||
with (
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result),
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
):
|
||||
main_module._run_hashcat_show("1000", "/tmp/h.txt", str(output))
|
||||
|
||||
content = output.read_text()
|
||||
assert "* Device" not in content
|
||||
|
||||
def test_lines_without_colon_excluded(self, main_module, tmp_path):
|
||||
stdout = b"abc123:password\nlinewithoutseparator\n"
|
||||
mock_result = self._make_mock_result(stdout)
|
||||
output = tmp_path / "out.txt"
|
||||
|
||||
with (
|
||||
patch("hate_crack.main.subprocess.run", return_value=mock_result),
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatPotfilePath", ""),
|
||||
):
|
||||
main_module._run_hashcat_show("1000", "/tmp/h.txt", str(output))
|
||||
|
||||
content = output.read_text()
|
||||
assert "linewithoutseparator" not in content
|
||||
assert "abc123:password" in content
|
||||
|
||||
def test_potfile_path_included_when_set(self, main_module, tmp_path):
|
||||
mock_result = self._make_mock_result(b"")
|
||||
output = tmp_path / "out.txt"
|
||||
captured_cmd = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured_cmd.extend(cmd)
|
||||
return mock_result
|
||||
|
||||
with (
|
||||
patch("hate_crack.main.subprocess.run", side_effect=fake_run),
|
||||
patch.object(main_module, "hcatBin", "hashcat"),
|
||||
patch.object(main_module, "hcatPotfilePath", "/my/potfile"),
|
||||
):
|
||||
main_module._run_hashcat_show("1000", "/tmp/h.txt", str(output))
|
||||
|
||||
assert any("--potfile-path=/my/potfile" in arg for arg in captured_cmd)
|
||||
|
||||
|
||||
class TestDedupNetntlmByUsername:
|
||||
def test_no_duplicates_no_output_file(self, tmp_path):
|
||||
input_file = tmp_path / "hashes.txt"
|
||||
input_file.write_text("user1::domain:challenge:response:blob\nuser2::domain:challenge:response:blob\n")
|
||||
output_file = tmp_path / "deduped.txt"
|
||||
|
||||
total, dupes = _dedup_netntlm_by_username(str(input_file), str(output_file))
|
||||
|
||||
assert total == 2
|
||||
assert dupes == 0
|
||||
assert not output_file.exists()
|
||||
|
||||
def test_duplicates_removed(self, tmp_path):
|
||||
input_file = tmp_path / "hashes.txt"
|
||||
input_file.write_text(
|
||||
"alice::domain:aaa:bbb:ccc\n"
|
||||
"bob::domain:ddd:eee:fff\n"
|
||||
"alice::domain:111:222:333\n"
|
||||
)
|
||||
output_file = tmp_path / "deduped.txt"
|
||||
|
||||
total, dupes = _dedup_netntlm_by_username(str(input_file), str(output_file))
|
||||
|
||||
assert total == 3
|
||||
assert dupes == 1
|
||||
assert output_file.exists()
|
||||
lines = output_file.read_text().splitlines()
|
||||
assert len(lines) == 2
|
||||
assert any("alice" in line for line in lines)
|
||||
assert any("bob" in line for line in lines)
|
||||
|
||||
def test_only_first_occurrence_kept(self, tmp_path):
|
||||
input_file = tmp_path / "hashes.txt"
|
||||
input_file.write_text(
|
||||
"alice::domain:first:aaa:bbb\n"
|
||||
"alice::domain:second:ccc:ddd\n"
|
||||
)
|
||||
output_file = tmp_path / "deduped.txt"
|
||||
|
||||
_dedup_netntlm_by_username(str(input_file), str(output_file))
|
||||
|
||||
content = output_file.read_text()
|
||||
assert "first" in content
|
||||
assert "second" not in content
|
||||
|
||||
def test_empty_file(self, tmp_path):
|
||||
input_file = tmp_path / "empty.txt"
|
||||
input_file.write_text("")
|
||||
output_file = tmp_path / "deduped.txt"
|
||||
|
||||
total, dupes = _dedup_netntlm_by_username(str(input_file), str(output_file))
|
||||
|
||||
assert total == 0
|
||||
assert dupes == 0
|
||||
assert not output_file.exists()
|
||||
|
||||
def test_missing_input_file(self, tmp_path):
|
||||
input_file = tmp_path / "nonexistent.txt"
|
||||
output_file = tmp_path / "deduped.txt"
|
||||
|
||||
total, dupes = _dedup_netntlm_by_username(str(input_file), str(output_file))
|
||||
|
||||
assert total == 0
|
||||
assert dupes == 0
|
||||
|
||||
def test_lines_without_delimiter(self, tmp_path):
|
||||
input_file = tmp_path / "hashes.txt"
|
||||
input_file.write_text("nodeilimiter\nnodeilimiter\n")
|
||||
output_file = tmp_path / "deduped.txt"
|
||||
|
||||
# Should not raise; whole line treated as username
|
||||
total, dupes = _dedup_netntlm_by_username(str(input_file), str(output_file))
|
||||
|
||||
assert total == 2
|
||||
assert dupes == 1
|
||||
|
||||
def test_case_insensitive_username_dedup(self, tmp_path):
|
||||
input_file = tmp_path / "hashes.txt"
|
||||
input_file.write_text("Alice::domain:aaa:bbb:ccc\nalice::domain:ddd:eee:fff\n")
|
||||
output_file = tmp_path / "deduped.txt"
|
||||
|
||||
total, dupes = _dedup_netntlm_by_username(str(input_file), str(output_file))
|
||||
|
||||
assert total == 2
|
||||
assert dupes == 1
|
||||
|
||||
|
||||
class TestResolveWordlistPath:
|
||||
def test_absolute_existing_file(self, main_module, tmp_path):
|
||||
wordlist = tmp_path / "words.txt"
|
||||
wordlist.write_text("word1\nword2\n")
|
||||
|
||||
result = main_module._resolve_wordlist_path(str(wordlist), str(tmp_path))
|
||||
|
||||
assert result == str(wordlist)
|
||||
|
||||
def test_relative_found_in_base_dir(self, main_module, tmp_path):
|
||||
wordlist = tmp_path / "words.txt"
|
||||
wordlist.write_text("word1\nword2\n")
|
||||
|
||||
result = main_module._resolve_wordlist_path("words.txt", str(tmp_path))
|
||||
|
||||
assert result == str(wordlist)
|
||||
|
||||
def test_not_found_returns_path_anyway(self, main_module, tmp_path):
|
||||
# When file not found, returns abspath of first candidate - does not raise
|
||||
result = main_module._resolve_wordlist_path("missing.txt", str(tmp_path))
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_empty_string_returns_empty(self, main_module):
|
||||
result = main_module._resolve_wordlist_path("", "/some/dir")
|
||||
|
||||
assert result == ""
|
||||
|
||||
def test_none_returns_none(self, main_module):
|
||||
result = main_module._resolve_wordlist_path(None, "/some/dir")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestGetRulePath:
|
||||
def test_found_in_rules_directory(self, main_module, tmp_path):
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
rule_file = rules_dir / "best64.rule"
|
||||
rule_file.write_text("rule content")
|
||||
|
||||
with patch.object(main_module, "rulesDirectory", str(rules_dir)):
|
||||
result = main_module.get_rule_path("best64.rule")
|
||||
|
||||
assert result == str(rule_file)
|
||||
|
||||
def test_found_in_fallback_dir(self, main_module, tmp_path):
|
||||
# rulesDirectory has no such file, fallback does
|
||||
empty_rules_dir = tmp_path / "empty_rules"
|
||||
empty_rules_dir.mkdir()
|
||||
fallback_dir = tmp_path / "fallback"
|
||||
fallback_dir.mkdir()
|
||||
rule_file = fallback_dir / "custom.rule"
|
||||
rule_file.write_text("rule content")
|
||||
|
||||
with patch.object(main_module, "rulesDirectory", str(empty_rules_dir)):
|
||||
result = main_module.get_rule_path("custom.rule", fallback_dir=str(fallback_dir))
|
||||
|
||||
assert result == str(rule_file)
|
||||
|
||||
def test_not_found_returns_first_candidate(self, main_module, tmp_path):
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
|
||||
with patch.object(main_module, "rulesDirectory", str(rules_dir)):
|
||||
result = main_module.get_rule_path("nonexistent.rule")
|
||||
|
||||
assert result == str(rules_dir / "nonexistent.rule")
|
||||
|
||||
def test_no_rules_directory_no_fallback_returns_rule_name(self, main_module):
|
||||
with patch.object(main_module, "rulesDirectory", ""):
|
||||
result = main_module.get_rule_path("some.rule")
|
||||
|
||||
assert result == "some.rule"
|
||||
|
||||
def test_fallback_checked_after_rules_directory(self, main_module, tmp_path):
|
||||
# Both directories have the rule; rules_directory takes priority
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
fallback_dir = tmp_path / "fallback"
|
||||
fallback_dir.mkdir()
|
||||
(rules_dir / "priority.rule").write_text("rules version")
|
||||
(fallback_dir / "priority.rule").write_text("fallback version")
|
||||
|
||||
with patch.object(main_module, "rulesDirectory", str(rules_dir)):
|
||||
result = main_module.get_rule_path("priority.rule", fallback_dir=str(fallback_dir))
|
||||
|
||||
assert result == str(rules_dir / "priority.rule")
|
||||
|
||||
|
||||
class TestCleanupWordlistArtifacts:
|
||||
def test_removes_out_files_from_cwd(self, main_module, tmp_path):
|
||||
artifact = tmp_path / "cracked.out"
|
||||
artifact.write_text("cracked passwords")
|
||||
|
||||
with (
|
||||
patch("os.getcwd", return_value=str(tmp_path)),
|
||||
patch.object(main_module, "hate_path", str(tmp_path)),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path / "wordlists")),
|
||||
):
|
||||
main_module.cleanup_wordlist_artifacts()
|
||||
|
||||
assert not artifact.exists()
|
||||
|
||||
def test_preserves_non_artifact_files(self, main_module, tmp_path):
|
||||
keeper = tmp_path / "important.txt"
|
||||
keeper.write_text("keep me")
|
||||
artifact = tmp_path / "remove.out"
|
||||
artifact.write_text("remove me")
|
||||
|
||||
with (
|
||||
patch("os.getcwd", return_value=str(tmp_path)),
|
||||
patch.object(main_module, "hate_path", str(tmp_path)),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path / "wordlists")),
|
||||
):
|
||||
main_module.cleanup_wordlist_artifacts()
|
||||
|
||||
assert keeper.exists()
|
||||
assert not artifact.exists()
|
||||
|
||||
def test_removes_out_files_from_hate_path(self, main_module, tmp_path):
|
||||
hate_dir = tmp_path / "hate_crack"
|
||||
hate_dir.mkdir()
|
||||
cwd_dir = tmp_path / "cwd"
|
||||
cwd_dir.mkdir()
|
||||
artifact = hate_dir / "session.out"
|
||||
artifact.write_text("output")
|
||||
|
||||
with (
|
||||
patch("os.getcwd", return_value=str(cwd_dir)),
|
||||
patch.object(main_module, "hate_path", str(hate_dir)),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path / "wordlists")),
|
||||
):
|
||||
main_module.cleanup_wordlist_artifacts()
|
||||
|
||||
assert not artifact.exists()
|
||||
|
||||
def test_missing_directory_does_not_raise(self, main_module, tmp_path):
|
||||
nonexistent = tmp_path / "nonexistent"
|
||||
cwd_dir = tmp_path / "cwd"
|
||||
cwd_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch("os.getcwd", return_value=str(cwd_dir)),
|
||||
patch.object(main_module, "hate_path", str(nonexistent)),
|
||||
patch.object(main_module, "hcatWordlists", str(tmp_path / "wordlists")),
|
||||
):
|
||||
# Should not raise even when directories don't exist
|
||||
main_module.cleanup_wordlist_artifacts()
|
||||
125
tests/test_proxy.py
Normal file
125
tests/test_proxy.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import pytest
|
||||
|
||||
|
||||
class TestGetAttrProxy:
|
||||
def test_known_attribute_proxied(self, hc_module):
|
||||
"""Accessing a known main.py global via hc_module returns main's value."""
|
||||
val = hc_module._main.debug_mode
|
||||
assert hc_module.debug_mode == val
|
||||
|
||||
def test_nonexistent_attribute_raises(self, hc_module):
|
||||
"""Accessing a nonexistent attribute raises AttributeError."""
|
||||
with pytest.raises(AttributeError):
|
||||
_ = hc_module.this_attribute_does_not_exist_xyz
|
||||
|
||||
def test_getattr_returns_main_function(self, hc_module):
|
||||
"""Functions defined in main.py are accessible via the proxy."""
|
||||
assert callable(hc_module.generate_session_id)
|
||||
|
||||
|
||||
class TestSyncGlobalsToMain:
|
||||
@pytest.mark.parametrize(
|
||||
"name,value",
|
||||
[
|
||||
("hcatHashType", "9999"),
|
||||
("hcatHashFile", "/tmp/synced.txt"),
|
||||
("hcatHashFileOrig", "/tmp/orig.txt"),
|
||||
("pipalPath", "/tmp/pipal"),
|
||||
("pipal_count", 42),
|
||||
("debug_mode", True),
|
||||
],
|
||||
)
|
||||
def test_syncs_global(self, hc_module, name, value):
|
||||
"""Setting a synced name in hc_module.__dict__ and calling sync propagates to main."""
|
||||
hc_module.__dict__[name] = value
|
||||
hc_module._sync_globals_to_main()
|
||||
assert getattr(hc_module._main, name) == value
|
||||
|
||||
def test_only_syncs_listed_names(self, hc_module):
|
||||
"""Names not in the sync list are not pushed to main."""
|
||||
hc_module.__dict__["_random_unlisted_var"] = "should_not_sync"
|
||||
hc_module._sync_globals_to_main()
|
||||
assert getattr(hc_module._main, "_random_unlisted_var", None) != "should_not_sync"
|
||||
|
||||
def test_absent_name_skipped(self, hc_module):
|
||||
"""If a synced name is absent from hc_module globals, main is not modified."""
|
||||
hc_module.__dict__.pop("pipal_count", None)
|
||||
original = getattr(hc_module._main, "pipal_count", None)
|
||||
hc_module._sync_globals_to_main()
|
||||
assert getattr(hc_module._main, "pipal_count", None) == original
|
||||
|
||||
|
||||
class TestSyncCallablesToMain:
|
||||
def test_syncs_callable_to_main(self, hc_module):
|
||||
"""A callable set in hc_module.__dict__ is pushed to main."""
|
||||
|
||||
def fake_fn():
|
||||
return "fake"
|
||||
|
||||
hc_module.__dict__["quit_hc"] = fake_fn
|
||||
hc_module._sync_callables_to_main()
|
||||
assert hc_module._main.quit_hc is fake_fn
|
||||
|
||||
def test_syncs_show_results(self, hc_module):
|
||||
"""show_results callable is pushed to main when present."""
|
||||
|
||||
def fake_fn():
|
||||
return "results"
|
||||
|
||||
hc_module.__dict__["show_results"] = fake_fn
|
||||
hc_module._sync_callables_to_main()
|
||||
assert hc_module._main.show_results is fake_fn
|
||||
|
||||
def test_skips_when_not_in_globals(self, hc_module):
|
||||
"""If a callable name is absent from hc_module globals, main is unchanged."""
|
||||
original = getattr(hc_module._main, "show_readme", None)
|
||||
hc_module.__dict__.pop("show_readme", None)
|
||||
hc_module._sync_callables_to_main()
|
||||
assert getattr(hc_module._main, "show_readme", None) == original
|
||||
|
||||
def test_all_callable_names_sync(self, hc_module):
|
||||
"""All callable names in the sync list are pushed when present."""
|
||||
names = [
|
||||
"weakpass_wordlist_menu",
|
||||
"download_hashmob_wordlists",
|
||||
"download_hashmob_rules",
|
||||
"hashview_api",
|
||||
"export_excel",
|
||||
"show_results",
|
||||
"show_readme",
|
||||
"quit_hc",
|
||||
]
|
||||
|
||||
def make_fn(n: str):
|
||||
def fn() -> str:
|
||||
return n
|
||||
|
||||
return fn
|
||||
|
||||
fakes = {name: make_fn(name) for name in names}
|
||||
for name, fn in fakes.items():
|
||||
hc_module.__dict__[name] = fn
|
||||
hc_module._sync_callables_to_main()
|
||||
for name, fn in fakes.items():
|
||||
assert getattr(hc_module._main, name) is fn
|
||||
|
||||
|
||||
class TestSymbolReexport:
|
||||
def test_main_function_accessible(self, hc_module):
|
||||
"""Functions from main.py should be accessible on hc_module."""
|
||||
assert callable(getattr(hc_module, "hcatBruteForce", None))
|
||||
|
||||
def test_hc_module_has_main_ref(self, hc_module):
|
||||
"""hc_module._main should be the hate_crack.main module."""
|
||||
import hate_crack.main as main_mod
|
||||
|
||||
assert hc_module._main is main_mod
|
||||
|
||||
def test_debug_mode_reexported(self, hc_module):
|
||||
"""Module-level globals from main.py appear on hc_module at load time."""
|
||||
assert hasattr(hc_module, "debug_mode")
|
||||
|
||||
def test_generate_session_id_reexported(self, hc_module):
|
||||
"""generate_session_id from main.py is accessible directly on hc_module."""
|
||||
fn = getattr(hc_module, "generate_session_id", None)
|
||||
assert callable(fn)
|
||||
Reference in New Issue
Block a user