Deploy Custom Toast Notifications with Intune – Part 2

This is a continuation of where we left off in part one. Part one covered the structure of toast notifications, how we can craft customized notification content, and how to trigger a toast notification using PowerShell. We also reviewed how to use the action buttons to run PowerShell scripts. Part two of this post covers how we deploy custom notifications to our endpoints. All scripts and files used in these posts are available on my github.

Deploying the Necessary Files

The more customization you add to your notification, the more files the notification will be dependent on. In our demonstration, we have .png files to display custom images, three PowerShell scripts to use with each of the three action buttons, and we also need to add the reg keys and a CMD file used for our custom protocol handler. You can change the locations of the files if you’d like, but I’ll have them stored in c:\programdata\toast. Since we are using Intune, we are assuming devices are AADJ with no line of sight to SMB file shares. So, we need these files stored locally on their devices. If you are still in an office setting with SMB shares, you can certainly use an SMB share. A quick review of our files is below:

ToastScript.cmdThe file called by the PowerShell Protocol Handler, which enables us to use PowerShell scripts behind the action buttons
NotificationXML.xmlOur XML payload file with all the configuration and settings for our toast notification
Button1-Downloads.ps1Action button 1 script to open the user’s downloads folder in file explorer
Button2-Gridview.ps1Action button 2 script to show the affected files in a PowerShell gridview, and allow them to delete from there
Button3-Delete.ps1Action button 3 script to auto-delete the files in the downloads folder
Smb.pngSmall image file. Change this to be an image you want
Smbnewbanner.pngLarge “hero” image file. Change this to be an image you want

We also need to add the necessary REG keys for the PowerShell Protocol handler. For that, we will use PowerShell:

New-PSDrive -PSProvider Registry -Name HKCR -Root HKEY_CLASSES_ROOT
$RegPath = "HKCR:\powershell\shell\open\command"
$command = "c:\ProgramData\Toast\ToastScript.cmd %1"
$defaulticon = "HKCR:\powershell\defaulticon\"
New-Item $regpath -Force
New-Item $defaulticon -force
New-ItemProperty -Path HKCR:\powershell -Name "URL Protocol" -Force
Set-ItemProperty -Path $RegPath -Name '(Default)' -Value $command -Force
Set-ItemProperty -Path $defaulticon -Name '(Default)' -Value 'powershell.exe,1' -Force
Set-ItemProperty -Path HKCR:\powershell -Name ‘(Default)’ -Value 'URL:PowerShell Protocol' -Force

We will package and deploy these files as a Win32 app. This will use a PowerShell script to add the protocol handler reg keys, and also move our files into the programdata\toast directory. Once we have the reg keys and files in place, we can use Proactive Remediations or Scheduled Tasks to regularly send toast notifications. Our Install script is below, which copies the needed files into the programdata\toast folder, and also adds the reg keys for the custom protocol handler. We also have an uninstall script to remove the files and reg keys if we need to.

Install Script:

$toastdir = "c:\programdata\toast"
New-Item -Path $toastdir -ItemType "Directory" -Force
Copy-Item ".\toastscript.cmd" -Destination $toastdir -Force
Copy-Item ".\notificationXML.xml" -Destination $toastdir -Force
Copy-Item ".\button1-downloads.ps1" -Destination $toastdir -Force
Copy-Item ".\button2-gridview.ps1" -Destination $toastdir -Force
Copy-Item ".\button3-delete.ps1" -Destination $toastdir -Force
Copy-Item ".\smb.png" -Destination $toastdir -Force
Copy-Item ".\smbnewbanner.png" -Destination $toastdir -Force

New-PSDrive -PSProvider Registry -Name HKCR -Root HKEY_CLASSES_ROOT
$RegPath = "HKCR:\powershell\shell\open\command"
$command = "c:\ProgramData\Toast\ToastScript.cmd %1"
$defaulticon = "HKCR:\powershell\defaulticon\"
New-Item $regpath -Force
New-Item $defaulticon -force
New-ItemProperty -Path HKCR:\powershell -Name "URL Protocol" -Force
Set-ItemProperty -Path $RegPath -Name '(Default)' -Value $command -Force
Set-ItemProperty -Path $defaulticon -Name '(Default)' -Value 'powershell.exe,1' -Force
Set-ItemProperty -Path HKCR:\powershell -Name '(Default)' -Value 'URL:PowerShell Protocol' -Force

Uninstall Script:

