Emergency server help: get in touch

Export AD Users to CSV: PowerShell Script with Last Logon

Free PowerShell script that exports Active Directory users to CSV or HTML with last logon, password age, manager, department and OU.

Version
1.0.0
Last updated
October 6, 2026
Language
PowerShell
Tested on
Run on 6 Oct 2026 on a Windows Server 2025 DC (build 26100.33438, Windows PowerShell 5.1) in our lab domain with a Windows 11 Pro member; syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.
License
MIT
Pricing
Free

Short answer: run .\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv on a machine with the ActiveDirectory module. You get one row per user with name, UPN, sAMAccountName, enabled state, last logon date, password last set, PasswordNeverExpires, manager, department, title, mail, created date and OU. Add -Enabled or -Disabled to filter, -SearchBase to limit it to one OU, -Properties for extra attributes and -ExportHtml for a page you can mail to a manager.

We ran this script on 6 October 2026 on a Windows Server 2025 Standard domain controller (build 26100.33438, September 2026 update, Windows PowerShell 5.1) and a Windows 11 Pro member PC in our lab domain contoso.com. The example output below is from that run; the lab domain is small and new, so your numbers will be bigger. It was also syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.

What it does

Most “export AD users” requests are the same: HR wants a list with managers and departments, an auditor wants enabled accounts with password ages, or you need a clean CSV before a migration. Get-ADUser -Filter * | Export-Csv gets you halfway, but the raw attributes come out as file-time numbers and full distinguished names that nobody outside IT can read.

This script does the conversions for you. It is read-only: it only queries Active Directory and writes the files you ask for.

  • lastLogonTimestamp and pwdLastSet are turned into normal dates (empty if never set).
  • PasswordNeverExpires is read from the userAccountControl flag 0x10000 (DONT_EXPIRE_PASSWORD).
  • The manager is shown as a name (“Alice Smith”), not a DN, and the OU column shows the container the account lives in.
  • Enabled/disabled filtering is done by the domain controller with an LDAP bitwise filter, so it is fast on large domains.
  • Extra attributes (employeeID, physicalDeliveryOfficeName, extensionAttribute1 …) become extra columns; multi-valued ones are joined with “; “.
  • Output to CSV (UTF-8, opens cleanly in Excel), a simple HTML table, the pipeline (-PassThru) or the screen.

Requirements

  • Windows PowerShell 5.1 or PowerShell 7 on Windows.
  • The ActiveDirectory module (RSAT AD DS tools, or run on a domain controller).
  • Read access to the user objects. Any domain user can read these attributes by default unless you have tightened permissions.
  • No admin rights needed for the export itself, only write access to the folder you export to.

Download and first run

  1. Copy the script from the box on this page (or use the download button) and save it as C:\Scripts\Export-ADUserReport.ps1.
  2. If you downloaded the file, remove the “downloaded from the internet” mark so the execution policy lets it run: Unblock-File C:\Scripts\Export-ADUserReport.ps1.
  3. Run it on a domain-joined machine that has the ActiveDirectory module: a domain controller, a management server, or a Windows 11 PC with RSAT (see Install RSAT on Windows 11).
  4. Read the built-in help once, then run it with no options to see the result on screen before you export anything.
cd C:\Scripts
Get-Help .\Export-ADUserReport.ps1 -Full
.\Export-ADUserReport.ps1 -Enabled
.\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv

Options

ParameterWhat it doesDefault
-SearchBaseDN of the OU or container to search, e.g. OU=Staff,DC=contoso,DC=comWhole domain
-SearchScopeBase, OneLevel or SubtreeSubtree
-EnabledOnly enabled accountsOff
-DisabledOnly disabled accounts (cannot be combined with -Enabled)Off
-PropertiesExtra LDAP attribute names to add as columnsNone
-ServerDomain controller or domain to queryA DC of your domain
-CredentialAlternate credentials for the queryCurrent user
-ExportCsvWrite a CSV fileNot written
-ExportHtmlWrite an HTML tableNot written
-PassThruSend the row objects down the pipelineOff

Usage examples

# Everyone, to CSV
.\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv

# Enabled users in one OU, as an HTML page for a department head
.\Export-ADUserReport.ps1 -SearchBase "OU=Sales,DC=contoso,DC=com" -Enabled -ExportHtml C:\Reports\sales.html

