mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
Merge pull request #137 from trustedsec/chore/ci-mirror-hashview
ci: mirror hashview lint & security gates
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -33,9 +33,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: uv sync --dev
|
||||
|
||||
- name: Ruff
|
||||
- name: Ruff (lint)
|
||||
run: uv run ruff check hate_crack
|
||||
|
||||
- name: Ruff (format)
|
||||
run: uv run ruff format --check hate_crack
|
||||
|
||||
- name: Ty
|
||||
run: uv run ty check hate_crack
|
||||
|
||||
@@ -43,3 +46,48 @@ jobs:
|
||||
env:
|
||||
HATE_CRACK_SKIP_INIT: "1"
|
||||
run: uv run pytest -q
|
||||
|
||||
# Bandit SAST against the committed baseline (.bandit-baseline.json): only
|
||||
# findings NOT already in the baseline fail the build. hate_crack shells out
|
||||
# to hashcat extensively, so the baseline captures the reviewed low-severity
|
||||
# subprocess/shlex findings. Config lives in [tool.bandit] in pyproject.toml.
|
||||
bandit:
|
||||
name: Bandit SAST
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
|
||||
- name: Bandit (vs baseline)
|
||||
run: >-
|
||||
uvx --from "bandit[toml]==1.9.4" bandit
|
||||
-r hate_crack -c pyproject.toml -b .bandit-baseline.json
|
||||
|
||||
# pip-audit: fail on dependencies with known CVEs (PyPI / OSV advisory DB).
|
||||
# Audits the synced project environment. PYSEC-2026-2447 (diskcache 5.6.3, a
|
||||
# transitive dep via instructor -> atomic-agents) is ignored because no fixed
|
||||
# release exists upstream; revisit when diskcache ships a fix.
|
||||
pip-audit:
|
||||
name: pip-audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Audit dependencies
|
||||
run: >-
|
||||
uv run --with pip-audit==2.10.0 pip-audit
|
||||
--progress-spinner off --ignore-vuln PYSEC-2026-2447
|
||||
|
||||
@@ -12,8 +12,6 @@ except _PackageNotFoundError:
|
||||
# "2.5.1.post1.dev0" → "2.5.1"
|
||||
# "2.5.1" → "2.5.1"
|
||||
__version__ = _re.sub(r"(\.post\d+|\.dev\d+)", "", _raw_version)
|
||||
__version_tuple__ = tuple(
|
||||
int(x) if x.isdigit() else x for x in __version__.split(".")
|
||||
)
|
||||
__version_tuple__ = tuple(int(x) if x.isdigit() else x for x in __version__.split("."))
|
||||
|
||||
__all__ = ["__version__", "__version_tuple__"]
|
||||
|
||||
+61
-33
@@ -129,7 +129,9 @@ def _streamed_download(
|
||||
allow_redirects=allow_redirects,
|
||||
) as r:
|
||||
r.raise_for_status()
|
||||
return _stream_response_to_file(r, dest_path, label=label, show_progress=show_progress)
|
||||
return _stream_response_to_file(
|
||||
r, dest_path, label=label, show_progress=show_progress
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -407,8 +409,10 @@ class TransmissionSession:
|
||||
percent_done = 0.0
|
||||
# Status is tokens[7] (best-effort); name is the rest.
|
||||
status = tokens[7] if len(tokens) > 8 else ""
|
||||
name = " ".join(tokens[8:]) if len(tokens) > 8 else (
|
||||
tokens[-1] if len(tokens) > 1 else ""
|
||||
name = (
|
||||
" ".join(tokens[8:])
|
||||
if len(tokens) > 8
|
||||
else (tokens[-1] if len(tokens) > 1 else "")
|
||||
)
|
||||
entries.append(
|
||||
{
|
||||
@@ -486,10 +490,7 @@ class TransmissionSession:
|
||||
if not entries:
|
||||
break
|
||||
for entry in entries:
|
||||
if (
|
||||
entry["percent_done"] >= 100.0
|
||||
and entry["id"] not in completed_ids
|
||||
):
|
||||
if entry["percent_done"] >= 100.0 and entry["id"] not in completed_ids:
|
||||
completed_ids.add(entry["id"])
|
||||
file_name = self.info_file(entry["id"])
|
||||
on_complete(entry["id"], file_name)
|
||||
@@ -627,9 +628,7 @@ def run_torrent_session(torrent_files, save_dir, *, print_fn=print) -> None:
|
||||
failed += 1
|
||||
return
|
||||
abs_path = (
|
||||
file_path
|
||||
if os.path.isabs(file_path)
|
||||
else os.path.join(save_dir, file_path)
|
||||
file_path if os.path.isabs(file_path) else os.path.join(save_dir, file_path)
|
||||
)
|
||||
if abs_path.endswith(".7z"):
|
||||
ok = extract_with_7z(abs_path, save_dir, remove_archive=True)
|
||||
@@ -653,9 +652,7 @@ def run_torrent_session(torrent_files, save_dir, *, print_fn=print) -> None:
|
||||
except KeyboardInterrupt:
|
||||
print_fn("\n[!] Torrent download interrupted.")
|
||||
raise
|
||||
print_fn(
|
||||
f"[i] Torrent session complete: {completed} succeeded, {failed} failed."
|
||||
)
|
||||
print_fn(f"[i] Torrent session complete: {completed} succeeded, {failed} failed.")
|
||||
|
||||
|
||||
def fetch_all_weakpass_wordlists_multithreaded(total_pages=None, threads=10):
|
||||
@@ -678,10 +675,9 @@ def fetch_all_weakpass_wordlists_multithreaded(total_pages=None, threads=10):
|
||||
last_page = None
|
||||
if isinstance(wordlists_raw, dict):
|
||||
# Check multiple possible locations for last_page
|
||||
last_page = (
|
||||
wordlists_raw.get("last_page")
|
||||
or wordlists_raw.get("meta", {}).get("last_page")
|
||||
)
|
||||
last_page = wordlists_raw.get("last_page") or wordlists_raw.get(
|
||||
"meta", {}
|
||||
).get("last_page")
|
||||
if "data" in wordlists_raw:
|
||||
wordlists_raw = wordlists_raw["data"]
|
||||
else:
|
||||
@@ -729,7 +725,9 @@ def fetch_all_weakpass_wordlists_multithreaded(total_pages=None, threads=10):
|
||||
seen.add(wl["name"])
|
||||
return result
|
||||
else:
|
||||
print("[!] Weakpass page 1 returned no results; falling back to 67 pages")
|
||||
print(
|
||||
"[!] Weakpass page 1 returned no results; falling back to 67 pages"
|
||||
)
|
||||
total_pages = 67
|
||||
entries1 = []
|
||||
except Exception as e:
|
||||
@@ -892,7 +890,8 @@ def fetch_torrent_metadata(torrent_url, save_dir=None, wordlist_id=None):
|
||||
r2 = requests.get(torrent_link, headers=headers, stream=True)
|
||||
content_type = r2.headers.get("Content-Type", "")
|
||||
local_filename = os.path.join(
|
||||
torrent_dir, filename if filename.endswith(".torrent") else filename + ".torrent"
|
||||
torrent_dir,
|
||||
filename if filename.endswith(".torrent") else filename + ".torrent",
|
||||
)
|
||||
if r2.status_code == 200 and not content_type.startswith("text/html"):
|
||||
with open(local_filename, "wb") as f:
|
||||
@@ -1010,7 +1009,9 @@ def weakpass_wordlist_menu(rank=-1):
|
||||
if not torrent_url:
|
||||
print(f"[!] Missing torrent URL for selection {idx}")
|
||||
continue
|
||||
meta = fetch_torrent_metadata(torrent_url, save_dir=save_dir, wordlist_id=entry.get("id"))
|
||||
meta = fetch_torrent_metadata(
|
||||
torrent_url, save_dir=save_dir, wordlist_id=entry.get("id")
|
||||
)
|
||||
if meta:
|
||||
torrent_files.append(meta)
|
||||
if torrent_files:
|
||||
@@ -1352,7 +1353,9 @@ class HashviewAPI:
|
||||
|
||||
all_hashfiles = self.get_hashfiles_by_type(hash_type)
|
||||
customer_hfs = [
|
||||
hf for hf in all_hashfiles if int(hf.get("customer_id", 0)) == int(customer_id)
|
||||
hf
|
||||
for hf in all_hashfiles
|
||||
if int(hf.get("customer_id", 0)) == int(customer_id)
|
||||
]
|
||||
|
||||
# The type-scoped endpoint already returns the hash_type, but normalize
|
||||
@@ -1570,7 +1573,12 @@ class HashviewAPI:
|
||||
return resp.json()
|
||||
|
||||
def download_left_hashes(
|
||||
self, customer_id, hashfile_id, output_file=None, hash_type=None, potfile_path=None
|
||||
self,
|
||||
customer_id,
|
||||
hashfile_id,
|
||||
output_file=None,
|
||||
hash_type=None,
|
||||
potfile_path=None,
|
||||
):
|
||||
import sys
|
||||
|
||||
@@ -1676,7 +1684,11 @@ class HashviewAPI:
|
||||
)
|
||||
|
||||
# Append found hash:clear pairs to the potfile
|
||||
resolved_potfile = potfile_path if potfile_path is not None else get_hcat_potfile_path()
|
||||
resolved_potfile = (
|
||||
potfile_path
|
||||
if potfile_path is not None
|
||||
else get_hcat_potfile_path()
|
||||
)
|
||||
if resolved_potfile:
|
||||
appended = 0
|
||||
with open(resolved_potfile, "a", encoding="utf-8") as pf:
|
||||
@@ -1810,7 +1822,9 @@ class HashviewAPI:
|
||||
r"filename=\"?([^\";]+)\"?", content_disp, re.IGNORECASE
|
||||
)
|
||||
output_file = (
|
||||
os.path.basename(match.group(1)) if match else f"wordlist_{wordlist_id}.gz"
|
||||
os.path.basename(match.group(1))
|
||||
if match
|
||||
else f"wordlist_{wordlist_id}.gz"
|
||||
)
|
||||
|
||||
if not os.path.isabs(output_file):
|
||||
@@ -2155,7 +2169,9 @@ def download_hashmob_wordlist(file_name, out_path):
|
||||
|
||||
def _attempt():
|
||||
_hashmob_limiter.wait()
|
||||
with requests.get(url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r:
|
||||
with requests.get(
|
||||
url, headers=headers, stream=True, timeout=60, allow_redirects=True
|
||||
) as r:
|
||||
if r.status_code == 429:
|
||||
raise _Hashmob429()
|
||||
r.raise_for_status()
|
||||
@@ -2171,7 +2187,9 @@ def download_hashmob_wordlist(file_name, out_path):
|
||||
real_url = match.group(1)
|
||||
print(f"Found meta refresh redirect to: {real_url}")
|
||||
return _streamed_download(real_url, out_path, label=file_name)
|
||||
print("Error: Received HTML instead of file. Possible permission or quota issue.")
|
||||
print(
|
||||
"Error: Received HTML instead of file. Possible permission or quota issue."
|
||||
)
|
||||
return False
|
||||
return _stream_response_to_file(r, out_path, label=file_name)
|
||||
|
||||
@@ -2281,19 +2299,31 @@ def download_hashmob_rule(file_name, out_path):
|
||||
print(
|
||||
f"[i] Hashmob rule not in pinned URL list, using public prefix: {file_name}"
|
||||
)
|
||||
primary_url = f"https://www.hashmob.net/api/v2/downloads/research/rules/{file_name}"
|
||||
primary_url = (
|
||||
f"https://www.hashmob.net/api/v2/downloads/research/rules/{file_name}"
|
||||
)
|
||||
alt_url = f"https://hashmob.net/api/v2/downloads/research/official/hashmob_rules/{file_name}"
|
||||
api_key = get_hashmob_api_key()
|
||||
headers = {"api-key": api_key} if api_key else {}
|
||||
|
||||
def _attempt():
|
||||
_hashmob_limiter.wait()
|
||||
with requests.get(primary_url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r:
|
||||
with requests.get(
|
||||
primary_url, headers=headers, stream=True, timeout=60, allow_redirects=True
|
||||
) as r:
|
||||
if r.status_code == 429:
|
||||
raise _Hashmob429()
|
||||
if r.status_code == 404 and alt_url:
|
||||
print(f"[i] Hashmob rule not found at primary URL, trying fallback: {alt_url}")
|
||||
with requests.get(alt_url, headers=headers, stream=True, timeout=60, allow_redirects=True) as r2:
|
||||
print(
|
||||
f"[i] Hashmob rule not found at primary URL, trying fallback: {alt_url}"
|
||||
)
|
||||
with requests.get(
|
||||
alt_url,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
timeout=60,
|
||||
allow_redirects=True,
|
||||
) as r2:
|
||||
if r2.status_code == 429:
|
||||
raise _Hashmob429()
|
||||
r2.raise_for_status()
|
||||
@@ -2554,9 +2584,7 @@ def download_official_wordlist(file_name, out_path):
|
||||
out_path = sanitize_filename(file_name)
|
||||
dest_dir = get_hcat_wordlists_dir()
|
||||
archive_path = (
|
||||
os.path.join(dest_dir, out_path)
|
||||
if not os.path.isabs(out_path)
|
||||
else out_path
|
||||
os.path.join(dest_dir, out_path) if not os.path.isabs(out_path) else out_path
|
||||
)
|
||||
os.makedirs(os.path.dirname(archive_path), exist_ok=True)
|
||||
ok = _streamed_download(url, archive_path, label=file_name)
|
||||
|
||||
+63
-30
@@ -170,9 +170,7 @@ def quick_crack(ctx: Any) -> None:
|
||||
if raw_choice == "":
|
||||
wordlist_choice = default_dir
|
||||
elif raw_choice.isdigit() and 1 <= int(raw_choice) <= len(wordlist_files):
|
||||
chosen = os.path.join(
|
||||
list_dir, wordlist_files[int(raw_choice) - 1]
|
||||
)
|
||||
chosen = os.path.join(list_dir, wordlist_files[int(raw_choice) - 1])
|
||||
if os.path.exists(chosen):
|
||||
wordlist_choice = chosen
|
||||
print(wordlist_choice)
|
||||
@@ -250,9 +248,7 @@ def extensive_crack(ctx: Any) -> None:
|
||||
ctx.hcatGoodMeasure(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
ctx.hcatRecycle(ctx.hcatHashType, ctx.hcatHashFile, ctx.hcatExtraCount)
|
||||
cracked_after = ctx.lineCount(out_path) if os.path.exists(out_path) else 0
|
||||
_notify.notify_job_done(
|
||||
"Extensive Crack", cracked_after, ctx.hcatHashFile
|
||||
)
|
||||
_notify.notify_job_done("Extensive Crack", cracked_after, ctx.hcatHashFile)
|
||||
# Note: ``cracked_before`` is tracked for potential future per-orchestrator
|
||||
# delta reporting, but today the notify message uses the absolute count
|
||||
# because that matches what single-attack notifications already report.
|
||||
@@ -308,7 +304,9 @@ def combinator_crack(ctx: Any) -> None:
|
||||
print("\n" + "=" * 60)
|
||||
print("COMBINATOR ATTACK")
|
||||
print("=" * 60)
|
||||
print("Combines 2-8 wordlists. 2 uses hashcat native mode; 3+ use external binaries.")
|
||||
print(
|
||||
"Combines 2-8 wordlists. 2 uses hashcat native mode; 3+ use external binaries."
|
||||
)
|
||||
print("=" * 60)
|
||||
|
||||
use_default = (
|
||||
@@ -318,7 +316,9 @@ def combinator_crack(ctx: Any) -> None:
|
||||
if use_default != "n":
|
||||
base = ctx.hcatCombinationWordlist
|
||||
wordlists = base if isinstance(base, list) else [base]
|
||||
wordlists = [ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists]
|
||||
wordlists = [
|
||||
ctx._resolve_wordlist_path(wl, ctx.hcatWordlists) for wl in wordlists
|
||||
]
|
||||
if len(wordlists) < 2:
|
||||
print("\n[!] Config does not have at least 2 wordlists.")
|
||||
print("Set hcatCombinationWordlist to a list of 2+ paths in config.json.")
|
||||
@@ -332,14 +332,18 @@ def combinator_crack(ctx: Any) -> None:
|
||||
print("\n[!] Combinator attack requires at least 2 wordlists.")
|
||||
print("Aborting combinator attack.")
|
||||
return
|
||||
separator = input("\nEnter separator between words (leave blank for none): ").strip()
|
||||
separator = input(
|
||||
"\nEnter separator between words (leave blank for none): "
|
||||
).strip()
|
||||
|
||||
if len(wordlists) == 2 and not separator:
|
||||
ctx.hcatCombination(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
elif len(wordlists) == 3 and not separator:
|
||||
ctx.hcatCombinator3(ctx.hcatHashType, ctx.hcatHashFile, wordlists)
|
||||
else:
|
||||
ctx.hcatCombinatorX(ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None)
|
||||
ctx.hcatCombinatorX(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, wordlists, separator or None
|
||||
)
|
||||
|
||||
|
||||
def hybrid_crack(ctx: Any) -> None:
|
||||
@@ -568,7 +572,9 @@ def ollama_attack(ctx: Any) -> None:
|
||||
items.append(("99", "Cancel"))
|
||||
|
||||
while True:
|
||||
choice = interactive_menu(items, title="\nLLM Attack", prompt="\n\tSelect generation mode: ")
|
||||
choice = interactive_menu(
|
||||
items, title="\nLLM Attack", prompt="\n\tSelect generation mode: "
|
||||
)
|
||||
if choice is None or choice == "99":
|
||||
return
|
||||
if choice == "1":
|
||||
@@ -656,7 +662,11 @@ def omen_attack(ctx: Any) -> None:
|
||||
("99", "Cancel"),
|
||||
]
|
||||
while True:
|
||||
choice = interactive_menu(model_items, title="\nOMEN Attack (Ordered Markov ENumerator)", prompt="\n\tChoice: ")
|
||||
choice = interactive_menu(
|
||||
model_items,
|
||||
title="\nOMEN Attack (Ordered Markov ENumerator)",
|
||||
prompt="\n\tChoice: ",
|
||||
)
|
||||
if choice is None or choice == "99":
|
||||
return
|
||||
if choice == "1":
|
||||
@@ -801,7 +811,7 @@ def combipow_crack(ctx: Any) -> None:
|
||||
if not os.path.isfile(path):
|
||||
print(f"[!] File not found: {path}")
|
||||
continue
|
||||
with (gzip.open(path, "rb") if path.endswith(".gz") else open(path, "rb")) as fh:
|
||||
with gzip.open(path, "rb") if path.endswith(".gz") else open(path, "rb") as fh:
|
||||
line_count = sum(1 for _ in fh)
|
||||
if line_count > 63:
|
||||
print(
|
||||
@@ -823,7 +833,9 @@ def generate_rules_crack(ctx: Any) -> None:
|
||||
print("RANDOM RULES ATTACK")
|
||||
print("=" * 60)
|
||||
print("Generates random hashcat mutation rules and applies them to a wordlist.")
|
||||
print("Use when known rulesets are exhausted - a chaos mode for rule-space exploration.")
|
||||
print(
|
||||
"Use when known rulesets are exhausted - a chaos mode for rule-space exploration."
|
||||
)
|
||||
print("=" * 60)
|
||||
|
||||
raw_count = input("\nNumber of random rules to generate (65536): ").strip()
|
||||
@@ -893,7 +905,9 @@ def generate_rules_crack(ctx: Any) -> None:
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
|
||||
ctx.hcatGenerateRules(ctx.hcatHashType, ctx.hcatHashFile, rule_count, wordlist_choice)
|
||||
ctx.hcatGenerateRules(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, rule_count, wordlist_choice
|
||||
)
|
||||
|
||||
|
||||
def ngram_attack(ctx: Any) -> None:
|
||||
@@ -929,7 +943,9 @@ def permute_crack(ctx: Any) -> None:
|
||||
print("PERMUTATION ATTACK")
|
||||
print("=" * 60)
|
||||
print("Generates ALL character permutations of each word in a targeted wordlist.")
|
||||
print("WARNING: Scales as N! per word. Only practical for words up to ~8 characters.")
|
||||
print(
|
||||
"WARNING: Scales as N! per word. Only practical for words up to ~8 characters."
|
||||
)
|
||||
print("Best for: short targeted wordlists (names, abbreviations, known fragments).")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -955,9 +971,7 @@ def permute_crack(ctx: Any) -> None:
|
||||
|
||||
wordlist_path = None
|
||||
while wordlist_path is None:
|
||||
raw = input(
|
||||
"\nEnter path to a wordlist FILE (tab to autocomplete): "
|
||||
).strip()
|
||||
raw = input("\nEnter path to a wordlist FILE (tab to autocomplete): ").strip()
|
||||
if not raw:
|
||||
continue
|
||||
if not os.path.exists(raw):
|
||||
@@ -971,7 +985,6 @@ def permute_crack(ctx: Any) -> None:
|
||||
ctx.hcatPermute(ctx.hcatHashType, ctx.hcatHashFile, wordlist_path)
|
||||
|
||||
|
||||
|
||||
def combinator_submenu(ctx: Any) -> None:
|
||||
items = [
|
||||
("1", "Combinator Attack (2-8 wordlists)"),
|
||||
@@ -1114,7 +1127,9 @@ def wordlist_filter_length(ctx: Any) -> None:
|
||||
if not os.path.isfile(infile):
|
||||
print(f"[!] File not found: {infile}")
|
||||
return
|
||||
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
||||
outfile = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter path to output wordlist"
|
||||
).strip()
|
||||
if not outfile:
|
||||
print("[!] Output path cannot be empty.")
|
||||
return
|
||||
@@ -1134,11 +1149,15 @@ def wordlist_filter_charclass_include(ctx: Any) -> None:
|
||||
if not os.path.isfile(infile):
|
||||
print(f"[!] File not found: {infile}")
|
||||
return
|
||||
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
||||
outfile = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter path to output wordlist"
|
||||
).strip()
|
||||
if not outfile:
|
||||
print("[!] Output path cannot be empty.")
|
||||
return
|
||||
print("[*] Char class mask: 1=lowercase, 2=uppercase, 4=digit, 8=symbol (additive, e.g. 3=lower+upper)")
|
||||
print(
|
||||
"[*] Char class mask: 1=lowercase, 2=uppercase, 4=digit, 8=symbol (additive, e.g. 3=lower+upper)"
|
||||
)
|
||||
mask = int(input("Mask value: ").strip() or "0")
|
||||
if ctx.wordlist_filter_req_include(infile, outfile, mask):
|
||||
print(f"\n[*] Filtered wordlist written to: {outfile}")
|
||||
@@ -1154,7 +1173,9 @@ def wordlist_filter_charclass_exclude(ctx: Any) -> None:
|
||||
if not os.path.isfile(infile):
|
||||
print(f"[!] File not found: {infile}")
|
||||
return
|
||||
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
||||
outfile = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter path to output wordlist"
|
||||
).strip()
|
||||
if not outfile:
|
||||
print("[!] Output path cannot be empty.")
|
||||
return
|
||||
@@ -1174,7 +1195,9 @@ def wordlist_cut_substring(ctx: Any) -> None:
|
||||
if not os.path.isfile(infile):
|
||||
print(f"[!] File not found: {infile}")
|
||||
return
|
||||
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
||||
outfile = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter path to output wordlist"
|
||||
).strip()
|
||||
if not outfile:
|
||||
print("[!] Output path cannot be empty.")
|
||||
return
|
||||
@@ -1195,7 +1218,9 @@ def wordlist_split_by_length(ctx: Any) -> None:
|
||||
if not os.path.isfile(infile):
|
||||
print(f"[!] File not found: {infile}")
|
||||
return
|
||||
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
|
||||
outdir = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter output directory path"
|
||||
).strip()
|
||||
if not outdir:
|
||||
print("[!] Output directory cannot be empty.")
|
||||
return
|
||||
@@ -1226,7 +1251,9 @@ def wordlist_subtract_words(ctx: Any) -> None:
|
||||
if not os.path.isfile(remove_file):
|
||||
print(f"[!] File not found: {remove_file}")
|
||||
return
|
||||
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
||||
outfile = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter path to output wordlist"
|
||||
).strip()
|
||||
if not outfile:
|
||||
print("[!] Output path cannot be empty.")
|
||||
return
|
||||
@@ -1241,12 +1268,16 @@ def wordlist_subtract_words(ctx: Any) -> None:
|
||||
if not os.path.isfile(infile):
|
||||
print(f"[!] File not found: {infile}")
|
||||
return
|
||||
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
|
||||
outfile = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter path to output wordlist"
|
||||
).strip()
|
||||
if not outfile:
|
||||
print("[!] Output path cannot be empty.")
|
||||
return
|
||||
raw = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter remove file paths", allow_multiple=True, base_dir=ctx.hcatWordlists
|
||||
"[*] Enter remove file paths",
|
||||
allow_multiple=True,
|
||||
base_dir=ctx.hcatWordlists,
|
||||
).strip()
|
||||
remove_files = [r.strip() for r in raw.split(",") if r.strip()]
|
||||
if not remove_files:
|
||||
@@ -1320,7 +1351,9 @@ def wordlist_optimize(ctx: Any) -> None:
|
||||
for p in not_found:
|
||||
print(f" {p}")
|
||||
return
|
||||
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
|
||||
outdir = ctx.select_file_with_autocomplete(
|
||||
"[*] Enter output directory path"
|
||||
).strip()
|
||||
if not outdir:
|
||||
print("[!] Output directory cannot be empty.")
|
||||
return
|
||||
|
||||
+3
-1
@@ -55,7 +55,9 @@ def setup_logging(logger: logging.Logger, hate_path: str, debug_mode: bool) -> N
|
||||
logger.addHandler(stream_handler)
|
||||
# Show HTTP requests made by the requests/urllib3 library.
|
||||
debug_handler = logging.StreamHandler(sys.stderr)
|
||||
debug_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s"))
|
||||
debug_handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
|
||||
)
|
||||
urllib3_logger = logging.getLogger("urllib3")
|
||||
urllib3_logger.setLevel(logging.DEBUG)
|
||||
urllib3_logger.addHandler(debug_handler)
|
||||
|
||||
+20
-13
@@ -479,7 +479,9 @@ omenTrainingList = config_parser.get("omenTrainingList", "rockyou.txt")
|
||||
omenMaxCandidates = int(config_parser.get("omenMaxCandidates", 1000000))
|
||||
pcfgRuleset = config_parser.get("pcfgRuleset", "DEFAULT")
|
||||
pcfgMaxCandidates = int(config_parser.get("pcfgMaxCandidates", 50000000))
|
||||
pcfgPrinceLingMaxCandidates = int(config_parser.get("pcfgPrinceLingMaxCandidates", 10000000))
|
||||
pcfgPrinceLingMaxCandidates = int(
|
||||
config_parser.get("pcfgPrinceLingMaxCandidates", 10000000)
|
||||
)
|
||||
|
||||
try:
|
||||
_cfg_optimized = config_parser["optimizedKernelAttacks"]
|
||||
@@ -746,9 +748,15 @@ if not SKIP_INIT:
|
||||
# Verify pcfg_cracker presence (optional, for PCFG attacks)
|
||||
# pcfg_cracker is pure-Python; we just check the script files exist.
|
||||
pcfg_guesser_script = os.path.join(hate_path, "pcfg_cracker", "pcfg_guesser.py")
|
||||
pcfg_prince_ling_script = os.path.join(hate_path, "pcfg_cracker", "prince_ling.py")
|
||||
if not os.path.isfile(pcfg_guesser_script) or not os.path.isfile(pcfg_prince_ling_script):
|
||||
print("pcfg_cracker not found at " + os.path.join(hate_path, "pcfg_cracker"))
|
||||
pcfg_prince_ling_script = os.path.join(
|
||||
hate_path, "pcfg_cracker", "prince_ling.py"
|
||||
)
|
||||
if not os.path.isfile(pcfg_guesser_script) or not os.path.isfile(
|
||||
pcfg_prince_ling_script
|
||||
):
|
||||
print(
|
||||
"pcfg_cracker not found at " + os.path.join(hate_path, "pcfg_cracker")
|
||||
)
|
||||
print("PCFG attacks will not be available. Run 'make' to fetch submodules.")
|
||||
elif not shutil.which("python3"):
|
||||
print("python3 not on PATH. PCFG attacks will not be available.")
|
||||
@@ -2818,8 +2826,11 @@ def hcatPrinceLing(hcatHashType, hcatHashFile):
|
||||
print(f"PCFG ruleset not found: {ruleset_dir}")
|
||||
return
|
||||
|
||||
cache_dir = hcatOptimizedWordlists if isinstance(hcatOptimizedWordlists, str) \
|
||||
cache_dir = (
|
||||
hcatOptimizedWordlists
|
||||
if isinstance(hcatOptimizedWordlists, str)
|
||||
else str(hcatOptimizedWordlists)
|
||||
)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
cache_path = os.path.join(cache_dir, f"pcfg_prince_ling_{pcfgRuleset}.txt")
|
||||
tmp_path = cache_path + ".tmp"
|
||||
@@ -3664,9 +3675,7 @@ def hashview_api():
|
||||
or api_name
|
||||
)
|
||||
try:
|
||||
download_result = api_harness.download_rules(
|
||||
rules_id, output_file
|
||||
)
|
||||
download_result = api_harness.download_rules(rules_id, output_file)
|
||||
print(f"\n✓ Success: Downloaded {download_result['size']} bytes")
|
||||
print(f" File: {download_result['output_file']}")
|
||||
except Exception as e:
|
||||
@@ -3936,8 +3945,8 @@ def hashview_api():
|
||||
print(
|
||||
"\nScanning customer hashfiles across common hash types..."
|
||||
)
|
||||
customer_hashfiles = (
|
||||
api_harness.get_all_customer_hashfiles(customer_id)
|
||||
customer_hashfiles = api_harness.get_all_customer_hashfiles(
|
||||
customer_id
|
||||
)
|
||||
except Exception as e:
|
||||
customer_hashfiles = []
|
||||
@@ -4981,9 +4990,7 @@ def main():
|
||||
else:
|
||||
argv = argv_temp # Fallback if subcommand not found
|
||||
|
||||
has_attack_subcommand = any(
|
||||
arg in _noninteractive.ATTACK_COMMANDS for arg in argv
|
||||
)
|
||||
has_attack_subcommand = any(arg in _noninteractive.ATTACK_COMMANDS for arg in argv)
|
||||
use_subcommand_parser = "hashview" in argv or has_attack_subcommand
|
||||
parser, hashview_parser = _build_parser(
|
||||
include_positional=not use_subcommand_parser,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This module owns the allowlist/blocklist of hash modes and the regex-based
|
||||
per-line validation used to decide whether to pass ``--username`` to hashcat.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
@@ -11,39 +12,59 @@ from typing import Final
|
||||
# Modes where bare hashes are the normal input AND files commonly carry a
|
||||
# ``username:`` prefix. Value is the expected hex length of the hash field.
|
||||
USERNAME_HASH_MODES: Final[dict[str, int]] = {
|
||||
"0": 32, # MD5
|
||||
"10": 32, # md5($pass.$salt)
|
||||
"20": 32, # md5($salt.$pass)
|
||||
"30": 32, # md5(unicode($pass).$salt)
|
||||
"40": 32, # md5($salt.unicode($pass))
|
||||
"50": 32, # HMAC-MD5
|
||||
"60": 32, # HMAC-MD5(key=$pass)
|
||||
"100": 40, # SHA1
|
||||
"101": 40, # nsldap/SHA1(Base64)
|
||||
"110": 40, # sha1($pass.$salt)
|
||||
"120": 40, # sha1($salt.$pass)
|
||||
"130": 40, # sha1(unicode($pass).$salt)
|
||||
"140": 40, # sha1($salt.unicode($pass))
|
||||
"150": 40, # HMAC-SHA1
|
||||
"160": 40, # HMAC-SHA1(key=$pass)
|
||||
"900": 32, # MD4
|
||||
"1000": 32, # NTLM bare
|
||||
"1400": 64, # SHA2-256
|
||||
"1410": 64, "1420": 64, "1430": 64, "1440": 64, "1450": 64, "1460": 64,
|
||||
"0": 32, # MD5
|
||||
"10": 32, # md5($pass.$salt)
|
||||
"20": 32, # md5($salt.$pass)
|
||||
"30": 32, # md5(unicode($pass).$salt)
|
||||
"40": 32, # md5($salt.unicode($pass))
|
||||
"50": 32, # HMAC-MD5
|
||||
"60": 32, # HMAC-MD5(key=$pass)
|
||||
"100": 40, # SHA1
|
||||
"101": 40, # nsldap/SHA1(Base64)
|
||||
"110": 40, # sha1($pass.$salt)
|
||||
"120": 40, # sha1($salt.$pass)
|
||||
"130": 40, # sha1(unicode($pass).$salt)
|
||||
"140": 40, # sha1($salt.unicode($pass))
|
||||
"150": 40, # HMAC-SHA1
|
||||
"160": 40, # HMAC-SHA1(key=$pass)
|
||||
"900": 32, # MD4
|
||||
"1000": 32, # NTLM bare
|
||||
"1400": 64, # SHA2-256
|
||||
"1410": 64,
|
||||
"1420": 64,
|
||||
"1430": 64,
|
||||
"1440": 64,
|
||||
"1450": 64,
|
||||
"1460": 64,
|
||||
"1700": 128, # SHA2-512
|
||||
"1710": 128, "1720": 128, "1730": 128, "1740": 128, "1750": 128, "1760": 128,
|
||||
"3000": 16, # LM (single half)
|
||||
"1710": 128,
|
||||
"1720": 128,
|
||||
"1730": 128,
|
||||
"1740": 128,
|
||||
"1750": 128,
|
||||
"1760": 128,
|
||||
"3000": 16, # LM (single half)
|
||||
}
|
||||
|
||||
# Modes explicitly excluded even if they appear in allowlist (they don't, but
|
||||
# this constant is the documentation of the intentional blocklist). Binary
|
||||
# formats, IKE-PSK, and NetNTLM (already preprocessed elsewhere).
|
||||
USERNAME_DETECT_BLOCKLIST: Final[frozenset[str]] = frozenset({
|
||||
"2500", "22000", "2501", "16800", "16801", "22001", # WPA variants (binary)
|
||||
"5300", "5400", # IKE-PSK
|
||||
"5500", "5600", # NetNTLM (own preprocess)
|
||||
"1800", "3200", # non-hex hash formats
|
||||
})
|
||||
USERNAME_DETECT_BLOCKLIST: Final[frozenset[str]] = frozenset(
|
||||
{
|
||||
"2500",
|
||||
"22000",
|
||||
"2501",
|
||||
"16800",
|
||||
"16801",
|
||||
"22001", # WPA variants (binary)
|
||||
"5300",
|
||||
"5400", # IKE-PSK
|
||||
"5500",
|
||||
"5600", # NetNTLM (own preprocess)
|
||||
"1800",
|
||||
"3200", # non-hex hash formats
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def detect_username_hash_format(
|
||||
|
||||
@@ -10,6 +10,15 @@ stages = ["pre-push"]
|
||||
pass_filenames = false
|
||||
always_run = true
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "ruff-format"
|
||||
name = "ruff-format"
|
||||
entry = "uv run ruff format --check hate_crack"
|
||||
language = "system"
|
||||
stages = ["pre-push"]
|
||||
pass_filenames = false
|
||||
always_run = true
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "ty"
|
||||
name = "ty"
|
||||
@@ -37,6 +46,15 @@ stages = ["pre-push"]
|
||||
pass_filenames = false
|
||||
always_run = true
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "bandit"
|
||||
name = "bandit security scan (vs baseline)"
|
||||
entry = "uvx --from 'bandit[toml]==1.9.4' bandit -r hate_crack -c pyproject.toml -b .bandit-baseline.json -q"
|
||||
language = "system"
|
||||
stages = ["pre-push"]
|
||||
pass_filenames = false
|
||||
always_run = true
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "audit-docs"
|
||||
name = "audit-docs"
|
||||
@@ -45,3 +63,26 @@ language = "system"
|
||||
stages = ["post-commit"]
|
||||
pass_filenames = false
|
||||
always_run = true
|
||||
|
||||
# General hygiene hooks (mirrors hashview's .pre-commit-config.yaml). These run
|
||||
# at the pre-commit stage, so `prek install` must include `--hook-type pre-commit`
|
||||
# (see CLAUDE.md). Auto-fixers modify files in place; re-stage and commit again.
|
||||
[[repos]]
|
||||
repo = "https://github.com/pre-commit/pre-commit-hooks"
|
||||
rev = "v5.0.0"
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "trailing-whitespace"
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "end-of-file-fixer"
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "check-yaml"
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "check-merge-conflict"
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "check-added-large-files"
|
||||
args = ["--maxkb=1024"]
|
||||
|
||||
@@ -50,6 +50,23 @@ exclude = [
|
||||
"rules",
|
||||
]
|
||||
|
||||
[tool.bandit]
|
||||
# hate_crack shells out to hashcat and its helper binaries extensively, so the
|
||||
# scan produces many low-severity subprocess/shlex findings. These are reviewed
|
||||
# and captured in .bandit-baseline.json; CI compares against that baseline so
|
||||
# only NEW findings fail the build (mirrors the hashview approach). Scan the
|
||||
# first-party package only — bundled third-party trees are excluded.
|
||||
exclude_dirs = [
|
||||
"tests",
|
||||
".venv",
|
||||
"build",
|
||||
"dist",
|
||||
"PACK",
|
||||
"hashcat-utils",
|
||||
"omen",
|
||||
"princeprocessor",
|
||||
]
|
||||
|
||||
[tool.ty.src]
|
||||
exclude = [
|
||||
"build/",
|
||||
|
||||
Reference in New Issue
Block a user