Public/Find-sqmADUser.ps1

<#
.SYNOPSIS
    Durchsucht Active Directory nach Benutzerkonten anhand eines Namensmusters.
 
.DESCRIPTION
    Sucht AD-Benutzerkonten, deren SamAccountName einem Wildcard-Muster entspricht,
    z.B. alle Service-Accounts mit dem Praefix "so_".
 
    Verwendet Wildcard-Syntax wie bei Get-ADUser -Filter bzw. dem PowerShell
    -like-Operator: '*' fuer beliebig viele Zeichen, '?' fuer genau ein Zeichen.
    Beispiel: 'so_*' findet 'so_backup', 'so_monitoring', 'so_svc01', ...
 
    Methoden:
    1. Get-ADUser -Filter (falls ActiveDirectory-Modul verfuegbar) - liefert die meisten Details
    2. LDAP direct query via DirectorySearcher (Fallback, kein Modul erforderlich) -
       unterstuetzt nur '*' als Wildcard, '?' wird dabei ebenfalls als '*' behandelt.
 
.PARAMETER Pattern
    Wildcard-Muster fuer den SamAccountName. Pipeline-faehig.
    Beispiel: 'so_*', 'svc-sql??', '*admin*'
 
.PARAMETER Domain
    Optional: AD-Domain (z.B. "contoso.com"). Wenn nicht angegeben, wird die
    aktuelle Domain des ausfuehrenden Benutzers verwendet.
 
.PARAMETER SearchBase
    Optional: Expliziter LDAP-Suchpfad (DistinguishedName einer OU), um die Suche
    einzuschraenken. Beispiel: 'OU=ServiceAccounts,DC=contoso,DC=com'
 
.PARAMETER DomainController
    Optional: Ziel-DC. Wird nur im RSAT-Pfad (Get-ADUser) verwendet.
 
.PARAMETER EnableException
    Loest bei Fehlern eine Exception aus, statt nur eine Warnung/Write-Error auszugeben.
 
.OUTPUTS
    PSCustomObject mit: SamAccountName, DisplayName, EmailAddress, Enabled,
    DistinguishedName, LastLogonDate, Domain, Source ('RSAT' oder 'LDAP')
 
.EXAMPLE
    Find-sqmADUser -Pattern 'so_*'
 
    Findet alle Konten, deren SamAccountName mit "so_" beginnt.
 
.EXAMPLE
    Find-sqmADUser -Pattern 'svc-*' -Domain 'contoso.com'
 
.EXAMPLE
    Find-sqmADUser -Pattern '*admin*' -SearchBase 'OU=Admins,DC=contoso,DC=com'
 
.EXAMPLE
    'so_*', 'svc-*' | Find-sqmADUser | Where-Object Enabled -eq $false
 
.NOTES
    Author: sqmSQLTool
    Benoetigt Lesezugriff auf Active Directory. Kein ActiveDirectory-Modul zwingend
    erforderlich (LDAP-Fallback via ADSI/DirectorySearcher).
#>