# Disabled accounts with employee ID and description, for an HR reconciliation
.\Export-ADUserReport.ps1 -Disabled -Properties employeeID,description -ExportCsv C:\Reports\disabled.csv

# Enabled users with no manager set
.\Export-ADUserReport.ps1 -Enabled -PassThru | Where-Object { -not $_.Manager } | Format-Table Name, Department

# Passwords that never expire, oldest first
.\Export-ADUserReport.ps1 -Enabled -PassThru | Where-Object PasswordNeverExpires | Sort-Object PasswordLastSet

CSV columns

We have not published sample output because the script has not run against a real domain yet. These are the columns you get, in order:

ColumnSourceNotes
Name, DisplayNamename, displayName
SamAccountName, UserPrincipalNameDefault Get-ADUser propertiesPre-Windows 2000 logon name and UPN
EnabledGet-ADUser EnabledTrue / False
LastLogonDatelastLogonTimestampCan be 9 to 14 days behind; empty if never logged on
PasswordLastSetpwdLastSetEmpty if 0 (user must change password at next logon)
PasswordNeverExpiresuserAccountControl 0x10000True / False
ManagermanagerName taken from the manager’s DN
Department, Title, Maildepartment, title, mail
CreatedwhenCreated
OUParent of the DNe.g. OU=Staff,DC=contoso,DC=com
DistinguishedNameDNUseful as a unique key
(your extras)-PropertiesOne column per attribute, in the order given

Example output

Run against the lab OU with -SearchBase 'OU=Lab,DC=contoso,DC=com' -ExportCsv C:\srvs-lab\out\ad-users.csv -ExportHtml C:\srvs-lab\out\ad-users.html. First three rows of the CSV:

8 user(s) written to C:\srvs-lab\out\ad-users.csv
HTML report written to C:\srvs-lab\out\ad-users.html

Name           SamAccountName UserPrincipalName      Enabled LastLogonDate PasswordLastSet      PasswordNeverExpires Manager     Department
----           -------------- -----------------      ------- ------------- ---------------      -------------------- -------     ----------
Alice Jones    alice          alice@contoso.com      True                  10/6/2026 2:17:12 PM False                            IT
Backup Service svc-backup     svc-backup@contoso.com True                  10/6/2026 2:17:13 PM False                            IT
Bob Smith      bob            bob@contoso.com        True                  10/6/2026 2:17:12 PM False                Alice Jones IT

LastLogonDate is empty for accounts that have never signed in, which is normal for the new test accounts.

Schedule it

Run it as a scheduled task so the report is waiting for you. A group managed service account (gMSA) is the cleanest identity for this: no password to store and nothing to expire. See our gMSA guide to create one and allow this server to retrieve its password.

$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Export-ADUserReport.ps1 -Enabled -ExportCsv C:\Reports\ad-users.csv'
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 7am
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\gmsa-adreport$' -LogonType Password
Register-ScheduledTask -TaskName 'AD user export' -Action $action -Trigger $trigger -Principal $principal

The file is overwritten on every run. If you need history, point the task at a small wrapper script that adds the date to the file name, or copy the file away after each run.

How it works

  1. Checks that the ActiveDirectory module is installed and loads it.
  2. Builds an LDAP filter: (&(objectCategory=person)(objectClass=user)), plus (!(userAccountControl:1.2.840.113556.1.4.803:=2)) for enabled or (userAccountControl:1.2.840.113556.1.4.803:=2) for disabled accounts. The long number is the LDAP “bitwise AND” matching rule; bit 2 is ACCOUNTDISABLE.
  3. Calls Get-ADUser -LDAPFilter ... -Properties ... with a page size of 500, asking only for the attributes it needs, which keeps the query light.
  4. Converts the file-time values with [DateTime]::FromFileTime(), treating 0 and the “never” value as empty.
  5. Writes the rows to the formats you chose. Nothing is written back to AD.

Limitations

  • LastLogonDate is approximate. lastLogonTimestamp is replicated but, with default settings, lags the real last logon by 9 to 14 days. That is fine for finding stale accounts; it is not proof of when someone last signed in. The exact per-DC lastLogon value is not replicated, and this script does not query every DC for it.
  • One domain at a time. Use -Server with another domain’s name to export it separately.
  • Contacts and inetOrgPerson. The filter returns user objects of category person. Contacts are not included.
  • Large exports. 50,000 users with many extra attributes produce a big file; filter with -SearchBase if Excel struggles.
  • Not yet run against a live domain (see the note at the top).

