mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-04-28 12:03:11 -07:00
Add Wordlist Tools submenu (key 80) with 7 preprocessing utilities backed by hashcat-utils binaries: - Filter by length (len.bin) - #90 - Require/exclude character classes (req-include.bin, req-exclude.bin) - #90 - Extract substring (cutb.bin) - #90 - Remove matching lines (rli.bin, rli2.bin) - #91 - Split by length (splitlen.bin) - #92 - Shard wordlist (gate.bin) - #94 Three-layer pattern: - main.py: low-level wrappers (wordlist_filter_len, wordlist_filter_req_include, wordlist_filter_req_exclude, wordlist_cutb, wordlist_splitlen, wordlist_subtract, wordlist_subtract_single, wordlist_gate) return bool for testability - attacks.py: UI handlers with input validation and the wordlist_tools_submenu dispatcher - hate_crack.py + main.py: menu item "80" wired in both get_main_menu_items and get_main_menu_options Move interactive_menu import to attacks.py module level (was local import in combinator_submenu) to support patching in tests.
297 lines
12 KiB
Python
297 lines
12 KiB
Python
import os
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from hate_crack.attacks import (
|
|
wordlist_cut_substring,
|
|
wordlist_filter_charclass_exclude,
|
|
wordlist_filter_charclass_include,
|
|
wordlist_filter_length,
|
|
wordlist_shard,
|
|
wordlist_split_by_length,
|
|
wordlist_subtract_words,
|
|
wordlist_tools_submenu,
|
|
)
|
|
|
|
|
|
def _make_ctx():
|
|
ctx = MagicMock()
|
|
ctx.wordlist_filter_len.return_value = True
|
|
ctx.wordlist_filter_req_include.return_value = True
|
|
ctx.wordlist_filter_req_exclude.return_value = True
|
|
ctx.wordlist_cutb.return_value = True
|
|
ctx.wordlist_splitlen.return_value = True
|
|
ctx.wordlist_subtract.return_value = True
|
|
ctx.wordlist_subtract_single.return_value = True
|
|
ctx.wordlist_gate.return_value = True
|
|
return ctx
|
|
|
|
|
|
class TestWordlistFilterLength:
|
|
def test_calls_wordlist_filter_len(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outfile = tmp_path / "out.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]):
|
|
wordlist_filter_length(ctx)
|
|
ctx.wordlist_filter_len.assert_called_once_with(str(infile), str(outfile), 4, 8)
|
|
|
|
def test_rejects_nonexistent_infile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
with patch("builtins.input", return_value="/nonexistent/file.txt"):
|
|
wordlist_filter_length(ctx)
|
|
ctx.wordlist_filter_len.assert_not_called()
|
|
|
|
def test_rejects_empty_outfile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
with patch("builtins.input", side_effect=[str(infile), ""]):
|
|
wordlist_filter_length(ctx)
|
|
ctx.wordlist_filter_len.assert_not_called()
|
|
|
|
def test_prints_success_message(self, tmp_path, capsys):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outfile = tmp_path / "out.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]):
|
|
wordlist_filter_length(ctx)
|
|
out = capsys.readouterr().out
|
|
assert "success" in out.lower() or "wrote" in out.lower() or str(outfile) in out
|
|
|
|
def test_prints_failure_message(self, tmp_path, capsys):
|
|
ctx = _make_ctx()
|
|
ctx.wordlist_filter_len.return_value = False
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outfile = tmp_path / "out.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]):
|
|
wordlist_filter_length(ctx)
|
|
out = capsys.readouterr().out
|
|
assert "fail" in out.lower() or "error" in out.lower() or "!" in out
|
|
|
|
|
|
class TestWordlistFilterCharclassInclude:
|
|
def test_calls_wordlist_filter_req_include(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outfile = tmp_path / "out.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "7"]):
|
|
wordlist_filter_charclass_include(ctx)
|
|
ctx.wordlist_filter_req_include.assert_called_once_with(str(infile), str(outfile), 7)
|
|
|
|
def test_rejects_nonexistent_infile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
with patch("builtins.input", return_value="/nonexistent/file.txt"):
|
|
wordlist_filter_charclass_include(ctx)
|
|
ctx.wordlist_filter_req_include.assert_not_called()
|
|
|
|
|
|
class TestWordlistFilterCharclassExclude:
|
|
def test_calls_wordlist_filter_req_exclude(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outfile = tmp_path / "out.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "8"]):
|
|
wordlist_filter_charclass_exclude(ctx)
|
|
ctx.wordlist_filter_req_exclude.assert_called_once_with(str(infile), str(outfile), 8)
|
|
|
|
def test_rejects_nonexistent_infile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
with patch("builtins.input", return_value="/nonexistent/file.txt"):
|
|
wordlist_filter_charclass_exclude(ctx)
|
|
ctx.wordlist_filter_req_exclude.assert_not_called()
|
|
|
|
|
|
class TestWordlistCutSubstring:
|
|
def test_calls_wordlist_cutb_with_length(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outfile = tmp_path / "out.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "2", "4"]):
|
|
wordlist_cut_substring(ctx)
|
|
ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, 4)
|
|
|
|
def test_calls_wordlist_cutb_without_length(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outfile = tmp_path / "out.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "2", ""]):
|
|
wordlist_cut_substring(ctx)
|
|
ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, None)
|
|
|
|
def test_rejects_nonexistent_infile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
with patch("builtins.input", return_value="/nonexistent/file.txt"):
|
|
wordlist_cut_substring(ctx)
|
|
ctx.wordlist_cutb.assert_not_called()
|
|
|
|
|
|
class TestWordlistSplitByLength:
|
|
def test_calls_wordlist_splitlen(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outdir = tmp_path / "split"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outdir)]):
|
|
wordlist_split_by_length(ctx)
|
|
ctx.wordlist_splitlen.assert_called_once_with(str(infile), str(outdir))
|
|
|
|
def test_creates_outdir_if_missing(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("test\n")
|
|
outdir = tmp_path / "split" / "nested"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outdir)]):
|
|
wordlist_split_by_length(ctx)
|
|
assert outdir.exists()
|
|
|
|
def test_rejects_nonexistent_infile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
with patch("builtins.input", return_value="/nonexistent/file.txt"):
|
|
wordlist_split_by_length(ctx)
|
|
ctx.wordlist_splitlen.assert_not_called()
|
|
|
|
|
|
class TestWordlistSubtractWords:
|
|
def test_single_remove_calls_wordlist_subtract_single(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("word1\n")
|
|
removefile = tmp_path / "remove.txt"
|
|
removefile.write_text("word1\n")
|
|
outfile = tmp_path / "out.txt"
|
|
# "1" = single remove
|
|
with patch("builtins.input", side_effect=["1", str(infile), str(removefile), str(outfile)]):
|
|
wordlist_subtract_words(ctx)
|
|
ctx.wordlist_subtract_single.assert_called_once_with(str(infile), str(removefile), str(outfile))
|
|
|
|
def test_multi_remove_calls_wordlist_subtract(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("word1\nword2\n")
|
|
removefile1 = tmp_path / "remove1.txt"
|
|
removefile1.write_text("word1\n")
|
|
removefile2 = tmp_path / "remove2.txt"
|
|
removefile2.write_text("word2\n")
|
|
outfile = tmp_path / "out.txt"
|
|
# "2" = multiple removes, then two remove files separated by comma or newline
|
|
with patch(
|
|
"builtins.input",
|
|
side_effect=["2", str(infile), str(outfile), f"{removefile1},{removefile2}"],
|
|
):
|
|
wordlist_subtract_words(ctx)
|
|
ctx.wordlist_subtract.assert_called_once_with(
|
|
str(infile), str(outfile), str(removefile1), str(removefile2)
|
|
)
|
|
|
|
def test_single_remove_rejects_nonexistent_infile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
with patch("builtins.input", side_effect=["1", "/nonexistent.txt"]):
|
|
wordlist_subtract_words(ctx)
|
|
ctx.wordlist_subtract_single.assert_not_called()
|
|
|
|
|
|
class TestWordlistShard:
|
|
def test_calls_wordlist_gate_with_correct_args(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("word1\nword2\nword3\n")
|
|
outfile = tmp_path / "shard.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "3", "0"]):
|
|
wordlist_shard(ctx)
|
|
ctx.wordlist_gate.assert_called_once_with(str(infile), str(outfile), 3, 0)
|
|
|
|
def test_rejects_offset_gte_mod(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("word1\n")
|
|
outfile = tmp_path / "shard.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "3", "3"]):
|
|
wordlist_shard(ctx)
|
|
ctx.wordlist_gate.assert_not_called()
|
|
|
|
def test_rejects_nonexistent_infile(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
with patch("builtins.input", return_value="/nonexistent/file.txt"):
|
|
wordlist_shard(ctx)
|
|
ctx.wordlist_gate.assert_not_called()
|
|
|
|
def test_rejects_mod_less_than_2(self, tmp_path):
|
|
ctx = _make_ctx()
|
|
infile = tmp_path / "in.txt"
|
|
infile.write_text("word1\n")
|
|
outfile = tmp_path / "shard.txt"
|
|
with patch("builtins.input", side_effect=[str(infile), str(outfile), "1", "0"]):
|
|
wordlist_shard(ctx)
|
|
ctx.wordlist_gate.assert_not_called()
|
|
|
|
|
|
class TestWordlistToolsSubmenu:
|
|
def test_submenu_dispatches_to_filter_length(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.wordlist_filter_length") as mock_fn, \
|
|
patch("hate_crack.attacks.interactive_menu", side_effect=["1", "99"]):
|
|
wordlist_tools_submenu(ctx)
|
|
mock_fn.assert_called_once_with(ctx)
|
|
|
|
def test_submenu_dispatches_to_filter_charclass_include(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.wordlist_filter_charclass_include") as mock_fn, \
|
|
patch("hate_crack.attacks.interactive_menu", side_effect=["2", "99"]):
|
|
wordlist_tools_submenu(ctx)
|
|
mock_fn.assert_called_once_with(ctx)
|
|
|
|
def test_submenu_dispatches_to_filter_charclass_exclude(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.wordlist_filter_charclass_exclude") as mock_fn, \
|
|
patch("hate_crack.attacks.interactive_menu", side_effect=["3", "99"]):
|
|
wordlist_tools_submenu(ctx)
|
|
mock_fn.assert_called_once_with(ctx)
|
|
|
|
def test_submenu_dispatches_to_cut_substring(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.wordlist_cut_substring") as mock_fn, \
|
|
patch("hate_crack.attacks.interactive_menu", side_effect=["4", "99"]):
|
|
wordlist_tools_submenu(ctx)
|
|
mock_fn.assert_called_once_with(ctx)
|
|
|
|
def test_submenu_dispatches_to_split_by_length(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.wordlist_split_by_length") as mock_fn, \
|
|
patch("hate_crack.attacks.interactive_menu", side_effect=["5", "99"]):
|
|
wordlist_tools_submenu(ctx)
|
|
mock_fn.assert_called_once_with(ctx)
|
|
|
|
def test_submenu_dispatches_to_subtract_words(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.wordlist_subtract_words") as mock_fn, \
|
|
patch("hate_crack.attacks.interactive_menu", side_effect=["6", "99"]):
|
|
wordlist_tools_submenu(ctx)
|
|
mock_fn.assert_called_once_with(ctx)
|
|
|
|
def test_submenu_dispatches_to_shard(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.wordlist_shard") as mock_fn, \
|
|
patch("hate_crack.attacks.interactive_menu", side_effect=["7", "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"):
|
|
wordlist_tools_submenu(ctx)
|
|
|
|
def test_submenu_exits_on_none(self):
|
|
ctx = _make_ctx()
|
|
with patch("hate_crack.attacks.interactive_menu", return_value=None):
|
|
wordlist_tools_submenu(ctx)
|