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.