function Find-sqmADUser
{
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Pattern,

        [Parameter(Mandatory = $false)]
        [string]$Domain,

        [Parameter(Mandatory = $false)]
        [string]$SearchBase,

        [Parameter(Mandatory = $false)]
        [string]$DomainController,

        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )

    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        $allResults = [System.Collections.Generic.List[PSCustomObject]]::new()

        # ── Domain ermitteln (einmalig) ──────────────────────────────────
        $targetDomain = $Domain
        if ([string]::IsNullOrWhiteSpace($targetDomain))
        {
            try
            {
                $targetDomain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name
            }
            catch
            {
                $targetDomain = $env:USERDNSDOMAIN
            }
        }

        # ── RSAT-Verfuegbarkeit einmalig pruefen ─────────────────────────
        $useRSAT = $false
        if (Get-Module -Name ActiveDirectory -ListAvailable -ErrorAction SilentlyContinue)
        {
            try
            {
                Import-Module ActiveDirectory -ErrorAction Stop
                $useRSAT = $true
                Invoke-sqmLogging -Message "ActiveDirectory-Modul geladen (RSAT-Pfad aktiv)." -FunctionName $functionName -Level 'INFO'
            }
            catch
            {
                Invoke-sqmLogging -Message "ActiveDirectory-Modul nicht ladbar: $_ - Fallback auf LDAP." -FunctionName $functionName -Level 'VERBOSE'
            }
        }
        else
        {
            Invoke-sqmLogging -Message "ActiveDirectory-Modul nicht installiert - Fallback auf LDAP." -FunctionName $functionName -Level 'VERBOSE'
        }

        Invoke-sqmLogging -Message "Starte $functionName | Domain: $targetDomain" -FunctionName $functionName -Level 'INFO'
    }

    process
    {
        foreach ($pat in $Pattern)
        {
            Invoke-sqmLogging -Message "Suche AD-Benutzer nach Muster '$pat'." -FunctionName $functionName -Level 'VERBOSE'

            #region --- RSAT-Pfad ---
            if ($useRSAT)
            {
                try
                {
                    $params = @{
                        Filter      = "SamAccountName -like '$pat'"
                        Properties  = @('DisplayName', 'EmailAddress', 'Enabled', 'LastLogonDate')
                        ErrorAction = 'Stop'
                    }
                    if ($SearchBase) { $params['SearchBase'] = $SearchBase }
                    if ($DomainController) { $params['Server'] = $DomainController }

                    $adUsers = @(Get-ADUser @params)

                    foreach ($u in $adUsers)
                    {
                        $allResults.Add([PSCustomObject]@{
                                SamAccountName    = $u.SamAccountName
                                DisplayName       = $u.DisplayName
                                EmailAddress      = $u.EmailAddress
                                Enabled           = $u.Enabled
                                DistinguishedName = $u.DistinguishedName
                                LastLogonDate     = $u.LastLogonDate
                                Domain            = $targetDomain
                                Source            = 'RSAT'
                            })
                    }

                    Invoke-sqmLogging -Message "[$pat] $($adUsers.Count) Treffer via Get-ADUser." -FunctionName $functionName -Level 'INFO'
                    continue
                }
                catch
                {
                    Invoke-sqmLogging -Message "[$pat] RSAT-Fehler: $_ - Fallback auf LDAP." -FunctionName $functionName -Level 'WARNING'
                }
            }
            #endregion

            #region --- LDAP-Fallback ---
            try
            {
                $root = if ($SearchBase)
                {
                    [System.DirectoryServices.DirectoryEntry]::new("LDAP://$SearchBase")
                }
                else
                {
                    $domainDN = 'DC=' + ($targetDomain -split '\.' -join ',DC=')
                    [System.DirectoryServices.DirectoryEntry]::new("LDAP://$domainDN")
                }

                # LDAP kennt nur '*' als Wildcard - '?' wird ebenfalls auf '*' abgebildet
                $ldapPattern = $pat -replace '\?', '*'

                $searcher = [System.DirectoryServices.DirectorySearcher]::new($root)
                $searcher.Filter = "(&(objectClass=user)(objectCategory=person)(sAMAccountName=$ldapPattern))"
                $searcher.PageSize = 1000
                $searcher.PropertiesToLoad.AddRange([string[]]@(
                        'sAMAccountName', 'displayName', 'mail', 'userAccountControl',
                        'distinguishedName', 'lastLogonTimestamp'
                    ))

                $results = $searcher.FindAll()

                foreach ($r in $results)
                {
                    $uac = if ($r.Properties['useraccountcontrol'].Count -gt 0) { [int]$r.Properties['useraccountcontrol'][0] } else { 0 }
                    $enabled = -not [bool]($uac -band 0x0002)

                    $lastLogon = $null
                    if ($r.Properties['lastlogontimestamp'].Count -gt 0)
                    {
                        $llRaw = [long]$r.Properties['lastlogontimestamp'][0]
                        if ($llRaw -gt 0) { $lastLogon = [datetime]::FromFileTime($llRaw) }
                    }

                    $allResults.Add([PSCustomObject]@{
                            SamAccountName    = $r.Properties['samaccountname'][0]
                            DisplayName       = if ($r.Properties['displayname'].Count -gt 0) { $r.Properties['displayname'][0] } else { '' }
                            EmailAddress      = if ($r.Properties['mail'].Count -gt 0) { $r.Properties['mail'][0] } else { '' }
                            Enabled           = $enabled
                            DistinguishedName = $r.Properties['distinguishedname'][0]
                            LastLogonDate     = $lastLogon
                            Domain            = $targetDomain
                            Source            = 'LDAP'
                        })
                }

                Invoke-sqmLogging -Message "[$pat] $($results.Count) Treffer via LDAP." -FunctionName $functionName -Level 'INFO'
                $results.Dispose()
            }
            catch
            {
                $errMsg = "Fehler bei Muster '$pat': $($_.Exception.Message)"
                Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level 'ERROR'
                if ($EnableException) { throw }
                Write-Error $errMsg
            }
            #endregion
        }
    }

    end
    {
        Invoke-sqmLogging -Message "$functionName abgeschlossen. $($allResults.Count) Benutzer gefunden." -FunctionName $functionName -Level 'INFO'
        return $allResults.ToArray()
    }
}