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' } } |