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>
This commit is contained in:
Justin Bollinger
2026-06-18 16:53:11 -04:00
co-authored by Claude Opus 4.8
parent dcf377c3d4
commit 370764497e
4 changed files with 65 additions and 85 deletions
+41 -46
View File
@@ -1127,7 +1127,7 @@ class HashviewAPI:
def get_hashfile_details(self, hashfile_id):
"""Get hashfile details and hashtype for a given hashfile_id."""
url = f"{self.base_url}/v1/hashfiles/{hashfile_id}/hash_type"
url = f"{self.base_url}/v1/getHashType/{hashfile_id}"
resp = self.session.get(url, headers=self._auth_headers())
resp.raise_for_status()
try:
@@ -1173,61 +1173,49 @@ class HashviewAPI:
return {"customers": customers}
return data
def list_hashfiles(self):
url = f"{self.base_url}/v1/hashfiles"
resp = self.session.get(url, headers=self._auth_headers())
resp.raise_for_status()
data = resp.json()
if "hashfiles" in data:
if isinstance(data["hashfiles"], str):
hashfiles = json.loads(data["hashfiles"])
else:
hashfiles = data["hashfiles"]
return hashfiles
return []
def get_customer_hashfiles(self, customer_id, hash_type=None):
"""Return a customer's hashfiles of a given hash_type.
def get_customer_hashfiles(self, customer_id):
all_hashfiles = self.list_hashfiles()
Hashview exposes no "list all hashfiles" route; the only enumeration
endpoint is ``/v1/hashfiles/hash_type/<hash_type>`` (see
:meth:`get_hashfiles_by_type`), which already returns ``customer_id``
and ``hash_type`` per file. We query that and filter by customer.
``hash_type`` is required to enumerate: without it there is no API
route to list a customer's files, so an empty list is returned.
"""
if hash_type is None:
if self.debug:
print(
"[DEBUG] get_customer_hashfiles: no hash_type given; Hashview "
"has no list-all route, returning []"
)
return []
all_hashfiles = self.get_hashfiles_by_type(hash_type)
customer_hfs = [
hf for hf in all_hashfiles if int(hf.get("customer_id", 0)) == customer_id
hf for hf in all_hashfiles if int(hf.get("customer_id", 0)) == int(customer_id)
]
# The type-scoped endpoint already returns the hash_type, but normalize
# the key so downstream callers can read either spelling.
for hf in customer_hfs:
if not (hf.get("hashtype") or hf.get("hash_type")):
hf["hash_type"] = str(hash_type)
if self.debug:
print(
f"[DEBUG] get_customer_hashfiles({customer_id}): found {len(customer_hfs)} hashfiles"
f"[DEBUG] get_customer_hashfiles({customer_id}, hash_type={hash_type}): "
f"found {len(customer_hfs)} hashfiles"
)
# Fetch hash types for any hashfiles missing them
for hf in customer_hfs:
if not (hf.get("hashtype") or hf.get("hash_type")):
hf_id = hf.get("id")
if hf_id is not None:
if self.debug:
print(f"[DEBUG] Fetching hash_type for hashfile {hf_id}")
try:
details = self.get_hashfile_details(hf_id)
hashtype = details.get("hashtype")
if hashtype:
hf["hash_type"] = hashtype
if self.debug:
print(
f"[DEBUG] Updated hashfile {hf_id} with hash_type={hashtype}"
)
elif self.debug:
print(
f"[DEBUG] No hashtype found in details for {hf_id}: {details}"
)
except Exception as e:
if self.debug:
print(
f"[DEBUG] Exception fetching hash_type for {hf_id}: {e}"
)
return customer_hfs
def get_customer_hashfiles_with_hashtype(self, customer_id, target_hashtype="1000"):
"""Return hashfiles for a customer that match the requested hashtype."""
customer_hashfiles = self.get_customer_hashfiles(customer_id)
customer_hashfiles = self.get_customer_hashfiles(
customer_id, hash_type=target_hashtype
)
if not customer_hashfiles:
return []
target_str = str(target_hashtype)
@@ -1604,8 +1592,13 @@ def download_hashes_from_hashview(
input_fn: Callable[[str], str] = input,
print_fn: Callable[..., None] = print,
potfile_path: Optional[str] = None,
hash_type: Optional[str] = None,
) -> Tuple[str, str]:
"""Interactive Hashview download flow used by CLI."""
"""Interactive Hashview download flow used by CLI.
``hash_type`` is required to enumerate a customer's hashfiles, since
Hashview only exposes a per-hash-type listing endpoint.
"""
try:
if not sys.stdin or not sys.stdin.isatty():
print_fn("\nAvailable Customers:")
@@ -1663,7 +1656,9 @@ def download_hashes_from_hashview(
else:
customer_id = int(customer_raw)
try:
customer_hashfiles = api_harness.get_customer_hashfiles(customer_id)
customer_hashfiles = api_harness.get_customer_hashfiles(
customer_id, hash_type=hash_type
)
if customer_hashfiles:
print_fn("\n" + "=" * 120)
print_fn(f"Hashfiles for Customer ID {customer_id}:")
+6 -3
View File
@@ -3757,15 +3757,18 @@ def hashview_api():
)
continue
# List hashfiles for the customer
# List hashfiles for the customer. Hashview has no
# list-all route, so we enumerate by the session hash
# type (the type of the hashes loaded in this session).
try:
customer_hashfiles = api_harness.get_customer_hashfiles(
customer_id
customer_id, hash_type=hcatHashType
)
if not customer_hashfiles:
print(
f"\nNo hashfiles found for customer ID {customer_id}"
f"\nNo hashfiles of type {hcatHashType} found for "
f"customer ID {customer_id}"
)
continue
+17 -35
View File
@@ -68,74 +68,56 @@ class TestHashviewAPI:
# Cleanup
def test_list_hashfiles_success(self, api):
"""Test successful hashfile listing with real API if possible, else mock."""
def test_get_hashfiles_by_type_success(self, api):
"""The /v1/hashfiles/hash_type/<type> endpoint returns a list (real API if possible)."""
hashview_url, hashview_api_key = self._get_hashview_config()
if hashview_url and hashview_api_key:
real_api = HashviewAPI(hashview_url, hashview_api_key)
result = real_api.list_hashfiles()
result = real_api.get_hashfiles_by_type("1000")
assert isinstance(result, list)
# If there are no hashfiles, that's valid, but if present, check structure
if result:
assert "name" in result[0]
else:
mock_response = Mock()
mock_response.json.return_value = {
"hashfiles": json.dumps(
[
{"id": 1, "customer_id": 1, "name": "hashfile1.txt"},
{"id": 2, "customer_id": 2, "name": "hashfile2.txt"},
]
)
}
mock_response.json.return_value = [
{"id": 1, "customer_id": 1, "name": "hashfile1.txt", "hash_type": 1000},
{"id": 2, "customer_id": 2, "name": "hashfile2.txt", "hash_type": 1000},
]
mock_response.raise_for_status = Mock()
api.session.get.return_value = mock_response
result = api.list_hashfiles()
result = api.get_hashfiles_by_type("1000")
assert isinstance(result, list)
assert len(result) == 2
assert result[0]["name"] == "hashfile1.txt"
def test_list_hashfiles_empty(self, api):
"""Test hashfile listing returns empty list if no hashfiles (real API if possible)."""
hashview_url, hashview_api_key = self._get_hashview_config()
if hashview_url and hashview_api_key:
real_api = HashviewAPI(hashview_url, hashview_api_key)
result = real_api.list_hashfiles()
# If there are no hashfiles, result should be []
if not result:
assert result == []
else:
assert isinstance(result, list)
else:
mock_response = Mock()
mock_response.json.return_value = {}
mock_response.raise_for_status = Mock()
api.session.get.return_value = mock_response
result = api.list_hashfiles()
assert result == []
def test_get_customer_hashfiles_requires_hash_type(self, api):
"""Without a hash_type there is no Hashview list route, so we return []."""
result = api.get_customer_hashfiles(1)
assert result == []
def test_get_customer_hashfiles(self, api):
"""Test filtering hashfiles by customer_id (real API if possible)."""
"""Filter the type-scoped hashfile list by customer_id (real API if possible)."""
hashview_url, hashview_api_key = self._get_hashview_config()
customer_id = os.environ.get("HASHVIEW_CUSTOMER_ID")
if hashview_url and hashview_api_key and customer_id:
real_api = HashviewAPI(hashview_url, hashview_api_key)
result = real_api.get_customer_hashfiles(int(customer_id))
result = real_api.get_customer_hashfiles(int(customer_id), hash_type="1000")
assert isinstance(result, list)
# If there are hashfiles, all should match customer_id
if result:
assert all(hf["customer_id"] == int(customer_id) for hf in result)
else:
api.list_hashfiles = Mock(
api.get_hashfiles_by_type = Mock(
return_value=[
{"id": 1, "customer_id": 1, "name": "hashfile1.txt"},
{"id": 2, "customer_id": 2, "name": "hashfile2.txt"},
{"id": 3, "customer_id": 1, "name": "hashfile3.txt"},
]
)
result = api.get_customer_hashfiles(1)
result = api.get_customer_hashfiles(1, hash_type="1000")
assert len(result) == 2
assert all(hf["customer_id"] == 1 for hf in result)
api.get_hashfiles_by_type.assert_called_once_with("1000")
def test_display_customers_multicolumn_empty(self, api, capsys):
"""Test display_customers_multicolumn with no customers (mock only, as real API not needed)."""
+1 -1
View File
@@ -226,7 +226,7 @@ def test_get_customer_hashfiles_with_hashtype_filters(monkeypatch):
monkeypatch.setattr(
hv,
"get_customer_hashfiles",
lambda customer_id: [
lambda customer_id, hash_type=None: [
{"customer_id": customer_id, "hashtype": "1000"},
{"customer_id": customer_id, "hash_type": "0"},
],