A more flexible SCCM App Detection with PowerShell

SCCM provides a few options to detect the presence of an application.  The default clauses are via MSI Product Code, Registry Key, or File.  You can also combine these in your detection.  Alternatively you can use a script to detect the presence (PowerShell, VBScript, or JavaScript).  Here’s what I’ve been using in PowerShell to detect most applications (assuming it writes a registry key to the standard “Uninstall” key:

The following code is the template that I modify for virtually all applications:

Try {
    [string]$appNameRegEx = '^Application Name'
    [string]$appVersion = '1.0.0.0'
    [string]$appVendorRegEx = 'Vendor'
    
    # Nothing should need to be modified below this line


    function Compare-Versions {
        [CmdletBinding()]
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullorEmpty()]
            [string]$DeploymentVersion,
            [Parameter(Mandatory=$true)]
            [ValidateNotNullorEmpty()]
            [string]$DetectedVersion
        )
        Try {
            # Attempting to use the [version] type
            [version]$vDeploymentVersion = $DeploymentVersion
            [version]$vDetectedVersion = $DetectedVersion
            if ($vDetectedVersion -ge $vDeploymentVersion) {
                Return $true
            } else {
                Return $false
            }
        } Catch {
            if (($DetectedVersion -notmatch '\D') -and ($DeploymentVersion -notmatch '\D')) {
                # Both versions contain only numbers
                Try {
                    [int32]$intDetectedVersion = $DetectedVersion
                    [int32]$intDeploymentVersion = $DeploymentVersion
                    if ($intDetectedVersion -ge $intDeploymentVersion) {
                        Return $true
                    } else {
                        Return $false
                    }
                } Catch {
                    # Failing back to comparing as strings
                }
            }     
            if ($DetectedVersion -ge $DeploymentVersion) {
                Return $true
            } else {
                Return $false
            }
        }
    }
    
    [string[]]$regKeyBranches = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
    :Search foreach ($regKeyBranch in $regKeyBranches) {
        If (Test-Path -LiteralPath $regKeyBranch -ErrorAction 'SilentlyContinue') {
            [psobject[]]$UninstallKeys = Get-ChildItem -Path $regKeyBranch -Recurse
            foreach ($UninstallKey in $UninstallKeys) {
                $regKey = ($UninstallKey.PSChildName)
                Try {$regKeyProperties = Get-ItemProperty -LiteralPath "$regKeyBranch\$regKey" -ErrorAction Stop}
                Catch {Continue}
                if ((($regKeyProperties.DisplayName) -match $appNameRegEx) -and (($regKeyProperties.Publisher) -match $appVendorRegEx)) {
                    Try {
                        if (($appVersion) -and (($regKeyProperties).DisplayVersion) -and (Compare-Versions -DeploymentVersion $appVersion -DetectedVersion (($regKeyProperties).DisplayVersion))) {
                            Write-Output ($regKeyProperties.DisplayName)
                            Write-Output ($regKeyProperties.DisplayVersion)
                            Break Search
                        }           
                    } Catch {
                        Continue
                    }
                }
            }
        }
    }


    Exit 0


} Catch {
    Exit 60001
}

Edit: 2017/03/16 – Download the script from GitHub here.

I use this code for MSI Products as well.  You may ask why, when you can just use the product code.  The answer to me is simple, different versions can use a different product code (just look at Java for one easy example).  I usually wrap installers in a script to handle newer versions (details coming in another blog post), however I’d rather SCCM also leave things alone if a user self updates to a newer version before a mass deployment.  Using this method gives me that flexibility, as SCCM will report the application as installed

So how does it work?  First we need to know what SCCM expects when using a script for application deployment.  For an application to be detected as “installed” your script needs to return output and have an exit code of 0.  You want no output, and an exit code of 0 for it to detect an application as “not installed”.  Almost any other scenario will result in a detection failure, and SCCM won’t try anything.  Here’s a link to Microsoft Docs for more details (and the relevant table): https://docs.microsoft.com/en-us/previous-versions/system-center/system-center-2012-R2/gg682159(v=technet.10)

