Skip to content

Commit e88d64a

Browse files
committed
Add AD Group Audit script with comprehensive member reporting
- Created ADGroupAudit.ps1: PowerShell script to audit AD group members - Supports comma-separated group names or file input - Extracts user details: names, emails, usernames, department, title, manager, etc. - Outputs to CSV with timestamped filenames - Includes comprehensive logging and error handling - Handles non-user members (groups, computers) gracefully - Added README.md with full documentation and usage examples
1 parent bf31269 commit e88d64a

2 files changed

Lines changed: 564 additions & 0 deletions

File tree

ad-group-audit/ADGroupAudit.ps1

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# PSScriptAnalyzer suppressions - these warnings are acceptable for this interactive script
2+
# .SYNOPSIS
3+
# AD Group Member Audit Script with interactive prompts
4+
# .DESCRIPTION
5+
# This script audits Active Directory group members. Write-Host is intentionally used
6+
# for interactive user prompts and colored output, which is appropriate for this use case.
7+
param(
8+
[Parameter(Mandatory = $false,
9+
HelpMessage = "Comma-separated list of AD group names (e.g. 'Group1,Group2,Group3')")]
10+
[string]$GroupNames,
11+
12+
[Parameter(Mandatory = $false,
13+
HelpMessage = "Path to a text file containing group names (one per line)")]
14+
[string]$GroupNamesFile,
15+
16+
[Parameter(Mandatory = $false,
17+
HelpMessage = "Path to output CSV file")]
18+
[string]$OutputCsvPath = $(Join-Path -Path (Get-Location) -ChildPath ("ADGroupAudit_{0:yyyyMMdd_HHmmss}.csv" -f (Get-Date))),
19+
20+
[Parameter(Mandatory = $false,
21+
HelpMessage = "Path to log file")]
22+
[string]$LogFilePath = $(Join-Path -Path (Get-Location) -ChildPath ("ADGroupAudit_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date)))
23+
)
24+
25+
# --- Interactive prompts for required parameters if not provided ---
26+
27+
if ([string]::IsNullOrWhiteSpace($GroupNames) -and [string]::IsNullOrWhiteSpace($GroupNamesFile)) {
28+
Write-Host ""
29+
Write-Host "=== AD Group Member Audit Script ===" -ForegroundColor Cyan
30+
Write-Host ""
31+
Write-Host "Please provide the group names:" -ForegroundColor Yellow
32+
Write-Host ""
33+
Write-Host "Options:" -ForegroundColor Yellow
34+
Write-Host " 1. Enter comma-separated group names (e.g. 'Group1,Group2,Group3')" -ForegroundColor Gray
35+
Write-Host " 2. Enter a file path containing group names (one per line)" -ForegroundColor Gray
36+
Write-Host ""
37+
38+
$userInput = Read-Host "Enter group names or file path"
39+
40+
if ([string]::IsNullOrWhiteSpace($userInput)) {
41+
Write-Error "Group names or file path is required. Script cannot continue."
42+
exit 1
43+
}
44+
45+
# Check if input is a file path
46+
if (Test-Path -LiteralPath $userInput -ErrorAction SilentlyContinue) {
47+
$GroupNamesFile = $userInput
48+
Write-Host "Using file path: $GroupNamesFile" -ForegroundColor Green
49+
} else {
50+
$GroupNames = $userInput
51+
Write-Host "Using comma-separated group names: $GroupNames" -ForegroundColor Green
52+
}
53+
Write-Host ""
54+
}
55+
56+
# --- Core script starts here ---
57+
58+
# Start transcript logging
59+
try {
60+
Start-Transcript -Path $LogFilePath -Append -ErrorAction Stop
61+
} catch {
62+
Write-Warning "Failed to start transcript logging: $($_.Exception.Message)"
63+
}
64+
65+
Write-Host "Starting AD Group Member audit..."
66+
Write-Host "Output CSV : $OutputCsvPath"
67+
Write-Host "Log file : $LogFilePath"
68+
Write-Host "Start time : $(Get-Date)"
69+
Write-Host ""
70+
71+
function Write-Log {
72+
param(
73+
[string]$Message,
74+
[string]$Level = "INFO"
75+
)
76+
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
77+
$line = "[$timestamp] [$Level] $Message"
78+
Write-Host $line
79+
}
80+
81+
# Check if ActiveDirectory module is available
82+
try {
83+
Import-Module ActiveDirectory -ErrorAction Stop
84+
Write-Log "ActiveDirectory module loaded successfully"
85+
} catch {
86+
Write-Error "Failed to import ActiveDirectory module. Please ensure RSAT-AD-PowerShell is installed."
87+
Write-Log "Failed to import ActiveDirectory module: $($_.Exception.Message)" "ERROR"
88+
try {
89+
Stop-Transcript | Out-Null
90+
} catch {
91+
Write-Warning "Failed to stop transcript: $($_.Exception.Message)"
92+
}
93+
exit 1
94+
}
95+
96+
# Collect group names from either parameter or file
97+
$groupList = New-Object System.Collections.Generic.List[string]
98+
99+
if (-not [string]::IsNullOrWhiteSpace($GroupNamesFile)) {
100+
Write-Log "Reading group names from file: $GroupNamesFile"
101+
102+
if (-not (Test-Path -LiteralPath $GroupNamesFile)) {
103+
Write-Error "File not found: $GroupNamesFile"
104+
Write-Log "File not found: $GroupNamesFile" "ERROR"
105+
try {
106+
Stop-Transcript | Out-Null
107+
} catch {
108+
Write-Warning "Failed to stop transcript: $($_.Exception.Message)"
109+
}
110+
exit 1
111+
}
112+
113+
try {
114+
$fileContent = Get-Content -Path $GroupNamesFile -ErrorAction Stop
115+
foreach ($line in $fileContent) {
116+
$trimmedLine = $line.Trim()
117+
if (-not [string]::IsNullOrWhiteSpace($trimmedLine)) {
118+
$groupList.Add($trimmedLine)
119+
}
120+
}
121+
Write-Log "Loaded $($groupList.Count) group names from file"
122+
} catch {
123+
Write-Error "Failed to read file: $($_.Exception.Message)"
124+
Write-Log "Failed to read file: $($_.Exception.Message)" "ERROR"
125+
try {
126+
Stop-Transcript | Out-Null
127+
} catch {
128+
Write-Warning "Failed to stop transcript: $($_.Exception.Message)"
129+
}
130+
exit 1
131+
}
132+
} elseif (-not [string]::IsNullOrWhiteSpace($GroupNames)) {
133+
Write-Log "Parsing comma-separated group names"
134+
135+
$groups = $GroupNames -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
136+
137+
foreach ($group in $groups) {
138+
$groupList.Add($group)
139+
}
140+
141+
Write-Log "Loaded $($groupList.Count) group names from parameter"
142+
} else {
143+
Write-Error "No group names provided. Please use -GroupNames or -GroupNamesFile parameter."
144+
Write-Log "No group names provided" "ERROR"
145+
try {
146+
Stop-Transcript | Out-Null
147+
} catch {
148+
Write-Warning "Failed to stop transcript: $($_.Exception.Message)"
149+
}
150+
exit 1
151+
}
152+
153+
if ($groupList.Count -eq 0) {
154+
Write-Error "No valid group names found."
155+
Write-Log "No valid group names found" "ERROR"
156+
try {
157+
Stop-Transcript | Out-Null
158+
} catch {
159+
Write-Warning "Failed to stop transcript: $($_.Exception.Message)"
160+
}
161+
exit 1
162+
}
163+
164+
Write-Log "Processing $($groupList.Count) group(s)..."
165+
166+
$errors = New-Object System.Collections.Generic.List[psobject]
167+
$idCounter = 0
168+
$script:CsvInitialized = $false
169+
170+
# Helper: write one row to CSV (streaming, header once)
171+
function Write-UserRow {
172+
param(
173+
[pscustomobject]$Row,
174+
[string]$CsvPath
175+
)
176+
177+
if (-not $script:CsvInitialized) {
178+
$Row | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
179+
$script:CsvInitialized = $true
180+
} else {
181+
$Row | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 -Append
182+
}
183+
}
184+
185+
# Process each group
186+
$groupIndex = 0
187+
foreach ($groupName in $groupList) {
188+
$groupIndex++
189+
Write-Progress -Activity "Auditing AD Groups" -Status "Processing group: $groupName ($groupIndex of $($groupList.Count))" -PercentComplete (($groupIndex / $groupList.Count) * 100)
190+
191+
Write-Log "Processing group: $groupName"
192+
193+
try {
194+
# Verify group exists
195+
$group = Get-ADGroup -Identity $groupName -ErrorAction Stop
196+
Write-Log "Found group: $groupName (DistinguishedName: $($group.DistinguishedName))"
197+
198+
# Get all members of the group
199+
$members = Get-ADGroupMember -Identity $groupName -ErrorAction Stop
200+
201+
if ($members.Count -eq 0) {
202+
Write-Log "Group '$groupName' has no members" "WARN"
203+
continue
204+
}
205+
206+
Write-Log "Found $($members.Count) member(s) in group '$groupName'"
207+
208+
# Process each member
209+
foreach ($member in $members) {
210+
try {
211+
# Check if member has SamAccountName (users do, groups/computers might not)
212+
$memberIdentity = $null
213+
if ($member.SamAccountName) {
214+
$memberIdentity = $member.SamAccountName
215+
} elseif ($member.DistinguishedName) {
216+
$memberIdentity = $member.DistinguishedName
217+
} else {
218+
Write-Log "Member object has no identifiable attribute (SamAccountName or DN): $($member | ConvertTo-Json -Compress)" "WARN"
219+
continue
220+
}
221+
222+
# Get detailed user information
223+
$user = Get-ADUser -Identity $memberIdentity -Properties DisplayName, EmailAddress, UserPrincipalName, Enabled, Department, Title, Office, Manager, LastLogonDate, Created, Modified -ErrorAction Stop
224+
225+
$idCounter++
226+
227+
# Get manager name if available
228+
$managerName = $null
229+
if ($user.Manager) {
230+
try {
231+
$manager = Get-ADUser -Identity $user.Manager -Properties DisplayName -ErrorAction SilentlyContinue
232+
if ($manager) {
233+
$managerName = $manager.DisplayName
234+
}
235+
} catch {
236+
# Manager lookup failed, leave as null
237+
}
238+
}
239+
240+
$row = [pscustomobject]@{
241+
ID = $idCounter
242+
GroupName = $groupName
243+
GroupDN = $group.DistinguishedName
244+
SamAccountName = $user.SamAccountName
245+
DisplayName = $user.DisplayName
246+
UserPrincipalName = $user.UserPrincipalName
247+
EmailAddress = $user.EmailAddress
248+
Enabled = $user.Enabled
249+
Department = $user.Department
250+
Title = $user.Title
251+
Office = $user.Office
252+
Manager = $managerName
253+
LastLogonDate = $user.LastLogonDate
254+
Created = $user.Created
255+
Modified = $user.Modified
256+
MemberDN = $member.DistinguishedName
257+
MemberObjectClass = if ($member.objectClass) { ($member.objectClass -join ',') } else { "User" }
258+
AuditDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
259+
}
260+
261+
Write-UserRow -Row $row -CsvPath $OutputCsvPath
262+
263+
} catch {
264+
# Member might not be a user (could be a group or computer)
265+
$memberSamAccount = if ($member.SamAccountName) { $member.SamAccountName } else { "N/A" }
266+
Write-Log "Member '$memberSamAccount' (ObjectClass: $($member.objectClass)) is not a user account or could not be retrieved: $($_.Exception.Message)" "WARN"
267+
268+
# Still record the member even if we can't get full user details
269+
$idCounter++
270+
$row = [pscustomobject]@{
271+
ID = $idCounter
272+
GroupName = $groupName
273+
GroupDN = $group.DistinguishedName
274+
SamAccountName = $memberSamAccount
275+
DisplayName = $null
276+
UserPrincipalName = $null
277+
EmailAddress = $null
278+
Enabled = $null
279+
Department = $null
280+
Title = $null
281+
Office = $null
282+
Manager = $null
283+
LastLogonDate = $null
284+
Created = $null
285+
Modified = $null
286+
MemberDN = if ($member.DistinguishedName) { $member.DistinguishedName } else { if ($member.Name) { $member.Name } else { "N/A" } }
287+
MemberObjectClass = if ($member.objectClass) { ($member.objectClass -join ',') } else { "Unknown" }
288+
AuditDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
289+
}
290+
291+
Write-UserRow -Row $row -CsvPath $OutputCsvPath
292+
}
293+
}
294+
295+
} catch {
296+
$errObj = [pscustomobject]@{
297+
GroupName = $groupName
298+
Error = $_.Exception.Message
299+
TimeStamp = Get-Date
300+
}
301+
$errors.Add($errObj) | Out-Null
302+
Write-Log "Failed to process group '$groupName': $($_.Exception.Message)" "ERROR"
303+
}
304+
}
305+
306+
Write-Progress -Activity "Auditing AD Groups" -Completed
307+
308+
Write-Log "Finished collecting group members. CSV written to '$OutputCsvPath'"
309+
310+
if ($errors.Count -gt 0) {
311+
$errorCsvPath = [System.IO.Path]::ChangeExtension($OutputCsvPath, ".errors.csv")
312+
try {
313+
$errors | Export-Csv -Path $errorCsvPath -NoTypeInformation -Encoding UTF8
314+
Write-Log "Encountered $($errors.Count) errors. Details saved to '$errorCsvPath'" "WARN"
315+
} catch {
316+
Write-Log "Failed to export error details: $($_.Exception.Message)" "ERROR"
317+
}
318+
} else {
319+
Write-Log "No errors encountered."
320+
}
321+
322+
Write-Host ""
323+
Write-Host "End time : $(Get-Date)"
324+
Write-Host "Total rows : $idCounter"
325+
Write-Host "Groups processed: $($groupList.Count)"
326+
Write-Host "Audit complete."
327+
328+
try {
329+
Stop-Transcript | Out-Null
330+
} catch {
331+
Write-Warning "Failed to stop transcript: $($_.Exception.Message)"
332+
}
333+

0 commit comments

Comments
 (0)