mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-04-28 12:03:11 -07:00
feat: unify combinator attacks into single 2-8 wordlist handler
- 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
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
"rules_directory": "./hashcat/rules",
|
||||
"hcatDictionaryWordlist": ["rockyou.txt"],
|
||||
"hcatCombinationWordlist": ["rockyou.txt","rockyou.txt"],
|
||||
"hcatCombinator3Wordlist": ["rockyou.txt","rockyou.txt","rockyou.txt"],
|
||||
"hcatCombinatorXWordlist": ["rockyou.txt","rockyou.txt"],
|
||||
"hcatHybridlist": ["rockyou.txt"],
|
||||
"hcatMiddleCombinatorMasks": ["2","4"," ","-","_","+",",",".","&"],
|
||||
"hcatMiddleBaseList": "rockyou.txt",
|
||||
|
||||
@@ -262,8 +262,7 @@ def combinator_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("COMBINATOR ATTACK")
|
||||
print("=" * 60)
|
||||
print("This attack combines two wordlists to generate candidates.")
|
||||
print("Example: wordlist1='password' + wordlist2='123' = 'password123'")
|
||||
print("Combines 2-8 wordlists. 2 uses hashcat native mode; 3+ use external binaries.")
|
||||
print("=" * 60)
|
||||
|
||||
use_default = (
|
||||
@@ -271,73 +270,30 @@ def combinator_crack(ctx: Any) -> None:
|
||||
)
|
||||
|
||||
if use_default != "n":
|
||||
print("\nUsing default wordlist(s) from config:")
|
||||
if isinstance(ctx.hcatCombinationWordlist, list):
|
||||
for wl in ctx.hcatCombinationWordlist:
|
||||
print(f" - {wl}")
|
||||
wordlists = ctx.hcatCombinationWordlist
|
||||
else:
|
||||
print(f" - {ctx.hcatCombinationWordlist}")
|
||||
wordlists = [ctx.hcatCombinationWordlist]
|
||||
else:
|
||||
print("\nSelect wordlists for combinator attack.")
|
||||
print("You need to provide exactly 2 wordlists.")
|
||||
print("You can enter:")
|
||||
print(" - Two file paths separated by commas")
|
||||
print(" - Press TAB to autocomplete file paths")
|
||||
|
||||
selection = ctx.select_file_with_autocomplete(
|
||||
"Enter 2 wordlist files (comma-separated)",
|
||||
allow_multiple=True,
|
||||
base_dir=ctx.hcatWordlists,
|
||||
)
|
||||
|
||||
if not selection:
|
||||
print("No wordlists selected. Aborting combinator attack.")
|
||||
base = ctx.hcatCombinationWordlist
|
||||
wordlists = base if isinstance(base, list) else [base]
|
||||
wordlists = [ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists]
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Config does not have at least 2 wordlists.")
|
||||
print("Set hcatCombinationWordlist to a list of 2+ paths in config.json.")
|
||||
print("Aborting combinator attack.")
|
||||
return
|
||||
|
||||
if isinstance(selection, str):
|
||||
wordlists = [selection]
|
||||
else:
|
||||
wordlists = selection
|
||||
|
||||
separator = ""
|
||||
else:
|
||||
print("\nEnter 2-8 wordlists. Enter a blank line when done.")
|
||||
wordlists = _prompt_wordlist_paths(ctx, max_count=8)
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Combinator attack requires at least 2 wordlists.")
|
||||
print("Aborting combinator attack.")
|
||||
return
|
||||
separator = input("\nEnter separator between words (leave blank for none): ").strip()
|
||||
|
||||
valid_wordlists = []
|
||||
for wl in wordlists[:2]: # Only use first 2
|
||||
resolved = ctx._resolve_wordlist_path(wl, ctx.hcatWordlists)
|
||||
if os.path.isfile(resolved):
|
||||
valid_wordlists.append(resolved)
|
||||
print(f"✓ Found: {resolved}")
|
||||
else:
|
||||
print(f"✗ Not found: {resolved}")
|
||||
|
||||
if len(valid_wordlists) < 2:
|
||||
print("\nCould not find 2 valid wordlists. Aborting combinator attack.")
|
||||
return
|
||||
|
||||
wordlists = valid_wordlists
|
||||
|
||||
wordlists = [
|
||||
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]}")
|
||||
print(f"Hash type: {ctx.hcatHashType}")
|
||||
print(f"Hash file: {ctx.hcatHashFile}")
|
||||
|
||||
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
if len(wordlists) == 2 and not separator:
|
||||
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
elif len(wordlists) == 3 and not separator:
|
||||
ctx.hcatCombinator3(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
else:
|
||||
ctx.hcatCombinatorX(ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None)
|
||||
|
||||
|
||||
def hybrid_crack(ctx: Any) -> None:
|
||||
@@ -428,108 +384,64 @@ def middle_combinator(ctx: Any) -> None:
|
||||
ctx.hcatMiddleCombinator(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
|
||||
def combinator3_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("COMBINATOR3 ATTACK")
|
||||
print("=" * 60)
|
||||
print("This attack combines three wordlists to generate candidates.")
|
||||
print("=" * 60)
|
||||
def _prompt_wordlist_paths(ctx, max_count: int) -> list[str]:
|
||||
"""Prompt for wordlist paths one at a time with tab-autocomplete.
|
||||
|
||||
use_default = (
|
||||
input("\nUse default combinator wordlists from config? (Y/n): ").strip().lower()
|
||||
)
|
||||
Stops when a blank line is entered or max_count paths have been collected.
|
||||
Returns a list of resolved, valid file paths.
|
||||
"""
|
||||
|
||||
if use_default != "n":
|
||||
base = ctx.hcatCombinationWordlist
|
||||
wordlists = base if isinstance(base, list) else [base]
|
||||
if len(wordlists) < 3:
|
||||
print("\n[!] Config does not have 3 wordlists for combinator3.")
|
||||
print("Set hcatCombinationWordlist to a list of 3 paths in config.json.")
|
||||
print("Aborting combinator3 attack.")
|
||||
return
|
||||
else:
|
||||
def path_completer(text, state):
|
||||
base = ctx.hcatWordlists
|
||||
if not text:
|
||||
pattern = os.path.join(base, "*")
|
||||
matches = glob.glob(pattern)
|
||||
else:
|
||||
expanded = os.path.expanduser(text)
|
||||
if expanded.startswith(("/", "./", "../", "~")):
|
||||
matches = glob.glob(expanded + "*")
|
||||
else:
|
||||
pattern = os.path.join(base, expanded + "*")
|
||||
matches = glob.glob(pattern)
|
||||
matches = [m + "/" if os.path.isdir(m) else m for m in matches]
|
||||
try:
|
||||
return matches[state]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
_configure_readline(path_completer)
|
||||
|
||||
collected: list[str] = []
|
||||
count = 1
|
||||
while len(collected) < max_count:
|
||||
raw = input(
|
||||
"\nEnter 3 wordlist file paths (comma-separated): "
|
||||
f"\nWordlist #{count} (tab to autocomplete, blank to finish): "
|
||||
).strip()
|
||||
if not raw:
|
||||
print("No wordlists provided. Aborting combinator3 attack.")
|
||||
return
|
||||
break
|
||||
resolved = ctx._resolve_wordlist_path(raw, ctx.hcatWordlists)
|
||||
if os.path.isfile(resolved):
|
||||
collected.append(resolved)
|
||||
print(f"Added: {resolved}")
|
||||
count += 1
|
||||
else:
|
||||
print(f"Not found: {resolved}")
|
||||
return collected
|
||||
|
||||
entries = [p.strip() for p in raw.split(",") if p.strip()]
|
||||
if len(entries) < 3:
|
||||
print("\n[!] Combinator3 attack requires exactly 3 wordlists.")
|
||||
print("Aborting combinator3 attack.")
|
||||
return
|
||||
|
||||
valid = []
|
||||
for p in entries[:3]:
|
||||
resolved = ctx._resolve_wordlist_path(p, ctx.hcatWordlists)
|
||||
if os.path.isfile(resolved):
|
||||
valid.append(resolved)
|
||||
print(f"Found: {resolved}")
|
||||
else:
|
||||
print(f"Not found: {resolved}")
|
||||
|
||||
if len(valid) < 3:
|
||||
print("\nCould not find 3 valid wordlists. Aborting combinator3 attack.")
|
||||
return
|
||||
|
||||
wordlists = valid
|
||||
|
||||
ctx.hcatCombinator3(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
def combinator3_crack(ctx: Any) -> None:
|
||||
"""3-way combinator attack (delegates to unified combinator_crack)."""
|
||||
combinator_crack(ctx)
|
||||
|
||||
|
||||
def combinatorX_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("COMBINATORX ATTACK")
|
||||
print("=" * 60)
|
||||
print("This attack combines 2-8 wordlists with an optional separator.")
|
||||
print("=" * 60)
|
||||
"""N-way combinator attack (delegates to unified combinator_crack)."""
|
||||
combinator_crack(ctx)
|
||||
|
||||
use_default = (
|
||||
input("\nUse default combinator wordlists from config? (Y/n): ").strip().lower()
|
||||
)
|
||||
|
||||
if use_default != "n":
|
||||
base = ctx.hcatCombinationWordlist
|
||||
wordlists = base if isinstance(base, list) else [base]
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Config does not have at least 2 wordlists for combinatorX.")
|
||||
print("Set hcatCombinationWordlist to a list of 2+ paths in config.json.")
|
||||
print("Aborting combinatorX attack.")
|
||||
return
|
||||
separator = ""
|
||||
else:
|
||||
raw = input(
|
||||
"\nEnter 2-8 wordlist file paths (comma-separated): "
|
||||
).strip()
|
||||
if not raw:
|
||||
print("No wordlists provided. Aborting combinatorX attack.")
|
||||
return
|
||||
|
||||
entries = [p.strip() for p in raw.split(",") if p.strip()]
|
||||
if len(entries) < 2:
|
||||
print("\n[!] CombinatorX attack requires at least 2 wordlists.")
|
||||
print("Aborting combinatorX attack.")
|
||||
return
|
||||
|
||||
valid = []
|
||||
for p in entries[:8]:
|
||||
resolved = ctx._resolve_wordlist_path(p, ctx.hcatWordlists)
|
||||
if os.path.isfile(resolved):
|
||||
valid.append(resolved)
|
||||
print(f"Found: {resolved}")
|
||||
else:
|
||||
print(f"Not found: {resolved}")
|
||||
|
||||
if len(valid) < 2:
|
||||
print("\nCould not find 2 valid wordlists. Aborting combinatorX attack.")
|
||||
return
|
||||
|
||||
wordlists = valid
|
||||
separator = input("\nEnter separator between words (leave blank for none): ").strip()
|
||||
|
||||
ctx.hcatCombinatorX(ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None)
|
||||
def combinator_3plus_crack(ctx: Any) -> None:
|
||||
"""3+ wordlist combinator (delegates to unified combinator_crack)."""
|
||||
combinator_crack(ctx)
|
||||
|
||||
|
||||
def bandrel_method(ctx: Any) -> None:
|
||||
@@ -727,12 +639,10 @@ def markov_brute_force(ctx: Any) -> None:
|
||||
|
||||
def combinator_submenu(ctx: Any) -> None:
|
||||
items = [
|
||||
("1", "Combinator Attack"),
|
||||
("1", "Combinator Attack (2-8 wordlists)"),
|
||||
("2", "YOLO Combinator Attack"),
|
||||
("3", "Middle Combinator Attack"),
|
||||
("4", "Thorough Combinator Attack"),
|
||||
("5", "Combinator3 Attack (3-way)"),
|
||||
("6", "CombinatorX Attack (N-way, 2-8 wordlists)"),
|
||||
("99", "Back to Main Menu"),
|
||||
]
|
||||
while True:
|
||||
@@ -747,7 +657,3 @@ def combinator_submenu(ctx: Any) -> None:
|
||||
middle_combinator(ctx)
|
||||
elif choice == "4":
|
||||
thorough_combinator(ctx)
|
||||
elif choice == "5":
|
||||
combinator3_crack(ctx)
|
||||
elif choice == "6":
|
||||
combinatorX_crack(ctx)
|
||||
|
||||
@@ -356,19 +356,19 @@ else:
|
||||
|
||||
|
||||
def _append_potfile_arg(cmd, *, use_potfile_path=True, potfile_path=None):
|
||||
if not use_potfile_path:
|
||||
return
|
||||
pot = potfile_path or hcatPotfilePath
|
||||
if pot:
|
||||
try:
|
||||
pot_dir = os.path.dirname(pot)
|
||||
if pot_dir:
|
||||
os.makedirs(pot_dir, exist_ok=True)
|
||||
if not os.path.exists(pot):
|
||||
open(pot, "a").close()
|
||||
except OSError:
|
||||
pass
|
||||
cmd.append(f"--potfile-path={pot}")
|
||||
if use_potfile_path:
|
||||
pot = potfile_path or hcatPotfilePath
|
||||
if pot:
|
||||
try:
|
||||
pot_dir = os.path.dirname(pot)
|
||||
if pot_dir:
|
||||
os.makedirs(pot_dir, exist_ok=True)
|
||||
if not os.path.exists(pot):
|
||||
open(pot, "a").close()
|
||||
except OSError:
|
||||
pass
|
||||
cmd.append(f"--potfile-path={pot}")
|
||||
_debug_cmd(cmd)
|
||||
|
||||
|
||||
rulesDirectory = config_parser["rules_directory"]
|
||||
@@ -401,6 +401,8 @@ pipalPath = config_parser["pipalPath"]
|
||||
hcatDictionaryWordlist = config_parser["hcatDictionaryWordlist"]
|
||||
hcatHybridlist = config_parser["hcatHybridlist"]
|
||||
hcatCombinationWordlist = config_parser["hcatCombinationWordlist"]
|
||||
hcatCombinator3Wordlist = config_parser.get("hcatCombinator3Wordlist", ["rockyou.txt", "rockyou.txt", "rockyou.txt"])
|
||||
hcatCombinatorXWordlist = config_parser.get("hcatCombinatorXWordlist", ["rockyou.txt", "rockyou.txt"])
|
||||
hcatMiddleCombinatorMasks = config_parser["hcatMiddleCombinatorMasks"]
|
||||
hcatMiddleBaseList = config_parser["hcatMiddleBaseList"]
|
||||
hcatThoroughCombinatorMasks = config_parser["hcatThoroughCombinatorMasks"]
|
||||
@@ -558,6 +560,8 @@ hcatDictionaryWordlist = _normalize_wordlist_setting(
|
||||
hcatCombinationWordlist = _normalize_wordlist_setting(
|
||||
hcatCombinationWordlist, wordlists_dir
|
||||
)
|
||||
hcatCombinator3Wordlist = _normalize_wordlist_setting(hcatCombinator3Wordlist, wordlists_dir)
|
||||
hcatCombinatorXWordlist = _normalize_wordlist_setting(hcatCombinatorXWordlist, wordlists_dir)
|
||||
hcatHybridlist = _normalize_wordlist_setting(hcatHybridlist, wordlists_dir)
|
||||
hcatMiddleBaseList = _normalize_wordlist_setting(hcatMiddleBaseList, wordlists_dir)
|
||||
hcatThoroughBaseList = _normalize_wordlist_setting(hcatThoroughBaseList, wordlists_dir)
|
||||
@@ -3391,6 +3395,10 @@ def combinatorX_crack():
|
||||
return _attacks.combinatorX_crack(_attack_ctx())
|
||||
|
||||
|
||||
def combinator_3plus_crack():
|
||||
return _attacks.combinator_3plus_crack(_attack_ctx())
|
||||
|
||||
|
||||
|
||||
def combinator_submenu():
|
||||
return _attacks.combinator_submenu(_attack_ctx())
|
||||
|
||||
@@ -216,7 +216,7 @@ class TestCombinatorCrack:
|
||||
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:
|
||||
def test_three_wordlists_in_config_routes_to_combinator3(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
|
||||
@@ -224,9 +224,10 @@ class TestCombinatorCrack:
|
||||
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
|
||||
ctx.hcatCombinator3.assert_called_once()
|
||||
ctx.hcatCombination.assert_not_called()
|
||||
call_wordlists = ctx.hcatCombinator3.call_args[0][2]
|
||||
assert len(call_wordlists) == 3
|
||||
|
||||
|
||||
class TestHybridCrack:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hate_crack.attacks import combinator3_crack, combinator_submenu, combinatorX_crack
|
||||
from hate_crack.attacks import combinator_crack, combinator_submenu
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
@@ -13,122 +10,134 @@ def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
return ctx
|
||||
|
||||
|
||||
class TestCombinator3Crack:
|
||||
def test_calls_hcatCombinator3_with_three_wordlists(self, tmp_path):
|
||||
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
|
||||
wl_arg = f"{tmp_path}/a.txt,{tmp_path}/b.txt,{tmp_path}/c.txt"
|
||||
with patch("builtins.input", side_effect=["n", wl_arg]):
|
||||
combinator3_crack(ctx)
|
||||
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_aborts_with_fewer_than_3_wordlists(self, tmp_path):
|
||||
def test_four_wordlists_calls_hcatCombinatorX(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
(tmp_path / "a.txt").write_text("word\n")
|
||||
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
|
||||
wl_arg = f"{tmp_path}/a.txt,{tmp_path}/a.txt"
|
||||
with patch("builtins.input", side_effect=["n", wl_arg]):
|
||||
combinator3_crack(ctx)
|
||||
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_aborts_when_no_wordlists_provided(self, tmp_path):
|
||||
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
|
||||
with patch("builtins.input", side_effect=["n", ""]):
|
||||
combinator3_crack(ctx)
|
||||
ctx.hcatCombinator3.assert_not_called()
|
||||
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_passes_exactly_3_wordlists(self, tmp_path):
|
||||
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
|
||||
wl_arg = f"{tmp_path}/a.txt,{tmp_path}/b.txt,{tmp_path}/c.txt"
|
||||
with patch("builtins.input", side_effect=["n", wl_arg]):
|
||||
combinator3_crack(ctx)
|
||||
call_args = ctx.hcatCombinator3.call_args
|
||||
wordlists = call_args[0][2] if len(call_args[0]) >= 3 else call_args[1].get("wordlists")
|
||||
assert len(wordlists) == 3
|
||||
|
||||
|
||||
class TestCombinatorXCrack:
|
||||
def test_calls_hcatCombinatorX_with_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
|
||||
wl_arg = f"{tmp_path}/a.txt,{tmp_path}/b.txt"
|
||||
with patch("builtins.input", side_effect=["n", wl_arg, ""]):
|
||||
combinatorX_crack(ctx)
|
||||
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()
|
||||
|
||||
def test_passes_separator_to_hcatCombinatorX(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
|
||||
wl_arg = f"{tmp_path}/a.txt,{tmp_path}/b.txt"
|
||||
with patch("builtins.input", side_effect=["n", wl_arg, "-"]):
|
||||
combinatorX_crack(ctx)
|
||||
call_args = ctx.hcatCombinatorX.call_args
|
||||
# separator may be positional or keyword
|
||||
positional_has_sep = len(call_args[0]) >= 4 and call_args[0][3] == "-"
|
||||
keyword_has_sep = call_args[1].get("separator") == "-"
|
||||
assert positional_has_sep or keyword_has_sep
|
||||
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
|
||||
wl_arg = f"{tmp_path}/a.txt"
|
||||
with patch("builtins.input", side_effect=["n", wl_arg, ""]):
|
||||
combinatorX_crack(ctx)
|
||||
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_no_separator_when_empty_input(self, tmp_path):
|
||||
def test_aborts_when_no_wordlists_provided(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
ctx.hcatWordlists = str(tmp_path)
|
||||
for name in ["a.txt", "b.txt"]:
|
||||
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
|
||||
wl_arg = f"{tmp_path}/a.txt,{tmp_path}/b.txt"
|
||||
with patch("builtins.input", side_effect=["n", wl_arg, ""]):
|
||||
combinatorX_crack(ctx)
|
||||
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")
|
||||
# separator should be None or empty string when nothing entered
|
||||
assert positional_sep in (None, "") and keyword_sep in (None, "")
|
||||
|
||||
|
||||
class TestCombinatorSubmenuUpdated:
|
||||
def test_submenu_has_combinator3_option(self):
|
||||
def test_submenu_option1_dispatches_to_combinator_crack(self):
|
||||
ctx = _make_ctx()
|
||||
with patch("hate_crack.attacks.combinator3_crack") as mock_c3, patch(
|
||||
"hate_crack.attacks.interactive_menu", side_effect=["5", "99"]
|
||||
with patch("hate_crack.attacks.combinator_crack") as mock_c, patch(
|
||||
"hate_crack.attacks.interactive_menu", side_effect=["1", "99"]
|
||||
):
|
||||
combinator_submenu(ctx)
|
||||
mock_c3.assert_called_once_with(ctx)
|
||||
mock_c.assert_called_once_with(ctx)
|
||||
|
||||
def test_submenu_has_combinatorX_option(self):
|
||||
ctx = _make_ctx()
|
||||
with patch("hate_crack.attacks.combinatorX_crack") as mock_cx, patch(
|
||||
"hate_crack.attacks.interactive_menu", side_effect=["6", "99"]
|
||||
):
|
||||
combinator_submenu(ctx)
|
||||
mock_cx.assert_called_once_with(ctx)
|
||||
|
||||
def test_submenu_items_include_new_attacks(self):
|
||||
"""Verify the submenu item list advertises options 5 and 6."""
|
||||
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 = []
|
||||
|
||||
@@ -140,5 +149,6 @@ class TestCombinatorSubmenuUpdated:
|
||||
combinator_submenu(ctx)
|
||||
|
||||
keys = [item[0] for item in captured_items]
|
||||
assert "5" in keys
|
||||
assert "6" in keys
|
||||
assert "1" in keys
|
||||
assert "5" not in keys
|
||||
assert "6" not in keys
|
||||
|
||||
Reference in New Issue
Block a user