Files
hate_crack/tests/test_random_rules_wrapper.py
Justin Bollinger b8797e582d feat: add random rules attack using generate-rules.bin (#87)
- Add hcatGenerateRules() in main.py: runs generate-rules.bin to
  produce N random rules, writes them to a temp file, runs hashcat
  with -r against a chosen wordlist, cleans up on exit
- Add generate_rules_crack() handler in attacks.py with count
  prompt (default 65536), wordlist picker with tab-completion,
  input validation, and abort on invalid input
- Add dispatcher generate_rules_crack() in main.py and key "20"
  in both main.py and hate_crack.py get_main_menu_options()
- Add ("20", "Random Rules Attack") to get_main_menu_items()
- Add tests: test_random_rules_attack.py (menu presence, handler
  wiring), test_random_rules_wrapper.py (subprocess behavior,
  cleanup, count passing, count tracking)
- Add key "20" to MENU_OPTION_TEST_CASES in test_ui_menu_options.py
- Update README: add option 20 to menu listing, add attack
  description, add version history entry
2026-03-19 12:13:38 -04:00

164 lines
7.3 KiB
Python

from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def main_module(hc_module):
return hc_module._main
def _make_mock_proc(wait_side_effect=None):
proc = MagicMock()
if wait_side_effect is not None:
proc.wait.side_effect = wait_side_effect
else:
proc.wait.return_value = None
proc.pid = 12345
return proc
class TestHcatGenerateRules:
def test_calls_generate_rules_bin(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\nc\n"
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.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result) as mock_run, \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 100, str(wl))
run_calls = mock_run.call_args_list
assert any("generate-rules.bin" in str(c) for c in run_calls)
def test_calls_hashcat_with_rule_flag(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\n"
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.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatGenerateRules("1000", hash_file, 100, str(wl))
popen_calls = mock_popen.call_args_list
assert any("-r" in str(c) for c in popen_calls)
def test_passes_rule_count_to_generate_rules_bin(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\n"
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.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result) as mock_run, \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 999, str(wl))
run_calls = mock_run.call_args_list
generate_call = next(
(c for c in run_calls if "generate-rules.bin" in str(c)), None
)
assert generate_call is not None
cmd_args = generate_call[0][0]
assert "999" in cmd_args
def test_cleans_up_temp_file(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\n"
captured_paths = []
import os as _os
original_unlink = _os.unlink
def capturing_unlink(path):
captured_paths.append(path)
original_unlink(path)
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.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc), \
patch("hate_crack.main.os.unlink", side_effect=capturing_unlink):
main_module.hcatGenerateRules("1000", hash_file, 50, str(wl))
assert any("hate_crack_random_" in p for p in captured_paths), \
f"Expected temp file cleanup, got: {captured_paths}"
def test_keyboard_interrupt_kills_process(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
mock_result = MagicMock()
mock_result.stdout = "l\n"
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.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 10, str(wl))
mock_proc.kill.assert_called_once()
def test_sets_hcatGenerateRulesCount(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\n"
# patch.object won't patch reads of module-level globals; set directly
original_cracked = main_module.hcatHashCracked
main_module.hcatHashCracked = 2
try:
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.object(main_module, "lineCount", return_value=5), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 10, str(wl))
finally:
main_module.hcatHashCracked = original_cracked
assert main_module.hcatGenerateRulesCount == 3 # 5 - 2