<# .SYNOPSIS Retrieves user login information from an Entra ID (Azure AD) group with recursive nested group support. .DESCRIPTION This script connects to Microsoft Graph and retrieves all users from a specified Entra ID group, including users in nested groups. For each user, it displays their display name, user principal name, and last sign-in date/time information. Features: • Recursive group membership enumeration (handles nested groups) • Circular reference protection to prevent infinite loops • Last sign-in activity tracking for security and compliance • Comprehensive error handling and diagnostic output • Automatic Microsoft Graph authentication with required scopes .PARAMETER GroupName The display name of the Entra ID group to analyze. This parameter is mandatory. The script will search for an exact match of the group display name. .EXAMPLE .\UserLastLoginList.ps1 -GroupName "# Developer ADM" Retrieves all users from the "# Developer ADM" group, including any nested group memberships. .EXAMPLE .\UserLastLoginList.ps1 -GroupName "IT Security Team" -Debug Retrieves users with detailed debug output showing group processing and user enumeration steps. .INPUTS None. You cannot pipe objects to this script. .OUTPUTS System.Management.Automation.PSCustomObject Returns a formatted table with the following properties for each user: - UserPrincipalName: The user's UPN (email-like identifier) - DisplayName: The user's display name - LastSignInDateTime: The user's last sign-in date/time, or "No sign-in data available" if unavailable .NOTES Requires PowerShell 5.1 or later Requires Microsoft.Graph PowerShell module Required Microsoft Graph Permissions: - User.Read.All: To read user profiles and sign-in activity - Group.Read.All: To read group memberships and nested groups The script will automatically prompt for authentication if not already connected to Microsoft Graph. Sign-in activity data may not be available for all users depending on license and retention policies. .LINK https://docs.microsoft.com/en-us/graph/api/user-get https://docs.microsoft.com/en-us/graph/api/group-list-members .COMPONENT Microsoft Graph PowerShell SDK .ROLE Identity and Access Management, Security Reporting .FUNCTIONALITY Entra ID group analysis, user activity reporting, nested group enumeration #> param( [Parameter( Mandatory = $true, HelpMessage = "Enter the display name of the Entra ID group to analyze" )] [ValidateNotNullOrEmpty()] [string]$GroupName ) <# .SYNOPSIS Retrieves an Entra ID group by its display name. .DESCRIPTION This function searches for an Entra ID group using its display name and returns the group's unique identifier. Uses Microsoft Graph API to perform an exact match search on the displayName property. .PARAMETER EntraGroupName The exact display name of the Entra ID group to search for. .OUTPUTS System.String Returns the group's unique identifier (GUID) if found, or $null if not found or error occurs. .EXAMPLE $groupId = Get-EntraGroupByName -EntraGroupName "Developers" Retrieves the group ID for the "Developers" group. #> function Get-EntraGroupByName { param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$EntraGroupName ) try { Write-Debug "Retrieving group ID for group: $EntraGroupName" # Get group by name $group = Get-MgGroup -Filter "displayName eq '$EntraGroupName'" if ($group) { return $group.Id } else { Write-Warning "Group '$EntraGroupName' not found" return $null } } catch { Write-Error "Error retrieving group: $($_.Exception.Message)" return $null } } # Initialize Microsoft Graph connection Write-Host "🔐 Initializing Microsoft Graph connection..." -ForegroundColor Cyan Write-Debug "Connecting to Microsoft Graph..." # Connect to Microsoft Graph if not already connected if (-not (Get-MgContext)) { Write-Host "🔑 Authenticating to Microsoft Graph with required scopes..." -ForegroundColor Yellow Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All" Write-Host "✅ Successfully connected to Microsoft Graph" -ForegroundColor Green } else { Write-Host "✅ Already connected to Microsoft Graph" -ForegroundColor Green } <# .SYNOPSIS Recursively retrieves all users from an Entra ID group, including nested groups. .DESCRIPTION This function performs a recursive search through an Entra ID group structure to find all users, including those in nested groups. It includes circular reference protection to prevent infinite loops when groups contain circular memberships. .PARAMETER GroupId The unique identifier (GUID) of the Entra ID group to process. .PARAMETER ProcessedGroups Internal hashtable used to track processed groups and prevent circular reference loops. This parameter is used internally for recursion and should not be specified by callers. .OUTPUTS System.Management.Automation.PSCustomObject[] Returns an array of custom objects with the following properties: - UserPrincipalName: The user's UPN - DisplayName: The user's display name - LastSignInDateTime: The user's last sign-in date/time or status message .EXAMPLE $users = Get-EntraGroupUsersRecursive -GroupId "12345678-1234-1234-1234-123456789012" Recursively retrieves all users from the specified group and its nested groups. .NOTES This function is designed to handle complex group hierarchies and prevents infinite recursion through circular group memberships. It processes both user and group objects within the membership. #> function Get-EntraGroupUsersRecursive { param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$GroupId, [hashtable]$ProcessedGroups = @{} ) try { # Prevent infinite recursion by tracking processed groups if ($ProcessedGroups.ContainsKey($GroupId)) { Write-Debug "Group $GroupId already processed, skipping to prevent recursion" return @() } $ProcessedGroups[$GroupId] = $true Write-Debug "Retrieving users recursively from group with ID: $GroupId" # Get group members $groupMembers = Get-MgGroupMember -GroupId $GroupId -All $users = @() foreach ($member in $groupMembers) { if ($member.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.user") { Write-Debug "Processing user: $($member.DisplayName) ($($member.Id))" $user = Get-MgUser -UserId $member.Id -Property "signInActivity,displayName,userPrincipalName" $users += [PSCustomObject]@{ UserPrincipalName = $user.UserPrincipalName DisplayName = $user.DisplayName LastSignInDateTime = if ($user.SignInActivity) { $user.SignInActivity.LastSignInDateTime } else { "No sign-in data available" } } } elseif ($member.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.group") { Write-Debug "Processing nested group: $($member.DisplayName) ($($member.Id))" $nestedUsers = Get-EntraGroupUsersRecursive -GroupId $member.Id -ProcessedGroups $ProcessedGroups $users += $nestedUsers } } return $users } catch { Write-Error "Error retrieving group users recursively: $($_.Exception.Message)" return @() } } # Main execution logic Write-Host "" Write-Host "🔍 Searching for Entra ID group: '$GroupName'" -ForegroundColor Cyan $groupId = Get-EntraGroupByName -EntraGroupName $GroupName if ($groupId) { Write-Host "✅ Group found! Group ID: $groupId" -ForegroundColor Green Write-Host "" # Get users recursively from the group Write-Host "👥 Retrieving users recursively from group (including nested groups)..." -ForegroundColor Cyan $recursiveUsers = Get-EntraGroupUsersRecursive -GroupId $groupId if ($recursiveUsers) { Write-Host "" Write-Host "📊 Analysis Results:" -ForegroundColor Green Write-Host "===================" -ForegroundColor Green Write-Host "Found $($recursiveUsers.Count) users (including nested groups)" -ForegroundColor White Write-Host "" # Display results sorted by display name $recursiveUsers | Sort-Object DisplayName | Format-Table -AutoSize # Additional statistics $usersWithSignInData = ($recursiveUsers | Where-Object { $_.LastSignInDateTime -ne "No sign-in data available" }).Count $usersWithoutSignInData = $recursiveUsers.Count - $usersWithSignInData Write-Host "" Write-Host "📈 Sign-in Data Summary:" -ForegroundColor Yellow Write-Host "========================" -ForegroundColor Yellow Write-Host "Users with sign-in data: $usersWithSignInData" -ForegroundColor White Write-Host "Users without sign-in data: $usersWithoutSignInData" -ForegroundColor White } else { Write-Warning "❌ No users found in the group hierarchy or an error occurred." } } else { Write-Warning "❌ Group '$GroupName' not found. Please verify the group name and try again." }