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.
The submodules-pre hashcat check now reads hcatPath and hcatBin from
config.json (mirroring main.py resolution logic) before falling back to
PATH lookup and the vendored hate_crack/hashcat/hashcat binary.
- Apply os.path.expanduser() to hcatPath so tilde (~) paths work,
consistent with every other config path (hcatWordlists, rulesDirectory,
hcatPotfilePath, hcatDebugLogPath)
- Improve error message when binary not found to show what hcatPath was
checked, making it easier to diagnose misconfiguration
- Update optimizedKernelAttacks in config.json.example to include all 21
attacks from DEFAULT_OPTIMIZED_ATTACKS (13 were added in v2.4.0 but
config.json.example was not updated, causing the runtime override to
exclude them)
- 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.
make clean deinits submodules and wipes the uv cache, causing
setuptools-scm to compute a wrong version during the subsequent
uv sync. The clean step is unnecessary for an upgrade - just
git pull && make install is sufficient.
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 _open_wordlist() helper that returns gzip.open() or open() based on
the .gz extension. Apply it to the three functions that open wordlist
files themselves before piping to an external binary:
- hcatPrince: prince base list fed to pp64.bin
- hcatPermute: wordlist fed to permute.bin
- hcatMarkovTrain: source file fed to hcstat2gen.bin
Attacks that pass wordlist paths directly to hashcat are unaffected -
hashcat already handles .gz natively.
Previously ngramX.bin accumulated all words from the whole file into a
flat array, producing candidates that crossed line boundaries. With a
corpus of "roses are red\nviolets are blue", it generated "red violets"
and "are red violets" - mixing lines that represent separate phrases.
Fixes ngramX.c to process each line independently so n-grams stay
within a single line, matching the expected behavior for song lyrics,
book passages, and similar per-line corpora.
- 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.
Add menu option 21 (Combipow Passphrase Attack) using combipow.bin from
hashcat-utils. Generates all unique non-empty subset combinations from a
short wordlist and pipes them into hashcat stdin.
- hcatCombipow(hash_type, hash_file, wordlist, use_space_sep) in main.py
- combipow_crack handler in attacks.py validates file, enforces 63-line
limit, prompts for space separator, then delegates to hcatCombipow
- Wired into get_main_menu_items() and get_main_menu_options() in both
main.py and hate_crack.py
- 12 tests covering menu presence, handler delegation, line-count
enforcement, file validation, and subprocess flag construction
Extends the combinator submenu (option 6) with two new attacks using
hashcat-utils binaries that were already compiled but unused.
- hcatCombinator3: 3-way wordlist combination via combinator3.bin piped
to hashcat stdin
- hcatCombinatorX: 2-8 wordlist combination via combinatorX.bin with
optional --sepFill separator, piped to hashcat stdin
- combinator3_crack handler: prompts for 3 comma-separated wordlist paths
- combinatorX_crack handler: prompts for 2-8 paths plus optional separator
- combinator_submenu updated with options 5 and 6
Closes#84, closes#85