SHELL/MFA.STATUS.ps1
|
$CheckId = "MFA.STATUS" $Title = "Supplemental - MFA status and Conditional Access coverage for users" $Level = "L1" $BenchmarkType = "Automated" function Get-PropValue { param( [AllowNull()]$Object, [string]$Name ) if ($null -eq $Object) { return $null } if ($Object -is [System.Collections.IDictionary]) { foreach ($Key in $Object.Keys) { if ([string]$Key -ieq $Name) { return $Object[$Key] } } } if ($Object.PSObject -and $Object.PSObject.Properties) { foreach ($Property in $Object.PSObject.Properties) { if ([string]$Property.Name -ieq $Name) { return $Property.Value } } } return $null } function Invoke-GraphPaged { param( [Parameter(Mandatory = $true)][string]$Uri, [hashtable]$Headers ) $Items = [System.Collections.Generic.List[object]]::new() $Next = $Uri while (-not [string]::IsNullOrWhiteSpace($Next)) { if ($Headers) { $Response = Invoke-MgGraphRequest -Method GET -Uri $Next -Headers $Headers -ErrorAction Stop } else { $Response = Invoke-MgGraphRequest -Method GET -Uri $Next -ErrorAction Stop } foreach ($Item in @(Get-PropValue -Object $Response -Name "value")) { $Items.Add($Item) } $Next = [string](Get-PropValue -Object $Response -Name "@odata.nextLink") } return @($Items) } function Convert-ToStringArray { param([AllowNull()]$Value) if ($null -eq $Value) { return @() } if ($Value -is [string]) { return @($Value) } if ($Value -is [System.Collections.IEnumerable]) { return @($Value | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } return @([string]$Value) } try { $RunStamp = Get-Date -Format "yyyyMMdd_HHmmss" $ProjectRoot = Split-Path -Path $PSScriptRoot -Parent $ManualReviewDir = Join-Path $ProjectRoot "manual_review" if (-not (Test-Path $ManualReviewDir)) { New-Item -Path $ManualReviewDir -ItemType Directory -Force | Out-Null } $CsvExportPath = Join-Path $ManualReviewDir "mfa_user_status_$RunStamp.csv" $SkusResponse = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/subscribedSkus" -ErrorAction Stop $ServicePlans = @($SkusResponse.value | ForEach-Object { @($_.servicePlans) } | Where-Object { $_ }) $ServicePlanNames = @($ServicePlans | ForEach-Object { [string]$_.servicePlanName } | Where-Object { $_ } | Select-Object -Unique) $HasAadPremium = @($ServicePlanNames | Where-Object { $_ -like "AAD_PREMIUM*" }).Count -gt 0 $SecurityDefaults = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy" -ErrorAction Stop $SecurityDefaultsEnabled = [bool](Get-PropValue -Object $SecurityDefaults -Name "isEnabled") $AllCAPolicies = @(Invoke-GraphPaged -Uri "https://graph.microsoft.com/beta/policies/conditionalAccessPolicies?`$top=200") $MfaCAPolicies = @( $AllCAPolicies | Where-Object { $State = [string](Get-PropValue -Object $_ -Name "state") if ($State -ne "enabled") { return $false } $GrantControls = Get-PropValue -Object $_ -Name "grantControls" $BuiltInControls = @(Convert-ToStringArray -Value (Get-PropValue -Object $GrantControls -Name "builtInControls")) $HasMfaBuiltIn = @($BuiltInControls | Where-Object { $_ -eq "mfa" }).Count -gt 0 $AuthStrength = Get-PropValue -Object $GrantControls -Name "authenticationStrength" $HasAuthStrength = ($null -ne $AuthStrength) return ($HasMfaBuiltIn -or $HasAuthStrength) } ) $MfaReport = @(Invoke-GraphPaged -Uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999" -Headers @{ "ConsistencyLevel" = "eventual" }) if ($MfaReport.Count -eq 0) { throw "No user registration details were returned from reports/authenticationMethods/userRegistrationDetails." } $UserResults = [System.Collections.Generic.List[object]]::new() foreach ($Lookup in $MfaReport) { $UserId = [string](Get-PropValue -Object $Lookup -Name "id") $UserPrincipalName = [string](Get-PropValue -Object $Lookup -Name "userPrincipalName") $DisplayName = [string](Get-PropValue -Object $Lookup -Name "userDisplayName") if ([string]::IsNullOrWhiteSpace($DisplayName)) { $DisplayName = [string](Get-PropValue -Object $Lookup -Name "displayName") } $IsExternal = ($UserPrincipalName -match "#EXT#") $IsAdmin = [bool](Get-PropValue -Object $Lookup -Name "isAdmin") $IsMfaCapable = [bool](Get-PropValue -Object $Lookup -Name "isMfaCapable") $IsMfaRegistered = [bool](Get-PropValue -Object $Lookup -Name "isMfaRegistered") $IsSsprCapable = [bool](Get-PropValue -Object $Lookup -Name "isSsprCapable") $IsSsprEnabled = [bool](Get-PropValue -Object $Lookup -Name "isSsprEnabled") $IsSsprRegistered = [bool](Get-PropValue -Object $Lookup -Name "isSsprRegistered") $IsPasswordlessCapable = [bool](Get-PropValue -Object $Lookup -Name "isPasswordlessCapable") $DefaultMfaMethod = [string](Get-PropValue -Object $Lookup -Name "defaultMfaMethod") $UserPreferredMfaMethod = [string](Get-PropValue -Object $Lookup -Name "userPreferredMethodForSecondaryAuthentication") $SystemPreferredEnabled = [bool](Get-PropValue -Object $Lookup -Name "isSystemPreferredAuthenticationMethodEnabled") $SystemPreferredMethods = @(Convert-ToStringArray -Value (Get-PropValue -Object $Lookup -Name "systemPreferredAuthenticationMethods")) $RegisteredMethods = @(Convert-ToStringArray -Value (Get-PropValue -Object $Lookup -Name "methodsRegistered")) $AppliedPolicies = [System.Collections.Generic.List[string]]::new() $DisabledPolicies = [System.Collections.Generic.List[string]]::new() $CaGaps = [System.Collections.Generic.List[string]]::new() foreach ($Policy in $MfaCAPolicies) { $PolicyName = [string](Get-PropValue -Object $Policy -Name "displayName") if ([string]::IsNullOrWhiteSpace($PolicyName)) { $PolicyName = [string](Get-PropValue -Object $Policy -Name "id") } $Conditions = Get-PropValue -Object $Policy -Name "conditions" $Users = Get-PropValue -Object $Conditions -Name "users" $IncludeUsers = @(Convert-ToStringArray -Value (Get-PropValue -Object $Users -Name "includeUsers")) $ExcludeUsers = @(Convert-ToStringArray -Value (Get-PropValue -Object $Users -Name "excludeUsers")) $TargetsAllUsers = @($IncludeUsers | Where-Object { $_ -eq "All" }).Count -gt 0 $DirectlyTargeted = @($IncludeUsers | Where-Object { $_ -eq $UserId }).Count -gt 0 $DirectlyExcluded = @($ExcludeUsers | Where-Object { $_ -eq $UserId }).Count -gt 0 if ($TargetsAllUsers -or $DirectlyTargeted) { if ($DirectlyExcluded) { $CaGaps.Add("$PolicyName (user excluded)") } else { $AppliedPolicies.Add($PolicyName) } } } if ($MfaCAPolicies.Count -eq 0 -and -not $SecurityDefaultsEnabled) { $CaGaps.Add("No enabled Conditional Access policies requiring MFA") } $InsecureDefaultMethod = $DefaultMfaMethod -in @("email", "mobilePhone") $Risk = "Low" if ($CaGaps.Count -gt 0) { $Risk = "Critical" } elseif ($IsAdmin -and ((-not $IsMfaRegistered) -or (-not $IsMfaCapable) -or $InsecureDefaultMethod)) { $Risk = "Critical" } elseif ((-not $IsAdmin) -and ((-not $IsMfaRegistered) -or (-not $IsMfaCapable) -or $InsecureDefaultMethod)) { $Risk = "High" } elseif (-not $IsSsprRegistered) { $Risk = "Medium" } $UserResults.Add([pscustomobject]@{ Name = $DisplayName Id = $UserId UserPrincipalName = $UserPrincipalName Risk = $Risk IsEnabled = $null IsExternal = $IsExternal IsAdmin = $IsAdmin IsSSPRCapable = $IsSsprCapable IsSSPREnabled = $IsSsprEnabled IsSSPRRegistered = $IsSsprRegistered IsMFACapable = $IsMfaCapable IsMFARegistered = $IsMfaRegistered RegisteredMFAMethods = ($RegisteredMethods -join ", ") DefaultMFAMethod = $DefaultMfaMethod SystemPreferredMethodEnforced = $SystemPreferredEnabled SystemEnforcedMethod = ($SystemPreferredMethods -join ", ") UserPreferredMFAMethod = $UserPreferredMfaMethod IsPasswordlessCapable = $IsPasswordlessCapable MemberOf = $null AppliedCAPolicies = (@($AppliedPolicies | Select-Object -Unique) -join ", ") CAPoliciesNotApplied = (@($DisabledPolicies | Select-Object -Unique) -join ", ") PossibleCAGaps = (@($CaGaps | Select-Object -Unique) -join ", ") }) } $SortedResults = @( $UserResults | Sort-Object @{ Expression = { switch ([string]$_.Risk) { "Critical" { 1 } "High" { 2 } "Medium" { 3 } default { 4 } } } }, UserPrincipalName ) $SortedResults | Export-Csv -Path $CsvExportPath -NoTypeInformation -Encoding UTF8 $RiskCounts = @{ Critical = @($SortedResults | Where-Object { $_.Risk -eq "Critical" }).Count High = @($SortedResults | Where-Object { $_.Risk -eq "High" }).Count Medium = @($SortedResults | Where-Object { $_.Risk -eq "Medium" }).Count Low = @($SortedResults | Where-Object { $_.Risk -eq "Low" }).Count } $Pass = $true $ErrorMessage = $null if ($SecurityDefaultsEnabled) { $Pass = $true } elseif ($MfaCAPolicies.Count -eq 0) { $Pass = $false $ErrorMessage = "No enabled Conditional Access policies requiring MFA were found and Security Defaults are disabled." } elseif ($RiskCounts.Critical -gt 0) { $Pass = $false $ErrorMessage = "One or more users are in Critical MFA risk state." } elseif ($RiskCounts.High -gt 0) { $Pass = $false $ErrorMessage = "One or more users are in High MFA risk state." } [pscustomobject]@{ CheckId = $CheckId Title = $Title Level = $Level BenchmarkType = $BenchmarkType Status = if ($Pass) { "PASS" } else { "FAIL" } Pass = $Pass Evidence = [pscustomobject]@{ HasAadPremiumLicensePlan = $HasAadPremium SecurityDefaultsEnabled = $SecurityDefaultsEnabled ConditionalAccessMfaPolicyCount = @($MfaCAPolicies).Count TotalUsersAssessed = @($SortedResults).Count RiskCounts = $RiskCounts CsvExportPath = $CsvExportPath CsvRowCount = @($SortedResults).Count ResultsPreview = @($SortedResults | Select-Object -First 50) SourceDocument = "Supplemental tenant MFA posture analysis (Graph beta reports + CA policy mapping)" } Error = $ErrorMessage Timestamp = Get-Date } } catch { [pscustomobject]@{ CheckId = $CheckId Title = $Title Level = $Level BenchmarkType = $BenchmarkType Status = "ERROR" Pass = $null Evidence = [pscustomobject]@{ SourceDocument = "Supplemental tenant MFA posture analysis (Graph beta reports + CA policy mapping)" } Error = $_.Exception.Message Timestamp = Get-Date } } |