Script exit codeData read from STDOUTData read from STDERRScript resultApplication detection state
0EmptyEmptySuccessNot installed
0EmptyNot emptyFailureUnknown
0Not emptyEmptySuccessInstalled
0Not emptyNot emptySuccessInstalled
Non-zero valueEmptyEmptyFailureUnknown
Non-zero valueEmptyNot emptyFailureUnknown
Non-zero valueNot emptyEmptyFailureUnknown
Non-zero valueNot emptyNot emptyFailureUnknown

With that in mind, let’s break everything down:

I have the entire script wrapped in a ‘Try {} Catch {}’.  The reason is that I intentionally wanted to exit with a non 0 exit code in the event there is an error in my code.

After that, is the following:

[string]$appNameRegEx = '^Application Name'
[string]$appVersion = '1.0.0.0'
[string]$appVendorRegEx = 'Vendor'

I’m setting a few variables at the top that in most cases are the only things that need to change.  If you’re not familiar with regular expressions, you’re missing out as it is extremely powerful even in relatively simple queries.  In this case the ‘^’ is meaning the match must be at the beginning of the string. Where needed you can make much more complex regular expressions to ensure you only match when you intend to.

After that is the function to compare the versions. Most applications will use a version that matches Major.Minor.Build.Revision. When comparing these directly it can figure out that version 10 is greater than version 9, whereas if you compare as a string it will see 9 as being greater than 1. The function attempts to convert the string it retrieves from the registry to a version, and if successful will use that for comparison. If that fails, it will attempt to convert to an integer (e.g. for applications that may use a date such as 20170505 as a version). Lastly, it will fall back to comparing as a string (which is what the variable at the top is declared as). Depending on the application and how the version is displayed (e.g. 1.0.2g) you may need to adjust as needed.

Below the function in a variable is the location of the branches that I’m checking (in this case the standard and the wow6432Node for 64-bit machines with 32-bit applications. Next looping through both branches and only continuing if the branch actually exists.  This accounts for 32-bit machines that won’t have the wow6432node.  I’m also setting the label ‘:Search’ so that when a match is found I can break this loop easily.

:Search foreach ($regKeyBranch in $regKeyBranches) {
        If (Test-Path -LiteralPath $regKeyBranch -ErrorAction 'SilentlyContinue') {

Next I’m getting all the registry keys and storing them in an array:

[psobject[]]$UninstallKeys = Get-ChildItem -Path $regKeyBranch -Recurse

Once they’re in an array, I’m going to loop through them to check each of the keys it returned.  The PSChildName is actually the registry key name in this case.  I then retrieve all the properties of the registry key:

foreach ($UninstallKey in $UninstallKeys) {
    $regKey = ($UninstallKey.PSChildName)
    Try {$regKeyProperties = Get-ItemProperty -LiteralPath "$regKeyBranch\$regKey" -ErrorAction Stop}
    Catch {Continue}

You’ll notice I’m continuing in the event of error.  If there’s a registry key it can’t read (perhaps due to permissions), it will just skip it and move to the next key.

The last portion is where I check the DisplayName property against the $appNameRegEx.  If there isn’t a property with DisplayName, no match is made and it will continue on to the next.

If a match is made, it then checks the version with the function to attempt a version comparison, integer comparison, and finally a string comparison if the first two failed.

if ((($regKeyProperties.DisplayName) -match $appNameRegEx) -and (($regKeyProperties.Publisher) -match $appVendorRegEx)) {
    Try {
        if (($appVersion) -and (($regKeyProperties).DisplayVersion) -and (Compare-Versions -DeploymentVersion $appVersion -DetectedVersion (($regKeyProperties).DisplayVersion))) {
            Write-Output ($regKeyProperties.DisplayName)
            Write-Output ($regKeyProperties.DisplayVersion)
            Break Search
        }           
    } Catch {
            Continue
    }

If the detected version is greater than or equal to the version being deployed, it breaks the loop after returning the display name and version.  Output is returned, and the script would exit with 0 (as a terminating error would result in the exit code being 60001 (set by me for an easy way to determine if an error occurred because of the code in the evaluation).  SCCM will report the application as installed.

If no output is found (and no error), the script exits without returning output signalling to SCCM that the application is not installed and depending on the deployment type will either make it available or start the installation.

Leave a comment

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.