Official documentation: Get-ADUser (Microsoft Learn) · UserAccountControl flags · The lastLogonTimestamp attribute (Microsoft AskDS)

Related: Get-ADUser PowerShell Examples: 25 Queries for Active Directory · Find Inactive AD Users and Computers: PowerShell Cleanup in 5 Steps · Bulk Create AD Users from CSV: PowerShell Script in 7 Steps · AD Password Expiry Email: Reliable PowerShell Reminder Script in 6 Steps · Install RSAT on Windows 11: 5 Methods, Including Offline

See also: Locked Out AD Users Report: PowerShell Script with Lockout Source · AD Privileged Group Report: Domain Admins and adminCount Audit · AD Nested Group Membership: PowerShell Tree with Loop Detection · Inactive AD Accounts Report: PowerShell Script with Safe Disable · AD Health Check Report: dcdiag and repadmin PowerShell Script

The script

Export-ADUserReport.ps1Download
# Export AD Users to CSV: PowerShell Script with Last Logon (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/export-ad-users-csv/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
    Export Active Directory users with the attributes admins actually ask for, to CSV and/or HTML.

.DESCRIPTION
    Read-only. Queries user objects with Get-ADUser and writes one row per user:
    Name, DisplayName, SamAccountName, UserPrincipalName, Enabled, LastLogonDate (from lastLogonTimestamp),
    PasswordLastSet (from pwdLastSet), PasswordNeverExpires (userAccountControl flag 0x10000), Manager,
    Department, Title, Mail, Created (whenCreated), OU (parent container) and DistinguishedName.
    Any extra LDAP attributes passed with -Properties are added as columns after those.

    lastLogonTimestamp is replicated, but by design it can be 9-14 days behind the real last logon.
    Use it to spot stale accounts, not to prove when someone last signed in.

.PARAMETER SearchBase
    Distinguished name of the OU or container to search, e.g. "OU=Staff,DC=contoso,DC=com". Default: whole domain.

.PARAMETER SearchScope
    Base, OneLevel or Subtree (default Subtree).

.PARAMETER Enabled
    Only enabled accounts.

.PARAMETER Disabled
    Only disabled accounts.

.PARAMETER Properties
    Extra LDAP attribute names to add as columns, e.g. employeeID, physicalDeliveryOfficeName, extensionAttribute1.
    Multi-valued attributes are joined with "; ".

.PARAMETER Server
    Domain controller or domain to query (default: a DC of the current domain).

.PARAMETER Credential
    Alternate credentials for the AD query.

.PARAMETER ExportCsv
    Write the report to this CSV file (UTF-8).

.PARAMETER ExportHtml
    Write the report to this HTML file.

.PARAMETER PassThru
    Output the row objects to the pipeline.

.EXAMPLE
    .\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv

.EXAMPLE
    .\Export-ADUserReport.ps1 -SearchBase "OU=Sales,DC=contoso,DC=com" -Enabled -ExportHtml C:\Reports\sales.html

.EXAMPLE
    .\Export-ADUserReport.ps1 -Disabled -Properties employeeID,description -ExportCsv C:\Reports\disabled.csv

.EXAMPLE
    .\Export-ADUserReport.ps1 -Enabled -PassThru | Where-Object { -not $_.Manager } | Format-Table Name,Department

.NOTES
    Name:     Export-ADUserReport.ps1
    Version:  1.0.0
    Source:   https://srvscripts.com/scripts/export-ad-users-csv/
    License:  MIT
    Requires: Windows PowerShell 5.1 or PowerShell 7 on Windows, ActiveDirectory module (RSAT), read access to AD.
#>
[CmdletBinding(DefaultParameterSetName = 'All')]
param(
    [ValidateNotNullOrEmpty()]
    [string]$SearchBase,

    [ValidateSet('Base', 'OneLevel', 'Subtree')]
    [string]$SearchScope = 'Subtree',

    [Parameter(ParameterSetName = 'Enabled')]
    [switch]$Enabled,

    [Parameter(ParameterSetName = 'Disabled')]
    [switch]$Disabled,

    [ValidatePattern('^[A-Za-z][A-Za-z0-9-]*$')]
    [string[]]$Properties,

    [ValidateNotNullOrEmpty()]
    [string]$Server,

    [System.Management.Automation.PSCredential]
    [System.Management.Automation.Credential()]
    $Credential = [System.Management.Automation.PSCredential]::Empty,

    [ValidateNotNullOrEmpty()]
    [string]$ExportCsv,

    [ValidateNotNullOrEmpty()]
    [string]$ExportHtml,

    [switch]$PassThru
)

Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'

