check-intune-user-enrollments.ps1

<#
.SYNOPSIS
  Lists users who are at or over device enrollment limit in either AAD or Intune
.DESCRIPTION
  Lists users who are at or over device enrollment limit in either AAD or Intune
  Displays all users in GridView
  Clicking on a user displays the devices, enrollment/registration date and the date last seen
  Checks through all Intune policies to find the one with the highest priority applicable to user
.INPUTS
None
.OUTPUTS
None
.NOTES
  Version: 1.0.2
  Author: Andrew Taylor
  WWW: andrewstaylor.com
  Creation Date: 09/01/2023
  Purpose/Change: Initial script development
  
.EXAMPLE
N/A
#>


<#PSScriptInfo
.VERSION 1.0.2
.GUID 979c8308-07a3-4918-9caf-58693fd44536
.AUTHOR AndrewTaylor
.COMPANYNAME
.COPYRIGHT GPL
.TAGS intune endpoint MEM environment enrollment
.LICENSEURI https://github.com/andrew-s-taylor/public/blob/main/LICENSE
.PROJECTURI https://github.com/andrew-s-taylor/public
.ICONURI
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
#>


################################ INSTALL MODULES ############################################

Write-Host "Installing Microsoft Graph modules if required (current user scope)"

#Install MS Graph if not available
if (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication) {
    Write-Host "Microsoft Graph Authentication Already Installed"
} 
else {
    Install-Module -Name Microsoft.Graph.Authentication -Scope CurrentUser -Repository PSGallery -Force -RequiredVersion 1.19.0 
    Write-Host "Microsoft Graph Authentication Installed"
}

if (Get-Module -ListAvailable -Name Microsoft.Graph.Users) {
    Write-Host "Microsoft Graph Users Already Installed "
} 
else {
    Install-Module -Name Microsoft.Graph.Users -Scope CurrentUser -Repository PSGallery -Force -RequiredVersion 1.19.0  
    Write-Host "Microsoft Graph Users Installed"
}

### IMPORT THEM
import-module Microsoft.Graph.Authentication
import-module Microsoft.Graph.Users

################################## END MODULES ##############################################

Function Get-ScriptVersion(){
    
    <#
    .SYNOPSIS
    This function is used to check if the running script is the latest version
    .DESCRIPTION
    This function checks GitHub and compares the 'live' version with the one running
    .EXAMPLE
    Get-ScriptVersion
    Returns a warning and URL if outdated
    .NOTES
    NAME: Get-ScriptVersion
    #>

    
    [cmdletbinding()]
    
    param
    (
        $liveuri
    )
$contentheaderraw = (Invoke-WebRequest -Uri $liveuri -Method Get)
$contentheader = $contentheaderraw.Content.Split([Environment]::NewLine)
$liveversion = (($contentheader | Select-String 'Version:') -replace '[^0-9.]','') | Select-Object -First 1
$currentversion = ((Get-Content -Path $PSCommandPath | Select-String -Pattern "Version: *") -replace '[^0-9.]','') | Select-Object -First 1
if ($liveversion -ne $currentversion) {
write-warning "Script has been updated, please download the latest version from $liveuri"
}
}
Get-ScriptVersion -liveuri "https://raw.githubusercontent.com/andrew-s-taylor/public/main/Powershell%20Scripts/Intune/check-intune-user-enrollments.ps1"

