Invoke-IntuneWin32AppRedeploy.ps1
|
<#PSScriptInfo
.VERSION 1.3.0 .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 Import-Module 'Microsoft.Graph.Authentication' -ErrorAction Stop # Connect to Microsoft Graph (interactive authentication) $requiredScopes = @('DeviceManagementApps.Read.All', 'User.Read.All') try { # Always disconnect and reconnect to ensure fresh authentication Disconnect-MgGraph -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null Connect-MgGraph -Scopes $requiredScopes -NoWelcome -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null } catch { throw "Failed to connect to Microsoft Graph: $_" } Write-Verbose "Getting Intune data" # Get mobile apps using direct REST call (more reliable than cmdlet) $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) # Get users using direct REST call $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) } #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, need to find the hash from logs $grsHash = $null $intuneLogList = Get-ChildItem -Path "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs" -Filter "*.log" -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -ExpandProperty FullName foreach ($intuneLog in $intuneLogList) { $appMatch = Select-String -Path $intuneLog -Pattern "\[Win32App\]\[GRSManager\] App with id: $appId is not expired\." -Context 0, 1 -ErrorAction SilentlyContinue if ($appMatch) { foreach ($match in $appMatch) { $nextLine = Get-Content $intuneLog -ErrorAction SilentlyContinue | Select-Object -Skip $match.LineNumber -First 1 if ($nextLine -match 'Hash\s*=\s*(.+)$') { $grsHash = $matches[1].Trim() break } } if ($grsHash) { break } } } $grsPath = "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps\$scopeId\GRS" if ($grsHash -and (Test-Path $grsPath)) { $grsKeyPath = Join-Path $grsPath $grsHash if (Test-Path $grsKeyPath) { Write-Verbose "Deleting GRS entry $grsKeyPath" Remove-Item $grsKeyPath -Force -Recurse } } elseif (Test-Path $grsPath) { # Fallback: check GRS properties for app ID reference Get-ChildItem -Path $grsPath | ForEach-Object { $grsProps = $_ | Get-ItemProperty $grsKeys = $grsProps.psobject.Properties.Name | Where-Object { $_ -like "*-*-*-*-*" } foreach ($key in $grsKeys) { if ($key -like "*$appId*") { Write-Verbose "Deleting GRS entry $($_.PSPath)" Remove-Item $_.PSPath -Force -Recurse break } } } } } 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 |