From d9e9930471bae279a7e9f6ed49ae1fafe793405b Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Fri, 6 Feb 2026 15:06:25 -0500 Subject: [PATCH] working loopback mode with tests --- README.md | 2 +- config.json.example | 1 + hate_crack/cli.py | 15 ++++++--- hate_crack/config.json.example | 1 + hate_crack/main.py | 60 ++++++++++++++++++++++++++++++---- tests/test_hashview.py | 4 +-- tests/test_utils.py | 11 +++++-- 7 files changed, 76 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 0b9434a..d9950c2 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,7 @@ Common options: - `--pipal-path `: Override pipal path. - `--maxruntime `: Override max runtime. - `--bandrel-basewords `: Override bandrel basewords file. -- `--debug`: Enable debug logging (writes `hate_crack.log` in repo root). +- `--debug`: Enable debug logging (writes to stderr). ### Hashview Integration diff --git a/config.json.example b/config.json.example index 5a6ca08..c6ded51 100644 --- a/config.json.example +++ b/config.json.example @@ -2,6 +2,7 @@ "hcatPath": "/path/to/hate_crack", "hcatBin": "hashcat", "hcatTuning": "--force --remove", + "hcatPotfilePath": "~/.hashcat/hashcat.potfile", "hcatWordlists": "/Passwords/wordlists", "hcatOptimizedWordlists": "/Passwords/optimized_wordlists", "rules_directory": "/path/to/hashcat/rules", diff --git a/hate_crack/cli.py b/hate_crack/cli.py index 910be72..e18ce28 100644 --- a/hate_crack/cli.py +++ b/hate_crack/cli.py @@ -1,5 +1,6 @@ import logging import os +import sys from typing import Optional @@ -18,10 +19,14 @@ def setup_logging(logger: logging.Logger, hate_path: str, debug_mode: bool) -> N if not debug_mode: return logger.setLevel(logging.DEBUG) - if not any(isinstance(h, logging.FileHandler) for h in logger.handlers): - log_path = os.path.join(hate_path, "hate_crack.log") - file_handler = logging.FileHandler(log_path) - file_handler.setFormatter( + # Debug logs go to stderr by default (no log file side effects). + has_stream = any( + isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler) + for h in logger.handlers + ) + if not has_stream: + stream_handler = logging.StreamHandler(sys.stderr) + stream_handler.setFormatter( logging.Formatter("%(asctime)s %(levelname)s %(message)s") ) - logger.addHandler(file_handler) + logger.addHandler(stream_handler) diff --git a/hate_crack/config.json.example b/hate_crack/config.json.example index ed5ee28..5b7ec14 100644 --- a/hate_crack/config.json.example +++ b/hate_crack/config.json.example @@ -2,6 +2,7 @@ "hcatPath": "", "hcatBin": "hashcat", "hcatTuning": "--force --remove", + "hcatPotfilePath": "~/.hashcat/hashcat.potfile", "hcatWordlists": "/Passwords/wordlists", "hcatOptimizedWordlists": "/Passwords/optimized_wordlists", "rules_directory": "/path/to/hashcat/rules", diff --git a/hate_crack/main.py b/hate_crack/main.py index 47faa9c..215a346 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -270,12 +270,18 @@ hcatRules: list[str] = [] # Optional: override hashcat's default potfile location. -# If unset/empty, we use hashcat's built-in default potfile behavior. -hcatPotfilePath = (config_parser.get("hcatPotfilePath") or "").strip() -if hcatPotfilePath: - hcatPotfilePath = os.path.expanduser(hcatPotfilePath) - if not os.path.isabs(hcatPotfilePath): - hcatPotfilePath = os.path.join(hate_path, hcatPotfilePath) +# Default: use ~/.hashcat/hashcat.potfile (explicitly passed to hashcat). +# Disable override with config `hcatPotfilePath: ""` or CLI `--no-potfile-path`. +if "hcatPotfilePath" not in config_parser: + hcatPotfilePath = os.path.expanduser("~/.hashcat/hashcat.potfile") +else: + _raw_pot = (config_parser.get("hcatPotfilePath") or "").strip() + if _raw_pot == "": + hcatPotfilePath = "" + else: + hcatPotfilePath = os.path.expanduser(_raw_pot) + if not os.path.isabs(hcatPotfilePath): + hcatPotfilePath = os.path.join(hate_path, hcatPotfilePath) def _append_potfile_arg(cmd, *, use_potfile_path=True, potfile_path=None): @@ -673,6 +679,16 @@ hcatProcess = 0 debug_mode = False +def _format_cmd(cmd): + # Shell-style quoting to mirror what a user could run in a terminal. + return " ".join(shlex.quote(str(part)) for part in cmd) + + +def _debug_cmd(cmd): + if debug_mode: + print(f"[DEBUG] hashcat cmd: {_format_cmd(cmd)}") + + # Sanitize filename for use as hashcat session name def generate_session_id(): """Sanitize the hashfile name for use as a hashcat session name @@ -980,7 +996,7 @@ def hcatQuickDictionary( hcatChains, wordlists, loopback=False, - use_potfile_path=False, + use_potfile_path=True, potfile_path=None, ): global hcatProcess @@ -1004,6 +1020,7 @@ def hcatQuickDictionary( cmd.extend(shlex.split(hcatChains)) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd, use_potfile_path=use_potfile_path, potfile_path=potfile_path) + _debug_cmd(cmd) hcatProcess = subprocess.Popen(cmd) try: hcatProcess.wait() @@ -2662,6 +2679,7 @@ def main(): global hashview_url, hashview_api_key global hcatPath, hcatBin, hcatWordlists, hcatOptimizedWordlists, rulesDirectory global pipalPath, maxruntime, bandrelbasewords + global hcatPotfilePath # Initialize global variables hcatHashFile = None @@ -2726,6 +2744,21 @@ def main(): help="Cleanup .out files, torrents, and extract or remove .7z archives", ) parser.add_argument("--debug", action="store_true", help="Enable debug mode") + parser.add_argument( + "--potfile-path", + dest="potfile_path", + default=None, + help=( + "Override hashcat potfile path (equivalent to hashcat --potfile-path). " + "Use empty string to disable overriding and use hashcat's built-in default." + ), + ) + parser.add_argument( + "--no-potfile-path", + dest="no_potfile_path", + action="store_true", + help="Do not pass --potfile-path to hashcat (use hashcat's built-in default).", + ) hashview_parser = None if not include_subcommands: return parser, hashview_parser @@ -2864,6 +2897,19 @@ def main(): global debug_mode debug_mode = args.debug + # CLI flags override config file. + if getattr(args, "no_potfile_path", False): + hcatPotfilePath = "" + if getattr(args, "potfile_path", None) is not None: + # Empty string means: revert to hashcat's default behavior. + if args.potfile_path.strip() == "": + hcatPotfilePath = "" + else: + p = os.path.expanduser(args.potfile_path.strip()) + if not os.path.isabs(p): + p = os.path.join(hate_path, p) + hcatPotfilePath = p + setup_logging(logger, hate_path, debug_mode) from types import SimpleNamespace diff --git a/tests/test_hashview.py b/tests/test_hashview.py index 752e426..4c76eeb 100644 --- a/tests/test_hashview.py +++ b/tests/test_hashview.py @@ -44,7 +44,7 @@ class TestHashviewAPI: @pytest.fixture def api(self): """Create a HashviewAPI instance with mocked session""" - with patch("requests.Session") as mock_session_class: + with patch("requests.Session"): api = HashviewAPI(base_url=HASHVIEW_URL, api_key=HASHVIEW_API_KEY) # Replace the session with a mock api.session = MagicMock() @@ -651,7 +651,7 @@ class TestHashviewAPI: # Download left hashes with different IDs (3_4 instead of 1_2) left_file = tmp_path / "left_3_4.txt" - result = api.download_left_hashes(3, 4, output_file=str(left_file)) + api.download_left_hashes(3, 4, output_file=str(left_file)) # Verify the different IDs' .out file wasn't affected with open(str(out_file), 'r') as f: diff --git a/tests/test_utils.py b/tests/test_utils.py index a213f0b..4e0e405 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,15 +15,20 @@ def test_resolve_path_none_and_expand(): assert os.path.isabs(resolved) -def test_setup_logging_adds_single_filehandler(tmp_path): +def test_setup_logging_adds_single_streamhandler(tmp_path): logger = logging.getLogger("hate_crack_test") logger.handlers.clear() cli.setup_logging(logger, str(tmp_path), debug_mode=True) cli.setup_logging(logger, str(tmp_path), debug_mode=True) + stream_handlers = [ + h + for h in logger.handlers + if isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler) + ] + assert len(stream_handlers) == 1 file_handlers = [h for h in logger.handlers if isinstance(h, logging.FileHandler)] - assert len(file_handlers) == 1 - assert os.path.basename(file_handlers[0].baseFilename) == "hate_crack.log" + assert file_handlers == [] logger.handlers.clear()