From 6fb261d76fa88f3dfb5a93f132d4a4c3c7cd178b Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 17 Feb 2026 14:01:58 -0500 Subject: [PATCH 1/5] feat: add OMEN attack as menu option 16 Add OMEN (Ordered Markov ENumerator) as a probability-ordered password candidate generator. Trains n-gram models on leaked passwords via createNG, then pipes candidates from enumNG into hashcat. Also fix a pre-existing bug where ensure_binary() used quit(1) instead of sys.exit(1) - quit() closes stdin before raising SystemExit, which caused "ValueError: I/O operation on closed file" when any optional binary check failed and the program continued to use input(). Co-Authored-By: Claude Opus 4.6 --- .gitmodules | 3 + Makefile | 9 +- config.json.example | 4 +- hate_crack.py | 1 + hate_crack/attacks.py | 20 +++++ hate_crack/main.py | 118 ++++++++++++++++++++++++-- omen | 1 + tests/test_omen_attack.py | 151 ++++++++++++++++++++++++++++++++++ tests/test_ui_menu_options.py | 1 + 9 files changed, 299 insertions(+), 9 deletions(-) create mode 160000 omen create mode 100644 tests/test_omen_attack.py diff --git a/.gitmodules b/.gitmodules index 965b1ca..e084200 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "HashcatRosetta"] path = HashcatRosetta url = https://github.com/bandrel/HashcatRosetta.git +[submodule "omen"] + path = omen + url = https://github.com/RUB-SysSec/OMEN.git diff --git a/Makefile b/Makefile index 9278900..87fc76f 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ submodules-pre: @# Ensure required directories exist (whether as submodules or vendored copies). @test -d hashcat-utils || { echo "Error: missing required directory: hashcat-utils"; exit 1; } @test -d princeprocessor || { echo "Error: missing required directory: princeprocessor"; exit 1; } + @test -d omen || { echo "Warning: missing directory: omen (OMEN attacks will not be available)"; } @# Keep per-length expander sources in sync (expander8.c..expander24.c). @# Patch hashcat-utils/src/Makefile so these new expanders are compiled by default. @bases="hashcat-utils hate_crack/hashcat-utils"; for base in $$bases; do src="$$base/src/expander.c"; test -f "$$src" || continue; for i in $$(seq 8 36); do dst="$$base/src/expander$$i.c"; if [ ! -f "$$dst" ]; then cp "$$src" "$$dst"; perl -pi -e "s/#define LEN_MAX 7/#define LEN_MAX $$i/g" "$$dst"; fi; done; mk="$$base/src/Makefile"; test -f "$$mk" || continue; exp_bins=""; exp_exes=""; for i in $$(seq 8 36); do exp_bins="$$exp_bins expander$$i.bin"; exp_exes="$$exp_exes expander$$i.exe"; done; EXP_BINS="$$exp_bins" perl -pi -e 'if(/^native:/ && index($$_, "expander8.bin") < 0){chomp; $$_ .= "$$ENV{EXP_BINS}"; $$_ .= "\n";}' "$$mk"; EXP_EXES="$$exp_exes" perl -pi -e 'if(/^windows:/ && index($$_, "expander8.exe") < 0){chomp; $$_ .= "$$ENV{EXP_EXES}"; $$_ .= "\n";}' "$$mk"; perl -0777 -pi -e 's/\n# Auto-added by hate_crack \\(submodules-pre\\)\n.*\z/\n/s' "$$mk"; printf '%s\n' '' '# Auto-added by hate_crack (submodules-pre)' 'expander%.bin: src/expander%.c' >> "$$mk"; printf '\t%s\n' '$${CC_NATIVE} $${CFLAGS_NATIVE} $${LDFLAGS_NATIVE} -o bin/$$@ $$<' >> "$$mk"; printf '%s\n' '' 'expander%.exe: src/expander%.c' >> "$$mk"; printf '\t%s\n' '$${CC_WINDOWS} $${CFLAGS_WINDOWS} -o bin/$$@ $$<' >> "$$mk"; done @@ -34,14 +35,18 @@ vendor-assets: exit 1; \ fi @echo "Syncing assets into package for uv tool install..." - @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor + @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor hate_crack/omen @cp -R hashcat-utils hate_crack/ @cp -R princeprocessor hate_crack/ + @if [ -d omen ]; then \ + cp -R omen hate_crack/; \ + rm -rf hate_crack/omen/.git; \ + fi @rm -rf hate_crack/hashcat-utils/.git hate_crack/princeprocessor/.git clean-vendor: @echo "Cleaning up vendored assets from working tree..." - @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor + @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor hate_crack/omen install: submodules vendor-assets @echo "Detecting OS and installing dependencies..." diff --git a/config.json.example b/config.json.example index fa5b07e..424e762 100644 --- a/config.json.example +++ b/config.json.example @@ -24,5 +24,7 @@ "hashview_api_key": "", "hashmob_api_key": "", "ollamaModel": "mistral", - "ollamaNumCtx": 2048 + "ollamaNumCtx": 2048, + "omenTrainingList": "rockyou.txt", + "omenMaxCandidates": 1000000 } diff --git a/hate_crack.py b/hate_crack.py index f018b00..a73b440 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -84,6 +84,7 @@ def get_main_menu_options(): "13": _attacks.bandrel_method, "14": _attacks.loopback_attack, "15": _attacks.ollama_attack, + "16": _attacks.omen_attack, "90": download_hashmob_rules, "91": weakpass_wordlist_menu, "92": download_hashmob_wordlists, diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index 420e23d..29c989a 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -499,3 +499,23 @@ def ollama_attack(ctx: Any) -> None: "location": location, } ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "target", target_info) + + +def omen_attack(ctx: Any) -> None: + print("\n\tOMEN Attack (Ordered Markov ENumerator)") + omen_dir = os.path.join(ctx.hate_path, "omen") + model_exists = os.path.isfile(os.path.join(omen_dir, "IP.level")) + if not model_exists: + print("\n\tNo OMEN model found. Training is required before generation.") + training_source = input( + "\n\tTraining source (path to password list, or press Enter for default): " + ).strip() + if not training_source: + training_source = ctx.omenTrainingList + ctx.hcatOmenTrain(training_source) + max_candidates = input( + f"\n\tMax candidates to generate ({ctx.omenMaxCandidates}): " + ).strip() + if not max_candidates: + max_candidates = str(ctx.omenMaxCandidates) + ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates)) diff --git a/hate_crack/main.py b/hate_crack/main.py index 24ef4ad..cf23d04 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -212,19 +212,19 @@ def ensure_binary(binary_path, build_dir=None, name=None): "\nRun 'make install' from the repository directory to install with assets:" ) print(" cd /path/to/hate_crack && make install") - quit(1) + sys.exit(1) # Binary missing - need to build print(f"Error: {name or 'binary'} not found at {binary_path}.") print("\nPlease build the utilities by running:") print(f" cd {build_dir} && make") print("\nEnsure build tools (gcc, make) are installed on your system.") - quit(1) + sys.exit(1) else: print( f"Error: {name or binary_path} not found or not executable at {binary_path}." ) - quit(1) + sys.exit(1) return binary_path @@ -468,10 +468,31 @@ except KeyError as e: ) ollamaNumCtx = int(default_config.get("ollamaNumCtx", 2048)) +try: + omenTrainingList = config_parser["omenTrainingList"] +except KeyError as e: + print( + "{0} is not defined in config.json using defaults from config.json.example".format( + e + ) + ) + omenTrainingList = default_config.get("omenTrainingList", "rockyou.txt") +try: + omenMaxCandidates = int(config_parser["omenMaxCandidates"]) +except KeyError as e: + print( + "{0} is not defined in config.json using defaults from config.json.example".format( + e + ) + ) + omenMaxCandidates = int(default_config.get("omenMaxCandidates", 1000000)) + hcatExpanderBin = "expander.bin" hcatCombinatorBin = "combinator.bin" hcatPrinceBin = "pp64.bin" hcatHcstat2genBin = "hcstat2gen.bin" +hcatOmenCreateBin = "createNG" +hcatOmenEnumBin = "enumNG" def _resolve_wordlist_path(wordlist, base_dir): @@ -606,6 +627,7 @@ hcatGoodMeasureBaseList = _normalize_wordlist_setting( hcatGoodMeasureBaseList, wordlists_dir ) hcatPrinceBaseList = _normalize_wordlist_setting(hcatPrinceBaseList, wordlists_dir) +omenTrainingList = _normalize_wordlist_setting(omenTrainingList, wordlists_dir) if not SKIP_INIT: # Verify hashcat binary is available # hcatBin should be in PATH or be an absolute path (resolved from hcatPath + hcatBin if configured) @@ -615,14 +637,14 @@ if not SKIP_INIT: print( f"Hashcat binary not found at {hcatBin}. Please check configuration and try again." ) - quit(1) + sys.exit(1) else: # hcatBin should be in PATH if shutil.which(hcatBin) is None: print( f'Hashcat binary "{hcatBin}" not found in PATH. Please check configuration and try again.' ) - quit(1) + sys.exit(1) # Verify hashcat-utils binaries exist and work # Note: hashcat-utils is part of hate_crack repo, not hashcat installation @@ -656,7 +678,7 @@ if not SKIP_INIT: print(f"Error: {name} binary at {binary_path} failed to execute: {e}") print("The binary may be compiled for the wrong architecture.") print("Try recompiling hashcat-utils for your system.") - quit(1) + sys.exit(1) # Verify princeprocessor binary # Note: princeprocessor is part of hate_crack repo, not hashcat installation @@ -682,6 +704,23 @@ if not SKIP_INIT: except SystemExit: print("LLM attacks will not be available.") + # Verify OMEN binaries (optional, for OMEN attack) + omen_create_path = os.path.join(hate_path, "omen", hcatOmenCreateBin) + omen_enum_path = os.path.join(hate_path, "omen", hcatOmenEnumBin) + try: + ensure_binary( + omen_create_path, + build_dir=os.path.join(hate_path, "omen"), + name="OMEN createNG", + ) + ensure_binary( + omen_enum_path, + build_dir=os.path.join(hate_path, "omen"), + name="OMEN enumNG", + ) + except SystemExit: + print("OMEN attacks will not be available.") + except Exception as e: print(f"Module initialization error: {e}") if not shutil.which("hashcat") and not os.path.exists("/usr/bin/hashcat"): @@ -2072,6 +2111,67 @@ def hcatPrince(hcatHashType, hcatHashFile): prince_proc.kill() +# OMEN Attack - Train model +def hcatOmenTrain(training_file): + omen_dir = os.path.join(hate_path, "omen") + create_bin = os.path.join(omen_dir, hcatOmenCreateBin) + if not os.path.isfile(create_bin): + print(f"Error: OMEN createNG binary not found: {create_bin}") + return + if not os.path.isfile(training_file): + print(f"Error: Training file not found: {training_file}") + return + print(f"Training OMEN model with: {training_file}") + cmd = [create_bin, "--iPwdList", training_file] + print(f"[*] Running: {_format_cmd(cmd)}") + proc = subprocess.Popen(cmd, cwd=omen_dir) + try: + proc.wait() + except KeyboardInterrupt: + print("Killing PID {0}...".format(str(proc.pid))) + proc.kill() + return + if proc.returncode == 0: + print("OMEN model training complete.") + else: + print(f"OMEN training failed with exit code {proc.returncode}") + + +# OMEN Attack - Generate candidates and pipe to hashcat +def hcatOmen(hcatHashType, hcatHashFile, max_candidates): + global hcatProcess + omen_dir = os.path.join(hate_path, "omen") + enum_bin = os.path.join(omen_dir, hcatOmenEnumBin) + if not os.path.isfile(enum_bin): + print(f"Error: OMEN enumNG binary not found: {enum_bin}") + return + enum_cmd = [enum_bin, "-p", "-m", str(max_candidates)] + hashcat_cmd = [ + hcatBin, + "-m", + hcatHashType, + hcatHashFile, + "--session", + generate_session_id(), + "-o", + f"{hcatHashFile}.out", + ] + hashcat_cmd.extend(shlex.split(hcatTuning)) + _append_potfile_arg(hashcat_cmd) + print(f"[*] Running: {_format_cmd(enum_cmd)} | {_format_cmd(hashcat_cmd)}") + _debug_cmd(hashcat_cmd) + enum_proc = subprocess.Popen(enum_cmd, cwd=omen_dir, stdout=subprocess.PIPE) + hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout) + enum_proc.stdout.close() + try: + hcatProcess.wait() + enum_proc.wait() + except KeyboardInterrupt: + print("Killing PID {0}...".format(str(hcatProcess.pid))) + hcatProcess.kill() + enum_proc.kill() + + # Extra - Good Measure def hcatGoodMeasure(hcatHashType, hcatHashFile): global hcatExtraCount @@ -2987,6 +3087,10 @@ def ollama_attack(): return _attacks.ollama_attack(_attack_ctx()) +def omen_attack(): + return _attacks.omen_attack(_attack_ctx()) + + # convert hex words for recycling def convert_hex(working_file): processed_words = [] @@ -3215,6 +3319,7 @@ def get_main_menu_options(): "13": bandrel_method, "14": loopback_attack, "15": ollama_attack, + "16": omen_attack, "90": download_hashmob_rules, "91": analyze_rules, "92": download_hashmob_wordlists, @@ -3867,6 +3972,7 @@ def main(): print("\t(13) Bandrel Methodology") print("\t(14) Loopback Attack") print("\t(15) LLM Attack") + print("\t(16) OMEN Attack") print("\n\t(90) Download rules from Hashmob.net") print("\n\t(91) Analyze Hashcat Rules") print("\t(92) Download wordlists from Hashmob.net") diff --git a/omen b/omen new file mode 160000 index 0000000..10aa99e --- /dev/null +++ b/omen @@ -0,0 +1 @@ +Subproject commit 10aa99e30bb88a10052d389feb53f739254eb1d1 diff --git a/tests/test_omen_attack.py b/tests/test_omen_attack.py new file mode 100644 index 0000000..cd8ae12 --- /dev/null +++ b/tests/test_omen_attack.py @@ -0,0 +1,151 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest + + +@pytest.fixture +def main_module(hc_module): + """Return the underlying hate_crack.main module for direct patching.""" + return hc_module._main + + +class TestHcatOmenTrain: + def test_builds_correct_command(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) + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenCreateBin", "createNG" + ), 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 + + main_module.hcatOmenTrain(str(training_file)) + + mock_popen.assert_called_once() + cmd = mock_popen.call_args[0][0] + assert cmd[0] == str(create_bin) + assert "--iPwdList" in cmd + assert str(training_file) in cmd + + def test_missing_binary(self, main_module, tmp_path, capsys): + training_file = tmp_path / "passwords.txt" + training_file.write_text("test\n") + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenCreateBin", "createNG" + ): + main_module.hcatOmenTrain(str(training_file)) + captured = capsys.readouterr() + assert "createNG binary not found" in captured.out + + def test_missing_training_file(self, main_module, tmp_path, capsys): + omen_dir = tmp_path / "omen" + omen_dir.mkdir() + create_bin = omen_dir / "createNG" + create_bin.touch() + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenCreateBin", "createNG" + ): + main_module.hcatOmenTrain("/nonexistent/file.txt") + captured = capsys.readouterr() + assert "Training file not found" in captured.out + + +class TestHcatOmen: + def test_builds_correct_pipe_commands(self, main_module, tmp_path): + omen_dir = tmp_path / "omen" + omen_dir.mkdir() + enum_bin = omen_dir / "enumNG" + enum_bin.touch() + enum_bin.chmod(0o755) + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenEnumBin", "enumNG" + ), patch.object(main_module, "hcatBin", "hashcat"), patch.object( + main_module, "hcatTuning", "--force" + ), patch.object( + main_module, "hcatPotfilePath", "" + ), patch.object( + main_module, "hcatHashFile", "/tmp/hashes.txt", create=True + ), patch( + "hate_crack.main.subprocess.Popen" + ) as mock_popen: + mock_enum_proc = MagicMock() + mock_enum_proc.stdout = MagicMock() + mock_hashcat_proc = MagicMock() + mock_hashcat_proc.wait.return_value = None + mock_enum_proc.wait.return_value = None + mock_popen.side_effect = [mock_enum_proc, mock_hashcat_proc] + + main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000) + + assert mock_popen.call_count == 2 + # First call: enumNG + enum_cmd = mock_popen.call_args_list[0][0][0] + assert enum_cmd[0] == str(enum_bin) + assert "-p" in enum_cmd + assert "-m" in enum_cmd + assert "500000" in enum_cmd + # Second call: hashcat + hashcat_cmd = mock_popen.call_args_list[1][0][0] + assert hashcat_cmd[0] == "hashcat" + assert "1000" in hashcat_cmd + assert "/tmp/hashes.txt" in hashcat_cmd + + def test_missing_binary(self, main_module, tmp_path, capsys): + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenEnumBin", "enumNG" + ): + main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000) + captured = capsys.readouterr() + assert "enumNG binary not found" in captured.out + + +class TestOmenAttackHandler: + def test_prompts_and_calls_hcatOmen(self): + ctx = MagicMock() + ctx.hate_path = "/fake/path" + ctx.omenTrainingList = "/fake/rockyou.txt" + ctx.omenMaxCandidates = 1000000 + ctx.hcatHashType = "1000" + ctx.hcatHashFile = "/tmp/hashes.txt" + + with patch("os.path.isfile", return_value=True), patch( + "builtins.input", return_value="" + ): + 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): + return "IP.level" not in path + + with patch("os.path.isfile", side_effect=fake_isfile), 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.hcatOmen.assert_called_once() diff --git a/tests/test_ui_menu_options.py b/tests/test_ui_menu_options.py index 9ecf38f..2df6ebd 100644 --- a/tests/test_ui_menu_options.py +++ b/tests/test_ui_menu_options.py @@ -26,6 +26,7 @@ MENU_OPTION_TEST_CASES = [ ("13", CLI_MODULE._attacks, "bandrel_method", "bandrel"), ("14", CLI_MODULE._attacks, "loopback_attack", "loopback"), ("15", CLI_MODULE._attacks, "ollama_attack", "ollama"), + ("16", CLI_MODULE._attacks, "omen_attack", "omen"), ("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"), ("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"), ("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"), From 1fa8b61d827dab14bb3f35c766ebb406ae86abdd Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 17 Feb 2026 14:03:39 -0500 Subject: [PATCH 2/5] fix: bail out early in omen_attack handler when binaries are missing The handler was prompting for training source and max candidates even when the OMEN binaries weren't built, leading to confusing error output. Now checks for createNG and enumNG up front and returns with a build instruction if either is missing. Co-Authored-By: Claude Opus 4.6 --- hate_crack/attacks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index 29c989a..b0c5c62 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -504,6 +504,12 @@ def ollama_attack(ctx: Any) -> None: def omen_attack(ctx: Any) -> None: print("\n\tOMEN Attack (Ordered Markov ENumerator)") omen_dir = os.path.join(ctx.hate_path, "omen") + create_bin = os.path.join(omen_dir, ctx.hcatOmenCreateBin) + enum_bin = os.path.join(omen_dir, ctx.hcatOmenEnumBin) + if not os.path.isfile(create_bin) or not os.path.isfile(enum_bin): + print("\n\tOMEN binaries not found. Build them with:") + print(f"\t cd {omen_dir} && make") + return model_exists = os.path.isfile(os.path.join(omen_dir, "IP.level")) if not model_exists: print("\n\tNo OMEN model found. Training is required before generation.") From b7d44f7c65acfa74e420ffaa3c59f6c236f61029 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 17 Feb 2026 14:04:11 -0500 Subject: [PATCH 3/5] fix: detect lowercase makefile in submodule build loop OMEN uses lowercase `makefile` which was missed by the capital-M check. On case-sensitive filesystems (Linux) this meant OMEN wouldn't be built during `make submodules`. Co-Authored-By: Claude Opus 4.6 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 87fc76f..c1e2bf1 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ submodules: $(MAKE) submodules-pre; \ if [ -f .gitmodules ] && command -v git >/dev/null 2>&1; then \ for path in $$(git config --file .gitmodules --get-regexp path | awk '{print $$2}'); do \ - if [ -f "$$path/Makefile" ]; then \ + if [ -f "$$path/Makefile" ] || [ -f "$$path/makefile" ]; then \ $(MAKE) -C "$$path"; \ fi; \ done; \ From 8423491124679dc88d30090c38fc9e1957c38411 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 17 Feb 2026 15:21:07 -0500 Subject: [PATCH 4/5] fix: add types-requests dev dependency to resolve mypy import-untyped error Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8d933a9..d6530ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ hate_crack = [ "config.json.example", "hashcat-utils/**", "princeprocessor/**", + "omen/**", ] [tool.setuptools_scm] @@ -70,3 +71,8 @@ ignore_missing_imports = true testpaths = [ "tests", ] + +[dependency-groups] +dev = [ + "types-requests>=2.32.4.20260107", +] From b474cd23f66951d233beb21d198b52b6df4b94b7 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 17 Feb 2026 15:21:48 -0500 Subject: [PATCH 5/5] chore: add generated file patterns to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b2c3dac..9869655 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ hate_crack/hashcat-utils/ hate_crack/princeprocessor/ *.txt.expanded *.hcmask +*.ollama_candidates +*.filtered +research/