$toastdir = c:\programdata\toast
Remove-Item -Path "$toastdir\toastscript.cmd" -Force
Remove-Item -Path "$toastdir\notificationXML.xml" -Force
Remove-Item -Path "$toastdir\button1-downloads.ps1" -Force
Remove-Item -Path "$toastdir\button2-gridview.ps1" -Force
Remove-Item -Path "$toastdir\button3-delete.ps1" -Force

New-PSDrive -PSProvider Registry -Name HKCR -Root HKEY_CLASSES_ROOT
Remove-Item -Path "HKCR:\powershell" -Recurse -Force

Wrap all these files as a .intunewin file along with our install and uninstall scripts:

Create the new app. Since this does a couple of different things and is only a component of the overall solution, I provided a detailed description.

Our Install/Uninstall commands are below. Since we are adding reg keys and copying data to the %programdata% folder, we need to run this as system:

  • %windir%\sysnative\WindowsPowerShell\v1.0\powershell.exe -Executionpolicy Bypass .\Win32-Install.ps1
  • %windir%\sysnative\WindowsPowerShell\v1.0\powershell.exe -Executionpolicy Bypass .\Win32-uninstall.ps1

Add whichever requirement rules you’d like and move on to detection rules. Since there are a bunch of files and reg values to check, I am using a custom detection script:

Here is the custom detection script:

$file1 = Test-Path "C:\ProgramData\Toast\ToastScript.cmd"
$file2 = Test-Path "C:\ProgramData\Toast\NotificationXML.xml"
$file3 = Test-Path "C:\ProgramData\Toast\Button1-Downloads.ps1"
$file4 = Test-Path "C:\ProgramData\Toast\Button2-Gridview.ps1"
$file5 = Test-Path "C:\ProgramData\Toast\Button3-Delete.ps1"
$file6 = Test-Path "C:\ProgramData\Toast\smb.png"
$file7 = Test-Path "C:\ProgramData\Toast\smbnewbanner.png"

New-PSDrive -PSProvider Registry -Name HKCR -Root HKEY_CLASSES_ROOT
$reg1 = Get-ItemPropertyValue "HKCR:\PowerShell" -Name "(Default)"
$reg2 = Get-ItemPropertyValue "HKCR:\PowerShell\defaulticon" -Name "(Default)"
$reg3 = Get-ItemPropertyValue "HKCR:\PowerShell\shell\open\command" -Name "(Default)"

If (((( $file1 -and $file2 -and $file3 -and $file4 -and $file5 -and $file6 -and $file7 -eq 'True' ) -and ($reg1 -eq "URL:PowerShell Protocol")) -and ($reg2 -eq "powershell.exe,1")) -and ($reg3 -eq "C:\ProgramData\toast\ToastScript.cmd %1"))
{
    Write-Output "Files and Reg values exist"
    exit 0
}
else {
    Write-Output "Missing Files or Registry Values"
    exit 1
}

Deploy to your target machines so they have the necessary files. Next, we need to move on to scheduling a regular check against the user downloads folder, which will trigger a toast notification if they have 30+ day old files larger than 500 MB. For the sake of testing and demonstrating, I have my scripts set to 1 day and 1 MB to make it easy to test on my lab machines. We have two ways to deploy this to our target devices – Proactive Remediation or Scheduled Task.

Toast Notifications Using Proactive Remediations

Assuming you have the proper licensing to use proactive remediations, this is a much easier method than using a scheduled task. I have my detection and remediation scripts below. The detection script is checking to see if there are files that fit the criteria of the toast notification, and the remediation script triggers the toast if files are detected.

Detection Script:

$DownloadsFiles = get-childitem -Path $env:USERPROFILE\Downloads -File -Recurse | Where-Object {$_.Length -gt 1MB} | Where-Object {$_.CreationTime -lt (Get-Date).AddDays(-1)}
If ($DownloadsFiles.count -eq 0) {
    Write-Output "No large 30 day old files found in Downloads folder"
    exit 0
}
else {
    Write-Output "Large 30+ old files in user downloads folder - sending toast notification"
    exit 1
}

Remediation Script:

