Public/Test-IntuneResourceAccess.ps1

function Test-IntuneResourceAccess {
    <#
    .SYNOPSIS
    Explains a possible Intune RBAC access path to a managed device.
    .DESCRIPTION
    This command returns AccessPathFound only when permission, administrator-group,
    scope-group and scope-tag evidence can all be established. It never infers AccessDenied.
    .PARAMETER UserPrincipalName
    The administrator to examine.
    .PARAMETER UserId
    The administrator's Microsoft Entra object ID.
    .PARAMETER DeviceName
    Exact Intune managed-device display name.
    .PARAMETER RequiredAction
    Exact Graph action to test. Defaults to managed-device read.
    .EXAMPLE
    Test-IntuneResourceAccess -UserPrincipalName 'admin@contoso.com' -DeviceName 'PC-001'
    #>

    [CmdletBinding(DefaultParameterSetName = 'ByUpn')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ByUpn')] [ValidateNotNullOrEmpty()] [string] $UserPrincipalName,
        [Parameter(Mandatory, ParameterSetName = 'ById')] [guid] $UserId,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $DeviceName,
        [ValidateNotNullOrEmpty()] [string] $RequiredAction = 'Microsoft.Intune_ManagedDevices_Read'
    )

    $null = Assert-IntuneAccessConnection -RequiredScope @(
        'DeviceManagementManagedDevices.Read.All', 'Device.Read.All'
    )
    $escapedName = $DeviceName.Replace("'", "''")
    $filter = [uri]::EscapeDataString("deviceName eq '$escapedName'")
    $stableUri = "deviceManagement/managedDevices?`$filter=$filter&`$select=id,deviceName,azureADDeviceId,userId"
    $devices = @(Invoke-IntuneAccessGraphRequest -Uri $stableUri)
    if ($devices.Count -eq 0) {
        throw "No Intune managed device with the exact name '$DeviceName' was found."
    }
    if ($devices.Count -gt 1) {
        throw "Several Intune managed devices have the exact name '$DeviceName'. Use a unique name; no device was selected arbitrarily."
    }

    $device = $devices[0]
    $resourceWarnings = [System.Collections.Generic.List[string]]::new()
    $betaDevice = $null
    try {
        $betaUri = "deviceManagement/managedDevices?`$filter=$filter&`$select=id,roleScopeTagIds"
        $betaDevices = @(Invoke-IntuneAccessGraphRequest -Uri $betaUri -ApiVersion beta)
        $deviceId = [string] (Get-IntuneAccessProperty $device 'id')
        $betaDevice = $betaDevices | Where-Object {
            [string] (Get-IntuneAccessProperty $_ 'id') -eq $deviceId
        } | Select-Object -First 1
        if ($null -eq $betaDevice) {
            $resourceWarnings.Add("Beta scope-tag enrichment did not return managed device '$deviceId'. Device scope-tag matching is NotEvaluated.")
        }
    }
    catch {
        $resourceWarnings.Add("Beta scope-tag enrichment failed for managed device '$DeviceName'. Stable v1.0 device data is retained and scope-tag matching is NotEvaluated. $($_.Exception.Message)")
    }
    $azureDeviceId = [string] (Get-IntuneAccessProperty $device 'azureADDeviceId')
    $parsedDeviceId = [guid]::Empty
    $deviceMembership = if ([guid]::TryParse($azureDeviceId, [ref] $parsedDeviceId)) {
        Get-IntuneAccessDeviceMembership -EntraDeviceId $parsedDeviceId
    }
    else {
        [PSCustomObject] @{ State = 'NotEvaluated'; Device = $null; GroupIds = @(); Groups = @(); Explanation = 'The managed device did not return a usable Microsoft Entra device ID.' }
    }
    $managedUserId = [string] (Get-IntuneAccessProperty $device 'userId')
    $associatedUserMembership = Get-IntuneAccessManagedDeviceUserMembership -UserId $managedUserId

    $access = if ($PSCmdlet.ParameterSetName -eq 'ById') {
        Get-IntuneAdminAccess -UserId $UserId
    }
    else {
        Get-IntuneAdminAccess -UserPrincipalName $UserPrincipalName
    }
    $missingValue = [object]::new()
    $deviceTagValue = Get-IntuneAccessProperty $betaDevice 'roleScopeTagIds' $missingValue
    $deviceTagDataState = if ([object]::ReferenceEquals($deviceTagValue, $missingValue)) { 'Missing' } else { 'Available' }
    $deviceTagIds = @($(if ($deviceTagDataState -eq 'Missing') { @() } else { @($deviceTagValue) }))
    if ($deviceTagDataState -eq 'Available' -and $deviceTagIds.Count -eq 0) { $deviceTagIds = @('0') }

    $paths = [System.Collections.Generic.List[object]]::new()
    foreach ($assignment in $access.RoleAssignments) {
        $permissionMatch = $RequiredAction -in @($assignment.Permissions)
        $adminMatch = $assignment.Applicability -eq 'Confirmed'

        $scopeGroupState = 'NotEvaluated'
        $scopeMatchSource = 'None'
        $scopeMatchGroupIds = @()
        $scopeType = [string] $assignment.ScopeType
        if ((Get-IntuneAccessProperty $assignment 'ScopeTypeDataState' 'Available') -eq 'Missing') {
            $scopeGroupState = 'NotEvaluated'
        }
        elseif ($scopeType -in @('allDevices', 'allDevicesAndLicensedUsers')) {
            $scopeGroupState = 'Matched'
            $scopeMatchSource = 'VirtualAllDevices'
        }
        elseif ((Get-IntuneAccessProperty $assignment 'ScopeGroupDataState' 'Available') -eq 'Missing') {
            $scopeGroupState = 'NotEvaluated'
        }
        elseif ($scopeType -eq 'resourceScope') {
            $scopeGroupIds = @($assignment.RawIds.ScopeGroupIds)
            $deviceMatches = @($(if ($deviceMembership.State -eq 'Evaluated') {
                @($scopeGroupIds | Where-Object { $_ -in $deviceMembership.GroupIds })
            }
            else { @() }))
            $userMatches = @($(if ($associatedUserMembership.State -eq 'Evaluated') {
                @($scopeGroupIds | Where-Object { $_ -in $associatedUserMembership.GroupIds })
            }
            else { @() }))

            if ($deviceMatches.Count -gt 0) {
                $scopeGroupState = 'Matched'
                $scopeMatchSource = 'DeviceGroup'
                $scopeMatchGroupIds = $deviceMatches
            }
            elseif ($userMatches.Count -gt 0) {
                $scopeGroupState = 'Matched'
                $scopeMatchSource = 'AssociatedUserGroup'
                $scopeMatchGroupIds = $userMatches
            }
            elseif ($deviceMembership.State -eq 'Evaluated' -and
                $associatedUserMembership.State -in @('Evaluated', 'NotApplicable')) {
                $scopeGroupState = 'NotMatched'
            }
            else {
                $scopeGroupState = 'NotEvaluated'
            }
        }

        $assignmentTagIds = @($assignment.RawIds.ScopeTagIds)
        $scopeTagState = if ((Get-IntuneAccessProperty $assignment 'ScopeTagDataState' 'Available') -eq 'Missing') {
            'NotEvaluated'
        }
        elseif ($assignmentTagIds.Count -eq 0) {
            'MatchedAllTags'
        }
        elseif ($deviceTagDataState -eq 'Missing') {
            'NotEvaluated'
        }
        elseif (@($assignmentTagIds | Where-Object { $_ -in $deviceTagIds }).Count -gt 0) {
            'Matched'
        }
        else {
            'NotMatched'
        }

        $pathState = if ($permissionMatch -and $adminMatch -and $scopeGroupState -eq 'Matched' -and $scopeTagState -in @('Matched', 'MatchedAllTags')) {
            'AccessPathFound'
        }
        else {
            'NotEvaluated'
        }
        $paths.Add([PSCustomObject] @{
            State                = $pathState
            RoleDefinition       = $assignment.RoleDefinition.DisplayName
            RoleAssignment       = $assignment.Name
            AdminGroups          = @($assignment.AdminGroupEvidence)
            Permission           = $RequiredAction
            PermissionMatch      = $permissionMatch
            AdminMembershipState = $assignment.Applicability
            ScopeGroupState      = $scopeGroupState
            ScopeMatchSource     = $scopeMatchSource
            ScopeMatchGroupIds   = $scopeMatchGroupIds
            ScopeGroups          = @($assignment.ScopeGroups)
            ScopeTagState        = $scopeTagState
            ScopeTags            = @($assignment.ScopeTags)
        })
    }

    $found = @($paths | Where-Object State -EQ 'AccessPathFound')
    [PSCustomObject] @{
        PSTypeName      = 'IntuneAccess.ResourceAccessExplanation'
        Experimental    = $true
        Administrator   = $access.User
        Resource        = [PSCustomObject] @{
            Id          = [string] (Get-IntuneAccessProperty $device 'id')
            Name        = [string] (Get-IntuneAccessProperty $device 'deviceName')
            Type        = 'Managed device'
            EntraDeviceId = $azureDeviceId
            AssociatedUserId = if ([string]::IsNullOrWhiteSpace($managedUserId)) { $null } else { $managedUserId }
            ScopeTagIds = $deviceTagIds
            ScopeTagDataState = $deviceTagDataState
            PropertySource = [PSCustomObject] @{
                IdentityAndRelationships = 'v1.0'
                ScopeTags = if ($deviceTagDataState -eq 'Available') { 'beta' } else { 'Missing' }
            }
        }
        RequiredAction  = $RequiredAction
        Result          = if ($found.Count -gt 0) { 'AccessPathFound' } else { 'NotEvaluated' }
        Reason          = if ($found.Count -gt 0) {
            "$($found.Count) complete Intune RBAC access path(s) were observed."
        }
        else {
            'No complete path could be proved. This is not an access-denied decision; Microsoft Entra roles and unsupported scope cases are not evaluated.'
        }
        AccessPaths     = $paths.ToArray()
        Evidence        = @($found)
        ResourceScopeEvidence = [PSCustomObject] @{
            DeviceMembershipState = $deviceMembership.State
            DeviceGroupIds        = @($deviceMembership.GroupIds)
            AssociatedUserMembershipState = $associatedUserMembership.State
            AssociatedUserGroupIds = @($associatedUserMembership.GroupIds)
        }
        Warnings        = @($access.Warnings) + $resourceWarnings.ToArray()
        GeneratedAt     = [DateTimeOffset]::Now
        ToolVersion     = $script:IntuneAccessVersion
    }
}