mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
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:
co-authored by
Claude Opus 4.8
parent
dcf377c3d4
commit
370764497e
+41
-46
@@ -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
@@ -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
@@ -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
@@ -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"},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user