Enhance MFA method reporting by adding new classification for default SMS/Voice methods and updating HTML output for clarity
This commit is contained in:
@@ -26,10 +26,14 @@
|
|||||||
are SMS and/or Voice call. These accounts
|
are SMS and/or Voice call. These accounts
|
||||||
will hit the blocking passkey prompt on
|
will hit the blocking passkey prompt on
|
||||||
Feb 2027 retirement with no fallback.
|
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,
|
- YELLOW ("SmsVoiceRegistered"): other MFA methods are registered too,
|
||||||
but SMS or Voice is also registered and
|
and SMS or Voice is also registered (but
|
||||||
still needs to be removed from the
|
not the default) - still needs to be
|
||||||
account ahead of the retirement.
|
removed ahead of the retirement.
|
||||||
- (none): no SMS/Voice exposure, or no MFA registered
|
- (none): no SMS/Voice exposure, or no MFA registered
|
||||||
at all (flagged separately in the Notes
|
at all (flagged separately in the Notes
|
||||||
column - a different problem, not this
|
column - a different problem, not this
|
||||||
@@ -70,9 +74,16 @@
|
|||||||
.NOTES
|
.NOTES
|
||||||
Author: Petr Štěpán
|
Author: Petr Štěpán
|
||||||
Created: 2026-07-27
|
Created: 2026-07-27
|
||||||
Version: 1.0.0
|
Version: 1.0.1
|
||||||
Changelog:
|
Changelog:
|
||||||
1.0.0 - Initial version
|
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
|
#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;}
|
TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}
|
||||||
TD{border: 1px solid #969595; padding: 5px;}
|
TD{border: 1px solid #969595; padding: 5px;}
|
||||||
tr.SmsVoiceOnly{background: #f8d7da;}
|
tr.SmsVoiceOnly{background: #f8d7da;}
|
||||||
|
tr.SmsVoiceDefault{background: #f8d7da;}
|
||||||
tr.SmsVoiceRegistered{background: #fff3cd;}
|
tr.SmsVoiceRegistered{background: #fff3cd;}
|
||||||
tr.MethodEnabledSmsVoice{background: #fff3cd;}
|
tr.MethodEnabledSmsVoice{background: #fff3cd;}
|
||||||
tr.MethodEnabled{background: #d4edda;}
|
tr.MethodEnabled{background: #d4edda;}
|
||||||
@@ -528,7 +540,8 @@ tr.MethodDisabled{background: #e2e3e5; color: #6c6c6c;}
|
|||||||
$xml.table.Attributes.Append($tableClassAttr) | Out-Null
|
$xml.table.Attributes.Append($tableClassAttr) | Out-Null
|
||||||
|
|
||||||
# Walk the generated <tr> rows in lock-step with the report rows to apply
|
# 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
|
$rowIndex = 0
|
||||||
foreach ($tableRow in $xml.table.SelectNodes('tr')) {
|
foreach ($tableRow in $xml.table.SelectNodes('tr')) {
|
||||||
if ($tableRow.SelectNodes('th').Count -eq 0 -and $rowIndex -lt $Report.Count) {
|
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)
|
$htmlBody = [string]::Format('<div>{0}</div>', $xml.OuterXml)
|
||||||
|
|
||||||
$countOnly = @($Report | Where-Object { $_.RowClass -eq 'SmsVoiceOnly' }).Count
|
$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 = ''
|
$methodPolicyHtml = ''
|
||||||
if ($MethodPolicyStates.Count -gt 0) {
|
if ($MethodPolicyStates.Count -gt 0) {
|
||||||
@@ -567,7 +581,8 @@ $($methodRows -join "`n")
|
|||||||
<p>-----------------------------------------------------------------------</p>
|
<p>-----------------------------------------------------------------------</p>
|
||||||
<p>Total users reported: $($Report.Count)</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:#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>
|
<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
|
$rowClass = $null
|
||||||
if ($hasPhoneMethod -and -not $hasStrongMethod) {
|
if ($hasPhoneMethod -and -not $hasStrongMethod) {
|
||||||
$rowClass = 'SmsVoiceOnly'
|
$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) {
|
elseif ($hasStrongMethod -and $hasPhoneMethod) {
|
||||||
# SMS/Voice is registered alongside other methods - not the user's
|
# SMS/Voice is registered alongside other methods - not the user's
|
||||||
# only option, but it still needs to be removed from their
|
# 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
|
Export-AuthenticationHtmlReport -Report $report -Path $htmlPath -TenantDisplayName $tenantDisplayName -IsPremiumTenant $isPremium -MethodPolicyStates $methodPolicyStates
|
||||||
|
|
||||||
$countOnly = @($report | Where-Object { $_.RowClass -eq 'SmsVoiceOnly' }).Count
|
$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 "`n===== SUMMARY =====" -ForegroundColor Magenta
|
||||||
Write-Host "Tenant: $tenantDisplayName"
|
Write-Host "Tenant: $tenantDisplayName"
|
||||||
Write-Host "Total users reported: $($report.Count)"
|
Write-Host "Total users reported: $($report.Count)"
|
||||||
Write-Host "Red (SMS/Voice only method): $countOnly" -ForegroundColor Red
|
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 "CSV report: $csvPath"
|
||||||
Write-Host "HTML report: $htmlPath"
|
Write-Host "HTML report: $htmlPath"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user