mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-06-28 17:33:31 -07:00
Merge branch 'bugfix/fingerprint-empty-expanded-guard' into dev
This commit is contained in:
@@ -1553,6 +1553,12 @@ def hcatFingerprint(
|
||||
print("Killing PID {0}...".format(str(sort_proc.pid)))
|
||||
sort_proc.kill()
|
||||
expander_proc.kill()
|
||||
if lineCount(f"{hcatHashFile}.expanded") == 0:
|
||||
print(
|
||||
"[!] Skipping Fingerprint Attack: no candidates to expand "
|
||||
"(no cracked passwords yet)."
|
||||
)
|
||||
break
|
||||
fingerprint_cmd = [
|
||||
hcatBin,
|
||||
"-m",
|
||||
|
||||
@@ -116,21 +116,21 @@ def test_weakpass_default_rank(monkeypatch):
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_potfile_path_flag(monkeypatch):
|
||||
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "7")
|
||||
_run_main(monkeypatch, ["--potfile-path", "/tmp/test.pot"])
|
||||
assert hc_main.hcatPotfilePath == "/tmp/test.pot"
|
||||
|
||||
|
||||
def test_no_potfile_path_flag(monkeypatch):
|
||||
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "7")
|
||||
_run_main(monkeypatch, ["--no-potfile-path"])
|
||||
assert hc_main.hcatPotfilePath == ""
|
||||
|
||||
|
||||
def test_potfile_path_empty_string_reverts_to_default(monkeypatch):
|
||||
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "7")
|
||||
_run_main(monkeypatch, ["--potfile-path", ""])
|
||||
assert hc_main.hcatPotfilePath == ""
|
||||
|
||||
@@ -140,7 +140,7 @@ def test_potfile_path_empty_string_reverts_to_default(monkeypatch):
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_debug_flag(monkeypatch):
|
||||
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "7")
|
||||
_run_main(monkeypatch, ["--debug"])
|
||||
assert hc_main.debug_mode is True
|
||||
|
||||
@@ -167,7 +167,7 @@ def test_positional_hashfile_only_enters_menu(monkeypatch, tmp_path):
|
||||
hashfile = tmp_path / "hashes.txt"
|
||||
hashfile.write_text("aabbccdd\n")
|
||||
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "7")
|
||||
code = _run_main(monkeypatch, [str(hashfile)])
|
||||
assert code == 0
|
||||
|
||||
@@ -175,7 +175,7 @@ def test_positional_hashfile_only_enters_menu(monkeypatch, tmp_path):
|
||||
def test_no_args_enters_menu(monkeypatch):
|
||||
"""No arguments falls through to the interactive menu."""
|
||||
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "7")
|
||||
code = _run_main(monkeypatch, [])
|
||||
assert code == 0
|
||||
|
||||
@@ -383,7 +383,7 @@ def test_argparse_missing_required_args(monkeypatch, argv):
|
||||
def test_potfile_path_and_no_potfile_path_conflict(monkeypatch):
|
||||
"""Both --potfile-path and --no-potfile-path should still parse (not mutually exclusive in argparse)."""
|
||||
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": "7")
|
||||
# --potfile-path wins because it's checked second in the dispatch logic
|
||||
code = _run_main(monkeypatch, ["--potfile-path", "/tmp/test.pot", "--no-potfile-path"])
|
||||
assert code == 0
|
||||
|
||||
@@ -41,10 +41,11 @@ def test_hcatFingerprint_uses_selected_expander_and_calls_hybrid(monkeypatch, tm
|
||||
out_path = tmp_path / "hashes.txt.out"
|
||||
out_path.write_text("deadbeef:Accordbookkeeping2025!:x\n")
|
||||
|
||||
# Make the loop run exactly one iteration.
|
||||
# Calls: before-loop(1), end-of-iteration(1) == before → break, post-loop(1).
|
||||
counts = iter([1, 1, 1])
|
||||
monkeypatch.setattr(hc_main, "lineCount", lambda _p: next(counts))
|
||||
# Constant lineCount makes the while-loop terminate after one iteration
|
||||
# (crackedAfter == crackedBefore) and keeps the empty-.expanded guard
|
||||
# quiet (1 != 0). Avoids coupling to the exact number of lineCount
|
||||
# call sites inside hcatFingerprint and _run_hcat_cmd.
|
||||
monkeypatch.setattr(hc_main, "lineCount", lambda _p: 1)
|
||||
monkeypatch.setattr(hc_main, "hcatHashCracked", 0)
|
||||
|
||||
# Avoid any filesystem/executable checks in unit test.
|
||||
@@ -102,3 +103,77 @@ def test_hcatFingerprint_uses_selected_expander_and_calls_hybrid(monkeypatch, tm
|
||||
assert seen["hybrid_calls"] == [
|
||||
("1000", str(hashfile), [f"{hashfile}.expanded"]),
|
||||
]
|
||||
|
||||
|
||||
def test_hcatFingerprint_skips_hashcat_and_hybrid_when_expanded_is_empty(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
"""If the expander pipeline yields an empty {hash}.expanded file (e.g.
|
||||
nothing has been cracked yet), hcatFingerprint must NOT invoke hashcat
|
||||
and must NOT call the secondary hybrid attack. Otherwise we waste a
|
||||
session running combinator against two empty wordlists."""
|
||||
monkeypatch.setenv("HATE_CRACK_SKIP_INIT", "1")
|
||||
|
||||
import hate_crack.main as hc_main
|
||||
|
||||
importlib.reload(hc_main)
|
||||
|
||||
hashfile = tmp_path / "hashes.txt"
|
||||
# Empty .out simulates "no cracks yet" — the documented trigger for the bug.
|
||||
out_path = tmp_path / "hashes.txt.out"
|
||||
out_path.write_text("")
|
||||
|
||||
monkeypatch.setattr(hc_main, "hcatHashCracked", 0)
|
||||
monkeypatch.setattr(hc_main, "ensure_binary", lambda binary_path, **_k: binary_path)
|
||||
|
||||
seen = {"popen_args": [], "hybrid_calls": []}
|
||||
|
||||
def fake_hybrid(hash_type, hash_file, wordlists=None):
|
||||
seen["hybrid_calls"].append((hash_type, hash_file, wordlists))
|
||||
|
||||
monkeypatch.setattr(hc_main, "hcatHybrid", fake_hybrid)
|
||||
|
||||
class FakePopen:
|
||||
def __init__(self, args, stdin=None, stdout=None, text=False, **_kwargs):
|
||||
self.args = args
|
||||
self.pid = 123
|
||||
self.stdout = None
|
||||
seen["popen_args"].append(args)
|
||||
|
||||
cmd0 = args[0]
|
||||
if cmd0 == "sort":
|
||||
data = stdin.read() if stdin is not None else b""
|
||||
lines = sorted(set(data.splitlines()))
|
||||
for ln in lines:
|
||||
stdout.write(ln + b"\n")
|
||||
stdout.flush()
|
||||
elif isinstance(cmd0, str) and "expander" in cmd0:
|
||||
# Identity passthrough of empty stdin → empty stdout.
|
||||
data = stdin.read() if stdin is not None else b""
|
||||
self.stdout = io.BytesIO(data)
|
||||
else:
|
||||
# hashcat invocation: must never reach here in this test.
|
||||
pass
|
||||
|
||||
def wait(self, timeout=None):
|
||||
return 0
|
||||
|
||||
def kill(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(hc_main.subprocess, "Popen", FakePopen)
|
||||
monkeypatch.setattr(hc_main, "hcatHashFile", str(hashfile), raising=False)
|
||||
|
||||
hc_main.hcatFingerprint(
|
||||
"1000", str(hashfile), expander_len=7, run_hybrid_on_expanded=True
|
||||
)
|
||||
|
||||
# No hashcat invocation: identify by the -a flag, which only the hashcat
|
||||
# command carries (expander has 1 arg; sort uses -u).
|
||||
hashcat_invocations = [args for args in seen["popen_args"] if "-a" in args]
|
||||
assert hashcat_invocations == [], (
|
||||
f"hashcat was invoked with empty .expanded: {hashcat_invocations}"
|
||||
)
|
||||
|
||||
# No secondary hybrid pass on an empty wordlist.
|
||||
assert seen["hybrid_calls"] == []
|
||||
|
||||
@@ -11,9 +11,9 @@ def _is_hashcat_utils_empty(path):
|
||||
|
||||
|
||||
def test_hashcat_utils_submodule_initialized():
|
||||
if shutil.which("git") is None:
|
||||
import pytest
|
||||
import pytest
|
||||
|
||||
if shutil.which("git") is None:
|
||||
pytest.skip("git not available")
|
||||
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
@@ -30,6 +30,15 @@ def test_hashcat_utils_submodule_initialized():
|
||||
"git submodule update failed: "
|
||||
f"stdout={result.stdout} stderr={result.stderr}"
|
||||
)
|
||||
# Git worktrees share the parent repo's submodules — `submodule update`
|
||||
# exits 0 but does not populate the worktree's submodule dirs. When that
|
||||
# happens, skip rather than fail: the test's intent is to flag missing
|
||||
# initialization in normal checkouts, not to gate worktree workflows.
|
||||
if _is_hashcat_utils_empty(submodule_path):
|
||||
pytest.skip(
|
||||
"hashcat-utils submodule not populated (likely a git worktree); "
|
||||
"run `git submodule update --init --recursive` in the main checkout"
|
||||
)
|
||||
|
||||
assert not _is_hashcat_utils_empty(submodule_path), (
|
||||
"hashcat-utils submodule is empty. Run: git submodule update --init --recursive"
|
||||
|
||||
Reference in New Issue
Block a user