Function Connect-ToGraph {
    <#
.SYNOPSIS
Authenticates to the Graph API via the Microsoft.Graph.Authentication module.
  
.DESCRIPTION
The Connect-ToGraph cmdlet is a wrapper cmdlet that helps authenticate to the Intune Graph API using the Microsoft.Graph.Authentication module. It leverages an Azure AD app ID and app secret for authentication or user-based auth.
  
.PARAMETER Tenant
Specifies the tenant (e.g. contoso.onmicrosoft.com) to which to authenticate.
  
.PARAMETER AppId
Specifies the Azure AD app ID (GUID) for the application that will be used to authenticate.
  
.PARAMETER AppSecret
Specifies the Azure AD app secret corresponding to the app ID that will be used to authenticate.
 
.PARAMETER Scopes
Specifies the user scopes for interactive authentication.
  
.EXAMPLE
Connect-ToGraph -TenantId $tenantID -AppId $app -AppSecret $secret
  
-#>

    [cmdletbinding()]
    param
    (
        [Parameter(Mandatory = $false)] [string]$Tenant,
        [Parameter(Mandatory = $false)] [string]$AppId,
        [Parameter(Mandatory = $false)] [string]$AppSecret,
        [Parameter(Mandatory = $false)] [string]$scopes
    )

    Process {
        Import-Module Microsoft.Graph.Authentication
        $version = (get-module microsoft.graph.authentication | Select-Object -expandproperty Version).major

        if ($AppId -ne "") {
            $body = @{
                grant_type    = "client_credentials";
                client_id     = $AppId;
                client_secret = $AppSecret;
                scope         = "https://graph.microsoft.com/.default";
            }
     
            $response = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token -Body $body
            $accessToken = $response.access_token
     
            $accessToken
            if ($version -eq 2) {
                write-host "Version 2 module detected"
                $accesstokenfinal = ConvertTo-SecureString -String $accessToken -AsPlainText -Force
            }
            else {
                write-host "Version 1 Module Detected"
                Select-MgProfile -Name Beta
                $accesstokenfinal = $accessToken
            }
            $graph = Connect-MgGraph  -AccessToken $accesstokenfinal 
            Write-Host "Connected to Intune tenant $TenantId using app-based authentication (Azure AD authentication not supported)"
        }
        else {
            if ($version -eq 2) {
                write-host "Version 2 module detected"
            }
            else {
                write-host "Version 1 Module Detected"
                Select-MgProfile -Name Beta
            }
            $graph = Connect-MgGraph -scopes $scopes
            Write-Host "Connected to Intune tenant $($graph.TenantId)"
        }
    }
}    
##############################################################################################################################################
##### UPDATE THESE VALUES IF AUTOMATED #######################################################################################################
##############################################################################################################################################
##Set to Yes
$automated = "No"

##Your Azure Tenant ID
$tenantid = "<YOUR TENANT ID>"

##Your App Registration Details
$clientId = "<YOUR CLIENT ID>"
$clientSecret = "<YOUR CLIENT SECRET>"

$EmailAddress = "<YOUR EMAIL ADDRESS>"

##From Address
$MailSender = "<YOUR FROM ADDRESS>"


##############################################################################################################################################

if ($automated -eq "No") {
################################## CONNECT TO GRAPH ##########################################
Connect-ToGraph -Scopes "Device.Read.All, User.Read.All, Domain.Read.All, Directory.Read.All, DeviceManagementManagedDevices.ReadWrite.All, openid, profile, email, offline_access, Mail.Send"

################################## END CONNECT TO GRAPH ######################################
}
else {
#Connect to GRAPH API
 
#Get Creds and connect
#Connect to Graph
write-host "Connecting to Graph"

Connect-ToGraph -AppId $clientId -AppSecret $clientSecret -Tenant $tenantid
write-host "Graph Connection Established"

}

##Create an array to store the users in so we can output later
$usersatrisk = @{}

##Get all users
write-host "Getting AAD Users" -ForegroundColor Green
$allusers = get-mguser -All


##Create an array for all limits so we can find the smallest
$userassignmentlimits = @()


##Get AAD Restrictions for the tenant
write-host "Getting AAD Restrictions" -ForegroundColor Green
$aaduri = "https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy"
$aadrestriction = (Invoke-MgGraphRequest -Uri $aaduri -Method GET -OutputType PSObject).userDeviceQuota
write-host "AAD Restriction set to $aadrestriction" -ForegroundColor Green


##Get Intune Restrictions
write-host "Getting Intune Restrictions" -ForegroundColor Green
$allintuneuri = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=*"
##Get all of the policies from Intune
$intunerestrictions = (Invoke-MgGraphRequest -Uri $allintuneuri -Method GET -OutputType PSObject).value | where-object '@odata.type' -eq "#microsoft.graph.deviceEnrollmentLimitConfiguration" | Sort-Object priority
##Get Default (priority of 0)
write-host "Getting Default Intune Restrictions" -ForegroundColor Green
$defaultrestriction = ($intunerestrictions | where-object 'priority' -eq 0).limit
##Add to the array
$userassignmentlimits += $defaultrestriction
write-host "Default Intune Restriction set to $defaultrestriction" -ForegroundColor Green