if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
    throw "The ActiveDirectory module is not installed. Install RSAT: Active Directory Domain Services tools, then run again."
}
Import-Module ActiveDirectory -Verbose:$false

if (-not $ExportCsv -and -not $ExportHtml -and -not $PassThru) {
    Write-Verbose 'No -ExportCsv, -ExportHtml or -PassThru given: results are shown on screen only.'
}

# ---- Helpers --------------------------------------------------------------------------------------------
function ConvertFrom-FileTimeValue {
    param($Value)
    if ($null -eq $Value) { return $null }
    $v = [int64]$Value
    if ($v -le 0 -or $v -eq [int64]::MaxValue) { return $null }
    [DateTime]::FromFileTime($v)
}

function Get-RdnValue {
    param([string]$DistinguishedName)
    if (-not $DistinguishedName) { return '' }
    if ($DistinguishedName -match '^(?:CN|OU)=((?:\\,|[^,])+)') { return ($Matches[1] -replace '\\,', ',') }
    $DistinguishedName
}

function Get-ParentPath {
    param([string]$DistinguishedName)
    # Strip the first RDN, honouring escaped commas.
    if ($DistinguishedName -match '^(?:\\,|[^,])+,(.+)$') { return $Matches[1] }
    ''
}

function Get-AttributeValue {
    param($Entity, [string]$Name)
    if ($Entity.PropertyNames -contains $Name) { return $Entity[$Name].Value }
    $null
}

function Format-AttributeValue {
    param($Value)
    if ($null -eq $Value) { return '' }
    if ($Value -is [System.Collections.ICollection] -and $Value -isnot [string] -and $Value -isnot [byte[]]) {
        return (@($Value) | ForEach-Object { [string]$_ }) -join '; '
    }
    if ($Value -is [byte[]]) { return [Convert]::ToBase64String($Value) }
    [string]$Value
}

# ---- Query ----------------------------------------------------------------------------------------------
$baseAttrs = 'displayName', 'mail', 'manager', 'department', 'title', 'whenCreated', 'lastLogonTimestamp', 'pwdLastSet', 'userAccountControl'
$extra = @()
if ($Properties) { $extra = @($Properties | Where-Object { $baseAttrs -notcontains $_ } | Select-Object -Unique) }

$ldap = '(&(objectCategory=person)(objectClass=user)'
if ($Enabled)  { $ldap += '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' }
if ($Disabled) { $ldap += '(userAccountControl:1.2.840.113556.1.4.803:=2)' }
$ldap += ')'

$query = @{
    LDAPFilter     = $ldap
    Properties     = @($baseAttrs + $extra)
    SearchScope    = $SearchScope
    ResultPageSize = 500
}
if ($SearchBase) { $query.SearchBase = $SearchBase }
if ($Server)     { $query.Server = $Server }
if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $query.Credential = $Credential }

Write-Verbose "LDAP filter: $ldap"
try {
    $users = @(Get-ADUser @query)
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
    throw "SearchBase not found: '$SearchBase'. Check the distinguished name (Get-ADOrganizationalUnit -Filter * | Select DistinguishedName)."
} catch {
    if ($_.Exception.Message -match 'attribute|property') {
        throw "AD rejected the query. Check the names given to -Properties (LDAP display names such as employeeID). Details: $($_.Exception.Message)"
    }
    throw
}

