From 631b528795726e46643831d599232938d9431d89 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 22:54:08 -0500 Subject: [PATCH 01/20] Add GitHub Actions test workflow --- .github/workflows/tests.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..88c9053 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: tests + +on: + push: + pull_request: + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y p7zip-full transmission-cli + + - name: Install uv + run: python -m pip install --upgrade pip uv + + - name: Run tests + env: + HATE_CRACK_RUN_E2E: "0" + HATE_CRACK_RUN_DOCKER_TESTS: "0" + HATE_CRACK_RUN_LIVE_TESTS: "0" + run: uv run pytest -v From 1f5470b2f3ff91f7a1b588aa121cfcb5e2bbd417 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:01:14 -0500 Subject: [PATCH 02/20] Expand Docker E2E tests with hashcat crack --- Dockerfile.test | 12 ++++++++ README.md | 6 ++++ tests/test_docker_script_install.py | 44 ++++++++++++++++++++++++----- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Dockerfile.test b/Dockerfile.test index f95ab86..95b9cb2 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -2,6 +2,18 @@ FROM python:3.13-slim WORKDIR /workspace +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gzip \ + hashcat \ + ocl-icd-libopencl1 \ + pocl-opencl-icd \ + p7zip-full \ + transmission-cli \ + && rm -rf /var/lib/apt/lists/* + RUN python -m pip install -q uv COPY . /workspace diff --git a/README.md b/README.md index 21e3f3b..8adb1a6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ cd hashcat/ make make install ``` + +### External Dependencies +These are required for certain download/extraction flows: + +- `7z`/`7za` (p7zip) — used to extract `.7z` archives. +- `transmission-cli` — used to download Weakpass torrents. ### Download hate_crack ```git clone --recurse-submodules https://github.com/trustedsec/hate_crack.git``` * Customize binary and wordlist paths in "config.json" diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 36ec926..6b3516c 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -6,14 +6,16 @@ from pathlib import Path import pytest -@pytest.mark.skipif( - os.environ.get("HATE_CRACK_RUN_DOCKER_TESTS") != "1", - reason="Set HATE_CRACK_RUN_DOCKER_TESTS=1 to run Docker-based tests.", -) -def test_docker_script_install_and_run(): +def _require_docker(): + if os.environ.get("HATE_CRACK_RUN_DOCKER_TESTS") != "1": + pytest.skip("Set HATE_CRACK_RUN_DOCKER_TESTS=1 to run Docker-based tests.") if shutil.which("docker") is None: pytest.skip("docker not available") + +@pytest.fixture(scope="session") +def docker_image(): + _require_docker() repo_root = Path(__file__).resolve().parents[1] image_tag = "hate-crack-e2e" @@ -31,18 +33,46 @@ def test_docker_script_install_and_run(): "Docker build failed. " f"stdout={build.stdout} stderr={build.stderr}" ) + return image_tag + +def _run_container(image_tag, command, timeout=180): try: run = subprocess.run( - ["docker", "run", "--rm", image_tag], + ["docker", "run", "--rm", image_tag, "bash", "-lc", command], capture_output=True, text=True, - timeout=120, + timeout=timeout, ) except subprocess.TimeoutExpired as exc: pytest.fail(f"Docker run timed out after {exc.timeout}s") + return run + +def test_docker_script_install_and_run(docker_image): + run = _run_container( + docker_image, + "/root/.local/bin/hate_crack --help >/tmp/hc_help.txt && ./hate_crack.py --help >/tmp/hc_script_help.txt", + timeout=120, + ) assert run.returncode == 0, ( "Docker script install/run failed. " f"stdout={run.stdout} stderr={run.stderr}" ) + + +def test_docker_hashcat_cracks_simple_password(docker_image): + command = ( + "set -euo pipefail; " + "curl -fsSL -o /tmp/rockyou.txt.gz https://weakpass.com/download/90/rockyou.txt.gz; " + "gzip -d /tmp/rockyou.txt.gz; " + "head -n 5000 /tmp/rockyou.txt > /tmp/rockyou.small.txt; " + "echo 5f4dcc3b5aa765d61d8327deb882cf99 > /tmp/hash.txt; " + "hashcat -m 0 -a 0 --potfile-disable -o /tmp/out.txt /tmp/hash.txt /tmp/rockyou.small.txt --quiet; " + "grep -q ':password' /tmp/out.txt" + ) + run = _run_container(docker_image, command, timeout=180) + assert run.returncode == 0, ( + "Docker hashcat crack failed. " + f"stdout={run.stdout} stderr={run.stderr}" + ) From afb453b01c81519a8ecf83710c7953467afe9f78 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:08:50 -0500 Subject: [PATCH 03/20] Use Makefile in Docker test image --- Dockerfile.test | 3 ++- Makefile | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/Dockerfile.test b/Dockerfile.test index 95b9cb2..5f8fd71 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -4,6 +4,7 @@ WORKDIR /workspace RUN apt-get update \ && apt-get install -y --no-install-recommends \ + build-essential \ ca-certificates \ curl \ gzip \ @@ -18,7 +19,7 @@ RUN python -m pip install -q uv COPY . /workspace -RUN uv tool install . +RUN make install ENV PATH="/root/.local/bin:${PATH}" ENV HATE_CRACK_SKIP_INIT=1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ab15e3 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +.PHONY: all install clean hashcat-utils + +all: hashcat-utils + +hashcat-utils: + $(MAKE) -C hashcat-utils + +install: hashcat-utils + uv tool install . + +clean: + -$(MAKE) -C hashcat-utils clean + rm -rf .pytest_cache .ruff_cache build dist *.egg-info + find . -name "__pycache__" -type d -prune -exec rm -rf {} + From 412d22bff080512c337fab459015dbd9ae1de46f Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:17:16 -0500 Subject: [PATCH 04/20] Add make test and update testing docs --- Makefile | 5 ++++- README.md | 46 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8ab15e3..f44c845 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all install clean hashcat-utils +.PHONY: all install clean hashcat-utils test all: hashcat-utils @@ -12,3 +12,6 @@ clean: -$(MAKE) -C hashcat-utils clean rm -rf .pytest_cache .ruff_cache build dist *.egg-info find . -name "__pycache__" -type d -prune -exec rm -rf {} + + +test: + uv run pytest -v diff --git a/README.md b/README.md index 8adb1a6..5d9f068 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,20 @@ These are required for certain download/extraction flows: - `7z`/`7za` (p7zip) — used to extract `.7z` archives. - `transmission-cli` — used to download Weakpass torrents. + +Install commands: + +Ubuntu/Kali: +``` +sudo apt-get update +sudo apt-get install -y p7zip-full transmission-cli +``` + +macOS (Homebrew): +``` +brew install p7zip transmission-cli +``` + ### Download hate_crack ```git clone --recurse-submodules https://github.com/trustedsec/hate_crack.git``` * Customize binary and wordlist paths in "config.json" @@ -90,6 +104,31 @@ You can also use Python directly: python hate_crack.py ``` +### Makefile helpers +Build hashcat-utils and install the tool: + +``` +make install +``` + +Build only hashcat-utils: + +``` +make +``` + +Clean build/test artifacts: + +``` +make clean +``` + +Run the test suite: + +``` +make test +``` + Common options: - `--download-hashview`: Download hashes from Hashview before cracking. - `--weakpass`: Download wordlists from Weakpass. @@ -141,6 +180,8 @@ uv run pytest -v uv run pytest tests/test_hashview.py -v ``` +You can also run the full suite with `make test`. + ### Live Hashview Tests The live Hashview upload test is skipped by default. To run it, set the @@ -164,6 +205,9 @@ Docker-based end-to-end install/run (cached via `Dockerfile.test`): HATE_CRACK_RUN_DOCKER_TESTS=1 uv run pytest tests/test_docker_script_install.py -v ``` +The Docker E2E test also downloads a small subset of rockyou and runs a basic +hashcat crack to validate external tool integration. + ### Test Structure - **tests/test_hashview.py**: Comprehensive test suite for HashviewAPI class with mocked API responses, including: @@ -176,7 +220,7 @@ All tests use mocked API calls, so they can run without connectivity to a Hashvi ### Continuous Integration -Tests automatically run on GitHub Actions for every push and pull request. The workflow tests against multiple Python versions (3.9, 3.10, 3.11, 3.12) to ensure compatibility. +Tests automatically run on GitHub Actions for every push and pull request (Ubuntu, Python 3.13). ------------------------------------------------------------------- From a0fec73fda72c83f8d63592fa1e927cb47215fa4 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:22:04 -0500 Subject: [PATCH 05/20] CI: install pytest --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 88c9053..2aa9f9d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y p7zip-full transmission-cli - name: Install uv - run: python -m pip install --upgrade pip uv + run: python -m pip install --upgrade pip uv pytest - name: Run tests env: From 92200a7964d014c0b9106ee88704fcbff5ea3ffb Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:24:19 -0500 Subject: [PATCH 06/20] CI: install project deps before pytest --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2aa9f9d..8f41ef2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,9 @@ jobs: - name: Install uv run: python -m pip install --upgrade pip uv pytest + - name: Install project dependencies + run: uv pip install . + - name: Run tests env: HATE_CRACK_RUN_E2E: "0" From 2bddce51a4e7397ee27e01afd8bc084786561559 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:28:31 +0000 Subject: [PATCH 10/20] Add Docker image cleanup to test fixture Co-authored-by: bandrel <3598052+bandrel@users.noreply.github.com> --- tests/test_docker_script_install.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 6b3516c..6d5040d 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -1,6 +1,7 @@ import os import shutil import subprocess +import uuid from pathlib import Path import pytest @@ -17,7 +18,8 @@ def _require_docker(): def docker_image(): _require_docker() repo_root = Path(__file__).resolve().parents[1] - image_tag = "hate-crack-e2e" + # Use a unique tag per test run to avoid conflicts + image_tag = f"hate-crack-e2e-{uuid.uuid4().hex[:8]}" try: build = subprocess.run( @@ -33,7 +35,20 @@ def docker_image(): "Docker build failed. " f"stdout={build.stdout} stderr={build.stderr}" ) - return image_tag + + yield image_tag + + # Cleanup: remove the Docker image after tests complete + try: + subprocess.run( + ["docker", "image", "rm", image_tag], + capture_output=True, + text=True, + timeout=60, + ) + except Exception: + # Don't fail the test if cleanup fails + pass def _run_container(image_tag, command, timeout=180): From afb6a4d4929f5e71da1cfd37d655a2c7d8065476 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:28:37 +0000 Subject: [PATCH 12/20] Fix Docker build to use absolute Dockerfile path Co-authored-by: bandrel <3598052+bandrel@users.noreply.github.com> --- tests/test_docker_script_install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 6b3516c..9c482ab 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -18,10 +18,11 @@ def docker_image(): _require_docker() repo_root = Path(__file__).resolve().parents[1] image_tag = "hate-crack-e2e" + dockerfile_path = repo_root / "Dockerfile.test" try: build = subprocess.run( - ["docker", "build", "-f", "Dockerfile.test", "-t", image_tag, str(repo_root)], + ["docker", "build", "-f", str(dockerfile_path), "-t", image_tag, str(repo_root)], capture_output=True, text=True, timeout=600, From 371e9dc3c3efeb3a5e02cb6717d13a634d8b4841 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:28:53 +0000 Subject: [PATCH 13/20] Replace external wordlist download with inline generation Co-authored-by: bandrel <3598052+bandrel@users.noreply.github.com> --- tests/test_docker_script_install.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 6b3516c..3475f64 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -62,16 +62,16 @@ def test_docker_script_install_and_run(docker_image): def test_docker_hashcat_cracks_simple_password(docker_image): + # Generate a minimal wordlist inline instead of downloading from external source + # Hash 5f4dcc3b5aa765d61d8327deb882cf99 is MD5("password") command = ( "set -euo pipefail; " - "curl -fsSL -o /tmp/rockyou.txt.gz https://weakpass.com/download/90/rockyou.txt.gz; " - "gzip -d /tmp/rockyou.txt.gz; " - "head -n 5000 /tmp/rockyou.txt > /tmp/rockyou.small.txt; " + "printf 'admin\\nroot\\npassword\\n123456\\ntest\\n' > /tmp/wordlist.txt; " "echo 5f4dcc3b5aa765d61d8327deb882cf99 > /tmp/hash.txt; " - "hashcat -m 0 -a 0 --potfile-disable -o /tmp/out.txt /tmp/hash.txt /tmp/rockyou.small.txt --quiet; " + "hashcat -m 0 -a 0 --potfile-disable -o /tmp/out.txt /tmp/hash.txt /tmp/wordlist.txt --quiet; " "grep -q ':password' /tmp/out.txt" ) - run = _run_container(docker_image, command, timeout=180) + run = _run_container(docker_image, command, timeout=60) assert run.returncode == 0, ( "Docker hashcat crack failed. " f"stdout={run.stdout} stderr={run.stderr}" From 0e7db78296d9ffd978eb01b70e3b8f4fc8e2a51a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:29:09 +0000 Subject: [PATCH 14/20] Improve error logging in Docker image cleanup Co-authored-by: bandrel <3598052+bandrel@users.noreply.github.com> --- tests/test_docker_script_install.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 6d5040d..0d8132b 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -1,6 +1,7 @@ import os import shutil import subprocess +import sys import uuid from pathlib import Path @@ -46,9 +47,9 @@ def docker_image(): text=True, timeout=60, ) - except Exception: - # Don't fail the test if cleanup fails - pass + except Exception as e: + # Don't fail the test if cleanup fails, but log the issue + print(f"Warning: Failed to remove Docker image {image_tag}: {e}", file=sys.stderr) def _run_container(image_tag, command, timeout=180): From ac7f809e33f8d0b4290153103558f792e8d6eb16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:29:41 +0000 Subject: [PATCH 15/20] Check return code and log stderr on cleanup failure Co-authored-by: bandrel <3598052+bandrel@users.noreply.github.com> --- tests/test_docker_script_install.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 0d8132b..69d73a2 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -41,15 +41,21 @@ def docker_image(): # Cleanup: remove the Docker image after tests complete try: - subprocess.run( + result = subprocess.run( ["docker", "image", "rm", image_tag], capture_output=True, text=True, timeout=60, ) + if result.returncode != 0: + print( + f"Warning: Failed to remove Docker image {image_tag}. " + f"stderr={result.stderr}", + file=sys.stderr + ) except Exception as e: # Don't fail the test if cleanup fails, but log the issue - print(f"Warning: Failed to remove Docker image {image_tag}: {e}", file=sys.stderr) + print(f"Warning: Exception while removing Docker image {image_tag}: {e}", file=sys.stderr) def _run_container(image_tag, command, timeout=180): From 0ae172acf46997fe18d5be39dc11ac10f870cf45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:30:05 +0000 Subject: [PATCH 16/20] Pin uv to version 0.9.28 to mitigate supply-chain risk Co-authored-by: bandrel <3598052+bandrel@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- Dockerfile.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f41ef2..4d0ef10 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y p7zip-full transmission-cli - name: Install uv - run: python -m pip install --upgrade pip uv pytest + run: python -m pip install --upgrade pip uv==0.9.28 pytest - name: Install project dependencies run: uv pip install . diff --git a/Dockerfile.test b/Dockerfile.test index 5f8fd71..3421916 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -15,7 +15,7 @@ RUN apt-get update \ transmission-cli \ && rm -rf /var/lib/apt/lists/* -RUN python -m pip install -q uv +RUN python -m pip install -q uv==0.9.28 COPY . /workspace From 96896dea8257ea512866259f4cf56260be6b5352 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:31:30 -0500 Subject: [PATCH 17/20] CI: create uv venv before installing deps --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f41ef2..4d3dc39 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,9 @@ jobs: run: python -m pip install --upgrade pip uv pytest - name: Install project dependencies - run: uv pip install . + run: | + uv venv + uv pip install . - name: Run tests env: From 9768ee14ec1fac56a6611e26d3852e495d96d4c6 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:43:35 -0500 Subject: [PATCH 18/20] CI: use venv python for pytest --- .github/workflows/tests.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 078c77f..a457d12 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,16 +20,17 @@ jobs: run: sudo apt-get update && sudo apt-get install -y p7zip-full transmission-cli - name: Install uv - run: python -m pip install --upgrade pip uv==0.9.28 pytest + run: python -m pip install --upgrade pip uv==0.9.28 - name: Install project dependencies run: | - uv venv - uv pip install . + uv venv .venv + uv pip install --python .venv/bin/python pytest + uv pip install --python .venv/bin/python . - name: Run tests env: HATE_CRACK_RUN_E2E: "0" HATE_CRACK_RUN_DOCKER_TESTS: "0" HATE_CRACK_RUN_LIVE_TESTS: "0" - run: uv run pytest -v + run: .venv/bin/python -m pytest -v From d2da6663e01c1c159571e616014694cd11258488 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:44:49 -0500 Subject: [PATCH 19/20] CI: set HATE_CRACK_SKIP_INIT for tests --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a457d12..c08a608 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,4 +33,5 @@ jobs: HATE_CRACK_RUN_E2E: "0" HATE_CRACK_RUN_DOCKER_TESTS: "0" HATE_CRACK_RUN_LIVE_TESTS: "0" + HATE_CRACK_SKIP_INIT: "1" run: .venv/bin/python -m pytest -v From 1dcc44691d27c33dc74c8b3b93cb3cb57f2bd72e Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Sat, 31 Jan 2026 23:46:28 -0500 Subject: [PATCH 20/20] Print hashview menu header even when API key missing --- hate_crack/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hate_crack/main.py b/hate_crack/main.py index f7753c4..5ba68dc 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -2063,6 +2063,7 @@ def main(): if args.hashview: if not hashview_api_key: + print("Available Customers:") print("\nError: Hashview API key not configured.") print("Please set 'hashview_api_key' in config.json") sys.exit(1)