Src/Private/Get-AbrEntraIDUsers.ps1
|
function Get-AbrEntraIDUsers { <# .SYNOPSIS Documents the Microsoft Entra ID user inventory and account configuration. .DESCRIPTION Collects and reports on: - User summary statistics (totals, guests, admins, disabled) - User account details (UPN, type, account status, last sign-in) - Guest user inventory .NOTES Version: 0.1.20 Author: Pai Wei Sing #> [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory)] [string]$TenantId ) begin { Write-PScriboMessage -Message "Collecting Entra ID User information for tenant $TenantId." Show-AbrDebugExecutionTime -Start -TitleMessage 'Users' } process { #region Users # Section{} created unconditionally so catch{} always writes inside it (never to document root) Section -Style Heading2 'Users' { Paragraph "The following section documents the user accounts configured in tenant $TenantId." BlankLine if ($script:Charts['Users']) { Image -Text 'User Type Breakdown' -Base64 $script:Charts['Users'] -Percent 65 -Align Center Paragraph "Figure: User Breakdown -- $($script:ChartData['MemberUsers']) members, $($script:ChartData['GuestUsers']) guests, $($script:ChartData['DisabledUsers']) disabled" BlankLine } try { Write-Host " - Retrieving all users (this may take a moment)..." $AllUsers = Get-MgUser -All ` -Property Id,DisplayName,UserPrincipalName,UserType,AccountEnabled,CreatedDateTime,SignInActivity,AssignedLicenses,Mail,JobTitle,Department,OnPremisesSyncEnabled,LastPasswordChangeDateTime ` -ErrorAction Stop if ($AllUsers) { #region User Statistics Summary $TotalUsers = @($AllUsers).Count $MemberUsers = @($AllUsers | Where-Object { $_.UserType -eq 'Member' }).Count $GuestUsers = @($AllUsers | Where-Object { $_.UserType -eq 'Guest' }).Count $DisabledUsers = @($AllUsers | Where-Object { -not $_.AccountEnabled }).Count # Store for chart caption at section top $script:ChartData['MemberUsers'] = $MemberUsers $script:ChartData['GuestUsers'] = $GuestUsers $script:ChartData['DisabledUsers'] = $DisabledUsers # Generate chart (outside PScribo pipeline) try { if (Get-Command New-AbrUserBreakdownDonut -ErrorAction SilentlyContinue) { $script:Charts['Users'] = New-AbrUserBreakdownDonut -Members ([int]$MemberUsers) -Guests ([int]$GuestUsers) -Disabled ([int]$DisabledUsers) -TenantId $TenantId } } catch { Write-AbrDebugLog "User chart failed: $($_.Exception.Message)" 'WARN' 'CHART' } $SyncedUsers = @($AllUsers | Where-Object { $_.OnPremisesSyncEnabled -eq $true }).Count $LicensedUsers = @($AllUsers | Where-Object { $_.AssignedLicenses.Count -gt 0 }).Count $SummaryObj = [System.Collections.ArrayList]::new() $summaryInObj = [ordered] @{ 'Total Users' = $TotalUsers 'Member Users' = $MemberUsers 'Guest Users' = $GuestUsers 'Disabled Accounts' = $DisabledUsers 'Hybrid (On-Prem Synced)' = $SyncedUsers 'Licensed Users' = $LicensedUsers 'Cloud-Only Users' = ($TotalUsers - $SyncedUsers) } $SummaryObj.Add([pscustomobject]$summaryInObj) | Out-Null $null = (& {if ($HealthCheck.EntraID.Guests) { $null = ($SummaryObj | Where-Object { [int]$_.'Guest Users' -gt 0 } | Set-Style -Style Warning | Out-Null) }}) $SumTableParams = @{ Name = "User Summary - $TenantId"; List = $true; ColumnWidths = 50, 50 } if ($Report.ShowTableCaptions) { $SumTableParams['Caption'] = "- $($SumTableParams.Name)" } $SummaryObj | Table @SumTableParams #endregion # chart rendered at section top #endregion #region User Account Table (InfoLevel 2: per-user detail) if ($InfoLevel.Users -ge 2) { $UserObj = [System.Collections.ArrayList]::new() foreach ($User in ($AllUsers | Sort-Object DisplayName)) { try { $LastSignIn = if ($User.SignInActivity.LastSignInDateTime) { ($User.SignInActivity.LastSignInDateTime).ToString('yyyy-MM-dd HH:mm') } elseif ($User.SignInActivity.LastNonInteractiveSignInDateTime) { ($User.SignInActivity.LastNonInteractiveSignInDateTime).ToString('yyyy-MM-dd HH:mm') + ' (Non-Interactive)' } else { 'Never / Unknown' } $userInObj = [ordered] @{ 'Display Name' = $User.DisplayName 'UPN' = $User.UserPrincipalName 'User Type' = $User.UserType 'Account Enabled' = if ($User.AccountEnabled) { 'Yes' } else { 'No' } 'Licensed' = if ($User.AssignedLicenses.Count -gt 0) { 'Yes' } else { 'No' } 'Hybrid Synced' = if ($User.OnPremisesSyncEnabled) { 'Yes' } else { 'No' } 'Last Sign-In' = $LastSignIn 'Job Title' = if ($User.JobTitle) { $User.JobTitle } else { '--' } 'Department' = if ($User.Department) { $User.Department } else { '--' } 'Created' = if ($User.CreatedDateTime) { ($User.CreatedDateTime).ToString('yyyy-MM-dd') } else { '--' } } $UserObj.Add([pscustomobject](ConvertTo-HashToYN $userInObj)) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "User '$($User.DisplayName)': $($_.Exception.Message)" } } $null = (& { if ($HealthCheck.EntraID.MFA) { $null = ($UserObj | Where-Object { $_.'Account Enabled' -eq 'Yes' -and $_.'Last Sign-In' -eq 'Never / Unknown' } | Set-Style -Style Warning | Out-Null) $null = ($UserObj | Where-Object { $_.'Account Enabled' -eq 'No' } | Set-Style -Style Critical | Out-Null) $StaleUsers = $UserObj | Where-Object { $_.'Account Enabled' -eq 'Yes' -and $_.'User Type' -eq 'Member' -and $_.'Last Sign-In' -notin @('Never / Unknown', '--') -and $_.'Last Sign-In' -notlike '*(Non-Interactive)*' } | Where-Object { try { ((Get-Date) - [datetime]::ParseExact($_.'Last Sign-In'.Trim(), 'yyyy-MM-dd HH:mm', $null)).Days -gt 90 } catch { $false } } foreach ($StaleUser in $StaleUsers) { $null = ($StaleUser | Set-Style -Style Warning | Out-Null) } } }) $UserTableParams = @{ Name = "User Accounts - $TenantId"; List = $false; ColumnWidths = 15, 20, 8, 9, 8, 8, 14, 8, 6, 4 } if ($Report.ShowTableCaptions) { $UserTableParams['Caption'] = "- $($UserTableParams.Name)" } $UserObj | Table @UserTableParams $null = ($script:ExcelSheets['All Users'] = $UserObj) } # end InfoLevel.Users -ge 2 #endregion #region Guest Users Detail (Level 2) if ($InfoLevel.Users -ge 2) { $Guests = $AllUsers | Where-Object { $_.UserType -eq 'Guest' } if ($Guests) { Section -Style Heading3 'Guest Users' { Paragraph "The following guest accounts have been provisioned in tenant $TenantId. Review regularly to ensure access is still required." BlankLine $GuestObj = [System.Collections.ArrayList]::new() foreach ($Guest in ($Guests | Sort-Object DisplayName)) { $LastGuestSignIn = if ($Guest.SignInActivity.LastSignInDateTime) { ($Guest.SignInActivity.LastSignInDateTime).ToString('yyyy-MM-dd') } else { 'Never / Unknown' } $guestInObj = [ordered] @{ 'Display Name' = $Guest.DisplayName 'Email / UPN' = $Guest.Mail 'Account Enabled' = if ($Guest.AccountEnabled) { 'Yes' } else { 'No' } 'Created' = if ($Guest.CreatedDateTime) { ($Guest.CreatedDateTime).ToString('yyyy-MM-dd') } else { '--' } 'Last Sign-In' = $LastGuestSignIn } $GuestObj.Add([pscustomobject](ConvertTo-HashToYN $guestInObj)) | Out-Null } $null = (& { if ($HealthCheck.EntraID.Guests) { $null = ($GuestObj | Where-Object { $_.'Account Enabled' -eq 'Yes' -and $_.'Last Sign-In' -eq 'Never / Unknown' } | Set-Style -Style Critical | Out-Null) $StaleGuests = $GuestObj | Where-Object { $_.'Account Enabled' -eq 'Yes' -and $_.'Last Sign-In' -ne 'Never / Unknown' -and $_.'Last Sign-In' -ne '--' } | Where-Object { try { ((Get-Date) - [datetime]::ParseExact($_.'Last Sign-In', 'yyyy-MM-dd', $null)).Days -gt 90 } catch { $false } } foreach ($StaleGuest in $StaleGuests) { $null = ($StaleGuest | Set-Style -Style Warning | Out-Null) } } }) $GuestTableParams = @{ Name = "Guest Users - $TenantId"; List = $false; ColumnWidths = 22, 28, 12, 12, 26 } if ($Report.ShowTableCaptions) { $GuestTableParams['Caption'] = "- $($GuestTableParams.Name)" } $GuestObj | Table @GuestTableParams $null = ($script:ExcelSheets['Guest Users'] = $GuestObj) } } } #endregion #region ACSC E8 Users Assessment BlankLine Paragraph "ACSC Essential Eight Maturity Level Assessment -- User Account Management:" BlankLine try { $DisabledCount = @($AllUsers | Where-Object { -not $_.AccountEnabled }).Count $GuestCount = @($AllUsers | Where-Object { $_.UserType -eq 'Guest' }).Count $NeverSignedIn = @($AllUsers | Where-Object { $_.AccountEnabled -and -not $_.SignInActivity.LastSignInDateTime -and -not $_.SignInActivity.LastNonInteractiveSignInDateTime }).Count $StaleCount = @($AllUsers | Where-Object { $_.AccountEnabled -and $_.UserType -eq 'Member' -and $_.SignInActivity.LastSignInDateTime -and ((Get-Date) - $_.SignInActivity.LastSignInDateTime).Days -gt 90 }).Count $LicensedGuests = @($AllUsers | Where-Object { $_.UserType -eq 'Guest' -and $_.AssignedLicenses.Count -gt 0 }).Count #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json) $_ComplianceVars = @{ 'StaleCount' = $StaleCount 'Stale45Count' = $Stale45Count 'DisabledCount' = $DisabledCount 'GuestCount' = $GuestCount 'LicensedGuests' = $LicensedGuests 'GuestAdminCount' = $GuestAdminCount 'NeverSignedIn' = $NeverSignedIn } $E8UserChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrE8Checks -Section 'Users') ` -Framework E8 ` -CallerVariables $_ComplianceVars New-AbrE8AssessmentTable -Checks $E8UserChecks -Name 'User Management' -TenantId $TenantId # Consolidated into ACSC E8 Assessment sheet if ($E8UserChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8UserChecks | Select-Object @{N='Section';E={'Users'}}, ML, Control, Status, Detail ))) } #endregion if ($script:IncludeCISBaseline) { BlankLine Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- User Account Management:" BlankLine #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json) $CISUserChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrCISChecks -Section 'Users') ` -Framework CIS ` -CallerVariables $_ComplianceVars New-AbrCISAssessmentTable -Checks $CISUserChecks -Name 'User Management' -TenantId $TenantId # Consolidated into CIS Assessment sheet if ($CISUserChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISUserChecks | Select-Object @{N='Section';E={'Users'}}, CISControl, Level, Status, Detail ))) } #endregion } } catch { Write-AbrSectionError -Section 'E8 Users Assessment' -Message "$($_.Exception.Message)" } #endregion } else { Paragraph "No users were returned for tenant $TenantId." } } catch { Write-AbrSectionError -Section 'Users section' -Message "$($_.Exception.Message)" } } # end Section Users #endregion } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Users' } } |