!! Updated 2023-07-13 – added additional versions of the functions using the Microsoft Graph PowerShell SDK (v2), as well as adding paging support to the original two functions.
Microsoft recently released a preview capability in Intune to run Remediations (formerly Proactive remediations – stop changing the name of things, Microsoft, really!!) on demand, which is a very cool feature. At the time of writing though, you can’t run a remediation against a co-managed device in the portal – the option is greyed out. You can still do this via MS Graph though.
You can also view Remediation script statuses in the portal as a preview feature, although I’ve found it to be hit and miss – the Remediations blade is there for some devices, and not for others.
I wrote a couple of PowerShell functions that let you invoke a Remediation on demand – whether co-managed or Intune-managed, and also view the Remediation statuses for a device.
The functions can be run against a single device, or multiple devices, and you only need to provide computer names. The function that invokes a Remediation will prompt you for which Remediation to run from those that are available to you. Simply select one and click OK.
The function will return no output if everything works successfully.
The function that displays the Remediations statuses can be piped to “Out-GridView” for an easy way to view the results, or you can filter for a particular workstation or Remediation using PowerShell.
A few things to note:
- These capabilities are in preview and the Graph endpoints are in beta, so there’s no promise something won’t change in the future (and we all know how much Microsoft loves to change things!)
- There are two versions of the functions – one using the Microsoft Graph PowerShell SDK (v2) and the other using the REST API and an access token obtained via the older Intune PowerShell SDK.
- Certain delegated permissions are required, these are listed in each script
- The MS documentation lists the DeviceManagementConfiguration.Read.All or DeviceManagementManagedDevices.Read.All delegated permissions as a requirement for the initiateOnDemandProactiveRemediation endpoint, however I believe this is incorrect and in fact the DeviceManagementManagedDevices.PrivilegedOperations.All permission is required.
- It can take some minutes after invoking a Remediation on-demand for the results to be available.
Here are the functions. The first two are using the Microsoft Graph PowerShell SDK (v2), and the last two use the REST API directly but use the older Intune PowerShell SDK to get an access token – you can get this token any way you want, of course.
function Invoke-MgDeviceRemediationOnDemand { | |
[CmdletBinding()] | |
Param | |
( | |
[Parameter(Mandatory=$true)] | |
[ValidateNotNull()] | |
[ValidateNotNullOrEmpty()] | |
[string[]] | |
$Computername | |
) | |
# Requires the Microsoft Graph PowerShell SDK (Microsoft.Graph.*) | |
# MS Graph required permissions (delegated) | |
# DeviceManagementManagedDevices.Read.All | |
# DeviceManagementConfiguration.Read.All | |
# DeviceManagementManagedDevices.PrivilegedOperations.All | |
Begin | |
{ | |
# Get NuGet | |
$provider = Get-PackageProvider NuGet -ErrorAction Ignore | |
if (-not $provider) | |
{ | |
Write-Host "Installing provider NuGet…" -NoNewline | |
try | |
{ | |
Find-PackageProvider -Name NuGet -ForceBootstrap -IncludeDependencies -Force -ErrorAction Stop -WarningAction SilentlyContinue | |
Write-Host "Success" -ForegroundColor Green | |
} | |
catch | |
{ | |
Write-Host "Failed" -ForegroundColor Red | |
throw $_.Exception.Message | |
} | |
} | |
$module = Import-Module Microsoft.Graph.DeviceManagement -PassThru -ErrorAction Ignore | |
if (-not $module) | |
{ | |
Write-Host "Installing module Microsoft.Graph.DeviceManagement…" -NoNewline | |
try | |
{ | |
Install-Module Microsoft.Graph.DeviceManagement -Scope CurrentUser -Force -ErrorAction Stop -WarningAction SilentlyContinue | |
Write-Host "Success" -ForegroundColor Green | |
} | |
catch | |
{ | |
Write-Host "Failed" -ForegroundColor Red | |
throw $_.Exception.Message | |
} | |
} | |
$module = Import-Module Microsoft.Graph.Beta.DeviceManagement -PassThru -ErrorAction Ignore | |
if (-not $module) | |
{ | |
Write-Host "Installing module Microsoft.Graph.Beta.DeviceManagement…" -NoNewline | |
try | |
{ | |
Install-Module Microsoft.Graph.Beta.DeviceManagement -Scope CurrentUser -Force -ErrorAction Stop -WarningAction SilentlyContinue | |
Write-Host "Success" -ForegroundColor Green | |
} | |
catch | |
{ | |
Write-Host "Failed" -ForegroundColor Red | |
throw $_.Exception.Message | |
} | |
} | |
try | |
{ | |
$null = Connect-MgGraph -Scopes "DeviceManagementManagedDevices.Read.All","DeviceManagementConfiguration.Read.All","DeviceManagementManagedDevices.PrivilegedOperations.All" -ErrorAction Stop | |
} | |
catch | |
{ | |
throw $_.Exception.Message | |
} | |
Function Invoke-IntuneOnDemandRemediation { | |
param($DeviceId,$RemediationId,$DeviceType) | |
if ($DeviceType -eq "CoManaged") | |
{ | |
$URL = "https://graph.microsoft.com/beta/deviceManagement/comanagedDevices('$DeviceID')/initiateOnDemandProactiveRemediation" | |
} | |
else | |
{ | |
$URL = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$DeviceID')/initiateOnDemandProactiveRemediation" | |
} | |
$body = @{ | |
"scriptPolicyId"="$RemediationID" | |
} | ConvertTo-Json | |
try | |
{ | |
$Response = Invoke-MgGraphRequest -Uri $URL -Method POST -Body $Body -ErrorAction Stop | |
} | |
catch | |
{ | |
$Response = $_.Exception.Message | |
} | |
return $Response | |
} | |
# Get list of remediations | |
try | |
{ | |
$result = Get-MgBetaDeviceManagementDeviceHealthScript -Property displayName,version,description,publisher,id -All -ErrorAction Stop | |
} | |
catch | |
{ | |
throw $_.Exception.Message | |
} | |
# Check we have an actual list | |
$Scripts = $Result | Select -Property DisplayName,Version,Description,Publisher,Id | |
if ($Scripts.Count -lt 1) | |
{ | |
Write-Warning "No remediation scripts found" | |
break | |
} | |
# Prompt user to select a script | |
$script:SelectedScript = $Scripts | Sort -Property displayName | Out-GridView -Title "Select a remediation" -OutputMode Single | |
if ($null -eq $SelectedScript) | |
{ | |
Write-Error "No remediation script selected" | |
break | |
} | |
} | |
Process | |
{ | |
foreach ($Computer in $Computername) | |
{ | |
# Find managed device in Graph | |
try | |
{ | |
$Device = Get-MgDeviceManagementManagedDevice -Filter "deviceName eq '$Computer'" -ErrorAction Stop | |
} | |
catch | |
{ | |
Write-Error $_.Exception.Message | |
continue | |
} | |
# Make sure only 1 result returned | |
if ($null -eq $Device) | |
{ | |
Write-Error "Device not found" | |
continue | |
} | |
if ($Device.Count -gt 1) | |
{ | |
Write-Error "Multiple devices found with the name '$Computer'. Device names must be unique." | |
continue | |
} | |
# Invoke the remediation | |
if ($Device.ManagementAgent -match "configurationManager") | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id -DeviceType "CoManaged" | |
} | |
else | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id | |
} | |
If ($null -ne $result) | |
{ | |
# Try the other managament agent option | |
if ($Device.ManagementAgent -match "configurationManager") | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id | |
} | |
else | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id -DeviceType "CoManaged" | |
} | |
If ($null -ne $result) | |
{ | |
Write-Error $result | |
continue | |
} | |
} | |
} | |
} | |
End | |
{ | |
$null = Disconnect-Graph | |
} | |
} | |
# Example usage | |
Invoke-MgDeviceRemediationOnDemand -Computername "PC001","PC002" |
Function Get-MgDeviceRemediationsStatus { | |
[CmdletBinding()] | |
Param | |
( | |
[Parameter(Mandatory=$true)] | |
[ValidateNotNull()] | |
[ValidateNotNullOrEmpty()] | |
[string[]] | |
$Computername | |
) | |
# Requires the Microsoft Graph PowerShell SDK (Microsoft.Graph.*) | |
# MS Graph required permissions (delegated) | |
# DeviceManagementManagedDevices.Read.All | |
# DeviceManagementConfiguration.Read.All | |
Begin | |
{ | |
# Get NuGet | |
$provider = Get-PackageProvider NuGet -ErrorAction Ignore | |
if (-not $provider) | |
{ | |
Write-Host "Installing provider NuGet…" -NoNewline | |
try | |
{ | |
Find-PackageProvider -Name NuGet -ForceBootstrap -IncludeDependencies -Force -ErrorAction Stop -WarningAction SilentlyContinue | |
Write-Host "Success" -ForegroundColor Green | |
} | |
catch | |
{ | |
Write-Host "Failed" -ForegroundColor Red | |
throw $_.Exception.Message | |
} | |
} | |
$module = Import-Module Microsoft.Graph.DeviceManagement -PassThru -ErrorAction Ignore | |
if (-not $module) | |
{ | |
Write-Host "Installing module Microsoft.Graph.DeviceManagement…" -NoNewline | |
try | |
{ | |
Install-Module Microsoft.Graph.DeviceManagement -Scope CurrentUser -Force -ErrorAction Stop -WarningAction SilentlyContinue | |
Write-Host "Success" -ForegroundColor Green | |
} | |
catch | |
{ | |
Write-Host "Failed" -ForegroundColor Red | |
throw $_.Exception.Message | |
} | |
} | |
$module = Import-Module Microsoft.Graph.Beta.DeviceManagement -PassThru -ErrorAction Ignore | |
if (-not $module) | |
{ | |
Write-Host "Installing module Microsoft.Graph.Beta.DeviceManagement…" -NoNewline | |
try | |
{ | |
Install-Module Microsoft.Graph.Beta.DeviceManagement -Scope CurrentUser -Force -ErrorAction Stop -WarningAction SilentlyContinue | |
Write-Host "Success" -ForegroundColor Green | |
} | |
catch | |
{ | |
Write-Host "Failed" -ForegroundColor Red | |
throw $_.Exception.Message | |
} | |
} | |
try | |
{ | |
$null = Connect-MgGraph -Scopes "DeviceManagementManagedDevices.Read.All","DeviceManagementConfiguration.Read.All" -ErrorAction Stop | |
} | |
catch | |
{ | |
throw $_.Exception.Message | |
} | |
} | |
Process | |
{ | |
foreach ($Computer in $Computername) | |
{ | |
# Find managed device in Graph | |
try | |
{ | |
$Device = Get-MgDeviceManagementManagedDevice -Filter "deviceName eq '$Computer'" -ErrorAction Stop | |
} | |
catch | |
{ | |
Write-Error $_.Exception.Message | |
continue | |
} | |
# Make sure only 1 result returned | |
if ($null -eq $Device) | |
{ | |
Write-Error "Device not found" | |
continue | |
} | |
if ($Device.Count -gt 1) | |
{ | |
Write-Error "Multiple devices found with the name '$Computer'. Device names must be unique." | |
continue | |
} | |
# Get the remediations status | |
try | |
{ | |
$result = Get-MgBetaDeviceManagementManagedDeviceHealthScriptState -ManagedDeviceId $Device.id -All -ErrorAction Stop | |
} | |
catch | |
{ | |
Write-Error $_.Exception.Message | |
continue | |
} | |
# Select only the desired properties | |
if ($result.Count -lt 1) | |
{ | |
Write-Warning "No remediations status found for '$Computer'" | |
continue | |
} | |
$result = $result | Select -Property * -Unique | |
$Properties = @("DeviceName","PolicyName","UserName","LastStateUpdateDateTime","DetectionState","RemediationState","PreRemediationDetectionScriptOutput","PreRemediationDetectionScriptError","RemediationScriptError","PostRemediationDetectionScriptOutput","PostRemediationDetectionScriptError") | |
($result | Select -Property $Properties | Sort PolicyName) | |
} | |
} | |
End | |
{ | |
$null = Disconnect-MgGraph | |
} | |
} | |
# Example usage | |
Get-MgDeviceRemediationsStatus -Computername "PC001","PC002" | Out-GridView |
function Invoke-IntuneRemediationOnDemand { | |
[CmdletBinding()] | |
Param | |
( | |
[Parameter(Mandatory=$true)] | |
[ValidateNotNull()] | |
[ValidateNotNullOrEmpty()] | |
[string[]] | |
$Computername | |
) | |
# Requires the Intune PowerShell SDK (Microsoft.Graph.Intune) | |
# MS Graph required permissions (delegated): | |
# DeviceManagementManagedDevices.Read.All | |
# DeviceManagementConfiguration.Read.All | |
# DeviceManagementManagedDevices.PrivilegedOperations.All | |
Begin | |
{ | |
$script:GraphToken = Connect-MSGraph -PassThru | |
$ProgressPreference = 'SilentlyContinue' | |
Function script:Invoke-LocalGraphRequest { | |
Param ($URL,$Headers,$Method,$Body) | |
try | |
{ | |
If ($Method -eq "POST") | |
{ | |
$WebRequest = Invoke-WebRequest -Uri $URL -Method $Method -Headers $Headers -Body $Body -ContentType "application/json" -UseBasicParsing | |
} | |
else | |
{ | |
$WebRequest = Invoke-WebRequest -Uri $URL -Method $Method -Headers $Headers -UseBasicParsing | |
} | |
} | |
catch | |
{ | |
$Response = $_ | |
$WebRequest = [PSCustomObject]@{ | |
Message = $response.Exception.Message | |
StatusCode = $response.Exception.Response.StatusCode | |
StatusDescription = $response.Exception.Response.StatusDescription | |
} | |
} | |
Return $WebRequest | |
} | |
Function Get-IntuneManagedDevices { | |
param($DeviceName) | |
$URL = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?`$filter=deviceName eq '$DeviceName'" | |
$headers = @{'Authorization'="Bearer " + $GraphToken} | |
$GraphRequest = Invoke-LocalGraphRequest -URL $URL -Headers $headers -Method GET | |
return $GraphRequest | |
} | |
Function Get-IntuneDeviceHealthScripts { | |
param($URL) | |
If ($null -eq $URL) | |
{ | |
$URL = "https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts" | |
} | |
$headers = @{'Authorization'="Bearer " + $GraphToken} | |
$GraphRequest = Invoke-LocalGraphRequest -URL $URL -Headers $headers -Method GET | |
return $GraphRequest | |
} | |
Function Invoke-IntuneOnDemandRemediation { | |
param($DeviceId,$RemediationId,$DeviceType) | |
if ($DeviceType -eq "CoManaged") | |
{ | |
$URL = "https://graph.microsoft.com/beta/deviceManagement/comanagedDevices('$DeviceID')/initiateOnDemandProactiveRemediation" | |
} | |
else | |
{ | |
$URL = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$DeviceID')/initiateOnDemandProactiveRemediation" | |
} | |
$headers = @{'Authorization'="Bearer " + $GraphToken} | |
$body = @{ | |
"scriptPolicyId"="$RemediationID" | |
} | ConvertTo-Json | |
$GraphRequest = Invoke-LocalGraphRequest -URL $URL -Headers $headers -Body $Body -Method POST | |
return $GraphRequest | |
} | |
# Get list of remediations | |
$result = Get-IntuneDeviceHealthScripts | |
if ($result.StatusCode -ne 200) | |
{ | |
Write-Error $result | |
break | |
} | |
# Do paging | |
$Content = $result.Content | ConvertFrom-Json | |
$ScriptList = [System.Collections.Generic.List[Object]]::new() | |
$ScriptList.AddRange($Content.value) | |
If ($null -ne $Content.'@odata.nextLink') | |
{ | |
do | |
{ | |
$result = Get-IntuneDeviceHealthScripts -URL $Content.'@odata.nextLink' | |
if ($result.StatusCode -ne 200) | |
{ | |
Write-Error $result | |
continue | |
} | |
$Content = $result.Content | ConvertFrom-Json | |
$ScriptList.AddRange($Content.value) | |
} | |
until ($null -eq $Content.'@odata.nextLink') | |
} | |
# Check we have an actual list | |
$Scripts = $ScriptList | Select -Property displayName,version,description,publisher,id | |
if ($Scripts.Count -lt 1) | |
{ | |
Write-Warning "No remediation scripts found" | |
break | |
} | |
# Prompt user to select a script | |
$script:SelectedScript = $Scripts | Sort -Property displayName | Out-GridView -Title "Select a remediation" -OutputMode Single | |
if ($null -eq $SelectedScript) | |
{ | |
Write-Error "No remediation script selected" | |
break | |
} | |
} | |
Process | |
{ | |
foreach ($Computer in $Computername) | |
{ | |
# Find managed device in Graph | |
$result = Get-IntuneManagedDevices -DeviceName "$Computer" | |
if ($result.StatusCode -ne 200) | |
{ | |
Write-Error $result | |
continue | |
} | |
# Make sure only 1 result returned | |
$Device = $result.Content | ConvertFrom-Json | Select -ExpandProperty value | |
if ($null -eq $Device) | |
{ | |
Write-Error "Device not found" | |
continue | |
} | |
if ($Device.Count -gt 1) | |
{ | |
Write-Error "Multiple devices found with the name '$Computer'. Device names must be unique." | |
continue | |
} | |
# Invoke the remediation | |
if ($Device.managementAgent -match "configurationManager") | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id -DeviceType "CoManaged" | |
} | |
else | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id | |
} | |
If ($result.StatusCode -ne 204) | |
{ | |
# Try the other managament agent option | |
if ($Device.managementAgent -match "configurationManager") | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id | |
} | |
else | |
{ | |
$result = Invoke-IntuneOnDemandRemediation -DeviceID $Device.id -RemediationID $SelectedScript.id -DeviceType "CoManaged" | |
} | |
If ($result.StatusCode -ne 204) | |
{ | |
Write-Error $result | |
continue | |
} | |
} | |
} | |
} | |
End | |
{ | |
} | |
} | |
# Example usage | |
Invoke-IntuneRemediationOnDemand -Computername "PC001","PC002" |
Function Get-IntuneDeviceRemediationsStatus { | |
[CmdletBinding()] | |
Param | |
( | |
[Parameter(Mandatory=$true)] | |
[ValidateNotNull()] | |
[ValidateNotNullOrEmpty()] | |
[string[]] | |
$Computername | |
) | |
# Requires the Intune PowerShell SDK (Microsoft.Graph.Intune) | |
# MS Graph required permissions (delegated) | |
# DeviceManagementManagedDevices.Read.All | |
# DeviceManagementConfiguration.Read.All | |
Begin | |
{ | |
$script:GraphToken = Connect-MSGraph -PassThru | |
$ProgressPreference = 'SilentlyContinue' | |
Function script:Invoke-LocalGraphRequest { | |
Param ($URL,$Headers,$Method) | |
try | |
{ | |
$WebRequest = Invoke-WebRequest -Uri $URL -Method $Method -Headers $Headers -UseBasicParsing | |
} | |
catch | |
{ | |
$Response = $_ | |
$WebRequest = [PSCustomObject]@{ | |
Message = $response.Exception.Message | |
StatusCode = $response.Exception.Response.StatusCode | |
StatusDescription = $response.Exception.Response.StatusDescription | |
} | |
} | |
Return $WebRequest | |
} | |
Function Get-IntuneManagedDevices { | |
param($DeviceName) | |
$URL = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?`$filter=deviceName eq '$DeviceName'" | |
$headers = @{'Authorization'="Bearer " + $GraphToken} | |
$GraphRequest = Invoke-LocalGraphRequest -URL $URL -Headers $headers -Method GET | |
return $GraphRequest | |
} | |
Function Get-IntuneSingleDeviceRemediationStatus | |
{ | |
param($DeviceId,$URL) | |
If ($null -eq $URL) | |
{ | |
$URL = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$DeviceID')/deviceHealthScriptStates" | |
} | |
$headers = @{'Authorization'="Bearer " + $GraphToken} | |
$GraphRequest = Invoke-LocalGraphRequest -URL $URL -Headers $headers -Method GET | |
return $GraphRequest | |
} | |
} | |
Process | |
{ | |
foreach ($Computer in $Computername) | |
{ | |
# Find managed device in Graph | |
$result = Get-IntuneManagedDevices -DeviceName "$Computer" | |
if ($result.StatusCode -ne 200) | |
{ | |
Write-Error $result | |
continue | |
} | |
# Make sure only 1 result returned | |
$Device = $result.Content | ConvertFrom-Json | Select -ExpandProperty value | |
if ($null -eq $Device) | |
{ | |
Write-Error "Device not found" | |
continue | |
} | |
if ($Device.Count -gt 1) | |
{ | |
Write-Error "Multiple devices found with the name '$Computer'. Device names must be unique." | |
continue | |
} | |
# Get the remediations status | |
$result = Get-IntuneSingleDeviceRemediationStatus -DeviceID $Device.id | |
if ($result.StatusCode -ne 200) | |
{ | |
Write-Error $result | |
continue | |
} | |
# Do paging | |
$Content = $result.Content | ConvertFrom-Json | |
$Statuses = [System.Collections.Generic.List[Object]]::new() | |
$Statuses.AddRange($Content.value) | |
If ($null -ne $Content.'@odata.nextLink') | |
{ | |
do | |
{ | |
$result = Get-IntuneSingleDeviceRemediationStatus -URL $Content.'@odata.nextLink' | |
if ($result.StatusCode -ne 200) | |
{ | |
Write-Error $result | |
continue | |
} | |
$Content = $result.Content | ConvertFrom-Json | |
$Statuses.AddRange($Content.value) | |
} | |
until ($null -eq $Content.'@odata.nextLink') | |
} | |
# Select only the desired properties | |
if ($Statuses.Count -lt 1) | |
{ | |
Write-Warning "No remediations status found for '$Computer'" | |
continue | |
} | |
$Statuses = $Statuses | Select -Property * -Unique | |
$Properties = @("deviceName","policyName","userName","lastStateUpdateDateTime","detectionState","remediationState","preRemediationDetectionScriptOutput","preRemediationDetectionScriptError","remediationScriptError","postRemediationDetectionScriptOutput","postRemediationDetectionScriptError") | |
($Statuses | Select -Property $Properties | Sort policyName) | |
} | |
} | |
End | |
{ | |
} | |
} | |
# Example usage | |
Get-IntuneDeviceRemediationsStatus -Computername "PC001","PC002" | Out-GridView |