diff --git a/hate_crack/api.py b/hate_crack/api.py index 47813c8..79e1b9a 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -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/`` (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}:") diff --git a/hate_crack/main.py b/hate_crack/main.py index 93dde4a..1e4e940 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -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 diff --git a/tests/test_hashview.py b/tests/test_hashview.py index b8d6905..a92aafc 100644 --- a/tests/test_hashview.py +++ b/tests/test_hashview.py @@ -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/ 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).""" diff --git a/tests/test_utils.py b/tests/test_utils.py index fd688cf..5d01b81 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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"}, ],