mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-04-28 12:03:11 -07:00
- Merge combinator, combinator3, and combinatorX into one unified combinator_crack function that routes by wordlist count: 2 (no sep) -> hcatCombination, 3 (no sep) -> hcatCombinator3, 4+ or any separator -> hcatCombinatorX - Replace comma-separated wordlist input with one-at-a-time tab-autocomplete prompts (blank line to finish) - Add _prompt_wordlist_paths helper using existing readline infrastructure - Add hcatCombinator3Wordlist and hcatCombinatorXWordlist config vars with rockyou.txt defaults - Print full hashcat command to stdout in --debug mode by calling _debug_cmd at the end of _append_potfile_arg (covers all 27 invocations) - Collapse combinator submenu from 6 options to 4; keep combinator3_crack, combinatorX_crack, and combinator_3plus_crack as delegation shims - Update tests to cover unified routing and new prompt interface
155 lines
6.1 KiB
Python
155 lines
6.1 KiB
Python
from unittest.mock import MagicMock, patch
|
|
|
|
from hate_crack.attacks import combinator_crack, combinator_submenu
|
|
|
|
|
|
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
|
ctx = MagicMock()
|
|
ctx.hcatHashType = hash_type
|
|
ctx.hcatHashFile = hash_file
|
|
return ctx
|
|
|
|
|
|
class TestCombinatorCrackUnified:
|
|
def test_two_wordlists_calls_hcatCombination(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
for name in ["a.txt", "b.txt"]:
|
|
(tmp_path / name).write_text("word\n")
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", "", ""]
|
|
with patch("builtins.input", side_effect=inputs):
|
|
combinator_crack(ctx)
|
|
ctx.hcatCombination.assert_called_once()
|
|
ctx.hcatCombinator3.assert_not_called()
|
|
ctx.hcatCombinatorX.assert_not_called()
|
|
|
|
def test_three_wordlists_calls_hcatCombinator3(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
for name in ["a.txt", "b.txt", "c.txt"]:
|
|
(tmp_path / name).write_text("word\n")
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", f"{tmp_path}/c.txt", "", ""]
|
|
with patch("builtins.input", side_effect=inputs):
|
|
combinator_crack(ctx)
|
|
ctx.hcatCombinator3.assert_called_once()
|
|
ctx.hcatCombination.assert_not_called()
|
|
ctx.hcatCombinatorX.assert_not_called()
|
|
|
|
def test_four_wordlists_calls_hcatCombinatorX(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
for name in ["a.txt", "b.txt", "c.txt", "d.txt"]:
|
|
(tmp_path / name).write_text("word\n")
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
inputs = [
|
|
"n",
|
|
f"{tmp_path}/a.txt",
|
|
f"{tmp_path}/b.txt",
|
|
f"{tmp_path}/c.txt",
|
|
f"{tmp_path}/d.txt",
|
|
"",
|
|
"",
|
|
]
|
|
with patch("builtins.input", side_effect=inputs):
|
|
combinator_crack(ctx)
|
|
ctx.hcatCombinatorX.assert_called_once()
|
|
ctx.hcatCombination.assert_not_called()
|
|
ctx.hcatCombinator3.assert_not_called()
|
|
|
|
def test_separator_forces_combinatorX_for_two_wordlists(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
for name in ["a.txt", "b.txt"]:
|
|
(tmp_path / name).write_text("word\n")
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", "", "-"]
|
|
with patch("builtins.input", side_effect=inputs):
|
|
combinator_crack(ctx)
|
|
ctx.hcatCombinatorX.assert_called_once()
|
|
ctx.hcatCombination.assert_not_called()
|
|
|
|
def test_separator_forces_combinatorX_for_three_wordlists(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
for name in ["a.txt", "b.txt", "c.txt"]:
|
|
(tmp_path / name).write_text("word\n")
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
inputs = ["n", f"{tmp_path}/a.txt", f"{tmp_path}/b.txt", f"{tmp_path}/c.txt", "", "-"]
|
|
with patch("builtins.input", side_effect=inputs):
|
|
combinator_crack(ctx)
|
|
ctx.hcatCombinatorX.assert_called_once()
|
|
ctx.hcatCombinator3.assert_not_called()
|
|
|
|
def test_aborts_with_fewer_than_2_wordlists(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
(tmp_path / "a.txt").write_text("word\n")
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
inputs = ["n", f"{tmp_path}/a.txt", ""]
|
|
with patch("builtins.input", side_effect=inputs):
|
|
combinator_crack(ctx)
|
|
ctx.hcatCombination.assert_not_called()
|
|
ctx.hcatCombinator3.assert_not_called()
|
|
ctx.hcatCombinatorX.assert_not_called()
|
|
|
|
def test_aborts_when_no_wordlists_provided(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
with patch("builtins.input", side_effect=["n", ""]):
|
|
combinator_crack(ctx)
|
|
ctx.hcatCombination.assert_not_called()
|
|
ctx.hcatCombinator3.assert_not_called()
|
|
ctx.hcatCombinatorX.assert_not_called()
|
|
|
|
def test_no_separator_passes_none_to_combinatorX(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
ctx.hcatWordlists = str(tmp_path)
|
|
for name in ["a.txt", "b.txt", "c.txt", "d.txt"]:
|
|
(tmp_path / name).write_text("word\n")
|
|
ctx._resolve_wordlist_path.side_effect = lambda p, base: p
|
|
inputs = [
|
|
"n",
|
|
f"{tmp_path}/a.txt",
|
|
f"{tmp_path}/b.txt",
|
|
f"{tmp_path}/c.txt",
|
|
f"{tmp_path}/d.txt",
|
|
"",
|
|
"",
|
|
]
|
|
with patch("builtins.input", side_effect=inputs):
|
|
combinator_crack(ctx)
|
|
call_args = ctx.hcatCombinatorX.call_args
|
|
positional_sep = call_args[0][3] if len(call_args[0]) >= 4 else None
|
|
keyword_sep = call_args[1].get("separator")
|
|
assert positional_sep in (None, "") and keyword_sep in (None, "")
|
|
|
|
|
|
class TestCombinatorSubmenuUpdated:
|
|
def test_submenu_option1_dispatches_to_combinator_crack(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.combinator_crack") as mock_c, patch(
|
|
"hate_crack.attacks.interactive_menu", side_effect=["1", "99"]
|
|
):
|
|
combinator_submenu(ctx)
|
|
mock_c.assert_called_once_with(ctx)
|
|
|
|
def test_submenu_has_no_separate_3plus_option(self):
|
|
"""Verify option 5 (3+) is removed - combinator is now unified under option 1."""
|
|
ctx = _make_ctx()
|
|
captured_items = []
|
|
|
|
def capture_menu(items, **kwargs):
|
|
captured_items.extend(items)
|
|
return "99"
|
|
|
|
with patch("hate_crack.attacks.interactive_menu", side_effect=capture_menu):
|
|
combinator_submenu(ctx)
|
|
|
|
keys = [item[0] for item in captured_items]
|
|
assert "1" in keys
|
|
assert "5" not in keys
|
|
assert "6" not in keys
|