public/maester/entra/Test-MtAppRegistrationOwnersWithoutMFA.ps1
|
<# .SYNOPSIS Tests if app registration owners have Multi-Factor Authentication (MFA) enabled. .DESCRIPTION This function checks all Entra ID app registrations and verifies that their owners have MFA registered. .OUTPUTS [bool] - Returns $true if all owners have MFA, $false if any owners lack MFA, $null if skipped .EXAMPLE Test-MtAppRegistrationOwnersWithoutMFA .LINK https://maester.dev/docs/commands/Test-MtAppRegistrationOwnersWithoutMFA #> function Test-MtAppRegistrationOwnersWithoutMFA { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'This test checks MFA for all app registration owners.')] [OutputType([bool])] param() # Early exit if Graph connection is not available if (-not (Test-MtConnection Graph)) { Add-MtTestResultDetail -SkippedBecause NotConnectedGraph return $null } try { Write-Verbose "Step 1: Retrieving app registrations with owners..." # Retrieve all applications with their owners in a single API call # The $expand parameter includes owner details to minimize round trips $allApps = Invoke-MtGraphRequest -RelativeUri 'applications?$expand=owners' -ErrorAction Stop $appsWithOwners = $allApps | Where-Object { $_.owners.Count -gt 0 } Write-Verbose "Found $($appsWithOwners.Count) app registrations with owners." # Early exit if no apps with owners are found if ($appsWithOwners.Count -eq 0) { Add-MtTestResultDetail -Result "No app registrations with owners found." return $true } Write-Verbose "Step 2: Collecting unique owner IDs for MFA lookup..." # Use HashSet for efficient duplicate detection in large datasets $uniqueOwnerIdsSet = [System.Collections.Generic.HashSet[string]]::new() foreach ($app in $appsWithOwners) { foreach ($owner in $app.owners) { if ($owner.id) { [void]$uniqueOwnerIdsSet.Add($owner.id) } } } # Convert to array for further processing $uniqueOwnerIds = @($uniqueOwnerIdsSet) Write-Verbose "Found $($uniqueOwnerIds.Count) unique owners to check." Write-Verbose "Step 3: Retrieving MFA registration status for owners..." # Query MFA registration details for all users $userRegistrationResponse = Invoke-MtGraphRequest -RelativeUri 'reports/authenticationMethods/userRegistrationDetails?$select=id,userPrincipalName,userDisplayName,isMfaRegistered' -ErrorAction Stop # Create lookup hashtable $ownerHashTable = @{} $uniqueOwnerIds | ForEach-Object { $ownerHashTable[$_] = $true } # Filter API response to only include relevant owners $relevantUserRegistrations = $userRegistrationResponse | Where-Object { $_.id -and $ownerHashTable.ContainsKey($_.id) } # Build MFA status lookup table for quick access during owner processing $mfaStatusLookup = @{} $validUserDetails = 0 foreach ($userDetail in $relevantUserRegistrations) { $mfaStatusLookup[$userDetail.id] = @{ isMfaRegistered = $userDetail.isMfaRegistered -eq $true userDisplayName = $userDetail.userDisplayName userPrincipalName = $userDetail.userPrincipalName } $validUserDetails++ } Write-Verbose "Retrieved MFA status for $validUserDetails relevant owners." Write-Verbose "Step 4: Analyzing MFA compliance for each owner..." # Pre-allocate collections for better performance in large environments $ownersWithoutMFA = [System.Collections.Generic.List[PSCustomObject]]::new() $skippedOwners = [System.Collections.Generic.List[PSCustomObject]]::new() $totalOwners = 0 $ownersWithMFA = 0 # Process each app and its owners to determine MFA compliance foreach ($app in $appsWithOwners) { foreach ($owner in $app.owners) { $totalOwners++ # Check if we have MFA data for this owner if ($mfaStatusLookup.ContainsKey($owner.id)) { if ($mfaStatusLookup[$owner.id].isMfaRegistered) { $ownersWithMFA++ } else { # Owner found but doesn't have MFA registered $ownersWithoutMFA.Add([PSCustomObject]@{ AppName = $app.displayName AppId = $app.appId OwnerName = $mfaStatusLookup[$owner.id].userDisplayName OwnerUPN = $mfaStatusLookup[$owner.id].userPrincipalName OwnerID = $owner.id MFAMethods = "No MFA registered" }) } } else { # Owner not found in MFA data - likely service principal or disabled user $ownerName = if ($owner.displayName) { $owner.displayName } elseif ($owner.userPrincipalName) { $owner.userPrincipalName } else { "Unknown" } $ownerType = if ($owner.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { "Service Principal" } elseif ($owner.'@odata.type' -eq '#microsoft.graph.user') { "User (possibly disabled)" } else { "Unknown type" } $skippedOwners.Add([PSCustomObject]@{ AppName = $app.displayName AppId = $app.appId OwnerName = $ownerName OwnerUPN = $owner.userPrincipalName OwnerID = $owner.id OwnerType = $ownerType Reason = "Could not retrieve MFA status ($ownerType)" }) Write-Verbose "Owner $ownerName ($ownerType) not found in registration details - likely service principal or disabled user." } } } Write-Verbose "Summary - Apps: $($appsWithOwners.Count), Total owners: $totalOwners, With MFA: $ownersWithMFA, Without MFA: $($ownersWithoutMFA.Count), Skipped: $($skippedOwners.Count)" # Determine test result: pass only if no owners lack MFA $testPassed = ($ownersWithoutMFA.Count -eq 0) # Generate detailed markdown report for the results if ($testPassed) { # All owners have MFA - generate success report $testResultMarkdown = "**Well done!** All app registration owners have MFA registered." if ($totalOwners -gt 0) { $testResultMarkdown += "`n`n**Summary:** Found $($appsWithOwners.Count) applications. All valid owners are registered for MFA.`n`n" # Include information about skipped owners if ($skippedOwners.Count -gt 0) { $testResultMarkdown += "`n`n**Note:** $($skippedOwners.Count) owner(s) could not be checked (service principals or disabled users)." # Detailed breakdown of skipped owners $testResultMarkdown += "`n`n**Skipped Owners:**`n`n| Application | Owner | Type | Reason |`n| --- | --- | --- | --- |`n" foreach ($skippedOwner in $skippedOwners) { $appLink = "[$($skippedOwner.AppName)]($($__MtSession.AdminPortalUrl.Azure)#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$($skippedOwner.AppId))" $ownerDisplay = if ($skippedOwner.OwnerType -like "*User*") { "[$($skippedOwner.OwnerName)]($($__MtSession.AdminPortalUrl.Azure)#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($skippedOwner.OwnerID))" } else { $skippedOwner.OwnerName } $testResultMarkdown += "| $appLink | $ownerDisplay | $($skippedOwner.OwnerType) | $($skippedOwner.Reason) |`n" } } } } else { # Owners without MFA - generate failure report $testResultMarkdown = "**Action Required:** Found $($ownersWithoutMFA.Count) applications with owners who have not registered for Multi-Factor Authentication (MFA).`n`n" # Create table of owners who need to register MFA $testResultMarkdown += "`n`n**App Registration Owners Without MFA:**`n`n| Application | Owner | UPN | MFA Status |`n| --- | --- | --- | --- |`n" foreach ($owner in $ownersWithoutMFA) { # Generate portal links for quick access to fix issues $appLink = "[$($owner.AppName)]($($__MtSession.AdminPortalUrl.Azure)#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$($owner.AppId))" $userLink = "[$($owner.OwnerUPN)]($($__MtSession.AdminPortalUrl.Azure)#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($owner.OwnerID))" $testResultMarkdown += "| $appLink | $($owner.OwnerName) | $userLink | $($owner.MFAMethods) |`n" } # Include skipped owners section if ($skippedOwners.Count -gt 0) { $testResultMarkdown += "`n`n**Skipped Owners (Could Not Check MFA):**`n`n| Application | Owner | Type | Reason |`n| --- | --- | --- | --- |`n" foreach ($skippedOwner in $skippedOwners) { $appLink = "[$($skippedOwner.AppName)]($($__MtSession.AdminPortalUrl.Azure)#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$($skippedOwner.AppId))" # Create user links for actual users only $ownerDisplay = if ($skippedOwner.OwnerType -like "*User*") { "[$($skippedOwner.OwnerName)]($($__MtSession.AdminPortalUrl.Azure)#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($skippedOwner.OwnerID))" } else { $skippedOwner.OwnerName } $testResultMarkdown += "| $appLink | $ownerDisplay | $($skippedOwner.OwnerType) | $($skippedOwner.Reason) |`n" } } } Add-MtTestResultDetail -Result $testResultMarkdown } catch { Write-Error $_.Exception.Message Add-MtTestResultDetail -Result "**Error** checking app registration owners: $($_.Exception.Message)" return $false } return $testPassed } |