##Get all Intune Managed Devices
write-host "Getting Intune Managed Devices" -ForegroundColor Green
$alldevicesuri = "https://graph.microsoft.com/beta/deviceManagement/manageddevices/"
$alldevices = (Invoke-MgGraphRequest -Method GET -Uri $alldevicesuri -OutputType PSObject).value

##Find all devices
##Create an array to store the devices and users for counting later
$deviceenroller = @()
##Create an array with device ID and user for looping through in drill-down
$devicesperuser = @()
##Loop through devices, get details and store in custom object within array
write-host "Getting Device Details" -ForegroundColor Green
$counter = 0
foreach ($device in $alldevices) {
    $deviceid = $device.id
    $counter++
    Write-Progress -Activity 'Processing Devices' -CurrentOperation $deviceid -PercentComplete (($counter / $alldevices.count) * 100)
    $deviceenrolleruri = "https://graph.microsoft.com/beta/deviceManagement/manageddevices/$deviceid/users"
    $assignednames = (((Invoke-MgGraphRequest -Method GET -Uri $deviceenrolleruri -OutputType PSObject).value).identities).issuerAssignedId
    ##Add to array
    $deviceenroller += $assignednames
    ##Create object to store device ID and user
    $object = [pscustomobject]@{
        DeviceID = $deviceid
        UserName = $assignednames
    }
    ##Add object to array
    $devicesperuser += $object

}

