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>
Rule numbers entered as N-M now expand to all integers in that range
before lookup, matching user expectation that 138-141 selects rules
138 through 141 sequentially. Also catches ValueError alongside
IndexError when a token cannot be converted to int.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move simple-term-menu from the [tui] optional extra into the main
dependencies list so arrow-key menu navigation works out of the box.
Users can still opt into the plain numbered menu with
HATE_CRACK_PLAIN_MENU=1.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Transmission's watch dir scanner runs every ~10s, so a 10s deadline
could expire before the first scan completes. 30s gives 2-3 scan
cycles of headroom.
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>
After make install, run uv sync --reinstall-package hate_crack so
setuptools-scm regenerates the version from the new git tag. Without
this, the installed version stays at the old value and the update
checker loops indefinitely.
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>
Prevent implicit None return when no data row matches the expected
transmission-remote --info-files format. Also clarify README macOS
install note.
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>
Pass fetch_torrent_metadata (HTTP-only) to download_all_weakpass_torrents
so the batch collects all .torrent files first, then runs exactly one
TransmissionSession rather than N single-torrent sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split download_torrent_file into fetch_torrent_metadata (HTTP-only) and
a backwards-compat shim. Update weakpass_wordlist_menu,
download_all_weakpass_torrents, and download_weakpass_torrent to collect
.torrent files first, then run a single TransmissionSession batch.
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>
The worker() function inside fetch_all_weakpass_wordlists_multithreaded()
called q.task_done() inside the except block. If the except block itself
throws (e.g., KeyboardInterrupt during print), task_done() is skipped
and q.join() hangs forever.
Move task_done() to a finally block to ensure it always runs, allowing
the queue manager to correctly track completion even if error handling
itself fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the hardcoded `total_pages=67` default with `None` (auto-detect).
On first call the function probes page 1 to read `last_page` from the
Inertia `data-page` payload; if found it drives the thread pool with that
count, if not found it falls back to a sequential walk until an empty page
is returned, and if the probe itself fails it degrades gracefully to 67.
Callers that pass `total_pages` explicitly are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before each download in both the 'all' and indexed-selection branches,
check whether the destination file already exists and has nonzero size
(accounting for .7z archives whose extracted name drops the suffix).
Mirrors the same guard already in place for rule downloads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: _with_hashmob_backoff no longer sleeps after the last attempt —
breaks out of the loop immediately so we don't waste up to 300s before
giving up.
Fix 2: download_hashmob_wordlist and download_hashmob_rule now open a
single streaming connection inside _attempt() and pass the already-open
response to _stream_response_to_file, eliminating the probe-then-reopen
pattern that caused 429 responses on the second request to bypass the
backoff retry machinery.
Fix 3: HashviewAPI.download_wordlist wraps _stream_response_to_file in
try/finally so resp.close() is guaranteed even on KeyboardInterrupt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _stream_response_to_file and _streamed_download helpers that
implement atomic .part-file writes, unified progress bars, and
KeyboardInterrupt cleanup in one place. Add _with_hashmob_backoff
for bounded 429 retry logic (max 6 attempts, step-doubling delay)
and _Hashmob429 sentinel exception so callers signal rate-limits
without re-implementing the backoff loop.
Refactor all four callers onto the helpers:
- download_hashmob_wordlist: removes inline backoff loop and fixes
the redirect recursion bug (redirect URL was passed as file_name,
bypassing the API prefix logic)
- download_hashmob_rule: removes duplicate backoff loop; keeps the
60-entry pinned URL dict and 404→alt_url fallback verbatim
- download_official_wordlist: delegates streaming to _streamed_download;
keeps .7z extraction tail
- HashviewAPI.download_wordlist: uses session.get + _stream_response_to_file
so cookie auth is preserved; removes the separate progress-bar
implementation
All public signatures are unchanged; 40 existing tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidates Pushover notification menu options under a new submenu
at main-menu option 82, and promotes notify_per_crack_enabled from
config-file-only to a runtime toggle with a UI-level guard that
refuses to enable it while global notifications are OFF.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Isolates blame churn from the ruff format pass in commit 4fa43a7,
which reformatted pre-existing lines in hate_crack/main.py that are
outside the scope of the notifications-submenu feature.
Enable locally with:
git config blame.ignoreRevsFile .git-blame-ignore-revs
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Adds menu option 84 (Send Test Pushover Notification) so users can verify
their Pushover credentials without running an attack. Ignores the global
notify_enabled toggle by design (prints a note when OFF).
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>
Calls `detect_username_hash_format` once during preprocessing in
`main()` (after NTLM/NetNTLM handling and before the potfile check) and
stores the result in `hcatUsernamePrefix`. A new helper
`_maybe_append_username_flag` is invoked from the universal
command-finalization chokepoint `_append_potfile_arg`, so every hashcat
command path automatically receives `--username` when a `user:hash`
file is detected. The helper guards against duplicates if `--username`
is already present in `hcatTuning`.
Skips detection for modes 1000/5500/5600 because the existing NTLM
preprocessing already strips usernames into a bare `.nt` file.
Also adds `hcatUsernamePrefix` to the proxy sync tuple in
`hate_crack.py` so tests that patch the root module propagate into
`hate_crack.main`.
Refs #107
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New pure-logic module `hate_crack.username_detect` exposes
`detect_username_hash_format`, which samples the first N non-empty lines
of a hash file and checks that every line matches `user:<hex>` with the
exact hex length expected for the given hashcat mode.
Ships allowlist of MD5/SHA1/SHA256/SHA512/MD4/NTLM/LM-family modes and
blocklist for WPA, IKE-PSK, NetNTLM, and non-hex formats.
Refs #107
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Automatically creates a new git tag on push to main based on
conventional commit prefixes: feat: bumps minor (2.5.x → 2.6.0),
fix:/perf: bumps patch (2.5.1 → 2.5.2). The new tag triggers the
existing release workflow to create a GitHub release.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add local_scheme = "no-local-version" to setuptools-scm config so
versions never include the +g<hash> suffix. Simplify the regex in
__init__.py to only strip .post/.dev suffixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
Merge both rule files into a temporary combined file so hashcat only
starts once per wordlist instead of twice, saving GPU initialization
overhead on each dictionary attack iteration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Read 1 MiB binary chunks and count newline bytes instead of iterating
text lines, which is significantly faster on large output files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a shared _RateLimiter class (1 req/2s) instead of per-function
locks with a 15-second sleep. Also tunes backoff from 256s to 30s with
30s penalty increments for faster retry on rate-limited responses.
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>
cleanup() previously always printed 'Cracked passwords combined in
X.out' even when combine_ntlm_output() returned early (no cracked
hashes) or the hash type wasn't NTLM pwdump. Now checks file
existence first and falls back to pointing at the raw .out file.
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>
Re-add hcatOptimizedWordlists config key (previously removed) with a
default of ./optimized_wordlists. Falls back to hcatWordlists if the
configured directory does not exist.
Update quick_crack to list files from and default to hcatOptimizedWordlists
instead of hcatWordlists when prompting for a wordlist or directory.
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.
ruff, ty, pytest, pytest-cov were in [project.optional-dependencies]
requiring --extra dev to install. Moved to [dependency-groups] which
uv sync includes by default, fixing the pre-push hook finding no ruff.
Replaces the _version.py import with importlib.metadata.version() so
the version is always read from the installed package, which setuptools-scm
writes correctly during uv sync. Removes the version_file config and
the stale-file workarounds from make install/clean.
_version.py is gitignored but persists on disk with a stale version.
Delete it before uv sync so setuptools-scm regenerates it from the
current git tag. Also remove it in make clean for consistency.
Hashcat path is configured via config.json at runtime. The build-time
check caused false failures when running as root (sudo) or before
config.json exists, and added no value since hashcat-utils builds
independently of the hashcat installation.