diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c08a608 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +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==0.9.28 + + - name: Install project dependencies + run: | + 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" + HATE_CRACK_SKIP_INIT: "1" + run: .venv/bin/python -m pytest -v diff --git a/Dockerfile.test b/Dockerfile.test index f95ab86..3421916 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -2,11 +2,24 @@ FROM python:3.13-slim WORKDIR /workspace -RUN python -m pip install -q uv +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + 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==0.9.28 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..f44c845 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: all install clean hashcat-utils test + +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 {} + + +test: + uv run pytest -v diff --git a/README.md b/README.md index 21e3f3b..5d9f068 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,26 @@ 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. + +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" @@ -84,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. @@ -135,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 @@ -158,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: @@ -170,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). ------------------------------------------------------------------- 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) diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 36ec926..e59dc1e 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -1,25 +1,31 @@ import os import shutil import subprocess +import sys +import uuid 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") - repo_root = Path(__file__).resolve().parents[1] - image_tag = "hate-crack-e2e" +@pytest.fixture(scope="session") +def docker_image(): + _require_docker() + repo_root = Path(__file__).resolve().parents[1] + # Use a unique tag per test run to avoid conflicts + image_tag = f"hate-crack-e2e-{uuid.uuid4().hex[:8]}" + + dockerfile_path = str(repo_root / "Dockerfile.test") try: build = subprocess.run( - ["docker", "build", "-f", "Dockerfile.test", "-t", image_tag, str(repo_root)], + ["docker", "build", "-f", dockerfile_path, "-t", image_tag, str(repo_root)], capture_output=True, text=True, timeout=600, @@ -31,18 +37,63 @@ def test_docker_script_install_and_run(): "Docker build failed. " f"stdout={build.stdout} stderr={build.stderr}" ) - + + yield image_tag + + # Cleanup: remove the Docker image after tests complete try: - run = subprocess.run( - ["docker", "run", "--rm", image_tag], + result = subprocess.run( + ["docker", "image", "rm", image_tag], capture_output=True, text=True, - timeout=120, + 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: Exception while removing Docker image {image_tag}: {e}", file=sys.stderr) + + +def _run_container(image_tag, command, timeout=180): + try: + run = subprocess.run( + ["docker", "run", "--rm", image_tag, "bash", "-lc", command], + capture_output=True, + text=True, + 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; " + "printf 'password\\nletmein\\n123456\\n' > /tmp/wordlist.txt; " + "echo 5f4dcc3b5aa765d61d8327deb882cf99 > /tmp/hash.txt; " + "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) + assert run.returncode == 0, ( + "Docker hashcat crack failed. " + f"stdout={run.stdout} stderr={run.stderr}" + )