Skip to content

Commit c6e20f7

Browse files
committed
WIP memberOfTransitive
1 parent 43b3b9d commit c6e20f7

File tree

3 files changed

+275
-73
lines changed

3 files changed

+275
-73
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
[CmdletBinding()]
2+
param (
3+
[Parameter()]
4+
[ValidateNotNullOrEmpty()]
5+
[ValidateScript({ Test-Path $_ -IsValid })]
6+
[string]
7+
$ExportDirectory = $PWD
8+
)
9+
10+
process {
11+
Start-Transcript -Path (Join-Path -Path $PWD -ChildPath "$($MyInvocation.MyCommand).RunHistory.log") -Append -Verbose:$false
12+
Export-AllUserGroupMemberships -ExportDirectory $ExportDirectory
13+
} # end process block
14+
15+
begin {
16+
# Import the functions to be used in this script.
17+
function Export-AllUserGroupMemberships {
18+
<#
19+
.SYNOPSIS
20+
Exports all users' group memberships from the current Active Directory domain.
21+
22+
.DESCRIPTION
23+
The purpose of this script is to get the members of all groups in Active Directory in a format that can be easily
24+
analyzed with tools like Excel or PowerBI. For this purpose, the script exports the data to a JSON file.
25+
26+
.PARAMETER ExportDirectory
27+
The directory to create group exports in. Defaults to the 'GroupExports' folder in the current directory.
28+
29+
.NOTES
30+
Author: Sam Erde
31+
Company: Sentinel Technologies, Inc
32+
Date: 2025-02-24
33+
34+
NOTE: Be sure to account for nested groups and circular groups!
35+
#>
36+
[CmdletBinding()]
37+
param (
38+
# The directory to create the exported file in. Defaults to the current directory.
39+
[Parameter()]
40+
[ValidateNotNullOrEmpty()]
41+
[ValidateScript({ Test-Path $_ -IsValid })]
42+
[string]
43+
$ExportDirectory = $PWD
44+
)
45+
46+
process {
47+
# Get all users in the domain and their group memberships.
48+
Write-Verbose -Message 'Getting all enabled users in the domain.'
49+
Write-Information 'Checking all users'' transitive group memberships in the domain. This will take a while...'
50+
$Users = Get-ADUser -Filter 'Enabled -eq $true' -Properties EmployeeId |
51+
Select-Object Name, DisplayName, samAccountName, userPrincipalName, EmployeeId, @{Name = 'Groups'; Expression = {
52+
Get-ADUserTransitiveGroupMembership -UserDN $_.DistinguishedName
53+
}
54+
}
55+
Write-Verbose -Message " - Found $($UserCount) users in the domain."
56+
57+
# Export the data to a JSON file.
58+
$JsonData = $Users | ConvertTo-Json
59+
$FilePath = (Join-Path -Path $ExportDirectory -ChildPath 'ADUsersGroupMemberships.json')
60+
Write-Verbose 'Exporting user group memberships to JSON file.'
61+
try {
62+
$JsonData | Out-File -FilePath $FilePath -Force
63+
Write-Verbose ' - Export complete!'
64+
} catch {
65+
throw "Unable to create the file '$FilePath'. $_"
66+
}
67+
} # process
68+
69+
# This begin block gets executed first.
70+
begin {
71+
# Start-Transcript -Path (Join-Path -Path $PWD -ChildPath "$($MyInvocation.MyCommand).RunHistory.log") -Append -Verbose:$false
72+
73+
Import-Module ActiveDirectory -Verbose:$false
74+
75+
# Check if the ExportDirectory exists; if not, create it. Quit if unable to create the directory.
76+
if (-not (Test-Path -Path $ExportDirectory -PathType Container)) {
77+
try {
78+
New-Item -Path (Split-Path -Path $ExportDirectory -Parent) -Name (Split-Path -Path $ExportDirectory -Leaf) -ItemType Directory
79+
} catch {
80+
throw "Failed to create directory '$ExportDirectory'. $_"
81+
} # end try
82+
} # end if
83+
} # begin
84+
85+
# This end block gets executed last.
86+
end {
87+
Remove-Variable ExportDirectory, FilePath, JsonData, Users -Verbose:$false -ErrorAction SilentlyContinue
88+
# Stop-Transcript -Verbose:$false
89+
} # end
90+
} # end function Export-AllUserGroupMemberships
91+
92+
function Get-ADUserTransitiveGroupMembership {
93+
<#
94+
.SYNOPSIS
95+
Get the full transitive group membership of an Active Directory user.
96+
97+
.DESCRIPTION
98+
Get the full transitive group membership of an Active Directory user by searching the global catalog. This performs
99+
a transitive LDAP query which effectively flattens the group membership hierarchy more efficiently than a recursive
100+
memberOf lookup could.
101+
102+
.PARAMETER UserDN
103+
The distinguished name of the user to search for. This is required and it accepts input from the pipeline.
104+
105+
.PARAMETER Server
106+
A global catalog domain controller to connect to. This will get a GC in the current forest if none is specified.
107+
108+
.PARAMETER Port
109+
Port to connect to the global catalog service on. Defaults to 3269 (using TLS).
110+
111+
.EXAMPLE
112+
Get-ADUser -Identity JaneDoe | Get-ADUserTransitiveGroupMembership
113+
114+
Gets the transitive group membership of the user JaneDoe (include all effective nested group memberships).
115+
116+
.EXAMPLE
117+
Get-ADUserTransitiveGroupMembership -UserDN 'CN=Jane Doe,OU=Users,DC=example,DC=com'
118+
119+
Gets the transitive group membership of the user Jane Doe (include all effective nested group memberships).
120+
121+
.NOTES
122+
Author: Sam Erde
123+
Company: Sentinel Technologies, Inc
124+
Version: 1.0.0
125+
Date: 2025-02-27
126+
#>
127+
128+
[CmdletBinding()]
129+
param (
130+
[Parameter(Mandatory, ValueFromPipeline, HelpMessage = 'The distinguished name of the user to search for.')]
131+
[string]$UserDN,
132+
133+
[Parameter(HelpMessage = 'A global catalog domain controller to connect to.')]
134+
[ValidateScript({ (Test-NetConnection -ComputerName $_ -InformationLevel Quiet -ErrorAction SilentlyContinue).PingSucceeded })]
135+
[string]$Server = ([System.DirectoryServices.ActiveDirectory.GlobalCatalog]::FindOne([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest)).Name,
136+
137+
# Port to connect to the global catalog service on.
138+
[Parameter(HelpMessage = 'Port to connect to the global catalog service on. Default is 3268, or 3269 for using TLS.')]
139+
[ValidateSet(3268, 3269)]
140+
[int]$Port = 3269
141+
)
142+
143+
begin {
144+
if ($Port -eq 3269) {
145+
$AltPort = 3268
146+
} else {
147+
$AltPort = 3269
148+
}
149+
150+
$CurrentProgressPreference = Get-Variable -Name ProgressPreference -ValueOnly
151+
Set-Variable -Name ProgressPreference 'SilentlyContinue' -Scope Global -Force -ErrorAction SilentlyContinue
152+
# Check if the global catalog server is available on the specified port.
153+
if (-not (Test-NetConnection -ComputerName $Server -Port $Port -InformationLevel Quiet -ErrorAction SilentlyContinue)) {
154+
if (-not (Test-NetConnection -ComputerName $Server -Port $AltPort -InformationLevel Quiet -ErrorAction SilentlyContinue)) {
155+
throw "Unable to connect to the global catalog server '$Server' on port '$Port' or '$AltPort.'"
156+
}
157+
}
158+
Set-Variable -Name ProgressPreference -Value $CurrentProgressPreference -Scope Global -Force -ErrorAction SilentlyContinue
159+
}
160+
161+
process {
162+
# Set the searcher parameters
163+
$Filter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:=$UserDN))"
164+
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
165+
$Searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server`:$Port")
166+
$Searcher.Filter = $Filter
167+
$Searcher.PageSize = 1000
168+
$Searcher.PropertiesToLoad.Add('DistinguishedName') | Out-Null
169+
$Results = $Searcher.FindAll()
170+
Write-Verbose "Found $($Results.Count) groups for ${UserDN}."
171+
$TransitiveMemberOfGroupDNs = foreach ($result in ($results.properties)) {
172+
$result['distinguishedname']
173+
}
174+
}
175+
176+
end {
177+
$TransitiveMemberOfGroupDNs | Sort-Object -Unique
178+
Remove-Variable Filter, TransitiveMemberOfGroupDNs, Results, Searcher, Server, Port, UserDN -ErrorAction SilentlyContinue
179+
}
180+
} # end function Get-ADUserTransitiveGroupMembership
181+
} # end begin block
182+
183+
end {
184+
Stop-Transcript -Verbose:$false
185+
Remove-Variable ExportDirectory -ErrorAction SilentlyContinue
186+
} # end end block

Active Directory/Get-ADUserTransitiveGroupMember.ps1

Lines changed: 0 additions & 73 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
function Get-ADUserTransitiveGroupMembership {
2+
<#
3+
.SYNOPSIS
4+
Get the full transitive group membership of an Active Directory user.
5+
6+
.DESCRIPTION
7+
Get the full transitive group membership of an Active Directory user by searching the global catalog. This performs
8+
a transitive LDAP query which effectively flattens the group membership hierarchy more efficiently than a recursive
9+
memberOf lookup could.
10+
11+
.PARAMETER UserDN
12+
The distinguished name of the user to search for. This is required and it accepts input from the pipeline.
13+
14+
.PARAMETER Server
15+
A global catalog domain controller to connect to. This will get a GC in the current forest if none is specified.
16+
17+
.PARAMETER Port
18+
Port to connect to the global catalog service on. Defaults to 3269 (using TLS).
19+
20+
.EXAMPLE
21+
Get-ADUser -Identity JaneDoe | Get-ADUserTransitiveGroupMembership
22+
23+
Gets the transitive group membership of the user JaneDoe (include all effective nested group memberships).
24+
25+
.EXAMPLE
26+
Get-ADUserTransitiveGroupMembership -UserDN 'CN=Jane Doe,OU=Users,DC=example,DC=com'
27+
28+
Gets the transitive group membership of the user Jane Doe (include all effective nested group memberships).
29+
30+
.NOTES
31+
Author: Sam Erde
32+
Company: Sentinel Technologies, Inc
33+
Version: 1.0.0
34+
Date: 2025-02-27
35+
#>
36+
37+
[CmdletBinding()]
38+
param (
39+
[Parameter(Mandatory, ValueFromPipeline, HelpMessage = 'The distinguished name of the user to search for.')]
40+
[string]$UserDN,
41+
42+
[Parameter(HelpMessage = 'A global catalog domain controller to connect to.')]
43+
[ValidateScript({ (Test-NetConnection -ComputerName $_ -InformationLevel Quiet -ErrorAction SilentlyContinue).PingSucceeded })]
44+
[string]$Server = ([System.DirectoryServices.ActiveDirectory.GlobalCatalog]::FindOne([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest)).Name,
45+
46+
# Port to connect to the global catalog service on.
47+
[Parameter(HelpMessage = 'Port to connect to the global catalog service on. Default is 3268, or 3269 for using TLS.')]
48+
[ValidateSet(3268, 3269)]
49+
[int]$Port = 3269
50+
)
51+
52+
begin {
53+
if ($Port -eq 3269) {
54+
$AltPort = 3268
55+
} else {
56+
$AltPort = 3269
57+
}
58+
59+
$CurrentProgressPreference = Get-Variable -Name ProgressPreference -ValueOnly
60+
Set-Variable -Name ProgressPreference 'SilentlyContinue' -Force -Scope Global -ErrorAction SilentlyContinue
61+
# Check if the global catalog server is available on the specified port.
62+
if (-not (Test-NetConnection -ComputerName $Server -Port $Port -InformationLevel Quiet -ErrorAction SilentlyContinue)) {
63+
if (-not (Test-NetConnection -ComputerName $Server -Port $AltPort -InformationLevel Quiet -ErrorAction SilentlyContinue)) {
64+
throw "Unable to connect to the global catalog server '$Server' on port '$Port' or '$AltPort.'"
65+
}
66+
}
67+
Set-Variable -Name ProgressPreference -Value $CurrentProgressPreference -Force -Scope Global -ErrorAction SilentlyContinue
68+
}
69+
70+
process {
71+
# Set the searcher parameters
72+
$Filter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:=$UserDN))"
73+
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
74+
$Searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server`:$Port")
75+
$Searcher.Filter = $Filter
76+
$Searcher.PageSize = 1000
77+
$Searcher.PropertiesToLoad.Add('DistinguishedName') | Out-Null
78+
$Results = $Searcher.FindAll()
79+
Write-Verbose "Found $($Results.Count) groups for ${UserDN}."
80+
$TransitiveMemberOfGroupDNs = foreach ($result in ($results.properties)) {
81+
$result['distinguishedname']
82+
}
83+
}
84+
85+
end {
86+
$TransitiveMemberOfGroupDNs | Sort-Object -Unique
87+
Remove-Variable Filter, TransitiveMemberOfGroupDNs, Results, Searcher, Server, Port, UserDN -ErrorAction SilentlyContinue
88+
}
89+
} # end function Get-ADUserTransitiveGroupMembership

0 commit comments

Comments
 (0)