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",
"hcatMiddleCombinator", "hcatThoroughCombinator", "hcatCombipow", "hcatPrince",
"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,
"80": _attacks.wordlist_tools_submenu,
"81": _attacks.rule_tools_submenu,
"83": toggle_notifications,
"90": download_hashmob_rules,
"91": weakpass_wordlist_menu,
"92": download_hashmob_wordlists,

View File

@@ -4,6 +4,7 @@ import os
import readline
from typing import Any
from hate_crack import notify as _notify
from hate_crack.api import download_hashmob_rules
from hate_crack.formatting import print_multicolumn_list
from hate_crack.menu import interactive_menu
@@ -107,6 +108,7 @@ def _select_rules(ctx) -> list[str] | None:
def quick_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Quick Crack")
wordlist_choice = None
default_dir = ctx.hcatOptimizedWordlists
@@ -177,6 +179,7 @@ def quick_crack(ctx: Any) -> None:
def loopback_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Loopback")
empty_wordlist = os.path.join(ctx.hcatWordlists, "empty.txt")
os.makedirs(ctx.hcatWordlists, exist_ok=True)
if not os.path.exists(empty_wordlist):
@@ -200,26 +203,43 @@ def loopback_attack(ctx: Any) -> None:
def extensive_crack(ctx: Any) -> None:
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
# Orchestrator attack: chains ~14 primitives. We suppress each primitive's
# own notifications and fire exactly one "Extensive Crack complete" at the
# end with the aggregate delta. This both prevents notification spam and
# gives the user an actually-useful summary.
_notify.prompt_notify_for_attack("Extensive Crack")
out_path = ctx.hcatHashFile + ".out"
cracked_before = ctx.lineCount(out_path) if os.path.exists(out_path) else 0
with _notify.suppressed_notifications():
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)
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)
# Note: ``cracked_before`` is tracked for potential future per-orchestrator
# delta reporting, but today the notify message uses the absolute count
# because that matches what single-attack notifications already report.
_ = cracked_before
def brute_force_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Brute Force")
hcatMinLen = int(
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:
_notify.prompt_notify_for_attack("Top Mask")
hcatTargetTime = int(
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:
_notify.prompt_notify_for_attack("Fingerprint")
while True:
raw = input("\nEnter expander max length (7-36) (7): ").strip()
if raw == "":
@@ -261,6 +283,7 @@ def fingerprint_crack(ctx: Any) -> None:
def combinator_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Combinator")
print("\n" + "=" * 60)
print("COMBINATOR ATTACK")
print("=" * 60)
@@ -299,6 +322,7 @@ def combinator_crack(ctx: Any) -> None:
def hybrid_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Hybrid")
print("\n" + "=" * 60)
print("HYBRID ATTACK")
print("=" * 60)
@@ -367,22 +391,27 @@ def hybrid_crack(ctx: Any) -> None:
def pathwell_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Pathwell Brute Force")
ctx.hcatPathwellBruteForce(ctx.hcatHashType, ctx.hcatHashFile)
def prince_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("PRINCE")
ctx.hcatPrince(ctx.hcatHashType, ctx.hcatHashFile)
def yolo_combination(ctx: Any) -> None:
_notify.prompt_notify_for_attack("YOLO Combination")
ctx.hcatYoloCombination(ctx.hcatHashType, ctx.hcatHashFile)
def thorough_combinator(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Thorough Combinator")
ctx.hcatThoroughCombinator(ctx.hcatHashType, ctx.hcatHashFile)
def middle_combinator(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Middle Combinator")
ctx.hcatMiddleCombinator(ctx.hcatHashType, ctx.hcatHashFile)
@@ -447,10 +476,12 @@ def combinator_3plus_crack(ctx: Any) -> None:
def bandrel_method(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Bandrel")
ctx.hcatBandrel(ctx.hcatHashType, ctx.hcatHashFile)
def ollama_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("LLM")
print("\n\tLLM Attack")
company = input("Company name: ").strip()
industry = input("Industry: ").strip()
@@ -491,6 +522,7 @@ def _omen_pick_training_wordlist(ctx: Any):
def omen_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("OMEN")
print("\n\tOMEN Attack (Ordered Markov ENumerator)")
omen_dir = os.path.join(ctx.hate_path, "omen")
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:
_notify.prompt_notify_for_attack("Ad-hoc Mask")
print(
"\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:
_notify.prompt_notify_for_attack("Markov Brute Force")
print("\n\tMarkov Brute Force Attack")
hcstat2_path = f"{ctx.hcatHashFile}.hcstat2"
need_training = True
@@ -640,6 +674,7 @@ def markov_brute_force(ctx: Any) -> None:
def combipow_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Combipow")
wordlist = None
while wordlist is None:
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:
_notify.prompt_notify_for_attack("Random Rules")
print("\n" + "=" * 60)
print("RANDOM RULES ATTACK")
print("=" * 60)
@@ -743,6 +779,7 @@ def generate_rules_crack(ctx: Any) -> None:
def ngram_attack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("N-gram")
print("\n" + "=" * 60)
print("NGRAM ATTACK")
print("=" * 60)
@@ -769,6 +806,7 @@ def ngram_attack(ctx: Any) -> None:
def permute_crack(ctx: Any) -> None:
_notify.prompt_notify_for_attack("Permute")
print("\n" + "=" * 60)
print("PERMUTATION ATTACK")
print("=" * 60)

View File

@@ -25,5 +25,13 @@
"passgptModel": "javirandor/passgpt-10characters",
"passgptMaxCandidates": 1000000,
"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
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"
hcatCombinatorBin = "combinator.bin"
hcatPrinceBin = "pp64.bin"
@@ -729,6 +737,93 @@ def _debug_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:
try:
with open(path, "rb") as f:
@@ -1189,12 +1284,7 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Brute Force", hash_file=hcatHashFile)
hcatBruteCount = lineCount(hcatHashFile + ".out")
@@ -1226,12 +1316,7 @@ def hcatDictionary(hcatHashType, hcatHashFile):
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Dictionary", hash_file=hcatHashFile)
rule_d3ad0ne = get_rule_path("d3ad0ne.rule")
rule_toxic = get_rule_path("T0XlC.rule")
@@ -1267,12 +1352,7 @@ def hcatDictionary(hcatHashType, hcatHashFile):
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Dictionary", hash_file=hcatHashFile)
finally:
os.unlink(combined_path)
@@ -1316,12 +1396,7 @@ def hcatQuickDictionary(
)
cmd = _add_debug_mode_for_rules(cmd)
_debug_cmd(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Quick Dictionary", hash_file=hcatHashFile)
# Top Mask Attack
@@ -1383,12 +1458,7 @@ def hcatTopMask(hcatHashType, hcatHashFile, hcatTargetTime):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Top Mask", hash_file=hcatHashFile)
hcatMaskCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1462,12 +1532,9 @@ def hcatFingerprint(
_insert_optimized_flag(fingerprint_cmd)
fingerprint_cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(fingerprint_cmd)
hcatProcess = subprocess.Popen(fingerprint_cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(
fingerprint_cmd, attack_name="Fingerprint", hash_file=hcatHashFile
)
# Secondary attack: run hybrid on the expanded candidates (mode 6/7 variants).
# 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)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Combination", hash_file=hcatHashFile)
hcatCombinationCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1568,15 +1630,15 @@ def hcatCombinator3(hcatHashType, hcatHashFile, wordlists):
_append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
assert generator_proc.stdout is not None
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout)
generator_proc.stdout.close()
try:
hcatProcess.wait()
generator_proc.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
generator_proc.kill()
_run_hcat_cmd(
hashcat_cmd,
attack_name="Combinator3",
hash_file=hcatHashFile,
stdin=generator_proc.stdout,
companion_procs=[generator_proc],
)
if generator_proc.stdout:
generator_proc.stdout.close()
hcatCombinator3Count = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1614,15 +1676,15 @@ def hcatCombinatorX(hcatHashType, hcatHashFile, wordlists, separator=None):
_append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
assert generator_proc.stdout is not None
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout)
generator_proc.stdout.close()
try:
hcatProcess.wait()
generator_proc.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
generator_proc.kill()
_run_hcat_cmd(
hashcat_cmd,
attack_name="CombinatorX",
hash_file=hcatHashFile,
stdin=generator_proc.stdout,
companion_procs=[generator_proc],
)
if generator_proc.stdout:
generator_proc.stdout.close()
hcatCombinatorXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1649,15 +1711,15 @@ def hcatNgramX(hcatHashType, hcatHashFile, corpus, group_size=3):
_append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
assert generator_proc.stdout is not None
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout)
generator_proc.stdout.close()
try:
hcatProcess.wait()
generator_proc.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
generator_proc.kill()
_run_hcat_cmd(
hashcat_cmd,
attack_name="NgramX",
hash_file=hcatHashFile,
stdin=generator_proc.stdout,
companion_procs=[generator_proc],
)
if generator_proc.stdout:
generator_proc.stdout.close()
hcatNgramXCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1711,12 +1773,7 @@ def hcatHybrid(hcatHashType, hcatHashFile, wordlists=None):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Hybrid", hash_file=hcatHashFile)
hcatHybridCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -1749,13 +1806,12 @@ def hcatYoloCombination(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
raise
_run_hcat_cmd(
cmd,
attack_name="YOLO Combination",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt:
pass
@@ -1800,12 +1856,7 @@ def hcatBandrel(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Bandrel", hash_file=hcatHashFile)
print(
"Checking passwords against pipal for top {0} passwords and basewords".format(
pipal_count
@@ -1845,12 +1896,7 @@ def hcatBandrel(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Bandrel", hash_file=hcatHashFile)
# 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))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
_run_hcat_cmd(
cmd,
attack_name="LLM",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
return
# 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))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
_run_hcat_cmd(
cmd,
attack_name="LLM",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
return
@@ -2135,13 +2185,12 @@ def hcatMiddleCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
raise
_run_hcat_cmd(
cmd,
attack_name="Middle Combinator",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt:
pass
@@ -2180,12 +2229,7 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Thorough Combinator", hash_file=hcatHashFile)
try:
for x in range(len(masks)):
@@ -2209,13 +2253,12 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
raise
_run_hcat_cmd(
cmd,
attack_name="Thorough Combinator",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt:
pass
try:
@@ -2240,13 +2283,12 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
raise
_run_hcat_cmd(
cmd,
attack_name="Thorough Combinator",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt:
pass
try:
@@ -2273,11 +2315,14 @@ def hcatThoroughCombinator(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
hcatProcess.wait()
_run_hcat_cmd(
cmd,
attack_name="Thorough Combinator",
hash_file=hcatHashFile,
reraise_interrupt=True,
)
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
pass
# Pathwell Mask Brute Force Attack
@@ -2300,12 +2345,7 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Pathwell Brute Force", hash_file=hcatHashFile)
def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""):
@@ -2329,12 +2369,7 @@ def hcatAdHocMask(hcatHashType, hcatHashFile, mask, custom_charsets=""):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Ad-hoc Mask", hash_file=hcatHashFile)
def hcatMarkovTrain(source_file, hcatHashFile):
@@ -2429,12 +2464,7 @@ def hcatMarkovBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen):
_insert_optimized_flag(cmd)
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Markov Brute Force", hash_file=hcatHashFile)
# Combipow Passphrase Attack
@@ -2478,15 +2508,16 @@ def hcatCombipow(hcatHashType, hcatHashFile, wordlist, use_space_sep=True):
hashcat_cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(hashcat_cmd)
generator_proc = subprocess.Popen(generator_cmd, stdout=subprocess.PIPE)
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=generator_proc.stdout)
generator_proc.stdout.close()
try:
hcatProcess.wait()
generator_proc.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
generator_proc.kill()
_run_hcat_cmd(
hashcat_cmd,
attack_name="Combipow",
hash_file=hcatHashFile,
stdin=generator_proc.stdout,
companion_procs=[generator_proc],
)
if generator_proc.stdout:
generator_proc.stdout.close()
finally:
if tmp_file is not None:
with contextlib.suppress(OSError):
@@ -2532,15 +2563,15 @@ def hcatPrince(hcatHashType, hcatHashFile):
hashcat_cmd = _add_debug_mode_for_rules(hashcat_cmd)
with _open_wordlist(prince_base) as base:
prince_proc = subprocess.Popen(prince_cmd, stdin=base, stdout=subprocess.PIPE)
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=prince_proc.stdout)
prince_proc.stdout.close()
try:
hcatProcess.wait()
prince_proc.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
prince_proc.kill()
_run_hcat_cmd(
hashcat_cmd,
attack_name="PRINCE",
hash_file=hcatHashFile,
stdin=prince_proc.stdout,
companion_procs=[prince_proc],
)
if prince_proc.stdout:
prince_proc.stdout.close()
def hcatPermute(hcatHashType, hcatHashFile, wordlist):
@@ -2570,17 +2601,15 @@ def hcatPermute(hcatHashType, hcatHashFile, wordlist):
permute_proc = subprocess.Popen(
[permute_path], stdin=wl_file, stdout=subprocess.PIPE
)
hcatProcess = subprocess.Popen(
hashcat_cmd, stdin=permute_proc.stdout
_run_hcat_cmd(
hashcat_cmd,
attack_name="Permute",
hash_file=hcatHashFile,
stdin=permute_proc.stdout,
companion_procs=[permute_proc],
)
permute_proc.stdout.close()
try:
hcatProcess.wait()
permute_proc.wait()
except KeyboardInterrupt:
print(f"Killing PID {hcatProcess.pid}...")
hcatProcess.kill()
permute_proc.kill()
if permute_proc.stdout:
permute_proc.stdout.close()
hcatPermuteCount = lineCount(f"{hcatHashFile}.out") - hcatHashCracked
@@ -2709,16 +2738,21 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates, hcatChains=""):
enum_proc = subprocess.Popen(
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:
hcatProcess.wait()
enum_proc.wait()
_run_hcat_cmd(
hashcat_cmd,
attack_name="OMEN",
hash_file=hcatHashFile,
stdin=enum_proc.stdout,
companion_procs=[enum_proc],
reraise_interrupt=True,
)
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
enum_proc.kill()
if enum_proc.stderr:
enum_proc.stderr.close()
return
if enum_proc.stdout:
enum_proc.stdout.close()
if enum_proc.returncode != 0:
stderr_output = (
enum_proc.stderr.read().decode("utf-8", errors="replace").strip()
@@ -2756,12 +2790,7 @@ def hcatGoodMeasure(hcatHashType, hcatHashFile):
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Good Measure", hash_file=hcatHashFile)
hcatExtraCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
@@ -2789,11 +2818,12 @@ def hcatLMtoNT():
]
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
hcatProcess.kill()
_run_hcat_cmd(
cmd,
attack_name="LM to NT (LM phase)",
hash_file=f"{hcatHashFile}.lm",
out_path=f"{hcatHashFile}.lm.cracked",
)
_write_delimited_field(f"{hcatHashFile}.lm.cracked", f"{hcatHashFile}.working", 2)
converted = convert_hex("{hash_file}.working".format(hash_file=hcatHashFile))
@@ -2840,12 +2870,12 @@ def hcatLMtoNT():
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print("Killing PID {0}...".format(str(hcatProcess.pid)))
hcatProcess.kill()
_run_hcat_cmd(
cmd,
attack_name="LM to NT (NT phase)",
hash_file=f"{hcatHashFile}.nt",
out_path=f"{hcatHashFile}.nt.out",
)
# 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))
_append_potfile_arg(cmd)
cmd = _add_debug_mode_for_rules(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Recycle", hash_file=hcatHashFile)
def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist):
@@ -2922,12 +2948,7 @@ def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist):
]
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print(f"Killing PID {hcatProcess.pid}...")
hcatProcess.kill()
_run_hcat_cmd(cmd, attack_name="Random Rules", hash_file=hcatHashFile)
finally:
if os.path.exists(rules_path):
os.unlink(rules_path)
@@ -4067,6 +4088,26 @@ def quit_hc():
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():
"""Return ordered (key, label) pairs for the main menu."""
items = [
@@ -4091,6 +4132,10 @@ def get_main_menu_items():
("22", "Combipow Passphrase Attack"),
("80", "Wordlist Tools"),
("81", "Rule File Tools"),
(
"83",
f"Toggle Pushover Notifications [{'ON' if _notify.get_settings().enabled else 'OFF'}]",
),
("90", "Download rules from Hashmob.net"),
("91", "Analyze Hashcat Rules"),
("92", "Download wordlists from Hashmob.net"),
@@ -4134,6 +4179,7 @@ def get_main_menu_options():
"22": combipow_crack,
"80": wordlist_tools_submenu,
"81": rule_tools_submenu,
"83": toggle_notifications,
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
"91": analyze_rules,
"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"),
("80", CLI_MODULE._attacks, "wordlist_tools_submenu", "wordlist-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"),
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),