$AppId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
$XMLString = Get-Content -Path c:\programdata\toast\NotificationXML.xml
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
$ToastXml = [Windows.Data.Xml.Dom.XmlDocument]::new()
$ToastXml.LoadXml($XmlString)
$Toast = [Windows.UI.Notifications.ToastNotification]::new($ToastXml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($Toast)

Navigate to the Intune admin dashboard, and select Reports > Endpoint Analytics > Proactive Remediations. Select Create Script Package:

Provide a name and description. Under Settings, upload the detection and remediation script.

Assign to your users, and set the schedule for how often you want this to run:

Here is an example of the notification center showing the proactive remediation triggering a Toast Notification at our scheduled time of 7:15 AM:

Toast Notifications using a Scheduled Task

If you lack the licensing to use proactive remediations, we can also deploy a scheduled task as a Win32 app. I’ve written a detailed blog on how to do this here, so I will not go into all the details. This is still reliant on the first step of deploying the necessary files to the machines, so we will make that Win32 app a dependency for this one. As a reminder, we have those files being copied to the c:\programdata\toast directory, so we’re assuming they already exist on our target devices:

Create a scheduled task on a machine with the settings you want and export the task as an XML file. Toast notifications must run as the signed-in user, so we need to remember to select the BUILTIN\Users group for who will run the task. The XML file along with all others is on my github if you want to use it as-is. It’s set to run every Friday at 9 AM and the trigger file is also located in the c:\programdata\toast directory.

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2023-01-12T06:02:29.4333371</Date>
    <Author>SMBtotheCloud</Author>
    <URI>\ToastNotification-DownloadsFolder</URI>
  </RegistrationInfo>
  <Triggers>
    <CalendarTrigger>
      <StartBoundary>2023-01-12T09:00:00</StartBoundary>
      <Enabled>true</Enabled>
      <ScheduleByWeek>
        <DaysOfWeek>
          <Friday />
        </DaysOfWeek>
        <WeeksInterval>1</WeeksInterval>
      </ScheduleByWeek>
    </CalendarTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <GroupId>S-1-5-32-545</GroupId>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
    <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>powershell.exe</Command>
      <Arguments>-WindowStyle Hidden -file C:\ProgramData\Toast\TriggerToast-DownloadsFolder.ps1</Arguments>
    </Exec>
  </Actions>
</Task>

Next, we need our Win32 app installation script. We need it to do two things:

  • Copy our action script to the c:\programdata\toast directory
  • Create the scheduled task from our XML file

Here is our install script for the Win32 app:

$taskdir = "C:\programdata\toast"
Copy-Item ".\TriggerToast-DownloadsFolder.ps1" -Destination $taskdir -Force
Register-ScheduledTask -xml (Get-Content '.\Toast-DownloadsFolder.xml' | Out-String) -TaskName "Toast-DownloadsFolder" -Force

And the uninstall script:

remove-item C:\programdata\toast\TriggerToast-DownloadsFolder.ps1 -force
unregister-scheduledtask -taskname Toast-DownloadsFolder -confirm:$false

Let’s package the install script, uninstall script, scheduled task XML file, and scheduled task action script as a Win32 app:

Create a new Win32 app and upload the .intunewin file. Provide a name & description:

Here are the install and uninstall scripts. We also want to make sure we run as system:

  • Install script: %windir%\sysnative\WindowsPowerShell\v1.0\powershell.exe -Executionpolicy Bypass .\Win32-Install-ScheduledTask.ps1
  • Uninstall script: %windir%\sysnative\WindowsPowerShell\v1.0\powershell.exe -Executionpolicy Bypass .\Win32-uninstall-ScheduledTask.ps1

Add your requirements. For detection rules, we have two. One to make sure the scheduled task exists, and the other to make sure our action script exists. The first detection screenshot is not a typo. We do not want a file extension on the file we are looking for. When a scheduled task is imported from an XML file, it strips the file extension and adds the file to the %windir%\system32\tasks folder. If we add the XML file extension, the detection rule will fail.

Add a dependency for the app we created in the first section of this post, which copied the other necessary files to our endpoints:

Assign to your target devices and complete the setup. After our devices check-in, we should see successful installs and the task existing on our client machines:

Your scheduled task is finished and ready to go.

Drawbacks and things to keep in mind:

If you have a use case for custom toast notifications, they are a great tool. However, there are some drawbacks and things you should remember if you use the methods from this post:

  • The files are dependent on each other and there are many moving pieces. The Win32 app that deploys our files contains several files all necessary for the notification to work. It’s important to make sure your script action buttons coincide with your notification script. It’s also important to make sure your uninstall rules are accurate. If you want to remove a notification, you’ll want all the files removed from the target systems.
  • Using this method, updating the notifications or making small adjustments will take some work since the detection rules will already be satisfied from previous deployments. You’ll either need to make new apps, adjust your detection rules, or uninstall the Win32 app, make your changes and then reinstall.

The BurntToast PowerShell Module

I came across BurntToast during my research and tested it out. It works great and there is already a ton of documentation out there on it, so I am not going to go into much detail. If you have a small number of endpoints or don’t want to mess with XML files, this is probably a much more efficient way for you to deliver toast notifications. Here are some links to get started if you want to try out BurntToast: