Demystifying Intune Custom App Detection Scripts

I’m sure everyone reading this has deployed many applications within Intune using the win32 format and most likely used an MSI code or a file/registry detection method to monitor for a completed install (and why wouldn’t you, they work perfectly)

Sometimes however, you might come across an application which is more tricky to detect, or you want to look for two things before you’re confident it’s installed ok. This is where a custom detection script comes into play.

As with my previous post on Proactive Remediations, Intune is looking for specific outputs for the script to return a success code.

In this case, it is looking for BOTH an exit with a 0 code and output on STDOUT. For those of you rapidly opening a new tab to google STDOUT, basically, it needs output of some sort, a simple:

write-output "hello world"
exit 0

Will work fine, so that is what we need the script to return at the end in order for it to be marked as successful.

Passing an exit code 1 or even an exit 0 without any output will flag as an install failure.

Important Note: Detection scripts ONLY work in the System context so if detecting at the user level, you need to enumerate the logged in user. Check the final example to see how.

Now for some examples:

A basic file check (in case you’re feeling scripty)

$File = "C:\windows\system32\notepad.exe"
if (Test-Path $File) {
    write-output "Notepad detected, exiting"
    exit 0
}
else {
    exit 1
}

A registry key (checking both it exists and the value is correct, good for versioning)

$Path = "HKLM:\SOFTWARE\7-Zip"
$Name = "Path"
$Type = "STRING"
$Value = "C:\Program Files\7-Zip\"

Try {
    $Registry = Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop | Select-Object -ExpandProperty $Name
    If ($Registry -eq $Value){
        Write-Output "Detected"
       Exit 0
    } 
    Exit 1
} 
Catch {
    Exit 1
}

Now for the ones it can’t do out of the box!

Check if a service exists

$service = get-service -name "MozillaMaintenance"
if ($service) {
    write-output "MozillaMaintenance detected, exiting"
    exit 0
}
else {
    exit 1
}

Service exists and is running:

$service = get-service -name "MozillaMaintenance"
if ($service.Status -eq "Running") {
    write-output "MozillaMaintenance detected and running, exiting"
    exit 0
}
else {
   exit 1
}

Both file and service exist

$File = "C:\windows\system32\notepad.exe"
$service = get-service -name "MozillaMaintenance"
if (($service) -and (Test-Path $File)) {
    write-output "MozillaMaintenance and Notepad detected, exiting"
    exit 0
}
else {
    exit 1
}

File is of a certain version

$fileversion = (Get-Command C:\Windows\System32\notepad.exe).Version
$build = 19041
$detectedbuild = $fileversion.Build
if ($detectedbuild -eq $build) {
    write-output "Detected"
    exit 0
}
else {
    exit 1
}

Or even the file was last modified today

$filedate = (Get-Item C:\Windows\System32\notepad.exe).LastWriteTime
if ($filedate -gt (Get-Date).AddDays(-1)) {
    write-output "Detected"
    exit 0
}
else {
    exit 1
}

When looking at logged in user, we need to find their details so for this I have a new function:

function getloggedindetails() {
    ##Find logged in username
    $user = Get-WmiObject Win32_Process -Filter "Name='explorer.exe'" |
      ForEach-Object { $_.GetOwner() } |
      Select-Object -Unique -Expand User
    
    ##Find logged in user's SID
    ##Loop through registry profilelist until ProfileImagePath matches and return the path
        $path= "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*"
        $sid = (Get-ItemProperty -Path $path | Where-Object { $_.ProfileImagePath -like "*$user" }).PSChildName

    $return = $sid, $user
    
    return $return
    }

Running this will give you the username and SID. For our 7-zip registry example:

$loggedinuser = getloggedindetails
##Set key

$sid = $loggedinuser[0]
$Path = "Registry::HKU\$sid\SOFTWARE\7-Zip"
$Name = "Path"
$Type = "STRING"
$Value = "C:\Program Files\7-Zip\"

Try {
    $Registry = Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop | Select-Object -ExpandProperty $Name
    If ($Registry -eq $Value){
        Write-Output "Detected"
       Exit 0
    } 
    Exit 1
} 
Catch {
    Exit 1
        write-host "not detected"
    write-host $path
}

Or for a file at the user level:

$username = $loggedinuser[1]
$File = "C:\users\$username\AppData\Local\Microsoft\Teams\update.exe"
if (Test-Path $File) {
    write-output "Teams Update detected, exiting"
    exit 0
}
else {
    exit 1
}

I hope that makes them easier to understand! Happy scripting…

