mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
refactor: address code-quality review findings on ui-polish branch
- Remove review-artifact "Defect 2" comment in main.py:2076; replace with accurate description of the invalid-cap fallback. Same label removed from the test section header in test_ollama_wordlist_sampling.py. - Lift nested _usable helper to module-level _usable_plaintext in main.py, placed alongside the other private wordlist helpers after _wordlist_path. Add six direct unit tests covering blank, whitespace, plain, hash:pw, multi-colon, and empty-plaintext cases. - Move spinner line-clear before thread.join() in progress.py so the terminal is cleaned up even if join() is interrupted by a DoubleInterrupt/second Ctrl-C. Add test_tty_clears_line_even_if_join_raises to cover this path. - Replace time.sleep-based elapsed-counter test with a deterministic version that patches time.monotonic; assert on actual rendered format with r"\d+s". Drop the misleading 0.05s sleep in test_tty_clears_line_on_exit (shorter than one tick, so no frame was ever guaranteed). - Add missing ollama* keys to hate_crack/config.json.example (package-data copy); root config.json.example already had them. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
fef606dbf4
commit
142c1fc99f
@@ -22,6 +22,10 @@
|
||||
"hashview_url": "http://localhost:8443",
|
||||
"hashview_api_key": "",
|
||||
"hashmob_api_key": "",
|
||||
"ollamaModel": "qwen2.5:32b",
|
||||
"ollamaNumCtx": 2048,
|
||||
"ollamaTimeout": 300,
|
||||
"ollamaMaxSampleLines": 500,
|
||||
"passgptModel": "javirandor/passgpt-10characters",
|
||||
"passgptMaxCandidates": 1000000,
|
||||
"passgptBatchSize": 1024,
|
||||
|
||||
+21
-13
@@ -897,6 +897,23 @@ def _wordlist_path(path: str):
|
||||
yield path
|
||||
|
||||
|
||||
def _usable_plaintext(raw: str) -> str:
|
||||
"""Return the usable plaintext from a raw wordlist line, or empty string.
|
||||
|
||||
Blank/whitespace-only lines are discarded. Lines in ``hash:password``
|
||||
format (as produced by hashcat ``--show``) are split on the first colon
|
||||
so only the plaintext portion is returned; lines with no colon are
|
||||
returned as-is. A ``hash:`` line whose plaintext is empty after
|
||||
stripping returns an empty string and is therefore also discarded.
|
||||
"""
|
||||
stripped = raw.strip()
|
||||
if not stripped:
|
||||
return ""
|
||||
if ":" in stripped:
|
||||
stripped = stripped.split(":", 1)[1]
|
||||
return stripped
|
||||
|
||||
|
||||
def _add_debug_mode_for_rules(cmd):
|
||||
"""Add debug mode arguments to hashcat command if rules are being used.
|
||||
|
||||
@@ -2053,27 +2070,18 @@ def hcatOllama(hcatHashType, hcatHashFile, mode, context_data):
|
||||
# stride-select across the whole file rather than taking a head slice.
|
||||
# A head-only sample misses the pattern variation across large wordlists
|
||||
# (e.g. rockyou.txt becomes more random further in).
|
||||
def _usable(raw: str) -> str:
|
||||
"""Return the usable plaintext from a raw line, or empty string."""
|
||||
stripped = raw.strip()
|
||||
if not stripped:
|
||||
return ""
|
||||
if ":" in stripped:
|
||||
stripped = stripped.split(":", 1)[1]
|
||||
return stripped
|
||||
|
||||
try:
|
||||
total_usable = 0
|
||||
with open(wordlist_path, "r", errors="ignore") as f:
|
||||
for raw in f:
|
||||
if _usable(raw):
|
||||
if _usable_plaintext(raw):
|
||||
total_usable += 1
|
||||
except Exception as e:
|
||||
print(f"Error reading wordlist: {e}")
|
||||
return
|
||||
|
||||
cap = ollamaMaxSampleLines
|
||||
# Defect 2: cap <= 0 is nonsense; treat it as "no cap" (default 500).
|
||||
# Invalid cap (zero or negative): fall back to the built-in default of 500.
|
||||
if cap <= 0:
|
||||
cap = 500
|
||||
if total_usable <= cap:
|
||||
@@ -2082,7 +2090,7 @@ def hcatOllama(hcatHashType, hcatHashFile, mode, context_data):
|
||||
sampled: list[str] = []
|
||||
with open(wordlist_path, "r", errors="ignore") as f:
|
||||
for raw in f:
|
||||
w = _usable(raw)
|
||||
w = _usable_plaintext(raw)
|
||||
if w:
|
||||
sampled.append(w)
|
||||
except Exception as e:
|
||||
@@ -2101,7 +2109,7 @@ def hcatOllama(hcatHashType, hcatHashFile, mode, context_data):
|
||||
usable_idx = 0
|
||||
with open(wordlist_path, "r", errors="ignore") as f:
|
||||
for raw in f:
|
||||
w = _usable(raw)
|
||||
w = _usable_plaintext(raw)
|
||||
if not w:
|
||||
continue
|
||||
if usable_idx in pick_set:
|
||||
|
||||
@@ -59,7 +59,12 @@ def spinner(message: str) -> "Generator[None, None, None]":
|
||||
yield
|
||||
finally:
|
||||
stop_event.set()
|
||||
thread.join()
|
||||
# Erase the spinner line so the next print starts clean.
|
||||
# Clear the spinner line *before* joining so the terminal is left clean
|
||||
# even if join() is interrupted by a second Ctrl-C (DoubleInterrupt).
|
||||
# hate_crack's _sigint_handler raises DoubleInterrupt on a second SIGINT
|
||||
# within 2 s, and join() can block up to _TICK_INTERVAL (0.12 s) — a
|
||||
# narrow but real window. Writing the escape sequence first ensures the
|
||||
# line is always erased regardless of what happens to the join.
|
||||
sys.stdout.write("\033[2K\r")
|
||||
sys.stdout.flush()
|
||||
thread.join()
|
||||
|
||||
@@ -250,7 +250,7 @@ def test_stride_cap_one(env) -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defect 2: ZeroDivisionError guard — cap=0 falls back to 500
|
||||
# Invalid-cap guard — zero/negative ollamaMaxSampleLines falls back to 500
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -338,3 +338,38 @@ def test_spinner_called_with_model_message(env, capsys):
|
||||
captured = capsys.readouterr()
|
||||
assert MODEL in captured.out
|
||||
assert "Generating password candidates via Ollama" in captured.out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _usable_plaintext unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_usable_plaintext_blank_line():
|
||||
"""A blank line must return an empty string (discarded)."""
|
||||
assert hc_main._usable_plaintext("") == ""
|
||||
|
||||
|
||||
def test_usable_plaintext_whitespace_only():
|
||||
"""A whitespace-only line must return an empty string (discarded)."""
|
||||
assert hc_main._usable_plaintext(" \t ") == ""
|
||||
|
||||
|
||||
def test_usable_plaintext_plain_password():
|
||||
"""A plain password line (no colon) is returned stripped."""
|
||||
assert hc_main._usable_plaintext("hunter2\n") == "hunter2"
|
||||
|
||||
|
||||
def test_usable_plaintext_hash_colon_password():
|
||||
"""A hash:password line returns only the password portion."""
|
||||
assert hc_main._usable_plaintext("aabbcc:hunter2") == "hunter2"
|
||||
|
||||
|
||||
def test_usable_plaintext_multiple_colons():
|
||||
"""A line with multiple colons splits only on the first colon."""
|
||||
assert hc_main._usable_plaintext("aabbcc:p@ss:word") == "p@ss:word"
|
||||
|
||||
|
||||
def test_usable_plaintext_hash_colon_empty():
|
||||
"""A hash: line with no plaintext after the colon returns empty string."""
|
||||
assert hc_main._usable_plaintext("aabbcc:") == ""
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from unittest import mock
|
||||
@@ -55,7 +56,7 @@ def test_tty_clears_line_on_exit() -> None:
|
||||
|
||||
with mock.patch("sys.stdout", buf):
|
||||
with spinner("Testing..."):
|
||||
time.sleep(0.05) # let spinner tick at least once
|
||||
pass # no sleep needed — the finally block writes the escape regardless
|
||||
|
||||
output = buf.getvalue()
|
||||
# Line clear sequence must be present somewhere after the spinner started.
|
||||
@@ -63,18 +64,29 @@ def test_tty_clears_line_on_exit() -> None:
|
||||
|
||||
|
||||
def test_tty_shows_elapsed_seconds() -> None:
|
||||
"""The spinner should show at least a 0s counter in its output."""
|
||||
"""The spinner must render the elapsed-seconds counter in the expected format.
|
||||
|
||||
time.monotonic is patched so the test is deterministic regardless of
|
||||
scheduling: the first call returns 0.0 (start), the second returns 3.0
|
||||
(first tick), giving a stable "3s" label without sleeping.
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
buf.isatty = lambda: True # type: ignore[attr-defined]
|
||||
|
||||
with mock.patch("sys.stdout", buf):
|
||||
# Patch monotonic: start=0.0, then 3.0 at the first tick, then keep
|
||||
# returning 3.0 so stop_event.wait() ends quickly.
|
||||
mono_values = iter([0.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0])
|
||||
|
||||
with mock.patch("sys.stdout", buf), mock.patch(
|
||||
"hate_crack.progress.time.monotonic", side_effect=mono_values
|
||||
):
|
||||
with spinner("Counting..."):
|
||||
time.sleep(0.25) # enough time for several ticks
|
||||
pass # body exits immediately; the thread gets one tick then stops
|
||||
|
||||
output = buf.getvalue()
|
||||
# Should have written at least one elapsed counter like "0s" or "1s"
|
||||
assert "s" in output
|
||||
assert "Counting..." in output
|
||||
# At least one frame must match the "Ns" elapsed format (e.g. "3s").
|
||||
assert re.search(r"\d+s", output), f"Expected elapsed counter in output: {output!r}"
|
||||
|
||||
|
||||
def test_tty_exception_still_clears_line() -> None:
|
||||
@@ -100,3 +112,40 @@ def test_spinner_does_not_swallow_exception() -> None:
|
||||
with pytest.raises(RuntimeError, match="test error"):
|
||||
with spinner("Should propagate"):
|
||||
raise RuntimeError("test error")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Teardown robustness: line must be cleared even if join() is interrupted
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_tty_clears_line_even_if_join_raises() -> None:
|
||||
"""Line-clear must happen even when thread.join() raises (e.g. DoubleInterrupt).
|
||||
|
||||
The fix moves the write("\033[2K\r") *before* thread.join() so an
|
||||
exception during join cannot prevent the terminal from being cleaned up.
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
buf.isatty = lambda: True # type: ignore[attr-defined]
|
||||
|
||||
class FakeThread:
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
def start(self) -> None:
|
||||
pass
|
||||
|
||||
def join(self, *args, **kwargs) -> None:
|
||||
raise KeyboardInterrupt("simulated double Ctrl-C during join")
|
||||
|
||||
with mock.patch("hate_crack.progress.threading.Thread", FakeThread), mock.patch(
|
||||
"sys.stdout", buf
|
||||
):
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
with spinner("Robust teardown"):
|
||||
pass
|
||||
|
||||
output = buf.getvalue()
|
||||
assert "\033[2K\r" in output, (
|
||||
"Line-clear escape sequence must be written before join() is called"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user