mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-03-12 21:23:05 -07:00
- Add rsync to _require_lima() prerequisite check; missing rsync now skips cleanly instead of failing with an opaque command-not-found - Add _truncate_output() helper and apply to all assertion messages to keep failure output readable when make/install emits thousands of lines - Increase limactl start timeout from 300s to 600s to accommodate slow Ubuntu image downloads - Add limactl stop before delete in cleanup for more reliable teardown - Add flag verification to test_lima_vm_install_and_run: checks 10 CLI flags in --help output, matching the local install test pattern - Add 3 unit tests: test_truncate_output_trims_long_text, test_truncate_output_short_text_unchanged, test_require_lima_skips_without_rsync
222 lines
7.1 KiB
Python
222 lines
7.1 KiB
Python
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _truncate_output(text: str, lines: int = 100) -> str:
|
|
"""Return the last `lines` lines of text to keep failure messages readable."""
|
|
all_lines = text.splitlines()
|
|
if len(all_lines) <= lines:
|
|
return text
|
|
kept = all_lines[-lines:]
|
|
return f"... ({len(all_lines) - lines} lines omitted) ...\n" + "\n".join(kept)
|
|
|
|
|
|
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")
|
|
if shutil.which("rsync") is None:
|
|
pytest.skip("rsync 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=600, # Ubuntu image download can take >5min on slow networks
|
|
)
|
|
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.\nstdout={_truncate_output(copy.stdout)}\nstderr={_truncate_output(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.\nstdout={_truncate_output(install.stdout)}\nstderr={_truncate_output(install.stderr)}"
|
|
)
|
|
|
|
yield vm_name
|
|
|
|
# Cleanup: stop then delete the Lima VM
|
|
try:
|
|
subprocess.run(
|
|
["limactl", "stop", "--force", vm_name],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
except Exception:
|
|
pass # Best-effort stop; proceed to delete regardless
|
|
|
|
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.\nstdout={_truncate_output(run.stdout)}\nstderr={_truncate_output(run.stderr)}"
|
|
)
|
|
output = run.stdout + run.stderr
|
|
expected_flags = [
|
|
"--download-hashview",
|
|
"--hashview",
|
|
"--download-torrent",
|
|
"--download-all-torrents",
|
|
"--weakpass",
|
|
"--rank",
|
|
"--hashmob",
|
|
"--rules",
|
|
"--cleanup",
|
|
"--debug",
|
|
]
|
|
for flag in expected_flags:
|
|
assert flag in output, f"Missing {flag} in help output"
|
|
|
|
|
|
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.\nstdout={_truncate_output(run.stdout)}\nstderr={_truncate_output(run.stderr)}"
|
|
)
|
|
|
|
|
|
# --- Unit tests (no Lima VM required) ---
|
|
|
|
|
|
def test_truncate_output_trims_long_text():
|
|
long = "\n".join(str(i) for i in range(200))
|
|
result = _truncate_output(long)
|
|
lines = result.splitlines()
|
|
assert len(lines) <= 102 # 100 lines + possible header
|
|
assert "199" in result # last lines present
|
|
assert result.startswith("... (") # header present
|
|
|
|
|
|
def test_truncate_output_short_text_unchanged():
|
|
short = "line1\nline2\nline3"
|
|
assert _truncate_output(short) == short
|
|
|
|
|
|
def test_require_lima_skips_without_rsync(monkeypatch):
|
|
monkeypatch.setenv("HATE_CRACK_RUN_LIMA_TESTS", "1")
|
|
monkeypatch.setattr(
|
|
shutil,
|
|
"which",
|
|
lambda cmd: None if cmd == "rsync" else "/usr/bin/limactl",
|
|
)
|
|
with pytest.raises(pytest.skip.Exception):
|
|
_require_lima()
|