Optimize Your Intune Environment: Remove And Report Unused Devices With PowerShell

Managing devices in Microsoft Intune can be a complex task, especially when ensuring that your device inventory remains compliant and up-to-date. I created a PowerShell script that automates the cleanup of non-compliant and stale devices by leveraging Microsoft Graph commands to report on devices based on their last sync time and compliance status. While you can find this information in the Intune Portal, using these handy PowerShell commands from Microsoft.Graph expedites the process. This allows administrators to quickly identify and remove outdated entries. This approach not only streamlines device management but also aligns with best practices for maintaining a secure and efficient Intune environment. This is a quick script I wrote to better understand the Microsoft graph commands and provide instant insight into device compliance and clean removals programmatically.

Requirements

You will need to have Microsoft.Graph.Users and Microsoft.Graph.DeviceManagement PowerShell modules installed and access to an available Entra ID tenant. Also, Ensure your account has the necessary permissions to manage devices, specifically the scopes DeviceManagementManagedDevices.ReadWrite.All and Device.ReadWrite.All.

Functionality

Report all Intune Devices out of compliance with Microsoft Graph PowerShell

When I was building this script and using it, I decided to create a -report parameter that when passed, creates a CSV file with the headers “DeviceName”, “ComplianceState”, “LastSyncDateTime” , and “User”. It loops through all the devices in Intune and appends the values if the state is not “compliant” and the last sync time is less than the -daysSinceSync argument, which defaults to 14.

./Remove-MSDevice -report -daysSinceSync 14

Here are the parameters, authentication, and reporting logic regions of the script

param (
    [Parameter(Mandatory=$false)]
    [Alias("name", "device")]
    [string] $deviceName,

    [Parameter(Mandatory=$false)]
    [Alias("days")]
    [ValidateRange(1, 60)]
    [int] $daysSinceSync = 14,

    [Parameter(Mandatory=$false, HelpMessage="Switch to report non compliant devices")]
    [switch] $report
)

# ---------------- Import modules --------------------
$modules = @("Microsoft.Graph.DeviceManagement", "Microsoft.Graph.Users")

$modules | % {
    try{
        if($null -eq (Get-Module -ListAvailable -Name $_)){
            Install-Module -Name $_ -Scope CurrentUser -AllowClobber -Force
            Import-Module -Name $_
        }
    }catch{
        Write-Error -Message "Could not import necessary modules. Exit 1"; exit 1
    }
}

# ----------- Authenticate user to Microsoft graph ---------------

try{
    $u = Get-MgUser -top 1 -ErrorAction SilentlyContinue
    if($null -eq $u){
        throw "Please connect to MSGraph"
    }
}catch{
    Write-Output $_.Exception.Message
    Connect-MgGraph -Scopes "DeviceManagementManagedDevices.ReadWrite.All", "Device.ReadWrite.All" -UseDeviceCode
}

$REGEXINPUT = '^[yY]$'

if($report.isPresent){
    
    $outFile = "stale-devices-$(get-date -Format "yyyyMMdd").csv"
    if(test-path $outFile){ remove-Item $outFile }
    $n = {} | select "DeviceName", "ComplianceState", "LastSyncDateTime" , "User" | Export-Csv -path $outFile
    $csvData = Import-csv -path $outFile

    $devices = Get-MgDeviceManagementManagedDevice | select * | ?{($_.ComplianceState -ne 'compliant') -and ($_.LastSyncDateTime -lt (Get-Date).AddDays(-$daysSinceSync))}

    $devices | % {
        $data = [PSCustomObject]@{
            DeviceName          = $_.DeviceName
            ComplianceState     = $_.ComplianceState
            LastSyncDateTime    = $_.LastSyncDateTime
            User                = $_.UserPrincipalName
        }
        $data | Export-Csv $outFile -Append
    }
}

Remove or Evaluate Stale Intune Device with Microsoft Graph PowerShell

You can also invoke this script to remove the device object from Entra ID and Intune. This command will search for the device with the DeviceName attribute of “PC-Test” in Intune and remove it. If there are duplicates, it will evaluate each and prompt the user to delete all objects or just the objects with the properties meeting the conditions – not in a compliant state and LastSycDateTime over 21 days.

