mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
Restore hate_crack package and add E2E install tests
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
.git
|
||||
.gitmodules
|
||||
.gitignore
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
.venv
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
__pycache__
|
||||
*.pyc
|
||||
build
|
||||
wordlists
|
||||
hashcat.pot
|
||||
left_*.txt
|
||||
left_*.txt.out
|
||||
*.out
|
||||
*.pot
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN python -m pip install -q uv
|
||||
|
||||
COPY . /workspace
|
||||
|
||||
RUN uv tool install .
|
||||
|
||||
ENV PATH="/root/.local/bin:${PATH}"
|
||||
ENV HATE_CRACK_SKIP_INIT=1
|
||||
|
||||
CMD ["bash", "-lc", "/root/.local/bin/hate_crack --help >/tmp/hc_help.txt && ./hate_crack.py --help >/tmp/hc_script_help.txt"]
|
||||
@@ -32,7 +32,7 @@ Core logic is now split into modules under `hate_crack/`:
|
||||
- `hate_crack/api.py`: Hashview, Weakpass, and Hashmob integrations (downloads/menus/helpers).
|
||||
- `hate_crack/attacks.py`: menu attack handlers.
|
||||
- `hate_crack/hashmob_wordlist.py`: Hashmob wordlist utilities (thin wrapper; calls into api.py).
|
||||
- `hate_crack/hate_crack.py`: module shim that loads the top-level script.
|
||||
- `hate_crack/main.py`: main CLI implementation.
|
||||
|
||||
The top-level `hate_crack.py` remains the main entry point and orchestrates these modules.
|
||||
|
||||
@@ -47,12 +47,41 @@ This project depends on and is inspired by a number of external projects and ser
|
||||
|
||||
-------------------------------------------------------------------
|
||||
## Usage
|
||||
`$ uv run hate_crack`
|
||||
You can run hate_crack as a tool, as a script, or via `uv run`:
|
||||
|
||||
```
|
||||
usage: uv run hate_crack.py
|
||||
uv run hate_crack.py
|
||||
or
|
||||
usage: uv run hate_crack.py <hash_file> <hash_type> [options]
|
||||
uv run hate_crack.py <hash_file> <hash_type> [options]
|
||||
```
|
||||
|
||||
### Run as a tool (recommended)
|
||||
Install once from the repo root:
|
||||
|
||||
```
|
||||
uv tool install .
|
||||
hate_crack
|
||||
```
|
||||
|
||||
If you run the tool outside the repo, set `HATE_CRACK_HOME` so assets like
|
||||
`hashcat-utils` can be found:
|
||||
|
||||
```
|
||||
HATE_CRACK_HOME=/path/to/hate_crack hate_crack
|
||||
```
|
||||
|
||||
### Run as a script
|
||||
The script uses a `uv` shebang. Make it executable and run:
|
||||
|
||||
```
|
||||
chmod +x hate_crack.py
|
||||
./hate_crack.py
|
||||
```
|
||||
|
||||
You can also use Python directly:
|
||||
|
||||
```
|
||||
python hate_crack.py
|
||||
```
|
||||
|
||||
Common options:
|
||||
@@ -106,6 +135,29 @@ uv run pytest -v
|
||||
uv run pytest tests/test_hashview.py -v
|
||||
```
|
||||
|
||||
### Live Hashview Tests
|
||||
|
||||
The live Hashview upload test is skipped by default. To run it, set the
|
||||
environment variable and provide valid credentials in `config.json`:
|
||||
|
||||
```bash
|
||||
HATE_CRACK_RUN_LIVE_TESTS=1 uv run pytest tests/test_upload_cracked_hashes.py -v
|
||||
```
|
||||
|
||||
### End-to-End Install Tests (Local + Docker)
|
||||
|
||||
Local uv tool install + script execution (uses a temporary HOME):
|
||||
|
||||
```bash
|
||||
HATE_CRACK_RUN_E2E=1 uv run pytest tests/test_e2e_local_install.py -v
|
||||
```
|
||||
|
||||
Docker-based end-to-end install/run (cached via `Dockerfile.test`):
|
||||
|
||||
```bash
|
||||
HATE_CRACK_RUN_DOCKER_TESTS=1 uv run pytest tests/test_docker_script_install.py -v
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
- **tests/test_hashview.py**: Comprehensive test suite for HashviewAPI class with mocked API responses, including:
|
||||
|
||||
+59
-2187
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
from .hate_crack import cli_main
|
||||
from .main import main as _main
|
||||
|
||||
|
||||
def main() -> int:
|
||||
return cli_main() or 0
|
||||
return _main() or 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+65
-5
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
@@ -386,8 +387,19 @@ def weakpass_wordlist_menu(rank=-1):
|
||||
continue
|
||||
return sorted(i for i in indices if 1 <= i <= max_index)
|
||||
|
||||
def _safe_input(prompt):
|
||||
try:
|
||||
if not sys.stdin or not sys.stdin.isatty():
|
||||
return "q"
|
||||
except Exception:
|
||||
return "q"
|
||||
try:
|
||||
return input(prompt)
|
||||
except EOFError:
|
||||
return "q"
|
||||
|
||||
try:
|
||||
sel = input("\nEnter the number(s) to download (e.g. 1,3,5-7) or 'q' to cancel: ")
|
||||
sel = _safe_input("\nEnter the number(s) to download (e.g. 1,3,5-7) or 'q' to cancel: ")
|
||||
if sel.lower() == 'q':
|
||||
print("Returning to menu...")
|
||||
return
|
||||
@@ -762,13 +774,36 @@ def download_hashes_from_hashview(
|
||||
print_fn: Callable[..., None] = print,
|
||||
) -> Tuple[str, str]:
|
||||
"""Interactive Hashview download flow used by CLI."""
|
||||
try:
|
||||
if not sys.stdin or not sys.stdin.isatty():
|
||||
print_fn("\nAvailable Customers:")
|
||||
raise ValueError("non-interactive")
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception:
|
||||
# If stdin status can't be determined, continue normally.
|
||||
pass
|
||||
api_harness = HashviewAPI(hashview_url, hashview_api_key, debug=debug_mode)
|
||||
customers = api_harness.list_customers_with_hashfiles()
|
||||
if customers:
|
||||
api_harness.display_customers_multicolumn(customers)
|
||||
else:
|
||||
print_fn("\nNo customers found with hashfiles.")
|
||||
customer_id = int(input_fn("\nEnter customer ID: "))
|
||||
def _safe_input(prompt):
|
||||
try:
|
||||
if not sys.stdin or not sys.stdin.isatty():
|
||||
return "q"
|
||||
except Exception:
|
||||
return "q"
|
||||
try:
|
||||
return input_fn(prompt)
|
||||
except EOFError:
|
||||
return "q"
|
||||
|
||||
customer_raw = _safe_input("\nEnter customer ID: ").strip()
|
||||
if customer_raw.lower() == "q":
|
||||
raise ValueError("cancelled")
|
||||
customer_id = int(customer_raw)
|
||||
try:
|
||||
customer_hashfiles = api_harness.get_customer_hashfiles(customer_id)
|
||||
if customer_hashfiles:
|
||||
@@ -790,7 +825,10 @@ def download_hashes_from_hashview(
|
||||
except Exception as exc:
|
||||
print_fn(f"\nWarning: Could not list hashfiles: {exc}")
|
||||
print_fn("You may need to manually find the hashfile ID in the web interface.")
|
||||
hashfile_id = int(input_fn("\nEnter hashfile ID: "))
|
||||
hashfile_raw = _safe_input("\nEnter hashfile ID: ").strip()
|
||||
if hashfile_raw.lower() == "q":
|
||||
raise ValueError("cancelled")
|
||||
hashfile_id = int(hashfile_raw)
|
||||
hcat_hash_type = "1000"
|
||||
output_file = f"left_{customer_id}_{hashfile_id}.txt"
|
||||
download_result = api_harness.download_left_hashes(customer_id, hashfile_id, output_file)
|
||||
@@ -1025,7 +1063,18 @@ def list_and_download_official_wordlists():
|
||||
file_name = entry.get('file_name', name)
|
||||
print(f"{idx+1}. {name} ({file_name})")
|
||||
print("a. Download ALL files")
|
||||
sel = input("Enter the number(s) to download (e.g. 1,3,5-7), or 'a' for all, or 'q' to quit: ")
|
||||
def _safe_input(prompt):
|
||||
try:
|
||||
if not sys.stdin or not sys.stdin.isatty():
|
||||
return "q"
|
||||
except Exception:
|
||||
return "q"
|
||||
try:
|
||||
return input(prompt)
|
||||
except EOFError:
|
||||
return "q"
|
||||
|
||||
sel = _safe_input("Enter the number(s) to download (e.g. 1,3,5-7), or 'a' for all, or 'q' to quit: ")
|
||||
if sel.lower() == 'q':
|
||||
return
|
||||
if sel.lower() == 'a':
|
||||
@@ -1087,7 +1136,18 @@ def list_and_download_hashmob_rules():
|
||||
if not rules:
|
||||
return
|
||||
print("a. Download ALL files")
|
||||
sel = input("Enter the number(s) to download (e.g. 1,3,5-7), or 'a' for all, or 'q' to quit: ")
|
||||
def _safe_input(prompt):
|
||||
try:
|
||||
if not sys.stdin or not sys.stdin.isatty():
|
||||
return "q"
|
||||
except Exception:
|
||||
return "q"
|
||||
try:
|
||||
return input(prompt)
|
||||
except EOFError:
|
||||
return "q"
|
||||
|
||||
sel = _safe_input("Enter the number(s) to download (e.g. 1,3,5-7), or 'a' for all, or 'q' to quit: ")
|
||||
if sel.lower() == 'q':
|
||||
return
|
||||
rules_dir = get_rules_dir()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hcatPath": "",
|
||||
"hcatBin": "hashcat",
|
||||
"hcatTuning": "--force --remove",
|
||||
"hcatWordlists": "/Passwords/wordlists",
|
||||
"hcatOptimizedWordlists": "/Passwords/optimized_wordlists",
|
||||
"rules_directory": "/path/to/hashcat/rules",
|
||||
"hcatDictionaryWordlist": ["rockyou.txt"],
|
||||
"hcatCombinationWordlist": ["rockyou.txt","rockyou.txt"],
|
||||
"hcatHybridlist": ["rockyou.txt"],
|
||||
"hcatMiddleCombinatorMasks": ["2","4"," ","-","_","+",",",".","&"],
|
||||
"hcatMiddleBaseList": "rockyou.txt",
|
||||
"hcatThoroughCombinatorMasks": ["0","1","2","3","4","5","6","7","8","9"," ","-","_","+",",","!","#","$","\"","%","&","'","(",")","*",".","/",":",";","<","=",">","?","@","[","\\","]","^","`","{","|","}","~"],
|
||||
"hcatThoroughBaseList": "rockyou.txt",
|
||||
"hcatGoodMeasureBaseList": "rockyou.txt",
|
||||
"hcatPrinceBaseList": "rockyou.txt",
|
||||
"pipalPath": "/path/to/pipal",
|
||||
"pipal_count" : 10,
|
||||
"bandrelmaxruntime": 300,
|
||||
"bandrel_common_basedwords": "welcome,password,p@ssword,p@$$word,changeme,letmein,summer,winter,spring,springtime,fall,autumn,monday,tuesday,wednesday,thursday,friday,saturday,sunday,january,february,march,april,may,june,july,august,september,october,november,december,christmas,easter,covid19",
|
||||
"hashview_url": "http://localhost:8443",
|
||||
"hashview_api_key": "",
|
||||
"hashmob_api_key": ""
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _resolve_root():
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "hate_crack.py"))
|
||||
|
||||
|
||||
def _load_root_module():
|
||||
try:
|
||||
from . import _root # type: ignore
|
||||
return _root
|
||||
except Exception:
|
||||
pass
|
||||
root_path = _resolve_root()
|
||||
if not os.path.isfile(root_path):
|
||||
raise FileNotFoundError(f"Root hate_crack.py not found at {root_path}")
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("hate_crack_root", root_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
_ROOT = _load_root_module()
|
||||
for _name, _value in _ROOT.__dict__.items():
|
||||
if _name.startswith("__") and _name not in {"__all__", "__doc__", "__name__", "__package__", "__loader__", "__spec__"}:
|
||||
continue
|
||||
globals().setdefault(_name, _value)
|
||||
|
||||
|
||||
def cli_main():
|
||||
if hasattr(_ROOT, "cli_main"):
|
||||
return _ROOT.cli_main()
|
||||
if hasattr(_ROOT, "main"):
|
||||
return _ROOT.main()
|
||||
raise AttributeError("Root hate_crack.py has no cli_main or main")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(cli_main())
|
||||
@@ -66,14 +66,40 @@ if _root_dir not in sys.path:
|
||||
sys.path.insert(0, _root_dir)
|
||||
|
||||
# Allow submodule imports (hate_crack.*) even when this file is imported as a module.
|
||||
_pkg_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "hate_crack")
|
||||
_pkg_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
if os.path.isdir(_pkg_dir):
|
||||
__path__ = [_pkg_dir]
|
||||
if "__spec__" in globals() and __spec__ is not None:
|
||||
__spec__.submodule_search_locations = __path__
|
||||
|
||||
|
||||
hate_path = os.path.dirname(os.path.realpath(__file__))
|
||||
def _has_hate_crack_assets(path):
|
||||
if not path:
|
||||
return False
|
||||
return (
|
||||
os.path.isfile(os.path.join(path, "config.json.example"))
|
||||
and os.path.isdir(os.path.join(path, "hashcat-utils"))
|
||||
)
|
||||
|
||||
|
||||
def _resolve_hate_path(package_path):
|
||||
env_override = os.environ.get("HATE_CRACK_HOME") or os.environ.get("HATE_CRACK_ASSETS")
|
||||
candidates = []
|
||||
if env_override:
|
||||
candidates.append(env_override)
|
||||
|
||||
cwd = os.getcwd()
|
||||
candidates.extend([cwd, os.path.abspath(os.path.join(cwd, os.pardir))])
|
||||
candidates.append(package_path)
|
||||
|
||||
for candidate in candidates:
|
||||
if _has_hate_crack_assets(candidate):
|
||||
return candidate
|
||||
|
||||
return package_path
|
||||
|
||||
|
||||
hate_path = _resolve_hate_path(os.path.dirname(os.path.realpath(__file__)))
|
||||
if not os.path.isfile(hate_path + '/config.json'):
|
||||
print('Initializing config.json from config.json.example')
|
||||
shutil.copy(hate_path + '/config.json.example', hate_path + '/config.json')
|
||||
+4
-3
@@ -3,16 +3,14 @@ requires = ["setuptools>=69"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hate-crack"
|
||||
name = "hate_crack"
|
||||
version = "2.0"
|
||||
description = "Menu driven Python wrapper for hashcat"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"pytest>=8.3.4",
|
||||
"requests>=2.31.0",
|
||||
"beautifulsoup4>=4.12.0",
|
||||
"ruff>=0.9.4",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -20,3 +18,6 @@ hate_crack = "hate_crack.__main__:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["hate_crack*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
hate_crack = ["config.json.example"]
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
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():
|
||||
if shutil.which("docker") is None:
|
||||
pytest.skip("docker not available")
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
image_tag = "hate-crack-e2e"
|
||||
|
||||
try:
|
||||
build = subprocess.run(
|
||||
["docker", "build", "-f", "Dockerfile.test", "-t", image_tag, str(repo_root)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
pytest.fail(f"Docker build timed out after {exc.timeout}s")
|
||||
|
||||
assert build.returncode == 0, (
|
||||
"Docker build failed. "
|
||||
f"stdout={build.stdout} stderr={build.stderr}"
|
||||
)
|
||||
|
||||
try:
|
||||
run = subprocess.run(
|
||||
["docker", "run", "--rm", image_tag],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
pytest.fail(f"Docker run timed out after {exc.timeout}s")
|
||||
|
||||
assert run.returncode == 0, (
|
||||
"Docker script install/run failed. "
|
||||
f"stdout={run.stdout} stderr={run.stderr}"
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("HATE_CRACK_RUN_E2E") != "1",
|
||||
reason="Set HATE_CRACK_RUN_E2E=1 to run local end-to-end install tests.",
|
||||
)
|
||||
def test_local_uv_tool_install_and_help(tmp_path):
|
||||
if shutil.which("uv") is None:
|
||||
pytest.skip("uv not available")
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
home_dir = tmp_path / "home"
|
||||
home_dir.mkdir()
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"HOME": str(home_dir),
|
||||
"PATH": f"{home_dir / '.local' / 'bin'}:{env.get('PATH', '')}",
|
||||
"HATE_CRACK_SKIP_INIT": "1",
|
||||
"HATE_CRACK_HOME": str(repo_root),
|
||||
"XDG_CACHE_HOME": str(tmp_path / "cache"),
|
||||
"XDG_CONFIG_HOME": str(tmp_path / "config"),
|
||||
"XDG_DATA_HOME": str(tmp_path / "data"),
|
||||
}
|
||||
)
|
||||
|
||||
install = subprocess.run(
|
||||
["uv", "tool", "install", "."],
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert install.returncode == 0, (
|
||||
"uv tool install failed. "
|
||||
f"stdout={install.stdout} stderr={install.stderr}"
|
||||
)
|
||||
|
||||
tool_help = subprocess.run(
|
||||
["hate_crack", "--help"],
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert tool_help.returncode == 0, (
|
||||
"hate_crack --help failed. "
|
||||
f"stdout={tool_help.stdout} stderr={tool_help.stderr}"
|
||||
)
|
||||
|
||||
script_help = subprocess.run(
|
||||
["./hate_crack.py", "--help"],
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert script_help.returncode == 0, (
|
||||
"./hate_crack.py --help failed. "
|
||||
f"stdout={script_help.stdout} stderr={script_help.stderr}"
|
||||
)
|
||||
|
||||
output = tool_help.stdout + tool_help.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"
|
||||
@@ -13,9 +13,11 @@ def get_hashview_config():
|
||||
return hashview_url, hashview_api_key
|
||||
|
||||
|
||||
RUN_LIVE = os.environ.get("HATE_CRACK_RUN_LIVE_TESTS") == "1"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not get_hashview_config()[0] or not get_hashview_config()[1],
|
||||
reason="Requires hashview_url and hashview_api_key in config.json."
|
||||
not RUN_LIVE or not get_hashview_config()[0] or not get_hashview_config()[1],
|
||||
reason="Requires HATE_CRACK_RUN_LIVE_TESTS=1 and hashview_url/hashview_api_key in config.json."
|
||||
)
|
||||
def test_upload_cracked_hashes_from_file():
|
||||
hashview_url, hashview_api_key = get_hashview_config()
|
||||
|
||||
Reference in New Issue
Block a user