Public/Get-sqmADMemberGroups.ps1
|
<# .SYNOPSIS Finds all Active Directory groups that contain a specified user, group, or computer. .DESCRIPTION Inverse operation to Get-sqmADGroupMembers. Lists all groups (direct and nested) that contain the specified member. .PARAMETER Identity Identity of the user, group, or computer. Can be: SamAccountName, UPN, or DistinguishedName Pipeline-capable. .PARAMETER Domain Optional: AD domain .PARAMETER Depth Maximum nesting depth for group expansion (default: 2) .PARAMETER OutputPath Optional: Output directory for TXT/CSV reports Default: C:\System\WinSrvLog\MSSQL .OUTPUTS PSCustomObject with Identity, GroupName, GroupCount, Groups[], Depth, TxtFile, CsvFile, Status .EXAMPLE Get-sqmADMemberGroups -Identity "john.doe" -Depth 2 .NOTES Author: sqmSQLTool Inverse of Get-sqmADGroupMembers #> function Get-sqmADMemberGroups { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string[]]$Identity, [Parameter(Mandatory = $false)] [string]$Domain, [Parameter(Mandatory = $false)] [ValidateRange(0, 10)] [int]$Depth = 2, [Parameter(Mandatory = $false)] [string]$OutputPath = "C:\System\WinSrvLog\MSSQL" ) begin { $functionName = $MyInvocation.MyCommand.Name $allResults = [System.Collections.Generic.List[PSCustomObject]]::new() try { $null = [ADSI]"LDAP://RootDSE" Invoke-sqmLogging -Message "ADSI connection successful." -FunctionName $functionName -Level "INFO" } catch { $errMsg = "ADSI connection failed - no Domain Controller reachable." Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" throw $errMsg } Invoke-sqmLogging -Message "Starting $functionName with Depth=$Depth" -FunctionName $functionName -Level "INFO" } process { foreach ($member in $Identity) { $parentGroups = [System.Collections.Generic.List[PSCustomObject]]::new() $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $datestamp = Get-Date -Format 'yyyy-MM-dd' try { $targetDomain = $Domain if (-not $targetDomain) { try { $targetDomain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name } catch { $targetDomain = $env:USERDNSDOMAIN } } $cleanIdentity = $member -replace '^[^\\]*\\', '' Invoke-sqmLogging -Message "[$cleanIdentity] Domain: $targetDomain, Depth: $Depth" -FunctionName $functionName -Level "VERBOSE" # Helper function for recursive group lookup function Find-ParentGroups { param( [string]$MemberIdentity, [int]$CurrentDepth, [int]$MaxDepth, [hashtable]$Visited ) if ($Visited.ContainsKey($MemberIdentity.ToLower())) { return @() } $Visited[$MemberIdentity.ToLower()] = $true $foundGroups = @() try { if (Get-Module -ListAvailable -Name ActiveDirectory -ErrorAction SilentlyContinue) { $null = Import-Module ActiveDirectory -ErrorAction Stop # Get immediate parent groups $memberGroups = Get-ADPrincipalGroupMembership -Identity $MemberIdentity -ErrorAction Stop foreach ($group in $memberGroups) { # Skip Domain Users - it's everyone if ($group.Name -eq 'Domain Users') { continue } $groupObj = [PSCustomObject]@{ SamAccountName = $group.SamAccountName DisplayName = $group.Name GroupScope = if ($group | Get-Member -Name GroupScope) { $group.GroupScope } else { 'Unknown' } Depth = $CurrentDepth } $foundGroups += $groupObj # Recurse if not at max depth if ($CurrentDepth -lt $MaxDepth) { $parentOfParent = Find-ParentGroups -MemberIdentity $group.SamAccountName -CurrentDepth ($CurrentDepth + 1) -MaxDepth $MaxDepth -Visited $Visited $foundGroups += $parentOfParent } } } } catch { # LDAP fallback if AD module fails try { $root = [ADSI]"LDAP://$targetDomain/RootDSE" $searcher = [System.DirectoryServices.DirectorySearcher]::new() $searcher.SearchRoot = [ADSI]("LDAP://" + $root.defaultNamingContext[0]) $searcher.Filter = "(&(|(sAMAccountName=$MemberIdentity)(userPrincipalName=$MemberIdentity)))" $result = $searcher.FindOne() if ($result) { $memberDN = $result.Properties['distinguishedName'][0] # Query for groups containing this member $groupSearcher = [System.DirectoryServices.DirectorySearcher]::new() $groupSearcher.SearchRoot = [ADSI]("LDAP://" + $root.defaultNamingContext[0]) $groupSearcher.Filter = "(&(objectClass=group)(member=$memberDN))" $groupResults = $groupSearcher.FindAll() foreach ($groupResult in $groupResults) { try { $groupEntry = $groupResult.GetDirectoryEntry() $sam = $groupEntry.psbase.InvokeGet("sAMAccountName") $disp = $groupEntry.psbase.InvokeGet("displayName") $scope = $groupEntry.psbase.InvokeGet("groupScope") if ($sam -ne 'Domain Users') { $groupObj = [PSCustomObject]@{ SamAccountName = $sam DisplayName = $disp GroupScope = $scope Depth = $CurrentDepth } $foundGroups += $groupObj # Recurse if not at max depth if ($CurrentDepth -lt $MaxDepth) { $parentOfParent = Find-ParentGroups -MemberIdentity $sam -CurrentDepth ($CurrentDepth + 1) -MaxDepth $MaxDepth -Visited $Visited $foundGroups += $parentOfParent } } } catch { } } } } catch { } } return $foundGroups } # Start lookup $allGroups = Find-ParentGroups -MemberIdentity $cleanIdentity -CurrentDepth 0 -MaxDepth $Depth -Visited @{} $parentGroups = $allGroups | Sort-Object -Property SamAccountName -Unique # Write reports $txtFile = $null $csvFile = $null if ($PSCmdlet.ShouldProcess($cleanIdentity, "Create report")) { if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction Stop | Out-Null } $safeIdentity = $cleanIdentity -replace '[\\/:*?"<>|]', '_' $txtFile = Join-Path $OutputPath "ADMemberGroups_${safeIdentity}_Depth${Depth}_${datestamp}.txt" $csvFile = Join-Path $OutputPath "ADMemberGroups_${safeIdentity}_Depth${Depth}_${datestamp}.csv" $lines = @( "# sqmSQLTool - www.powershelldba.de" "# ================================================================" "# AD Member Groups Report" "# Member : $cleanIdentity" "# Domain : $targetDomain" "# Depth : $Depth" "# Created : $timestamp" "# Groups : $($parentGroups.Count)" "# ================================================================" "" ("{0,-30} {1,-35} {2,-12} {3}" -f 'GroupName', 'DisplayName', 'Scope', 'Level') ("-" * 95) ) foreach ($group in $parentGroups) { $lines += ("{0,-30} {1,-35} {2,-12} {3}" -f $group.SamAccountName, $group.DisplayName, $group.GroupScope, $group.Depth) } $lines | Out-File -FilePath $txtFile -Encoding UTF8 -Force $parentGroups | Export-Csv -Path $csvFile -Encoding UTF8 -NoTypeInformation -Force Invoke-sqmLogging -Message "[$cleanIdentity] Report: $txtFile" -FunctionName $functionName -Level "INFO" } $allResults.Add([PSCustomObject]@{ Identity = $cleanIdentity Domain = $targetDomain Depth = $Depth GroupCount = $parentGroups.Count Groups = $parentGroups Timestamp = $timestamp TxtFile = $txtFile CsvFile = $csvFile Status = if ($parentGroups.Count -gt 0) { 'OK' } else { 'NoGroups' } }) Invoke-sqmLogging -Message "[$cleanIdentity] $($parentGroups.Count) Groups found with Depth=$Depth" -FunctionName $functionName -Level "VERBOSE" } catch { $errMsg = "Error processing member '$member': $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" $allResults.Add([PSCustomObject]@{ Identity = $member Domain = $Domain Depth = $Depth GroupCount = 0 Groups = $null Timestamp = $timestamp TxtFile = $null CsvFile = $null Status = 'Error' Message = $errMsg }) } } } end { Invoke-sqmLogging -Message "$functionName completed. $($allResults.Count) members processed." -FunctionName $functionName -Level "INFO" return $allResults } } |