- Suppress transmission-daemon stdout/stderr (background process)
- Create a watch/ subdir in the session temp config dir; start daemon
with --watch-dir instead of --no-watch-dir
- Replace transmission-remote -a with shutil.copy2 into the watch dir,
then poll until the daemon picks it up
- Save .torrent metadata files to tempfile.gettempdir() instead of the
wordlist directory; update cleanup to match
- Update TransmissionSession.add tests to cover the new watch-dir flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_run_upgrade now runs `git pull && git fetch --tags && make install`
instead of the bespoke _install_system_deps function. make install is
idempotent, handles all platforms, installs system deps (transmission-
daemon, p7zip), rebuilds the Python package, and updates the CLI shim.
Remove the now-unnecessary uv binary pre-check (make install locates
it) and its stale test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _install_system_deps() called by _run_upgrade() after a successful
git pull. Installs transmission-daemon (Linux) or transmission-cli brew
formula (macOS) so users who upgrade don't need to re-run make install
manually. Also corrects _run_upgrade to exit 1 on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete wordlists/kill_transmission.sh (replaced by Python extraction).
Update Makefile, Dockerfile.test, lima config, README, TESTING.md, and
test_dependencies.py to reference transmission-daemon/transmission-remote
instead of transmission-cli.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace check_transmission_cli with check_transmission_daemon, add
_pick_free_port, TransmissionSession context manager, and
run_torrent_session. A single transmission-daemon process handles all
selected torrents in parallel and tears down via transmission-remote
--exit on completion or interrupt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
download_official_wordlist now propagates KeyboardInterrupt (via
_streamed_download) so callers like list_and_download_official_wordlists
can catch it and return to the menu. The old test expected False to be
returned, which no longer matches the intended design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix two test issues:
1. Patch time.sleep in hate_crack.api namespace to properly mock the module import (lines 384, 403). Tests were patching the global time.sleep, allowing real sleeps to execute and making tests slow.
2. Remove vacuous assertion in test_meta_refresh_redirect_uses_verbatim_url (line 440) that is always True due to URL domain mismatch. The equality assertion above it already covers the correctness requirement.
These changes speed up TestHashmobBackoff tests from slow to instant, and remove misleading logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the four new private helpers (_stream_response_to_file,
_streamed_download, _with_hashmob_backoff, _Hashmob429) and the
list_and_download_official_wordlists skip-existing path (10 new tests,
40 total in the file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added `test_labels_refresh_between_iterations` that sequences a toggle
then captures the submenu items twice, asserting the label flips
between renders. Guards against a regression where `items` is hoisted
out of the while-loop.
Also documented why the inline `from hate_crack.menu import
interactive_menu` is not actually redundant with the module-scope
import at main.py:77 — it re-reads the attribute on every call, which
is what lets tests patch `hate_crack.menu.interactive_menu`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotes notify_per_crack_enabled from config-file-only to a runtime
toggle with the same style (global-decl, default-init, OSError-via-logger)
as the existing toggle_enabled, with full TDD coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add save_per_crack_enabled() as a data-layer sibling to save_enabled(),
using the same _atomic_rewrite primitive so mid-write crashes cannot
corrupt config.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tests/test_random_rules_attack.py purges and re-imports hate_crack.*
modules, which leaves main._notify pointing at a different notify
object than a top-level patch("hate_crack.notify._send_pushover")
would touch. Under the full suite that caused the test's mock to
miss and the production call to hit the real Pushover API.
Switch to patch.object(hc_main, "_notify") -- same pattern as
tests/test_run_hcat_cmd.py but anchored to the exact module object
already bound as hc_main, so it is immune to sys.modules churn
regardless of import order. Drop the now-redundant _install_settings
helper and _reset_notify_state fixture.
Canned send path so a user can verify Pushover credentials without
running an attack. Ignores the global notify_enabled toggle — the test's
purpose is to confirm the pipe is live, not that attack notifications
are enabled. Prints a note when the global toggle is OFF so the user is
not confused later.
hate_crack.main calls notify.init() at import time with whatever
config.json is resolved from the developer's environment (often
~/.hate_crack/config.json). If that file has notify_enabled: true, the
per-attack prompt in attacks.py fires input() during tests and trips
pytest's capture fd, failing unrelated tests.
Add an autouse conftest fixture that clears notify module state before
and after every test so the suite is hermetic regardless of local
config. Notify-specific tests already use their own
clear_state_for_tests() fixture; this change covers the rest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`_run_hashcat_show` is called during the initial potfile check (main
preprocessing) and by `combine_ntlm_output` via `check_potfile()`. It
invokes `hashcat --show` via `subprocess.run` and previously did not go
through `_append_potfile_arg`, so it never received `--username`.
Without `--username`, hashcat treats the first colon of a `user:hash`
line as part of the hash and fails to match any potfile entries. This
caused the initial "already cracked" check to report zero hits for
legitimately cracked `user:hash` input files.
Route the command build through `_maybe_append_username_flag` before
invoking subprocess, mirroring the `_append_potfile_arg` pattern for
normal attack commands. Adds `TestUsernameInjectionIntoShow` covering
both flag-set and flag-unset code paths.
Refs #107
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Introduce hate_crack.notify package with a small functional public API
and a CrackTailer thread for polling hashcat output files. Package
layout keeps the HTTP call (_send_pushover) isolated so future backends
(Slack, generic webhooks) can be added as a sibling function rather
than a framework rewrite.
Core pieces:
- settings.py: NotifySettings dataclass plus atomic config persistence
(save_enabled, add_to_allowlist) via read-modify-write + os.replace.
- pushover.py: single _send_pushover() that never raises; network
errors, missing requests, and missing creds all funnel to False.
- _suppress.py: thread-local suppression context manager so
orchestrator attacks can chain primitives without flooding
notifications.
- tailer.py: CrackTailer(threading.Thread) that seeks to EOF on start,
polls at a user-configurable interval, and collapses per-tick bursts
into a single aggregate notification when they exceed the cap.
- __init__.py: public API (init, prompt_notify_for_attack,
notify_job_done, notify_crack, start_tailer, stop_tailer,
toggle_enabled, suppressed_notifications). Privacy guarantee:
notification payloads contain only attack name, counts, and hash
path, never plaintexts.
72 new tests cover dataclass defaults, atomic config writes, idempotent
allowlist updates, HTTP payload privacy, suppression nesting and
thread-locality, tailer EOF seek, burst cap, truncation recovery, and
the per-attack prompt's [y/n/always] flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unit tests for `detect_username_hash_format`: per-mode positives
(MD5/SHA1/SHA256/SHA512/NTLM/LM), negatives (bare hash, wrong hex
length, non-hex, mixed valid/invalid, pwdump, trailing garbage),
blocklist modes, unknown modes, and file-handling (BOM, CRLF, null
bytes, unicode username, comments, blank lines, missing/empty file,
sample_size).
Integration tests for the injection flow: asserts `--username` appears
in the command emitted by `hcatBruteForce` when `hcatUsernamePrefix` is
set, asserts no duplicate when `hcatTuning` already includes
`--username`, and verifies `_append_potfile_arg` still injects the flag
even when called with `use_potfile_path=False`.
Refs #107
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old loop reassigned crackedBefore at the top of each iteration and
initialized crackedAfter to 0, which could cause the loop to enter
spuriously or skip entirely. Switch to while True / break to properly
detect when an iteration produces no new cracks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that a pwdump file at /tmp/test_hashes.ntds produces output
at /tmp/test_hashes.ntds.out using real hashcat. Confirms no files
leak into the project directory. Gated behind HATE_CRACK_RUN_E2E=1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Output files now land next to the original hashfile. resolve_path()
already resolves relative paths against HATE_CRACK_ORIG_CWD, so
relocating the hashfile into CWD was unnecessary and created
confusing symlinks in the working directory.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bash shim uses `uv run --directory <install_dir>` which changes the
process CWD to the install directory. _ensure_hashfile_in_cwd() and the
Hashview download path used os.getcwd() to determine the target directory
for output files (.out, .nt, etc.), causing them to land in the install
directory instead of where the user ran the command.
Add orig_cwd() helper that reads HATE_CRACK_ORIG_CWD (set by the shim)
and use it in _ensure_hashfile_in_cwd(), the Hashview download path, and
the potfile fallback path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace input() with ctx.select_file_with_autocomplete() for all file
and directory path prompts in the 7 wordlist tools submenu functions.
Non-path prompts (lengths, masks, offsets, mode selection) remain as
plain input() calls.
Update tests to set ctx.select_file_with_autocomplete.side_effect for
file path values and leave builtins.input patches only for non-path
inputs.
- Delete block that wrongly appended found hashes back into the left
(unsolved) file - found hashes belong only in the potfile
- Fix get_hcat_potfile_path() to return "" when config key is
explicitly set to "", respecting user intent to disable potfile override
- Fix get_hcat_potfile_path() to resolve relative paths relative to the
config file directory, matching main.py's hate_path resolution
- Add potfile_path parameter to download_left_hashes() and
download_hashes_from_hashview() so CLI --potfile-path and
--no-potfile-path overrides propagate to the API merge step
- Update main.py call sites to pass hcatPotfilePath through
- Add tests covering all four bug fixes
When running as root or via sudo, /root/.local/bin may not be in PATH.
Use shutil.which with fallback to ~/.local/bin/uv, and fail clearly
if uv can't be found.
uv's PEP 517 build isolation copies source to a temp dir without .git,
so setuptools-scm can't determine the version and falls back to the
existing _version.py. Forcing --reinstall-package hate_crack makes uv
rebuild from source in the actual repo dir (editable mode) where git
is accessible, so the correct version is generated.
Refactors the upgrade logic into _run_upgrade() so it can be called
both from the startup version check prompt and directly via --update,
bypassing the version comparison entirely.
When a newer release is found, prompt the user to upgrade in-place.
Resolves the git repo root via `git rev-parse --show-toplevel` before
running `git pull && make clean && make && make install`, so the upgrade
works correctly whether hate_crack is run from source or installed into
site-packages. Ctrl-C and EOF at the prompt continue normally.
Resolves merge conflict with origin/main (keys 19-22 from main kept,
wordlist tools submenu added at key 80).
Fixes test isolation bug in test_random_rules_attack.py where
load_cli_module() nuked hate_crack.attacks from sys.modules, breaking
__globals__ references in wordlist_tools_submenu for downstream tests.
Also corrects wrong menu key assertion (21 not 20) for generate_rules_crack.
Resolves merge conflict with origin/main (keys 19-21 used by ngram,
permute, random-rules). Combipow takes key 22.
- gzip support: decompress .gz wordlists to a temp file before passing
path to combipow.bin (which requires a filename argument)
- UI line-counting uses gzip.open for .gz files
- Update tests to reference key 22 instead of 21
- Add _is_gzipped() magic-byte detector and _wordlist_path() context
manager that transparently decompresses gzip files to a temp path
- Apply gzip handling to hcatCombinator3 and hcatCombinatorX via
contextlib.ExitStack so compressed wordlists work without manual prep
- Add hcatNgramX() wrapper using ngramX.bin <corpus> <group_size> piped
to hashcat, with gzip auto-detection on the corpus file
- Add ngram_attack() handler in attacks.py with tab-autocomplete corpus
selection and configurable group size (default 3)
- Register attack as menu option 19 in both main.py and hate_crack.py
- Fix wordlist_optimizer.py: .app extension on macOS was wrong, use .bin
- Add tests/test_ngram_gzip.py covering ngram_attack handler, _is_gzipped,
and _wordlist_path context manager (temp file cleanup, plain passthrough)
- Merge combinator, combinator3, and combinatorX into one unified
combinator_crack function that routes by wordlist count:
2 (no sep) -> hcatCombination, 3 (no sep) -> hcatCombinator3,
4+ or any separator -> hcatCombinatorX
- Replace comma-separated wordlist input with one-at-a-time
tab-autocomplete prompts (blank line to finish)
- Add _prompt_wordlist_paths helper using existing readline infrastructure
- Add hcatCombinator3Wordlist and hcatCombinatorXWordlist config vars
with rockyou.txt defaults
- Print full hashcat command to stdout in --debug mode by calling
_debug_cmd at the end of _append_potfile_arg (covers all 27 invocations)
- Collapse combinator submenu from 6 options to 4; keep combinator3_crack,
combinatorX_crack, and combinator_3plus_crack as delegation shims
- Update tests to cover unified routing and new prompt interface
Replace direct module attribute mutation with patch.object for cleaner
test isolation. Use hate_crack.main.subprocess.Popen for targeted mocking
instead of the global subprocess.Popen.
Hashcat renames ~/.hashcat/sessions/hashcat.induct after each session.
When the directory is absent the loopback test fails with "No such file
or directory". Recreate the directory in the test setup so it always
exists when the loopback command runs.