Entra/EntraPasswordAuthChecks.ps1
|
# ------------------------------------------------------------------- # Entra ID -- Password & Authentication Checks # Extracted from Get-EntraSecurityConfig.ps1 (#256) # Runs in shared scope: $settings, $checkIdCounter, Add-Setting, # $context, $sspr, $authPolicy, $orgSettings # ------------------------------------------------------------------- [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] param() # ------------------------------------------------------------------ # 1. Security Defaults # ------------------------------------------------------------------ try { Write-Verbose "Checking security defaults..." $secDefaults = Invoke-MgGraphRequest -Method GET -Uri '/v1.0/policies/identitySecurityDefaultsEnforcementPolicy' -ErrorAction Stop if (-not $secDefaults) { throw "API returned null response" } $isEnabled = $secDefaults['isEnabled'] $settingParams = @{ Category = 'Security Defaults' Setting = 'Security Defaults Enabled' CurrentValue = "$isEnabled" RecommendedValue = 'True (if no Conditional Access)' Status = $(if ($isEnabled) { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-SECDEFAULT-001' Remediation = 'Run: Update-MgPolicyIdentitySecurityDefaultsEnforcementPolicy -IsEnabled $true. Entra admin center > Properties > Manage security defaults.' } Add-Setting @settingParams } catch { Write-Warning "Could not retrieve security defaults: $_" $settingParams = @{ Category = 'Security Defaults' Setting = 'Security Defaults Enabled' CurrentValue = 'Unable to retrieve' RecommendedValue = 'True (if no CA)' Status = 'Review' CheckId = 'ENTRA-SECDEFAULT-001' Remediation = 'Run: Update-MgPolicyIdentitySecurityDefaultsEnforcementPolicy -IsEnabled $true. Entra admin center > Properties > Manage security defaults.' } Add-Setting @settingParams } # ------------------------------------------------------------------ # 7. Self-Service Password Reset # ------------------------------------------------------------------ try { Write-Verbose "Checking SSPR configuration..." $graphParams = @{ Method = 'GET' Uri = '/v1.0/policies/authenticationMethodsPolicy' ErrorAction = 'Stop' } $sspr = Invoke-MgGraphRequest @graphParams $ssprRegistration = $sspr['registrationEnforcement']['authenticationMethodsRegistrationCampaign']['state'] $settingParams = @{ Category = 'Password Management' Setting = 'Auth Method Registration Campaign' CurrentValue = "$ssprRegistration" RecommendedValue = 'enabled' Status = $(if ($ssprRegistration -eq 'enabled') { 'Pass' } else { 'Warning' }) CheckId = 'ENTRA-MFA-001' Remediation = 'Run: Update-MgBetaPolicyAuthenticationMethodPolicy with RegistrationEnforcement settings. Entra admin center > Protection > Authentication methods > Registration campaign.' } Add-Setting @settingParams } catch { Write-Warning "Could not check SSPR: $_" } # ------------------------------------------------------------------ # 7b. Authentication Methods -- SMS/Voice/Email (CIS 5.2.3.5, 5.2.3.7) # ------------------------------------------------------------------ try { if ($sspr) { $authMethods = $sspr['authenticationMethodConfigurations'] if ($authMethods) { # CIS 5.2.3.5 -- SMS sign-in disabled $smsMethod = $authMethods | Where-Object { $_['id'] -eq 'Sms' } $smsState = if ($smsMethod) { $smsMethod['state'] } else { 'not found' } $settingParams = @{ Category = 'Authentication Methods' Setting = 'SMS Authentication' CurrentValue = "$smsState" RecommendedValue = 'disabled' Status = $(if ($smsState -eq 'disabled') { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-AUTHMETHOD-001' Remediation = 'Entra admin center > Protection > Authentication methods > SMS > Disable. SMS is vulnerable to SIM-swapping attacks.' } Add-Setting @settingParams # CIS 5.2.3.5 -- Voice call disabled $voiceMethod = $authMethods | Where-Object { $_['id'] -eq 'Voice' } $voiceState = if ($voiceMethod) { $voiceMethod['state'] } else { 'not found' } $settingParams = @{ Category = 'Authentication Methods' Setting = 'Voice Call Authentication' CurrentValue = "$voiceState" RecommendedValue = 'disabled' Status = $(if ($voiceState -eq 'disabled') { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-AUTHMETHOD-001' Remediation = 'Entra admin center > Protection > Authentication methods > Voice call > Disable. Voice is vulnerable to telephony-based attacks.' } Add-Setting @settingParams # CIS 5.2.3.7 -- Email OTP disabled $emailMethod = $authMethods | Where-Object { $_['id'] -eq 'Email' } $emailState = if ($emailMethod) { $emailMethod['state'] } else { 'not found' } $settingParams = @{ Category = 'Authentication Methods' Setting = 'Email OTP Authentication' CurrentValue = "$emailState" RecommendedValue = 'disabled' Status = $(if ($emailState -eq 'disabled') { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-AUTHMETHOD-002' Remediation = 'Entra admin center > Protection > Authentication methods > Email OTP > Disable. Email OTP is a weaker authentication factor.' } Add-Setting @settingParams } } } catch { Write-Warning "Could not check authentication method configurations: $_" } # ------------------------------------------------------------------ # 7c. SSPR Enabled for All Users (CIS 5.2.4.1) # ------------------------------------------------------------------ try { if ($sspr) { $campaign = $sspr['registrationEnforcement']['authenticationMethodsRegistrationCampaign'] $campaignState = $campaign['state'] $includeTargets = $campaign['includeTargets'] $targetsAll = $false if ($includeTargets) { $targetsAll = $includeTargets | Where-Object { $_['id'] -eq 'all_users' -or $_['targetType'] -eq 'group' } } $settingParams = @{ Category = 'Password Management' Setting = 'SSPR Registration Campaign Targets All Users' CurrentValue = $(if ($campaignState -eq 'enabled' -and $targetsAll) { 'Enabled for all users' } elseif ($campaignState -eq 'enabled') { 'Enabled (limited scope)' } else { 'Disabled' }) RecommendedValue = 'Enabled for all users' Status = $(if ($campaignState -eq 'enabled' -and $targetsAll) { 'Pass' } elseif ($campaignState -eq 'enabled') { 'Warning' } else { 'Fail' }) CheckId = 'ENTRA-SSPR-001' Remediation = 'Entra admin center > Protection > Authentication methods > Registration campaign > Enable and target All Users.' } Add-Setting @settingParams } } catch { Write-Warning "Could not check SSPR targeting: $_" } # ------------------------------------------------------------------ # 8. Password Protection (Banned Passwords) # ------------------------------------------------------------------ try { Write-Verbose "Checking password protection..." $graphParams = @{ Method = 'GET' Uri = '/v1.0/settings' ErrorAction = 'Stop' } $passwordProtection = Invoke-MgGraphRequest @graphParams $pwSettings = $passwordProtection['value'] | Where-Object { $_['displayName'] -eq 'Password Rule Settings' } if ($pwSettings) { $bannedListEntry = if ($pwSettings['values']) { $pwSettings['values'] | Where-Object { $_['name'] -eq 'BannedPasswordList' } } else { $null } $bannedList = if ($bannedListEntry) { $bannedListEntry['value'] } else { $null } $enforceCustomEntry = if ($pwSettings['values']) { $pwSettings['values'] | Where-Object { $_['name'] -eq 'EnableBannedPasswordCheck' } } else { $null } $enforceCustom = if ($enforceCustomEntry) { $enforceCustomEntry['value'] } else { $null } $lockoutEntry = if ($pwSettings['values']) { $pwSettings['values'] | Where-Object { $_['name'] -eq 'LockoutThreshold' } } else { $null } $lockoutThreshold = if ($lockoutEntry) { $lockoutEntry['value'] } else { $null } $settingParams = @{ Category = 'Password Management' Setting = 'Custom Banned Password List Enforced' CurrentValue = "$enforceCustom" RecommendedValue = 'True' Status = $(if ($enforceCustom -eq 'True') { 'Pass' } else { 'Warning' }) CheckId = 'ENTRA-PASSWORD-002' Remediation = 'Run: Update-MgBetaDirectorySetting for Password Rule Settings with CustomBannedPasswordsEnforced = true. Entra admin center > Protection > Password protection.' } Add-Setting @settingParams $bannedCount = if ($bannedList) { ($bannedList -split ',').Count } else { 0 } $settingParams = @{ Category = 'Password Management' Setting = 'Custom Banned Password Count' CurrentValue = "$bannedCount" RecommendedValue = '1+' Status = $(if ($bannedCount -gt 0) { 'Pass' } else { 'Warning' }) CheckId = 'ENTRA-PASSWORD-004' Remediation = 'Run: Update-MgBetaDirectorySetting for Password Rule Settings to add organization-specific terms. Entra admin center > Protection > Password protection.' } Add-Setting @settingParams $settingParams = @{ Category = 'Password Management' Setting = 'Smart Lockout Threshold' CurrentValue = "$lockoutThreshold" RecommendedValue = '10' Status = $(if ([int]$lockoutThreshold -le 10) { 'Pass' } else { 'Review' }) CheckId = 'ENTRA-PASSWORD-003' Remediation = 'Run: Update-MgBetaDirectorySetting for Password Rule Settings with LockoutThreshold. Entra admin center > Protection > Password protection.' } Add-Setting @settingParams } } catch { Write-Warning "Could not check password protection: $_" } # ------------------------------------------------------------------ # 9. Password Expiration Policy # ------------------------------------------------------------------ try { Write-Verbose "Checking password expiration..." $domains = Invoke-MgGraphRequest -Method GET -Uri '/v1.0/domains' -ErrorAction Stop $domainList = if ($domains -and $domains['value']) { @($domains['value']) } else { @() } foreach ($domain in $domainList) { if (-not $domain['isVerified']) { continue } $validityDays = $domain['passwordValidityPeriodInDays'] $neverExpires = ($validityDays -eq 2147483647) $settingParams = @{ Category = 'Password Management' Setting = "Password Expiration: $($domain['id'])" CurrentValue = $(if ($neverExpires) { 'Never expires' } else { "$validityDays days" }) RecommendedValue = 'Never expires (with MFA)' Status = $(if ($neverExpires) { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-PASSWORD-001' Remediation = 'Run: Update-MgDomain -DomainId {domain} -PasswordValidityPeriodInDays 2147483647. M365 admin center > Settings > Password expiration policy.' } Add-Setting @settingParams } } catch { Write-Warning "Could not check password expiration: $_" } # ------------------------------------------------------------------ # 20. Authenticator Fatigue Protection (CIS 5.2.3.1) # ------------------------------------------------------------------ try { if ($sspr) { $authMethods = $sspr['authenticationMethodConfigurations'] $authenticator = $authMethods | Where-Object { $_['id'] -eq 'MicrosoftAuthenticator' } if ($authenticator) { $featureSettings = $authenticator['featureSettings'] $numberMatch = $featureSettings['numberMatchingRequiredState']['state'] $appInfo = $featureSettings['displayAppInformationRequiredState']['state'] $fatiguePassed = ($numberMatch -eq 'enabled') -and ($appInfo -eq 'enabled') $settingParams = @{ Category = 'Authentication Methods' Setting = 'Authenticator Fatigue Protection' CurrentValue = "Number matching: $numberMatch; App context: $appInfo" RecommendedValue = 'Both enabled' Status = $(if ($fatiguePassed) { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-AUTHMETHOD-003' Remediation = 'Entra admin center > Protection > Authentication methods > Microsoft Authenticator > Configure > Require number matching = Enabled, Show application name = Enabled.' } Add-Setting @settingParams } else { $settingParams = @{ Category = 'Authentication Methods' Setting = 'Authenticator Fatigue Protection' CurrentValue = 'Microsoft Authenticator not configured' RecommendedValue = 'Both enabled' Status = 'Review' CheckId = 'ENTRA-AUTHMETHOD-003' Remediation = 'Enable Microsoft Authenticator and configure number matching + application context display.' } Add-Setting @settingParams } } } catch { Write-Warning "Could not check authenticator fatigue protection: $_" } # ------------------------------------------------------------------ # 21. System-Preferred MFA (CIS 5.2.3.6) # ------------------------------------------------------------------ try { if ($sspr) { $systemPreferred = $sspr['systemCredentialPreferences'] $sysState = if ($systemPreferred) { $systemPreferred['state'] } else { 'not configured' } $settingParams = @{ Category = 'Authentication Methods' Setting = 'System-Preferred MFA' CurrentValue = "$sysState" RecommendedValue = 'enabled' Status = $(if ($sysState -eq 'enabled') { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-AUTHMETHOD-004' Remediation = 'Entra admin center > Protection > Authentication methods > Settings > System-preferred multifactor authentication > Enabled.' } Add-Setting @settingParams } } catch { Write-Warning "Could not check system-preferred MFA: $_" } # ------------------------------------------------------------------ # 27. Password Protection On-Premises (CIS 5.2.3.3) # ------------------------------------------------------------------ try { Write-Verbose "Checking password protection on-premises setting..." # Check if tenant uses directory sync (hybrid) -- on-prem check is irrelevant for cloud-only # Reuse $orgSettings from section 14 (LinkedIn check) which fetches /beta/organization/{tenantId} $isCloudOnly = $true if ($orgSettings -and $orgSettings['onPremisesSyncEnabled'] -eq $true) { $isCloudOnly = $false } elseif (-not $orgSettings) { $isCloudOnly = $null # Org data not available -- fall through to normal check } if ($isCloudOnly -eq $true) { $settingParams = @{ Category = 'Password Management' Setting = 'Password Protection On-Premises' CurrentValue = 'Not applicable (cloud-only tenant)' RecommendedValue = 'True (if hybrid)' Status = 'Info' CheckId = 'ENTRA-PASSWORD-005' Remediation = 'Not applicable for cloud-only tenants. If you configure hybrid identity in the future, enable on-premises password protection.' } Add-Setting @settingParams } # Reuse $pwSettings from section 8 if available elseif ($pwSettings) { $onPremEntry = if ($pwSettings['values']) { $pwSettings['values'] | Where-Object { $_['name'] -eq 'EnableBannedPasswordCheckOnPremises' } } else { $null } $onPremEnabled = if ($onPremEntry) { $onPremEntry['value'] } else { $null } $settingParams = @{ Category = 'Password Management' Setting = 'Password Protection On-Premises' CurrentValue = "$onPremEnabled" RecommendedValue = 'True' Status = $(if ($onPremEnabled -eq 'True') { 'Pass' } else { 'Fail' }) CheckId = 'ENTRA-PASSWORD-005' Remediation = 'Entra admin center > Protection > Authentication methods > Password protection > Enable password protection on Windows Server Active Directory > Yes.' } Add-Setting @settingParams } else { $settingParams = @{ Category = 'Password Management' Setting = 'Password Protection On-Premises' CurrentValue = 'Password Rule Settings not available' RecommendedValue = 'True' Status = 'Review' CheckId = 'ENTRA-PASSWORD-005' Remediation = 'Entra admin center > Protection > Authentication methods > Password protection. Verify on-premises password protection is enabled.' } Add-Setting @settingParams } } catch { Write-Warning "Could not check password protection on-premises: $_" } # ------------------------------------------------------------------ # 33. Password Hash Sync (CIS 5.1.8.1) # ------------------------------------------------------------------ try { Write-Verbose "Checking password hash sync for hybrid deployments..." $graphParams = @{ Method = 'GET' Uri = '/v1.0/organization' ErrorAction = 'Stop' } $orgInfo = Invoke-MgGraphRequest @graphParams $orgValue = if ($orgInfo -and $orgInfo['value']) { @($orgInfo['value']) } else { @() } if ($orgValue -and $orgValue.Count -gt 0) { $org = $orgValue[0] $onPremSync = $org['onPremisesSyncEnabled'] if ($null -eq $onPremSync -or $onPremSync -eq $false) { # Cloud-only tenant, PHS not applicable $settingParams = @{ Category = 'Hybrid Identity' Setting = 'Password Hash Sync' CurrentValue = 'Cloud-only tenant (no directory sync)' RecommendedValue = 'Enabled (if hybrid)' Status = 'Info' CheckId = 'ENTRA-HYBRID-001' Remediation = 'Not applicable for cloud-only tenants. If you configure hybrid identity in the future, enable Password Hash Sync in Azure AD Connect.' } Add-Setting @settingParams } else { # Hybrid tenant, check PHS via on-premises sync status $phsEnabled = $org['onPremisesLastPasswordSyncDateTime'] if ($phsEnabled) { $settingParams = @{ Category = 'Hybrid Identity' Setting = 'Password Hash Sync' CurrentValue = "Enabled (last sync: $phsEnabled)" RecommendedValue = 'Enabled' Status = 'Pass' CheckId = 'ENTRA-HYBRID-001' Remediation = 'Password Hash Sync is enabled. Verify it remains active in Azure AD Connect configuration.' } Add-Setting @settingParams } else { $settingParams = @{ Category = 'Hybrid Identity' Setting = 'Password Hash Sync' CurrentValue = 'Directory sync enabled but no password sync detected' RecommendedValue = 'Enabled' Status = 'Fail' CheckId = 'ENTRA-HYBRID-001' Remediation = 'Enable Password Hash Sync in Azure AD Connect > Optional Features. This provides leaked credential detection and backup authentication.' } Add-Setting @settingParams } } } else { $settingParams = @{ Category = 'Hybrid Identity' Setting = 'Password Hash Sync' CurrentValue = 'Organization data not available' RecommendedValue = 'Enabled (if hybrid)' Status = 'Review' CheckId = 'ENTRA-HYBRID-001' Remediation = 'Verify Password Hash Sync status in Azure AD Connect. Entra admin center > Identity > Hybrid management > Azure AD Connect.' } Add-Setting @settingParams } } catch { Write-Warning "Could not check password hash sync: $_" } |