AADdevice-Cleanup.ps1
<#PSScriptInfo .VERSION 2.2 .GUID 79afdf4a-d694-45ff-8d6c-4f585d96b284 .AUTHOR Jeff Gilbert (@JeffGilb) .DESCRIPTION Clean-up (disable or delete) device accounts in Azure AD based on the length of time they've been inactive. Will not disable or delete Hybrid Azure AD joined or Autopilot registered devices. .TAGS .RELEASENOTES Version 1.0: Original published version. Version 2.0: Updated to improve Autopilot and Hybrid Azure AD joined device disable/delete behavior as well as logging/reporting improvements. Version 2.1: Added check for required Azure AD PowerShell module. Version 2.2: Fixed bug in event logging. ************************************************************************************************************************* READ ME! ************************************************************************************************************************* This script is provided on an "as is" without warranties of any kind. USE AT YOUR OWN RISK. Test in your own environment before using in production. You assume all risk if you run the script. Review the Authentication function section to determine how you will authenticate with Azure AD. The default configuration is to interactively log on to Azure AD, but you can modify the script to silently authenticate to leverage the script in a scheduled task. More information here: https://www.jeffgilb.com/connecting-to-azure-ad-with-powershell/ It is not advisable to immediately delete a device that appears to be stale because you can't undo a deletion in the case of false positives. As a best practice, disable a device for a grace period before deleting it. In your policy, define a timeframe to disable a device before deleting it. If your device is under control of Intune or any other MDM solution, retire the device in the management system before disabling or deleting it. Don't delete system-managed devices (i.e. Autopiot registered devices). Once deleted, these devices can't be reprovisioned. ***This script uses the Get-AzureADDevice cmdlet which excludes system-managed devices by default.*** Hybrid Azure AD joined devices should follow your policies for on-premises stale device management. This script is used to manage stale Azure AD device accounts and WILL NOT delete Hybrid Azure AD joined devices. When configured, BitLocker keys for Windows 10 devices are stored on the device object in Azure AD. If you delete a stale device, you also delete the BitLocker keys that are stored on the device. You should determine whether your cleanup policy aligns with the actual lifecycle of your device before deleting a stale device. Need help defining a cleanup policy? Read this article: https://docs.microsoft.com/azure/active-directory/devices/manage-stale-devices#plan-the-cleanup-of-your-stale-devices Required parameters: action, days Optional parameter: whatIf Device accounts must be disabled in order to be deleted. CSV reports are created in the public user documents directory. Application Event Logs are generated when the script runs to disable or delete accounts. Syntax: AADdevice-Cleanup -action <disable or delete> -days <number of days inactive> -WhatIf (optionally just create a CSV report instead of doing the action) Examples: Disable device accounts that have been inactive for 60 days: .\AADdevice-Cleanup -action disable -days 60 Delete device accounts that have been inactive for 90 days: .\AADdevice-Cleanup -action delete -days 90 Create a report of devices that have been inactive 30 days: .\AADdevice-Cleanup -action disable -days 30 -WhatIf ************************************************************************************************************************* #> param ( [parameter(mandatory=$true)][ValidateSet("disable","delete")][string]$action, [string]$days = $(throw "-days is required."), [switch]$whatIf = $false ) Function Authenticate { <# Ways to authenticate to run the script. Un-comment your favorite: See blog post for more information: https://www.jeffgilb.com/connecting-to-azure-ad-with-powershell/ #1. # Interactive log on to Azure AD Connect-AzureAD | Out-Null #2. Put your username and password in plain text...please don't do this unless you're just testing. $user = "svc-account@mydomain.com" $password = "This1sMyP@ssw0rd" $secPass = ConvertTo-SecureString $password -AsPlainText -Force $Cred = New-Object System.Management.Automation.PSCredential ($user, $secPass) Connect-AzureAD -Credential $cred | Out-Null #3. Create and use an encrypted password file. Good for running scripts as a scheduled task. # Create the password file (this should only be done once outside of the normal script operations). # This password file will only work on the computer you create it on. (Get-Credential).Password | ConvertFrom-SecureString | Out-File "C:\temp\password.txt" # Use the password file to authenticate $user = "svc-account@mydomain.com" $password = Get-Content "C:\temp\password.txt" | ConvertTo-SecureString $file = "C:\temp\password.txt" $MyCredential=New-Object -TypeName System.Management.Automation.PSCredential ` -ArgumentList $user, (Get-Content $file | ConvertTo-SecureString) Connect-AzureAD -Credential $MyCredential | Out-Null #4. Don't put any credentials here at all and run the script using Azure Automation Run As accounts. # I might make a blog about doing that. If I do, I'll update this script. https://docs.microsoft.com/en-us/azure/automation/manage-runas-account #> # Default authentication is an interactive log on to Azure AD Connect-AzureAD | Out-Null } Function write-Log { param ( [Parameter(Mandatory=$True)][array]$LogOutput, [Parameter(Mandatory=$True)][string]$Path ) $currentDate = (Get-Date -UFormat "%d-%m-%Y") $currentTime = (Get-Date -UFormat "%T") $logOutput = $logOutput -join (" ") "[$currentDate $currentTime] $logOutput" | Out-File $Path -Append } Function whatIf { param ( [Parameter(Mandatory=$True)][array]$LogOutput, [Parameter(Mandatory=$True)][string]$Path ) $currentDate = (Get-Date -UFormat "%d-%m-%Y") $currentTime = (Get-Date -UFormat "%T") $logOutput = $logOutput -join (" ") "[$currentDate $currentTime] $logOutput" | Out-File $Path -Append } Function Disable { # Will not disable hybrid Azure AD joined (HAADJ) devices. $deviceList = Get-AzureADDevice | Where {$_.ApproximateLastLogonTimeStamp -le $x} | select-object AccountEnabled, DisplayName, ObjectID, DeviceTrustType ForEach ($name in $devicelist){ # Set-AzureADDevice: https://docs.microsoft.com/en-us/powershell/module/azuread/Set-AzureADDevice?view=azureadps-2.0 If ($name.AccountEnabled -eq "True"){ $HAADJdevice = $deviceList | Where {$name.DeviceTrustType -eq "ServerAd"} $AADJdevice = $deviceList | Where {$name.DeviceTrustType -eq "AzureAd"} $workPlace = $deviceList | Where {$name.DeviceTrustType -eq "Workplace"} $blank = $deviceList | Where {$name.DeviceTrustType -lt " "} If($HAADJdevice) { write-host -Foreground Yellow $name.Displayname " is HAADJ. Skipping." write-log -LogOutput ("Skipping hybrid joined device: "+$name.displayname) -Path $LogFile } ElseIf($AADJdevice) { Write-host -ForeGround Cyan "Disabling " $name.DisplayName write-log -LogOutput ("Disabling stale device: "+$name.displayname) -Path $LogFile Set-AzureADDevice -ObjectId $name.ObjectID -AccountEnabled 0 } ElseIf($workPlace) { Write-host -ForeGround Cyan "Disabling " $name.DisplayName write-log -LogOutput ("Disabling stale device: "+$name.displayname) -Path $LogFile Set-AzureADDevice -ObjectId $name.ObjectID -AccountEnabled 0 } ElseIf($blank) { Write-host -ForeGround Cyan "Disabling " $name.DisplayName write-log -LogOutput ("Disabling stale device: "+$name.displayname) -Path $LogFile Set-AzureADDevice -ObjectId $name.ObjectID -AccountEnabled 0 } } Else{ write-host -Foreground Yellow $name.Displayname " already disabled. Skipping." write-log -LogOutput ("Device already disabled: "+$name.displayname) -Path $LogFile } } write-host -ForegroundColor Yellow `n"Disable actions logged to $LogFile." # Create a disable Event Log event try{ Write-EventLog -LogName Application -Source AADdevice-Cleanup -EventID 123 -EntryType Information -Message "AADdevice-Cleanup disable action triggered. Log file at $LogFile." -Category 1 -RawData 10,20 -ErrorAction Stop } Catch{ New-EventLog -LogName Application -Source AADdevice-Cleanup Write-EventLog -LogName Application -Source AADdevice-Cleanup -EventID 123 -EntryType Information -Message "AADdevice-Cleanup disable action triggered. Log file at $LogFile." -Category 1 -RawData 10,20 } Write-host -ForegroundColor Yellow `n"Disable actions recorded in the Application Event Log. Source: AADdevice-Cleanup. EventId: 123." Write-host "" } Function Delete { <# Do not delete: - Enabled device accounts (will check for this before the other states) - Autopilot devices - HAADJ devices #> $deviceList = Get-AzureADDevice | Where {$_.ApproximateLastLogonTimeStamp -le $x} | select-object AccountEnabled, DisplayName, ObjectID, DeviceTrustType $ZTDIDdevice = Get-AzureADDevice -Filter "startswith(DevicePhysicalIds, '[ZTDID]')" | Where {$_.ApproximateLastLogonTimeStamp -le $x } | select-object DisplayName, ObjectId, DeviceTrustType ForEach ($name in $devicelist){ # Remove-AzureADDevice https://docs.microsoft.com/en-us/powershell/module/azuread/remove-azureaddevice?view=azureadps-2.0 If ($name.AccountEnabled -ne "True"){ $autopilot = $ZTDIDdevice | where {$_.ObjectId -eq $name.ObjectId} $HAADJdevice = $deviceList | Where {$name.DeviceTrustType -eq "ServerAd"} $AADJdevice = $deviceList | Where {$name.DeviceTrustType -eq "AzureAd"} $workPlace = $deviceList | Where {$name.DeviceTrustType -eq "Workplace"} $blank = $deviceList | Where {$name.DeviceTrustType -lt " "} If($autopilot) { write-host -Foreground Yellow $name.Displayname "is an autopilot device. Skipping." write-log -LogOutput ("Skipping autopilot device: "+$name.displayname) -Path $LogFile } ElseIf($HAADJdevice) { write-host -Foreground Yellow $name.Displayname "is HAADJ. Skipping." write-log -LogOutput ("Skipping hybrid joined device: "+$name.displayname) -Path $LogFile } ElseIf($AADJdevice) { Write-host -ForeGround Cyan "Deleting " $name.DisplayName write-log -LogOutput ("Deleting stale device: "+$name.displayname) -Path $LogFile Remove-AzureADDevice -ObjectId $name.ObjectID } ElseIf($workPlace) { Write-host -ForeGround Cyan "Deleting " $name.DisplayName write-log -LogOutput ("Deleting stale device: "+$name.displayname) -Path $LogFile Remove-AzureADDevice -ObjectId $name.ObjectID } ElseIf($blank) { Write-host -ForeGround Cyan "Deleting " $name.DisplayName write-log -LogOutput ("Deleting stale device: "+$name.displayname) -Path $LogFile Remove-AzureADDevice -ObjectId $name.ObjectID } } Else{ write-host -Foreground Yellow $name.Displayname "not disabled. Skipping." write-log -LogOutput ("Skipping enabled device: "+$name.displayname) -Path $LogFile } } write-host -ForegroundColor Yellow `n"Delete actions logged to $LogFile." # Create a delete Event Log event try{ Write-EventLog -LogName Application -Source AADdevice-Cleanup -EventID 125 -EntryType Warning -Message "AADdevice-Cleanup delete action triggered. Log file at $LogFile." -Category 1 -RawData 10,20 -ErrorAction Stop } Catch{ New-EventLog –LogName Application –Source AADdevice-Cleanup Write-EventLog -LogName Application -Source AADdevice-Cleanup -EventID 125 -EntryType Warning -Message "AADdevice-Cleanup delete action triggered. Log file at $LogFile." -Category 1 -RawData 10,20 } write-host -ForegroundColor Yellow `n"Delete actions recorded in the Application Event Log. Source: AADdevice-Cleanup. EventId: 125." Write-host "" } $x = (Get-Date).AddDays(-$days) $logFolder = "C:\Users\Public\Documents" $logFile = $LogFolder + "\" + (Get-Date -UFormat "%d-%m-%Y") + "-AADdevice-Cleanup.log" $whatIfLogFile = $LogFolder + "\" + (Get-Date -UFormat "%d-%m-%Y") + "-whatIf-AADdevice-Cleanup.log" Write-Host "Checking for AzureAD module..." $AadModule = Get-Module -Name "AzureAD" -ListAvailable if ($AadModule -eq $null) { Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable } if ($AadModule -eq $null) { write-host write-host "AzureAD Powershell module not installed..." -f Red write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow write-host "Script can't continue..." -f Red write-host exit } Authenticate write-output `n"Action to take: $action" write-output "Number of inactive days: $days" write-output "Report only? $whatIf"`n write-host "Looking for devices to $action that have last activity before" $x"." `n # Cleanup actions If ($whatIf -ne $false){ $deviceList = Get-AzureADDevice | Where {$_.ApproximateLastLogonTimeStamp -le $x} | select-object DisplayName |` ForEach-Object { write-host -ForeGround Cyan "$($_.DisplayName)" write-log -LogOutput ("Stale device found: "+$_.displayname) -Path $whatIfLogFile } write-host `n"whatIf log saved to $whatIfLogFile" exit } Else{ If($action -eq "delete"){Delete} Else{Disable} } exit |