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