mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat: add ngramX attack and gzip auto-detection for external binaries
- Add _is_gzipped() magic-byte detector and _wordlist_path() context manager that transparently decompresses gzip files to a temp path - Apply gzip handling to hcatCombinator3 and hcatCombinatorX via contextlib.ExitStack so compressed wordlists work without manual prep - Add hcatNgramX() wrapper using ngramX.bin <corpus> <group_size> piped to hashcat, with gzip auto-detection on the corpus file - Add ngram_attack() handler in attacks.py with tab-autocomplete corpus selection and configurable group size (default 3) - Register attack as menu option 19 in both main.py and hate_crack.py - Fix wordlist_optimizer.py: .app extension on macOS was wrong, use .bin - Add tests/test_ngram_gzip.py covering ngram_attack handler, _is_gzipped, and _wordlist_path context manager (temp file cleanup, plain passthrough)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import gzip
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from hate_crack.attacks import ngram_attack
|
||||
|
||||
|
||||
def _make_ctx(hash_type="1000", hash_file="/tmp/hashes.txt"):
|
||||
ctx = MagicMock()
|
||||
ctx.hcatHashType = hash_type
|
||||
ctx.hcatHashFile = hash_file
|
||||
return ctx
|
||||
|
||||
|
||||
class TestNgramAttack:
|
||||
def test_calls_hcatNgramX_with_corpus_and_group_size(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\nletmein\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="3"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once_with(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, str(corpus), 3
|
||||
)
|
||||
|
||||
def test_default_group_size_is_3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value=""):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once()
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 3
|
||||
|
||||
def test_invalid_group_size_defaults_to_3(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="abc"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_called_once()
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 3
|
||||
|
||||
def test_aborts_when_no_corpus_selected(self):
|
||||
ctx = _make_ctx()
|
||||
ctx.select_file_with_autocomplete.return_value = None
|
||||
|
||||
ngram_attack(ctx)
|
||||
|
||||
ctx.hcatNgramX.assert_not_called()
|
||||
|
||||
def test_custom_group_size_passed_through(self, tmp_path):
|
||||
ctx = _make_ctx()
|
||||
corpus = tmp_path / "corpus.txt"
|
||||
corpus.write_text("password\n")
|
||||
ctx.select_file_with_autocomplete.return_value = str(corpus)
|
||||
|
||||
with patch("builtins.input", return_value="5"):
|
||||
ngram_attack(ctx)
|
||||
|
||||
assert ctx.hcatNgramX.call_args[0][3] == 5
|
||||
|
||||
|
||||
class TestIsGzipped:
|
||||
def test_detects_gzip_file(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
gz_file = tmp_path / "test.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\n")
|
||||
|
||||
assert _is_gzipped(str(gz_file)) is True
|
||||
|
||||
def test_plain_file_not_detected_as_gzip(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
plain = tmp_path / "test.txt"
|
||||
plain.write_bytes(b"password\n")
|
||||
|
||||
assert _is_gzipped(str(plain)) is False
|
||||
|
||||
def test_missing_file_returns_false(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
assert _is_gzipped(str(tmp_path / "nonexistent.txt")) is False
|
||||
|
||||
def test_empty_file_returns_false(self, tmp_path):
|
||||
from hate_crack.main import _is_gzipped
|
||||
|
||||
empty = tmp_path / "empty.txt"
|
||||
empty.write_bytes(b"")
|
||||
|
||||
assert _is_gzipped(str(empty)) is False
|
||||
|
||||
|
||||
class TestWordlistPath:
|
||||
def test_plain_file_yields_original_path(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
plain = tmp_path / "words.txt"
|
||||
plain.write_text("password\n")
|
||||
|
||||
with _wordlist_path(str(plain)) as result:
|
||||
assert result == str(plain)
|
||||
|
||||
def test_gzip_file_yields_temp_file_with_content(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
gz_file = tmp_path / "words.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\nletmein\n")
|
||||
|
||||
with _wordlist_path(str(gz_file)) as result:
|
||||
assert result != str(gz_file)
|
||||
assert os.path.isfile(result)
|
||||
with open(result, "rb") as f:
|
||||
assert f.read() == b"password\nletmein\n"
|
||||
|
||||
def test_gzip_temp_file_removed_after_context(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
gz_file = tmp_path / "words.txt.gz"
|
||||
with gzip.open(str(gz_file), "wb") as f:
|
||||
f.write(b"password\n")
|
||||
|
||||
with _wordlist_path(str(gz_file)) as result:
|
||||
tmp_path_used = result
|
||||
|
||||
assert not os.path.exists(tmp_path_used)
|
||||
|
||||
def test_plain_file_not_deleted_after_context(self, tmp_path):
|
||||
from hate_crack.main import _wordlist_path
|
||||
|
||||
plain = tmp_path / "words.txt"
|
||||
plain.write_text("password\n")
|
||||
|
||||
with _wordlist_path(str(plain)) as result:
|
||||
assert result == str(plain)
|
||||
|
||||
assert plain.exists()
|
||||
Reference in New Issue
Block a user