tests/Test-Assessment.21877.ps1

<#
.SYNOPSIS
Tests if all guest users have assigned sponsors in the tenant.
#>


function Test-Assessment-21877 {
    [ZtTest(
        Category = 'Application management',
        ImplementationCost = 'Medium',
        MinimumLicense = ('Free'),
        Pillar = 'Identity',
        RiskLevel = 'Medium',
        SfiPillar = 'Protect tenants and isolate production systems',
        TenantType = ('Workforce','External'),
        TestId = 21877,
        Title = 'All guests have a sponsor',
        UserImpact = 'Medium'
    )]
    [CmdletBinding()]
    param(
        $Database
    )

    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    if ((Get-MgContext).Environment -ne 'Global') {
        Write-PSFMessage "This test is only applicable to the Global environment." -Tag Test -Level VeryVerbose
        return
    }

    $activity = "Checking All guests have a sponsor"
    Write-ZtProgress -Activity $activity -Status "Getting guest users"

    # Get all guest users
    $sqlGuests = @"
SELECT id, userPrincipalName, displayName, userType
FROM User
WHERE userType = 'Guest'
"@

    $guestUsers = @(Invoke-DatabaseQuery -Database $Database -Sql $sqlGuests)
    $totalGuestCount = $guestUsers.Count

    # Early return if no guests exist
    if ($totalGuestCount -eq 0) {
        $testParams = @{
            TestId             = '21877'
            Title              = 'All guests have a sponsor'
            UserImpact         = 'Medium'
            Risk               = 'Medium'
            ImplementationCost = 'Medium'
            AppliesTo          = 'Identity'
            Tag                = 'Identity'
            Status             = $true
            Result             = "✅ No guest accounts found in the tenant."
        }
        Add-ZtTestResultDetail @testParams
        return
    }

    Write-ZtProgress -Activity $activity -Status "Checking sponsors for $totalGuestCount guest users"

    $guestsWithoutSponsors = [System.Collections.Generic.List[object]]::new()
    $sponsorlessLimit = 20
    $lookupFailures = [System.Collections.Generic.List[string]]::new()
    $lookupFailureCount = 0

    for ($offset = 0; $offset -lt $totalGuestCount -and $guestsWithoutSponsors.Count -lt $sponsorlessLimit; $offset += 19) {
        $lastIndex = [Math]::Min($offset + 18, $totalGuestCount - 1)
        $batchGuests = @($guestUsers[$offset..$lastIndex])
        $processedGuestIds = [System.Collections.Generic.HashSet[string]]::new()
        try {
            Invoke-ZtGraphBatchRequest -Path 'users/{0}?$select=id,displayName,userPrincipalName,userType&$expand=sponsors($select=id)' -ArgumentList $batchGuests -Properties id -ApiVersion 'v1.0' -Raw -ErrorAction Stop | ForEach-Object {
                $batchResult = $_
                $guest = $batchGuests[[int]$batchResult.id - 1]
                [void]$processedGuestIds.Add([string]$guest.id)
                if ($batchResult.status -lt 200 -or $batchResult.status -ge 300) {
                    $lookupFailureCount++
                    if ($lookupFailures.Count -lt 20) {
                        $errorCode = $batchResult.body.error.code
                        $lookupFailures.Add("Guest $($guest.id): HTTP $($batchResult.status)$(if ($errorCode) { " ($errorCode)" })")
                    }
                }
                elseif (-not $batchResult.body) {
                    $lookupFailureCount++
                    if ($lookupFailures.Count -lt 20) {
                        $lookupFailures.Add("Guest $($guest.id): empty response")
                    }
                }
                elseif ((-not $batchResult.body.sponsors -or $batchResult.body.sponsors.Count -eq 0) -and $guestsWithoutSponsors.Count -lt $sponsorlessLimit) {
                    $guestsWithoutSponsors.Add($batchResult.body)
                }
            }
        }
        catch {
            foreach ($guest in $batchGuests) {
                if (-not $processedGuestIds.Contains([string]$guest.id)) {
                    $lookupFailureCount++
                    if ($lookupFailures.Count -lt 20) {
                        $lookupFailures.Add("Guest $($guest.id): batch request failed")
                    }
                }
            }
            break
        }
    }

    $guestsWithoutSponsorsCount = $guestsWithoutSponsors.Count
    $passed = $guestsWithoutSponsorsCount -eq 0

    # Build result markdown
    if ($lookupFailureCount -gt 0 -and $passed) {
        $testResultMarkdown = "Sponsor lookup incomplete for $lookupFailureCount request(s) (up to 20 shown): $($lookupFailures -join '; '). No confirmed guests without sponsors."
    }
    elseif ($passed) {
        $testResultMarkdown = "✅ All guest accounts in the tenant have an assigned sponsor."
    }
    else {
        # Build table rows efficiently using StringBuilder
        $tableRowsBuilder = [System.Text.StringBuilder]::new()

        foreach ($guest in $guestsWithoutSponsors) {
            $portalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/{0}/hidePreviewBanner~/true' -f $guest.id
            $displayName = Get-SafeMarkdown $guest.displayName
            [void]$tableRowsBuilder.AppendLine("| [$displayName]($portalLink) | $($guest.userPrincipalName) |")
        }

        $detailedReport = @"
## Guest users without sponsors
 
- Total count of guests in the tenant: $totalGuestCount
- $(if ($guestsWithoutSponsorsCount -eq $sponsorlessLimit) { 'Found at least 20 without sponsors' } elseif ($lookupFailureCount -gt 0) { "Confirmed $guestsWithoutSponsorsCount without sponsors" } else { "Found $guestsWithoutSponsorsCount without sponsors" })
 
| User Display Name | User Principal Name |
| :---------------- | :------------------ |
$($tableRowsBuilder.ToString())
"@


        $testResultMarkdown = "❌ One or more guest accounts have no sponsor recorded.`n`n$detailedReport"
        if ($lookupFailureCount -gt 0) {
            $testResultMarkdown += "`n`nSponsor lookup incomplete for $lookupFailureCount request(s) (up to 20 shown): $($lookupFailures -join '; ')."
        }
    }

    $testParams = @{
        TestId             = '21877'
        Title              = 'All guests have a sponsor'
        UserImpact         = 'Medium'
        Risk               = 'Medium'
        ImplementationCost = 'Medium'
        AppliesTo          = 'Identity'
        Tag                = 'Identity'
        Status             = $passed
        Result             = $testResultMarkdown
    }
    if ($lookupFailureCount -gt 0 -and $passed) {
        $testParams.CustomStatus = 'Error'
    }
    Add-ZtTestResultDetail @testParams
}