Commit Graph
211 Commits
Author SHA1 Message Date
Justin BollingerandClaude Opus 4.8 9d089d9a21 fix: pipal base-word parsing and shell-safe invocation
pipal() built one rigid regex requiring exactly pipal_count consecutive
base-word lines, so any cracked set with fewer unique base words than
pipal_count (default 10) matched nothing and returned []. Parse the
"Top N base words" section line by line instead, collecting up to
pipal_count words and stopping at the blank line that ends the section.

Also replace the shell=True string-formatted Popen with list-form
arguments so file paths containing shell metacharacters can't be
interpreted as commands.

Adds tests/test_pipal_e2e.py: hermetic end-to-end tests that drive the
real pipal() through a fake pipal executable (parsing, $HEX decode,
fewer-basewords regression, and injection safety).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 16:28:52 -04:00
Justin BollingerandClaude Opus 4.8 a7233c2f4f fix: tab completion on custom file-path prompts
The "p. Enter a custom path" branches of the OMEN and Markov training
pickers, the combipow wordlist prompt, and the rule cleanup/optimize
output-path prompts used a bare input() with no readline completer, so
TAB did nothing. Route them through select_file_with_autocomplete.

Also drop the path completer after each selection so later numeric-menu
and y/n prompts don't inherit stale file-path tab completion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 16:16:06 -04:00
Justin BollingerandClaude dde6f92d96 test: stub hashcat potfile-recovery call in non-interactive integration tests
The pre-dispatch 'check POT file' step in main() shells out to the real
hashcat binary, which is absent in CI, causing the four test_main_* tests
to fail with FileNotFoundError. Stub _run_hashcat_show in the shared
_run_main helper — these tests verify dispatch routing and prompt
suppression, not potfile recovery.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 20:35:11 -04:00
Justin Bollinger d6af30c3db Merge remote-tracking branch 'origin/main' into feature/cli-noninteractive
# Conflicts:
#	CHANGELOG.md
2026-07-24 20:29:25 -04:00
Justin BollingerandClaude Sonnet 4.6 1a8bacfbc7 fix: resolve --rules dest collision, non-blocking dedup prompt, subcommand detection (#17)
- Change quick subparser --rules to dest=rule_files to prevent collision with the top-level --rules=store_true Hashmob download flag
- Update run_noninteractive to read args.rule_files instead of args.rules
- Move ATTACK_COMMANDS above run_noninteractive (Fix 5)
- Fix has_attack_subcommand to scan all argv elements (supports leading global flags like --debug)
- Replace raw input() dedup prompt with _auto_input() so non-interactive runs don't block
- Update existing quick dispatch tests to use rule_files= namespace key
- Add test_main_quick_with_rules_dispatches: proves --rules routes to crack, not download
- Add test_main_debug_flag_before_subcommand: proves leading global flags don't break routing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 20:15:47 -04:00
Justin BollingerandClaude Sonnet 4.6 e11a15d4c2 feat: wire non-interactive attack subcommands into the CLI (#17)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 20:07:48 -04:00
Justin BollingerandClaude Sonnet 4.6 1f0dcb4589 test: cover unknown-command and multi-rule dispatch paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 20:03:50 -04:00
Justin BollingerandClaude Sonnet 4.6 27791457fb feat: add non-interactive attack dispatcher
Appends `run_noninteractive(ctx, args)` to noninteractive.py, which
dispatches quick/dict/brute/topmask commands to the appropriate hcat*
function. Returns 0 on success, 1 on bad inputs, 2 on unknown command.
Includes 6 new unit tests (12 total in file), all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 20:01:06 -04:00
Justin BollingerandClaude Sonnet 4.6 db374edda2 refactor: harden rule-chain builder (types, empty-token guard, stronger tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:59:22 -04:00
Justin BollingerandClaude Sonnet 4.6 d2e45afb4a feat: add rule-chain builder for non-interactive mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:55:43 -04:00
Justin BollingerandClaude 2376ac5c10 fix(llm): only prepend http:// to OLLAMA_HOST when no scheme is present
ollamaUrl was built as "http://" + OLLAMA_HOST, so any value carrying a scheme
- the form Ollama's own tooling accepts - was mangled:

    OLLAMA_HOST=https://ollama.example.com
    -> http://https://ollama.example.com

The LLM attack then could not connect, and reaching a remote Ollama over TLS
was impossible. llm.py derives its client base URL as f"{ollamaUrl}/v1", so the
malformation propagated into the OpenAI-compatible client.

Extract _normalize_ollama_url() so the logic is unit-testable - ollamaUrl is
assigned at module import, which is otherwise only reachable via reload - and
prepend the scheme only when absent. Also strip trailing slashes, since callers
append paths. The bare host:port default is unchanged.

Fixes #119

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 19:19:38 -04:00
Justin BollingerandClaude 6448f550ee feat(llm): pre-fill LLM target industry/location from local model research
Target-info mode (menu 12) asked for company, industry, and location as three
blank prompts. Once the company name is known the local Ollama model can often
supply the other two, so ask it and offer the answers as editable defaults.

- llm.research_target(): TargetResearchInput/TargetResearchOutput schemas plus a
  research SystemPromptGenerator that tells the model to return empty strings
  when it does not genuinely recognize the organization, so an unknown small
  client yields blank prompts instead of a confident hallucination.
  APITimeoutError is translated to LLMTimeoutError like generate_candidates.
- clean_research_field(): strips, collapses whitespace, caps at 80 chars, and
  turns anything non-string into "" so model output cannot be pasted unbounded
  into a prompt default.
- main.hcatOllamaResearchTarget(): runs the call inside the existing spinner and
  degrades to blank suggestions on timeout or any other failure, so research can
  never block the attack.
- attacks.ollama_attack(): shows suggestions as "Industry (freight rail): "
  defaults, labelled explicitly as the model's GUESSES rather than OSINT.
- New ollamaAutoResearch config toggle (default true) in both config examples.

Research uses only the configured local Ollama server; the client name is never
sent to a search engine or third-party company-data API.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 18:42:50 -04:00
Justin BollingerandClaude 07ca6ba7e3 fix(ui): route ollama/omen submenus through interactive_menu; re-prompt pickers on invalid input
- ollama_attack: convert generation-mode menu to interactive_menu with
  dynamic items list (option 3 only present when cracked file exists);
  cancel via 99 or Escape; re-prompts in a while loop
- omen_attack: convert existing-model menu to interactive_menu; cancel
  via 99 or Escape (replaces hard-coded "3. Cancel"); re-prompts in loop
- _omen_pick_training_wordlist: replace abort-on-invalid with re-prompt
  loop; add explicit 'q' cancel key so users can exit without being
  trapped; callers already handle None correctly
- _markov_pick_training_source: same re-prompt-loop + 'q' cancel fix;
  callers already handle None correctly
- Update all affected tests to patch interactive_menu at
  hate_crack.attacks.interactive_menu (module-level import path) and
  drive follow-up prompts via builtins.input
- Add new tests: arrow-menu reach, Escape/99 cancel, re-prompt loop
  coverage for both pickers, and conditional option-3 key presence

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 18:31:04 -04:00
Justin BollingerandClaude 39e0dd956a feat(llm): add cracked-password generation mode to the LLM attack
Adds a third LLM Attack generation mode ("cracked") that samples the
plaintexts already recovered this session from <hashfile>.out and asks the
model to infer the target organization's password conventions and emit new
candidates in the same style.

- llm.py: new _CRACKED_PROMPT (offensive candidate generation, explicitly
  not the denylist-oriented _WORDLIST_PROMPT), a _PROMPTS mode->prompt map,
  and a "cracked" branch in _build_request that tells the model not to
  repeat already-cracked passwords.
- main.py: extract the wordlist sampling logic into the shared module-level
  helper _sample_plaintext_file(path, cap, source_label) and call it from
  both the wordlist and cracked branches of hcatOllama; guard missing and
  empty .out files.
- attacks.py: offer mode 3 only when <hashfile>.out exists and is non-empty
  (matching _markov_pick_training_source), with a clear message otherwise.
- README: document the three modes and the widened ollamaMaxSampleLines
  scope.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 18:18:52 -04:00
Justin BollingerandClaude 142c1fc99f refactor: address code-quality review findings on ui-polish branch
- Remove review-artifact "Defect 2" comment in main.py:2076; replace with
  accurate description of the invalid-cap fallback.  Same label removed from
  the test section header in test_ollama_wordlist_sampling.py.
- Lift nested _usable helper to module-level _usable_plaintext in main.py,
  placed alongside the other private wordlist helpers after _wordlist_path.
  Add six direct unit tests covering blank, whitespace, plain, hash:pw,
  multi-colon, and empty-plaintext cases.
- Move spinner line-clear before thread.join() in progress.py so the terminal
  is cleaned up even if join() is interrupted by a DoubleInterrupt/second Ctrl-C.
  Add test_tty_clears_line_even_if_join_raises to cover this path.
- Replace time.sleep-based elapsed-counter test with a deterministic version
  that patches time.monotonic; assert on actual rendered format with r"\d+s".
  Drop the misleading 0.05s sleep in test_tty_clears_line_on_exit (shorter
  than one tick, so no frame was ever guaranteed).
- Add missing ollama* keys to hate_crack/config.json.example (package-data
  copy); root config.json.example already had them.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 18:08:11 -04:00
Justin BollingerandClaude fef606dbf4 fix(llm): fix stride sampling exactness, zero-cap crash, and test quality
- Defect 1 (main.py): Replace floating-point stride loop with exact
  floor(k * total / cap) index formula so sampling always returns
  EXACTLY cap items for any 1 <= cap <= total_usable, including the
  stride < 2 regime where the old approach returned cap-1 items.

- Defect 2 (main.py): Validate ollamaMaxSampleLines before computing
  the stride; values <= 0 are nonsensical and fall back to 500
  (same as the default) rather than raising ZeroDivisionError.

- Defect 3 (test_progress_spinner.py): Rewrite test_non_tty_no_extra_thread
  to patch threading.Thread and assert it was never called, so the test
  actually fails when the spinner starts a thread in the non-TTY path.

- Defect 4 (test_progress_spinner.py): Simplify test_non_tty_prints_message
  to use monkeypatch.setattr instead of the dead mock.patch.object +
  manual assign/restore layering; removes two # type: ignore comments.

- docs(README.md): Document ollamaMaxSampleLines alongside ollamaNumCtx /
  ollamaTimeout in the Ollama Configuration section.

- tests: Add boundary-case tests for stride < 2 (total==cap, total==cap+1,
  total=3/cap=2, cap=1) and zero-cap fallback; make
  test_ollama_max_sample_lines_default exercise behaviour rather than
  the ambient config value.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 17:57:09 -04:00
Justin BollingerandClaude e5e1107359 fix(llm): add spinner during Ollama generation and cap wordlist sample
Defect 1 — hcatOllama showed a plain print then blocked silently for
up to 300 s while waiting for the LLM.  A new generic context manager
`hate_crack/progress.py::spinner()` now runs a daemon thread that
repaints a single line with a frame + elapsed-seconds counter every
~120 ms.  TTY guard ensures non-TTY/test environments get a single
plain print instead of ANSI control characters.  The line is erased
with \033[2K\r on exit (normal and exceptional).

Defect 2 — the wordlist path materialised every line in memory and
built a prompt that could massively overflow ollamaNumCtx on large
wordlists.  The path now does a two-pass evenly-spaced sample: pass 1
counts usable lines, pass 2 stride-selects up to `ollamaMaxSampleLines`
(default 500, new config key) spread across the full file.  When no
capping occurs the message reads "Loaded N passwords"; when capping
occurs it reads "Sampled N of M passwords".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 17:49:24 -04:00
Justin BollingerandClaude 549c5a0a64 fix(llm): bound Ollama requests with a configurable timeout
The Atomic Agents client was built with no timeout, so an Ollama server that
accepted the TCP connection but never replied (most commonly a large model still
loading into VRAM) left the CLI blocked in agent.run() forever. The caller's
`except Exception` handler only ever fired on connection-refused.

- llm.generate_candidates() takes a `timeout` parameter (default
  DEFAULT_TIMEOUT_SECONDS = 300.0) and forwards it to the OpenAI client.
- openai.APITimeoutError is translated into a new domain-level
  llm.LLMTimeoutError so main.py need not import openai itself, keeping the
  atomic-agents/instructor dependency isolated to llm.py as documented.
- hcatOllama passes the new `ollamaTimeout` config value and prints
  timeout-specific guidance (elapsed seconds, VRAM-loading hint, the setting to
  raise) instead of the misleading "ensure Ollama is running" message.
- Documented `ollamaTimeout` in config.json.example and README.

Also closes two test gaps: the defensive `except ValueError` handler in
hcatOllama now has coverage, and the misleadingly-named `test_unknown_mode_raises`
is split into explicit unit tests of _build_request's mode validation. Ticked the
completed steps in the LLM Atomic Agents plan doc.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 17:17:58 -04:00
Justin BollingerandClaude 81577ae060 Merge branch 'main' into feature/llm-atomic-agents
Conflicts resolved:
- CHANGELOG.md: kept both [2.12.0] (branch) and [2.11.4] (main), newest first
- pyproject.toml: kept main's tighter version pins (click>=8.4.2, requests>=2.34.2,
  packaging>=26.2, pytest-cov==7.1.0) and the branch's atomic-agents>=2.0.0 dep

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 16:57:24 -04:00
Justin BollingerandClaude 665c8ab47c fix(ci): add test CI, repair auto-tag releases, harden action pins (2.11.4)
The repo had no CI: ruff/ty/pytest ran only in local prek pre-push hooks, so
a commit pushed without hooks installed reached main unvalidated -- and
auto-tag would then cut a release from it. Adds ci.yml (ruff, ty, pytest on
3.13) and gates tagging on it.

Auto-tagged versions also never produced a release. The tag is pushed with
the default GITHUB_TOKEN, and GitHub suppresses workflow triggers for
GITHUB_TOKEN-created events, so the tag-triggered release.yml never fired --
v2.11.3 was tagged with no release. auto-tag now creates the release itself,
idempotently; release.yml stays as the manual-tag path.

Also fixes version logic that matched the `!` breaking marker but only ever
bumped minor (and never saw BREAKING CHANGE: footers, since only subjects
were inspected), serializes concurrent merges that both computed the same
tag, and repins softprops/action-gh-release from an arbitrary master commit
to v2.6.2 with all workflows on one actions/checkout version.

Clears the pre-existing ty error in notify/tailer.py that would have made
the new type-check step red: _read_new_lines read self._file_pos
(int | None) while its only None-guard lived in the caller, so the position
is passed in explicitly as an int. Behavior unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 16:39:59 -04:00
Justin BollingerandClaude Opus 4.8 bcea02b2e7 test(llm): cover wordlist-mode abort when no wordlist picked
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:10:39 -04:00
Justin BollingerandClaude Sonnet 4.6 a4da571ab4 feat(llm): add wordlist (denylist) mode to LLM attack menu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 12:08:20 -04:00
Justin BollingerandClaude Opus 4.8 93960e2d58 test(llm): cover per-rule hashcat loop and hash:password stripping
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:06:12 -04:00
Justin BollingerandClaude Sonnet 4.6 4df1121051 refactor(llm): delegate candidate generation to llm module; drop auto-pull
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 11:59:04 -04:00
Justin BollingerandClaude Sonnet 4.6 a57a4165f1 refactor(llm): validate AgentConfig in prod; spec-mock the client in tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 11:50:19 -04:00
Justin BollingerandClaude Sonnet 4.6 1cc71abfef feat(llm): structured candidate generation module via Atomic Agents
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 11:48:19 -04:00
Justin BollingerandClaude Opus 4.8 fe206420e9 fix(rules): pass required mode arg to cleanup-rules.bin (2.11.3)
cleanup-rules.bin requires a mode argument (1=CPU, 2=GPU) and exits with
usage text otherwise, so Rule File Tools cleanup always failed. Pass the
mode (defaulting to GPU) so cleanup actually runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 11:28:56 -04:00
Justin BollingerandClaude Opus 4.8 a15860a63e feat(hashview): report cracked-hash upload counts (2.11.2)
The upload previously printed only "✓ Success: OK", so there was no way
to tell how many hashes were accepted. upload_cracked_hashes now surfaces
client-side uploaded/skipped counts in its return value, and the CLI
prints them plus the server's verified/updated/unmatched breakdown when a
newer Hashview reports them (see hashview #355/#356).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:26:26 -04:00
Justin Bollinger a14272a058 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	CHANGELOG.md
2026-07-23 14:00:58 -04:00
Justin BollingerandClaude Opus 4.8 2ed9cc28e6 fix(hashview): decode $HEX[...] before upload for older servers
hate_crack forwarded hashcat's $HEX[...] plaintext verbatim. A Hashview
that verifies plaintext literally hashes the string "$HEX[..]" and
rejects the batch. Decode $HEX to the exact bytes the server must
re-hash and send those on the wire (body is now bytes):

- UTF-16LE modes (NTLM 1000, MSSQL 1731): latin-1 code points -> UTF-8
- raw-byte modes (0/100/300/900/1400/1700): the decoded bytes as-is
- unsafe (embedded CR/LF) or unknown modes: keep the $HEX token verbatim
  and rely on a $HEX-aware server

Also fixes the NTLM validation digest to use latin-1 -> UTF-16LE so
high-byte $HEX plaintexts verify instead of being silently unverifiable.
Verified end-to-end: the emitted wire verifies against both the old
(literal) and new ($HEX-aware) Hashview verifiers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:26:09 -04:00
Justin BollingerandClaude Opus 4.8 ecedcbf7dc fix(hashview): validate cracked pairs client-side before upload
upload_cracked_hashes sent every hash:plaintext pair to Hashview
untouched. A single line whose plaintext doesn't match the declared
hash mode (e.g. a stray MD5 hash mixed into an NTLM list) made
Hashview reject the entire batch with an opaque
"Plaintext for hash ... was found to be invalid" error.

Add two client-side guards (default on, disable via validate=False):
1. Length filter — drop hashes whose hex width is wrong for the mode.
2. Plaintext verification — for reproducible fast modes (MD5, SHA1,
   MD4, NTLM, SHA2-256/512) recompute the digest from the plaintext
   ($HEX[...] decoded) and skip pairs that don't match.

Skipped lines are reported with line number and reason instead of
failing the whole upload; raise clearly if nothing valid remains.
Ships a pure-Python MD4 since OpenSSL 3 dropped it from hashlib.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:52:55 -04:00
Justin BollingerandClaude Opus 4.8 5510755c12 feat(wordlist): shard into all N parts in one run with numbered filenames
Shard Wordlist (option 7) now prompts for an output base path and a shard
count, then writes all N interleaved parts as base.001..base.00N in a single
pass instead of one file per modulus/offset invocation. Matches the intended
distributed-cracking workflow: split once, copy one part per node.

Updates tests and README usage docs; bumps CHANGELOG to 2.10.11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:51:07 -04:00
Justin BollingerandClaude Opus 4.8 144d1a5334 feat(hashview): download rule files from /v1/rules
Add HashviewAPI.list_rules and download_rules wrapping the Hashview
rule endpoints. The server gzip-compresses plaintext rules on the
fly, so download_rules decompresses before saving, yielding a file
usable directly with hashcat -r. Wired into the interactive Hashview
menu (Download Rule) and the CLI (hashview download-rules
--rules-id/--output). Unknown rule ids surface the real HTTP 404.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:06:19 -04:00
Justin BollingerandClaude Opus 4.8 61b197f10e fix(hashview): correct hash-type parsing for MD5 and hashfile listing
get_hashfile_details used 'or' fallthrough on hash_type, so MD5
(hash_type 0, falsy) resolved to the envelope 'type' string
('message') instead of 0. Select by key presence and drop the
bogus 'type' fallback.

get_hashfile_hash_type looked for file_ids/ids/hashfile_ids keys
that the endpoint never returns; it always yielded []. Read the
actual 'hashfiles' envelope array and extract each file id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:56:27 -04:00
Justin BollingerandClaude Opus 4.8 ace699b872 fix(hashview): accept native-JSON customer arrays from /v1/customers
The server now returns `users` as a native JSON array (issue #229,
no double-decode), but list_customers unconditionally ran
json.loads() on it, raising TypeError and breaking the entire
customer->hashfile enumeration flow. Accept both the native array
and the legacy double-encoded string, mirroring list_wordlists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:51:20 -04:00
Justin BollingerandClaude Opus 4.8 6c7b31dbb7 fix(update): repair master->main tracking in self-updater
Old clones made before the default-branch rename sit on a local `master`
whose upstream still points at the deleted refs/heads/master, so both a
bare `git pull` and `_run_upgrade()`'s `git checkout main` failed on stale
clones. The updater now fetches origin first, checks out main via
`git checkout -B main origin/main`, repairs the upstream to origin/main,
and pulls explicitly with `git pull origin main` so it never consults the
dangling branch.*.merge config. Existing dirty-branch and detached-HEAD
guards are unchanged.

Adds tests covering the renamed-clone migration and the fetch-failure bail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:42:42 -04:00
Justin BollingerandClaude Opus 4.8 0c4f291174 test(hashview): skip job tests when server lacks cracked-hash history
The live upload-hashfile-job subprocess tests hard-failed when pointed at
a hash type the Hashview server has no cracked-hash history for: the
hashfile upload succeeds, but Hashview's task planner refuses to create a
job ("Not enough data to determine effective tasks for this hash type").
That is a server-side data limitation, not a client defect.

Treat that specific message as a skip (after asserting the hashfile
upload itself succeeded) across all three upload-job tests, so results
don't depend on which hash types have cracked data on the target
instance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:17:38 -04:00
Justin Bollinger 1af6984ff4 Merge branch 'feat/hashview-live-tests-local' into integrate/hashview-routes
# Conflicts:
#	hate_crack/api.py
#	tests/test_hashview.py
2026-06-25 10:55:49 -04:00
Justin BollingerandClaude Opus 4.8 048ed7cac1 feat(test): run live Hashview tests against a local docker stack
The live Hashview tests previously could only run against a remote server
configured in config.json, and the CLI ignored env-var overrides, so the
suite always hit prod. Prod runs an older Hashview whose /v1/jobs/add 500s
on a notify_email kwarg, which masked real client/server drift.

Wire the suite up to a local Hashview docker stack:

- main.py: HASHVIEW_URL / HASHVIEW_API_KEY env vars now override config.json,
  so the CLI can be pointed at a local instance without editing config.
- tests/_hashview_local.py + pytest_configure: when HASHVIEW_TEST_LOCAL=1,
  bring up + seed the Hashview compose stack (HASHVIEW_REPO), verify
  authenticated API access, export the HASHVIEW_* env, and tear down
  (HASHVIEW_KEEP=1 keeps it). Runs in configure so collection-time skipif
  on HASHVIEW_TEST_REAL sees the exported env.
- tests/hashview_local_seed.py: seed an admin api_key + non-default password
  (else Hashview's setup guard redirects every request to /setup), a
  customer, a hashfile, and cracked "effective task" data (else /v1/jobs/add
  has no tasks to schedule).
- api.py: download_left_hashes now uses GET /v1/hashfiles/<id>; the old
  /v1/hashfiles/<id>/left route 404s on current servers.
- subprocess tests: assert on the CLI's actual output ("Job ID:") rather than
  the literal "Job created", and strip ambient HASHVIEW_* creds from the
  no-key-configured check so the env override doesn't defeat it.
- README/CLAUDE: document HASHVIEW_TEST_LOCAL and the env overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:17:49 -04:00
Justin BollingerandClaude Opus 4.8 c07bab1a2b fix(hashview): degrade to direct hashfile-ID entry when listing API absent
The hashfile-listing route (/v1/hashfiles/hash_type/<type>) only exists
on Hashview builds from 2026-06-08+ (v0.8.3-dev). On `main` and older
servers it 404s, which surfaced as "Could not list hashfiles" and a dead
end. The download flow is now version-adaptive:

- get_all_customer_hashfiles stops sweeping after the first 404 (the
  listing endpoint is absent) instead of probing every common type.
- The menu treats listing as best-effort: when no listing is available
  it prints a clear note and accepts a hashfile ID typed directly (read
  from the web UI), with no list-membership check. The chosen file's
  type is resolved via GET /v1/getHashType/<id> and downloaded via
  GET /v1/hashfiles/<id> -- both present on all server versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:56:48 -04:00
Justin BollingerandClaude Opus 4.8 347e3529d5 feat(hashview): display a customer's hashfiles instead of asking for type
Downloading previously required knowing/typing the hash type because
Hashview only lists hashfiles per type. The download flow now sweeps
common hashcat modes (HashviewAPI.get_all_customer_hashfiles) and shows
all of the customer's uploaded hashfiles to pick from, deduped by id.
A manual hash-type prompt remains as a fallback only when nothing is
found among the common set (uncommon modes). The chosen file's real
hash type still drives the download.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:44:48 -04:00
Justin BollingerandClaude Opus 4.8 90653b856f fix(hashview): correct remaining job/hashfile API routes
Continues the v0.8.3-dev API alignment beyond the hashfile-listing 404:

- download_left_hashes: "left" hashes are GET /v1/hashfiles/<id>
  (returns uncracked ciphertexts), not the nonexistent /<id>/left.
- delete_job: DELETE /v1/jobs/<id>, not GET /v1/jobs/delete/<id>.
- start_job: POST /v1/jobs/start/<id>, not GET (route is POST-only).
- stop_job: Hashview has no stop route; raise NotImplementedError
  pointing at delete_job rather than calling a phantom endpoint.
- found-hash bulk export: no such route exists (only single-hash
  POST /v1/search); the best-effort merge is documented and degrades
  gracefully on the expected 404.

Adds offline tests asserting start_job POSTs, delete_job uses DELETE,
and stop_job raises. Documents the release in README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:59:40 -04:00
Justin BollingerandClaude Opus 4.8 370764497e fix(hashview): list customer hashfiles via per-type endpoint
The customer->hashfile flow called GET /v1/hashfiles to list all
hashfiles, then filtered client-side. That route does not exist in
Hashview (confirmed against v0.8.3-dev), so it 404'd as soon as a
customer ID was entered ("Could not list hashfiles").

Hashview exposes no list-all route; the only enumeration endpoint is
GET /v1/hashfiles/hash_type/<hash_type>, which already returns
customer_id and hash_type per file. Rebuild get_customer_hashfiles on
top of get_hashfiles_by_type, scoped to the session hash type
(hcatHashType), and filter by customer. Also fix get_hashfile_details
to call GET /v1/getHashType/<id> instead of the nonexistent
/v1/hashfiles/<id>/hash_type. Remove the dead list_hashfiles wrapper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:53:11 -04:00
Justin BollingerandClaude Opus 4.7 d096cd53a8 fix(pipal): preserve newline on $HEX[...] cracked rows
binascii.unhexlify().decode() returned bytes without the trailing newline
that normal rows inherit from password[-1], so HEX-encoded cracked
passwords concatenated with the next entry in the .passwords file fed to
pipal. Pipal under-counted entries and ranked corrupted mashups as base
words. Re-append \n if missing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:02:17 -04:00
Justin BollingerandClaude Opus 4.7 b5390b1b0f fix(notify): align attack name passed to _should_fire with prompt name (#110)
Pushover notifications were silently dropped for Quick Crack, Loopback,
Combinator, PRINCE-LING and N-gram because the handlers prompted under
one name (e.g. "Quick Crack") while the underlying hashcat wrapper
fired with a different label ("Quick Dictionary"). _should_fire keyed
the per-run consent on the prompt name, so the consent check at fire
time always missed.

Threaded the prompt name down to _run_hcat_cmd by:
- Adding attack_name kwargs to hcatQuickDictionary and hcatPrince, the
  two wrappers shared by multiple user-facing handlers.
- Relabelling hcatCombination/Combinator3/CombinatorX (all reach the
  user via combinator_crack which prompts "Combinator") and hcatNgramX
  (single caller: ngram_attack prompting "N-gram").
- Updating quick_crack, loopback_attack, and hcatPrinceLing to pass the
  matching attack_name override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:40:26 -04:00
Justin BollingerandClaude Opus 4.7 74d665e6ee fix(update): switch to main before pull to stop upgrade loop on dev
Symptom: user on the `dev` branch upgrading from v2.10.0 to v2.10.2 saw
the "Upgrade now? [y/N]" prompt fire on every restart. Accepting it ran
the install pipeline successfully but the next launch re-prompted the
same way.

Root cause: release tags in this repo live on main-side merge commits
(e.g. v2.10.2 is on the `Merge branch 'dev' into main` commit). That
commit is *downstream* of `dev`, so walking back from dev's HEAD,
setuptools-scm finds only v2.10.0 and reports e.g. `2.10.0.post1.dev9`.
After hate_crack/__init__.py strips the post/dev suffix, `__version__`
is `"2.10.0"` again. check_for_updates() sees `2.10.2 > 2.10.0` and
re-prompts forever. `git pull` on dev is a no-op because dev is fully
synced — the tag just isn't on the branch.

Fix: _run_upgrade() now detects the current branch via `git symbolic-ref
--short HEAD`. If it's not `main`, refuse to clobber uncommitted work
(git status --porcelain), then `git checkout main` before running the
existing pull/install pipeline. The release-tagged commit is now on
HEAD, setuptools-scm regenerates a clean version, and the loop ends.

Safety guards:
  - Uncommitted changes: bail with manual-steps message (exit 1) — no
    git checkout attempted.
  - main checked out in another worktree: surface git's stderr and bail
    with manual instructions.
  - Detached HEAD (symbolic-ref returncode != 0): skip the switch and
    let the existing flow run — no regression for users on a tag.
  - Already on main: skip the switch block; behavior unchanged.

Tests (tests/test_version_check.py):
  - test_run_upgrade_switches_from_dev_to_main_then_upgrades
  - test_run_upgrade_bails_when_non_main_branch_is_dirty
  - test_run_upgrade_bails_when_checkout_main_fails
  - test_run_upgrade_skips_switch_when_already_on_main
  - test_run_upgrade_skips_switch_on_detached_head

Four pre-existing tests updated to thread an extra branch_proc mock
through the new symbolic-ref subprocess call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:31:14 -04:00
Justin BollingerandClaude Opus 4.7 51083c876c fix: force LC_ALL=C for sort -u subprocesses to handle non-UTF-8 bytes
macOS `sort` is locale-strict: under LC_COLLATE=en_US.UTF-8 (the default
on most macOS shells) it errors out with "sort: Illegal byte sequence"
when stdin contains bytes that are not valid UTF-8. Cracked-password
streams routinely contain such bytes - hex-encoded fields, mixed
encodings, binary garbage from poorly-encoded source hashes - so this
fires in real fingerprint runs whenever the pot already has any non-
ASCII output.

Symptom in the fingerprint attack: the expander -> sort pipeline emits
"sort: Illegal byte sequence" and produces an empty .expanded file. The
empty-.expanded guard added in the previous patch then triggers the
"no candidates to expand" skip message - which is misleading, because
the user does have cracks; they just got dropped on the sort step.

Pass env={**os.environ, "LC_ALL": "C"} to all three subprocess.Popen
calls that invoke `sort -u`:
  - _write_field_sorted_unique  (main.py:1163)
  - hcatFingerprint expander    (main.py:1544)
  - hcatLMtoNT combinator dedupe (main.py:2995)

LC_ALL=C makes sort byte-collation only. Dedup correctness is
unaffected (byte equality is locale-independent), and hashcat doesn't
care about wordlist order.

Also adds an AST-level test that fails if any future `sort` Popen lacks
an env kwarg, so the locale fix can't silently regress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:04:42 -04:00
Justin BollingerandClaude Opus 4.7 5dd32fe6c1 test: repair pre-existing flakes in cli_flags, fingerprint iter, submodule
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>
2026-05-26 14:57:22 -04:00
Justin BollingerandClaude Opus 4.7 6040460919 fix(fingerprint): skip hashcat when expanded wordlist is empty
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>
2026-05-26 14:57:11 -04:00
Justin Bollinger 317222b5b7 Merge branch 'bugfix/hashview-download' into dev 2026-05-05 18:51:09 -04:00