How to Configure Windows Server 2019 Active Directory Reporting
Active Directory reporting provides visibility into the state of your directory — user account status, group memberships, password policy compliance, inactive accounts, privileged access, and more. Regular AD reports are essential for security auditing, compliance requirements (SOX, HIPAA, PCI-DSS), and operational hygiene. Windows Server 2019 includes the Active Directory PowerShell module that enables comprehensive custom reporting. This guide covers generating useful AD reports using PowerShell, scheduling them, and exporting results for audit purposes.
Reporting on User Account Status
Generate a report of all user accounts, their status, and last logon information. Note that lastLogonDate is the replicated attribute (updated every 14 days), while lastLogon is DC-local. For accurate lastLogon, query all DCs:
$reportPath = "C:ADReportsUserAccounts_$(Get-Date -Format yyyyMMdd).csv"
Get-ADUser -Filter * -Properties DisplayName, SamAccountName, Enabled, `
LastLogonDate, PasswordLastSet, PasswordNeverExpires, `
LockedOut, AccountExpirationDate, Department, Title, Manager, `
Created, Modified, DistinguishedName |
Select DisplayName, SamAccountName, Enabled, LastLogonDate, `
PasswordLastSet, PasswordNeverExpires, LockedOut, `
AccountExpirationDate, Department, Title, `
@{N='Manager';E={(Get-ADUser $_.Manager -ErrorAction SilentlyContinue).SamAccountName}}, `
Created, DistinguishedName |
Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
Write-Output "User report saved: $reportPath"
Reporting on Inactive Accounts
Inactive accounts that have not logged on in 90 days are a security risk. Generate a stale account report:
$cutoffDate = (Get-Date).AddDays(-90)
$inactiveUsers = Get-ADUser -Filter {
Enabled -eq $true -and LastLogonDate -lt $cutoffDate
} -Properties LastLogonDate, Department, Created, DistinguishedName |
Select SamAccountName, LastLogonDate, Department, Created, DistinguishedName |
Sort LastLogonDate
$inactiveUsers | Export-Csv "C:ADReportsInactiveUsers_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
Write-Output "Inactive users found: $($inactiveUsers.Count)"
Find accounts that have never logged on:
Get-ADUser -Filter {Enabled -eq $true -and LastLogonDate -notlike "*"} `
-Properties Created, Department |
Select SamAccountName, Created, Department |
Export-Csv "C:ADReportsNeverLoggedIn.csv" -NoTypeInformation
Password Expiry and Policy Compliance Report
Report on users with expiring passwords, never-expiring passwords, and those who must change on next logon:
$maxPwdAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
$warningDays = 14
$passwordReport = Get-ADUser -Filter * -Properties PasswordLastSet, PasswordNeverExpires, PasswordExpired, PasswordNotRequired, Department |
Select SamAccountName, Department,
PasswordLastSet,
PasswordNeverExpires,
PasswordExpired,
PasswordNotRequired,
@{N='DaysUntilExpiry';E={
if ($_.PasswordNeverExpires) { "Never Expires" }
elseif ($_.PasswordLastSet) {
$expiry = $_.PasswordLastSet.AddDays($maxPwdAge)
[math]::Round(($expiry - (Get-Date)).TotalDays)
} else { "Unknown" }
}}
$passwordReport | Where-Object {$_.DaysUntilExpiry -is [int] -and $_.DaysUntilExpiry -lt $warningDays} |
Export-Csv "C:ADReportsExpiringPasswords_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
Privileged Account and Group Membership Report
Report on members of highly privileged groups for security auditing:
$privilegedGroups = @(
"Domain Admins",
"Enterprise Admins",
"Schema Admins",
"Group Policy Creator Owners",
"Administrators",
"Account Operators",
"Backup Operators",
"Server Operators"
)
$privReport = @()
foreach ($group in $privilegedGroups) {
$members = Get-ADGroupMember -Identity $group -Recursive -ErrorAction SilentlyContinue
foreach ($member in $members) {
$user = Get-ADUser $member.SamAccountName -Properties LastLogonDate, Department, Enabled -ErrorAction SilentlyContinue
if ($user) {
$privReport += [PSCustomObject]@{
Group = $group
SamAccountName = $user.SamAccountName
Enabled = $user.Enabled
Department = $user.Department
LastLogon = $user.LastLogonDate
}
}
}
}
$privReport | Export-Csv "C:ADReportsPrivilegedAccounts_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
Write-Output "Privileged account entries: $($privReport.Count)"
Group Membership Changes Report
Track group membership changes using Security event log events 4728, 4729, 4732, 4733, 4756, 4757. Query the PDC for recent membership changes:
$pdc = (Get-ADDomain).PDCEmulator
Get-WinEvent -ComputerName $pdc -FilterHashtable @{
LogName = 'Security'
Id = 4728, 4729, 4732, 4733, 4756, 4757
StartTime = (Get-Date).AddDays(-7)
} | ForEach-Object {
$xml = [xml]$_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
EventId = $_.Id
GroupName = $xml.Event.EventData.Data | Where-Object {$_.Name -eq "TargetUserName"} | Select-Object -ExpandProperty '#text'
ChangedUser = $xml.Event.EventData.Data | Where-Object {$_.Name -eq "SubjectUserName"} | Select-Object -ExpandProperty '#text'
Action = if ($_.Id -in 4728,4732,4756) {"Added"} else {"Removed"}
}
} | Export-Csv "C:ADReportsGroupChanges_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
Computer Account Report
Report on stale computer accounts that have not checked in with the domain for 90 days:
$cutoff = (Get-Date).AddDays(-90)
Get-ADComputer -Filter {Enabled -eq $true} `
-Properties LastLogonDate, OperatingSystem, OperatingSystemVersion, Created |
Where-Object {$_.LastLogonDate -lt $cutoff -or $_.LastLogonDate -eq $null} |
Select Name, OperatingSystem, LastLogonDate, Created, DistinguishedName |
Sort LastLogonDate |
Export-Csv "C:ADReportsStaleComputers_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
Automating Report Distribution
Create a master reporting script that generates all reports and emails them to the AD team:
$reports = Get-ChildItem "C:ADReports*$(Get-Date -Format yyyyMMdd)*.csv"
Send-MailMessage `
-To "[email protected]","[email protected]" `
-From "[email protected]" `
-Subject "Weekly AD Report - $(Get-Date -Format 'dd MMM yyyy')" `
-Body "Please find attached this week's Active Directory reports." `
-Attachments $reports.FullName `
-SmtpServer "smtp.contoso.com"
Register the master script as a weekly scheduled task:
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-File C:ADReportsGenerate-AllReports.ps1"
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At "06:00"
Register-ScheduledTask -TaskName "AD Weekly Reports" -Action $action -Trigger $trigger -RunLevel Highest -User "SYSTEM"
Regular Active Directory reporting is a cornerstone of both security operations and IT compliance. Automated weekly reports on inactive accounts, privileged access, and password policy compliance dramatically reduce the risk of account-based attacks and ensure your AD environment remains clean and auditable. Store all reports in a location accessible to your security and audit teams for at least 12 months to meet common compliance requirements.