mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
fix: OMEN attack silent failure - validate model files, capture enumNG errors, add wordlist picker
- Add _omen_model_is_valid() checking all 5 required model files - Add _omen_model_info() reading model_info.json metadata - hcatOmenTrain returns bool and writes training metadata - Capture enumNG stderr and report errors instead of silent failure - Rewrite omen_attack with train/use/cancel menu and wordlist picker
This commit is contained in:
+227
-49
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -55,18 +56,66 @@ class TestHcatOmenTrain:
|
||||
assert "-l" in cmd
|
||||
assert str(model_dir / "LN") in cmd
|
||||
|
||||
def test_missing_binary(self, main_module, tmp_path, capsys):
|
||||
def test_returns_true_and_writes_metadata_on_success(self, main_module, tmp_path):
|
||||
training_file = tmp_path / "passwords.txt"
|
||||
training_file.write_text("password123\nletmein\n")
|
||||
omen_dir = tmp_path / "omen"
|
||||
omen_dir.mkdir()
|
||||
create_bin = omen_dir / "createNG"
|
||||
create_bin.touch()
|
||||
create_bin.chmod(0o755)
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
|
||||
with patch.object(main_module, "_omen_dir", str(omen_dir)), \
|
||||
patch.object(main_module, "hcatOmenCreateBin", "createNG"), \
|
||||
patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.wait.return_value = None
|
||||
mock_proc.returncode = 0
|
||||
mock_popen.return_value = mock_proc
|
||||
|
||||
result = main_module.hcatOmenTrain(str(training_file))
|
||||
|
||||
assert result is True
|
||||
info_path = model_dir / "model_info.json"
|
||||
assert info_path.exists()
|
||||
info = json.loads(info_path.read_text())
|
||||
assert info["training_file"] == str(training_file)
|
||||
assert "trained_at" in info
|
||||
|
||||
def test_returns_false_on_failure(self, main_module, tmp_path):
|
||||
training_file = tmp_path / "passwords.txt"
|
||||
training_file.write_text("test\n")
|
||||
omen_dir = tmp_path / "omen"
|
||||
omen_dir.mkdir()
|
||||
create_bin = omen_dir / "createNG"
|
||||
create_bin.touch()
|
||||
create_bin.chmod(0o755)
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), patch.object(
|
||||
main_module, "_omen_dir", str(tmp_path / "omen")
|
||||
), patch.object(main_module, "hcatOmenCreateBin", "createNG"):
|
||||
main_module.hcatOmenTrain(str(training_file))
|
||||
captured = capsys.readouterr()
|
||||
assert "createNG binary not found" in captured.out
|
||||
with patch.object(main_module, "_omen_dir", str(omen_dir)), \
|
||||
patch.object(main_module, "hcatOmenCreateBin", "createNG"), \
|
||||
patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.wait.return_value = None
|
||||
mock_proc.returncode = 1
|
||||
mock_popen.return_value = mock_proc
|
||||
|
||||
def test_missing_training_file(self, main_module, tmp_path, capsys):
|
||||
result = main_module.hcatOmenTrain(str(training_file))
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_missing_binary_returns_false(self, main_module, tmp_path):
|
||||
with patch.object(main_module, "_omen_dir", str(tmp_path / "omen")), \
|
||||
patch.object(main_module, "hcatOmenCreateBin", "createNG"):
|
||||
result = main_module.hcatOmenTrain("/nonexistent/file.txt")
|
||||
assert result is False
|
||||
|
||||
def test_missing_training_file_returns_false(self, main_module, tmp_path):
|
||||
omen_dir = tmp_path / "omen"
|
||||
omen_dir.mkdir()
|
||||
create_bin = omen_dir / "createNG"
|
||||
@@ -74,14 +123,11 @@ class TestHcatOmenTrain:
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
|
||||
with patch.object(main_module, "hate_path", str(tmp_path)), patch.object(
|
||||
main_module, "_omen_dir", str(omen_dir)
|
||||
), patch.object(
|
||||
main_module, "hcatOmenCreateBin", "createNG"
|
||||
), patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)):
|
||||
main_module.hcatOmenTrain("/nonexistent/file.txt")
|
||||
captured = capsys.readouterr()
|
||||
assert "Training file not found" in captured.out
|
||||
with patch.object(main_module, "_omen_dir", str(omen_dir)), \
|
||||
patch.object(main_module, "hcatOmenCreateBin", "createNG"), \
|
||||
patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)):
|
||||
result = main_module.hcatOmenTrain("/nonexistent/file.txt")
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestHcatOmen:
|
||||
@@ -112,6 +158,7 @@ class TestHcatOmen:
|
||||
) as mock_popen:
|
||||
mock_enum_proc = MagicMock()
|
||||
mock_enum_proc.stdout = MagicMock()
|
||||
mock_enum_proc.returncode = 0
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_enum_proc.wait.return_value = None
|
||||
@@ -163,47 +210,178 @@ class TestHcatOmen:
|
||||
captured = capsys.readouterr()
|
||||
assert "OMEN model not found" in captured.out
|
||||
|
||||
def test_prints_enumng_stderr_on_failure(self, main_module, tmp_path, capsys):
|
||||
omen_dir = tmp_path / "omen"
|
||||
omen_dir.mkdir()
|
||||
enum_bin = omen_dir / "enumNG"
|
||||
enum_bin.touch()
|
||||
enum_bin.chmod(0o755)
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
(model_dir / "createConfig").write_text("# test config\n")
|
||||
|
||||
with patch.object(main_module, "_omen_dir", str(omen_dir)), \
|
||||
patch.object(main_module, "hcatOmenEnumBin", "enumNG"), \
|
||||
patch.object(main_module, "hcatBin", "hashcat"), \
|
||||
patch.object(main_module, "hcatTuning", ""), \
|
||||
patch.object(main_module, "hcatPotfilePath", ""), \
|
||||
patch.object(main_module, "hcatHashFile", "/tmp/hashes.txt", create=True), \
|
||||
patch("hate_crack.main._omen_model_dir", return_value=str(model_dir)), \
|
||||
patch("hate_crack.main.subprocess.Popen") as mock_popen:
|
||||
mock_enum_proc = MagicMock()
|
||||
mock_enum_proc.stdout = MagicMock()
|
||||
mock_enum_proc.stderr.read.return_value = b"ERROR: Could not open CP.level"
|
||||
mock_enum_proc.wait.return_value = None
|
||||
mock_enum_proc.returncode = 1
|
||||
mock_hashcat_proc = MagicMock()
|
||||
mock_hashcat_proc.wait.return_value = None
|
||||
mock_popen.side_effect = [mock_enum_proc, mock_hashcat_proc]
|
||||
|
||||
main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "enumNG failed" in captured.out
|
||||
assert "Could not open CP.level" in captured.out
|
||||
|
||||
|
||||
class TestOmenAttackHandler:
|
||||
def test_prompts_and_calls_hcatOmen(self):
|
||||
def _make_ctx(self, tmp_path, model_valid=True):
|
||||
ctx = MagicMock()
|
||||
ctx.hate_path = "/fake/path"
|
||||
ctx.omenTrainingList = "/fake/rockyou.txt"
|
||||
ctx.hate_path = str(tmp_path)
|
||||
ctx._omen_dir = str(tmp_path / "omen")
|
||||
ctx.hcatOmenCreateBin = "createNG"
|
||||
ctx.hcatOmenEnumBin = "enumNG"
|
||||
ctx.omenTrainingList = "/default/rockyou.txt"
|
||||
ctx.omenMaxCandidates = 1000000
|
||||
ctx.hcatHashType = "1000"
|
||||
ctx.hcatHashFile = "/tmp/hashes.txt"
|
||||
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
||||
ctx._omen_model_is_valid.return_value = model_valid
|
||||
ctx._omen_model_info.return_value = (
|
||||
{"training_file": "/old/rockyou.txt"} if model_valid else None
|
||||
)
|
||||
ctx._omen_model_dir.return_value = str(tmp_path / "model")
|
||||
ctx.hcatOmenTrain.return_value = True
|
||||
ctx.list_wordlist_files.return_value = ["rockyou.txt", "custom.txt"]
|
||||
return ctx
|
||||
|
||||
def fake_isfile(path):
|
||||
# Binaries exist, model exists
|
||||
return True
|
||||
|
||||
with patch("os.path.isfile", side_effect=fake_isfile), patch(
|
||||
"os.path.expanduser", return_value="/fake/home"
|
||||
), patch("builtins.input", return_value=""):
|
||||
def test_use_existing_model(self, tmp_path):
|
||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||
with patch("os.path.isfile", return_value=True), patch(
|
||||
"builtins.input", side_effect=["1", ""]
|
||||
):
|
||||
from hate_crack.attacks import omen_attack
|
||||
|
||||
omen_attack(ctx)
|
||||
|
||||
ctx.hcatOmen.assert_called_once_with("1000", "/tmp/hashes.txt", 1000000)
|
||||
|
||||
def test_trains_when_no_model(self):
|
||||
ctx = MagicMock()
|
||||
ctx.hate_path = "/fake/path"
|
||||
ctx.omenTrainingList = "/fake/rockyou.txt"
|
||||
ctx.omenMaxCandidates = 1000000
|
||||
ctx.hcatHashType = "1000"
|
||||
ctx.hcatHashFile = "/tmp/hashes.txt"
|
||||
|
||||
def fake_isfile(path):
|
||||
# Binaries exist, but createConfig does not
|
||||
return "createConfig" not in path
|
||||
|
||||
with patch("os.path.isfile", side_effect=fake_isfile), patch(
|
||||
"os.path.expanduser", return_value="/fake/home"
|
||||
), patch("builtins.input", return_value=""):
|
||||
from hate_crack.attacks import omen_attack
|
||||
|
||||
omen_attack(ctx)
|
||||
|
||||
ctx.hcatOmenTrain.assert_called_once_with("/fake/rockyou.txt")
|
||||
ctx.hcatOmenTrain.assert_not_called()
|
||||
ctx.hcatOmen.assert_called_once()
|
||||
|
||||
def test_train_new_model_with_wordlist_pick(self, tmp_path):
|
||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||
with patch("os.path.isfile", return_value=True), patch(
|
||||
"builtins.input", side_effect=["2", "1", ""]
|
||||
):
|
||||
from hate_crack.attacks import omen_attack
|
||||
|
||||
omen_attack(ctx)
|
||||
ctx.hcatOmenTrain.assert_called_once()
|
||||
training_arg = ctx.hcatOmenTrain.call_args[0][0]
|
||||
assert "rockyou.txt" in training_arg
|
||||
ctx.hcatOmen.assert_called_once()
|
||||
|
||||
def test_cancel_aborts(self, tmp_path):
|
||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||
with patch("os.path.isfile", return_value=True), patch(
|
||||
"builtins.input", side_effect=["3"]
|
||||
):
|
||||
from hate_crack.attacks import omen_attack
|
||||
|
||||
omen_attack(ctx)
|
||||
ctx.hcatOmenTrain.assert_not_called()
|
||||
ctx.hcatOmen.assert_not_called()
|
||||
|
||||
def test_no_model_goes_straight_to_training(self, tmp_path):
|
||||
ctx = self._make_ctx(tmp_path, model_valid=False)
|
||||
with patch("os.path.isfile", return_value=True), patch(
|
||||
"builtins.input", side_effect=["1", ""]
|
||||
):
|
||||
from hate_crack.attacks import omen_attack
|
||||
|
||||
omen_attack(ctx)
|
||||
ctx.hcatOmenTrain.assert_called_once()
|
||||
ctx.hcatOmen.assert_called_once()
|
||||
|
||||
def test_training_failure_aborts_enumeration(self, tmp_path):
|
||||
ctx = self._make_ctx(tmp_path, model_valid=False)
|
||||
ctx.hcatOmenTrain.return_value = False
|
||||
with patch("os.path.isfile", return_value=True), patch(
|
||||
"builtins.input", side_effect=["1"]
|
||||
):
|
||||
from hate_crack.attacks import omen_attack
|
||||
|
||||
omen_attack(ctx)
|
||||
ctx.hcatOmen.assert_not_called()
|
||||
|
||||
def test_custom_path_for_training(self, tmp_path):
|
||||
ctx = self._make_ctx(tmp_path, model_valid=False)
|
||||
with patch("os.path.isfile", return_value=True), patch(
|
||||
"builtins.input", side_effect=["p", "/custom/wordlist.txt", ""]
|
||||
):
|
||||
from hate_crack.attacks import omen_attack
|
||||
|
||||
omen_attack(ctx)
|
||||
ctx.hcatOmenTrain.assert_called_once_with("/custom/wordlist.txt")
|
||||
|
||||
|
||||
class TestOmenModelValidation:
|
||||
@pytest.fixture
|
||||
def model_dir(self, tmp_path):
|
||||
d = tmp_path / "model"
|
||||
d.mkdir()
|
||||
return d
|
||||
|
||||
def _create_valid_model(self, model_dir):
|
||||
for name in ["createConfig", "CP.level", "IP.level", "EP.level", "LN.level"]:
|
||||
(model_dir / name).write_text("data")
|
||||
|
||||
def test_valid_model_returns_true(self, main_module, model_dir):
|
||||
self._create_valid_model(model_dir)
|
||||
assert main_module._omen_model_is_valid(str(model_dir)) is True
|
||||
|
||||
def test_missing_level_file_returns_false(self, main_module, model_dir):
|
||||
self._create_valid_model(model_dir)
|
||||
(model_dir / "CP.level").unlink()
|
||||
assert main_module._omen_model_is_valid(str(model_dir)) is False
|
||||
|
||||
def test_empty_file_returns_false(self, main_module, model_dir):
|
||||
self._create_valid_model(model_dir)
|
||||
(model_dir / "EP.level").write_text("")
|
||||
assert main_module._omen_model_is_valid(str(model_dir)) is False
|
||||
|
||||
def test_missing_dir_returns_false(self, main_module, tmp_path):
|
||||
assert main_module._omen_model_is_valid(str(tmp_path / "nonexistent")) is False
|
||||
|
||||
def test_config_only_returns_false(self, main_module, model_dir):
|
||||
(model_dir / "createConfig").write_text("data")
|
||||
assert main_module._omen_model_is_valid(str(model_dir)) is False
|
||||
|
||||
|
||||
class TestOmenModelInfo:
|
||||
def test_returns_info_when_metadata_exists(self, main_module, tmp_path):
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
info = {"training_file": "/path/to/rockyou.txt", "trained_at": "2026-03-17T12:00:00"}
|
||||
(model_dir / "model_info.json").write_text(json.dumps(info))
|
||||
result = main_module._omen_model_info(str(model_dir))
|
||||
assert result["training_file"] == "/path/to/rockyou.txt"
|
||||
|
||||
def test_returns_none_when_no_metadata(self, main_module, tmp_path):
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
assert main_module._omen_model_info(str(model_dir)) is None
|
||||
|
||||
def test_returns_none_on_corrupt_json(self, main_module, tmp_path):
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
(model_dir / "model_info.json").write_text("not json")
|
||||
assert main_module._omen_model_info(str(model_dir)) is None
|
||||
|
||||
Reference in New Issue
Block a user