32 thoughts on “Demystifying Intune Custom App Detection Scripts”

  1. Just a note. This code:
    $File = “C:\windows\system32\notepad.exe”
    if ($file) {
    write-output “Notepad detected, exiting”
    exit 0
    }
    else {
    exit 1
    }

    Does not check for the existance of a file. The $File string will always be true.

    You need:
    if (Test-Path $File) {…

    Reply
  2. We are trying to have a appx dependency and msix installing via powershell in a win32 app. How would I detect these are installed and correct version? Dont really see and folders. They are modern apps.

    Reply
  3. For the Version Check, would it be more beneficial to check the registered entry in the registry?

    Im thinking something like this:

    $APPINSTALL = Get-ItemProperty “HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*” | where {$_.DisplayName -like “*APPNAME*”}

    IF(Test-Path “$APPINSTALL .PSPath”)
    {
    IF($APPINSTALL .DisplayVersion -ge “3.0.279”)
    {
    write-host “Install Success”
    exit 0
    }

    ELSE
    {
    Write-Host “Installed Version Not Match, Upgrade Failed”
    exit 1
    }

    }

    ELSEIF(!(Test-Path “$APPINSTALL .PSPath”))
    {
    Write-Host “Install failed!”
    exit 1
    }

    Reply
  4. Thank you Andrew for this invaluable information.
    How about in a scenario where we are trying to update a Dell WD19 docking station firmware through Endpoint manager. In this scenario, the dock needs to be directly connected to the device and detected before it installs the firmware package.

    How can I create a simple custom detection rule in powershell, that would show either “yes” the docking station is attached proceed with the installation or “no” its not attached-and install when detected.

    Reply
    • Hi,
      If you can get the hardware ID for the dock, something like this should work:
      $dock = get-pnpdevice | where-object InstanceId -eq “USB\VID_14B0&PID_0184\5&21F3FF01&0&5”
      if ($status -eq “OK”) {
      ##Dock attached
      }
      else {
      ##Not attached
      }

      Replace the hardware ID with whatever you find for the dock, that’s just a random one I grabbed from device manager

      Reply
  5. What context do the scripts run in ? I have a couple of installers that will run in user space. Does the custom detection script follow the install context (System / User)

    Reply
  6. What would the JSON be for the following script you posted for a Custom Compliance Script?

    $File = “C:\windows\system32\notepad.exe”
    $service = get-service -name “MozillaMaintenance”
    if (($service) -and (Test-Path $File)) {
    write-output “MozillaMaintenance and Notepad detected, exiting”
    exit 0
    }
    else {
    exit 1
    }

    Reply
    • I’d split it into two so you can give the users a better reason for it, otherwise they’ll have to guess which it is:

      Powershell:
      $File = “C:\windows\system32\notepad.exe”
      $service = get-service -name “MozillaMaintenance”
      if ($service) {
      $mozillamaintenance = “True”
      }
      else {
      $mozillamaintenance = “False”
      }

      if ($File) {
      $fileexists = “True”
      }
      else {
      $fileexists = “False”
      }

      $hash = @{
      Mozilla = $mozillamaintenance
      Notepad = $fileexists
      }
      return $hash | ConvertTo-Json -Compress

      JSON:
      {
      “Rules”:[
      {
      “SettingName”:”Notepad”,
      “Operator”:”IsEquals”,
      “DataType”:”String”,
      “Operand”:”True”,
      “MoreInfoUrl”:”https://google.com”,
      “RemediationStrings”:[
      {
      “Language”: “en_US”,
      “Title”: “Notepad Missing”,
      “Description”: “Notepad is Missing”
      }
      ]
      },
      {
      “SettingName”:”Mozilla”,
      “Operator”:”IsEquals”,
      “DataType”:”String”,
      “Operand”:”True”,
      “MoreInfoUrl”:”https://google.com”,
      “RemediationStrings”:[
      {
      “Language”: “en_US”,
      “Title”: “Mozilla”,
      “Description”: “Mozilla Maintenance Service is Missing”
      }
      ]
      }
      ]
      }

      Reply
    • You can check for multiple files, but the output will always only be either Detected or Not Detected. If you add some logging into your script, you could use that to show you what was missing though

      Reply
  7. Great article – thank you! Question for you –

    We make Visual Studio Code available in the software center as a user install. Visual Studio Code installs into the user’s profile. I’ve found that the detection rules run as the system and cannot be used to detect a user-based install. My expectation from Intune is that the company portal will display whether an app is installed for the user or not, but I can’t seem to get it to work. Any clues for a user-app detection method?

    Reply
    • Hi,
      You’re going to need to get creative with PowerShell.
      One option would be to enumerate the logged on user and populate that into the path.

      If they are single user devices, a simpler option would just be to search c:\users and the visual studio code executable. This one means if the path changes, it doesn’t matter, but obviously only works if there is only one user.

      Or combine the two and search the logged on users profile.

      If you need any help with the script, let me know

      Reply
  8. Very nice post, extremely helpful. I am trying to use the detection script during the requirements phase of my Intune Win32 app deployment. Identify if a service exists, and if not exit code 1 and proceed with install. Then use the same script with exit code 0 to verify that the service now exists. However, my application does not appear to be installing with these conditions. Any thoughts?

    $Service = Get-Service -Name “FakeService”
    If ($Service) {
    Write-Output “FakeService detected, exiting.”
    Exit 0
    }
    Else {
    Write-Output “FakeService not detected, installing.”
    Exit 1
    }

    Reply
    • Hi Glenn,
      Requirements scripts work slightly differently so in this case you would want something like
      $service = get-service -name “FakeService”
      $status = ($service.status).tostring()

      Then in the requirements, you want to look for a string which NOT equals Running (or stopped, disabled, whatever status the service should be in)

      That will only install on machines without the service.

      Then afterwards you want a detection script which matches what you have to confirm the app is installed

      Hopefully this makes sense

      Reply
  9. Hi Andrew,

    First off, really well writte article.

    Im having a problem with a detection script and was wondering if you could check it out.
    #Set Path to check SAP install
    $SAPinstallPath = “C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe”

    # Set the registry key path and value name
    $keyPath = “HKCU:\SOFTWARE\SAP\General”
    $valueName = “BrowserControl”
    $Value = 0x00000001

    if (Test-Path $SAPinstallPath) {
    # Check if the registry key exists
    if (Test-Path $keyPath) {
    # Get the value of the DWORD
    $dwordValue = (Get-ItemProperty $keyPath -Name $valueName).$valueName

    # Convert the $Value variable to a hexadecimal string
    $hexValue = $Value.ToString(“X8”)

    # Check if the DWORD value is set to 1
    if ($dwordValue -eq $hexValue) {
    Write-Output “SAP was intalled successfully”
    exit 0
    } else {
    exit 2
    }
    }
    else {
    exit 3
    }
    }
    else {
    exit 1
    }

    I tried everything I know but for the life of me I cant get intune to detect the installation. When I run the script locally it works as intended.

    Reply
    • Hi,
      I can see a few possible issues there:
      1) Your users may not be able to query the Program Files directory
      2) Exit codes should be 0 or 1
      3) Detection scripts run in the system context so can’t query HKCU

      Try this:

      function getloggedindetails() {
      <# .SYNOPSIS This function is used to find the logged in user SID and username when running as System .DESCRIPTION This function is used to find the logged in user SID and username when running as System .EXAMPLE getloggedindetails Returns the SID and Username in an array .NOTES NAME: getloggedindetails Written by: Andrew Taylor (https://andrewstaylor.com) #>
      ##Find logged in username
      $user = Get-WmiObject Win32_Process -Filter "Name='explorer.exe'" |
      ForEach-Object { $_.GetOwner() } |
      Select-Object -Unique -Expand User

      ##Find logged in user's SID
      ##Loop through registry profilelist until ProfileImagePath matches and return the path
      $path= "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*"
      $sid = (Get-ItemProperty -Path $path | Where-Object { $_.ProfileImagePath -like "*$user" }).PSChildName

      $return = $sid, $user

      return $return
      }

      $loggedinuser = getloggedindetails
      $sid = $loggedinuser[0]
      $user = $loggedinuser[1]

      #Set Path to check SAP install
      $SAPinstallPath = "C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe"

      # Set the registry key path and value name
      $keyPath = "Registry::HKU\$sid\SOFTWARE\SAP\General"
      $valueName = "BrowserControl"
      $Value = 0x00000001

      ##Set a counter
      $check = 0
      if (Test-Path $SAPinstallPath) {
      ##Executable found
      $check++
      }

      # Check if the registry key exists
      if (Test-Path $keyPath) {
      # Get the value of the DWORD
      $dwordValue = (Get-ItemProperty $keyPath -Name $valueName).$valueName

      # Convert the $Value variable to a hexadecimal string
      $hexValue = $Value.ToString("X8")

      # Check if the DWORD value is set to 1
      if ($dwordValue -eq $hexValue) {
      ##Reg correct
      $check++
      }
      }

      if ($check -eq 2) {
      write-host "Install successful"
      exit 0
      }
      else {
      write-host "Install failed"
      exit 1
      }

      Reply
      • Hi Andrew,

        thanks for your response it worked.
        I just edited the script to account for the fact that multiple users may be signed in to the device.
        Updated Script:
        function Get-LoggedInDetails {

        # Find logged-in user details
        $loggedInUsers = Get-WmiObject Win32_Process -Filter “Name=’explorer.exe'” |
        ForEach-Object {
        $_.GetOwner() | Select-Object User, Domain
        } | Select-Object -Unique

        # Find logged-in user’s SID for each user
        $loggedInUsers | ForEach-Object {
        $user = $_.User
        $domain = $_.Domain

        $path = “HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*”
        $sid = (Get-ItemProperty -Path $path | Where-Object { $_.ProfileImagePath -like “*\$user” }).PSChildName

        [PSCustomObject]@{
        SID = $sid
        User = “$domain\$user”
        }
        }
        }

        # Get data from the function
        $loggedInUsers = Get-LoggedInDetails

        # Set Path to check SAP install
        $SAPinstallPath = “C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe”

        # Set the registry key value name
        $valueName = “BrowserControl”
        $Value = 0x00000001

        # Iterate through each logged-in user
        foreach ($userDetails in $loggedInUsers) {
        $sid = $userDetails.SID
        $user = $userDetails.User

        # Set a counter
        $check = 0

        if (Test-Path $SAPinstallPath) {
        # Executable found
        $check++
        }

        # Check if the registry key exists
        $keyPath = “Registry::HKU\$sid\SOFTWARE\SAP\General”
        if (Test-Path $keyPath) {
        # Get the value of the DWORD
        $dwordValue = (Get-ItemProperty $keyPath -Name $valueName).$valueName

        # Convert the $Value variable to a hexadecimal string
        $hexValue = $Value.ToString(“X8”)

        # Check if the DWORD value is set to 1
        if ($dwordValue -eq $hexValue) {
        # Registry correct
        $check++
        }
        }

        if ($check -eq 2) {
        Write-Host “Install successful”

        Exit 0
        }
        }

        Write-Host “Install failed”

        Exit 1

        Really cant express how thankful I am, I was stuck on this for too long :).

        Reply
  10. Hi Andrew,
    I have been trying to write a script to detect a folder or even a .exe file on the users Desktop. I was wondering if there was a custom detection script to detect the folder or file on any users Desktop once installed?

    Tried using %username% and %userprofile% but it does not detect the folder/file any way I do it?

    Reply
    • Hi,
      Absolutely, if it’s a file, you just want to loop through the users and send the exit code if you find it. Obviously this is a bit more risky as that doesn’t necessarily mean it’s on all desktops.

      Something like this:
      # Define the file name to search for
      $fileName = "file.exe"

      # Get all user profiles on the machine
      $userProfiles = Get-ChildItem -Path "C:\Users" -Directory

      # Loop through each user profile and search for the file on their desktop
      foreach ($userProfile in $userProfiles) {
      $desktopPath = Join-Path -Path $userProfile.FullName -ChildPath "Desktop"
      $filePath = Join-Path -Path $desktopPath -ChildPath $fileName
      if (Test-Path $filePath) {
      Write-Output "File found on $($userProfile.Name)'s desktop: $filePath"
      exit 0
      }
      }

      If you want to check everyone has it, count the number of profiles and then use a counter to compare:
      # Define the file name to search for
      $fileName = "file.exe"

      # Get all user profiles on the machine
      $userProfiles = Get-ChildItem -Path "C:\Users" -Directory
      $totalprofiles = $userProfiles.Count

      ##Set a blank count
      $found = 0
      # Loop through each user profile and search for the file on their desktop
      foreach ($userProfile in $userProfiles) {
      $desktopPath = Join-Path -Path $userProfile.FullName -ChildPath "Desktop"
      $filePath = Join-Path -Path $desktopPath -ChildPath $fileName
      if (Test-Path $filePath) {
      Write-Output "File found on $($userProfile.Name)'s desktop: $filePath"
      $found++
      }
      }

      if ($found -eq $totalprofiles) {
      Write-Output "File found on all user profiles"
      exit 0
      } else {
      Write-Output "File not found on all user profiles"
      exit 0
      }

      I hope this helps

      Reply

Leave a Comment