From 67c462cd415fc74905ffa6f3b5d1532183e6d074 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 17 Mar 2026 15:05:36 -0400 Subject: [PATCH] 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 --- tests/test_omen_attack.py | 276 +++++++++++++++++++++++++++++++------- 1 file changed, 227 insertions(+), 49 deletions(-) diff --git a/tests/test_omen_attack.py b/tests/test_omen_attack.py index 6ee3f40..9fde1d6 100644 --- a/tests/test_omen_attack.py +++ b/tests/test_omen_attack.py @@ -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