Public/Test-IntuneWin32Rule.ps1

function Test-IntuneWin32Rule {
    <#
    .EXTERNALHELP IntuneScriptLab-Help.xml
    .SYNOPSIS
        Applies a Win32 app file, registry or MSI rule to this device the way the Intune agent does.
    #>

    [CmdletBinding(DefaultParameterSetName = 'File')]
    [OutputType('IntuneScriptLab.RuleResult')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'File')]
        [string]$Path,

        [Parameter(Mandatory, ParameterSetName = 'File')]
        [string]$FileOrFolderName,

        [Parameter(Mandatory, ParameterSetName = 'File')]
        [ValidateSet('Exists', 'DoesNotExist', 'Version', 'SizeInMB', 'ModifiedDate', 'CreatedDate')]
        [string]$FileOperation,

        [Parameter(Mandatory, ParameterSetName = 'Registry')]
        [string]$KeyPath,

        [Parameter(ParameterSetName = 'Registry')]
        [string]$ValueName,

        [Parameter(Mandatory, ParameterSetName = 'Registry')]
        [ValidateSet('Exists', 'DoesNotExist', 'String', 'Integer', 'Version')]
        [string]$RegistryOperation,

        [Parameter(Mandatory, ParameterSetName = 'ProductCode')]
        [string]$ProductCode,

        [Parameter(ParameterSetName = 'File')]
        [Parameter(ParameterSetName = 'Registry')]
        [Parameter(ParameterSetName = 'ProductCode')]
        [ValidateSet('Equal', 'NotEqual', 'GreaterThan', 'GreaterThanOrEqual', 'LessThan', 'LessThanOrEqual')]
        [string]$Operator,

        [Parameter(ParameterSetName = 'File')]
        [Parameter(ParameterSetName = 'Registry')]
        [Parameter(ParameterSetName = 'ProductCode')]
        [AllowEmptyString()]
        [string]$Value,

        [Parameter(ParameterSetName = 'File')]
        [Parameter(ParameterSetName = 'Registry')]
        [switch]$Check32BitOn64System,

        [ValidateSet('Detection', 'Requirement')]
        [string]$RuleType = 'Detection',

        [Parameter(Mandatory, ParameterSetName = 'Rule')]
        [hashtable]$Rule
    )
    Write-Verbose "Starting $($MyInvocation.MyCommand.Name) ($($PSCmdlet.ParameterSetName))"

    if ($PSCmdlet.ParameterSetName -eq 'Rule') {
        $splat = ConvertFrom-IslRuleSpec -Rule $Rule
        if ($Rule.ContainsKey('RuleType') -and $Rule.RuleType) { $splat.RuleType = $Rule.RuleType }
        else { $splat.RuleType = $RuleType }
        return Test-IntuneWin32Rule @splat
    }

    $kind = $PSCmdlet.ParameterSetName
    $operation = switch ($kind) {
        'File' { $FileOperation }
        'Registry' { $RegistryOperation }
        'ProductCode' { if ($Operator) { 'Version' } else { 'Exists' } }
    }
    $result = [pscustomobject]@{
        PSTypeName           = 'IntuneScriptLab.RuleResult'
        Met                  = $false
        Kind                 = $kind
        RuleType             = $RuleType
        Target               = ''
        Operation            = $operation
        Operator             = $Operator
        Value                = $Value
        Actual               = $null
        Check32BitOn64System = [bool]$Check32BitOn64System
        Reason               = ''
    }
    $needsValue = $operation -notin 'Exists', 'DoesNotExist'
    if ($needsValue -and -not $Operator) {
        throw "A $kind $operation rule needs -Operator and -Value"
    }
    if ($needsValue -and -not $PSBoundParameters.ContainsKey('Value')) {
        throw "A $kind $operation rule needs -Value"
    }

    switch ($kind) {
        'File' {
            $folder = Resolve-IslRulePath -Path $Path -Check32BitOn64System $Check32BitOn64System
            $target = Join-Path -Path $folder -ChildPath $FileOrFolderName
            $result.Target = $target
            $exists = Test-Path -LiteralPath $target
            $result.Actual = if ($exists) { 'present' } else { 'absent' }
            switch ($operation) {
                'Exists' {
                    $result.Met = $exists
                    $result.Reason = if ($exists) { "$target exists" } else { "$target does not exist" }
                }
                'DoesNotExist' {
                    # The agent cannot evaluate this operation on a file: never met (W32-FILE-NOTEXIST,
                    # W32-FILE-NOTEXIST-FALSE), so it is reported as it would be on the device
                    $result.Reason = if ($exists) {
                        ("$target exists, and the agent reports a file DoesNotExist $($RuleType.ToLower()) " +
                            'rule on a present file as an invalid rule (0x87D30004, W32-FILE-NOTEXIST-FALSE)')
                    }
                    else {
                        ("$target is absent, but the agent evaluates a file DoesNotExist " +
                            "$($RuleType.ToLower()) rule as not met even then (W32-FILE-NOTEXIST); use a " +
                            'registry DoesNotExist rule or a script instead')
                    }
                }
                default {
                    if (-not $exists) {
                        $result.Reason = "$target does not exist, so the $operation rule is not met"
                        break
                    }
                    $item = Get-Item -LiteralPath $target
                    switch ($operation) {
                        'Version' {
                            $info = $item.VersionInfo
                            if (-not $info -or -not $info.FileVersion) {
                                $result.Actual = ''
                                $result.Reason = "$target has no version resource, so the Version rule is not met"
                                break
                            }
                            $fileVersion = '{0}.{1}.{2}.{3}' -f $info.FileMajorPart, $info.FileMinorPart,
                                $info.FileBuildPart, $info.FilePrivatePart
                            $comparisonSplat = @{
                                Actual = $fileVersion; Expected = $Value; Type = 'Version'; Operator = $Operator
                            }
                            $comparison = Compare-IslRuleValue @comparisonSplat
                            $result.Actual = $fileVersion
                            $result.Met = $comparison.Met
                            $result.Reason = "File version $($comparison.Reason)"
                        }
                        'SizeInMB' {
                            $sizeMb = [math]::Floor($item.Length / 1MB)
                            $comparisonSplat = @{
                                Actual = $sizeMb; Expected = $Value; Type = 'Integer'; Operator = $Operator
                            }
                            $comparison = Compare-IslRuleValue @comparisonSplat
                            $result.Actual = "$sizeMb"
                            $result.Met = $comparison.Met
                            $result.Reason = "Size $($item.Length) bytes is $sizeMb MiB rounded down: " +
                                $comparison.Reason
                        }
                        default {
                            $stamp = if ($operation -eq 'ModifiedDate') { $item.LastWriteTimeUtc }
                            else { $item.CreationTimeUtc }
                            $comparisonSplat = @{
                                Actual = $stamp; Expected = $Value; Type = 'DateTime'; Operator = $Operator
                            }
                            $comparison = Compare-IslRuleValue @comparisonSplat
                            $result.Actual = $stamp.ToString('o')
                            $result.Met = $comparison.Met
                            $result.Reason = "$operation $($comparison.Reason)"
                        }
                    }
                }
            }
        }
        'Registry' {
            $view = if ($Check32BitOn64System -and [Environment]::Is64BitOperatingSystem) { 'Registry32' }
            elseif ([Environment]::Is64BitOperatingSystem) { 'Registry64' }
            else { 'Default' }
            $hiveName, $subKey = $KeyPath -split '\\', 2
            $hive = switch -Regex ($hiveName) {
                '^(HKEY_LOCAL_MACHINE|HKLM)$' { 'LocalMachine' }
                '^(HKEY_CURRENT_USER|HKCU)$' { 'CurrentUser' }
                '^(HKEY_CLASSES_ROOT|HKCR)$' { 'ClassesRoot' }
                '^(HKEY_USERS|HKU)$' { 'Users' }
                '^(HKEY_CURRENT_CONFIG|HKCC)$' { 'CurrentConfig' }
                default { throw "Registry key path must start with a hive (HKEY_LOCAL_MACHINE\...): $KeyPath" }
            }
            $result.Target = if ($ValueName) { "$KeyPath : $ValueName" } else { $KeyPath }
            $viewLabel = if ($view -eq 'Registry32') { '32-bit view' } else { '64-bit view' }
            $base = $null
            $key = $null
            try {
                $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, $view)
                $key = if ($subKey) { $base.OpenSubKey($subKey) } else { $base }
                $keyExists = $null -ne $key
                $valueExists = $keyExists -and $ValueName -and ($key.GetValueNames() -contains $ValueName)
                $data = if ($valueExists) { $key.GetValue($ValueName) } else { $null }
            }
            finally {
                if ($key -and $key -ne $base) { $key.Dispose() }
                if ($base) { $base.Dispose() }
            }
            $present = if ($ValueName) { $valueExists } else { $keyExists }
            $what = if ($ValueName) { 'value' } else { 'key' }
            $result.Actual = if ($ValueName -and $valueExists) { "$data" }
            elseif ($present) { 'present' } else { 'absent' }
            switch ($operation) {
                'Exists' {
                    $result.Met = $present
                    $result.Reason = "The $what $(if ($present) { 'exists' } else { 'does not exist' }) " +
                        "in the $viewLabel"
                }
                'DoesNotExist' {
                    $result.Met = -not $present
                    $result.Reason = "The $what $(if ($present) { 'exists' } else { 'does not exist' }) " +
                        "in the $viewLabel (registry DoesNotExist is evaluated, W32-REG-KEY-NOTEXIST)"
                }
                default {
                    if (-not $ValueName) { throw "A registry $operation rule needs -ValueName" }
                    if (-not $valueExists) {
                        $result.Reason = "The value does not exist in the $viewLabel, so the rule is not met"
                        break
                    }
                    $comparisonSplat = @{
                        Actual = $data; Expected = $Value; Type = $operation; Operator = $Operator
                    }
                    $comparison = Compare-IslRuleValue @comparisonSplat
                    $result.Met = $comparison.Met
                    $result.Reason = "Value in the ${viewLabel}: $($comparison.Reason)"
                }
            }
        }
        'ProductCode' {
            $result.Target = $ProductCode
            $product = Get-IslMsiProduct -ProductCode $ProductCode
            if (-not $product) {
                $result.Actual = 'not installed'
                $result.Reason = "No installed product has code $ProductCode (HKLM 64-bit, HKLM 32-bit, HKCU)"
                break
            }
            $result.Actual = $product.DisplayVersion
            if (-not $Operator) {
                $result.Met = $true
                $result.Reason = "$($product.DisplayName) $($product.DisplayVersion) is installed " +
                    "($($product.View))"
                break
            }
            $comparisonSplat = @{
                Actual = $product.DisplayVersion; Expected = $Value; Type = 'Version'; Operator = $Operator
            }
            $comparison = Compare-IslRuleValue @comparisonSplat
            $result.Met = $comparison.Met
            $result.Reason = "$($product.DisplayName) is installed ($($product.View)); version " +
                $comparison.Reason
        }
    }

    Write-Verbose "Completed $($MyInvocation.MyCommand.Name): $($result.Reason)"
    $result
}