From 6b6c2f8b4b8c8fc4da47b29d7c34555584b066b2 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Fri, 20 Feb 2026 18:27:09 -0500 Subject: [PATCH] feat: add Lima VM E2E tests and fix princeprocessor build Add Ubuntu 24.04 Lima VM test track that runs hate_crack installation end-to-end in a real VM, giving higher confidence than Docker-based tests. - Add lima/hate-crack-test.yaml: Ubuntu 24.04 VM config with hashcat and build deps pre-installed via apt; uv installed via official installer - Add tests/test_lima_vm_install.py: mirrors Docker E2E test structure; uses rsync with targeted excludes (wordlists, compiled host binaries) and builds wheel directly to avoid setuptools-scm sdist file filtering - Fix Makefile: add princeprocessor build step with aarch64-compatible CFLAGS (drops -m64); copy binary to submodule root for vendor-assets - Add Lima tests to prek.toml pre-push hook - Document Lima VM tests in TESTING.md Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 6 ++ TESTING.md | 26 ++++++ lima/hate-crack-test.yaml | 41 +++++++++ prek.toml | 1 + tests/test_lima_vm_install.py | 156 ++++++++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+) create mode 100644 lima/hate-crack-test.yaml create mode 100644 tests/test_lima_vm_install.py diff --git a/Makefile b/Makefile index 3fa3ea2..6bea46d 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,12 @@ submodules: echo "hashcat already installed in PATH, skipping submodule compilation"; \ continue; \ fi; \ + if [ "$$path" = "princeprocessor" ]; then \ + $(MAKE) -C "$$path/src" CFLAGS_LINUX64="-W -Wall -std=c99 -O2 -s -DLINUX"; \ + if [ -f "$$path/src/pp64.bin" ]; then cp "$$path/src/pp64.bin" "$$path/"; \ + elif [ -f "$$path/src/ppAppleArm64.bin" ]; then cp "$$path/src/ppAppleArm64.bin" "$$path/pp64.bin"; fi; \ + continue; \ + fi; \ if [ -f "$$path/Makefile" ] || [ -f "$$path/makefile" ]; then \ $(MAKE) -C "$$path"; \ fi; \ diff --git a/TESTING.md b/TESTING.md index 5d4d85c..f6caea0 100644 --- a/TESTING.md +++ b/TESTING.md @@ -90,6 +90,7 @@ By default, external service checks are skipped. Enable them explicitly: - `HATE_CRACK_RUN_LIVE_HASHVIEW_TESTS=1` — run live Hashview wordlist upload tests - `HATE_CRACK_RUN_E2E=1` — run end-to-end local installation tests - `HATE_CRACK_RUN_DOCKER_TESTS=1` — run Docker-based end-to-end tests +- `HATE_CRACK_RUN_LIMA_TESTS=1` — run Lima VM-based end-to-end tests (requires Lima installed) When `HASHMOB_TEST_REAL` is enabled, tests will still skip if Hashmob returns errors like HTTP 523 (origin unreachable). @@ -110,6 +111,7 @@ Highlights: 7. UI menu options (all attack modes) 8. Hashcat-utils submodule verification 9. Docker and E2E installation tests (opt-in) +10. Lima VM installation tests (opt-in) ## Benefits @@ -151,8 +153,32 @@ HATE_CRACK_RUN_E2E=1 uv run pytest tests/test_e2e_local_install.py -v # Run Docker tests HATE_CRACK_RUN_DOCKER_TESTS=1 uv run pytest tests/test_docker_script_install.py -v + +# Run Lima VM tests +# Prerequisite: brew install lima +HATE_CRACK_RUN_LIMA_TESTS=1 uv run pytest tests/test_lima_vm_install.py -v ``` +## Lima VM Tests + +`tests/test_lima_vm_install.py` runs hate_crack inside a real Ubuntu 24.04 VM via [Lima](https://lima-vm.io/). Unlike the Docker tests, this exercises a real kernel and full Ubuntu userspace, giving higher confidence that installation works on the distros users actually run. + +**Prerequisites:** + +```bash +brew install lima +``` + +**Run:** + +```bash +HATE_CRACK_RUN_LIMA_TESTS=1 uv run pytest tests/test_lima_vm_install.py -v +``` + +**Note:** The first run takes several minutes - the VM provision script runs `apt-get install` for hashcat and all build dependencies. Subsequent runs on the same machine are faster if Lima caches the base image. + +The VM is created with a unique name per test session and deleted automatically in teardown. To verify cleanup: `limactl list`. + ## Note on Real API Testing While these mocked tests validate the code logic, you may still want to occasionally run integration tests against a real Hashview instance to ensure the API hasn't changed. The test files can be easily modified to toggle between mocked and real API calls if needed. diff --git a/lima/hate-crack-test.yaml b/lima/hate-crack-test.yaml new file mode 100644 index 0000000..3edd13c --- /dev/null +++ b/lima/hate-crack-test.yaml @@ -0,0 +1,41 @@ +# Lima VM configuration for hate_crack E2E testing +# Ubuntu 24.04 LTS with hashcat and build dependencies pre-installed. +# Usage: limactl start --name hate-crack-e2e lima/hate-crack-test.yaml + +images: + - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" + arch: "x86_64" + - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" + arch: "aarch64" + +cpus: 2 +memory: "4GiB" +disk: "20GiB" + +# No host mounts - full isolation mirrors a real user install +mounts: [] + +provision: + - mode: system + script: | + #!/bin/bash + set -euo pipefail + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ + git \ + gzip \ + hashcat \ + ocl-icd-libopencl1 \ + pocl-opencl-icd \ + p7zip-full \ + transmission-cli + + - mode: user + script: | + #!/bin/bash + set -euo pipefail + curl -LsSf https://astral.sh/uv/install.sh | sh diff --git a/prek.toml b/prek.toml index ef73ef5..7968744 100644 --- a/prek.toml +++ b/prek.toml @@ -3,6 +3,7 @@ commands = [ "uv run ruff check hate_crack", "uv run ty check hate_crack", "HATE_CRACK_SKIP_INIT=1 uv run pytest -q", + "HATE_CRACK_RUN_LIMA_TESTS=1 uv run pytest tests/test_lima_vm_install.py -v", ] [hooks.post-commit] diff --git a/tests/test_lima_vm_install.py b/tests/test_lima_vm_install.py new file mode 100644 index 0000000..50496d8 --- /dev/null +++ b/tests/test_lima_vm_install.py @@ -0,0 +1,156 @@ +import os +import shutil +import subprocess +import sys +import uuid +from pathlib import Path + +import pytest + + +def _require_lima(): + if os.environ.get("HATE_CRACK_RUN_LIMA_TESTS") != "1": + pytest.skip("Set HATE_CRACK_RUN_LIMA_TESTS=1 to run Lima VM tests.") + if shutil.which("limactl") is None: + pytest.skip("limactl not available") + + +@pytest.fixture(scope="session") +def lima_vm(): + _require_lima() + repo_root = Path(__file__).resolve().parents[1] + vm_name = f"hate-crack-e2e-{uuid.uuid4().hex[:8]}" + yaml_path = str(repo_root / "lima" / "hate-crack-test.yaml") + + try: + start = subprocess.run( + ["limactl", "start", "--name", vm_name, yaml_path], + capture_output=True, + text=True, + timeout=300, + ) + except subprocess.TimeoutExpired as exc: + pytest.fail(f"limactl start timed out after {exc.timeout}s") + + assert start.returncode == 0, ( + f"limactl start failed. stdout={start.stdout} stderr={start.stderr}" + ) + + ssh_config = Path.home() / ".lima" / vm_name / "ssh.config" + # Use rsync directly to exclude large runtime-only directories that aren't + # needed for installation (wordlists, crack results, the hashcat binary - + # the VM has hashcat installed via apt). + rsync_cmd = [ + "rsync", "-a", "--delete", + "--exclude=wordlists/", + "--exclude=hashcat/", + "--exclude=results/", + "--exclude=*.pot", + "--exclude=*.ntds", + "--exclude=*.ntds.*", + # Exclude host-compiled binaries so the VM always builds from source. + # Keep the bin/ dir itself (empty is fine); make clean recreates it anyway. + "--exclude=princeprocessor/*.bin", + "--exclude=princeprocessor/src/*.bin", + "--exclude=hashcat-utils/bin/*.bin", + "--exclude=hashcat-utils/bin/*.exe", + "--exclude=hashcat-utils/bin/*.app", + "-e", f"ssh -F {ssh_config}", + f"{repo_root}/", + f"lima-{vm_name}:/tmp/hate_crack/", + ] + try: + copy = subprocess.run( + rsync_cmd, + capture_output=True, + text=True, + timeout=120, + ) + except subprocess.TimeoutExpired as exc: + pytest.fail(f"rsync copy timed out after {exc.timeout}s") + + assert copy.returncode == 0, ( + f"rsync copy failed. stdout={copy.stdout} stderr={copy.stderr}" + ) + + install_cmd = ( + "cd /tmp/hate_crack && " + "make submodules vendor-assets && " + # Build the wheel directly (skips sdist) so freshly-compiled binaries + # in hate_crack/hashcat-utils/bin/ are included via package-data. + "rm -rf dist && " + "$HOME/.local/bin/uv build --wheel && " + "$HOME/.local/bin/uv tool install dist/hate_crack-*.whl && " + "make clean-vendor" + ) + try: + install = subprocess.run( + ["limactl", "shell", vm_name, "--", "bash", "-lc", install_cmd], + capture_output=True, + text=True, + timeout=600, + ) + except subprocess.TimeoutExpired as exc: + pytest.fail(f"Installation timed out after {exc.timeout}s") + + assert install.returncode == 0, ( + f"Installation failed. stdout={install.stdout} stderr={install.stderr}" + ) + + yield vm_name + + try: + result = subprocess.run( + ["limactl", "delete", "--force", vm_name], + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode != 0: + print( + f"Warning: Failed to delete Lima VM {vm_name}. stderr={result.stderr}", + file=sys.stderr, + ) + except Exception as e: + print( + f"Warning: Exception while deleting Lima VM {vm_name}: {e}", + file=sys.stderr, + ) + + +def _run_vm(vm_name, command, timeout=180): + try: + run = subprocess.run( + ["limactl", "shell", vm_name, "--", "bash", "-lc", command], + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired as exc: + pytest.fail(f"Lima VM command timed out after {exc.timeout}s") + return run + + +def test_lima_vm_install_and_run(lima_vm): + run = _run_vm( + lima_vm, + "cd /tmp/hate_crack && $HOME/.local/bin/hate_crack --help && ./hate_crack.py --help", + timeout=120, + ) + assert run.returncode == 0, ( + f"Lima VM install/run failed. stdout={run.stdout} stderr={run.stderr}" + ) + + +def test_lima_hashcat_cracks_simple_password(lima_vm): + 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_vm(lima_vm, command, timeout=180) + assert run.returncode == 0, ( + f"Lima VM hashcat crack failed. stdout={run.stdout} stderr={run.stderr}" + )