#!/usr/bin/env python3
"""Agent for performing hash cracking with hashcat — hash identification, attack management, and result analysis."""

import json
import argparse
import subprocess
import hashlib
import re
from collections import Counter
from pathlib import Path


HASH_PATTERNS = {
    "MD5": (r"^[a-f0-9]{32}$", 0),
    "SHA1": (r"^[a-f0-9]{40}$", 100),
    "SHA256": (r"^[a-f0-9]{64}$", 1400),
    "SHA512": (r"^[a-f0-9]{128}$", 1700),
    "NTLM": (r"^[a-f0-9]{32}$", 1000),
    "bcrypt": (r"^\$2[aby]?\$\d+\$.{53}$", 3200),
    "sha512crypt": (r"^\$6\$[^\$]+\$[a-zA-Z0-9./]{86}$", 1800),
    "sha256crypt": (r"^\$5\$[^\$]+\$[a-zA-Z0-9./]{43}$", 7400),
    "md5crypt": (r"^\$1\$[^\$]+\$[a-zA-Z0-9./]{22}$", 500),
    "NetNTLMv2": (r"^[^:]+::\S+:[a-f0-9]{16}:[a-f0-9]{32}:[a-f0-9]+$", 5600),
    "Kerberos_TGS": (r"^\$krb5tgs\$", 13100),
    "Kerberos_AS": (r"^\$krb5asrep\$", 18200),
}


def identify_hash(hash_string):
    """Identify hash type and return hashcat mode."""
    candidates = []
    for name, (pattern, mode) in HASH_PATTERNS.items():
        if re.match(pattern, hash_string.strip(), re.IGNORECASE):
            candidates.append({"type": name, "hashcat_mode": mode})
    return {"hash": hash_string[:40] + "..." if len(hash_string) > 40 else hash_string, "candidates": candidates}


def identify_hashes_file(hash_file):
    """Identify hash types from a file of hashes."""
    hashes = Path(hash_file).read_text(encoding="utf-8", errors="replace").strip().splitlines()
    results = []
    for h in hashes[:100]:
        h = h.strip()
        if h:
            results.append(identify_hash(h))
    types = {}
    for r in results:
        for c in r["candidates"]:
            types[c["type"]] = types.get(c["type"], 0) + 1
    return {"total_hashes": len(results), "type_distribution": types, "samples": results[:5]}


def run_hashcat(hash_file, mode, attack="dictionary", wordlist=None, rules=None, mask=None):
    """Execute hashcat with specified attack mode."""
    cmd = ["hashcat", "-m", str(mode), "--quiet", "--potfile-disable", "-o", "/tmp/hashcat_out.txt"]
    if attack == "dictionary":
        if not wordlist:
            return {"error": "wordlist required for dictionary attack"}
        cmd += ["-a", "0", hash_file, wordlist]
        if rules:
            cmd += ["-r", rules]
    elif attack == "brute":
        m = mask or "?a?a?a?a?a?a?a?a"
        cmd += ["-a", "3", hash_file, m]
    elif attack == "combinator":
        if not wordlist:
            return {"error": "two wordlists required (comma-separated)"}
        wl = wordlist.split(",")
        if len(wl) != 2:
            return {"error": "combinator needs two wordlists: list1.txt,list2.txt"}
        cmd += ["-a", "1", hash_file, wl[0], wl[1]]
    else:
        return {"error": f"Unknown attack type: {attack}"}
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
        cracked = []
        out_file = Path("/tmp/hashcat_out.txt")
        if out_file.exists():
            for line in out_file.read_text().strip().splitlines():
                parts = line.split(":", 1)
                if len(parts) == 2:
                    cracked.append({"hash": parts[0][:30] + "...", "plain": parts[1]})
        return {
            "attack": attack, "mode": mode, "cracked_count": len(cracked),
            "cracked": cracked[:50], "return_code": result.returncode,
            "stderr_snippet": result.stderr[:300] if result.stderr else "",
        }
    except FileNotFoundError:
        return {"error": "hashcat not found in PATH"}
    except subprocess.TimeoutExpired:
        return {"error": "hashcat timed out after 3600s"}


def parse_hashcat_status(potfile):
    """Parse hashcat potfile for cracked results."""
    cracked = []
    try:
        for line in Path(potfile).read_text(encoding="utf-8", errors="replace").strip().splitlines():
            parts = line.split(":", 1)
            if len(parts) == 2:
                cracked.append({"hash": parts[0], "plain": parts[1]})
    except FileNotFoundError:
        return {"error": f"Potfile not found: {potfile}"}
    passwords = [c["plain"] for c in cracked]
    length_dist = {}
    for p in passwords:
        l = len(p)
        bucket = f"{l}" if l <= 8 else "9-12" if l <= 12 else "13+"
        length_dist[bucket] = length_dist.get(bucket, 0) + 1
    charset = {"lowercase_only": 0, "uppercase_mixed": 0, "with_digits": 0, "with_special": 0}
    for p in passwords:
        if re.match(r"^[a-z]+$", p):
            charset["lowercase_only"] += 1
        elif re.search(r"\d", p):
            charset["with_digits"] += 1
        elif re.search(r"[!@#$%^&*()_+=\-\[\]{};:'\",.<>?/\\|`~]", p):
            charset["with_special"] += 1
        else:
            charset["uppercase_mixed"] += 1
    return {
        "total_cracked": len(cracked),
        "length_distribution": length_dist,
        "charset_analysis": charset,
        "top_passwords": [p for p, _ in Counter(passwords).most_common(10)],
    }


def generate_hash(plaintext, algorithm="sha256"):
    """Generate hash of a plaintext string for testing."""
    algos = {"md5": hashlib.md5, "sha1": hashlib.sha1, "sha256": hashlib.sha256, "sha512": hashlib.sha512}
    if algorithm not in algos:
        return {"error": f"Unsupported: {algorithm}. Use: {list(algos.keys())}"}
    h = algos[algorithm](plaintext.encode()).hexdigest()
    return {"plaintext": plaintext, "algorithm": algorithm, "hash": h}


def main():
    parser = argparse.ArgumentParser(description="Hashcat Hash Cracking Agent")
    sub = parser.add_subparsers(dest="command")
    i = sub.add_parser("identify", help="Identify hash type")
    i.add_argument("--hash", help="Single hash string")
    i.add_argument("--file", help="File containing hashes")
    c = sub.add_parser("crack", help="Run hashcat")
    c.add_argument("--hash-file", required=True)
    c.add_argument("--mode", type=int, required=True, help="Hashcat mode number")
    c.add_argument("--attack", default="dictionary", choices=["dictionary", "brute", "combinator"])
    c.add_argument("--wordlist", help="Wordlist path (or two comma-separated for combinator)")
    c.add_argument("--rules", help="Hashcat rules file")
    c.add_argument("--mask", help="Brute force mask")
    p = sub.add_parser("parse", help="Parse potfile results")
    p.add_argument("--potfile", required=True)
    g = sub.add_parser("gen", help="Generate test hash")
    g.add_argument("--text", required=True)
    g.add_argument("--algo", default="sha256")
    args = parser.parse_args()
    if args.command == "identify":
        result = identify_hashes_file(args.file) if args.file else identify_hash(args.hash) if args.hash else {"error": "provide --hash or --file"}
    elif args.command == "crack":
        result = run_hashcat(args.hash_file, args.mode, args.attack, args.wordlist, args.rules, args.mask)
    elif args.command == "parse":
        result = parse_hashcat_status(args.potfile)
    elif args.command == "gen":
        result = generate_hash(args.text, args.algo)
    else:
        parser.print_help()
        return
    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()
