PowerShell: Get All Devices and Last Login from Active Directory
A PowerShell script to export every computer in Active Directory with its last logon date, operating system, and status — essential for cleanup, compliance, and licence management.
Overview
Knowing which devices are active in your Active Directory and when they last signed in is critical for security hygiene, licence management, and compliance audits. Stale computer accounts are a security risk — attackers can impersonate them, and compliance frameworks like CMMC require you to maintain an accurate device inventory.
This script exports every computer in AD with its last logon timestamp, OS version, and status to a CSV file.
Prerequisites
- Active Directory PowerShell module (
RSAT-AD-PowerShell) - Read access to AD computer objects
- PowerShell 5.1 or later (PowerShell 7.6 LTS recommended)
The Script
<#
.SYNOPSIS
Export all AD computers with last logon date and OS info.
.DESCRIPTION
Queries Active Directory for all computer objects and exports
a report with last logon, OS version, enabled status, and
staleness classification.
.PARAMETER DaysInactive
Number of days since last logon to classify as inactive. Default: 90.
.PARAMETER SearchBase
OU to search. Defaults to entire domain.
.PARAMETER OutputPath
Directory for the CSV file. Defaults to current directory.
#>
param(
[int]$DaysInactive = 90,
[string]$SearchBase = "",
[string]$OutputPath = "."
)
Import-Module ActiveDirectory -ErrorAction Stop
$cutoffDate = (Get-Date).AddDays(-$DaysInactive)
$timestamp = Get-Date -Format "yyyy-MM-dd_HHmm"
Write-Host "Querying Active Directory computers..." -ForegroundColor Yellow
# Build query parameters
$queryParams = @{
Filter = '*'
Properties = @(
'LastLogonTimestamp',
'LastLogonDate',
'OperatingSystem',
'OperatingSystemVersion',
'Enabled',
'Created',
'Description',
'DistinguishedName',
'IPv4Address',
'PasswordLastSet'
)
}
if ($SearchBase) {
$queryParams['SearchBase'] = $SearchBase
}
$computers = Get-ADComputer @queryParams
Write-Host "Found $($computers.Count) computer objects." -ForegroundColor Cyan
$results = foreach ($pc in $computers) {
# LastLogonTimestamp is replicated (slight delay but consistent across DCs)
# Convert from file time if present
$lastLogon = if ($pc.LastLogonTimestamp) {
[DateTime]::FromFileTime($pc.LastLogonTimestamp)
} elseif ($pc.LastLogonDate) {
$pc.LastLogonDate
} else {
$null
}
$daysSinceLogon = if ($lastLogon) {
[math]::Round(((Get-Date) - $lastLogon).TotalDays, 0)
} else {
-1
}
$status = if (-not $pc.Enabled) {
"Disabled"
} elseif ($null -eq $lastLogon) {
"Never logged in"
} elseif ($lastLogon -lt $cutoffDate) {
"Inactive ($daysSinceLogon days)"
} else {
"Active"
}
[PSCustomObject]@{
Name = $pc.Name
Enabled = $pc.Enabled
Status = $status
LastLogon = $lastLogon
DaysSinceLogon = $daysSinceLogon
OperatingSystem = $pc.OperatingSystem
OSVersion = $pc.OperatingSystemVersion
IPv4Address = $pc.IPv4Address
Created = $pc.Created
PasswordLastSet = $pc.PasswordLastSet
Description = $pc.Description
OU = ($pc.DistinguishedName -replace '^CN=[^,]+,', '')
}
}
# Sort by last logon (most stale first)
$sorted = $results | Sort-Object DaysSinceLogon -Descending
# Export
$filename = "AD-Devices_$timestamp.csv"
$filepath = Join-Path $OutputPath $filename
$sorted | Export-Csv -Path $filepath -NoTypeInformation -Encoding UTF8
# Summary
$total = $sorted.Count
$active = ($sorted | Where-Object { $_.Status -eq "Active" }).Count
$inactive = ($sorted | Where-Object { $_.Status -like "Inactive*" }).Count
$disabled = ($sorted | Where-Object { $_.Status -eq "Disabled" }).Count
$neverLoggedIn = ($sorted | Where-Object { $_.Status -eq "Never logged in" }).Count
Write-Host "`n--- Summary ---" -ForegroundColor Cyan
Write-Host "Total: $total"
Write-Host "Active: $active" -ForegroundColor Green
Write-Host "Inactive (>$DaysInactive days): $inactive" -ForegroundColor Yellow
Write-Host "Disabled: $disabled" -ForegroundColor Gray
Write-Host "Never logged in: $neverLoggedIn" -ForegroundColor Red
Write-Host "`nExported to: $filepath" -ForegroundColor Green
# Also output the top 20 most stale active computers
$staleActive = $sorted | Where-Object { $_.Enabled -and $_.DaysSinceLogon -gt $DaysInactive } | Select-Object -First 20
if ($staleActive) {
Write-Host "`n--- Top 20 Stale Active Computers (candidates for cleanup) ---" -ForegroundColor Yellow
$staleActive | Format-Table Name, DaysSinceLogon, OperatingSystem, OU -AutoSize
}
Usage
Basic export (entire domain, 90-day threshold)
.\Get-ADDeviceReport.ps1
Custom inactivity threshold
.\Get-ADDeviceReport.ps1 -DaysInactive 60
Scan a specific OU
.\Get-ADDeviceReport.ps1 -SearchBase "OU=Workstations,DC=corp,DC=example,DC=com"
Export to a network share
.\Get-ADDeviceReport.ps1 -OutputPath "\\server\reports"
Output Example
| Name | Enabled | Status | LastLogon | DaysSinceLogon | OperatingSystem |
|---|---|---|---|---|---|
| OLD-PC01 | True | Inactive (342 days) | 2025-04-22 | 342 | Windows 10 Pro |
| CONF-ROOM | True | Never logged in | -1 | Windows 11 Enterprise | |
| WS-ABC123 | True | Active | 2026-03-28 | 1 | Windows 11 Pro |
| LT-JSMITH | False | Disabled | 2025-12-15 | 104 | Windows 11 Pro |
Automating Cleanup
Disable stale computers (preview first)
# Preview — shows what would be disabled
$stale = Get-ADComputer -Filter {LastLogonTimestamp -lt $cutoffDate -and Enabled -eq $true} -Properties LastLogonTimestamp
$stale | Select-Object Name, @{N='LastLogon';E={[DateTime]::FromFileTime($_.LastLogonTimestamp)}}
# Disable (uncomment when ready)
# $stale | Disable-ADAccount
# $stale | Set-ADComputer -Description "Disabled by cleanup script $(Get-Date -Format 'yyyy-MM-dd')"
Always preview before disabling. Some computers (conference room displays, kiosks, rarely-used lab machines) may have legitimate long gaps between sign-ins. Move disabled accounts to a "Disabled" OU and wait 30 days before deleting.
Schedule monthly reports
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File C:\Scripts\Get-ADDeviceReport.ps1 -OutputPath C:\Reports"
$trigger = New-ScheduledTaskTrigger -Monthly -DaysOfMonth 1 -At 7am
Register-ScheduledTask -TaskName "Monthly AD Device Report" -Action $action -Trigger $trigger -RunLevel Highest
Why This Matters
| Use Case | What the Report Tells You |
|---|---|
| Security | Stale accounts can be exploited for lateral movement |
| Compliance | CMMC, NIST, and CIS require an accurate asset inventory |
| Licensing | Inactive devices may hold Windows or M365 licences unnecessarily |
| Cleanup | Reducing AD clutter improves Group Policy processing time |
| Auditing | Provides evidence of asset management for auditors |
Related Articles
Was this article helpful?
