From a15860a63e367f271d054125c36446e4e7b5c38e Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Thu, 23 Jul 2026 14:26:26 -0400 Subject: [PATCH] feat(hashview): report cracked-hash upload counts (2.11.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 12 ++++++++++++ hate_crack/api.py | 5 +++++ hate_crack/main.py | 26 ++++++++++++++++++++++++-- tests/test_hashview.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e5617f..40d5b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Dates are omitted for releases predating this file; see the git tags for exact timing. +## [2.11.2] - 2026-07-23 + +### Added + +- **Hashview cracked-hash upload now reports how many hashes landed.** After an upload the + CLI prints the number of pairs the client sent and how many it skipped by validation — + available regardless of the server version — and, against a Hashview that reports import + counts, also shows how many were newly cracked, verified, and left unmatched (already + cracked or not present). `upload_cracked_hashes` surfaces `uploaded`/`skipped` in its + return value alongside any server-provided counts. Previously the upload only printed + `✓ Success: OK` with no indication of how many hashes were accepted. + ## [2.11.1] - 2026-07-23 ### Fixed diff --git a/hate_crack/api.py b/hate_crack/api.py index b9e5bcc..767657a 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -1770,6 +1770,11 @@ class HashviewAPI: raise Exception( f"Hashview API Error: {json_response.get('msg', 'Unknown error')}" ) + # Surface what the client actually sent so the caller can report a + # count even against a Hashview that returns a bare {"msg": "OK"}. + if isinstance(json_response, dict): + json_response.setdefault("uploaded", len(valid_lines)) + json_response.setdefault("skipped", len(skipped)) return json_response except (json.JSONDecodeError, ValueError): raise Exception(f"Invalid API response: {resp.text[:200]}") diff --git a/hate_crack/main.py b/hate_crack/main.py index a523ca7..53a6dbf 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -3445,8 +3445,30 @@ def hashview_api(): print( f"\n✓ Success: {result.get('msg', 'Cracked hashes uploaded')}" ) - if "count" in result: - print(f" Imported: {result['count']} hashes") + if not isinstance(result, dict): + result = {} + # What this client sent (available regardless of server version). + if "uploaded" in result: + line = f" Uploaded: {result['uploaded']} pair(s)" + if result.get("skipped"): + line += f" ({result['skipped']} skipped by validation)" + print(line) + # What the server actually did (newer Hashview reports these). + if "verified" in result or "updated" in result or "count" in result: + updated = result.get("updated", result.get("count")) + print(f" Newly cracked in Hashview: {updated}") + if "verified" in result: + print(f" Verified: {result['verified']}") + if result.get("unmatched"): + print( + " Unmatched (already cracked or not in Hashview): " + f"{result['unmatched']}" + ) + else: + print( + " (This Hashview did not report an import count; " + "upgrade Hashview to see how many landed.)" + ) except Exception as e: print(f"\n✗ Error: {str(e)}") import traceback diff --git a/tests/test_hashview.py b/tests/test_hashview.py index f266a1f..e92254e 100644 --- a/tests/test_hashview.py +++ b/tests/test_hashview.py @@ -379,6 +379,40 @@ class TestHashviewAPI: out = capsys.readouterr().out assert "Skipped 1 line" in out + def test_upload_surfaces_client_counts(self, api, tmp_path): + """upload_cracked_hashes reports uploaded/skipped even when the server + returns a bare OK with no counts of its own.""" + cracked_file = tmp_path / "cracked.txt" + cracked_file.write_text( + "5f4dcc3b5aa765d61d8327deb882cf99:password\n" # MD5 -> skipped + "8846f7eaee8fb117ad06bdd830b7586c:password\n" # NTLM -> uploaded + ) + mock_response = Mock() + mock_response.json.return_value = {"status": 200, "type": "message", "msg": "OK"} + mock_response.raise_for_status = Mock() + api.session.post.return_value = mock_response + + result = api.upload_cracked_hashes(str(cracked_file), hash_type="1000") + assert result["uploaded"] == 1 + assert result["skipped"] == 1 + + def test_upload_preserves_server_counts(self, api, tmp_path): + """A Hashview that reports counts keeps them; client counts are added + without clobbering the server's.""" + cracked_file = tmp_path / "cracked.txt" + cracked_file.write_text("8846f7eaee8fb117ad06bdd830b7586c:password\n") + mock_response = Mock() + mock_response.json.return_value = { + "msg": "OK", "count": 1, "verified": 1, "updated": 1, "unmatched": 0, + } + mock_response.raise_for_status = Mock() + api.session.post.return_value = mock_response + + result = api.upload_cracked_hashes(str(cracked_file), hash_type="1000") + assert result["updated"] == 1 + assert result["verified"] == 1 + assert result["uploaded"] == 1 # client count added alongside + def _sent_body(self, api): sent = api.session.post.call_args.kwargs.get("data") if sent is None: