diff --git a/hate_crack/main.py b/hate_crack/main.py index 2a14025..0dda836 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -4964,7 +4964,9 @@ def main(): else: argv = argv_temp # Fallback if subcommand not found - has_attack_subcommand = bool(argv) and argv[0] in _noninteractive.ATTACK_COMMANDS + has_attack_subcommand = any( + arg in _noninteractive.ATTACK_COMMANDS for arg in argv + ) use_subcommand_parser = "hashview" in argv or has_attack_subcommand parser, hashview_parser = _build_parser( include_positional=not use_subcommand_parser, @@ -5370,12 +5372,10 @@ def main(): f"Detected {duplicates} duplicate account(s) out of" f" {total} total NetNTLM hashes." ) - dedup_choice = ( - input( - "Would you like to ignore duplicate accounts" - " (keep first occurrence only)? (Y) " - ) - or "Y" + dedup_choice = _auto_input( + "Would you like to ignore duplicate accounts" + " (keep first occurrence only)? (Y) ", + "Y", ) if dedup_choice.upper() == "Y": hcatHashFileOrig = hcatHashFile diff --git a/hate_crack/noninteractive.py b/hate_crack/noninteractive.py index 4c732e4..18a23bb 100644 --- a/hate_crack/noninteractive.py +++ b/hate_crack/noninteractive.py @@ -9,6 +9,8 @@ the same pattern ``attacks.py`` uses). See import os from typing import Any +ATTACK_COMMANDS = ("quick", "dict", "brute", "topmask") + def build_rule_chains(ctx: Any, rule_tokens: list[str] | None) -> list[str]: """Convert CLI ``--rules`` tokens into hashcat ``-r`` chain strings. @@ -56,7 +58,7 @@ def run_noninteractive(ctx: Any, args: Any) -> int: print(f"Error: wordlist not found: {args.wordlist}") return 1 try: - chains = build_rule_chains(ctx, args.rules) + chains = build_rule_chains(ctx, args.rule_files) except (FileNotFoundError, ValueError) as exc: print(f"Error: invalid --rules value: {exc}") return 1 @@ -88,9 +90,6 @@ def run_noninteractive(ctx: Any, args: Any) -> int: return 2 -ATTACK_COMMANDS = ("quick", "dict", "brute", "topmask") - - def add_attack_subparsers(subparsers) -> None: """Register the non-interactive attack subcommands on an argparse subparsers object (the same one used for ``hashview``). @@ -112,6 +111,7 @@ def add_attack_subparsers(subparsers) -> None: "--rules", nargs="*", default=[], + dest="rule_files", metavar="RULE", help="Rule filename(s) from the rules directory. Chain with '+' " "(e.g. best64.rule+d3ad0ne.rule). Omit to run without rules.", diff --git a/tests/test_noninteractive.py b/tests/test_noninteractive.py index b25d5bf..9386df3 100644 --- a/tests/test_noninteractive.py +++ b/tests/test_noninteractive.py @@ -87,7 +87,7 @@ def test_dispatch_quick_calls_quick_dictionary(tmp_path): wl = tmp_path / "rockyou.txt" wl.write_text("password\n") ctx = _spy_ctx(tmp_path) - args = SimpleNamespace(command="quick", wordlist=str(wl), rules=["best64.rule"]) + args = SimpleNamespace(command="quick", wordlist=str(wl), rule_files=["best64.rule"]) code = ni.run_noninteractive(ctx, args) assert code == 0 assert [c[0] for c in ctx.calls] == ["hcatQuickDictionary"] @@ -101,7 +101,7 @@ def test_dispatch_quick_calls_quick_dictionary(tmp_path): def test_dispatch_quick_missing_wordlist_returns_1(tmp_path): (tmp_path / "rules").mkdir() ctx = _spy_ctx(tmp_path) - args = SimpleNamespace(command="quick", wordlist=str(tmp_path / "nope.txt"), rules=[]) + args = SimpleNamespace(command="quick", wordlist=str(tmp_path / "nope.txt"), rule_files=[]) assert ni.run_noninteractive(ctx, args) == 1 assert ctx.calls == [] @@ -111,7 +111,7 @@ def test_dispatch_quick_unknown_rule_returns_1(tmp_path): wl = tmp_path / "rockyou.txt" wl.write_text("password\n") ctx = _spy_ctx(tmp_path) - args = SimpleNamespace(command="quick", wordlist=str(wl), rules=["ghost.rule"]) + args = SimpleNamespace(command="quick", wordlist=str(wl), rule_files=["ghost.rule"]) assert ni.run_noninteractive(ctx, args) == 1 assert ctx.calls == [] @@ -154,7 +154,7 @@ def test_dispatch_quick_multiple_rules_runs_each_as_separate_pass(tmp_path): wl.write_text("password\n") ctx = _spy_ctx(tmp_path) args = SimpleNamespace( - command="quick", wordlist=str(wl), rules=["best64.rule", "d3ad0ne.rule"] + command="quick", wordlist=str(wl), rule_files=["best64.rule", "d3ad0ne.rule"] ) assert ni.run_noninteractive(ctx, args) == 0 assert len(ctx.calls) == 2 @@ -215,3 +215,41 @@ def test_main_quick_bad_hashtype_errors(monkeypatch, tmp_path): monkeypatch, ["quick", str(hf), "notanumber", "--wordlist", str(wl)] ) assert code != 0 + + +def test_main_quick_with_rules_dispatches(monkeypatch, tmp_path): + hf = _make_ntlm_hashfile(tmp_path) + wl = tmp_path / "rockyou.txt" + wl.write_text("password\n") + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + (rules_dir / "best64.rule").write_text(":\n") + monkeypatch.setattr(hc_main, "rulesDirectory", str(rules_dir)) + calls = [] + monkeypatch.setattr( + hc_main, "hcatQuickDictionary", lambda *a, **k: calls.append(a) + ) + code = _run_main( + monkeypatch, + ["quick", str(hf), "1000", "--wordlist", str(wl), "--rules", "best64.rule"], + ) + assert code == 0 + assert len(calls) == 1 + # the rule chain (4th positional arg) must reference best64.rule + assert "best64.rule" in calls[0][2] + + +def test_main_debug_flag_before_subcommand(monkeypatch, tmp_path): + hf = _make_ntlm_hashfile(tmp_path) + wl = tmp_path / "w.txt" + wl.write_text("x\n") + calls = [] + monkeypatch.setattr( + hc_main, "hcatQuickDictionary", lambda *a, **k: calls.append(a) + ) + code = _run_main( + monkeypatch, + ["--debug", "quick", str(hf), "1000", "--wordlist", str(wl)], + ) + assert code == 0 + assert len(calls) == 1