Merge pull request #61 from trustedsec/ci-github-actions

Add CI workflow and E2E install tests
This commit is contained in:
Justin Bollinger
2026-01-31 23:47:32 -05:00
committed by GitHub
6 changed files with 184 additions and 15 deletions
+37
View File
@@ -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
+15 -2
View File
@@ -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
+17
View File
@@ -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
+51 -1
View File
@@ -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).
-------------------------------------------------------------------
+1
View File
@@ -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)
+63 -12
View File
@@ -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}"
)