feat(notify): wire Pushover notifications into attacks and config (WIP)

Adds notify_* keys to both config.json.example files, threads
notification calls through hashcat invocations in main.py, and
exposes menu/attack hooks. Pushed for manual testing — verification
and PR still pending.

Refs #106

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-04-22 15:16:12 -04:00
parent f9926c0b41
commit baeca07b70
7 changed files with 551 additions and 231 deletions

View File

@@ -37,5 +37,13 @@
"hcatCombinator3", "hcatCombinatorX", "hcatHybrid", "hcatYoloCombination", "hcatCombinator3", "hcatCombinatorX", "hcatHybrid", "hcatYoloCombination",
"hcatMiddleCombinator", "hcatThoroughCombinator", "hcatCombipow", "hcatPrince", "hcatMiddleCombinator", "hcatThoroughCombinator", "hcatCombipow", "hcatPrince",
"hcatPermute" "hcatPermute"
] ],
"notify_enabled": false,
"notify_pushover_token": "",
"notify_pushover_user": "",
"notify_per_crack_enabled": false,
"notify_attack_allowlist": [],
"notify_suppress_in_orchestrators": true,
"notify_max_cracks_per_burst": 5,
"notify_poll_interval_seconds": 5.0
} }

View File

@@ -94,6 +94,7 @@ def get_main_menu_options():
"22": _attacks.combipow_crack, "22": _attacks.combipow_crack,
"80": _attacks.wordlist_tools_submenu, "80": _attacks.wordlist_tools_submenu,
"81": _attacks.rule_tools_submenu, "81": _attacks.rule_tools_submenu,
"83": toggle_notifications,
"90": download_hashmob_rules, "90": download_hashmob_rules,
"91": weakpass_wordlist_menu, "91": weakpass_wordlist_menu,
"92": download_hashmob_wordlists, "92": download_hashmob_wordlists,

View File

