Three unrelated test issues were masking the fingerprint regression
above and need to be green for the suite to be trustworthy:
1. tests/test_cli_flags.py: 7 tests monkeypatched input() to return "5",
but in the no-hashfile main menu "5" enters the Wordlist Tools
submenu and then loops forever on "Split by Length" / "File not
found". Changed to "7" (Exit), the documented exit option.
2. tests/test_fingerprint_expander_and_hybrid.py: the iter-based
lineCount mock (iter([1,1,1])) raised StopIteration because
_run_hcat_cmd now also calls lineCount once per invocation when
notifications fire. Replaced with a constant `lambda _p: 1` so the
test no longer couples to internal call counts.
3. tests/test_submodule_hashcat_utils.py: `git submodule update --init`
exits 0 in git worktrees but does not populate submodule
directories. The test failed environmentally for anyone running it
from a worktree. After the init attempt, if the dir is still empty,
pytest.skip with a clear message rather than fail - preserves the
original intent for normal checkouts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When no hashes have been cracked yet, the fingerprint attack's expander
pipeline produces an empty {hash}.expanded file. Previously the function
would still launch a hashcat combinator session against two empty
wordlists (and, when invoked via the menu, six more hashcat sessions
from the secondary hybrid pass) - wasting cycles and creating confusing
empty intermediate files.
Add an early-exit guard after the expander pipeline: if .expanded is
empty, print an informative skip message and break out of the loop
before invoking hashcat or hcatHybrid.
Mirrors the existing "if hcatNewPasswords > 0" guard pattern in
hcatRecycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wordlist_optimize worker called wordlist_subtract_single (rli2.bin), which
requires both files to be lexicographically sorted. splitlen.bin preserves
insertion order, so rli2.bin silently missed duplicates. Switch to
wordlist_subtract (rli.bin), which loads the remove-list into a hash set and
is order-independent. Also add "(comma-separated)" to the input paths prompt.
Update worker tests to patch wordlist_subtract instead of wordlist_subtract_single.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove `allow_multiple=True` from wordlist_optimize handler to avoid
AttributeError when the return value is a list rather than a str
- Remove redundant `import tempfile` inside wordlist_optimize worker body
(tempfile is already imported at module level on line 29)
- Add TestWordlistOptimizeWorker with 6 tests covering: fast-path
(empty outdir), merge-path (subtract+append), new-length copy,
splitlen failure, subtract failure, and missing-input skip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds wordlist_optimize worker to main.py that consolidates multiple
wordlists into per-length deduplicated files using splitlen.bin and
rli2.bin. Wires handler in attacks.py and registers it as option 8 in
the Wordlist Tools submenu. Adds 16 tests covering happy path, empty
input, missing files, empty outdir, failure path, and submenu dispatch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch default from simple-term-menu (arrow-key) to plain numbered
input so all keys including 10+ can be selected by typing a number;
arrow-key mode is now opt-in via HATE_CRACK_ARROW_MENU=1
- Right-align key numbers within brackets so single- and multi-digit
items line up: [ 1] through [99]
- Use [key] format consistently in both plain and arrow-key modes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Menu options jumped from 9 to 13 because items 10-12 were removed
historically but remaining items kept their old numbers. Renumbers
the contiguous attack block to 10-21, closing the gap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add transmission-cli package (provides transmission-remote binary)
- Fix SETUPTOOLS_SCM_PRETEND_VERSION placement before RUN make install
- Fix PATH to use /root/.local/bin and include venv bin
- Switch TransmissionSession.add() from watch-dir polling to transmission-remote -a
- Remove --no-auth flag (unrecognized in transmission-remote 4.1.0)
- Add test_docker_torrent_downloads_wordlists E2E test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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.