##Get Group Assigned Enrollment Limits
write-host "Getting Group Assigned Enrollment Limits" -ForegroundColor Green
##Build an empty hash table to use, otherwise it's really slow!
$userpolicies = @()
##Ignore the defaults, we grabbed those earlier
$nondefaultintune = $intunerestrictions | Where-Object 'priority' -ne 0 | Sort-Object priority
foreach ($setlimits in $nondefaultintune) {
    ##Ignore anything not assigned
    if ($null -ne $setlimits.assignments) {
        $priority = $setlimits.priority
        $limit = $setlimits.limit
        $assignid = $setlimits.id
        ##Get Group Assignments for each policy
        $assignmenturi = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/" + $assignid + "?`$expand=assignments"
        $individualassignments = ((Invoke-MgGraphRequest -Uri $assignmenturi -Method GET -OutputType PSObject).assignments).target
        foreach ($assignment in $individualassignments) {
            ##Check if user is in the group and add to array
            $groupid = $assignment.groupID
            $policyid = $assignid
            ##Create object to store policy details
            $object = [pscustomobject]@{
                PolicyID = $policyid
                GroupID  = $groupid
                Limit    = $limit
                Priority = $priority
            }
            ##Add to array
            $userpolicies += $object

        }
    }
}

##Get a list of devices and their owners, we'll drill down properly later
write-host "Getting Registered Devices and Owners" -ForegroundColor Green
$registereddeviceuri = "https://graph.microsoft.com/beta/devices?`$expand=registeredOwners"
$allregistered = (Invoke-MgGraphRequest -Uri $registereddeviceuri -Method GET -OutputType PSObject).value | select-object -ExpandProperty RegisteredOwners | select-object ID, userPrincipalName

write-host "Checking All Users" -ForegroundColor Green
##Now we need to loop through the users
$counter = 0
foreach ($user in $allusers) {
    ##Get User Details
    $userid = $user.id
    $username = $user.DisplayName
    $userupn = $user.UserPrincipalName
    $counter++
    Write-Progress -Activity 'Processing Users' -CurrentOperation $username -PercentComplete (($counter / $allusers.count) * 100)

    ##Get Users Groups
    $groupsuri = "https://graph.microsoft.com/v1.0/users/$userid/memberOf"
    $usergroups = (Invoke-MgGraphRequest -Uri $groupsuri -Method GET -OutputType PSObject).value | Select-Object ID

    ##Get User Assignments
    ##Policies are already in Priority order, loop through until the group hits
    foreach ($policy in $userpolicies) {
        $policygroupid = $policy.GroupID
        if ($usergroups -match $policygroupid) {
            ##Group found, add to array
            $userassignmentlimits += $policy.limit
            ##Stop there, we don't need anything else
            break
        }
    }
    
    ##Grab the final entry which if custom limits assigned will be the highest priority, if not array will only contain defaults anyway
    $lowestassignment = $userassignmentlimits | Select-Object -Last 1

    ##Get User Devices

    ##Check if registered is less than AAD Limit
    $registereddevices = (@($allregistered | where-object userPrincipalName -eq "$userupn" | Select-Object id)).count

    if ($registereddevices -ge $aadrestriction) {
        ##At limit, add to array
        $userdetails = $username + ":" + $userid
        $devicedetails = "Registered Devices - Total of " + $registereddevices + " Devices"
        $usersatrisk.add($userdetails, $devicedetails)
    }

    ##Check if managed is less than Intune limit
    ##Fine Devices for the user
    $devicecount = ($deviceenroller -match $userupn).Count

    if ($devicecount -ge $lowestassignment) {
        ##At limit, add to array
        $userdetails = $username + ":" + $userid
        $devicedetails = "Managed Devices - Total of " + $devicecount + " Devices"
        $usersatrisk.add($userdetails, $devicedetails)
    }

}

if ($automated -eq "No") {
write-host "Output into Grid-View" -ForegroundColor Green
##Output array for further selection
$selecteduser = $usersatrisk | Out-GridView -Title "Users at or above device limit" -OutputMode Single

##Check if Ok has been clicked
$checkforselection = $selecteduser.Name
If ($checkforselection)
{
   ##User Selected, continue
##########################################INDIVIDUAL USER ########################################

##Get User ID
$userid = ($selecteduser.Name -Split (":"))[1]


##Get Intune Limit
##Get User Details
write-host "Getting User Details" -ForegroundColor Green
$user = Get-MgUser -UserId $userid
$username = $user.DisplayName
$userupn = $user.UserPrincipalName
##Get Users Groups
write-host "Getting User Groups" -ForegroundColor Green
$groupsuri = "https://graph.microsoft.com/v1.0/users/$userid/memberOf"
$usergroups = (Invoke-MgGraphRequest -Uri $groupsuri -Method GET -OutputType PSObject).value | Select-Object ID

##Get User Assignments
write-host "Getting User Assignments" -ForegroundColor Green
foreach ($policy in $userpolicies) {
    $policygroupid = $policy.GroupID
    if ($usergroups -match $policygroupid) {
        ##Group found, add to array
        $userassignmentlimits += $policy.limit
        ##Stop there, we don't need anything else
        break
    }
}

$lowestassignment = $userassignmentlimits | Select-Object -Last 1
write-host "Intune Limit is $lowestassignment" -ForegroundColor Green

##Create an array to store device details
$userdevices = @()

##Get Registered Device
write-host "Getting Registered Devices for user $username" -ForegroundColor Green

$registereddeviceuri = "https://graph.microsoft.com/beta/users/$userid/registeredDevices"
$registereddevices = (Invoke-MgGraphRequest -Uri $registereddeviceuri -Method GET -OutputType PSObject).Value

foreach ($registereddevice in $registereddevices) {
    $deviceid = $registereddevice.id
    $displayname = $registereddevice.displayName
    $created = $registereddevice.registrationDateTime
    $lastseen = $registereddevice.approximateLastSignInDateTime
    $object = [pscustomobject]@{
        DeviceID    = $deviceid
        DisplayName = $displayname
        Created     = $created
        LastSeen    = $lastseen
        Type        = "Registered"
    }
    $userdevices += $object
}


##Get Managed Devices
write-host "Getting Managed Devices for user $username" -ForegroundColor Green
$manageddevicesperuser = $devicesperuser | Where-Object UserName -eq "$userupn" | Select-Object DeviceID
foreach ($manageddevice in $manageddevicesperuser) {
    $deviceid = $manageddevice.DeviceID
    $uri = "https://graph.microsoft.com/beta/deviceManagement/manageddevices/$deviceid/"
    $manageddevicedetails = Invoke-MgGraphRequest -Uri $uri -Method GET -OutputType PSObject
    $displayname = $manageddevicedetails.deviceName
    $created = $manageddevicedetails.enrolledDateTime
    $lastseen = $manageddevicedetails.lastSyncDateTime
    $object = [pscustomobject]@{
        DeviceID    = $deviceid
        DisplayName = $displayname
        Created     = $created
        LastSeen    = $lastseen
        Type        = "Managed"
    }
    $userdevices += $object
}

##Loop Through


$Title = "Devices for " + $username + " - AAD Limit: " + $aadrestriction + " - Intune Limit: " + $lowestassignment

$userdevices | Out-GridView -Title $Title

}
Else
{
    ##Nothing selected, exit
   exit
}
}
else {

##Automated, begin email

write-host "Automation started" -ForegroundColor Green
##Create an array to store device details
$userdevices = @()

foreach ($useratrisk in $usersatrisk) {
    ##Get User ID
$userid = ($useratrisk.Keys -Split (":"))[1]


##Get Intune Limit
##Get User Details
write-host "Getting User Details" -ForegroundColor Green
$user = Get-MgUser -UserId $userid
$username = $user.DisplayName
$userupn = $user.UserPrincipalName
##Get Users Groups
write-host "Getting User Groups" -ForegroundColor Green
$groupsuri = "https://graph.microsoft.com/v1.0/users/$userid/memberOf"
$usergroups = (Invoke-MgGraphRequest -Uri $groupsuri -Method GET -OutputType PSObject).value | Select-Object ID

##Get User Assignments
write-host "Getting User Assignments" -ForegroundColor Green
foreach ($policy in $userpolicies) {
    $policygroupid = $policy.GroupID
    if ($usergroups -match $policygroupid) {
        ##Group found, add to array
        $userassignmentlimits += $policy.limit
        ##Stop there, we don't need anything else
        break
    }
}

$lowestassignment = $userassignmentlimits | Select-Object -Last 1
write-host "Intune Limit is $lowestassignment" -ForegroundColor Green


##Get Registered Device
write-host "Getting Registered Devices for user $username" -ForegroundColor Green

$registereddeviceuri = "https://graph.microsoft.com/beta/users/$userid/registeredDevices"
$registereddevices = (Invoke-MgGraphRequest -Uri $registereddeviceuri -Method GET -OutputType PSObject).Value

foreach ($registereddevice in $registereddevices) {
    $deviceid = $registereddevice.id
    $displayname = $registereddevice.displayName
    $created = $registereddevice.registrationDateTime
    $lastseen = $registereddevice.approximateLastSignInDateTime
    $object = [pscustomobject]@{
        DeviceID    = $deviceid
        DisplayName = $displayname
        Created     = $created
        LastSeen    = $lastseen
        Type        = "Registered"
        User        = $userupn
    }
    $userdevices += $object
}


##Get Managed Devices
write-host "Getting Managed Devices for user $username" -ForegroundColor Green
$manageddevicesperuser = $devicesperuser | Where-Object UserName -eq "$userupn" | Select-Object DeviceID
foreach ($manageddevice in $manageddevicesperuser) {
    $deviceid = $manageddevice.DeviceID
    $uri = "https://graph.microsoft.com/beta/deviceManagement/manageddevices/$deviceid/"
    $manageddevicedetails = Invoke-MgGraphRequest -Uri $uri -Method GET -OutputType PSObject
    $displayname = $manageddevicedetails.deviceName
    $created = $manageddevicedetails.enrolledDateTime
    $lastseen = $manageddevicedetails.lastSyncDateTime
    $object = [pscustomobject]@{
        DeviceID    = $deviceid
        DisplayName = $displayname
        Created     = $created
        LastSeen    = $lastseen
        Type        = "Managed"
        User        = $userupn
    }
    $userdevices += $object
}

}

function ConvertTo-StringData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [HashTable[]]$HashTable
    )
    process {
        foreach ($item in $HashTable) {
            foreach ($entry in $item.GetEnumerator()) {
                "{0} <br> {1}" -f $entry.Key, $entry.Value
            }
        }
    }
}

$listofusers = $usersatrisk | ConvertTo-StringData
$listofdevices = $userdevices | out-string

#Send Mail
$URLsend = "https://graph.microsoft.com/v1.0/users/$MailSender/sendMail"
$BodyJsonsend = @"
                    {
                        "message": {
                          "subject": "Users at Enrollment Limit",
                          "body": {
                            "contentType": "HTML",
                            "content": "The following users are on or over their limit<br>
                            $listofusers <br>
                            Devices: <br>
                            $listofdevices
                            "
                          },
                          "toRecipients": [
                            {
                              "emailAddress": {
                                "address": "$EmailAddress"
                              }
                            }
                          ]
                        },
                        "saveToSentItems": "false"
                      }
"@


Invoke-MgGraphRequest -Method POST -Uri $URLsend -Body $BodyJsonsend


}