@@ -4,6 +4,7 @@ import os
import readline import readline
from typing import Any from typing import Any
from hate_crack import notify as _notify
from hate_crack.api import download_hashmob_rules from hate_crack.api import download_hashmob_rules
from hate_crack.formatting import print_multicolumn_list from hate_crack.formatting import print_multicolumn_list
from hate_crack.menu import interactive_menu from hate_crack.menu import interactive_menu
@@ -107,6 +108,7 @@ def _select_rules(ctx) -> list[str] | None:
def quick_crack(ctx: Any) -> None: def quick_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Quick Crack")
wordlist_choice = None wordlist_choice = None
default_dir = ctx.hcatOptimizedWordlists default_dir = ctx.hcatOptimizedWordlists
@@ -177,6 +179,7 @@ def quick_crack(ctx: Any) -> None:
def loopback_attack(ctx: Any) -> None: def loopback_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Loopback")
empty_wordlist = os.path.join(ctx.hcatWordlists, "empty.txt") empty_wordlist = os.path.join(ctx.hcatWordlists, "empty.txt")
os.makedirs(ctx.hcatWordlists, exist_ok=True) os.makedirs(ctx.hcatWordlists, exist_ok=True)
if not os.path.exists(empty_wordlist): if not os.path.exists(empty_wordlist):
@@ -200,26 +203,43 @@ def loopback_attack(ctx: Any) -> None:
def extensive_crack(ctx: Any) -> None: def extensive_crack(ctx: Any) -> None:
ctx.hcatBruteForce(ctx.hcatHashType, ctx.hcatHashFile, "1", "7") # Orchestrator attack: chains ~14 primitives. We suppress each primitive's
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatBruteCount) # own notifications and fire exactly one "Extensive Crack complete" at the
ctx.hcatDictionary(ctx.hcatHashType, ctx.hcatHashFile) # end with the aggregate delta. This both prevents notification spam and
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatDictionaryCount) # gives the user an actually-useful summary.
hcatTargetTime = 4 * 60 * 60 _notify.prompt_notify_for_attack("Extensive Crack")
ctx.hcatTopMask(ctx.hcatHashType, ctx.hcatHashFile, hcatTargetTime) out_path = ctx.hcatHashFile + ".out"
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatMaskCount) cracked_before = ctx.lineCount(out_path) if os.path.exists(out_path) else 0
ctx.hcatFingerprint( with _notify.suppressed_notifications():
ctx.hcatHashType, ctx.hcatHashFile, 7, run_hybrid_on_expanded=False ctx.hcatBruteForce(ctx.hcatHashType, ctx.hcatHashFile, "1", "7")
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatBruteCount)
ctx.hcatDictionary(ctx.hcatHashType, ctx.hcatHashFile)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatDictionaryCount)
hcatTargetTime = 4 * 60 * 60
ctx.hcatTopMask(ctx.hcatHashType, ctx.hcatHashFile, hcatTargetTime)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatMaskCount)
ctx.hcatFingerprint(
ctx.hcatHashType, ctx.hcatHashFile, 7, run_hybrid_on_expanded=False
)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatFingerprintCount)
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatCombinationCount)
ctx.hcatHybrid(ctx.hcatHashType, ctx.hcatHashFile)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatHybridCount)
ctx.hcatGoodMeasure(ctx.hcatHashType, ctx.hcatHashFile)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount)
cracked_after = ctx.lineCount(out_path) if os.path.exists(out_path) else 0
_notify.notify_job_done(
"Extensive Crack", cracked_after, ctx.hcatHashFile
) )
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatFingerprintCount) # Note: ``cracked_before`` is tracked for potential future per-orchestrator
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile) # delta reporting, but today the notify message uses the absolute count
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatCombinationCount) # because that matches what single-attack notifications already report.
ctx.hcatHybrid(ctx.hcatHashType, ctx.hcatHashFile) _ = cracked_before
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatHybridCount)
ctx.hcatGoodMeasure(ctx.hcatHashType, ctx.hcatHashFile)
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount)
def brute_force_crack(ctx: Any) -> None: def brute_force_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Brute Force")
hcatMinLen = int( hcatMinLen = int(
input("\nEnter the minimum password length to brute force (1): ") or 1 input("\nEnter the minimum password length to brute force (1): ") or 1
) )
@@ -230,6 +250,7 @@ def brute_force_crack(ctx: Any) -> None:
def top_mask_crack(ctx: Any) -> None: def top_mask_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Top Mask")
hcatTargetTime = int( hcatTargetTime = int(
input("\nEnter a target time for completion in hours (4): ") or 4 input("\nEnter a target time for completion in hours (4): ") or 4
) )
@@ -238,6 +259,7 @@ def top_mask_crack(ctx: Any) -> None:
def fingerprint_crack(ctx: Any) -> None: def fingerprint_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Fingerprint")
while True: while True:
raw = input("\nEnter expander max length (7-36) (7): ").strip() raw = input("\nEnter expander max length (7-36) (7): ").strip()
if raw == "": if raw == "":
@@ -261,6 +283,7 @@ def fingerprint_crack(ctx: Any) -> None:
def combinator_crack(ctx: Any) -> None: def combinator_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Combinator")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("COMBINATOR ATTACK") print("COMBINATOR ATTACK")
print("=" * 60) print("=" * 60)
@@ -299,6 +322,7 @@ def combinator_crack(ctx: Any) -> None:
def hybrid_crack(ctx: Any) -> None: def hybrid_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Hybrid")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("HYBRID ATTACK") print("HYBRID ATTACK")
print("=" * 60) print("=" * 60)
@@ -367,22 +391,27 @@ def hybrid_crack(ctx: Any) -> None:
def pathwell_crack(ctx: Any) -> None: def pathwell_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Pathwell Brute Force")
ctx.hcatPathwellBruteForce(ctx.hcatHashType, ctx.hcatHashFile) ctx.hcatPathwellBruteForce(ctx.hcatHashType, ctx.hcatHashFile)
def prince_attack(ctx: Any) -> None: def prince_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("PRINCE")
ctx.hcatPrince(ctx.hcatHashType, ctx.hcatHashFile) ctx.hcatPrince(ctx.hcatHashType, ctx.hcatHashFile)
def yolo_combination(ctx: Any) -> None: def yolo_combination(ctx: Any) -> None:
_notify.prompt_notify_for_attack("YOLO Combination")
ctx.hcatYoloCombination(ctx.hcatHashType, ctx.hcatHashFile) ctx.hcatYoloCombination(ctx.hcatHashType, ctx.hcatHashFile)
def thorough_combinator(ctx: Any) -> None: def thorough_combinator(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Thorough Combinator")
ctx.hcatThoroughCombinator(ctx.hcatHashType, ctx.hcatHashFile) ctx.hcatThoroughCombinator(ctx.hcatHashType, ctx.hcatHashFile)
def middle_combinator(ctx: Any) -> None: def middle_combinator(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Middle Combinator")
ctx.hcatMiddleCombinator(ctx.hcatHashType, ctx.hcatHashFile) ctx.hcatMiddleCombinator(ctx.hcatHashType, ctx.hcatHashFile)
@@ -447,10 +476,12 @@ def combinator_3plus_crack(ctx: Any) -> None:
def bandrel_method(ctx: Any) -> None: def bandrel_method(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Bandrel")
ctx.hcatBandrel(ctx.hcatHashType, ctx.hcatHashFile) ctx.hcatBandrel(ctx.hcatHashType, ctx.hcatHashFile)
def ollama_attack(ctx: Any) -> None: def ollama_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("LLM")
print("\n\tLLM Attack") print("\n\tLLM Attack")
company = input("Company name: ").strip() company = input("Company name: ").strip()
industry = input("Industry: ").strip() industry = input("Industry: ").strip()
@@ -491,6 +522,7 @@ def _omen_pick_training_wordlist(ctx: Any):
def omen_attack(ctx: Any) -> None: def omen_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("OMEN")
print("\n\tOMEN Attack (Ordered Markov ENumerator)") print("\n\tOMEN Attack (Ordered Markov ENumerator)")
omen_dir = os.path.join(ctx.hate_path, "omen") omen_dir = os.path.join(ctx.hate_path, "omen")
create_bin = os.path.join(omen_dir, ctx.hcatOmenCreateBin) create_bin = os.path.join(omen_dir, ctx.hcatOmenCreateBin)
@@ -579,6 +611,7 @@ def _markov_pick_training_source(ctx: Any):
def adhoc_mask_crack(ctx: Any) -> None: def adhoc_mask_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Ad-hoc Mask")
print( print(
"\nEnter a hashcat mask. Tokens: ?l=lower ?u=upper ?d=digit ?s=special ?a=all ?b=binary ?1-?4=custom" "\nEnter a hashcat mask. Tokens: ?l=lower ?u=upper ?d=digit ?s=special ?a=all ?b=binary ?1-?4=custom"
) )
@@ -603,6 +636,7 @@ def adhoc_mask_crack(ctx: Any) -> None:
def markov_brute_force(ctx: Any) -> None: def markov_brute_force(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Markov Brute Force")
print("\n\tMarkov Brute Force Attack") print("\n\tMarkov Brute Force Attack")
hcstat2_path = f"{ctx.hcatHashFile}.hcstat2" hcstat2_path = f"{ctx.hcatHashFile}.hcstat2"
need_training = True need_training = True
@@ -640,6 +674,7 @@ def markov_brute_force(ctx: Any) -> None:
def combipow_crack(ctx: Any) -> None: def combipow_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Combipow")
wordlist = None wordlist = None
while wordlist is None: while wordlist is None:
path = input("\n[*] Enter path to wordlist (max 63 lines recommended): ").strip() path = input("\n[*] Enter path to wordlist (max 63 lines recommended): ").strip()
@@ -665,6 +700,7 @@ def combipow_crack(ctx: Any) -> None:
def generate_rules_crack(ctx: Any) -> None: def generate_rules_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Random Rules")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("RANDOM RULES ATTACK") print("RANDOM RULES ATTACK")
print("=" * 60) print("=" * 60)
@@ -743,6 +779,7 @@ def generate_rules_crack(ctx: Any) -> None:
def ngram_attack(ctx: Any) -> None: def ngram_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("N-gram")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("NGRAM ATTACK") print("NGRAM ATTACK")
print("=" * 60) print("=" * 60)
@@ -769,6 +806,7 @@ def ngram_attack(ctx: Any) -> None:
def permute_crack(ctx: Any) -> None: def permute_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Permute")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("PERMUTATION ATTACK") print("PERMUTATION ATTACK")
print("=" * 60) print("=" * 60)

View File

@@ -25,5 +25,13 @@
"passgptModel": "javirandor/passgpt-10characters", "passgptModel": "javirandor/passgpt-10characters",
"passgptMaxCandidates": 1000000, "passgptMaxCandidates": 1000000,
"passgptBatchSize": 1024, "passgptBatchSize": 1024,
"passgptTrainingList": "" "passgptTrainingList": "",
"notify_enabled": false,
"notify_pushover_token": "",
"notify_pushover_user": "",
"notify_per_crack_enabled": false,
"notify_attack_allowlist": [],
"notify_suppress_in_orchestrators": true,
"notify_max_cracks_per_burst": 5,
"notify_poll_interval_seconds": 5.0
} }

View File

@@ -443,6 +443,14 @@ except KeyError:
pass pass
check_for_updates_enabled = config_parser.get("check_for_updates", True) check_for_updates_enabled = config_parser.get("check_for_updates", True)
# Notification subsystem bootstrap. The notify module stores its own
# settings snapshot; we just hand it the resolved config path so it can
# atomically rewrite config.json when the user toggles enabled / answers
# "always" at a prompt.
from hate_crack import notify as _notify # noqa: E402 (kept close to config load)
_notify.init(_config_path, config_parser)
hcatExpanderBin = "expander.bin" hcatExpanderBin = "expander.bin"
hcatCombinatorBin = "combinator.bin" hcatCombinatorBin = "combinator.bin"
hcatPrinceBin = "pp64.bin" hcatPrinceBin = "pp64.bin"
@@ -729,6 +737,93 @@ def _debug_cmd(cmd):
print(f"[DEBUG] hashcat cmd: {_format_cmd(cmd)}") print(f"[DEBUG] hashcat cmd: {_format_cmd(cmd)}")
def _run_hcat_cmd(
cmd,
attack_name: str = "",
hash_file: str | None = None,
*,
stdin=None,
companion_procs=None,
reraise_interrupt: bool = False,
out_path: str | None = None,
):
"""Execute a hashcat subprocess and bracket it with notify hooks.
This consolidates the ``hcatProcess = subprocess.Popen(cmd); try:
wait() except KeyboardInterrupt: kill()`` dance that was duplicated
at ~31 sites in this module. The payoff: every hashcat invocation
now fires job-done notifications consistently, and the per-crack
tailer lifecycle is handled in exactly one place.
- ``attack_name`` is the label that appears in notifications. Pass
an empty string for no-notify invocations.
- ``hash_file`` is required to locate ``{hash_file}.out`` for the
tailer. When omitted, we skip the tailer and the job-done count.
- ``stdin`` mirrors the ``subprocess.Popen(..., stdin=...)`` kwarg
for generator-pipe callers.
- ``companion_procs`` is a list of generator ``Popen`` handles that
feed into this hashcat instance. On normal completion we
``wait()`` them; on ``KeyboardInterrupt`` we ``kill()`` them
alongside the hashcat process. This preserves the prior behavior
where a ctrl-C must tear down both sides of a pipe.
Notifications are fire-and-forget: suppression (see
``notify.suppressed_notifications``) and disabled-globally state are
both handled inside the notify module, so callers need not branch.
"""
global hcatProcess
companions = list(companion_procs) if companion_procs else []
# Resolve the output file path used for the tailer and cracked-count
# readback. Most hashcat calls write to ``{hash_file}.out``; a few
# multi-phase flows (LM-to-NT) write to a different file, in which
# case the caller passes ``out_path`` explicitly.
resolved_out = out_path if out_path else (hash_file + ".out" if hash_file else None)
tailer = None
if attack_name and resolved_out and not _notify.is_suppressed():
tailer = _notify.start_tailer(resolved_out, attack_name)
popen_kwargs = {"stdin": stdin} if stdin is not None else {}
hcatProcess = subprocess.Popen(cmd, **popen_kwargs)
interrupted = False
try:
hcatProcess.wait()
for gen in companions:
try:
gen.wait()
except Exception:
pass
except KeyboardInterrupt:
interrupted = True
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
for gen in companions:
try:
gen.kill()
except Exception:
pass
finally:
_notify.stop_tailer(tailer)
# Only incur a lineCount read when notifications will actually fire.
# This avoids disturbing existing tests that assert a specific number
# of file reads during an attack; ``_should_fire`` mirrors the check
# inside ``notify_job_done`` itself.
if (
attack_name
and resolved_out
and not _notify.is_suppressed()
and _notify.get_settings().enabled
):
cracked = lineCount(resolved_out)
_notify.notify_job_done(attack_name, cracked, hash_file or resolved_out)
if interrupted and reraise_interrupt:
raise KeyboardInterrupt
def _is_gzipped(path: str) -> bool: def _is_gzipped(path: str) -> bool:
try: try:
with open(path, "rb") as f: with open(path, "rb") as f:
@@ -1189,12 +1284,7 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Brute Force", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
hcatBruteCount = lineCount(hcatHashFile + ".out") hcatBruteCount = lineCount(hcatHashFile + ".out")
@@ -1226,12 +1316,7 @@ def hcatDictionary(hcatHashType, hcatHashFile):
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd) cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Dictionary", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
rule_d3ad0ne = get_rule_path("d3ad0ne.rule") rule_d3ad0ne = get_rule_path("d3ad0ne.rule")
rule_toxic = get_rule_path("T0XlC.rule") rule_toxic = get_rule_path("T0XlC.rule")
@@ -1267,12 +1352,7 @@ def hcatDictionary(hcatHashType, hcatHashFile):
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd) cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Dictionary", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
finally: finally:
os.unlink(combined_path) os.unlink(combined_path)
@@ -1316,12 +1396,7 @@ def hcatQuickDictionary(
) )
cmd = _add_debug_mode_for_rules(cmd) cmd = _add_debug_mode_for_rules(cmd)
_debug_cmd(cmd) _debug_cmd(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Quick Dictionary", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
# Top Mask Attack # Top Mask Attack
@@ -1383,12 +1458,7 @@ def hcatTopMask(hcatHashType, hcatHashFile, hcatTargetTime):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Top Mask", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
hcatMaskCount = lineCount(hcatHashFile + ".out") - hcatHashCracked hcatMaskCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1462,12 +1532,9 @@ def hcatFingerprint(
_insert_optimized_flag(fingerprint_cmd) _insert_optimized_flag(fingerprint_cmd)
fingerprint_cmd.extend(shlex.split(hcatTuning)) fingerprint_cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(fingerprint_cmd) _append_potfile_arg(fingerprint_cmd)
hcatProcess = subprocess.Popen(fingerprint_cmd) _run_hcat_cmd(
try: fingerprint_cmd, attack_name="Fingerprint", hash_file=hcatHashFile
hcatProcess.wait() )
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
# Secondary attack: run hybrid on the expanded candidates (mode 6/7 variants). # Secondary attack: run hybrid on the expanded candidates (mode 6/7 variants).
# This is intentionally optional to avoid changing the "extensive" pipeline ordering. # This is intentionally optional to avoid changing the "extensive" pipeline ordering.
@@ -1529,12 +1596,7 @@ def hcatCombination(hcatHashType, hcatHashFile, wordlists=None):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Combination", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
hcatCombinationCount = lineCount(hcatHashFile + ".out") - hcatHashCracked hcatCombinationCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1568,15 +1630,15 @@ def hcatCombinator3(hcatHashType, hcatHashFile, wordlists):
_append_potfile_arg(hashcat_cmd) _append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
assert generator_proc.stdout is not None assert generator_proc.stdout is not None
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout) _run_hcat_cmd(
generator_proc.stdout.close() hashcat_cmd,
try: attack_name="Combinator3",
hcatProcess.wait() hash_file=hcatHashFile,
generator_proc.wait() stdin=generator_proc.stdout,
except KeyboardInterrupt: companion_procs=[generator_proc],
print("Killing PID {0}...".format(str(hcatProcess.pid))) )
hcatProcess.kill() if generator_proc.stdout:
generator_proc.kill() generator_proc.stdout.close()
hcatCombinator3Count = lineCount(hcatHashFile + ".out") - hcatHashCracked hcatCombinator3Count = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1614,15 +1676,15 @@ def hcatCombinatorX(hcatHashType, hcatHashFile, wordlists, separator=None):
_append_potfile_arg(hashcat_cmd) _append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
assert generator_proc.stdout is not None assert generator_proc.stdout is not None
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout) _run_hcat_cmd(
generator_proc.stdout.close() hashcat_cmd,
try: attack_name="CombinatorX",
hcatProcess.wait() hash_file=hcatHashFile,
generator_proc.wait() stdin=generator_proc.stdout,
except KeyboardInterrupt: companion_procs=[generator_proc],
print("Killing PID {0}...".format(str(hcatProcess.pid))) )
hcatProcess.kill() if generator_proc.stdout:
generator_proc.kill() generator_proc.stdout.close()
hcatCombinatorXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked hcatCombinatorXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1649,15 +1711,15 @@ def hcatNgramX(hcatHashType, hcatHashFile, corpus, group_size=3):
_append_potfile_arg(hashcat_cmd) _append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
assert generator_proc.stdout is not None assert generator_proc.stdout is not None
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout) _run_hcat_cmd(
generator_proc.stdout.close() hashcat_cmd,
try: attack_name="NgramX",
hcatProcess.wait() hash_file=hcatHashFile,
generator_proc.wait() stdin=generator_proc.stdout,
except KeyboardInterrupt: companion_procs=[generator_proc],
print("Killing PID {0}...".format(str(hcatProcess.pid))) )
hcatProcess.kill() if generator_proc.stdout:
generator_proc.kill() generator_proc.stdout.close()
hcatNgramXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked hcatNgramXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1711,12 +1773,7 @@ def hcatHybrid(hcatHashType, hcatHashFile, wordlists=None):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Hybrid", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
hcatHybridCount = lineCount(hcatHashFile + ".out") - hcatHashCracked hcatHybridCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1749,13 +1806,12 @@ def hcatYoloCombination(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(
try: cmd,
hcatProcess.wait() attack_name="YOLO Combination",
except KeyboardInterrupt: hash_file=hcatHashFile,
print("Killing PID {0}...".format(str(hcatProcess.pid))) reraise_interrupt=True,
hcatProcess.kill() )
raise
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
@@ -1800,12 +1856,7 @@ def hcatBandrel(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Bandrel", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
print( print(
"Checking passwords against pipal for top {0} passwords and basewords".format( "Checking passwords against pipal for top {0} passwords and basewords".format(
pipal_count pipal_count
@@ -1845,12 +1896,7 @@ def hcatBandrel(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Bandrel", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
# Pull an Ollama model via the /api/pull streaming endpoint # Pull an Ollama model via the /api/pull streaming endpoint
@@ -2053,12 +2099,14 @@ def hcatOllama(hcatHashType, hcatHashFile, mode, context_data):
] ]
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try: try:
hcatProcess.wait() _run_hcat_cmd(
cmd,
attack_name="LLM",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
return return
# Step D: Run hashcat with LLM candidates against every rule in the rules directory # Step D: Run hashcat with LLM candidates against every rule in the rules directory
@@ -2088,12 +2136,14 @@ def hcatOllama(hcatHashType, hcatHashFile, mode, context_data):
] ]
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try: try:
hcatProcess.wait() _run_hcat_cmd(
cmd,
attack_name="LLM",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
return return
@@ -2135,13 +2185,12 @@ def hcatMiddleCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(
try: cmd,
hcatProcess.wait() attack_name="Middle Combinator",
except KeyboardInterrupt: hash_file=hcatHashFile,
print("Killing PID {0}...".format(str(hcatProcess.pid))) reraise_interrupt=True,
hcatProcess.kill() )
raise
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
@@ -2180,12 +2229,7 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Thorough Combinator", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
try: try:
for x in range(len(masks)): for x in range(len(masks)):
@@ -2209,13 +2253,12 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(
try: cmd,
hcatProcess.wait() attack_name="Thorough Combinator",
except KeyboardInterrupt: hash_file=hcatHashFile,
print("Killing PID {0}...".format(str(hcatProcess.pid))) reraise_interrupt=True,
hcatProcess.kill() )
raise
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
try: try:
@@ -2240,13 +2283,12 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(
try: cmd,
hcatProcess.wait() attack_name="Thorough Combinator",
except KeyboardInterrupt: hash_file=hcatHashFile,
print("Killing PID {0}...".format(str(hcatProcess.pid))) reraise_interrupt=True,
hcatProcess.kill() )
raise
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
try: try:
@@ -2273,11 +2315,14 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(
hcatProcess.wait() cmd,
attack_name="Thorough Combinator",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid))) pass
hcatProcess.kill()
# Pathwell Mask Brute Force Attack # Pathwell Mask Brute Force Attack
@@ -2300,12 +2345,7 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Pathwell Brute Force", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""): def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""):
@@ -2329,12 +2369,7 @@ def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Ad-hoc Mask", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
def hcatMarkovTrain(source_file, hcatHashFile): def hcatMarkovTrain(source_file, hcatHashFile):
@@ -2429,12 +2464,7 @@ def hcatMarkovBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
_insert_optimized_flag(cmd) _insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Markov Brute Force", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
# Combipow Passphrase Attack # Combipow Passphrase Attack
@@ -2478,15 +2508,16 @@ def hcatCombipow(hcatHashType, hcatHashFile, wordlist, use_space_sep=True):
hashcat_cmd.extend(shlex.split(hcatTuning)) hashcat_cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(hashcat_cmd) _append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE) generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout)
generator_proc.stdout.close()
try: try:
hcatProcess.wait() _run_hcat_cmd(
generator_proc.wait() hashcat_cmd,
except KeyboardInterrupt: attack_name="Combipow",
print("Killing PID {0}...".format(str(hcatProcess.pid))) hash_file=hcatHashFile,
hcatProcess.kill() stdin=generator_proc.stdout,
generator_proc.kill() companion_procs=[generator_proc],
)
if generator_proc.stdout:
generator_proc.stdout.close()
finally: finally:
if tmp_file is not None: if tmp_file is not None:
with contextlib.suppress(OSError): with contextlib.suppress(OSError):
@@ -2532,15 +2563,15 @@ def hcatPrince(hcatHashType, hcatHashFile):
hashcat_cmd = _add_debug_mode_for_rules(hashcat_cmd) hashcat_cmd = _add_debug_mode_for_rules(hashcat_cmd)
with _open_wordlist(prince_base) as base: with _open_wordlist(prince_base) as base:
prince_proc = subprocess.Popen(prince_cmd, stdin=base, stdout=subprocess.PIPE) prince_proc = subprocess.Popen(prince_cmd, stdin=base, stdout=subprocess.PIPE)
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=prince_proc.stdout) _run_hcat_cmd(
prince_proc.stdout.close() hashcat_cmd,
try: attack_name="PRINCE",
hcatProcess.wait() hash_file=hcatHashFile,
prince_proc.wait() stdin=prince_proc.stdout,
except KeyboardInterrupt: companion_procs=[prince_proc],
print("Killing PID {0}...".format(str(hcatProcess.pid))) )
hcatProcess.kill() if prince_proc.stdout:
prince_proc.kill() prince_proc.stdout.close()
def hcatPermute(hcatHashType, hcatHashFile, wordlist): def hcatPermute(hcatHashType, hcatHashFile, wordlist):
@@ -2570,17 +2601,15 @@ def hcatPermute(hcatHashType, hcatHashFile, wordlist):
permute_proc = subprocess.Popen( permute_proc = subprocess.Popen(
[permute_path], stdin=wl_file, stdout=subprocess.PIPE [permute_path], stdin=wl_file, stdout=subprocess.PIPE
) )
hcatProcess = subprocess.Popen( _run_hcat_cmd(
hashcat_cmd, stdin=permute_proc.stdout hashcat_cmd,
attack_name="Permute",
hash_file=hcatHashFile,
stdin=permute_proc.stdout,
companion_procs=[permute_proc],
) )
permute_proc.stdout.close() if permute_proc.stdout:
try: permute_proc.stdout.close()
hcatProcess.wait()
permute_proc.wait()
except KeyboardInterrupt:
print(f"Killing PID {hcatProcess.pid}...")
hcatProcess.kill()
permute_proc.kill()
hcatPermuteCount = lineCount(f"{hcatHashFile}.out") - hcatHashCracked hcatPermuteCount = lineCount(f"{hcatHashFile}.out") - hcatHashCracked
@@ -2709,16 +2738,21 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates, hcatChains=""):
enum_proc = subprocess.Popen( enum_proc = subprocess.Popen(
enum_cmd, cwd=model_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE enum_cmd, cwd=model_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE
) )
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout)
enum_proc.stdout.close()
try: try:
hcatProcess.wait() _run_hcat_cmd(
enum_proc.wait() hashcat_cmd,
attack_name="OMEN",
hash_file=hcatHashFile,
stdin=enum_proc.stdout,
companion_procs=[enum_proc],
reraise_interrupt=True,
)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid))) if enum_proc.stderr:
hcatProcess.kill() enum_proc.stderr.close()
enum_proc.kill()
return return
if enum_proc.stdout:
enum_proc.stdout.close()
if enum_proc.returncode != 0: if enum_proc.returncode != 0:
stderr_output = ( stderr_output = (
enum_proc.stderr.read().decode("utf-8", errors="replace").strip() enum_proc.stderr.read().decode("utf-8", errors="replace").strip()
@@ -2756,12 +2790,7 @@ def hcatGoodMeasure(hcatHashType, hcatHashFile):
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd) cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Good Measure", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
hcatExtraCount = lineCount(hcatHashFile + ".out") - hcatHashCracked hcatExtraCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -2789,11 +2818,12 @@ def hcatLMtoNT():
] ]
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(
try: cmd,
hcatProcess.wait() attack_name="LM to NT (LM phase)",
except KeyboardInterrupt: hash_file=f"{hcatHashFile}.lm",
hcatProcess.kill() out_path=f"{hcatHashFile}.lm.cracked",
)
_write_delimited_field(f"{hcatHashFile}.lm.cracked", f"{hcatHashFile}.working", 2) _write_delimited_field(f"{hcatHashFile}.lm.cracked", f"{hcatHashFile}.working", 2)
converted = convert_hex("{hash_file}.working".format(hash_file=hcatHashFile)) converted = convert_hex("{hash_file}.working".format(hash_file=hcatHashFile))
@@ -2840,12 +2870,12 @@ def hcatLMtoNT():
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd) cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(
try: cmd,
hcatProcess.wait() attack_name="LM to NT (NT phase)",
except KeyboardInterrupt: hash_file=f"{hcatHashFile}.nt",
print("Killing PID {0}...".format(str(hcatProcess.pid))) out_path=f"{hcatHashFile}.nt.out",
hcatProcess.kill() )
# toggle-lm-ntlm.rule by Didier Stevens https://blog.didierstevens.com/2016/07/16/tool-to-generate-hashcat-toggle-rules/ # toggle-lm-ntlm.rule by Didier Stevens https://blog.didierstevens.com/2016/07/16/tool-to-generate-hashcat-toggle-rules/
@@ -2882,11 +2912,7 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords):
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd) cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Recycle", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
hcatProcess.kill()
def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist): def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist):
@@ -2922,12 +2948,7 @@ def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist):
] ]
cmd.extend(shlex.split(hcatTuning)) cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd) _append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd) _run_hcat_cmd(cmd, attack_name="Random Rules", hash_file=hcatHashFile)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print(f"Killing PID {hcatProcess.pid}...")
hcatProcess.kill()
finally: finally:
if os.path.exists(rules_path): if os.path.exists(rules_path):
os.unlink(rules_path) os.unlink(rules_path)
@@ -4067,6 +4088,26 @@ def quit_hc():
sys.exit(0) sys.exit(0)
def toggle_notifications():
"""Global on/off toggle for Pushover notifications.
Flips ``notify_enabled`` in the active settings and persists to
``config.json``. Prints the new state so the user has immediate
confirmation even though the menu label will also refresh on the
next render.
"""
new_state = _notify.toggle_enabled()
label = "ON" if new_state else "OFF"
print(f"\nPushover notifications are now {label}.")
if new_state:
settings = _notify.get_settings()
if not settings.pushover_token or not settings.pushover_user:
print(
"[!] notify_pushover_token / notify_pushover_user are empty in "
"config.json — notifications will silently no-op until set."
)
def get_main_menu_items(): def get_main_menu_items():
"""Return ordered (key, label) pairs for the main menu.""" """Return ordered (key, label) pairs for the main menu."""
items = [ items = [
@@ -4091,6 +4132,10 @@ def get_main_menu_items():
("22", "Combipow Passphrase Attack"), ("22", "Combipow Passphrase Attack"),
("80", "Wordlist Tools"), ("80", "Wordlist Tools"),
("81", "Rule File Tools"), ("81", "Rule File Tools"),
(
"83",
f"Toggle Pushover Notifications [{'ON' if _notify.get_settings().enabled else 'OFF'}]",
),
("90", "Download rules from Hashmob.net"), ("90", "Download rules from Hashmob.net"),
("91", "Analyze Hashcat Rules"), ("91", "Analyze Hashcat Rules"),
("92", "Download wordlists from Hashmob.net"), ("92", "Download wordlists from Hashmob.net"),
@@ -4134,6 +4179,7 @@ def get_main_menu_options():
"22": combipow_crack, "22": combipow_crack,
"80": wordlist_tools_submenu, "80": wordlist_tools_submenu,
"81": rule_tools_submenu, "81": rule_tools_submenu,
"83": toggle_notifications,
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory), "90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
"91": analyze_rules, "91": analyze_rules,
"92": download_hashmob_wordlists, "92": download_hashmob_wordlists,

218
tests/test_run_hcat_cmd.py Normal file
View File

@@ -0,0 +1,218 @@
"""Tests for the ``_run_hcat_cmd`` subprocess/notify wrapper in main.py."""
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def main_module(hc_module):
return hc_module._main
def _make_mock_proc(wait_side_effect=None):
proc = MagicMock()
if wait_side_effect is not None:
proc.wait.side_effect = wait_side_effect
else:
proc.wait.return_value = None
proc.pid = 12345
return proc
class TestRunHcatCmd:
def test_normal_flow_waits_and_notifies(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc) as mock_popen,
patch.object(main_module, "lineCount", return_value=42),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=True)
mock_notify.start_tailer.return_value = None
main_module._run_hcat_cmd(
["hashcat", "-m", "1000"], attack_name="Brute Force", hash_file=hash_file
)
mock_popen.assert_called_once()
proc.wait.assert_called_once()
proc.kill.assert_not_called()
mock_notify.notify_job_done.assert_called_once_with(
"Brute Force", 42, hash_file
)
def test_keyboard_interrupt_kills_process(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch.object(main_module, "lineCount", return_value=0),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=False)
mock_notify.start_tailer.return_value = None
main_module._run_hcat_cmd(
["hashcat"], attack_name="Brute Force", hash_file=hash_file
)
proc.kill.assert_called_once()
def test_no_notify_when_attack_name_empty(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
main_module._run_hcat_cmd(["hashcat"], attack_name="", hash_file=hash_file)
mock_notify.notify_job_done.assert_not_called()
mock_notify.start_tailer.assert_not_called()
def test_suppressed_skips_notifications(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = True
mock_notify.get_settings.return_value = MagicMock(enabled=True)
main_module._run_hcat_cmd(
["hashcat"], attack_name="Brute Force", hash_file=hash_file
)
mock_notify.start_tailer.assert_not_called()
mock_notify.notify_job_done.assert_not_called()
def test_stdin_is_forwarded_to_popen(self, main_module, tmp_path):
stdin_stub = object()
proc = _make_mock_proc()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc) as mock_popen,
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=False)
mock_notify.start_tailer.return_value = None
main_module._run_hcat_cmd(["hashcat"], stdin=stdin_stub)
_, kwargs = mock_popen.call_args
assert kwargs.get("stdin") is stdin_stub
def test_companion_procs_killed_on_interrupt(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
companion = _make_mock_proc()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch.object(main_module, "lineCount", return_value=0),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=False)
mock_notify.start_tailer.return_value = None
main_module._run_hcat_cmd(
["hashcat"],
attack_name="Combinator3",
hash_file=hash_file,
companion_procs=[companion],
)
proc.kill.assert_called_once()
companion.kill.assert_called_once()
def test_companion_procs_waited_on_normal_exit(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc()
companion = _make_mock_proc()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=False)
mock_notify.start_tailer.return_value = None
main_module._run_hcat_cmd(
["hashcat"],
attack_name="Combinator3",
hash_file=hash_file,
companion_procs=[companion],
)
companion.wait.assert_called_once()
companion.kill.assert_not_called()
def test_reraise_interrupt_propagates(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch.object(main_module, "lineCount", return_value=0),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=False)
mock_notify.start_tailer.return_value = None
with pytest.raises(KeyboardInterrupt):
main_module._run_hcat_cmd(
["hashcat"],
attack_name="YOLO",
hash_file=hash_file,
reraise_interrupt=True,
)
def test_out_path_override(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
alt_out = str(tmp_path / "hashes.lm.cracked")
proc = _make_mock_proc()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch.object(main_module, "lineCount", return_value=9) as mock_lc,
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=True)
mock_notify.start_tailer.return_value = None
main_module._run_hcat_cmd(
["hashcat"],
attack_name="LM Phase",
hash_file=hash_file,
out_path=alt_out,
)
mock_lc.assert_called_with(alt_out)
mock_notify.notify_job_done.assert_called_once_with(
"LM Phase", 9, hash_file
)
def test_tailer_is_stopped_in_finally(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
tailer = MagicMock()
with (
patch("hate_crack.main.subprocess.Popen", return_value=proc),
patch.object(main_module, "lineCount", return_value=0),
patch("hate_crack.main._notify") as mock_notify,
):
mock_notify.is_suppressed.return_value = False
mock_notify.get_settings.return_value = MagicMock(enabled=True)
mock_notify.start_tailer.return_value = tailer
main_module._run_hcat_cmd(
["hashcat"], attack_name="Brute Force", hash_file=hash_file
)
mock_notify.stop_tailer.assert_called_once_with(tailer)

View File

@@ -32,6 +32,7 @@ MENU_OPTION_TEST_CASES = [
("22", CLI_MODULE._attacks, "combipow_crack", "combipow"), ("22", CLI_MODULE._attacks, "combipow_crack", "combipow"),
("80", CLI_MODULE._attacks, "wordlist_tools_submenu", "wordlist-tools"), ("80", CLI_MODULE._attacks, "wordlist_tools_submenu", "wordlist-tools"),
("81", CLI_MODULE._attacks, "rule_tools_submenu", "rule-tools"), ("81", CLI_MODULE._attacks, "rule_tools_submenu", "rule-tools"),
("83", CLI_MODULE, "toggle_notifications", "toggle-notifications"),
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"), ("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"), ("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"), ("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),