./Remove-MSDevice -deviceName "PC-Test" -daysSinceSync 21

Logic for the device evaluation and removal. I create two functions, one RemoveDeviceFromEntraAndIntune that does exactly that using the Remove-MgDeviceManagementManagedDevice and Remove-MgDeviceByDeviceId. It handles errors if either command fail by concatenating it to the $err string and throwing the error. The EvaluateStaleDevice function uses the daysSinceSync parameter and checks the compliance state of the device. This function is invoked when there are duplicate devices. The script loops through the devices with the same deviceName and evaluates the conditions. If they are not met, user is shown data of the stale device and prompted for removal. If yes, only the devices failing the conditions are removed by calling RemoveDeviceFromEntraAndIntune

The script defines two functions for device evaluation and removal:

  • RemoveDeviceFromEntraAndIntune: This function removes devices using Remove-MgDeviceManagementManagedDevice and Remove-MgDeviceByDeviceId. It handles errors by concatenating any failures to the $err string and throws an error if either command fails.
  • EvaluateStaleDevice: This function checks the compliance state of a device against the daysSinceSync parameter. It is invoked when there are duplicate devices with the same deviceName. The script loops through these devices and evaluates their conditions. If a device does not meet the criteria, its details are displayed to the user, who is then prompted for removal. If the user confirms, only the devices that fail the conditions are removed by calling RemoveDeviceFromEntraAndIntune within its scope.
function RemoveDeviceFromEntraAndIntune($devObj) {
    # Write-Output "removal function called for $($devObj.DeviceName)"
    try{
        Remove-MgDeviceManagementManagedDevice -ManagedDeviceId $devObj.Id
        Remove-MgDeviceByDeviceId -DeviceId $devObj.AzureAdDeviceId
        Start-Sleep -Seconds 3
        $err = ""

        if($null -ne (Get-MgDeviceManagementManagedDevice -ManagedDeviceId $devObj.Id)){
            $err += "- Failed to removed from Intune"
        }
        if($null -ne (Get-MgDeviceByDeviceId -DeviceId $devObj.AzureAdDeviceId)){
            $err += "- Failed to remove from Entra ID"
        }
        if(-not ([string]::IsNullOrEmpty($err))){
            throw "$($devObj.DeviceName) $err"
        }
    }catch{
        Write-Output $_.Exception.Message
        # outer try will catch this
        # throw "Something went wrong. Failed to cleanly remove $($devObj.DeviceName)"
    }
}

function EvaluateStaleDevice($device){

    if(($_.daysSinceSyncDateTime -lt (Get-Date).AddDays(-$daysSinceSync)) -and ($_.ComplianceState -ne "compliant")){
        Write-Output("Possible stale device object`nDisplayName: {0}`nDeviceId: {1}`ndaysSinceSyncTime: {2}`nCompliance state: {3}" -f $_.DeviceName, $_.Id, $_.daysSinceSyncDateTime, $_.ComplianceState)
        # based on the device status, prompt user for verification of deletion
        $userIn = Read-Host "Would you like to delete this stale object (y/Y): " 

        if($userIn -match $REGEXINPUT ){
            RemoveDeviceFromEntraAndIntune -devObj $_
        }
    }
}

if($PSBoundParameters.ContainsKey('deviceName')){
    try {
        $deviceObject = Get-MgDeviceManagementManagedDevice -Filter "DeviceName eq '$($deviceName)'" | select *
        
        if($null -eq $deviceObject){
            throw "$deviceName was not found in Intune"
        }

        if($deviceObject.Count -gt 1){
            # notify user multiple objects with same DeviceName exist in Intune
            Write-Output("{0} - {1} Device Objects found. Please view the stale object and determine which one you want to remove" -f $deviceObject[0].DeviceName, $deviceObject.Count)

            # promt user to remove both objects
            $userInBoth = Read-Host "Would you like to remove both MDM device objects (y/Y): "
            # if user wants to remove all device objects
            if($userInBoth -match $REGEXINPUT){

                # call the helper function to remove the devices from Intune and Entra
                $deviceObject | %{ RemoveDeviceFromEntraAndIntune -devObj $_ }
            }else{
                # Stale device objects defined as devices that haven't synced in 14 days and status not compliant status
                $deviceObject | ForEach-Object {
                    EvaluateStaleDevice -device $_
                }
            }
        }else{  
            # remove the device if only one object found
            RemoveDeviceFromEntraAndIntune -devObj $deviceObject
        }
    }catch {
        Write-Output $_.Exception.Message; exit 1
    }
}

Piping Filtered Devices to the Script using Get-MgDeviceManagementManagedDevice

You can use the -Filter flag or pipe from Where-Object using any property returned by the Get-MgDeviceManagementManagedDevice command, or any command that returns an accurate device name. Maybe a hybrid environment that has stale devices in the cloud from sync or imaging issues, you can leverage the Get-ADComputer command from the ActiveDirectory module to cleanup the environment.

This example filter the devices not in a compliant state and invokes the script for each device.

Get-MgDeviceManagementManagedDevice | Where-Object {$_.ComplianceState -ne 'compliant'} | ForEach-Object { ./Remove-MSDevice.ps1 -deviceName $_.DeviceName }

Completed Script

<#
.SYNOPSIS
    Removes device objects from Intune and Entra ID. Detects devices out of compliance and lastSync time, and duplicate devices

.DESCRIPTION
    Takes the -deviceName argument to remove and deletes it from Intune and Entra ID
    Detects duplicate devices and evaluates them, using the -daysSinceSync (default 14 days) and compliance status
    Prompts the user to delete all device objects with a duplicate name or just the stale device
    Using the -reports will create a CSV file of not compliant devices that havnt synced within the last -daysSinceSync (default 14)

.PARAMETER deviceName
    The name of the device to remove.

.PARAMETER daysSinceSync
    The number of days since the device last synced. Default is 14 days.

.PARAMETER reports

    Generates a report in CSV format of devices in Intune that meet the conditions of not compliant and last checkin tile less than 14 days ago (default).
    Headers are "DeviceName", "ComplianceState", "LastSyncDateTime" , and "User"

.NOTES
    This function is not supported in Linux.

.LINK

.EXAMPLE
    offboardingWorkstation.ps1 -deviceName "LT-5234431" -daysSinceSync 21
    The results of ths command will remove the device from Entra and Intune. If duplicate devices are detected, user will be
    asked to delete all of the duplicates. if no, user will be prompted to remove the device objects deemed stale through the conditions compliance status and last sync time

.EXAMPLE

    offboardingWorkstation.ps1 -report
    Creates a csv file named stale-devices-YYYYMMdd.csv with not compliant device that havent synced 
    "DeviceName", "ComplianceState", "LastSyncDateTime" , and "User"
#>

param (
    [Parameter(Mandatory=$false)]
    [Alias("name", "device")]
    [string] $deviceName,

    [Parameter(Mandatory=$false)]
    [Alias("days")]
    [ValidateRange(1, 60)]
    [int] $daysSinceSync = 14,

    [Parameter(Mandatory=$false, HelpMessage="Switch to report non compliant devices")]
    [switch] $report
)

# ---------------- Import modules --------------------
$modules = @("Microsoft.Graph.DeviceManagement", "Microsoft.Graph.Users")

$modules | % {
    try{
        if($null -eq (Get-Module -ListAvailable -Name $_)){
            Install-Module -Name $_ -Scope CurrentUser -AllowClobber -Force
            Import-Module -Name $_
        }
    }catch{
        Write-Error -Message "Could not import necessary modules. Exit 1"; exit 1
    }
}

try{
    $u = Get-MgUser -top 1 -ErrorAction SilentlyContinue
    if($null -eq $u){
        throw "Please connect to MSGraph"
    }
}catch{
    Write-Output $_.Exception.Message
    Connect-MgGraph -Scopes "DeviceManagementManagedDevices.ReadWrite.All", "Device.ReadWrite.All" -UseDeviceCode
}

$REGEXINPUT = '^[yY]$'

function RemoveDeviceFromEntraAndIntune($devObj) {
    # Write-Output "removal function called for $($devObj.DeviceName)"
    try{
        Remove-MgDeviceManagementManagedDevice -ManagedDeviceId $devObj.Id
        Remove-MgDeviceByDeviceId -DeviceId $devObj.AzureAdDeviceId
        Start-Sleep -Seconds 3
        $err = ""

        if($null -ne (Get-MgDeviceManagementManagedDevice -ManagedDeviceId $devObj.Id)){
            $err += "- Failed to removed from Intune"
        }
        if($null -ne (Get-MgDeviceByDeviceId -DeviceId $devObj.AzureAdDeviceId)){
            $err += "- Failed to remove from Entra ID"
        }
        if(-not ([string]::IsNullOrEmpty($err))){
            throw "$($devObj.DeviceName) $err"
        }
    }catch{
        Write-Output $_.Exception.Message
        # outer try will catch this
        # throw "Something went wrong. Failed to cleanly remove $($devObj.DeviceName)"
    }
}

function EvaluateStaleDevice($device){

    if(($_.daysSinceSyncDateTime -lt (Get-Date).AddDays(-$daysSinceSync)) -and ($_.ComplianceState -ne "compliant")){
        Write-Output("Possible stale device object`nDisplayName: {0}`nDeviceId: {1}`ndaysSinceSyncTime: {2}`nCompliance state: {3}" -f $_.DeviceName, $_.Id, $_.daysSinceSyncDateTime, $_.ComplianceState)
        # based on the device status, prompt user for verification of deletion
        $userIn = Read-Host "Would you like to delete this stale object (y/Y): " 

        if($userIn -match $REGEXINPUT ){
            RemoveDeviceFromEntraAndIntune -devObj $_
        }
    }
}

if($PSBoundParameters.ContainsKey('deviceName')){
    try {
        $deviceObject = Get-MgDeviceManagementManagedDevice -Filter "DeviceName eq '$($deviceName)'" | select *
        
        if($null -eq $deviceObject){
            throw "$deviceName was not found in Intune"
        }

        if($deviceObject.Count -gt 1){
            # notify user multiple objects with same DeviceName exist in Intune
            Write-Output("{0} - {1} Device Objects found. Please view the stale object and determine which one you want to remove" -f $deviceObject[0].DeviceName, $deviceObject.Count)

            # promt user to remove both objects
            $userInBoth = Read-Host "Would you like to remove both MDM device objects (y/Y): "
            # if user wants to remove all device objects
            if($userInBoth -match $REGEXINPUT){

                # call the helper function to remove the devices from Intune and Entra
                $deviceObject | %{ RemoveDeviceFromEntraAndIntune -devObj $_ }
            }else{
                # Stale device objects defined as devices that haven't synced in 14 days and status not compliant status
                $deviceObject | ForEach-Object {
                    EvaluateStaleDevice -device $_
                }
            }
        }else{  
            # remove the device if only one object found
            RemoveDeviceFromEntraAndIntune -devObj $deviceObject
        }
    }catch {
        Write-Output $_.Exception.Message; exit 1
    }
}

if($report.isPresent){
    
    $outFile = "stale-devices-$(get-date -Format "yyyyMMdd").csv"
    if(test-path $outFile){ remove-Item $outFile }
    $n = {} | select "DeviceName", "ComplianceState", "LastSyncDateTime" , "User" | Export-Csv -path $outFile
    $csvData = Import-csv -path $outFile

    $devices = Get-MgDeviceManagementManagedDevice | select * | ?{($_.ComplianceState -ne 'compliant') -and ($_.LastSyncDateTime -lt (Get-Date).AddDays(-$daysSinceSync))}

    $devices | % {
        $data = [PSCustomObject]@{
            DeviceName          = $_.DeviceName
            ComplianceState     = $_.ComplianceState
            LastSyncDateTime    = $_.LastSyncDateTime
            User                = $_.UserPrincipalName
        }
        $data | Export-Csv $outFile -Append
    }
}

Conclusion

This script can streamlines device management in Intune and Entra ID by reporting and automating the removal of non-compliant and stale devices, showcasing the power of Microsoft Graph PowerShell. Leveraging such tools not only enhances efficiency but also represents a valuable practice in your IT journey, as it equips you with essential skills for managing cloud resources effectively. Drop a comment or contact me with any suggestions

Leave a Reply