mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat(wordlist-tools): add Optimize Wordlists submenu option (8)
Adds wordlist_optimize worker to main.py that consolidates multiple wordlists into per-length deduplicated files using splitlen.bin and rli2.bin. Wires handler in attacks.py and registers it as option 8 in the Wordlist Tools submenu. Adds 16 tests covering happy path, empty input, missing files, empty outdir, failure path, and submenu dispatch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
0435314043
commit
cf12c3264e
@@ -1192,6 +1192,33 @@ def wordlist_shard(ctx: Any) -> None:
|
||||
print("[!] Shard failed.")
|
||||
|
||||
|
||||
def wordlist_optimize(ctx: Any) -> None:
|
||||
"""Prompt for input wordlists and output directory, then optimize."""
|
||||
raw = ctx.select_file_with_autocomplete(
|
||||
"\n[*] Enter input wordlist paths",
|
||||
allow_multiple=True,
|
||||
base_dir=ctx.hcatWordlists,
|
||||
).strip()
|
||||
inputs = [p.strip() for p in raw.split(",") if p.strip()]
|
||||
if not inputs:
|
||||
print("[!] No input wordlists provided.")
|
||||
return
|
||||
missing = [p for p in inputs if not os.path.isfile(p)]
|
||||
if missing:
|
||||
print("[!] Files not found:")
|
||||
for p in missing:
|
||||
print(f" {p}")
|
||||
return
|
||||
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
|
||||
if not outdir:
|
||||
print("[!] Output directory cannot be empty.")
|
||||
return
|
||||
if ctx.wordlist_optimize(inputs, outdir):
|
||||
print(f"\n[*] Optimized wordlists written to: {outdir}")
|
||||
else:
|
||||
print("[!] Optimization failed.")
|
||||
|
||||
|
||||
def wordlist_tools_submenu(ctx: Any) -> None:
|
||||
"""Display the Wordlist Tools submenu and dispatch to the selected handler."""
|
||||
items = [
|
||||
@@ -1202,6 +1229,7 @@ def wordlist_tools_submenu(ctx: Any) -> None:
|
||||
("5", "Split by Length"),
|
||||
("6", "Subtract Wordlist"),
|
||||
("7", "Shard Wordlist"),
|
||||
("8", "Optimize Wordlists"),
|
||||
("99", "Back to Main Menu"),
|
||||
]
|
||||
while True:
|
||||
@@ -1222,3 +1250,5 @@ def wordlist_tools_submenu(ctx: Any) -> None:
|
||||
wordlist_subtract_words(ctx)
|
||||
elif choice == "7":
|
||||
wordlist_shard(ctx)
|
||||
elif choice == "8":
|
||||
wordlist_optimize(ctx)
|
||||
|
||||
@@ -4011,6 +4011,43 @@ def wordlist_gate(infile: str, outfile: str, mod: int, offset: int) -> bool:
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def wordlist_optimize(input_wordlists: list[str], outdir: str) -> bool:
|
||||
"""Consolidate wordlists into per-length deduplicated files in outdir."""
|
||||
import tempfile
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
for wl in input_wordlists:
|
||||
if not os.path.isfile(wl):
|
||||
print(f"[!] Skipping missing wordlist: {wl}")
|
||||
continue
|
||||
if not os.listdir(outdir):
|
||||
if not wordlist_splitlen(wl, outdir):
|
||||
return False
|
||||
continue
|
||||
with tempfile.TemporaryDirectory(prefix="hc_optimize_") as tmp:
|
||||
if not wordlist_splitlen(wl, tmp):
|
||||
return False
|
||||
for fname in os.listdir(tmp):
|
||||
src = os.path.join(tmp, fname)
|
||||
dst = os.path.join(outdir, fname)
|
||||
if not os.path.isfile(dst):
|
||||
shutil.copyfile(src, dst)
|
||||
continue
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, prefix="hc_optimize_", suffix=".out"
|
||||
) as out_fh:
|
||||
out_path = out_fh.name
|
||||
try:
|
||||
if not wordlist_subtract_single(src, dst, out_path):
|
||||
return False
|
||||
if os.path.getsize(out_path) > 0:
|
||||
with open(dst, "ab") as df, open(out_path, "rb") as sf:
|
||||
df.write(sf.read())
|
||||
finally:
|
||||
if os.path.isfile(out_path):
|
||||
os.remove(out_path)
|
||||
return True
|
||||
|
||||
|
||||
def wordlist_tools_submenu():
|
||||
return _attacks.wordlist_tools_submenu(_attack_ctx())
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from hate_crack.attacks import (
|
||||
wordlist_filter_charclass_exclude,
|
||||
wordlist_filter_charclass_include,
|
||||
wordlist_filter_length,
|
||||
wordlist_optimize,
|
||||
wordlist_shard,
|
||||
wordlist_split_by_length,
|
||||
wordlist_subtract_words,
|
||||
@@ -26,6 +27,7 @@ def _make_ctx():
|
||||
ctx.wordlist_subtract.return_value = True
|
||||
ctx.wordlist_subtract_single.return_value = True
|
||||
ctx.wordlist_gate.return_value = True
|
||||
ctx.wordlist_optimize.return_value = True
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -297,6 +299,13 @@ class TestWordlistToolsSubmenu:
|
||||
wordlist_tools_submenu(ctx)
|
||||
mock_fn.assert_called_once_with(ctx)
|
||||
|
||||
def test_submenu_dispatches_to_optimize(self):
|
||||
ctx = _make_ctx()
|
||||
with patch("hate_crack.attacks.wordlist_optimize") as mock_fn, \
|
||||
patch("hate_crack.attacks.interactive_menu", side_effect=["8", "99"]):
|
||||
wordlist_tools_submenu(ctx)
|
||||
mock_fn.assert_called_once_with(ctx)
|
||||
|
||||
def test_submenu_exits_on_99(self):
|
||||
ctx = _make_ctx()
|
||||
with patch("hate_crack.attacks.interactive_menu", return_value="99"):
|
||||
@@ -306,3 +315,74 @@ class TestWordlistToolsSubmenu:
|
||||
ctx = _make_ctx()
|
||||
with patch("hate_crack.attacks.interactive_menu", return_value=None):
|
||||
wordlist_tools_submenu(ctx)
|
||||
|
||||
|
||||
class TestWordlistOptimize:
|
||||
def test_happy_path(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
wl_a = tmp_path / "a.txt"
|
||||
wl_a.write_text("word1\n")
|
||||
wl_b = tmp_path / "b.txt"
|
||||
wl_b.write_text("word2\n")
|
||||
outdir = str(tmp_path / "out")
|
||||
ctx.select_file_with_autocomplete.side_effect = [
|
||||
f"{wl_a},{wl_b}",
|
||||
outdir,
|
||||
]
|
||||
with patch("hate_crack.attacks.os.path.isfile", return_value=True):
|
||||
wordlist_optimize(ctx)
|
||||
ctx.wordlist_optimize.assert_called_once_with(
|
||||
[str(wl_a), str(wl_b)], outdir
|
||||
)
|
||||
out = capsys.readouterr().out
|
||||
assert outdir in out
|
||||
|
||||
def test_empty_input_rejection(self, capsys):
|
||||
ctx = _make_ctx()
|
||||
ctx.select_file_with_autocomplete.return_value = ","
|
||||
wordlist_optimize(ctx)
|
||||
out = capsys.readouterr().out
|
||||
assert "No input wordlists provided" in out
|
||||
ctx.wordlist_optimize.assert_not_called()
|
||||
|
||||
def test_blank_input_rejection(self, capsys):
|
||||
ctx = _make_ctx()
|
||||
ctx.select_file_with_autocomplete.return_value = ""
|
||||
wordlist_optimize(ctx)
|
||||
out = capsys.readouterr().out
|
||||
assert "No input wordlists provided" in out
|
||||
ctx.wordlist_optimize.assert_not_called()
|
||||
|
||||
def test_missing_file_rejection(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
existing = tmp_path / "a.txt"
|
||||
existing.write_text("word\n")
|
||||
ctx.select_file_with_autocomplete.return_value = f"{existing},/nonexistent/missing.txt"
|
||||
with patch("hate_crack.attacks.os.path.isfile", side_effect=lambda p: p == str(existing)):
|
||||
wordlist_optimize(ctx)
|
||||
out = capsys.readouterr().out
|
||||
assert "Files not found" in out
|
||||
ctx.wordlist_optimize.assert_not_called()
|
||||
|
||||
def test_empty_outdir_rejection(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
wl = tmp_path / "a.txt"
|
||||
wl.write_text("word\n")
|
||||
ctx.select_file_with_autocomplete.side_effect = [str(wl), ""]
|
||||
with patch("hate_crack.attacks.os.path.isfile", return_value=True):
|
||||
wordlist_optimize(ctx)
|
||||
out = capsys.readouterr().out
|
||||
assert "Output directory cannot be empty" in out
|
||||
ctx.wordlist_optimize.assert_not_called()
|
||||
|
||||
def test_failure_path(self, tmp_path, capsys):
|
||||
ctx = _make_ctx()
|
||||
ctx.wordlist_optimize.return_value = False
|
||||
wl = tmp_path / "a.txt"
|
||||
wl.write_text("word\n")
|
||||
outdir = str(tmp_path / "out")
|
||||
ctx.select_file_with_autocomplete.side_effect = [str(wl), outdir]
|
||||
with patch("hate_crack.attacks.os.path.isfile", return_value=True):
|
||||
wordlist_optimize(ctx)
|
||||
out = capsys.readouterr().out
|
||||
assert "Optimization failed" in out
|
||||
|
||||
Reference in New Issue
Block a user