Private/Resolve-IntuneAccessDeviceAssignment.ps1
|
function Test-IntuneAccessAssignmentFilterRule { [CmdletBinding()] param( [AllowNull()] [object] $Filter, [Parameter(Mandatory)] [object] $Device ) if ($null -eq $Filter) { return [PSCustomObject] @{ State = 'NotEvaluated'; Explanation = 'The assignment referenced a filter that was not collected.'; Rule = ''; ObservedValue = $null } } $rule = [string] (Get-IntuneAccessProperty $Filter 'Rule') if ([string]::IsNullOrWhiteSpace($rule)) { return [PSCustomObject] @{ State = 'NotEvaluated'; Explanation = 'The assignment filter rule was empty.'; Rule = $rule; ObservedValue = $null } } $propertyMap = @{ 'device.devicename' = 'DeviceName'; 'device.manufacturer' = 'Manufacturer'; 'device.model' = 'Model' 'device.operatingsystemsku' = 'OperatingSystemSku'; 'device.osversion' = 'OsVersion' 'device.ownership' = 'Ownership'; 'device.enrollmentprofileName' = 'EnrollmentProfileName' } $match = [regex]::Match($rule.Trim(), '^\(?\s*\[?(?<property>device\.[A-Za-z0-9]+)\]?\s+-(?<operator>eq|ne|contains|startsWith)\s+"(?<value>[^"]*)"\s*\)?$', 'IgnoreCase') if (-not $match.Success) { return [PSCustomObject] @{ State = 'NotEvaluated'; Explanation = 'The filter uses a compound or unsupported expression. The raw rule is retained.'; Rule = $rule; ObservedValue = $null } } $propertyKey = $match.Groups['property'].Value.ToLowerInvariant() if (-not $propertyMap.ContainsKey($propertyKey)) { return [PSCustomObject] @{ State = 'NotEvaluated'; Explanation = "The device property '$propertyKey' is not present in the collected evidence model."; Rule = $rule; ObservedValue = $null } } $observed = [string] (Get-IntuneAccessProperty $Device $propertyMap[$propertyKey] '') $expected = $match.Groups['value'].Value $isMatch = switch ($match.Groups['operator'].Value.ToLowerInvariant()) { 'eq' { $observed -ieq $expected } 'ne' { $observed -ine $expected } 'contains' { $observed.IndexOf($expected, [StringComparison]::OrdinalIgnoreCase) -ge 0 } 'startswith' { $observed.StartsWith($expected, [StringComparison]::OrdinalIgnoreCase) } } [PSCustomObject] @{ State = if ($isMatch) { 'Matched' } else { 'NotMatched' }; Explanation = "The supported filter expression was evaluated against $propertyKey."; Rule = $rule; ObservedValue = $observed } } function Resolve-IntuneAccessDeviceAssignment { [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Device, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $Workload, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $Assignment, [Parameter(Mandatory)] [object] $DeviceMembership, [Parameter(Mandatory)] [object] $UserMembership, [AllowEmptyCollection()] [object[]] $DeploymentOutcome = @() ) $deviceGroupIds = @((Get-IntuneAccessProperty $DeviceMembership 'GroupIds' @()) | ForEach-Object { ([string] $_).ToLowerInvariant() }) $userGroupIds = @((Get-IntuneAccessProperty $UserMembership 'GroupIds' @()) | ForEach-Object { ([string] $_).ToLowerInvariant() }) $deviceMembershipState = [string] (Get-IntuneAccessProperty $DeviceMembership 'State' 'NotEvaluated') $userMembershipState = [string] (Get-IntuneAccessProperty $UserMembership 'State' 'NotEvaluated') $results = [System.Collections.Generic.List[object]]::new() foreach ($item in $Workload) { $itemAssignments = @($Assignment | Where-Object WorkloadId -EQ $item.Id) $paths = [System.Collections.Generic.List[object]]::new() foreach ($configured in $itemAssignments) { $targetType = [string] $configured.TargetType $groupId = ([string] $configured.GroupId).ToLowerInvariant() $isExclusion = $targetType -eq 'Excluded group' $deviceTargetState = 'NotMatched' $userTargetState = 'NotMatched' if ($targetType -eq 'All devices') { $deviceTargetState = 'Matched'; $userTargetState = 'NotApplicable' } elseif ($targetType -in @('All users', 'All licensed users')) { $userTargetState = if ([string]::IsNullOrWhiteSpace([string] $Device.UserId)) { 'NotEvaluated' } else { 'Matched' }; $deviceTargetState = 'NotApplicable' } elseif ($targetType -in @('Included group', 'Excluded group')) { $deviceTargetState = if ($deviceMembershipState -eq 'Evaluated') { if ($groupId -in $deviceGroupIds) { 'Matched' } else { 'NotMatched' } } else { 'NotEvaluated' } $userTargetState = if ($userMembershipState -eq 'Evaluated') { if ($groupId -in $userGroupIds) { 'Matched' } else { 'NotMatched' } } elseif ($userMembershipState -eq 'NotApplicable') { 'NotApplicable' } else { 'NotEvaluated' } } else { $deviceTargetState = 'NotEvaluated'; $userTargetState = 'NotEvaluated' } $targetMatch = $deviceTargetState -eq 'Matched' -or $userTargetState -eq 'Matched' $targetUncertain = $deviceTargetState -eq 'NotEvaluated' -or $userTargetState -eq 'NotEvaluated' $filterEvaluation = [PSCustomObject] @{ State = 'NotApplicable'; Explanation = 'No assignment filter was configured.'; Rule = ''; ObservedValue = $null } if (-not [string]::IsNullOrWhiteSpace([string] $configured.FilterId)) { $filterEvaluation = Test-IntuneAccessAssignmentFilterRule -Filter $configured.Filter -Device $Device if ([string] $configured.FilterMode -ieq 'exclude') { if ($filterEvaluation.State -eq 'Matched') { $filterEvaluation.State = 'Excluded' } elseif ($filterEvaluation.State -eq 'NotMatched') { $filterEvaluation.State = 'Passed' } } else { if ($filterEvaluation.State -eq 'Matched') { $filterEvaluation.State = 'Passed' } elseif ($filterEvaluation.State -eq 'NotMatched') { $filterEvaluation.State = 'FilteredOut' } } } $pathState = if ($isExclusion -and $targetMatch) { 'Excluded' } elseif ($targetMatch -and $filterEvaluation.State -in @('NotApplicable', 'Passed')) { 'Included' } elseif ($targetMatch -and $filterEvaluation.State -in @('FilteredOut', 'Excluded')) { 'FilteredOut' } elseif ($targetMatch -and $filterEvaluation.State -eq 'NotEvaluated') { 'NotEvaluated' } elseif ($targetUncertain) { 'NotEvaluated' } else { 'NotMatched' } $paths.Add([PSCustomObject] @{ PSTypeName = 'IntuneAccess.AssignmentPath' AssignmentId = [string] $configured.Id Intent = [string] $configured.Intent TargetType = $targetType GroupId = [string] $configured.GroupId GroupName = [string] (Get-IntuneAccessProperty (Get-IntuneAccessProperty $configured 'Group') 'DisplayName' '') DeviceTargetState = $deviceTargetState UserTargetState = $userTargetState FilterId = [string] $configured.FilterId FilterMode = [string] $configured.FilterMode FilterState = [string] $filterEvaluation.State FilterRule = [string] $filterEvaluation.Rule FilterObservedValue = $filterEvaluation.ObservedValue PathState = $pathState SourceApiVersion = [string] $configured.SourceApiVersion EvidenceState = if ($pathState -eq 'NotEvaluated') { 'NotEvaluated' } else { 'CalculatedFromObservedConfiguration' } }) } $pathArray = $paths.ToArray() $calculated = if (@($pathArray | Where-Object PathState -EQ 'Excluded').Count -gt 0) { 'Excluded' } elseif (@($pathArray | Where-Object PathState -EQ 'Included').Count -gt 0) { 'Included' } elseif (@($pathArray | Where-Object PathState -EQ 'NotEvaluated').Count -gt 0) { 'NotEvaluated' } elseif ($itemAssignments.Count -eq 0) { 'NotAssigned' } else { 'NotTargeted' } $outcomes = @($DeploymentOutcome | Where-Object { $_.WorkloadId -eq $item.Id -and $_.DeviceId -eq $Device.Id }) $reportedState = if ($outcomes.Count -eq 0) { 'NoReportedEvidence' } elseif (@($outcomes | Where-Object Category -EQ 'Error').Count -gt 0) { 'Error' } elseif (@($outcomes | Where-Object Category -EQ 'Success').Count -gt 0) { 'Success' } else { [string] $outcomes[0].Category } $results.Add([PSCustomObject] @{ PSTypeName = 'IntuneAccess.DeviceAssignmentExplanation' DeviceId = [string] $Device.Id DeviceName = [string] $Device.DeviceName WorkloadId = [string] $item.Id WorkloadName = [string] $item.Name WorkloadType = [string] $item.WorkloadType AssignmentState = $calculated ReportedOutcomeState = $reportedState AssignmentPaths = $pathArray DeploymentOutcomes = $outcomes EvidenceBoundary = 'Assignment configuration, group membership, supported filter evaluation and reported outcome are separate evidence layers. Assignment does not prove delivery.' GeneratedAt = [DateTimeOffset]::Now }) } $results.ToArray() } |