Private/ADMonitor/Core/Get-ADMonitorData.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-ADMonitorData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$LdapConnection,

        [ValidateSet('Fast', 'Full')]
        [string]$ScanMode = 'Fast',

        [switch]$Quiet
    )

    $result = @{
        privilegedGroups = @{}
        adminSDHolderACL = @()
        krbtgtPwdLastSet = $null
        krbtgtKeyVersion = 0
        trusts           = @()
        gpoObjects       = @{}
        sensitiveAcls    = @{}
        certTemplates    = @{}
        delegations      = @{}
        dnsRecords       = @()
        schemaVersion    = 0
        recentlyChanged  = @()
        domainName       = ''
        collectedAt      = [datetime]::UtcNow.ToString('o')
        scanMode         = $ScanMode
        errors           = @{}
    }

    $domainDN = $LdapConnection.DomainDN
    $configDN = $LdapConnection.ConfigDN
    $schemaDN = $LdapConnection.SchemaDN
    $result.domainName = ($domainDN -replace '^DC=', '' -replace ',DC=', '.').ToLower()

    # ── 1. Privileged group membership (Fast + Full) ───────────────────
    if (-not $Quiet) {
        Write-ProgressLine -Phase SCANNING -Message 'Collecting privileged group membership'
    }

    try {
        $privData = Get-ADPrivilegedMembers -Connection $LdapConnection -Quiet:$Quiet

        foreach ($groupName in $privData.PrivilegedGroups.Keys) {
            $members = $privData.PrivilegedGroups[$groupName]
            $memberNames = @($members | Where-Object { -not $_.IsGroup } | ForEach-Object { $_.SamAccountName }) | Sort-Object
            $result.privilegedGroups[$groupName] = $memberNames
        }

        # AdminSDHolder ACL entries
        if ($privData.AdminSDHolderACL -and $privData.AdminSDHolderACL -is [System.DirectoryServices.ActiveDirectorySecurity]) {
            $sd = $privData.AdminSDHolderACL
            $aclEntries = [System.Collections.Generic.List[hashtable]]::new()
            try {
                $rules = $sd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
                foreach ($rule in $rules) {
                    $aclEntries.Add(@{
                        identity = $rule.IdentityReference.Value
                        rights   = $rule.ActiveDirectoryRights.ToString()
                        type     = $rule.AccessControlType.ToString()
                    })
                }
            } catch {
                Write-Verbose "Failed to parse AdminSDHolder ACL rules: $_"
            }
            $result.adminSDHolderACL = @($aclEntries)
        }

        # krbtgt info
        if ($privData.KrbtgtAccount) {
            $result.krbtgtPwdLastSet = if ($privData.KrbtgtAccount.PwdLastSet) {
                $privData.KrbtgtAccount.PwdLastSet.ToString('o')
            } else { $null }
            $result.krbtgtKeyVersion = $privData.KrbtgtAccount.KeyVersionNumber
        }
    } catch {
        Write-Warning "Failed to collect privileged group data: $_"
        $result.errors['privilegedGroups'] = $_.Exception.Message
    }

    # ── 2. Trust relationships (Fast + Full) ───────────────────────────
    if (-not $Quiet) {
        Write-ProgressLine -Phase SCANNING -Message 'Collecting trust relationships'
    }

    try {
        $trustData = Get-ADTrustRelationships -Connection $LdapConnection -Quiet:$Quiet
        $trusts = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($t in $trustData) {
            $trusts.Add(@{
                name               = $t.TrustPartner
                flatName           = $t.FlatName
                direction          = $t.TrustDirection
                type               = $t.TrustType
                isTransitive       = $t.IsTransitive
                sidFiltering       = $t.SIDFilteringEnabled
                forestTransitive   = $t.ForestTransitive
                withinForest       = $t.WithinForest
                whenCreated        = if ($t.WhenCreated) { $t.WhenCreated.ToString('o') } else { $null }
                whenChanged        = if ($t.WhenChanged) { $t.WhenChanged.ToString('o') } else { $null }
                trustAttributes    = $t.TrustAttributes
                trustSID           = $t.TrustSID
            })
        }
        $result.trusts = @($trusts)
    } catch {
        Write-Warning "Failed to collect trust relationships: $_"
        $result.errors['trusts'] = $_.Exception.Message
    }

    # ── 3. Recently changed objects (Fast + Full) ──────────────────────
    if (-not $Quiet) {
        Write-ProgressLine -Phase SCANNING -Message 'Querying recently changed objects'
    }

    try {
        $searchRoot = New-LdapSearchRoot -Connection $LdapConnection -SearchBase $domainDN
        $recentDate = [datetime]::UtcNow.AddDays(-7)
        $recentFilter = "(&(whenChanged>=$($recentDate.ToString('yyyyMMddHHmmss.0Z')))(|(objectClass=user)(objectClass=group)(objectClass=computer)(objectClass=organizationalUnit)))"

        $recentResults = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter $recentFilter `
            -Properties @('distinguishedName', 'objectClass', 'whenChanged', 'sAMAccountName', 'whenCreated')

        $recentList = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($obj in $recentResults) {
            $objClasses = if ($obj.ContainsKey('objectclass')) {
                $oc = $obj['objectclass']
                if ($oc -is [array]) { $oc } else { @($oc) }
            } else { @() }

            $primaryClass = if ($objClasses -contains 'computer') { 'computer' }
                           elseif ($objClasses -contains 'group') { 'group' }
                           elseif ($objClasses -contains 'user') { 'user' }
                           elseif ($objClasses -contains 'organizationalUnit') { 'organizationalUnit' }
                           else { ($objClasses | Select-Object -Last 1) }

            $recentList.Add(@{
                dn          = if ($obj.ContainsKey('distinguishedname')) { $obj['distinguishedname'] } else { '' }
                objectClass = $primaryClass
                sam         = if ($obj.ContainsKey('samaccountname')) { $obj['samaccountname'] } else { '' }
                whenChanged = if ($obj.ContainsKey('whenchanged') -and $obj['whenchanged']) { $obj['whenchanged'].ToString('o') } else { $null }
                whenCreated = if ($obj.ContainsKey('whencreated') -and $obj['whencreated']) { $obj['whencreated'].ToString('o') } else { $null }
            })
        }
        $result.recentlyChanged = @($recentList)

        if (-not $Quiet) {
            Write-ProgressLine -Phase SCANNING -Message "Found $($recentList.Count) recently changed objects"
        }
    } catch {
        Write-Verbose "Failed to query recently changed objects: $_"
        $result.errors['recentlyChanged'] = $_.Exception.Message
    }

    # ── Full mode: additional data collection ──────────────────────────
    if ($ScanMode -eq 'Full') {

        # ── 4. GPO Objects ─────────────────────────────────────────────
        if (-not $Quiet) {
            Write-ProgressLine -Phase SCANNING -Message 'Collecting Group Policy Objects'
        }

        try {
            $gpoData = Get-ADGroupPolicyObjects -Connection $LdapConnection -Quiet:$Quiet
            $gpoMap = @{}
            foreach ($gpo in $gpoData.GPOs) {
                $gpoMap[$gpo.GUID] = @{
                    name          = $gpo.DisplayName
                    guid          = $gpo.GUID
                    whenChanged   = if ($gpo.WhenChanged) { $gpo.WhenChanged.ToString('o') } else { $null }
                    versionNumber = $gpo.VersionNumber
                    flags         = $gpo.Flags
                    isLinked      = $gpo.IsLinked
                    linkedTo      = @($gpo.LinkedTo | ForEach-Object {
                        @{
                            containerDN = $_.ContainerDN
                            isEnabled   = $_.IsEnabled
                            isEnforced  = $_.IsEnforced
                        }
                    })
                }
            }
            $result.gpoObjects = $gpoMap
        } catch {
            Write-Warning "Failed to collect GPO data: $_"
            $result.errors['gpoObjects'] = $_.Exception.Message
        }

        # ── 5. Sensitive ACLs ──────────────────────────────────────────
        if (-not $Quiet) {
            Write-ProgressLine -Phase SCANNING -Message 'Collecting sensitive object ACLs'
        }

        try {
            $aclData = Get-ADObjectACLs -Connection $LdapConnection -Quiet:$Quiet
            $aclMap = @{}

            foreach ($objName in $aclData.CriticalObjectACLs.Keys) {
                $objInfo = $aclData.CriticalObjectACLs[$objName]
                $aceEntries = [System.Collections.Generic.List[hashtable]]::new()
                if ($objInfo.ACEs) {
                    foreach ($ace in $objInfo.ACEs) {
                        if ($ace.AccessControlType -ne 'Allow') { continue }
                        $aceEntries.Add(@{
                            identity = $ace.IdentityReference
                            rights   = $ace.ActiveDirectoryRights
                            objectType = $ace.ObjectType
                            isInherited = $ace.IsInherited
                        })
                    }
                }
                $aclMap[$objName] = @{
                    objectDN = $objInfo.ObjectDN
                    aces     = @($aceEntries)
                }
            }

            # Also include dangerous ACEs for comparison
            $aclMap['_dangerousACEs'] = @{
                objectDN = ''
                aces     = @($aclData.DangerousACEs | ForEach-Object {
                    @{
                        identity   = $_.IdentityReference
                        rights     = $_.ActiveDirectoryRights
                        objectType = $_.ObjectType
                        objectDN   = $_.ObjectDN
                        objectName = $_.ObjectName
                    }
                })
            }

            $result.sensitiveAcls = $aclMap
        } catch {
            Write-Warning "Failed to collect ACL data: $_"
            $result.errors['sensitiveAcls'] = $_.Exception.Message
        }

        # ── 6. Certificate templates ───────────────────────────────────
        if (-not $Quiet) {
            Write-ProgressLine -Phase SCANNING -Message 'Collecting certificate template data'
        }

        try {
            $certData = Get-ADCertificateServices -Connection $LdapConnection -Quiet:$Quiet
            $certMap = @{}
            foreach ($tmpl in $certData.CertificateTemplates) {
                $certMap[$tmpl.Name] = @{
                    displayName             = $tmpl.DisplayName
                    dn                      = $tmpl.DN
                    schemaVersion           = $tmpl.SchemaVersion
                    enrolleeSuppliesSubject = $tmpl.EnrolleeSuppliesSubject
                    allowsAuthentication    = $tmpl.AllowsAuthentication
                    isPublished             = $tmpl.IsPublished
                    whenChanged             = if ($tmpl.WhenChanged) { $tmpl.WhenChanged.ToString('o') } else { $null }
                    enrollmentPermissions   = @($tmpl.EnrollmentPermissions | ForEach-Object {
                        @{ identity = $_.Identity; right = $_.Right }
                    })
                }
            }
            $result.certTemplates = $certMap
        } catch {
            Write-Warning "Failed to collect certificate template data: $_"
            $result.errors['certTemplates'] = $_.Exception.Message
        }

        # ── 7. OU Delegations ──────────────────────────────────────────
        if (-not $Quiet) {
            Write-ProgressLine -Phase SCANNING -Message 'Collecting OU delegation data'
        }

        try {
            # Reuse ACL data if already collected
            if (-not $aclData) {
                $aclData = Get-ADObjectACLs -Connection $LdapConnection -Quiet:$Quiet
            }

            $delegationMap = @{}
            foreach ($delegation in $aclData.OUDelegation) {
                $ouDN = $delegation.OUDN
                if (-not $delegationMap.ContainsKey($ouDN)) {
                    $delegationMap[$ouDN] = [System.Collections.Generic.List[hashtable]]::new()
                }
                $delegationMap[$ouDN].Add(@{
                    identity    = $delegation.IdentityReference
                    rights      = $delegation.ActiveDirectoryRights
                    objectType  = $delegation.ObjectType
                    isInherited = $delegation.IsInherited
                })
            }
            $result.delegations = $delegationMap
        } catch {
            Write-Warning "Failed to collect delegation data: $_"
            $result.errors['delegations'] = $_.Exception.Message
        }

        # ── 8. DNS Records ─────────────────────────────────────────────
        if (-not $Quiet) {
            Write-ProgressLine -Phase SCANNING -Message 'Collecting DNS records'
        }

        try {
            $dnsContainers = @(
                "CN=MicrosoftDNS,DC=DomainDnsZones,$domainDN"
                "CN=MicrosoftDNS,CN=System,$domainDN"
            )

            $dnsRecords = [System.Collections.Generic.List[hashtable]]::new()
            foreach ($dnsContainer in $dnsContainers) {
                try {
                    $dnsRoot = New-LdapSearchRoot -Connection $LdapConnection -SearchBase $dnsContainer
                    $zoneResults = Invoke-LdapQuery -SearchRoot $dnsRoot `
                        -Filter '(objectClass=dnsZone)' `
                        -Properties @('dc', 'distinguishedName') `
                        -Scope OneLevel

                    foreach ($zone in $zoneResults) {
                        $zoneName = if ($zone.ContainsKey('dc')) { $zone['dc'] } else { '' }
                        if ($zoneName -eq '..Cache' -or $zoneName -eq 'RootDNSServers') { continue }

                        $zoneDN = $zone['distinguishedname']
                        try {
                            $zoneRoot = New-LdapSearchRoot -Connection $LdapConnection -SearchBase $zoneDN
                            $recordResults = Invoke-LdapQuery -SearchRoot $zoneRoot `
                                -Filter '(objectClass=dnsNode)' `
                                -Properties @('dc', 'dnsRecord', 'whenChanged') `
                                -Scope OneLevel

                            foreach ($record in $recordResults) {
                                $recName = if ($record.ContainsKey('dc')) { $record['dc'] } else { '' }
                                $dnsRecords.Add(@{
                                    name        = $recName
                                    zone        = $zoneName
                                    whenChanged = if ($record.ContainsKey('whenchanged') -and $record['whenchanged']) {
                                        $record['whenchanged'].ToString('o')
                                    } else { $null }
                                })
                            }
                        } catch {
                            Write-Verbose "Failed to enumerate DNS records in zone $zoneName`: $_"
                        }
                    }
                } catch {
                    Write-Verbose "DNS container not accessible: $dnsContainer"
                }
            }
            $result.dnsRecords = @($dnsRecords)
        } catch {
            Write-Verbose "Failed to collect DNS data: $_"
            $result.errors['dnsRecords'] = $_.Exception.Message
        }

        # ── 9. Schema version ──────────────────────────────────────────
        if (-not $Quiet) {
            Write-ProgressLine -Phase SCANNING -Message 'Reading schema version'
        }

        try {
            $schemaRoot = New-LdapSearchRoot -Connection $LdapConnection -SearchBase $schemaDN
            $schemaResults = @(Invoke-LdapQuery -SearchRoot $schemaRoot `
                -Filter '(objectClass=dMD)' `
                -Properties @('objectVersion') `
                -Scope Base)

            if ($schemaResults.Count -gt 0 -and $schemaResults[0].ContainsKey('objectversion')) {
                $result.schemaVersion = [int]$schemaResults[0]['objectversion']
            }
        } catch {
            Write-Verbose "Failed to read schema version: $_"
            $result.errors['schemaVersion'] = $_.Exception.Message
        }
    }

    # ── Summary ────────────────────────────────────────────────────────
    if (-not $Quiet) {
        $groupCount = $result.privilegedGroups.Keys.Count
        $totalMembers = ($result.privilegedGroups.Values | ForEach-Object { $_.Count } | Measure-Object -Sum).Sum
        $summary = "Data collection complete: $groupCount privileged groups, $totalMembers members, $($result.trusts.Count) trusts"
        if ($ScanMode -eq 'Full') {
            $summary += ", $($result.gpoObjects.Keys.Count) GPOs, $($result.certTemplates.Keys.Count) cert templates"
        }
        if ($result.errors.Count -gt 0) {
            $summary += " ($($result.errors.Count) error(s))"
        }
        Write-ProgressLine -Phase SCANNING -Message $summary
    }

    return $result
}