mirror of
https://dev.azure.com/effectory/Survey%20Software/_git/Cloud%20Engineering
synced 2026-02-27 10:45:02 +01:00
251 lines
9.4 KiB
PowerShell
251 lines
9.4 KiB
PowerShell
<#
|
|
.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."
|
|
}
|