mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
fix(ui): route ollama/omen submenus through interactive_menu; re-prompt pickers on invalid input
- ollama_attack: convert generation-mode menu to interactive_menu with dynamic items list (option 3 only present when cracked file exists); cancel via 99 or Escape; re-prompts in a while loop - omen_attack: convert existing-model menu to interactive_menu; cancel via 99 or Escape (replaces hard-coded "3. Cancel"); re-prompts in loop - _omen_pick_training_wordlist: replace abort-on-invalid with re-prompt loop; add explicit 'q' cancel key so users can exit without being trapped; callers already handle None correctly - _markov_pick_training_source: same re-prompt-loop + 'q' cancel fix; callers already handle None correctly - Update all affected tests to patch interactive_menu at hate_crack.attacks.interactive_menu (module-level import path) and drive follow-up prompts via builtins.input - Add new tests: arrow-menu reach, Escape/99 cancel, re-prompt loop coverage for both pickers, and conditional option-3 key presence Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
39e0dd956a
commit
07ca6ba7e3
+102
-87
@@ -512,65 +512,73 @@ def bandrel_method(ctx: Any) -> None:
|
|||||||
|
|
||||||
def ollama_attack(ctx: Any) -> None:
|
def ollama_attack(ctx: Any) -> None:
|
||||||
_notify.prompt_notify_for_attack("LLM")
|
_notify.prompt_notify_for_attack("LLM")
|
||||||
print("\n\tLLM Attack")
|
|
||||||
print("\t1. Target info (company / industry / location)")
|
|
||||||
print("\t2. Wordlist (generate basewords from a sample wordlist)")
|
|
||||||
# Cracked-password mode is only offered when this session actually has
|
# Cracked-password mode is only offered when this session actually has
|
||||||
# plaintexts to learn from, matching _markov_pick_training_source.
|
# plaintexts to learn from, matching _markov_pick_training_source.
|
||||||
out_path = f"{ctx.hcatHashFile}.out"
|
out_path = f"{ctx.hcatHashFile}.out"
|
||||||
has_cracked = os.path.isfile(out_path) and os.path.getsize(out_path) > 0
|
has_cracked = os.path.isfile(out_path) and os.path.getsize(out_path) > 0
|
||||||
if has_cracked:
|
|
||||||
print("\t3. Cracked passwords (current session)")
|
|
||||||
choice = input("\n\tSelect generation mode: ").strip()
|
|
||||||
|
|
||||||
if choice == "1":
|
items: list[tuple[str, str]] = [
|
||||||
company = input("Company name: ").strip()
|
("1", "Target info (company / industry / location)"),
|
||||||
industry = input("Industry: ").strip()
|
("2", "Wordlist (generate basewords from a sample wordlist)"),
|
||||||
location = input("Location: ").strip()
|
]
|
||||||
ctx.hcatOllama(
|
if has_cracked:
|
||||||
ctx.hcatHashType,
|
items.append(("3", "Cracked passwords (current session)"))
|
||||||
ctx.hcatHashFile,
|
items.append(("99", "Cancel"))
|
||||||
"target",
|
|
||||||
{"company": company, "industry": industry, "location": location},
|
while True:
|
||||||
)
|
choice = interactive_menu(items, title="\nLLM Attack", prompt="\n\tSelect generation mode: ")
|
||||||
elif choice == "2":
|
if choice is None or choice == "99":
|
||||||
path = _omen_pick_training_wordlist(ctx, title="LLM Sample Wordlists")
|
return
|
||||||
if not path:
|
if choice == "1":
|
||||||
|
company = input("Company name: ").strip()
|
||||||
|
industry = input("Industry: ").strip()
|
||||||
|
location = input("Location: ").strip()
|
||||||
|
ctx.hcatOllama(
|
||||||
|
ctx.hcatHashType,
|
||||||
|
ctx.hcatHashFile,
|
||||||
|
"target",
|
||||||
|
{"company": company, "industry": industry, "location": location},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
elif choice == "2":
|
||||||
|
path = _omen_pick_training_wordlist(ctx, title="LLM Sample Wordlists")
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "wordlist", path)
|
||||||
|
return
|
||||||
|
elif choice == "3" and has_cracked:
|
||||||
|
ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "cracked", out_path)
|
||||||
return
|
return
|
||||||
ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "wordlist", path)
|
|
||||||
elif choice == "3" and has_cracked:
|
|
||||||
ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "cracked", out_path)
|
|
||||||
elif choice == "3":
|
|
||||||
print("\t[!] No cracked passwords yet — crack some hashes first.")
|
|
||||||
else:
|
|
||||||
print("\t[!] Invalid selection.")
|
|
||||||
|
|
||||||
|
|
||||||
def _omen_pick_training_wordlist(ctx: Any, title: str = "Training Wordlists"):
|
def _omen_pick_training_wordlist(ctx: Any, title: str = "Training Wordlists"):
|
||||||
"""Show wordlist picker. Returns path or None."""
|
"""Show wordlist picker. Returns path or None (user cancelled with 'q')."""
|
||||||
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
||||||
if wordlist_files:
|
while True:
|
||||||
entries = [f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)]
|
if wordlist_files:
|
||||||
max_len = max((len(e) for e in entries), default=24)
|
entries = [f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)]
|
||||||
print_multicolumn_list(
|
max_len = max((len(e) for e in entries), default=24)
|
||||||
title,
|
print_multicolumn_list(
|
||||||
entries,
|
title,
|
||||||
min_col_width=max_len,
|
entries,
|
||||||
max_col_width=max_len,
|
min_col_width=max_len,
|
||||||
)
|
max_col_width=max_len,
|
||||||
print("\tp. Enter a custom path")
|
)
|
||||||
sel = input("\n\tSelect wordlist: ").strip()
|
print("\tp. Enter a custom path")
|
||||||
if sel.lower() == "p":
|
print("\tq. Cancel")
|
||||||
path = input("\n\tPath to wordlist: ").strip()
|
sel = input("\n\tSelect wordlist: ").strip()
|
||||||
return path if path else None
|
if sel.lower() == "q":
|
||||||
try:
|
return None
|
||||||
idx = int(sel)
|
if sel.lower() == "p":
|
||||||
if 1 <= idx <= len(wordlist_files):
|
path = input("\n\tPath to wordlist: ").strip()
|
||||||
return os.path.join(ctx.hcatWordlists, wordlist_files[idx - 1])
|
return path if path else None
|
||||||
except (ValueError, IndexError):
|
try:
|
||||||
pass
|
idx = int(sel)
|
||||||
print("\t[!] Invalid selection.")
|
if 1 <= idx <= len(wordlist_files):
|
||||||
return None
|
return os.path.join(ctx.hcatWordlists, wordlist_files[idx - 1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
print("\t[!] Invalid selection.")
|
||||||
|
|
||||||
|
|
||||||
def omen_attack(ctx: Any) -> None:
|
def omen_attack(ctx: Any) -> None:
|
||||||
@@ -592,16 +600,20 @@ def omen_attack(ctx: Any) -> None:
|
|||||||
info = ctx._omen_model_info(model_dir)
|
info = ctx._omen_model_info(model_dir)
|
||||||
trained_with = info.get("training_file", "unknown") if info else "unknown"
|
trained_with = info.get("training_file", "unknown") if info else "unknown"
|
||||||
print(f"\n\tOMEN model found (trained with: {trained_with})")
|
print(f"\n\tOMEN model found (trained with: {trained_with})")
|
||||||
print("\t1. Use existing model")
|
model_items = [
|
||||||
print("\t2. Train new model (overwrites existing)")
|
("1", "Use existing model"),
|
||||||
print("\t3. Cancel")
|
("2", "Train new model (overwrites existing)"),
|
||||||
choice = input("\n\tChoice: ").strip()
|
("99", "Cancel"),
|
||||||
if choice == "1":
|
]
|
||||||
need_training = False
|
while True:
|
||||||
elif choice == "3":
|
choice = interactive_menu(model_items, title="\nOMEN Attack (Ordered Markov ENumerator)", prompt="\n\tChoice: ")
|
||||||
return
|
if choice is None or choice == "99":
|
||||||
elif choice != "2":
|
return
|
||||||
return
|
if choice == "1":
|
||||||
|
need_training = False
|
||||||
|
break
|
||||||
|
elif choice == "2":
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
print("\n\tNo valid OMEN model found. Training is required.")
|
print("\n\tNo valid OMEN model found. Training is required.")
|
||||||
|
|
||||||
@@ -628,38 +640,41 @@ def omen_attack(ctx: Any) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _markov_pick_training_source(ctx: Any):
|
def _markov_pick_training_source(ctx: Any):
|
||||||
"""Prompt user to select markov training source. Returns file path or None."""
|
"""Prompt user to select markov training source. Returns file path or None (user cancelled with 'q')."""
|
||||||
out_path = f"{ctx.hcatHashFile}.out"
|
out_path = f"{ctx.hcatHashFile}.out"
|
||||||
has_cracked = os.path.isfile(out_path) and os.path.getsize(out_path) > 0
|
has_cracked = os.path.isfile(out_path) and os.path.getsize(out_path) > 0
|
||||||
|
|
||||||
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
|
||||||
entries = []
|
while True:
|
||||||
if has_cracked:
|
entries = []
|
||||||
entries.append("0) Cracked passwords (current session)")
|
if has_cracked:
|
||||||
entries.extend([f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)])
|
entries.append("0) Cracked passwords (current session)")
|
||||||
if entries:
|
entries.extend([f"{i}) {f}" for i, f in enumerate(wordlist_files, start=1)])
|
||||||
max_len = max((len(e) for e in entries), default=24)
|
if entries:
|
||||||
print_multicolumn_list(
|
max_len = max((len(e) for e in entries), default=24)
|
||||||
"Markov Training Source",
|
print_multicolumn_list(
|
||||||
entries,
|
"Markov Training Source",
|
||||||
min_col_width=max_len,
|
entries,
|
||||||
max_col_width=max_len,
|
min_col_width=max_len,
|
||||||
)
|
max_col_width=max_len,
|
||||||
print("\tp. Enter a custom path")
|
)
|
||||||
sel = input("\n\tSelect training source: ").strip()
|
print("\tp. Enter a custom path")
|
||||||
if sel == "0" and has_cracked:
|
print("\tq. Cancel")
|
||||||
return out_path
|
sel = input("\n\tSelect training source: ").strip()
|
||||||
if sel.lower() == "p":
|
if sel.lower() == "q":
|
||||||
path = input("\n\tPath to training file: ").strip()
|
return None
|
||||||
return path if path else None
|
if sel == "0" and has_cracked:
|
||||||
try:
|
return out_path
|
||||||
idx = int(sel)
|
if sel.lower() == "p":
|
||||||
if 1 <= idx <= len(wordlist_files):
|
path = input("\n\tPath to training file: ").strip()
|
||||||
return os.path.join(ctx.hcatWordlists, wordlist_files[idx - 1])
|
return path if path else None
|
||||||
except (ValueError, IndexError):
|
try:
|
||||||
pass
|
idx = int(sel)
|
||||||
print("\t[!] Invalid selection.")
|
if 1 <= idx <= len(wordlist_files):
|
||||||
return None
|
return os.path.join(ctx.hcatWordlists, wordlist_files[idx - 1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
print("\t[!] Invalid selection.")
|
||||||
|
|
||||||
|
|
||||||
def adhoc_mask_crack(ctx: Any) -> None:
|
def adhoc_mask_crack(ctx: Any) -> None:
|
||||||
|
|||||||
+184
-24
@@ -329,7 +329,10 @@ class TestOllamaAttack:
|
|||||||
def test_calls_hcatOllama_with_context(self) -> None:
|
def test_calls_hcatOllama_with_context(self) -> None:
|
||||||
ctx = _make_ctx()
|
ctx = _make_ctx()
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["1", "ACME", "tech", "NYC"]):
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["ACME", "tech", "NYC"]),
|
||||||
|
):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
ctx.hcatOllama.assert_called_once_with(
|
ctx.hcatOllama.assert_called_once_with(
|
||||||
@@ -342,7 +345,10 @@ class TestOllamaAttack:
|
|||||||
def test_passes_hash_type_and_file(self) -> None:
|
def test_passes_hash_type_and_file(self) -> None:
|
||||||
ctx = _make_ctx(hash_type="1800", hash_file="/tmp/sha512.txt")
|
ctx = _make_ctx(hash_type="1800", hash_file="/tmp/sha512.txt")
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["1", "Corp", "finance", "London"]):
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["Corp", "finance", "London"]),
|
||||||
|
):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
call_args = ctx.hcatOllama.call_args[0]
|
call_args = ctx.hcatOllama.call_args[0]
|
||||||
@@ -352,7 +358,10 @@ class TestOllamaAttack:
|
|||||||
def test_strips_whitespace_from_inputs(self) -> None:
|
def test_strips_whitespace_from_inputs(self) -> None:
|
||||||
ctx = _make_ctx()
|
ctx = _make_ctx()
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["1", " ACME ", " tech ", " NYC "]):
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=[" ACME ", " tech ", " NYC "]),
|
||||||
|
):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
target_info = ctx.hcatOllama.call_args[0][3]
|
target_info = ctx.hcatOllama.call_args[0][3]
|
||||||
@@ -363,7 +372,10 @@ class TestOllamaAttack:
|
|||||||
def test_target_string_is_literal_target(self) -> None:
|
def test_target_string_is_literal_target(self) -> None:
|
||||||
ctx = _make_ctx()
|
ctx = _make_ctx()
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["1", "X", "Y", "Z"]):
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["X", "Y", "Z"]),
|
||||||
|
):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
assert ctx.hcatOllama.call_args[0][2] == "target"
|
assert ctx.hcatOllama.call_args[0][2] == "target"
|
||||||
@@ -373,30 +385,44 @@ class TestOllamaAttack:
|
|||||||
ctx.list_wordlist_files.return_value = ["rockyou.txt"]
|
ctx.list_wordlist_files.return_value = ["rockyou.txt"]
|
||||||
ctx.hcatWordlists = "/tmp/wl"
|
ctx.hcatWordlists = "/tmp/wl"
|
||||||
|
|
||||||
# mode "2", then pick wordlist "1"
|
# mode "2" from interactive_menu, then pick wordlist "1" via input
|
||||||
with patch("builtins.input", side_effect=["2", "1"]):
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="2"),
|
||||||
|
patch("builtins.input", side_effect=["1"]),
|
||||||
|
):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
args = ctx.hcatOllama.call_args[0]
|
args = ctx.hcatOllama.call_args[0]
|
||||||
assert args[2] == "wordlist"
|
assert args[2] == "wordlist"
|
||||||
assert args[3].endswith("rockyou.txt")
|
assert args[3].endswith("rockyou.txt")
|
||||||
|
|
||||||
def test_invalid_mode_does_not_call_hcatOllama(self) -> None:
|
def test_escape_cancels_without_calling_hcatOllama(self) -> None:
|
||||||
|
"""None from interactive_menu (Escape / 99) cancels the attack."""
|
||||||
ctx = _make_ctx()
|
ctx = _make_ctx()
|
||||||
with patch("builtins.input", side_effect=["9"]):
|
with patch("hate_crack.attacks.interactive_menu", return_value=None):
|
||||||
|
ollama_attack(ctx)
|
||||||
|
ctx.hcatOllama.assert_not_called()
|
||||||
|
|
||||||
|
def test_cancel_key_cancels_without_calling_hcatOllama(self) -> None:
|
||||||
|
"""Selecting '99' (Cancel) cancels the attack."""
|
||||||
|
ctx = _make_ctx()
|
||||||
|
with patch("hate_crack.attacks.interactive_menu", return_value="99"):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
ctx.hcatOllama.assert_not_called()
|
ctx.hcatOllama.assert_not_called()
|
||||||
|
|
||||||
def test_wordlist_mode_aborts_when_no_wordlist_picked(self) -> None:
|
def test_wordlist_mode_aborts_when_no_wordlist_picked(self) -> None:
|
||||||
ctx = _make_ctx()
|
ctx = _make_ctx()
|
||||||
# mode "2", then an invalid picker selection -> picker returns None
|
# mode "2" from interactive_menu, then user cancels the file picker
|
||||||
ctx.list_wordlist_files.return_value = []
|
ctx.list_wordlist_files.return_value = []
|
||||||
with patch("builtins.input", side_effect=["2", "nonsense"]):
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="2"),
|
||||||
|
patch("builtins.input", side_effect=["q"]),
|
||||||
|
):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
ctx.hcatOllama.assert_not_called()
|
ctx.hcatOllama.assert_not_called()
|
||||||
|
|
||||||
def test_cracked_mode_offered_when_out_file_has_content(
|
def test_cracked_mode_offered_when_out_file_has_content(
|
||||||
self, tmp_path: Path, capsys
|
self, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
hash_file = tmp_path / "hashes.txt"
|
hash_file = tmp_path / "hashes.txt"
|
||||||
hash_file.touch()
|
hash_file.touch()
|
||||||
@@ -404,43 +430,63 @@ class TestOllamaAttack:
|
|||||||
out_file.write_text("hash:Summer2024!\n")
|
out_file.write_text("hash:Summer2024!\n")
|
||||||
ctx = _make_ctx(hash_file=str(hash_file))
|
ctx = _make_ctx(hash_file=str(hash_file))
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["3"]):
|
captured_items: list[list[tuple[str, str]]] = []
|
||||||
|
|
||||||
|
def capture_menu(items, **kwargs):
|
||||||
|
captured_items.append(list(items))
|
||||||
|
return "3"
|
||||||
|
|
||||||
|
with patch("hate_crack.attacks.interactive_menu", side_effect=capture_menu):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
assert "3. Cracked passwords" in capsys.readouterr().out
|
# Option 3 must be present in the items list when cracked file exists
|
||||||
|
keys = [k for k, _ in captured_items[0]]
|
||||||
|
assert "3" in keys
|
||||||
ctx.hcatOllama.assert_called_once_with(
|
ctx.hcatOllama.assert_called_once_with(
|
||||||
ctx.hcatHashType, str(hash_file), "cracked", str(out_file)
|
ctx.hcatHashType, str(hash_file), "cracked", str(out_file)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cracked_mode_not_offered_when_out_file_missing(
|
def test_cracked_mode_not_offered_when_out_file_missing(
|
||||||
self, tmp_path: Path, capsys
|
self, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Option 3 must NOT appear in items when no cracked file exists."""
|
||||||
hash_file = tmp_path / "hashes.txt"
|
hash_file = tmp_path / "hashes.txt"
|
||||||
hash_file.touch()
|
hash_file.touch()
|
||||||
ctx = _make_ctx(hash_file=str(hash_file))
|
ctx = _make_ctx(hash_file=str(hash_file))
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["3"]):
|
captured_items: list[list[tuple[str, str]]] = []
|
||||||
|
|
||||||
|
def capture_menu(items, **kwargs):
|
||||||
|
captured_items.append(list(items))
|
||||||
|
return "99" # cancel
|
||||||
|
|
||||||
|
with patch("hate_crack.attacks.interactive_menu", side_effect=capture_menu):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
out = capsys.readouterr().out
|
keys = [k for k, _ in captured_items[0]]
|
||||||
assert "3. Cracked passwords" not in out
|
assert "3" not in keys
|
||||||
assert "No cracked passwords yet" in out
|
|
||||||
ctx.hcatOllama.assert_not_called()
|
ctx.hcatOllama.assert_not_called()
|
||||||
|
|
||||||
def test_cracked_mode_not_offered_when_out_file_empty(
|
def test_cracked_mode_not_offered_when_out_file_empty(
|
||||||
self, tmp_path: Path, capsys
|
self, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Option 3 must NOT appear in items when cracked file exists but is empty."""
|
||||||
hash_file = tmp_path / "hashes.txt"
|
hash_file = tmp_path / "hashes.txt"
|
||||||
hash_file.touch()
|
hash_file.touch()
|
||||||
(tmp_path / "hashes.txt.out").touch() # exists but zero bytes
|
(tmp_path / "hashes.txt.out").touch() # exists but zero bytes
|
||||||
ctx = _make_ctx(hash_file=str(hash_file))
|
ctx = _make_ctx(hash_file=str(hash_file))
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["3"]):
|
captured_items: list[list[tuple[str, str]]] = []
|
||||||
|
|
||||||
|
def capture_menu(items, **kwargs):
|
||||||
|
captured_items.append(list(items))
|
||||||
|
return "99" # cancel
|
||||||
|
|
||||||
|
with patch("hate_crack.attacks.interactive_menu", side_effect=capture_menu):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
out = capsys.readouterr().out
|
keys = [k for k, _ in captured_items[0]]
|
||||||
assert "3. Cracked passwords" not in out
|
assert "3" not in keys
|
||||||
assert "No cracked passwords yet" in out
|
|
||||||
ctx.hcatOllama.assert_not_called()
|
ctx.hcatOllama.assert_not_called()
|
||||||
|
|
||||||
def test_target_and_wordlist_modes_unaffected_by_cracked_option(
|
def test_target_and_wordlist_modes_unaffected_by_cracked_option(
|
||||||
@@ -452,7 +498,121 @@ class TestOllamaAttack:
|
|||||||
(tmp_path / "hashes.txt.out").write_text("hash:Summer2024!\n")
|
(tmp_path / "hashes.txt.out").write_text("hash:Summer2024!\n")
|
||||||
ctx = _make_ctx(hash_file=str(hash_file))
|
ctx = _make_ctx(hash_file=str(hash_file))
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["1", "ACME", "tech", "NYC"]):
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["ACME", "tech", "NYC"]),
|
||||||
|
):
|
||||||
ollama_attack(ctx)
|
ollama_attack(ctx)
|
||||||
|
|
||||||
assert ctx.hcatOllama.call_args[0][2] == "target"
|
assert ctx.hcatOllama.call_args[0][2] == "target"
|
||||||
|
|
||||||
|
def test_arrow_menu_env_reaches_ollama_attack(self) -> None:
|
||||||
|
"""HATE_CRACK_ARROW_MENU=1 routes through interactive_menu in ollama_attack."""
|
||||||
|
import os
|
||||||
|
from hate_crack.attacks import interactive_menu as real_im
|
||||||
|
|
||||||
|
ctx = _make_ctx()
|
||||||
|
calls: list[tuple] = []
|
||||||
|
|
||||||
|
def spy_menu(items, **kwargs):
|
||||||
|
calls.append(tuple(items))
|
||||||
|
return "99" # cancel immediately
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("hate_crack.attacks.interactive_menu", side_effect=spy_menu),
|
||||||
|
):
|
||||||
|
ollama_attack(ctx)
|
||||||
|
|
||||||
|
# interactive_menu was called — confirms the arrow-menu code path is reachable
|
||||||
|
assert len(calls) == 1
|
||||||
|
keys = [k for k, _ in calls[0]]
|
||||||
|
assert "1" in keys
|
||||||
|
assert "2" in keys
|
||||||
|
assert "99" in keys
|
||||||
|
|
||||||
|
|
||||||
|
class TestOmenPickTrainingWordlistReprompt:
|
||||||
|
"""_omen_pick_training_wordlist re-prompts on invalid input instead of aborting."""
|
||||||
|
|
||||||
|
def _make_ctx(self, wordlist_files=None):
|
||||||
|
ctx = MagicMock()
|
||||||
|
ctx.list_wordlist_files.return_value = wordlist_files or ["rockyou.txt"]
|
||||||
|
ctx.hcatWordlists = "/tmp/wl"
|
||||||
|
ctx.hcatHashFile = "/tmp/hashes.txt"
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def test_invalid_input_reprompts_then_valid_pick(self) -> None:
|
||||||
|
from hate_crack.attacks import _omen_pick_training_wordlist
|
||||||
|
|
||||||
|
ctx = self._make_ctx(["rockyou.txt"])
|
||||||
|
# First input is invalid, second is valid
|
||||||
|
with patch("builtins.input", side_effect=["bad", "1"]):
|
||||||
|
result = _omen_pick_training_wordlist(ctx)
|
||||||
|
assert result is not None
|
||||||
|
assert "rockyou.txt" in result
|
||||||
|
|
||||||
|
def test_cancel_with_q_returns_none(self) -> None:
|
||||||
|
from hate_crack.attacks import _omen_pick_training_wordlist
|
||||||
|
|
||||||
|
ctx = self._make_ctx(["rockyou.txt"])
|
||||||
|
with patch("builtins.input", return_value="q"):
|
||||||
|
result = _omen_pick_training_wordlist(ctx)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_multiple_invalid_inputs_then_cancel(self) -> None:
|
||||||
|
from hate_crack.attacks import _omen_pick_training_wordlist
|
||||||
|
|
||||||
|
ctx = self._make_ctx(["rockyou.txt"])
|
||||||
|
with patch("builtins.input", side_effect=["99", "abc", "q"]):
|
||||||
|
result = _omen_pick_training_wordlist(ctx)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestMarkovPickTrainingSourceReprompt:
|
||||||
|
"""_markov_pick_training_source re-prompts on invalid input instead of aborting."""
|
||||||
|
|
||||||
|
def _make_ctx(self, tmp_path, has_cracked=False, wordlist_files=None):
|
||||||
|
ctx = MagicMock()
|
||||||
|
hash_file = str(tmp_path / "hashes.txt")
|
||||||
|
ctx.hcatHashFile = hash_file
|
||||||
|
ctx.list_wordlist_files.return_value = wordlist_files or ["rockyou.txt"]
|
||||||
|
ctx.hcatWordlists = str(tmp_path / "wordlists")
|
||||||
|
if has_cracked:
|
||||||
|
(tmp_path / "hashes.txt.out").write_text("cracked_pw\n")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def test_invalid_input_reprompts_then_valid_pick(self, tmp_path: Path) -> None:
|
||||||
|
from hate_crack.attacks import _markov_pick_training_source
|
||||||
|
|
||||||
|
ctx = self._make_ctx(tmp_path, wordlist_files=["rockyou.txt"])
|
||||||
|
with patch("builtins.input", side_effect=["bad", "1"]):
|
||||||
|
result = _markov_pick_training_source(ctx)
|
||||||
|
assert result is not None
|
||||||
|
assert "rockyou.txt" in result
|
||||||
|
|
||||||
|
def test_cancel_with_q_returns_none(self, tmp_path: Path) -> None:
|
||||||
|
from hate_crack.attacks import _markov_pick_training_source
|
||||||
|
|
||||||
|
ctx = self._make_ctx(tmp_path)
|
||||||
|
with patch("builtins.input", return_value="q"):
|
||||||
|
result = _markov_pick_training_source(ctx)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_multiple_invalid_then_cancel(self, tmp_path: Path) -> None:
|
||||||
|
from hate_crack.attacks import _markov_pick_training_source
|
||||||
|
|
||||||
|
ctx = self._make_ctx(tmp_path, wordlist_files=["rockyou.txt"])
|
||||||
|
with patch("builtins.input", side_effect=["99", "abc", "q"]):
|
||||||
|
result = _markov_pick_training_source(ctx)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_caller_handles_none_correctly(self, tmp_path: Path) -> None:
|
||||||
|
"""markov_brute_force returns early without crashing when picker returns None."""
|
||||||
|
from hate_crack.attacks import markov_brute_force
|
||||||
|
|
||||||
|
ctx = self._make_ctx(tmp_path, wordlist_files=["rockyou.txt"])
|
||||||
|
# No .hcstat2 file → goes straight to picker; user cancels
|
||||||
|
with patch("builtins.input", return_value="q"):
|
||||||
|
markov_brute_force(ctx)
|
||||||
|
ctx.hcatMarkovTrain.assert_not_called()
|
||||||
|
ctx.hcatMarkovBruteForce.assert_not_called()
|
||||||
|
|||||||
+40
-14
@@ -277,8 +277,10 @@ class TestOmenAttackHandler:
|
|||||||
def test_use_existing_model(self, tmp_path):
|
def test_use_existing_model(self, tmp_path):
|
||||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
self._setup_rules_dir(tmp_path)
|
self._setup_rules_dir(tmp_path)
|
||||||
with patch("os.path.isfile", return_value=True), patch(
|
with (
|
||||||
"builtins.input", side_effect=["1", "", "0"]
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["", "0"]),
|
||||||
):
|
):
|
||||||
from hate_crack.attacks import omen_attack
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
@@ -289,8 +291,10 @@ class TestOmenAttackHandler:
|
|||||||
def test_train_new_model_with_wordlist_pick(self, tmp_path):
|
def test_train_new_model_with_wordlist_pick(self, tmp_path):
|
||||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
self._setup_rules_dir(tmp_path)
|
self._setup_rules_dir(tmp_path)
|
||||||
with patch("os.path.isfile", return_value=True), patch(
|
with (
|
||||||
"builtins.input", side_effect=["2", "1", "", "0"]
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="2"),
|
||||||
|
patch("builtins.input", side_effect=["1", "", "0"]),
|
||||||
):
|
):
|
||||||
from hate_crack.attacks import omen_attack
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
@@ -302,8 +306,22 @@ class TestOmenAttackHandler:
|
|||||||
|
|
||||||
def test_cancel_aborts(self, tmp_path):
|
def test_cancel_aborts(self, tmp_path):
|
||||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
with patch("os.path.isfile", return_value=True), patch(
|
with (
|
||||||
"builtins.input", side_effect=["3"]
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="99"),
|
||||||
|
):
|
||||||
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
|
omen_attack(ctx)
|
||||||
|
ctx.hcatOmenTrain.assert_not_called()
|
||||||
|
ctx.hcatOmen.assert_not_called()
|
||||||
|
|
||||||
|
def test_escape_cancels(self, tmp_path):
|
||||||
|
"""None from interactive_menu (Escape) cancels the attack."""
|
||||||
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
|
with (
|
||||||
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value=None),
|
||||||
):
|
):
|
||||||
from hate_crack.attacks import omen_attack
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
@@ -348,8 +366,10 @@ class TestOmenAttackHandler:
|
|||||||
def test_rules_passed_to_hcatOmen(self, tmp_path):
|
def test_rules_passed_to_hcatOmen(self, tmp_path):
|
||||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
self._setup_rules_dir(tmp_path, ["best64.rule"])
|
self._setup_rules_dir(tmp_path, ["best64.rule"])
|
||||||
with patch("os.path.isfile", return_value=True), patch(
|
with (
|
||||||
"builtins.input", side_effect=["1", "", "1"]
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["", "1"]),
|
||||||
):
|
):
|
||||||
from hate_crack.attacks import omen_attack
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
@@ -361,8 +381,10 @@ class TestOmenAttackHandler:
|
|||||||
def test_multiple_rule_chains_spawn_multiple_calls(self, tmp_path):
|
def test_multiple_rule_chains_spawn_multiple_calls(self, tmp_path):
|
||||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
self._setup_rules_dir(tmp_path, ["best64.rule", "dive.rule"])
|
self._setup_rules_dir(tmp_path, ["best64.rule", "dive.rule"])
|
||||||
with patch("os.path.isfile", return_value=True), patch(
|
with (
|
||||||
"builtins.input", side_effect=["1", "", "1,2"]
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["", "1,2"]),
|
||||||
):
|
):
|
||||||
from hate_crack.attacks import omen_attack
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
@@ -372,8 +394,10 @@ class TestOmenAttackHandler:
|
|||||||
def test_cancel_from_rules_aborts(self, tmp_path):
|
def test_cancel_from_rules_aborts(self, tmp_path):
|
||||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
self._setup_rules_dir(tmp_path, ["best64.rule"])
|
self._setup_rules_dir(tmp_path, ["best64.rule"])
|
||||||
with patch("os.path.isfile", return_value=True), patch(
|
with (
|
||||||
"builtins.input", side_effect=["1", "", "99"]
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["", "99"]),
|
||||||
):
|
):
|
||||||
from hate_crack.attacks import omen_attack
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
@@ -383,8 +407,10 @@ class TestOmenAttackHandler:
|
|||||||
def test_no_rules_passes_empty_chain(self, tmp_path):
|
def test_no_rules_passes_empty_chain(self, tmp_path):
|
||||||
ctx = self._make_ctx(tmp_path, model_valid=True)
|
ctx = self._make_ctx(tmp_path, model_valid=True)
|
||||||
self._setup_rules_dir(tmp_path, ["best64.rule"])
|
self._setup_rules_dir(tmp_path, ["best64.rule"])
|
||||||
with patch("os.path.isfile", return_value=True), patch(
|
with (
|
||||||
"builtins.input", side_effect=["1", "", "0"]
|
patch("os.path.isfile", return_value=True),
|
||||||
|
patch("hate_crack.attacks.interactive_menu", return_value="1"),
|
||||||
|
patch("builtins.input", side_effect=["", "0"]),
|
||||||
):
|
):
|
||||||
from hate_crack.attacks import omen_attack
|
from hate_crack.attacks import omen_attack
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user