{"id":8893,"date":"2025-05-20T02:40:32","date_gmt":"2025-05-20T02:40:32","guid":{"rendered":"https:\/\/pariswells.com\/blog\/?p=8893"},"modified":"2025-12-09T22:20:32","modified_gmt":"2025-12-09T22:20:32","slug":"list-inactive-guest-users-in-azure-ad","status":"publish","type":"post","link":"https:\/\/pariswells.com\/blog\/research\/list-inactive-guest-users-in-azure-ad","title":{"rendered":"List inactive Guest\\Users\\Members Users and Devices in Azure AD \\ Entra"},"content":{"rendered":"\n<pre class=\"wp-block-code\"><code lang=\"powershell\" class=\"language-powershell\"># This script requires PowerShell 7 or later for the following reasons:\n# 1. The Microsoft Graph PowerShell SDK (Microsoft.Graph module) is designed for PowerShell 7+ and may not work reliably in Windows PowerShell 5.1.\n# 2. The SignInActivity property is not returned or accessible in PowerShell 5.1 due to .NET and serialization limitations.\n# 3. Modern authentication methods used by Connect-MgGraph are only fully supported in PowerShell 7+.\n# 4. Paging and the -All switch for Get-MgUser are more reliable in PowerShell 7+.\n# 5. Microsoft is focusing new features and bug fixes for the Graph SDK on PowerShell 7+ only.\n# Attempting to run this script in Windows PowerShell 5.1 or earlier will result in errors or missing data.\n\n# Check for PowerShell 7+\nif ($PSVersionTable.PSVersion.Major -lt 7) {\n    Write-Host \"ERROR: This script requires PowerShell 7 or later. Please run it in PowerShell 7+ (pwsh.exe).\" -ForegroundColor Red\n    exit 1\n}\n\n# Check if Microsoft.Graph module is installed\n$module = Get-Module -Name Microsoft.Graph -ListAvailable\nif (-not $module) {\n    Write-Host \"Microsoft.Graph module not found. Installing...\"\n    try {\n        Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force -ErrorAction Stop\n        Write-Host \"Microsoft.Graph module installed successfully.\"\n    }\n    catch {\n        Write-Host \"ERROR: Failed to install Microsoft.Graph module. Error: $_\" -ForegroundColor Red\n        exit 1\n    }\n}\nelse {\n    Write-Host \"Microsoft.Graph module is already installed.\"\n}\n\n# Import the module if not already loaded\nif (-not (Get-Command -Name Connect-MgGraph -ErrorAction SilentlyContinue)) {\n    Import-Module Microsoft.Graph -ErrorAction Stop\n}\n\n# Connect to Microsoft Graph with required scopes\nWrite-Host \"Connecting to Microsoft Graph...\"\nConnect-MgGraph -Scopes \"User.Read.All\", \"AuditLog.Read.All\", \"Device.Read.All\", \"Organization.Read.All\"\n\n# Define log file\n$logFile = \"InactiveUsersAndOldDevices_Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').log\"\n\n# Dynamically retrieve the organization's primary domain\nWrite-Host \"Retrieving organization details to determine primary domain...\"\n$org = Get-MgOrganization\n$verifiedDomains = $org.VerifiedDomains\n$initialDomain = $verifiedDomains | Where-Object { $_.IsInitial -eq $true } | Select-Object -ExpandProperty Name -First 1\n$customDomains = $verifiedDomains | Where-Object { $_.IsInitial -eq $false } | Select-Object -ExpandProperty Name\n\nif ($customDomains.Count -gt 0) {\n    $orgDomain = $customDomains[0]  # Select the first custom domain as primary; adjust logic if needed for multiple domains\n} else {\n    $orgDomain = $initialDomain  # Fallback to initial domain if no custom domains exist\n}\n\nWrite-Host \"Primary domain dynamically set to: $orgDomain\"\n\n# Threshold: 6 months ago\n$inactiveThreshold = (Get-Date).AddMonths(-6)\n\n# Initialize log\nAdd-Content -Path $logFile -Value \"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Starting Inactive Users + Old Enabled Devices Report.\"\nAdd-Content -Path $logFile -Value \"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Primary domain: $orgDomain\"\n\n#####################################################################\n# 1. INACTIVE ENABLED USERS (6+ months no sign-in)\n#####################################################################\nWrite-Host \"Retrieving inactive enabled users...\" -ForegroundColor Cyan\n\n$allUsers = Get-MgUser -All -Property Id, DisplayName, SignInActivity, AccountEnabled, UserType, UserPrincipalName -ConsistencyLevel eventual -CountVariable userCount |\n    Where-Object {\n        $_.AccountEnabled -eq $true -and\n        $_.SignInActivity.LastSignInDateTime -and\n        ([datetime]$_.SignInActivity.LastSignInDateTime) -lt $inactiveThreshold\n    }\n\n$guestUsers = $allUsers | Where-Object UserType -eq 'Guest'\n$memberUsers = $allUsers | Where-Object UserType -eq 'Member'\n\n# Log Guest Users\nAdd-Content -Path $logFile -Value \"`n$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Inactive Guest Users (6+ months no sign-in):\"\nif ($guestUsers.Count -eq 0) {\n    $msg = \"No inactive Guest users found.\"\n    Write-Host $msg\n    Add-Content -Path $logFile -Value $msg\n} else {\n    foreach ($user in $guestUsers) {\n        $msg = \"Guest: $($user.DisplayName) | UPN: $($user.UserPrincipalName) | Last sign-in: $($user.SignInActivity.LastSignInDateTime)\"\n        Write-Host $msg -ForegroundColor Yellow\n        Add-Content -Path $logFile -Value $msg\n    }\n}\n\n# Log Member Users\nAdd-Content -Path $logFile -Value \"`n$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Inactive Member Users (6+ months no sign-in):\"\nif ($memberUsers.Count -eq 0) {\n    $msg = \"No inactive Member users found.\"\n    Write-Host $msg\n    Add-Content -Path $logFile -Value $msg\n} else {\n    foreach ($user in $memberUsers) {\n        $msg = \"Member: $($user.DisplayName) | UPN: $($user.UserPrincipalName) | Last sign-in: $($user.SignInActivity.LastSignInDateTime)\"\n        Write-Host $msg -ForegroundColor Yellow\n        Add-Content -Path $logFile -Value $msg\n    }\n}\n\n\n#####################################################################\n# 2. OLD ENABLED DEVICES (6+ months no sign-in)\n#####################################################################\nWrite-Host \"Retrieving old enabled devices (no sign-in for 6+ months)...\" -ForegroundColor Cyan\n\n# Get all Entra ID devices\n$devices = Get-MgDevice -All -Property Id, DisplayName, ApproximateLastSignInDateTime, AccountEnabled, OperatingSystem, TrustType, ProfileType, DeviceOwnership\n\n# Filter for enabled stale devices\n$oldDevices = @()\nforeach ($device in $devices) {\n    if ($device.AccountEnabled -eq $true -and\n        $device.ApproximateLastSignInDateTime -ne $null -and\n        ([datetime]$device.ApproximateLastSignInDateTime) -lt $inactiveThreshold) {\n\n        # Get owner if available\n        $ownerUPN = \"Unknown\"\n        $ownerDisplayName = \"Unknown\"\n        try {\n            $ownerRef = Get-MgDeviceRegisteredOwner -DeviceId $device.Id -Top 1\n            if ($ownerRef) {\n                $ownerId = $ownerRef.Id\n                $owner = Get-MgUser -UserId $ownerId -Property DisplayName, UserPrincipalName\n                $ownerDisplayName = $owner.DisplayName\n                $ownerUPN = $owner.UserPrincipalName\n            }\n        } catch {\n            Write-Warning \"Failed to get owner for device $($device.DisplayName): $_\"\n        }\n\n        $oldDevices += [PSCustomObject]@{\n            DeviceName = $device.DisplayName\n            DeviceId = $device.Id\n            OperatingSystem = $device.OperatingSystem\n            TrustType = $device.TrustType\n            ProfileType = $device.ProfileType\n            DeviceOwnership = $device.DeviceOwnership\n            LastSignInDateTime = $device.ApproximateLastSignInDateTime\n            OwnerDisplayName = $ownerDisplayName\n            OwnerUPN = $ownerUPN\n        }\n    }\n}\n\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-8893","post","type-post","status-publish","format-standard","hentry","category-research"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/posts\/8893","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/comments?post=8893"}],"version-history":[{"count":14,"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/posts\/8893\/revisions"}],"predecessor-version":[{"id":9418,"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/posts\/8893\/revisions\/9418"}],"wp:attachment":[{"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/media?parent=8893"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/categories?post=8893"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pariswells.com\/blog\/wp-json\/wp\/v2\/tags?post=8893"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}