Private/AD/Core/Get-ReconnaissanceData.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Get-ReconnaissanceData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Connection,

        [string[]]$Categories = @('All'),

        [int]$InactiveDays = 90,

        [int]$PasswordAgeDays = 365,

        [string]$NtdsPath,

        [string]$WeakPasswordList,

        [switch]$Quiet
    )

    # ── Category-to-data-source mapping ──────────────────────────────────
    $categoryDataNeeds = @{
        DomainForest        = @('DomainInfo', 'DomainControllers')
        Trusts              = @('TrustRelationships')
        PrivilegedAccounts  = @('PrivilegedMembers', 'DomainInfo')
        PasswordPolicy      = @('PasswordPolicies', 'DomainInfo')
        Kerberos            = @('KerberosConfig')
        ACLDelegation       = @('ObjectACLs', 'PrivilegedMembers')
        GroupPolicy         = @('GroupPolicyObjects')
        LogonScripts        = @('LogonScripts')
        CertificateServices = @('CertificateServices')
        StaleObjects        = @('StaleObjects')
        Network             = @('NetworkConfig')
        TierZero            = @('PrivilegedMembers', 'TierZeroSignals')
        Logging             = @('NetworkConfig')
        Tradecraft          = @('DomainControllers', 'TradecraftSignals')
    }

    # Resolve which data sources are required
    $requiredSources = [System.Collections.Generic.HashSet[string]]::new(
        [StringComparer]::OrdinalIgnoreCase
    )

    if ($Categories -contains 'All') {
        foreach ($sources in $categoryDataNeeds.Values) {
            foreach ($s in $sources) { [void]$requiredSources.Add($s) }
        }
    } else {
        foreach ($cat in $Categories) {
            if ($categoryDataNeeds.ContainsKey($cat)) {
                foreach ($s in $categoryDataNeeds[$cat]) {
                    [void]$requiredSources.Add($s)
                }
            }
        }
    }

    # Always collect module availability
    [void]$requiredSources.Add('ModuleAvailability')

    # ── Initialize result hashtable ──────────────────────────────────────
    $data = @{
        Domain              = $null
        DomainControllers   = $null
        Trusts              = $null
        PrivilegedAccounts  = $null
        PasswordPolicies    = $null
        Kerberos            = $null
        ACLs                = $null
        GroupPolicies       = $null
        LogonScripts        = $null
        CertificateServices = $null
        StaleObjects        = $null
        Network             = $null
        TierZero            = $null
        Tradecraft          = $null
        ModuleAvailability  = $null
        Connection          = $Connection
        Errors              = @{}
    }

    # Helper: determine whether a data source is needed
    $needsSource = { param([string]$Name) $requiredSources.Contains($Name) }

    # ── 1. Module Availability ───────────────────────────────────────────
    if (& $needsSource 'ModuleAvailability') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Checking module availability'
        }
        try {
            $data.ModuleAvailability = Test-ADModuleAvailability
        } catch {
            $data.Errors['ModuleAvailability'] = $_.Exception.Message
            $data.ModuleAvailability = @{
                ActiveDirectory = $false
                GroupPolicy     = $false
                DSInternals     = $false
                PSPKI           = $false
            }
        }
    }

    # ── 2. Domain Information ────────────────────────────────────────────
    if (& $needsSource 'DomainInfo') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Collecting domain information'
        }
        try {
            $data.Domain = Get-ADDomainInfo -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to collect domain information: $_"
            $data.Errors['DomainInfo'] = $_.Exception.Message
        }
    }

    # ── 3. Domain Controllers ────────────────────────────────────────────
    if (& $needsSource 'DomainControllers') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Enumerating domain controllers'
        }
        try {
            $data.DomainControllers = Get-ADDomainControllers -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to enumerate domain controllers: $_"
            $data.Errors['DomainControllers'] = $_.Exception.Message
        }
    }

    # ── 4. Trust Relationships ───────────────────────────────────────────
    if (& $needsSource 'TrustRelationships') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Collecting trust relationships'
        }
        try {
            $data.Trusts = Get-ADTrustRelationships -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to collect trust relationships: $_"
            $data.Errors['TrustRelationships'] = $_.Exception.Message
        }
    }

    # ── 5. Privileged Members ────────────────────────────────────────────
    if (& $needsSource 'PrivilegedMembers') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Enumerating privileged group members'
        }
        try {
            $data.PrivilegedAccounts = Get-ADPrivilegedMembers -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to enumerate privileged members: $_"
            $data.Errors['PrivilegedMembers'] = $_.Exception.Message
        }
    }

    # ── 6. Password Policies ────────────────────────────────────────────
    if (& $needsSource 'PasswordPolicies') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Collecting password policies'
        }
        try {
            $data.PasswordPolicies = Get-ADPasswordPolicies -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to collect password policies: $_"
            $data.Errors['PasswordPolicies'] = $_.Exception.Message
        }
    }

    # ── 7. Kerberos Configuration ────────────────────────────────────────
    if (& $needsSource 'KerberosConfig') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Analyzing Kerberos configuration'
        }
        try {
            $data.Kerberos = Get-ADKerberosConfig -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to analyze Kerberos configuration: $_"
            $data.Errors['KerberosConfig'] = $_.Exception.Message
        }
    }

    # ── 8. Object ACLs / Delegation ──────────────────────────────────────
    if (& $needsSource 'ObjectACLs') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Auditing object ACLs and delegation'
        }
        try {
            $data.ACLs = Get-ADObjectACLs -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to audit object ACLs: $_"
            $data.Errors['ObjectACLs'] = $_.Exception.Message
        }
    }

    # ── 9. Group Policy Objects ──────────────────────────────────────────
    if (& $needsSource 'GroupPolicyObjects') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Collecting Group Policy Objects'
        }
        try {
            $data.GroupPolicies = Get-ADGroupPolicyObjects -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to collect Group Policy Objects: $_"
            $data.Errors['GroupPolicyObjects'] = $_.Exception.Message
        }
    }

    # ── 10. Logon Scripts ────────────────────────────────────────────────
    if (& $needsSource 'LogonScripts') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Analyzing logon scripts'
        }
        try {
            $data.LogonScripts = Get-ADLogonScripts -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to analyze logon scripts: $_"
            $data.Errors['LogonScripts'] = $_.Exception.Message
        }
    }

    # ── 11. Certificate Services (AD CS) ─────────────────────────────────
    if (& $needsSource 'CertificateServices') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Enumerating AD Certificate Services'
        }
        try {
            $data.CertificateServices = Get-ADCertificateServices -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to enumerate Certificate Services: $_"
            $data.Errors['CertificateServices'] = $_.Exception.Message
        }
    }

    # ── 12. Network policy (relay-precondition surface) ──────────────────
    if (& $needsSource 'NetworkConfig') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Reading network-layer policy from SYSVOL'
        }
        try {
            $data.Network = Get-ADNetworkConfig -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to read network policy from SYSVOL: $_"
            $data.Errors['NetworkConfig'] = $_.Exception.Message
        }
    }

    # ── 13. Tier-Zero signals (MSOL_ accounts, hybrid identity surface) ──
    if (& $needsSource 'TierZeroSignals') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Scanning for Tier-0 hybrid-identity signals'
        }
        try {
            $data.TierZero = Get-ADTierZeroSignals -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Tier-Zero signal collection failed: $_"
            $data.Errors['TierZeroSignals'] = $_.Exception.Message
        }
    }

    # ── 14. Tradecraft signals (cpassword, DCShadow, BitLocker, RODC) ────
    if (& $needsSource 'TradecraftSignals') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Scanning for adversary-tradecraft signals'
        }
        try {
            $data.Tradecraft = Get-ADTradecraftSignals -Connection $Connection -Quiet:$Quiet
        } catch {
            Write-Warning "Tradecraft signal collection failed: $_"
            $data.Errors['TradecraftSignals'] = $_.Exception.Message
        }
    }

    # ── 15. Stale Objects ────────────────────────────────────────────────
    if (& $needsSource 'StaleObjects') {
        if (-not $Quiet) {
            Write-ProgressLine -Phase RECON -Message 'Identifying stale and abandoned objects'
        }
        try {
            $data.StaleObjects = Get-ADStaleObjects `
                -Connection $Connection `
                -InactiveDays $InactiveDays `
                -PasswordAgeDays $PasswordAgeDays `
                -Quiet:$Quiet
        } catch {
            Write-Warning "Failed to identify stale objects: $_"
            $data.Errors['StaleObjects'] = $_.Exception.Message
        }
    }

    # ── Summary ──────────────────────────────────────────────────────────
    if (-not $Quiet) {
        $collectedCount = 0
        $nullKeys = [System.Collections.Generic.List[string]]::new()

        foreach ($key in @('Domain', 'DomainControllers', 'Trusts', 'PrivilegedAccounts',
                           'PasswordPolicies', 'Kerberos', 'ACLs', 'GroupPolicies',
                           'LogonScripts', 'CertificateServices', 'StaleObjects', 'Network', 'TierZero', 'Tradecraft')) {
            if ($null -ne $data[$key]) {
                $collectedCount++
            } elseif ($requiredSources.Count -gt 0) {
                # Only track as missing if it was actually requested
                $sourceMapping = @{
                    Domain             = 'DomainInfo'
                    DomainControllers  = 'DomainControllers'
                    Trusts             = 'TrustRelationships'
                    PrivilegedAccounts = 'PrivilegedMembers'
                    PasswordPolicies   = 'PasswordPolicies'
                    Kerberos           = 'KerberosConfig'
                    ACLs               = 'ObjectACLs'
                    GroupPolicies      = 'GroupPolicyObjects'
                    LogonScripts       = 'LogonScripts'
                    CertificateServices = 'CertificateServices'
                    StaleObjects       = 'StaleObjects'
                    Network            = 'NetworkConfig'
                    TierZero           = 'TierZeroSignals'
                    Tradecraft         = 'TradecraftSignals'
                }
                if ($sourceMapping.ContainsKey($key) -and $requiredSources.Contains($sourceMapping[$key])) {
                    $nullKeys.Add($key)
                }
            }
        }

        $errorCount = $data.Errors.Count
        $domainName = if ($data.Domain) { $data.Domain.DomainName } else { 'unknown' }

        $summary = "Reconnaissance complete for $domainName`: $collectedCount data source(s) collected"
        if ($errorCount -gt 0) {
            $summary += ", $errorCount error(s)"
        }
        if ($nullKeys.Count -gt 0) {
            $summary += " (missing: $($nullKeys -join ', '))"
        }
        Write-ProgressLine -Phase RECON -Message $summary
    }

    return $data
}