Disclaimer! Do this at your own risk. I do not take any responsibility in impact this may cause for you or your tenant.

Update: Now that Microsoft has released their own “fix” for this issue, I would advise to test that out first. Though their fix do not support all shortcuts, so this solution can be used to complement with other company specific applications.

Firstly, make sure that your ASR rule for Block Win32 API calls from Office macro has been changed to Audit mode. In the bottom of the article you can see how to verify it is reflected on the computer.

To identify what shortcuts were deleted in your tenant, run the following in Advanced Hunting

DeviceEvents
| where ActionType == "AsrOfficeMacroWin32ApiCallsBlocked" and Timestamp >= datetime("2023-01-13 00:00:00Z")
| order by Timestamp
| where FileName endswith ".lnk"
| summarize count() by FileName
| sort by count_

The output should be something similar to

Stable method

We have experienced that the most stable method of fixing shortcuts is using a modified version of Microsoft’s script using Proactive Remediations to run the script. This modified script checks if the shortcuts exist AND if they are pointing correctly – this so that it re-runs if there are any broken shortcuts. If one or more shortcuts are not “healthy” the remediation script is run.

You can add your own applications to the Detection and Remediation script by finding the correct names in the registry under “HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\”

$programs = @{
    "Adobe Acrobat"                = "Acrobat.exe"
    "[Adobe Photoshop]"            = "photoshop.exe"
    "[Adobe Illustrator]"          = "illustrator.exe"
    "Adobe Creative Cloud"         = "Creative Cloud.exe"
    "Adobe Substance 3D Painter"   = "Adobe Substance 3D Painter.exe"
    "Firefox Private Browsing"     = "private_browsing.exe"
    "Firefox"                      = "firefox.exe"
    "Google Chrome"                = "chrome.exe"
    "Microsoft Edge"               = "msedge.exe"
    "Notepad++"                    = "notepad++.exe"
    "Parallels Client"             = "APPServerClient.exe"
    "Remote Desktop"               = "msrdcw.exe"
    "TeamViewer"                   = "TeamViewer.exe"
    "[Royal TS]"                   = "royalts.exe"
    "Elgato StreamDeck"            = "StreamDeck.exe"
    "[Visual Studio]"              = "devenv.exe"
    "Visual Studio Code"           = "code.exe"
    "Camtasia Studio"              = "CamtasiaStudio.exe"
    "Camtasia Recorder"            = "CamtasiaRecorder.exe"
    "Jabra Direct"                 = "jabra-direct.exe"
    "7-Zip File Manager"           = "7zFM.exe"
    "Access"                       = "MSACCESS.EXE"
    "Excel"                        = "EXCEL.EXE"
    "OneDrive"                     = "onedrive.exe"
    "OneNote"                      = "ONENOTE.EXE"
    "Outlook"                      = "OUTLOOK.EXE"
    "PowerPoint"                   = "POWERPNT.EXE"
    "Project"                      = "WINPROJ.EXE"
    "Publisher"                    = "MSPUB.EXE"
    "Visio"                        = "VISIO.EXE"
    "Word"                         = "WINWORD.exe"
    "[PowerShell 7]"               = "pwsh.exe"
    "SQL Server Management Studio" = "ssms.exe"
    "Azure Data Studio"            = "azuredatastudio.exe"
    "Zoom"                         = "zoom.exe"
    "Skype for Business"           = "Skype.exe"
    "VLC Player"                   = "vlc.exe"   
    "Cisco Jabber"                 = "CiscoJabber.exe"
    "Microsoft Teams"              = "msteams.exe"
    "PuTTY"                        = "putty.exe"
    "wordpad"                      = "WORDPAD.EXE"
    "[AutoCAD]"                    = "acad.exe"
    "[CORSAIR iCUE Software]"      = "iCue.exe"
    "[Steam]"                      = "steam.exe"
    "Paint"                        = "mspaint.exe"
}

$LogFileName = "ShortcutRepairs.log";
$LogFilePath = "$env:temp\$LogFileName";

Function Log {
    param($message);
    $currenttime = Get-Date -format u;
    $outputstring = "[" + $currenttime + "] " + $message;
    $outputstring | Out-File $LogFilepath -Append;
}

Function LogAndConsole($message) {
    Write-Host $message -ForegroundColor Green
    Log $message
}

Function LogErrorAndConsole($message) {
    Write-Host $message -ForegroundColor Red
    Log $message
}

$programs.GetEnumerator() | ForEach-Object {
    $reg_path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($_.Value)"
    try {
$apppath = $null
$target = $null
        try { $apppath = Get-ItemPropertyValue $reg_path -Name "Path" -ErrorAction SilentlyContinue } catch {}
if ($apppath -ne $null)
{
$target = $apppath + "\" + $_.Value
}
else
{
try { $target = Get-ItemPropertyValue $reg_path -Name "(default)" -ErrorAction SilentlyContinue } catch {}
}
        if ($target -ne $null) {
                
$target = $target.Trim("`"")
                $shortcut_path = "$env:PROGRAMDATA\Microsoft\Windows\Start Menu\Programs\$($_.Key).lnk"

                $sh = New-Object -ComObject WScript.Shell
                if(Test-Path($sh.CreateShortcut($shortcut_path).TargetPath)){
                    LogAndConsole ("Shortcut already present and pointing correctly for {0} in Start Menu" -f $_.Key)
                }
                else{
                    LogErrorAndConsole ("Shortcut not correct - starting for {0} - starting remediation" -f $_.Key)
                    Exit 1
                }
        }
    }
    catch {
        $failures += 1
        LogErrorAndConsole "Exception: $_"
    }
}
$programs = @{
    "Adobe Acrobat"                = "Acrobat.exe"
    "[Adobe Photoshop]"            = "photoshop.exe"
    "[Adobe Illustrator]"          = "illustrator.exe"
    "Adobe Creative Cloud"         = "Creative Cloud.exe"
    "Adobe Substance 3D Painter"   = "Adobe Substance 3D Painter.exe"
    "Firefox Private Browsing"     = "private_browsing.exe"
    "Firefox"                      = "firefox.exe"
    "Google Chrome"                = "chrome.exe"
    "Microsoft Edge"               = "msedge.exe"
    "Notepad++"                    = "notepad++.exe"
    "Parallels Client"             = "APPServerClient.exe"
    "Remote Desktop"               = "msrdcw.exe"
    "TeamViewer"                   = "TeamViewer.exe"
    "[Royal TS]"                   = "royalts.exe"
    "Elgato StreamDeck"            = "StreamDeck.exe"
    "[Visual Studio]"              = "devenv.exe"
    "Visual Studio Code"           = "code.exe"
    "Camtasia Studio"              = "CamtasiaStudio.exe"
    "Camtasia Recorder"            = "CamtasiaRecorder.exe"
    "Jabra Direct"                 = "jabra-direct.exe"
    "7-Zip File Manager"           = "7zFM.exe"
    "Access"                       = "MSACCESS.EXE"
    "Excel"                        = "EXCEL.EXE"
    "OneDrive"                     = "onedrive.exe"
    "OneNote"                      = "ONENOTE.EXE"
    "Outlook"                      = "OUTLOOK.EXE"
    "PowerPoint"                   = "POWERPNT.EXE"
    "Project"                      = "WINPROJ.EXE"
    "Publisher"                    = "MSPUB.EXE"
    "Visio"                        = "VISIO.EXE"
    "Word"                         = "WINWORD.exe"
    "[PowerShell 7]"               = "pwsh.exe"
    "SQL Server Management Studio" = "ssms.exe"
    "Azure Data Studio"            = "azuredatastudio.exe"
    "Zoom"                         = "zoom.exe"
    "Skype for Business"           = "Skype.exe"
    "VLC Player"                   = "vlc.exe"   
    "Cisco Jabber"                 = "CiscoJabber.exe"
    "Microsoft Teams"              = "msteams.exe"
    "PuTTY"                        = "putty.exe"
    "wordpad"                      = "WORDPAD.EXE"
    "[AutoCAD]"                    = "acad.exe"
    "[CORSAIR iCUE Software]"      = "iCue.exe"
    "[Steam]"                      = "steam.exe"
    "Paint"                        = "mspaint.exe"
}

$LogFileName = "ShortcutRepairs.log";
$LogFilePath = "$env:temp\$LogFileName";

Function Log {
    param($message);
    $currenttime = Get-Date -format u;
    $outputstring = "[" + $currenttime + "] " + $message;
    $outputstring | Out-File $LogFilepath -Append;
}

Function LogAndConsole($message) {
    Write-Host $message -ForegroundColor Green
    Log $message
}

Function LogErrorAndConsole($message) {
    Write-Host $message -ForegroundColor Red
    Log $message
}

$programs.GetEnumerator() | ForEach-Object {
    $reg_path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($_.Value)"
    try {
$apppath = $null
$target = $null
        try { $apppath = Get-ItemPropertyValue $reg_path -Name "Path" -ErrorAction SilentlyContinue } catch {}
if ($apppath -ne $null)
{
$target = $apppath + "\" + $_.Value
}
else
{
try { $target = Get-ItemPropertyValue $reg_path -Name "(default)" -ErrorAction SilentlyContinue } catch {}
}
        if ($target -ne $null) {
                LogAndConsole ("Recreating shortcut for {0} in Start Menu" -f $_.Key)
$target = $target.Trim("`"")
                $shortcut_path = "$env:PROGRAMDATA\Microsoft\Windows\Start Menu\Programs\$($_.Key).lnk"
                $description = $_.Key
                $workingdirectory = (Get-ChildItem $target).DirectoryName
                $WshShell = New-Object -ComObject WScript.Shell
                $Shortcut = $WshShell.CreateShortcut($shortcut_path)
                $Shortcut.TargetPath = $target
                $Shortcut.Description = $description
                $shortcut.WorkingDirectory = $workingdirectory
                $Shortcut.Save()
                $success += 1
        }
    }
    catch {
        $failures += 1
        LogErrorAndConsole "Exception: $_"
    }
}

Alternative method

Create a Folder that includes all the shortcuts you want to restore to the startmenu and create a Install.ps1 script – The script will only copy the shortcuts where the application is actually installed – so you can include all shortcuts in the folder even though they might not be installed on all computers.

$StartMenuFolder = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs"
$ShortCuts = Get-ChildItem -Filter "*.lnk"
$ShortCuts | % {
    If(test-path("$StartMenuFolder\$($_.name)")){
        "$($_.name) already exist in start menu"
    }
    else {
        "$($_.name) not found in start menu - checking if program pointed to by shortcut exist"
        $sh = New-Object -ComObject WScript.Shell
        if(Test-Path($sh.CreateShortcut($_.FullName).TargetPath)){
            "Program exist - copying $($_.Name) into start menu folder"
            Copy-Item -Path $_.FullName -Destination $StartMenuFolder -Force
        }
        else {
            "Did not find $($sh.CreateShortcut($_.FullName).TargetPath) - will not copy $($_.name)"
        }
    }
}

Should look something similar to this.

If you have multiple languages installed on your clients in your tenant it might be wise to change the properties of each shortcut to use system variable instead. Change to %programfiles% or %programfiles(x86)% depending on where it is.

Create a detectionscript – this might need to be edited, depending on what applications you want to verify have shortcuts.

$StartMenuFolder = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs"
$Count = (Get-ChildItem $StartMenuFolder | ? Name -match "Word|Outlook|Powerpoint|Edge").count
If($count -ge 4){"Installed"}

Create the package and uppload to Intune with install command:

powershell.exe -noprofile -executionpolicy bypass -file .\Install.ps1

Troubleshooting ASR Rules on the client

If the shortcuts keep disappering you can use the following code to check what ASR rules are still in block mode by running this script in PowerShell in Administrator mode. It is the Win32 API calls from Office macros that is the cause of this issue that you need to check for.

White = Off
Red = Block
Yellow = Audit
Orange = Warn

$MPPref = Get-MpPreference -ErrorAction SilentlyContinue
$AttackSurfaceIDs = $MPPref | Select-Object -ExpandProperty AttackSurfaceReductionRules_Ids
$AttackSurfaceActions = $MPPref | Select-Object -ExpandProperty AttackSurfaceReductionRules_Actions
$i = 0

foreach($Rule in $AttackSurfaceIDs){

$Color = Switch($AttackSurfaceActions[$i])
{
0 {"White"}
1 {"Red"}
2 {"Yellow"}
6 {"Orange"}
}

$RuleName = Switch($Rule)
{
56a863a9-875e-4185-98a7-b882c64b5ce5 {"Block abuse of exploited vulnerable signed drivers"}
7674ba52-37eb-4a4f-a9a1-f0f9a1619a2c {"Block Adobe Reader from creating child processes"}
d4f940ab-401b-4efc-aadc-ad5f3c50688a {"Block all Office applications from creating child processes"}
9e6c4e1f-7d60-472f-ba1a-a39ef669e4b2 {"Block credential stealing from the Windows local security authority subsystem (lsass.exe)"}
be9ba2d9-53ea-4cdc-84e5-9b1eeee46550 {"Block executable content from email client and webmail"}
01443614-cd74-433a-b99e-2ecdc07bfc25 {"Block executable files from running unless they meet a prevalence, age, or trusted list criterion"}
5beb7efe-fd9a-4556-801d-275e5ffc04cc {"Block execution of potentially obfuscated scripts"}
d3e037e1-3eb8-44c8-a917-57927947596d {"Block JavaScript or VBScript from launching downloaded executable content"}
3b576869-a4ec-4529-8536-b80a7769e899 {"Block Office applications from creating executable content"}
75668c1f-73b5-4cf0-bb93-3ecf5cb7cc84 {"Block Office applications from injecting code into other processes"}
26190899-1602-49e8-8b27-eb1d0a1ce869 {"Block Office communication application from creating child processes"}
e6db77e5-3df2-4cf1-b95a-636979351e5b {"Block persistence through WMI event subscription - File and folder exclusions not supported."}
d1e49aac-8f56-4280-b9ba-993a6d77406c {"Block process creations originating from PSExec and WMI commands"}
b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4 {"Block untrusted and unsigned processes that run from USB"}
92e97fa1-2edf-4476-bdd6-9dd0b4dddc7b {"Block Win32 API calls from Office macros"}
c1db55ab-c21a-4637-bb3f-a12568109d35 {"Use advanced protection against ransomware"}
}

Write-Host $RuleName -ForegroundColor $Color
$i++
}

Output similar to this:


Strangely enough Intune still does not support adding printers that are published through a print server directly, even though it is just a one-liner PowerShell script to execute. Universal Print from our experience has been a little unstable and it’s rare that anyone needs to print outside of the network anyway.

Add-Printer -ConnectionName "\\company.local\printername"

The issue here is that this one-liner needs to be run under the user context and here is where it get’s tricky. There might be more than one way to achieve this, but the script following utilizes a workaround where we create a schedule task to be run under user context from the package deployed. This also helps with the issue that the server hosting the print queue might not be available at the time the script is intially run. The logic here can probably be improved, but it at least works, and uninstallation also work. A second thing that we have to “workaround” is that PowerShell scripts ran from Task Scheduler under user context will show for the end user, which is not desired. We circumvent that annoiance by creating a VBS script that runs from Task Scheduler that runs the actual PowerShell script.

We need two scripts in order for this whole solution to work as well as a custom detection script, unless you figure another way of detecting the printer afterwards easily.

Logging goes to the IntuneManagementExtension folder since we then can run a “Collect diagnostics” on a device directly from Endpoint Manager and fetch the log from folder 42. This so that we don’t need to involve the user if we need to troubleshoot.

The first one is for creating the scheduled task and also creates a VBS script to run.

The script has 2 arguments:

  • printerFQDN – Example \\mycompany.local\printqueue1
  • uninstall – Add -uninstall to remove the printer
param (
    [switch]$uninstall = $false,
    [Parameter(Mandatory = $true)]
    [string]$printerFQDN
)

$taskScheduleName = "PrintInstall-$($printerFQDN -replace '\W','')"
$logfileName = "$taskScheduleName-TaskSchedule.log"
$scriptDir = "$env:ProgramData\$taskScheduleName"
$scriptName = "InstallPrinter.ps1"

Start-Transcript -Path $(Join-Path -Path C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\ -ChildPath $logfileName) -Force
if (!(Test-Path $scriptDir)) {
    New-Item -Path $scriptDir -ItemType Directory
}
Copy-Item -Path .\$scriptName -Destination $scriptDir\$scriptName -Force

if ($uninstall) {
    # Make the task-scheduler silent
    $vbs = @"
command = "powershell.exe -nologo -executionpolicy Bypass -windowstyle hidden -file $scriptDir\$scriptName -printerFQDN $printerFQDN -uninstall"
set shell = CreateObject("WScript.Shell")
shell.Run command,0
"@

    $vbs | Out-File -FilePath "$scriptDir\$($scriptName.replace(".ps1","-uninstall.vbs"))" -Force

    Unregister-ScheduledTask -TaskName $taskScheduleName -Confirm:$false -ErrorAction SilentlyContinue
    $action = New-ScheduledTaskAction -Execute "$scriptDir\$($scriptName.replace(".ps1","-uninstall.vbs"))"
    $trigger = New-ScheduledTaskTrigger -AtLogOn
    $principal = New-ScheduledTaskPrincipal -UserId (whoami)
    $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries
    $task = New-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings
    Register-ScheduledTask RemoveThatPrinter -InputObject $task
    Start-ScheduledTask -TaskName RemoveThatPrinter
    Start-Sleep -Seconds 1
    Unregister-ScheduledTask -TaskName RemoveThatPrinter -Confirm:$false
    Remove-Item -Path $scriptDir -Recurse -Force
} 
else {
    # Make the task-scheduler silent
    $vbs = @"
command = "powershell.exe -nologo -executionpolicy Bypass -windowstyle hidden -file $scriptDir\$scriptName -printerFQDN $printerFQDN"
set shell = CreateObject("WScript.Shell")
shell.Run command,0
"@

    $vbs | Out-File -FilePath "$scriptDir\$($scriptName.replace("ps1","vbs"))" -Force
    Unregister-ScheduledTask -TaskName $taskScheduleName -Confirm:$false -ErrorAction SilentlyContinue
    $action = New-ScheduledTaskAction -Execute "$scriptDir\$($scriptName.replace("ps1","vbs"))"
    $trigger = @(
        $(New-ScheduledTaskTrigger -AtLogOn),
        $(New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 10))
    )
    $principal = New-ScheduledTaskPrincipal -UserId (whoami)
    $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -Hidden
    $task = New-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings
    Register-ScheduledTask $taskScheduleName -InputObject $task
    Start-ScheduledTask -TaskName $taskScheduleName
}

Stop-Transcript
exit 0
param (
    [switch]$uninstall = $false,
    [Parameter(Mandatory = $true)]
    [string]$printerFQDN
)

# Starting our logging.
$taskScheduleName = "PrintInstall-$($printerFQDN -replace '\W','')"
$logfileName = "$taskScheduleName.log"
$scriptDir = "$env:ProgramData\$taskScheduleName"
$scriptName = "InstallPrinter.ps1"

Start-Transcript -Path $(Join-Path -Path C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\ -ChildPath $logfileName) -Force

if (!($uninstall)) {
    "Installing printer $printerFQDN"
    Add-Printer -ConnectionName $printerFQDN
}
else {
    "Uninstalling printer $printerFQDN"
    Remove-Printer -Name $printerFQDN
}

Stop-Transcript

# Let's tell IME that all is well :)
exit 0

Save both of these files into the same folder and package it as a Win32 file using IntuneWin. Once done you are ready to install printers.

Uppload the package with “Install Behaviour” set to User and assign.

The detectionscript needs to be created based on what the taskname ends up being named.

if(Get-ScheduledTask -TaskName PrintInstall-xxxxx -ErrorAction SilentlyContinue){"Detected"}

Two improvements that should be made to the scripts would be:

  • Check for domain line of sight before trying to add printer
  • Self remove the scheduled task once printer has been added
  • Possibility to add an array of printerFQDN’s for multiple printers