Invoke-IntuneWin32AppRedeploy.ps1
|
<#PSScriptInfo
.VERSION 1.3.2 .GUID 3f8e7d2a-5c4b-4e9f-a1d6-8b7c3e2f1a0d .AUTHOR Mark Orr .COPYRIGHT (c) 2026 Orr365. All rights reserved. .DESCRIPTION Forces a redeploy of Intune Win32 applications by clearing local registry state and restarting the Intune Management Extension service. Uses Microsoft Graph for app and user name resolution. .TAGS Intune Win32App Redeploy MicrosoftGraph Endpoint .LICENSEURI https://github.com/markorr321/Invoke-IntuneWin32AppRedeploy/blob/main/LICENSE .PROJECTURI https://github.com/markorr321/Invoke-IntuneWin32AppRedeploy .ICONURI .EXTERNALMODULEDEPENDENCIES Microsoft.Graph.Authentication #> [CmdletBinding()] param ( [Alias('Online')] [switch] $fetchOnline, [switch] $excludeSystemApp ) function Invoke-IntuneWin32AppRedeploy { <# .SYNOPSIS Function for forcing redeploy of selected Win32App deployed from Intune. .DESCRIPTION Function for forcing redeploy of selected Win32App deployed from Intune. OutGridView is used to output found Apps. Redeploy means that corresponding registry keys will be deleted from registry and service IntuneManagementExtension will be restarted. .PARAMETER Online Switch for getting Apps and User names from Intune, so locally used IDs can be translated to them. .PARAMETER excludeSystemApp Switch for excluding Apps targeted to SYSTEM. .EXAMPLE Invoke-IntuneWin32AppRedeploy Get and show Win32App(s) deployed from Intune to this computer. Selected ones will be then redeployed. .EXAMPLE Invoke-IntuneWin32AppRedeploy -Online Get and show Win32App(s) deployed from Intune with friendly names. Selected ones will be then redeployed. .NOTES Original Author: @AndrewZtrhgf Updated for Microsoft.Graph module #> [CmdletBinding()] param ( [Alias('Online')] [switch] $fetchOnline, [switch] $excludeSystemApp ) #region helper function function _getTargetName { param ([string] $id) Write-Verbose "Translating $id" if (!$id) { Write-Verbose "id was null" return } elseif ($id -eq 'device') { return 'Device' } $errPref = $ErrorActionPreference $ErrorActionPreference = "Stop" try { if ($id -eq '00000000-0000-0000-0000-000000000000' -or $id -eq 'S-0-0-00-0000000000-0000000000-000000000-000') { return 'Device' } elseif ($id -match "^S-1-5-21") { # it is local account return ((New-Object System.Security.Principal.SecurityIdentifier($id)).Translate([System.Security.Principal.NTAccount])).Value } else { # it is Entra ID account if ($fetchOnline) { return ($intuneUser | Where-Object { $_.Id -eq $id }).UserPrincipalName } else { return $id } } } catch { Write-Warning "Unable to translate $id to account name ($_)" $ErrorActionPreference = $errPref return $id } } function _getIntuneApp { param ([string] $appID) $intuneApp | Where-Object { $_.Id -eq $appID } } #endregion helper function #region prepare if (! ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { throw "Run as administrator" } if ($fetchOnline) { # Ensure NuGet provider is installed if (!(Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { Write-Host "Installing NuGet provider..." -ForegroundColor Yellow Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null } # Check for Microsoft.Graph.Authentication module and install if missing if (!(Get-Module 'Microsoft.Graph.Authentication' -ListAvailable)) { Write-Host "Installing required module: Microsoft.Graph.Authentication..." -ForegroundColor Yellow try { Install-Module 'Microsoft.Graph.Authentication' -Scope AllUsers -Force -AllowClobber -ErrorAction Stop } catch { throw "Failed to install module 'Microsoft.Graph.Authentication': $_" } } # Import the module Write-Host "Loading Microsoft Graph module..." -ForegroundColor Cyan Import-Module 'Microsoft.Graph.Authentication' -ErrorAction Stop # Connect to Microsoft Graph (force fresh interactive authentication) $requiredScopes = @('DeviceManagementApps.Read.All', 'User.Read.All') try { # Disconnect to clear any cached tokens and force fresh login Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Write-Host "Connecting to Microsoft Graph (browser auth)..." -ForegroundColor Cyan Connect-MgGraph -Scopes $requiredScopes -NoWelcome -ErrorAction Stop | Out-Null Write-Host "Connected to Microsoft Graph" -ForegroundColor Green } catch { throw "Failed to connect to Microsoft Graph: $_" } Write-Host "Fetching Intune apps..." -ForegroundColor Cyan $intuneApp = @() $uri = "https://graph.microsoft.com/v1.0/deviceAppManagement/mobileApps?`$select=id,displayName" do { $response = Invoke-MgGraphRequest -Uri $uri -Method GET $intuneApp += $response.value | ForEach-Object { [PSCustomObject]@{ Id = $_.id; DisplayName = $_.displayName } } $uri = $response.'@odata.nextLink' } while ($uri) Write-Host "Found $($intuneApp.Count) apps" -ForegroundColor Green Write-Host "Fetching users..." -ForegroundColor Cyan $intuneUser = @() $uri = "https://graph.microsoft.com/v1.0/users?`$select=id,userPrincipalName" do { $response = Invoke-MgGraphRequest -Uri $uri -Method GET $intuneUser += $response.value | ForEach-Object { [PSCustomObject]@{ Id = $_.id; UserPrincipalName = $_.userPrincipalName } } $uri = $response.'@odata.nextLink' } while ($uri) Write-Host "Found $($intuneUser.Count) users" -ForegroundColor Green } #endregion prepare #region get data $win32App = foreach ($app in (Get-ChildItem "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps" -ErrorAction SilentlyContinue)) { $userEntraObjectID = Split-Path $app.Name -Leaf if ($excludeSystemApp -and $userEntraObjectID -eq "00000000-0000-0000-0000-000000000000") { Write-Verbose "Skipping system deployments" continue } $userWin32AppRoot = $app.PSPath $win32AppIDList = Get-ChildItem $userWin32AppRoot | Select-Object -ExpandProperty PSChildName | ForEach-Object { $_ -replace "_\d+$" } | Select-Object -Unique $win32AppIDList | ForEach-Object { $win32AppID = $_ Write-Verbose "Processing App ID $win32AppID" $newestWin32AppRecord = Get-ChildItem $userWin32AppRoot | Where-Object { $_.PSChildName -Match ([regex]::escape($win32AppID)) } | Sort-Object -Descending -Property PSChildName | Select-Object -First 1 try { $lastUpdatedTimeUtc = Get-ItemPropertyValue $newestWin32AppRecord.PSPath -Name LastUpdatedTimeUtc -ErrorAction Stop } catch { $lastUpdatedTimeUtc = $null } try { $complianceStateMessage = Get-ItemPropertyValue "$($newestWin32AppRecord.PSPath)\ComplianceStateMessage" -Name ComplianceStateMessage -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Write-Verbose "`tUnable to get Compliance State Message data" } $lastError = $complianceStateMessage.ErrorCode if (!$lastError) { $lastError = 0 } if ($fetchOnline) { $property = [ordered]@{ "Scope" = _getTargetName $userEntraObjectID "DisplayName" = (_getIntuneApp $win32AppID).DisplayName "Id" = $win32AppID "LastUpdatedTimeUtc" = $lastUpdatedTimeUtc "ProductVersion" = $complianceStateMessage.ProductVersion "LastError" = $lastError "ScopeId" = $userEntraObjectID } } else { $property = [ordered]@{ "Scope" = _getTargetName $userEntraObjectID "Id" = $win32AppID "LastUpdatedTimeUtc" = $lastUpdatedTimeUtc "ProductVersion" = $complianceStateMessage.ProductVersion "LastError" = $lastError "ScopeId" = $userEntraObjectID } } New-Object -TypeName PSObject -Property $property } } #endregion get data #region let user redeploy chosen app if ($win32App) { $hasDisplayNameProp = $win32App | Get-Member -Name DisplayName $appToRedeploy = $win32App | Where-Object { if ($hasDisplayNameProp) { if ($_.DisplayName) { $true } } else { $true } } | Out-GridView -PassThru -Title "Pick app(s) for redeploy" if (!$appToRedeploy) { Write-Warning "No apps selected for redeploy" return } $win32AppKeys = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps" -Recurse -Depth 2 | Select-Object PSChildName, PSPath, PSParentPath $appToRedeploy | ForEach-Object { $appId = $_.id $scopeId = $_.scopeId if ($scopeId -eq 'device') { $scopeId = "00000000-0000-0000-0000-000000000000" } Write-Warning "Preparing redeploy for app $appId (scope $scopeId)" $win32AppKeyToDelete = $win32AppKeys | Where-Object { $_.PSChildName -Match "^$appId`_\d+" -and $_.PSParentPath -Match "\\$scopeId$" } if ($win32AppKeyToDelete) { $win32AppKeyToDelete | ForEach-Object { Write-Verbose "Deleting $($_.PSPath)" Remove-Item $_.PSPath -Force -Recurse } # Also clear GRS (Global Re-evaluation Schedule) entries for this app # GRS folders are named by hash, but contain a property with the app ID as the name $grsPath = "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps\$scopeId\GRS" if (Test-Path $grsPath) { Get-ChildItem -Path $grsPath | ForEach-Object { $grsProps = $_ | Get-ItemProperty # Check if any property name matches the app ID if ($grsProps.psobject.Properties.Name -contains $appId) { Write-Warning "Deleting GRS entry $($_.PSChildName) for app $appId" Remove-Item $_.PSPath -Force -Recurse } } } } else { throw "BUG??? App $appId with scope $scopeId wasn't found in the registry" } } Write-Warning "Invoking redeploy (by restarting service IntuneManagementExtension). Redeploy can take several minutes!" Restart-Service IntuneManagementExtension -Force } else { Write-Warning "No deployed Win32App detected" } #endregion let user redeploy chosen app } # Auto-execute when script is run directly Invoke-IntuneWin32AppRedeploy @PSBoundParameters |