Src/Private/Get-AbrEntraIDTenantSettings.ps1

function Get-AbrEntraIDTenantSettings {
    [CmdletBinding()]
    param ([Parameter(Position=0,Mandatory)][string]$TenantId)

    begin {
        Write-PScriboMessage "Collecting Tenant Settings for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Tenant Settings'
    }

    process {
        try {
                Write-Host " - Retrieving authorization policy and tenant settings..."

                # Authorization Policy (controls who can do what in the tenant)
                $AuthzPolicy = Invoke-MgGraphRequest -Method GET `
                    -Uri 'https://graph.microsoft.com/v1.0/policies/authorizationPolicy' `
                    -ErrorAction Stop

                # ── New gap detections ───────────────────────────────────────────────
                # Non-admin tenant creation
                $CanCreateTenants      = ($AuthzPolicy.defaultUserRolePermissions.allowedToCreateTenants -eq $true)
                $CanCreateTenantsState = if ($CanCreateTenants) { 'Allowed [WARN]' } else { 'Restricted [OK]' }

                # Guest access level
                $GuestRoleId = $AuthzPolicy.guestUserRoleId
                $GuestAccessLevel = switch ($GuestRoleId) {
                    '2af84b1e-32c8-42b7-82bc-daa82404023b' { 'mostRestrictive' }
                    '10dae51f-b6af-4016-8d66-8c2a99b929b3' { 'limited' }
                    'a0b1b346-4d3e-4e8b-98f8-753987be4970' { 'sameAsMembers' }
                    default { 'unknown' }
                }
                $GuestAccessLevelDesc = switch ($GuestRoleId) {
                    '2af84b1e-32c8-42b7-82bc-daa82404023b' { 'Most restricted (guests see only their own objects) [OK]' }
                    '10dae51f-b6af-4016-8d66-8c2a99b929b3' { 'Limited access (default) [WARN] -- consider most restricted' }
                    'a0b1b346-4d3e-4e8b-98f8-753987be4970' { 'Same access as members [FAIL] -- overly permissive' }
                    default { $GuestRoleId }
                }

                # User consent to apps
                $ConsentPolicies       = $AuthzPolicy.permissionGrantPoliciesAssigned
                $UserConsentDisabled   = (-not $ConsentPolicies -or $ConsentPolicies.Count -eq 0 -or $ConsentPolicies -contains 'ManagePermissionGrantsForSelf.microsoft-user-default-low')
                $UserConsentVerifiedOnly = ($ConsentPolicies -contains 'ManagePermissionGrantsForSelf.microsoft-verified-apps-only')
                $UserConsentPolicy     = if ($UserConsentDisabled) { 'Disabled or admin-only [OK]' }
                                         elseif ($UserConsentVerifiedOnly) { 'Allowed for verified publishers only [WARN]' }
                                         else { 'Allowed for all apps [FAIL]' }

                # Admin consent workflow
                $AdminConsentWorkflowEnabled = $false
                $AdminConsentWorkflowState   = 'Not configured [WARN]'
                try {
                    $ConsentWorkflow = Invoke-MgGraphRequest -Method GET `
                        -Uri 'https://graph.microsoft.com/v1.0/policies/adminConsentRequestPolicy' `
                        -ErrorAction SilentlyContinue
                    $AdminConsentWorkflowEnabled = ($ConsentWorkflow.isEnabled -eq $true)
                    $AdminConsentWorkflowState   = if ($AdminConsentWorkflowEnabled) { 'Enabled [OK]' } else { 'Disabled [WARN]' }
                } catch { $AdminConsentWorkflowState = 'Unable to retrieve' }

                # Per-user MFA count (from MFA section data if available, else approximate)
                $PerUserMfaCount = if ($script:LegacyMfaEnabledCount) { $script:LegacyMfaEnabledCount } else { 0 }

                # Custom banned password (from Password Protection section)
                $CustomBannedPasswordEnabled = $false
                $CustomBannedPasswordState   = '[INFO] Check Entra ID Password Protection blade'
                try {
                    $PwdProt = Invoke-MgGraphRequest -Method GET `
                        -Uri 'https://graph.microsoft.com/beta/settings' -ErrorAction SilentlyContinue
                    if ($PwdProt -and $PwdProt.value) {
                        $PwdSetting = $PwdProt.value | Where-Object { $_.displayName -like '*Password*' } | Select-Object -First 1
                        if ($PwdSetting) {
                            $bannedVal = ($PwdSetting.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheck' }).value
                            $CustomBannedPasswordEnabled = ($bannedVal -eq 'True')
                            $CustomBannedPasswordState   = if ($CustomBannedPasswordEnabled) { 'Enabled [OK]' } else { 'Not enabled [WARN]' }
                        }
                    }
                } catch {}
                # LinkedIn account connections
                $LinkedInEnabled = $false
                try {
                    $LinkedInEnabled = ($AuthzPolicy.linkedInAccountConnectionsEnabled -eq $true)
                } catch { $LinkedInEnabled = $false }
                $LinkedInState = if ($LinkedInEnabled) { 'Enabled [WARN]' } else { 'Disabled [OK]' }

                # Restrict non-admin access to Entra admin portal
                $RestrictAdminPortal      = ($AuthzPolicy.blockMsolPowerShell -eq $true -or
                                             $AuthzPolicy.defaultUserRolePermissions.allowedToCreateApps -eq $false)
                try {
                    $PortalResp = Invoke-MgGraphRequest -Method GET `
                        -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy' `
                        -ErrorAction SilentlyContinue
                    $RestrictAdminPortal = ($PortalResp.restrictNonAdminUsers -eq $true)
                } catch {}
                $RestrictAdminPortalState = if ($RestrictAdminPortal) { 'Restricted to admins [OK]' } else { 'Not restricted [WARN]' }

                # Hybrid tenant + Password Hash Sync detection
                $IsHybridTenant          = $false
                $PasswordHashSyncEnabled  = $false
                $PasswordHashSyncDetail   = 'Cloud-only tenant -- Password Hash Sync not applicable. [INFO]'
                try {
                    $OrgOnPrem = Invoke-MgGraphRequest -Method GET `
                        -Uri 'https://graph.microsoft.com/v1.0/organization?$select=id,onPremisesSyncEnabled,onPremisesLastSyncDateTime' `
                        -ErrorAction SilentlyContinue
                    $OrgData = if ($OrgOnPrem.value) { $OrgOnPrem.value[0] } else { $OrgOnPrem }
                    $IsHybridTenant = ($OrgData.onPremisesSyncEnabled -eq $true)
                    if ($IsHybridTenant) {
                        # Check if PHS is the sync method (onPremisesProvisioningErrors empty + no federation)
                        $Domains2 = Invoke-MgGraphRequest -Method GET `
                            -Uri 'https://graph.microsoft.com/v1.0/domains?$select=id,authenticationType' `
                            -ErrorAction SilentlyContinue
                        $FedDomains = if ($Domains2.value) { @($Domains2.value | Where-Object { $_.authenticationType -eq 'Federated' }).Count } else { 0 }
                        $PasswordHashSyncEnabled = ($FedDomains -eq 0)  # no federated domains = managed auth (PHS/PTA)
                        $PasswordHashSyncDetail  = if ($PasswordHashSyncEnabled) { "Hybrid tenant with managed authentication ($($FedDomains) federated domains). PHS likely enabled. [OK]" }
                                                   else { "Hybrid tenant with $($FedDomains) federated domain(s). Verify PHS is enabled as fallback in Azure AD Connect. [WARN]" }
                    }
                } catch {}

                # Dynamic group for guest users
                $GuestDynamicGroupExists = $false
                $GuestDynamicGroupDetail = 'No dynamic group found with membership rule targeting guest users. [WARN]'
                try {
                    $DynGroups = Invoke-MgGraphRequest -Method GET `
                        -Uri "https://graph.microsoft.com/v1.0/groups?`$filter=groupTypes/any(c:c eq 'DynamicMembership')&`$select=id,displayName,membershipRule" `
                        -ErrorAction SilentlyContinue
                    if ($DynGroups -and $DynGroups.value) {
                        $GuestGroup = $DynGroups.value | Where-Object { $_.membershipRule -like '*userType*guest*' -or $_.membershipRule -like '*userType -eq "Guest"*' } | Select-Object -First 1
                        if ($GuestGroup) {
                            $GuestDynamicGroupExists = $true
                            $GuestDynamicGroupDetail = "Dynamic group '$($GuestGroup.displayName)' exists for guest users. [OK]"
                        }
                    }
                } catch {}

                # ── End new detections ───────────────────────────────────────────────

                #region User Permission Settings
                Section -Style Heading2 'User Permission Settings' {
                    Paragraph "The following settings control what standard users can do in tenant $TenantId. Overly permissive settings allow users to register apps, invite guests, and create groups without admin oversight."
                    BlankLine

                    $UserPermObj = [System.Collections.ArrayList]::new()
                    $upInObj = [ordered] @{
                        'Users Can Register Applications'         = if ($AuthzPolicy.defaultUserRolePermissions.allowedToCreateApps -eq $true) { '[WARN] Yes -- users can register app registrations without admin approval' } else { '[OK] No -- app registration restricted to admins' }
                        'Users Can Create Security Groups'        = if ($AuthzPolicy.defaultUserRolePermissions.allowedToCreateSecurityGroups -eq $true) { '[INFO] Yes' } else { '[OK] No' }
                        'Users Can Create M365 Groups'            = if ($AuthzPolicy.defaultUserRolePermissions.allowedToCreateTenants -eq $true) { '[INFO] Yes' } else { '[OK] No' }
                        'Guest Invite Permissions'                = switch ($AuthzPolicy.allowInvitesFrom) {
                            'adminsAndGuestInviters'  { '[OK] Admins and Guest Inviters role only' }
                            'adminsGuestInvitersAndAllMembers' { '[WARN] All members can invite guests' }
                            'everyone'                { '[FAIL] Everyone including guests can invite' }
                            'none'                    { '[OK] Nobody (disabled)' }
                            default                   { $AuthzPolicy.allowInvitesFrom }
                        }
                        'User Consent to Apps (Own Data)'         = if ($AuthzPolicy.defaultUserRolePermissions.allowedToCreateApps) { '[INFO] Permitted' } else { '[OK] Restricted' }
                        'Default User Role'                       = if ($AuthzPolicy.defaultUserRolePermissions.allowedToReadBitlockerKeysForOwnedDevice) { 'Standard (can read own BitLocker keys)' } else { 'Standard' }
                    }
                    $UserPermObj.Add([pscustomobject]$upInObj) | Out-Null
                    $null = (& {
                        if ($HealthCheck.EntraID.Applications) {
                            $null = ($UserPermObj | Where-Object { $_.'Users Can Register Applications' -like '*WARN*' } | Set-Style -Style Warning | Out-Null)
                            $null = ($UserPermObj | Where-Object { $_.'Guest Invite Permissions'        -like '*FAIL*' } | Set-Style -Style Critical | Out-Null)
                            $null = ($UserPermObj | Where-Object { $_.'Guest Invite Permissions'        -like '*WARN*' } | Set-Style -Style Warning  | Out-Null)
                        }
                    })
                    $UserPermTableParams = @{ Name = "User Permission Settings - $TenantId"; List = $true; ColumnWidths = 42, 58 }
                    if ($Report.ShowTableCaptions) { $UserPermTableParams['Caption'] = "- $($UserPermTableParams.Name)" }
                    $UserPermObj | Table @UserPermTableParams
                    $null = ($script:ExcelSheets['User Permission Settings'] = $UserPermObj)
                }
                #endregion

                #region External Collaboration / B2B Settings
                Write-Host " - Retrieving external collaboration settings..."
                Section -Style Heading2 'External Collaboration (B2B) Settings' {
                    Paragraph "The following documents the external identity and guest collaboration settings for tenant $TenantId."
                    BlankLine

                    try {
                        # Note: externalIdentitiesPolicy returns BadRequest on some tenants/licences.
                        # Use -ErrorAction Stop so the catch block can show a graceful placeholder.
                        $ExtCollab = Invoke-MgGraphRequest -Method GET `
                            -Uri 'https://graph.microsoft.com/v1.0/policies/externalIdentitiesPolicy' `
                            -ErrorAction Stop

                        $B2BObj = [System.Collections.ArrayList]::new()
                        $b2bInObj = [ordered] @{
                            'Allow External Users to Leave Tenant'    = if ($ExtCollab.allowExternalIdentitiesToLeave -eq $true) { 'Yes' } else { 'No' }
                            'Allow Deleted Users to Be Re-invited'    = if ($ExtCollab.allowDeletedIdentitiesDataRemoval -eq $true) { 'Yes' } else { 'No' }
                            'Guest Invite Restriction'                = switch ($AuthzPolicy.allowInvitesFrom) {
                                'adminsAndGuestInviters'            { 'Admins + Guest Inviters role only [OK]' }
                                'adminsGuestInvitersAndAllMembers'  { 'All members can invite [WARN]' }
                                'everyone'                          { 'Everyone including guests [FAIL]' }
                                'none'                              { 'Nobody - guest invites disabled [OK]' }
                                default                             { $AuthzPolicy.allowInvitesFrom }
                            }
                            'Guest User Access Level'                 = switch ($AuthzPolicy.guestUserRoleId) {
                                'a0b1b346-4d3e-4e8b-98f8-753987be4970' { 'Same access as members [WARN - overly permissive]' }
                                '10dae51f-b6af-4016-8d66-8c2a99b929b3' { 'Limited access (default) [OK]' }
                                '2af84b1e-32c8-42b7-82bc-daa82404023b' { 'Most restricted [OK]' }
                                default                                { $AuthzPolicy.guestUserRoleId }
                            }
                        }
                        $B2BObj.Add([pscustomobject]$b2bInObj) | Out-Null
                        $null = (& {
                            if ($HealthCheck.EntraID.Guests) {
                                $null = ($B2BObj | Where-Object { $_.'Guest Invite Restriction' -like '*FAIL*' }  | Set-Style -Style Critical | Out-Null)
                                $null = ($B2BObj | Where-Object { $_.'Guest Invite Restriction' -like '*WARN*' }  | Set-Style -Style Warning  | Out-Null)
                                $null = ($B2BObj | Where-Object { $_.'Guest User Access Level'  -like '*WARN*' }  | Set-Style -Style Warning  | Out-Null)
                            }
                        })
                        $B2BTableParams = @{ Name = "External Collaboration Settings - $TenantId"; List = $true; ColumnWidths = 42, 58 }
                        if ($Report.ShowTableCaptions) { $B2BTableParams['Caption'] = "- $($B2BTableParams.Name)" }
                        $B2BObj | Table @B2BTableParams
                        $null = ($script:ExcelSheets['B2B Settings'] = $B2BObj)
                    } catch {
                        # Endpoint not available on this tenant/licence - show placeholder
                        $B2BObj = [System.Collections.ArrayList]::new()
                        $b2bFallback = [ordered] @{
                            'Allow External Users to Leave Tenant' = 'N/A -- externalIdentitiesPolicy endpoint not available'
                            'Allow Deleted Users to Be Re-invited' = 'N/A'
                            'Guest Invite Restriction'             = switch ($AuthzPolicy.allowInvitesFrom) {
                                'adminsAndGuestInviters'           { 'Admins + Guest Inviters only [OK]' }
                                'adminsGuestInvitersAndAllMembers' { 'All members can invite [WARN]' }
                                'everyone'                         { 'Everyone including guests [FAIL]' }
                                'none'                             { 'Nobody [OK]' }
                                default { if ($AuthzPolicy.allowInvitesFrom) { $AuthzPolicy.allowInvitesFrom } else { 'N/A' } }
                            }
                            'Guest User Access Level'              = switch ($AuthzPolicy.guestUserRoleId) {
                                '2af84b1e-32c8-42b7-82bc-daa82404023b' { 'Most restricted [OK]' }
                                '10dae51f-b6af-4016-8d66-8c2a99b929b3' { 'Limited access (default) [WARN]' }
                                'a0b1b346-4d3e-4e8b-98f8-753987be4970' { 'Same access as members [FAIL]' }
                                default { if ($AuthzPolicy.guestUserRoleId) { $AuthzPolicy.guestUserRoleId } else { 'N/A' } }
                            }
                        }
                        $null = $B2BObj.Add([pscustomobject]$b2bFallback)
                        $B2BTableParams = @{ Name = "External Collaboration Settings - $TenantId"; List = $true; ColumnWidths = 42, 58 }
                        if ($Report.ShowTableCaptions) { $B2BTableParams['Caption'] = "- $($B2BTableParams.Name)" }
                        $B2BObj | Table @B2BTableParams
                    }
                }
                #endregion

                #region Password Protection
                Write-Host " - Retrieving password protection settings..."
                Section -Style Heading2 'Password Protection' {
                    Paragraph "The following documents the Smart Lockout and custom banned password configuration for tenant $TenantId."
                    BlankLine

                    try {
                        $PwdProtection = Invoke-MgGraphRequest -Method GET `
                            -Uri 'https://graph.microsoft.com/beta/settings' `
                            -ErrorAction SilentlyContinue

                        # Find the Password Rule Settings template
                        $PwdSettings = $null
                        if ($PwdProtection -and $PwdProtection.value) {
                            $PwdSettings = $PwdProtection.value | Where-Object { $_.displayName -eq 'Password Rule Settings' -or $_.templateId -eq '5cf42378-d67d-4f36-ba46-e8b86229381d' } | Select-Object -First 1
                        }

                        # Smart Lockout from authorizationPolicy
                        $LockoutThreshold = 10  # default
                        $LockoutDuration  = 60  # default seconds

                        $PwdObj = [System.Collections.ArrayList]::new()
                        $pwdInObj = [ordered] @{
                            'Smart Lockout Threshold'           = "$LockoutThreshold attempts (default -- configurable in Entra ID Password Protection blade)"
                            'Smart Lockout Duration'            = "$LockoutDuration seconds (default)"
                            'Custom Banned Passwords Enabled'   = if ($PwdSettings) {
                                $val = ($PwdSettings.values | Where-Object { $_.name -eq 'BannedPasswordCheckOnPremisesMode' }).value
                                if ($val) { $val } else { '[INFO] Check Entra ID Password Protection blade' }
                            } else { '[INFO] Check Entra ID Password Protection blade' }
                            'Password Spray Protection'         = '[OK] Microsoft Entra Smart Lockout is always active for cloud accounts'
                            'On-Premises Password Protection'   = if ($PwdSettings) { '[INFO] Check if Entra Password Protection agent is deployed to DCs' } else { '[INFO] Not configured via Graph -- verify in portal' }
                        }
                        $PwdObj.Add([pscustomobject]$pwdInObj) | Out-Null
                        $PwdTableParams = @{ Name = "Password Protection Settings - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                        if ($Report.ShowTableCaptions) { $PwdTableParams['Caption'] = "- $($PwdTableParams.Name)" }
                        $PwdObj | Table @PwdTableParams
                        $null = ($script:ExcelSheets['Password Protection'] = $PwdObj)
                    } catch {
                        Write-AbrSectionError -Section 'Password Protection' -Message "$($_.Exception.Message)"
                    }
                }
                #endregion

                #region ACSC E8 + CIS Tenant Settings Assessment
                BlankLine
                if ($script:IncludeACSCe8) {
                    Paragraph "ACSC Essential Eight Maturity Level Assessment -- Tenant Settings:"
                    BlankLine
                    $CanRegApps   = $AuthzPolicy.defaultUserRolePermissions.allowedToCreateApps -eq $true
                    $InviteLevel  = $AuthzPolicy.allowInvitesFrom
                    #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json)
                    $CanRegAppsState = if ($CanRegApps) { 'Users CAN register apps -- restrict to admins [WARN]' } else { 'Restricted to admins [OK]' }
                    $_ComplianceVars = @{
                        'CanRegApps'                  = $CanRegApps
                        'CanRegAppsDetail'            = $CanRegAppsDetail
                        'CanRegAppsState'             = $CanRegAppsState
                        'InviteLevel'                 = $InviteLevel
                        'CanCreateTenants'            = $CanCreateTenants
                        'CanCreateTenantsState'       = $CanCreateTenantsState
                        'GuestAccessLevel'            = $GuestAccessLevel
                        'GuestAccessLevelDesc'        = $GuestAccessLevelDesc
                        'UserConsentDisabled'         = $UserConsentDisabled
                        'UserConsentVerifiedOnly'     = $UserConsentVerifiedOnly
                        'UserConsentPolicy'           = $UserConsentPolicy
                        'AdminConsentWorkflowEnabled' = $AdminConsentWorkflowEnabled
                        'AdminConsentWorkflowState'   = $AdminConsentWorkflowState
                        'PerUserMfaCount'             = $PerUserMfaCount
                        'CustomBannedPasswordEnabled'  = $CustomBannedPasswordEnabled
                        'CustomBannedPasswordState'    = $CustomBannedPasswordState
                        'LinkedInEnabled'              = $LinkedInEnabled
                        'LinkedInState'                = $LinkedInState
                        'RestrictAdminPortal'          = $RestrictAdminPortal
                        'RestrictAdminPortalState'     = $RestrictAdminPortalState
                        'IsHybridTenant'               = $IsHybridTenant
                        'PasswordHashSyncEnabled'      = $PasswordHashSyncEnabled
                        'PasswordHashSyncDetail'       = $PasswordHashSyncDetail
                        'GuestDynamicGroupExists'      = $GuestDynamicGroupExists
                        'GuestDynamicGroupDetail'      = $GuestDynamicGroupDetail
                    }
                    $E8TenChecks = Build-AbrComplianceChecks `
                        -Definitions (Get-AbrE8Checks -Section 'TenantSettings') `
                        -Framework E8 `
                        -CallerVariables $_ComplianceVars
                    New-AbrE8AssessmentTable -Checks $E8TenChecks -Name 'Tenant Settings' -TenantId $TenantId
                    # Consolidated into ACSC E8 Assessment sheet
                    if ($E8TenChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8TenChecks | Select-Object @{N='Section';E={'Tenant Settings'}}, ML, Control, Status, Detail ))) }
                    #endregion
                }
                if ($script:IncludeCISBaseline) {
                    BlankLine
                    Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Tenant Settings:"
                    BlankLine
                    #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json)
                    $CISTenChecks = Build-AbrComplianceChecks `
                        -Definitions (Get-AbrCISChecks -Section 'TenantSettings') `
                        -Framework CIS `
                        -CallerVariables $_ComplianceVars
                    New-AbrCISAssessmentTable -Checks $CISTenChecks -Name 'Tenant Settings' -TenantId $TenantId
                    # Consolidated into CIS Assessment sheet
                    if ($CISTenChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISTenChecks | Select-Object @{N='Section';E={'Tenant Settings'}}, CISControl, Level, Status, Detail ))) }
                    #endregion
                }
                #endregion

        } catch {
                Write-AbrSectionError -Section 'Tenant Settings' -Message "$($_.Exception.Message)"
        }
    }

    end { Show-AbrDebugExecutionTime -End -TitleMessage 'Tenant Settings' }
}