Public/Test-IntuneAssignmentFilter.ps1
|
function Test-IntuneAssignmentFilter { <# .EXTERNALHELP IntuneScriptLab-Help.xml .SYNOPSIS Evaluates an assignment filter rule against a device the way the Intune service does. #> [CmdletBinding()] [OutputType('IntuneScriptLab.FilterResult')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$Rule, [Parameter(Position = 1)] $Device, [ValidateSet('Include', 'Exclude')] [string]$Mode = 'Include', [switch]$SyntaxOnly ) begin { Write-Verbose "Starting $($MyInvocation.MyCommand.Name) ($Mode)" $facts = [System.Collections.Generic.Dictionary[string, object]]::new( [System.StringComparer]::OrdinalIgnoreCase) $source = $null if ($null -ne $Device) { $source = $Device } elseif (-not $SyntaxOnly) { $source = Get-IslFilterDeviceFact } if ($source -is [System.Collections.IDictionary]) { foreach ($key in $source.Keys) { $facts["$key"] = $source[$key] } } elseif ($null -ne $source) { foreach ($property in $source.PSObject.Properties) { $facts[$property.Name] = $property.Value } } $view = [ordered]@{} foreach ($key in @($facts.Keys | Sort-Object)) { $view[$key] = $facts[$key] } $deviceView = [pscustomobject]$view function Get-VersionPart { # Four numeric parts, missing ones as 0; $null when the text is not a version param([string]$Text) if ($Text -notmatch '^\s*\d+(\.\d+){0,3}\s*$') { return $null } $parts = [System.Collections.Generic.List[long]]::new() foreach ($part in ($Text.Trim() -split '\.')) { $parts.Add([long]$part) } while ($parts.Count -lt 4) { $parts.Add(0) } $parts.ToArray() } function Compare-Version { # -1, 0 or 1 the way the filter evaluator ordered versions; $null when a side is not one param([string]$Actual, [string]$Expected) $left = Get-VersionPart -Text $Actual $right = Get-VersionPart -Text $Expected if ($null -eq $left -or $null -eq $right) { return $null } for ($index = 0; $index -lt 4; $index++) { if ($left[$index] -lt $right[$index]) { return -1 } if ($left[$index] -gt $right[$index]) { return 1 } } 0 } function Test-Clause { param($Clause) $raw = if ($facts.ContainsKey($Clause.Property)) { $facts[$Clause.Property] } else { $null } $actual = if ($null -eq $raw) { '' } else { "$raw" } $Clause.Actual = if ($null -eq $raw) { $null } else { "$raw" } $values = @(foreach ($item in @($Clause.Value)) { if ($null -ne $item) { "$item".Trim() } }) $expected = if ($values.Count) { $values[0] } else { '' } $ignoreCase = [System.StringComparison]::OrdinalIgnoreCase $order = $null if ($Clause.Kind -eq 'Version') { $order = Compare-Version -Actual $actual -Expected $expected } $matched = switch ($Clause.Operator) { 'eq' { if ($Clause.Kind -eq 'Version') { $order -eq 0 } else { $actual -eq $expected } } 'ne' { if ($Clause.Kind -eq 'Version') { $order -ne 0 } else { $actual -ne $expected } } 'in' { $values -contains $actual } 'notIn' { $values -notcontains $actual } 'startsWith' { $actual.StartsWith($expected, $ignoreCase) } 'contains' { $actual.IndexOf($expected, $ignoreCase) -ge 0 } 'notContains' { $actual.IndexOf($expected, $ignoreCase) -lt 0 } 'gt' { $null -ne $order -and $order -gt 0 } 'ge' { $null -ne $order -and $order -ge 0 } 'lt' { $null -ne $order -and $order -lt 0 } 'le' { $null -ne $order -and $order -le 0 } } $Clause.Matched = [bool]$matched $Clause.Matched } function Test-Node { param($Node) switch ($Node.Type) { 'Clause' { Test-Clause -Clause $Node.Clause } 'And' { # Both sides run so every clause reports what it saw $left = Test-Node -Node $Node.Left $right = Test-Node -Node $Node.Right $left -and $right } 'Or' { $left = Test-Node -Node $Node.Left $right = Test-Node -Node $Node.Right $left -or $right } } } } process { $parsed = ConvertFrom-IslFilterRule -Rule $Rule if ($parsed.Error) { $errorSplat = @{ Message = $parsed.Error ErrorId = 'IslFilterRuleInvalid' Category = 'InvalidData' TargetObject = $Rule } Write-Error @errorSplat return } foreach ($warning in $parsed.Warnings) { Write-Warning $warning.Message } $matched = $null $applicable = $null $clauseCount = @($parsed.Clauses).Count if ($SyntaxOnly) { $reason = "The rule is valid: $clauseCount clause(s), $(@($parsed.Warnings).Count) warning(s)" } else { $matched = [bool](Test-Node -Node $parsed.Tree) $applicable = if ($Mode -eq 'Include') { $matched } else { -not $matched } $reason = if ($Mode -eq 'Include' -and $matched) { 'Included: the rule matches this device, so the assignment applies' } elseif ($Mode -eq 'Include') { 'Not applicable: the rule does not match this device; the portal shows "Filters criteria ' + 'are not met." (W32-FILTER-INCLUDE)' } elseif ($matched) { 'Not applicable: the exclude rule matches this device (W32-FILTER-EXCLUDE)' } else { 'Included: the exclude rule does not match this device, so the assignment applies' } } Write-Verbose "Rule with $clauseCount clause(s): matched=$matched applicable=$applicable" [pscustomobject]@{ PSTypeName = 'IntuneScriptLab.FilterResult' Rule = $Rule Mode = $Mode Matched = $matched Applicable = $applicable Reason = $reason Clauses = $parsed.Clauses Warnings = @($parsed.Warnings | ForEach-Object { $_.Message }) Device = $deviceView } } end { Write-Verbose "Completed $($MyInvocation.MyCommand.Name)" } } |