#!/usr/bin/env python3
# For authorized penetration testing and educational environments only.
# Usage against targets without prior mutual consent is illegal.
# It is the end user's responsibility to obey all applicable local, state and federal laws.
"""BloodHound CE reconnaissance agent using bloodhound Python ingestor and Neo4j."""

import json
import sys
import argparse
import subprocess
from datetime import datetime

try:
    from neo4j import GraphDatabase
except ImportError:
    print("Install: pip install neo4j")
    sys.exit(1)


def collect_bloodhound_data(domain, username, password, dc_ip, method="all"):
    """Run BloodHound Python ingestor to collect AD data."""
    cmd = [
        "bloodhound-python", "-d", domain, "-u", username, "-p", password,
        "-ns", dc_ip, "-c", method, "--zip",
    ]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
        return {"status": "completed", "output": result.stdout[:1000]}
    except FileNotFoundError:
        return {"status": "error", "message": "Install: pip install bloodhound"}
    except subprocess.TimeoutExpired:
        return {"status": "timeout"}


def query_shortest_path_to_da(driver):
    """Find shortest path to Domain Admins."""
    with driver.session() as session:
        result = session.run(
            "MATCH p=shortestPath((u:User)-[*1..]->(g:Group {name: $group})) "
            "WHERE u.enabled = true RETURN u.name AS user, length(p) AS hops "
            "ORDER BY hops LIMIT 10",
            group="DOMAIN ADMINS@DOMAIN.LOCAL",
        )
        return [{"user": r["user"], "hops": r["hops"]} for r in result]


def query_kerberoastable_users(driver):
    """Find kerberoastable user accounts."""
    with driver.session() as session:
        result = session.run(
            "MATCH (u:User) WHERE u.hasspn = true AND u.enabled = true "
            "RETURN u.name AS user, u.serviceprincipalnames AS spns, "
            "u.admincount AS admin_count ORDER BY u.admincount DESC"
        )
        return [{"user": r["user"], "spns": r["spns"],
                 "admin": r["admin_count"]} for r in result]


def query_unconstrained_delegation(driver):
    """Find computers with unconstrained delegation."""
    with driver.session() as session:
        result = session.run(
            "MATCH (c:Computer) WHERE c.unconstraineddelegation = true "
            "RETURN c.name AS computer, c.operatingsystem AS os"
        )
        return [{"computer": r["computer"], "os": r["os"]} for r in result]


def query_as_rep_roastable(driver):
    """Find AS-REP roastable accounts (no pre-auth required)."""
    with driver.session() as session:
        result = session.run(
            "MATCH (u:User) WHERE u.dontreqpreauth = true AND u.enabled = true "
            "RETURN u.name AS user, u.admincount AS admin_count"
        )
        return [{"user": r["user"], "admin": r["admin_count"]} for r in result]


def run_recon(neo4j_uri, neo4j_user, neo4j_password):
    """Run BloodHound reconnaissance queries."""
    driver = GraphDatabase.driver(neo4j_uri, auth=(neo4j_user, neo4j_password))
    print(f"\n{'='*60}")
    print(f"  BLOODHOUND CE RECONNAISSANCE")
    print(f"  Generated: {datetime.utcnow().isoformat()} UTC")
    print(f"{'='*60}\n")

    paths = query_shortest_path_to_da(driver)
    print(f"--- SHORTEST PATHS TO DOMAIN ADMIN ({len(paths)}) ---")
    for p in paths:
        print(f"  {p['user']}: {p['hops']} hops")

    kerb = query_kerberoastable_users(driver)
    print(f"\n--- KERBEROASTABLE USERS ({len(kerb)}) ---")
    for k in kerb[:10]:
        print(f"  {k['user']} (admin={k['admin']})")

    deleg = query_unconstrained_delegation(driver)
    print(f"\n--- UNCONSTRAINED DELEGATION ({len(deleg)}) ---")
    for d in deleg:
        print(f"  {d['computer']}: {d['os']}")

    asrep = query_as_rep_roastable(driver)
    print(f"\n--- AS-REP ROASTABLE ({len(asrep)}) ---")
    for a in asrep:
        print(f"  {a['user']} (admin={a['admin']})")

    driver.close()
    return {"paths_to_da": paths, "kerberoastable": kerb,
            "unconstrained_delegation": deleg, "asrep_roastable": asrep}


def main():
    parser = argparse.ArgumentParser(description="BloodHound CE Recon Agent")
    parser.add_argument("--neo4j-uri", default="bolt://localhost:7687", help="Neo4j URI")
    parser.add_argument("--neo4j-user", default="neo4j", help="Neo4j username")
    parser.add_argument("--neo4j-password", required=True, help="Neo4j password")
    parser.add_argument("--collect", action="store_true", help="Run data collection first")
    parser.add_argument("--domain", help="AD domain for collection")
    parser.add_argument("--ad-user", help="AD username for collection")
    parser.add_argument("--ad-pass", help="AD password for collection")
    parser.add_argument("--dc-ip", help="Domain controller IP")
    parser.add_argument("--output", help="Save report to JSON file")
    args = parser.parse_args()

    if args.collect and args.domain:
        result = collect_bloodhound_data(args.domain, args.ad_user, args.ad_pass, args.dc_ip)
        print(json.dumps(result, indent=2))

    report = run_recon(args.neo4j_uri, args.neo4j_user, args.neo4j_password)
    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2, default=str)
        print(f"\n[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
