Merge branch 'fix/upload-counts' (2.11.2)

This commit is contained in:
Justin Bollinger
2026-07-23 14:26:31 -04:00
4 changed files with 75 additions and 2 deletions
+12
View File
@@ -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
+5
View File
@@ -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]}")
+24 -2
View File
@@ -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
+34
View File
@@ -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: