As I’m sure some of you know, I’m a big fan of Powershell scripting and find most tasks can be done far more quickly with a script than manually.
Whilst the Powershell scripts within Intune work nicely, they are run-once scripts (unless you want to start deleting registry keys) and sometimes you want a script which runs regularly.
This is where Proactive Remediations fits in nicely, think of it as Scheduled Tasks for Intune (but with some added logic), but they can be difficult to get your head around at first so I’ll go through how they work and the steps involved in this post.
Firstly, if you’ve never used them before, you’ll find them hidden in the Reports Menu (Reports – Endpoint Analytics – Proactive Remediations):

The first time you load it, you’ll have to confirm you are licensed to use it, I’m assuming if you’re reading this that you must be.
Once inside, it’s split into 3 sections: Detection Script, Remediation Script and Assigment (which includes how often it runs)
Detection Script
This is the logic that decides if the second remediation script will run or not and it can be absolutely anything you can query with powershell.
What you are looking at here are the exit codes. If Intune is given an exit code of 0, it will NOT run the remediation script, this is a clean exit and the machine has (or doesn’t have) whatever you are looking for.
On the other hand, if it finds the exit code is 1, this will trigger the next script to run.
For example, if I’m looking for a particular registry key (in this case fastboot), the script will look like this:
$Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power"
$Name = "HiberbootEnabled"
$Type = "DWORD"
$Value = 0
Try {
$Registry = Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop | Select-Object -ExpandProperty $Name
If ($Registry -eq $Value){
Write-Output "Compliant"
Exit 0
}
Write-Warning "Not Compliant"
Exit 1
}
Catch {
Write-Warning "Not Compliant"
Exit 1
}
At the top we just set what we are expecting to see in a good machine.
Then we check if the registry key exists at all with the Try code block. Obviously if it doesn’t exist, we catch the error and error out with a code 1:
}
Write-Warning "Not Compliant"
Exit 1
}
If the key exists, we then query the value of it to make sure it matches what we are looking for. If we find a match, exit with a code 0, that machine is fine and does not require anything else to run. If the value doesn’t match, we need to sort that!
Registry key is just one example though, we could also use:
Does a file exist?
$File = "C:\windows\system32\notepad.exe"
if (!$file) {
write-host "Not found"
exit 1
}
else {
write-host "Found"
exit 0
}
Or machines which haven’t rebooted in the last 3 days:
$uptime = get-uptime
if ($uptime.days -gt 3) {
write-host "Not rebooted in last 3 days"
exit 1
}
else {
write-host "All fine, recently rebooted"
exit 0
}
I’m sure you get the idea!
Remediation Script
This is the part which actually fixes whatever you were initially looking for. We don’t have to worry about exit codes for this one, it runs regardless.
Logging can be found in the Intunemanagementextension.log file, but for ease, I prefer to add my own logging via the start-transcript functionality. I often add the same logic into the remediation script as well just to be extra careful, all it takes is one typo in the detection and it could remediate everything!
So in the case of the fastboot, we simply set the reg key:
Start-Transcript -Path $env:TEMP\DisableFastBoot.txt
if((Test-Path -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power") -ne $true) {
write-host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power does not exist"
New-Item "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power" -force -ea SilentlyContinue
write-host "Key Created" };
New-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power' -Name 'HiberbootEnabled' -Value 0 -PropertyType DWord -Force -ea SilentlyContinue;
write-host "Value Set"
Stop-Transcript
For the missing file, we could download it from GitHub/Azure Blob etc.
Start-Transcript -Path $env:TEMP\copyfile.log
$File = "C:\windows\system32\notepad.exe"
if (!$file) {
write-host "File not found, downloading"
$fileurl = "https://my.file.to.download"
#Set the download location
$output = "c:\my.location\my.file.to.download"
#Download it
Invoke-WebRequest -Uri $appurl -OutFile $output -Method Get
write-host "file downloaded to $output"
}
Stop-Transcript
Or for the machine that hasn’t restarted, we could force a reboot
shutdown /t 300 /r /c "Your machine will restart in 5 minutes, please save any work now"
Scheduling
Once your scripts are created and tested, the next step is to assign them to a group, all users etc.
Once assigned, navigate back to the assignment screen and you can see your options for scheduling (and filtering if required)
Your options here are:
Once (specify date and time)
Hourly (specify how many hours apart between runs)
Daily (specify how many days apart and at what time)

Hopefully that has taken some of the mystery away and everyone will start to use this excellent feature! If there are any particular remediations you want me to add code for, just let me know in the comments.
great article. something I’d like to point out it’s the Invoke-WebRequest usage
I replace by azcopy https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-v10
I’ve noticed Invoke-WebRequest breaking a lot regardless of some tweaks around http/https and TLS settings when downloading from azure blob storage
I place c:\programdata\%workingdir%\azcopy and from there I call the files I need to download and validate files hash every time to make sure the file is ok then I proceed with all other requirements.
Thanks Thiago! I’m a big fan of azcopy and use it regularly on my machines, just a shame I can’t script the installation of it without ending up using an invoke-webrequest to grab it from somewhere.
Ideally a powershell script or module for AZCopy would make life a lot easier.
Is that just a matter of specifying TLS 1.2? I’ve incorporated the following into any script that has to make web or API calls, just because of the unpredictability, and I haven’t had TLS related errors since.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Hi Andrew
Proactive remediation has become one of my favourite tools in MEM. I do have one pretty niche requirement for a customer where they want to be able to apply settings based on the “Primary User” attribute in MEM.
Is there any way “Primary User” can be injected into the detection and remediation script?
Hi,
Would it be a user or device level script?
You can detect the primary user in graph or on the device itself, so it should be possible, but the method depends on what the script is doing afterwards
Hi Andrew,
I am wondering if a remediation script is used for uninstallation of unwanted software, how we can have a detailed report how many times the script has been removed this SW from one and the same device for a period of time?
I think that I can create a reg key which flags the existence of the SW before the removal and with second remediation script to catch this reg key and to take an info about the device in a csv file on the blob..
But is there more elegant solution for detailed report?
Hi,
The device status will show you the results and should repeat for each machine.
You can also access the output of a proactive remediation script so another option would be:
1) Set a reg key with a value of 1
2) Each time it runs, read the reg key, increment and replace the key value
3) Output the reg key value at the end.
That way it will show you how many times it has run against each machine
Thanks for the quick response, Andrew.
Your solution is better, because all can be implemented in only 1 proactive remediation script.
And also do you think that MS Graph can be used for such kind of detailed reports?
You could probably work with this part:
https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts/{ProacID}/deviceRunStates
The ID of each item is the script ID followed by the device ID
great! Thanks! I will try that.