Enhance MFA method reporting by adding new classification for default SMS/Voice methods and updating HTML output for clarity

This commit is contained in:
Petr Štěpán
2026-07-29 14:27:01 +02:00
parent 7b82711ec7
commit 698139884f
+37 -9
View File
@@ -26,10 +26,14 @@
are SMS and/or Voice call. These accounts
will hit the blocking passkey prompt on
Feb 2027 retirement with no fallback.
- RED ("SmsVoiceDefault"): the user's actual default/preferred
method is SMS or Voice, even though
other methods are also registered - the
direct risk this retirement is about.
- YELLOW ("SmsVoiceRegistered"): other MFA methods are registered too,
but SMS or Voice is also registered and
still needs to be removed from the
account ahead of the retirement.
and SMS or Voice is also registered (but
not the default) - still needs to be
removed ahead of the retirement.
- (none): no SMS/Voice exposure, or no MFA registered
at all (flagged separately in the Notes
column - a different problem, not this
@@ -70,9 +74,16 @@
.NOTES
Author: Petr Štěpán
Created: 2026-07-27
Version: 1.0.0
Version: 1.0.1
Changelog:
1.0.0 - Initial version
1.0.1 - Added tenant authentication methods policy overview (console
summary and HTML report), progress bar for per-user
processing, pre-flight Microsoft.Graph module version check
with an offer to auto-repair mismatched/missing modules,
and a new RED classification for accounts whose actual
default sign-in method is SMS/Voice (previously only
flagged yellow unless it was the user's only method).
#>
#Requires -Version 7.0
@@ -506,6 +517,7 @@ TABLE{border: 1px solid #969595; border-collapse: collapse; font-size: 9pt;}
TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}
TD{border: 1px solid #969595; padding: 5px;}
tr.SmsVoiceOnly{background: #f8d7da;}
tr.SmsVoiceDefault{background: #f8d7da;}
tr.SmsVoiceRegistered{background: #fff3cd;}
tr.MethodEnabledSmsVoice{background: #fff3cd;}
tr.MethodEnabled{background: #d4edda;}
@@ -528,7 +540,8 @@ tr.MethodDisabled{background: #e2e3e5; color: #6c6c6c;}
$xml.table.Attributes.Append($tableClassAttr) | Out-Null
# Walk the generated <tr> rows in lock-step with the report rows to apply
# the RowClass (SmsVoiceOnly / SmsVoiceRegistered / none) computed earlier.
# the RowClass (SmsVoiceOnly / SmsVoiceDefault / SmsVoiceRegistered / none)
# computed earlier.
$rowIndex = 0
foreach ($tableRow in $xml.table.SelectNodes('tr')) {
if ($tableRow.SelectNodes('th').Count -eq 0 -and $rowIndex -lt $Report.Count) {
@@ -543,7 +556,8 @@ tr.MethodDisabled{background: #e2e3e5; color: #6c6c6c;}
$htmlBody = [string]::Format('<div>{0}</div>', $xml.OuterXml)
$countOnly = @($Report | Where-Object { $_.RowClass -eq 'SmsVoiceOnly' }).Count
$countDefault = @($Report | Where-Object { $_.RowClass -eq 'SmsVoiceRegistered' }).Count
$countDefault = @($Report | Where-Object { $_.RowClass -eq 'SmsVoiceDefault' }).Count
$countRegistered = @($Report | Where-Object { $_.RowClass -eq 'SmsVoiceRegistered' }).Count
$methodPolicyHtml = ''
if ($MethodPolicyStates.Count -gt 0) {
@@ -567,7 +581,8 @@ $($methodRows -join "`n")
<p>-----------------------------------------------------------------------</p>
<p>Total users reported: $($Report.Count)</p>
<p style="background:#f8d7da; display:inline-block; padding:2px 6px;">Red - only MFA method is SMS/Voice: $countOnly</p><br/>
<p style="background:#fff3cd; display:inline-block; padding:2px 6px;">Yellow - SMS/Voice registered alongside other methods: $countDefault</p>
<p style="background:#f8d7da; display:inline-block; padding:2px 6px;">Red - default method is SMS/Voice: $countDefault</p><br/>
<p style="background:#fff3cd; display:inline-block; padding:2px 6px;">Yellow - SMS/Voice registered, not default: $countRegistered</p>
<p>Reference: https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sms-voice-retirement</p>
"@
@@ -716,10 +731,21 @@ try {
}
}
# Only matches a verified value (from the Graph API preference read,
# or the tenant-wide report) or our own logical deduction above (not
# a guess) - 'Not reported' and $null correctly fail to match.
$defaultIsPhone = $defaultMethod -match '(?i)^(sms|voice)'
$rowClass = $null
if ($hasPhoneMethod -and -not $hasStrongMethod) {
$rowClass = 'SmsVoiceOnly'
}
elseif ($defaultIsPhone) {
# The user's actual default/preferred method is SMS or Voice -
# this is the direct risk the retirement is about, regardless of
# what other methods are also registered.
$rowClass = 'SmsVoiceDefault'
}
elseif ($hasStrongMethod -and $hasPhoneMethod) {
# SMS/Voice is registered alongside other methods - not the user's
# only option, but it still needs to be removed from their
@@ -776,13 +802,15 @@ try {
Export-AuthenticationHtmlReport -Report $report -Path $htmlPath -TenantDisplayName $tenantDisplayName -IsPremiumTenant $isPremium -MethodPolicyStates $methodPolicyStates
$countOnly = @($report | Where-Object { $_.RowClass -eq 'SmsVoiceOnly' }).Count
$countDefault = @($report | Where-Object { $_.RowClass -eq 'SmsVoiceRegistered' }).Count
$countDefault = @($report | Where-Object { $_.RowClass -eq 'SmsVoiceDefault' }).Count
$countRegistered = @($report | Where-Object { $_.RowClass -eq 'SmsVoiceRegistered' }).Count
Write-Host "`n===== SUMMARY =====" -ForegroundColor Magenta
Write-Host "Tenant: $tenantDisplayName"
Write-Host "Total users reported: $($report.Count)"
Write-Host "Red (SMS/Voice only method): $countOnly" -ForegroundColor Red
Write-Host "Yellow (SMS/Voice registered alongside other methods): $countDefault" -ForegroundColor Yellow
Write-Host "Red (SMS/Voice is default method): $countDefault" -ForegroundColor Red
Write-Host "Yellow (SMS/Voice registered, not default): $countRegistered" -ForegroundColor Yellow
Write-Host "CSV report: $csvPath"
Write-Host "HTML report: $htmlPath"