Public/New-IntuneWin32Rule.ps1

function New-IntuneWin32Rule {
    <#
    .SYNOPSIS
        Creates detection or requirement rules for Intune Win32 applications.

    .DESCRIPTION
        The New-IntuneWin32Rule function generates rule objects that define detection logic or system requirements
        for Win32 applications in Microsoft Intune. It supports file/folder checks, registry checks,
        script-based detection, and MSI product code detection.

    .PARAMETER RuleParentType
        The parent type of rule to create: 'detection' or 'requirement'.

    .PARAMETER RuleType
        The type of rule to create: 'FileOrFolder', 'Registry', 'Script', or 'MSI'.
        The other parameters are dynamic and depend on RuleType:
          FileOrFolder Path, FileOrFolderName, OperationType, Operator, ComparisonValue,
                        Check32BitOn64BitSystem
          Registry Path (key path), KeyName (optional sub key), ValueName, Operator (exists,
                        notExists or a comparison), DataType (string, integer, version) and Value for
                        comparisons, Check32BitOn64BitSystem
          Script ScriptPath, RunAs32Bit, EnforceSignatureCheck; for requirement rules also
                        RunAsAccount, DisplayName, Operator, ComparisonValue, OperationType
          MSI MSIPath, ProductCode, ProductVersionOperator, ProductVersion, AutoDetect
                        (AutoDetect reads the product code and version from the MSI; Windows only).
                        MSI rules can only be detection rules.

    .EXAMPLE
        New-IntuneWin32Rule -RuleParentType 'detection' -RuleType 'FileOrFolder' -Path "C:\Program Files\MyApp" -FileOrFolderName "MyApp.exe" -OperationType "exists"

        Creates a file existence detection rule.

    .EXAMPLE
        New-IntuneWin32Rule -RuleParentType 'detection' -RuleType 'Registry' -Path "HKLM:\Software" -KeyName "MyApp" -ValueName "Version" -Operator "equal" -DataType "version" -Value "1.0"

        Creates a registry value detection rule.

    .EXAMPLE
        New-IntuneWin32Rule -RuleParentType 'requirement' -RuleType 'Script' -ScriptPath "C:\Scripts\check.ps1" -RunAsAccount system

        Creates a script-based requirement rule.

    .OUTPUTS
        System.Collections.Hashtable
        The rule, with property names as used by the Microsoft Graph win32LobApp rule types.

    .NOTES
        This function uses dynamic parameters based on the RuleType selected.
        Different rule types require different parameters to be specified.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Only builds an in-memory hashtable; no system state is changed.')]
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param (
        [Parameter(Mandatory, HelpMessage = 'The parent type of rule to create.')]
        [ValidateSet('detection', 'requirement')]
        [string]$RuleParentType,

        [Parameter(Mandatory, HelpMessage = 'The type of rule to create.')]
        [ValidateSet('FileOrFolder', 'Registry', 'Script', 'MSI')]
        [string]$RuleType
    )

    DynamicParam {
        $paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        $MultiSetParameters = @(
            @{Name = "Operator"; ParameterType = [string]; HelpMessage = "The comparison operator.";
                ValidateSet = @('equal', 'notEqual', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual')
            },
            @{Name = "ComparisonValue"; ParameterType = [string]; HelpMessage = "The value to compare against." },
            @{Name = "Path"; ParameterType = [string]; Mandatory = $true; HelpMessage = "The path to the file or folder.";
                ValidateScript = { if (Test-Path -Path $_ -IsValid) { $true } else { throw "Path doesn't seem to be valid." } }
            },
            @{Name = "OperationType"; ParameterType = [string]; ValidateSet = @("notConfigured", "string", "dateTime", "integer", "float", "version", "boolean");
                HelpMessage = "The operation type for comparison."
            }
        )

        # Define parameter sets based on RuleType
        switch ($RuleType) {
            'FileOrFolder' {
                $fileParams = @(
                    @{Name = "FileOrFolderName"; ParameterType = [string]; Mandatory = $true; HelpMessage = "The file or folder name." }
                    @{Name = "Check32BitOn64BitSystem"; ParameterType = [switch]; HelpMessage = "Expand 32bit variables on a 64bit system?" }
                )
                $FileSystemParamOperationType = $MultiSetParameters[3]
                $FileSystemParamOperationType.Mandatory = $true
                $FileSystemParamOperationType.ValidateSet = @("notConfigured", "exists", "modifiedDate", "createdDate", "version", "sizeInMB")
                $fileParams += $FileSystemParamOperationType

                $fileParams += $MultiSetParameters[2]
                $fileParams += $MultiSetParameters[0]
                $fileParams += $MultiSetParameters[1]
                foreach ($p in $fileParams) {
                    $param = $(New-DynamicParameter @p)
                    $paramDictionary.Add($param.Name, $param.Parameter)
                }
            }
            'Registry' {
                $registryParams = @(
                    @{Name = "Path"; ParameterType = [string]; Mandatory = $true; HelpMessage = "The registry key path, for example HKEY_LOCAL_MACHINE\Software." }
                    @{Name = "KeyName"; ParameterType = [string]; HelpMessage = "The registry key name, appended to Path." }
                    @{Name = "Check32BitOn64BitSystem"; ParameterType = [switch]; HelpMessage = "Search the 32-bit registry on 64-bit systems." }
                    @{Name = "ValueName"; ParameterType = [string]; HelpMessage = "The registry value name." }
                    @{Name = "Operator"; ParameterType = [string]; HelpMessage = "The registry comparison operator.";
                        ValidateSet = @('equal', 'notEqual', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'exists', 'notExists')
                    }
                    @{Name = "DataType"; ParameterType = [string]; HelpMessage = "The registry value data type.";
                        ValidateSet = @('string', 'integer', 'version')
                    }
                    @{Name = "Value"; ParameterType = [string]; HelpMessage = "The registry value to compare against." }
                )
                foreach ($p in $registryParams) {
                    $param = $(New-DynamicParameter @p)
                    $paramDictionary.Add($param.Name, $param.Parameter)
                }
            }
            'Script' {
                $scriptParams = @(
                    @{Name = "ScriptPath"; ParameterType = [string]; Mandatory = $true; HelpMessage = "The script path.";
                        ValidateScript = { if (Test-Path -Path $_ ) { $true } else { throw "Script path doesn't seem to be valid." } }
                    }
                    @{Name = "RunAs32Bit"; ParameterType = [switch]; HelpMessage = "Run script as 32-bit on 64-bit clients" }
                    @{Name = "EnforceSignatureCheck"; ParameterType = [switch]; HelpMessage = "Enforce script signature check" }

                )
                if ($RuleParentType -ne 'detection') {
                    $scriptParams += @{Name = "RunAsAccount"; ParameterType = [string]; Mandatory = $true; ValidateSet = @("system", "user");
                        HelpMessage = "The account to run the script as."
                    }
                    $scriptParams += @{Name = "DisplayName"; ParameterType = [string]; HelpMessage = "The display name of the script." }
                    $scriptParams += $MultiSetParameters[0, 1, 3]
                }
                foreach ($p in $scriptParams) {
                    $param = $(New-DynamicParameter @p)
                    $paramDictionary.Add($param.Name, $param.Parameter)
                }
            }
            'MSI' {
                $msiParams = @(
                    @{Name = "MSIPath"; ParameterType = [string]; Mandatory = $true; HelpMessage = "The path to the MSI file.";
                        ValidateScript = { if (Test-Path -Path $_ -IsValid) { $true }else { throw "MSI path doesn't seem to be valid." } }
                    }
                    @{Name = "ProductCode"; ParameterType = [string]; HelpMessage = "The product code to detect." }
                    @{Name = "ProductVersionOperator"; ParameterType = [string]; HelpMessage = "The MSI operation type.";
                        ValidateSet = @('notConfigured', 'equal', 'notEqual', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual')
                    }
                    @{Name = "ProductVersion"; ParameterType = [string]; HelpMessage = "The value to compare against." }
                    @{Name = "AutoDetect"; ParameterType = [bool]; HelpMessage = "Detect the MSI properties automatically." }
                )
                foreach ($p in $msiParams) {
                    $param = $(New-DynamicParameter @p)
                    $paramDictionary.Add($param.Name, $param.Parameter)
                }
            }
        }
        return $paramDictionary
    }
    begin {
        $TelemetryArgs = @{
            ModuleName    = $MyInvocation.MyCommand.Module.Name
            ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
            CommandName   = $MyInvocation.MyCommand.Name
            ExecutionID   = [guid]::NewGuid().ToString()
        }
        Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer
        $TelemetryFailed = $false
    }
    process {
        try {
            # Builds the rule types documented at
            # https://learn.microsoft.com/graph/api/resources/intune-apps-win32lobapprule
            # Dynamic parameters are not created as variables, so read them from PSBoundParameters
            $Bound = $PSBoundParameters
            $Operator = $(if ($Bound.ContainsKey('Operator')) { $Bound['Operator'] } else { 'notConfigured' })
            $Rule = [ordered]@{}
            switch ($RuleType) {
                'FileOrFolder' {
                    $Rule['@odata.type'] = '#microsoft.graph.win32LobAppFileSystemRule'
                    $Rule['ruleType'] = $RuleParentType
                    $Rule['path'] = $Bound['Path']
                    $Rule['fileOrFolderName'] = $Bound['FileOrFolderName']
                    $Rule['check32BitOn64System'] = [bool]$Bound['Check32BitOn64BitSystem']
                    $Rule['operationType'] = $Bound['OperationType']
                    $Rule['operator'] = $Operator
                    if ($Bound.ContainsKey('ComparisonValue')) {
                        $Rule['comparisonValue'] = $Bound['ComparisonValue']
                    }
                }
                'Registry' {
                    $KeyPath = [string]$Bound['Path']
                    if ($Bound['KeyName']) {
                        $KeyPath = $KeyPath.TrimEnd('\') + '\' + $Bound['KeyName']
                    }
                    $Rule['@odata.type'] = '#microsoft.graph.win32LobAppRegistryRule'
                    $Rule['ruleType'] = $RuleParentType
                    $Rule['check32BitOn64System'] = [bool]$Bound['Check32BitOn64BitSystem']
                    $Rule['keyPath'] = $KeyPath
                    $Rule['valueName'] = [string]$Bound['ValueName']
                    switch ($Operator) {
                        'exists' {
                            $Rule['operationType'] = 'exists'
                            $Rule['operator'] = 'notConfigured'
                        }
                        'notExists' {
                            $Rule['operationType'] = 'doesNotExist'
                            $Rule['operator'] = 'notConfigured'
                        }
                        'notConfigured' {
                            # Without an operator the rule checks that the key or value exists
                            $Rule['operationType'] = 'exists'
                            $Rule['operator'] = 'notConfigured'
                        }
                        default {
                            if (-not $Bound.ContainsKey('DataType')) {
                                throw "A registry comparison ($Operator) needs -DataType (string, integer or version)."
                            }
                            $Rule['operationType'] = $Bound['DataType']
                            $Rule['operator'] = $Operator
                            $Rule['comparisonValue'] = [string]$Bound['Value']
                        }
                    }
                }
                'Script' {
                    $ScriptPath = $Bound['ScriptPath']
                    try {
                        $ScriptContent = Get-Content -Path $ScriptPath -Raw -ErrorAction Stop
                    }
                    catch {
                        throw "Failed to read script content from $($ScriptPath): $($_.Exception.Message)"
                    }
                    $Rule['@odata.type'] = '#microsoft.graph.win32LobAppPowerShellScriptRule'
                    $Rule['ruleType'] = $RuleParentType
                    $Rule['enforceSignatureCheck'] = [bool]$Bound['EnforceSignatureCheck']
                    $Rule['runAs32Bit'] = [bool]$Bound['RunAs32Bit']
                    $Rule['scriptContent'] = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($ScriptContent))
                    if ($RuleParentType -eq 'detection') {
                        # Detection scripts must not set displayName, runAsAccount or a comparison
                        $Rule['operationType'] = 'notConfigured'
                        $Rule['operator'] = 'notConfigured'
                    }
                    else {
                        $Rule['displayName'] = $(if ($Bound['DisplayName']) { $Bound['DisplayName'] } else { Split-Path -Path $ScriptPath -Leaf })
                        $Rule['runAsAccount'] = $Bound['RunAsAccount']
                        $Rule['operationType'] = $(if ($Bound.ContainsKey('OperationType')) { $Bound['OperationType'] } else { 'notConfigured' })
                        $Rule['operator'] = $Operator
                        if ($Bound.ContainsKey('ComparisonValue')) {
                            $Rule['comparisonValue'] = $Bound['ComparisonValue']
                        }
                    }
                }
                'MSI' {
                    if ($RuleParentType -ne 'detection') {
                        throw 'MSI (product code) rules can only be used as detection rules.'
                    }
                    $ProductCode = $Bound['ProductCode']
                    $ProductVersion = $Bound['ProductVersion']
                    $VersionOperator = $(if ($Bound.ContainsKey('ProductVersionOperator')) { $Bound['ProductVersionOperator'] } else { 'notConfigured' })
                    if ($Bound['AutoDetect'] -eq $true) {
                        $MSIInfo = Get-MSIProperty -Path $Bound['MSIPath'] -ErrorAction Stop
                        $ProductCode = $MSIInfo.ProductCode
                        $ProductVersion = $MSIInfo.ProductVersion
                        $VersionOperator = 'equal'
                    }
                    if ([string]::IsNullOrEmpty($ProductCode)) {
                        throw 'An MSI rule needs -ProductCode or -AutoDetect $true.'
                    }
                    $Rule['@odata.type'] = '#microsoft.graph.win32LobAppProductCodeRule'
                    $Rule['ruleType'] = $RuleParentType
                    $Rule['productCode'] = $ProductCode
                    $Rule['productVersionOperator'] = $VersionOperator
                    $Rule['productVersion'] = $ProductVersion
                }
            }
            # Return a plain hashtable (the documented output type) with the keys in a stable order
            $Result = @{}
            foreach ($Key in $Rule.Keys) {
                $Result[$Key] = $Rule[$Key]
            }
            $Result
        }
        catch {
            if (-not $TelemetryFailed) {
                $TelemetryFailed = $true
                Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
            }
            throw
        }
    }
    end {
        if (-not $TelemetryFailed) {
            Invoke-TelemetryCollection @TelemetryArgs -Stage End
        }
    }
}