private/tenantinfo/ai/Add-ZtAgentOwnershipDistribution.ps1

<#
.SYNOPSIS
    Adds the agent identity owner and effective sponsor distribution to tenant information.
#>


function Add-ZtAgentOwnershipDistribution {
    [CmdletBinding()]
    param(
        $Database
    )

    $tenantInfoName = 'AgentOwnershipDistribution'
    $activity = 'Getting agent ownership distribution'
    Write-ZtProgress -Activity $activity -Status 'Processing'

    try {
        $rows = @(Invoke-DatabaseQuery -Database $Database -Sql @"
with agent_owners as (
    select
        id,
        owners
    from main.ServicePrincipal
    where "@odata.type" = '#microsoft.graph.agentIdentity'
)
select
    coalesce(ai.id, sp.id) as id,
    ai.displayName,
    ai.accountEnabled,
    to_json(ai.sponsors) as sponsorsJson,
    ai.id is not null as hasSponsorSnapshot,
    sp.id is not null as hasOwnerSnapshot,
    case
        when sp.owners is null then 0
        when json_type(sp.owners) = 'ARRAY' then coalesce(json_array_length(sp.owners), 0)
        when json_type(sp.owners) = 'OBJECT' then 1
        else 0
    end as ownerCount
from main.AgentIdentity ai
full outer join agent_owners sp on ai.id = sp.id
order by coalesce(ai.displayName, '')
"@
)
    }
    catch {
        Write-PSFMessage "Failed to query agent ownership distribution: $_" -Tag Test -Level Warning -ErrorRecord $_
        Add-ZtTenantInfo -Name $tenantInfoName -Value $null
        return
    }

    $snapshotMismatches = @($rows | Where-Object {
        -not ($_.hasSponsorSnapshot -and $_.hasOwnerSnapshot)
    })
    $matchedRows = @($rows | Where-Object {
        $_.hasSponsorSnapshot -and $_.hasOwnerSnapshot
    })

    if ($snapshotMismatches.Count -gt 0) {
        Write-PSFMessage "$($snapshotMismatches.Count) agent identities were excluded because the owner and sponsor snapshots did not match." -Tag Test -Level Warning
    }

    $agentIdentities = @($matchedRows | ForEach-Object {
        $sponsors = if ($_.sponsorsJson -and $_.sponsorsJson -ne 'null') {
            @($_.sponsorsJson | ConvertFrom-Json)
        }
        else {
            @()
        }

        [PSCustomObject]@{
            Id             = $_.id
            DisplayName    = $_.displayName
            AccountEnabled = $_.accountEnabled
            HasOwner       = [int]$_.ownerCount -ge 1
            Sponsors       = $sponsors
        }
    })

    $uniqueGroupIds = @($agentIdentities |
        ForEach-Object { $_.Sponsors } |
        Where-Object {
            $null -ne $_ -and (
                $_.'@odata.type' -eq '#microsoft.graph.group' -or
                ($null -eq $_.'@odata.type' -and $null -ne $_.PSObject.Properties['mailEnabled'])
            )
        } |
        Select-Object -ExpandProperty id -Unique)

    $groupHasMembers = @{}
    if ($uniqueGroupIds.Count -gt 0) {
        try {
            $groupCountResults = @(Invoke-ZtGraphBatchRequest `
                -Path 'groups/{0}/transitiveMembers/$count' `
                -ArgumentList $uniqueGroupIds `
                -Header @{ 'ConsistencyLevel' = 'eventual' } `
                -NoPaging `
                -Matched `
                -ErrorAction Stop)

            $expectedGroupIds = @{}
            foreach ($groupId in $uniqueGroupIds) {
                $expectedGroupIds[[string]$groupId] = $true
            }
            $groupResolutionFailed = $false

            foreach ($countResult in @($groupCountResults)) {
                if (-not $countResult.Success) {
                    $groupResolutionFailed = $true
                    Write-PSFMessage "Failed to resolve a sponsor group member count (status $($countResult.Status))." -Tag Test -Level Warning
                    continue
                }

                $groupId = [string]$countResult.Argument
                if (-not $expectedGroupIds.ContainsKey($groupId) -or $groupHasMembers.ContainsKey($groupId)) {
                    $groupResolutionFailed = $true
                    continue
                }

                $groupHasMembers[$groupId] = ([int]($countResult.Result | Select-Object -First 1) -gt 0)
            }

            if ($groupResolutionFailed -or $groupHasMembers.Count -ne $uniqueGroupIds.Count) {
                Write-PSFMessage 'Agent ownership distribution was omitted because one or more sponsor groups could not be resolved.' -Tag Test -Level Warning
                Add-ZtTenantInfo -Name $tenantInfoName -Value $null
                return
            }
        }
        catch {
            Write-PSFMessage "Failed to resolve agent sponsor group membership: $_" -Tag Test -Level Warning -ErrorRecord $_
            Add-ZtTenantInfo -Name $tenantInfoName -Value $null
            return
        }
    }

    $agentsByBucket = @{
        ownerAndSponsor = [System.Collections.Generic.List[object]]::new()
        ownerOnly       = [System.Collections.Generic.List[object]]::new()
        sponsorOnly     = [System.Collections.Generic.List[object]]::new()
        neither         = [System.Collections.Generic.List[object]]::new()
    }

    foreach ($agentIdentity in $agentIdentities) {
        $hasSponsor = $false
        foreach ($sponsor in @($agentIdentity.Sponsors | Where-Object { $null -ne $_ })) {
            $odataType = $sponsor.'@odata.type'
            if (-not $odataType) {
                $odataType = if ($null -ne $sponsor.PSObject.Properties['mailEnabled']) {
                    '#microsoft.graph.group'
                }
                else {
                    '#microsoft.graph.user'
                }
            }

            if ($odataType -eq '#microsoft.graph.user' -or
                ($odataType -eq '#microsoft.graph.group' -and $groupHasMembers[$sponsor.id])) {
                $hasSponsor = $true
                break
            }
        }

        $bucket = if ($agentIdentity.HasOwner -and $hasSponsor) {
            'ownerAndSponsor'
        }
        elseif ($agentIdentity.HasOwner) {
            'ownerOnly'
        }
        elseif ($hasSponsor) {
            'sponsorOnly'
        }
        else {
            'neither'
        }

        $agentsByBucket[$bucket].Add([PSCustomObject]@{
            displayName    = $agentIdentity.DisplayName
            accountEnabled = $agentIdentity.AccountEnabled
        })
    }

    $distribution = [PSCustomObject]@{
        ownerAndSponsor = $agentsByBucket.ownerAndSponsor.Count
        ownerOnly       = $agentsByBucket.ownerOnly.Count
        sponsorOnly     = $agentsByBucket.sponsorOnly.Count
        neither         = $agentsByBucket.neither.Count
        skippedCount    = $snapshotMismatches.Count
        agents           = [PSCustomObject]@{
            ownerAndSponsor = @($agentsByBucket.ownerAndSponsor | Sort-Object displayName)
            ownerOnly       = @($agentsByBucket.ownerOnly | Sort-Object displayName)
            sponsorOnly     = @($agentsByBucket.sponsorOnly | Sort-Object displayName)
            neither         = @($agentsByBucket.neither | Sort-Object displayName)
        }
    }

    Add-ZtTenantInfo -Name $tenantInfoName -Value $distribution
    Write-ZtProgress -Activity $activity -Status 'Completed'
}