# ---- Build rows -----------------------------------------------------------------------------------------
$rows = foreach ($u in $users) {
    $uac = 0
    $uacValue = Get-AttributeValue $u 'userAccountControl'
    if ($null -ne $uacValue) { $uac = [int]$uacValue }
    $row = [ordered]@{
        Name                 = $u.Name
        DisplayName          = Format-AttributeValue (Get-AttributeValue $u 'displayName')
        SamAccountName       = $u.SamAccountName
        UserPrincipalName    = $u.UserPrincipalName
        Enabled              = $u.Enabled
        LastLogonDate        = ConvertFrom-FileTimeValue (Get-AttributeValue $u 'lastLogonTimestamp')
        PasswordLastSet      = ConvertFrom-FileTimeValue (Get-AttributeValue $u 'pwdLastSet')
        PasswordNeverExpires = [bool]($uac -band 0x10000)
        Manager              = Get-RdnValue (Format-AttributeValue (Get-AttributeValue $u 'manager'))
        Department           = Format-AttributeValue (Get-AttributeValue $u 'department')
        Title                = Format-AttributeValue (Get-AttributeValue $u 'title')
        Mail                 = Format-AttributeValue (Get-AttributeValue $u 'mail')
        Created              = (Get-AttributeValue $u 'whenCreated')
        OU                   = Get-ParentPath $u.DistinguishedName
        DistinguishedName    = $u.DistinguishedName
    }
    foreach ($a in $extra) {
        $row[$a] = Format-AttributeValue (Get-AttributeValue $u $a)
    }
    New-Object -TypeName psobject -Property $row
}
$rows = @($rows | Sort-Object Name)

# ---- Output ---------------------------------------------------------------------------------------------
if ($ExportCsv) {
    $rows | Export-Csv -LiteralPath $ExportCsv -NoTypeInformation -Encoding UTF8
    Write-Information ("{0} user(s) written to {1}" -f $rows.Count, $ExportCsv) -InformationAction Continue
}
if ($ExportHtml) {
    $css = 'body{font-family:Segoe UI,Arial,sans-serif;font-size:13px;margin:20px}table{border-collapse:collapse}' +
           'th,td{border:1px solid #ccc;padding:4px 8px;text-align:left}th{background:#f0f0f0}tr:nth-child(even){background:#fafafa}'
    $title = 'AD user report'
    $pre = "<h1>$title</h1><p>Generated {0:yyyy-MM-dd HH:mm} on {1}. {2} user(s).</p>" -f (Get-Date), $env:COMPUTERNAME, $rows.Count
    $rows | ConvertTo-Html -Title $title -Head "<style>$css</style>" -PreContent $pre | Out-File -LiteralPath $ExportHtml -Encoding utf8
    Write-Information ("HTML report written to {0}" -f $ExportHtml) -InformationAction Continue
}
if ($PassThru) { return $rows }
if (-not $ExportCsv -and -not $ExportHtml) {
    if (-not $rows.Count) { Write-Information 'No users matched.' -InformationAction Continue; return }
    $rows | Format-Table Name, SamAccountName, Enabled, LastLogonDate, PasswordLastSet, Department, Title -AutoSize
}
Version 1.0.0 · SHA-256 730c89665413a60bd851dfaf31ca6b19fec3f2d6dbbae90922241b8049abdfa3
Download and verify on Linux or macOS
curl -fsSL -o Export-ADUserReport.ps1 https://scr.srvscripts.com/export-ad-users-csv/Export-ADUserReport.ps1 && curl -fsSL https://scr.srvscripts.com/export-ad-users-csv/Export-ADUserReport.ps1.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/export-ad-users-csv/Export-ADUserReport.ps1' -OutFile 'Export-ADUserReport.ps1'; if ((Get-FileHash 'Export-ADUserReport.ps1' -Algorithm SHA256).Hash -eq '730C89665413A60BD851DFAF31CA6B19FEC3F2D6DBBAE90922241B8049ABDFA3') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

How do I export all AD users to CSV with PowerShell?

Run Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv on a machine with the ActiveDirectory module. Without the script, Get-ADUser -Filter * -Properties * | Export-Csv works but gives raw file-time numbers and DNs.

Why is LastLogonDate different from what the user says?

It comes from lastLogonTimestamp, which domain controllers only update when the stored value is older than about 9 to 14 days. It is designed for finding inactive accounts, not for exact logon times.

Can I export only one OU?

Yes. Pass the OU distinguished name to -SearchBase, for example -SearchBase “OU=Sales,DC=contoso,DC=com”. Add -SearchScope OneLevel to skip sub-OUs.

How do I add employeeID or other attributes?

Use -Properties with the LDAP display names, for example -Properties employeeID,physicalDeliveryOfficeName. Each one becomes a column at the end of the CSV.

Does the script change anything in Active Directory?

No. It only reads user objects and writes the CSV or HTML file you ask for.

Why does the CSV show strange characters in Excel?

The file is UTF-8. Open it with Data, From Text/CSV in Excel and choose UTF-8 if names with accents look wrong.

Changelog

  • 1.0.0 — First release.

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.