Collectors/ConditionalAccess.ps1

@{
    Name           = 'ConditionalAccess'
    FileName       = 'conditionalAccess'
    ApiVersion     = 'v1.0'
    RequiredScopes = @('Policy.Read.All')
    Description    = 'Conditional Access policies with resolved display names for users, groups, roles, applications and locations.'
    Collect        = {
        $policies = @(Invoke-TLGraphRequest -Uri '/v1.0/identity/conditionalAccess/policies' -All)

        # Role template id -> display name (CA references roles by template id)
        $roleNames = @{}
        try {
            foreach ($roleDefinition in @(Invoke-TLGraphRequest -Uri '/v1.0/roleManagement/directory/roleDefinitions?$select=templateId,displayName' -All)) {
                $roleNames[[string]$roleDefinition.templateId] = [string]$roleDefinition.displayName
            }
        }
        catch { Write-Verbose ("Role definitions unavailable for name resolution: {0}" -f $_.Exception.Message) }

        # Named location id -> display name
        $locationNames = @{}
        try {
            foreach ($location in @(Invoke-TLGraphRequest -Uri '/v1.0/identity/conditionalAccess/namedLocations?$select=id,displayName' -All)) {
                $locationNames[[string]$location.id] = [string]$location.displayName
            }
        }
        catch { Write-Verbose ("Named locations unavailable for name resolution: {0}" -f $_.Exception.Message) }

        # Gather every referenced id once, resolve in batches
        $directoryIds = [System.Collections.Generic.List[string]]::new()
        $applicationIds = [System.Collections.Generic.List[string]]::new()
        foreach ($policy in $policies) {
            $conditions = $policy.conditions
            if ($null -eq $conditions) { continue }
            if ($conditions.PSObject.Properties['users'] -and $conditions.users) {
                foreach ($id in @($conditions.users.includeUsers) + @($conditions.users.excludeUsers) +
                    @($conditions.users.includeGroups) + @($conditions.users.excludeGroups)) {
                    if ($id) { $directoryIds.Add([string]$id) }
                }
            }
            if ($conditions.PSObject.Properties['applications'] -and $conditions.applications) {
                foreach ($id in @($conditions.applications.includeApplications) + @($conditions.applications.excludeApplications)) {
                    if ($id) { $applicationIds.Add([string]$id) }
                }
            }
        }
        $directoryMap = Resolve-TLObjectId -Id $directoryIds.ToArray()
        $applicationMap = Resolve-TLAppId -AppId $applicationIds.ToArray()

        function Resolve-DirectoryNames {
            param([object]$Ids, [hashtable]$Map)
            @(foreach ($id in @($Ids | Where-Object { $_ })) {
                    if ($Map.ContainsKey([string]$id)) { $Map[[string]$id].DisplayName } else { [string]$id }
                })
        }
        function Resolve-SimpleNames {
            param([object]$Ids, [hashtable]$Map)
            @(foreach ($id in @($Ids | Where-Object { $_ })) {
                    if ($Map.ContainsKey([string]$id)) { $Map[[string]$id] } else { [string]$id }
                })
        }

        foreach ($policy in $policies) {
            $conditions = $policy.conditions
            $users = $null; $applications = $null; $locations = $null
            if ($conditions) {
                if ($conditions.PSObject.Properties['users']) { $users = $conditions.users }
                if ($conditions.PSObject.Properties['applications']) { $applications = $conditions.applications }
                if ($conditions.PSObject.Properties['locations']) { $locations = $conditions.locations }
            }
            $resolved = [ordered]@{
                usersInclude     = @(); usersExclude = @()
                groupsInclude    = @(); groupsExclude = @()
                rolesInclude     = @(); rolesExclude = @()
                appsInclude      = @(); appsExclude = @()
                locationsInclude = @(); locationsExclude = @()
            }
            if ($users) {
                $resolved['usersInclude'] = Resolve-DirectoryNames -Ids $users.includeUsers -Map $directoryMap
                $resolved['usersExclude'] = Resolve-DirectoryNames -Ids $users.excludeUsers -Map $directoryMap
                $resolved['groupsInclude'] = Resolve-DirectoryNames -Ids $users.includeGroups -Map $directoryMap
                $resolved['groupsExclude'] = Resolve-DirectoryNames -Ids $users.excludeGroups -Map $directoryMap
                $resolved['rolesInclude'] = Resolve-SimpleNames -Ids $users.includeRoles -Map $roleNames
                $resolved['rolesExclude'] = Resolve-SimpleNames -Ids $users.excludeRoles -Map $roleNames
            }
            if ($applications) {
                $resolved['appsInclude'] = Resolve-SimpleNames -Ids $applications.includeApplications -Map $applicationMap
                $resolved['appsExclude'] = Resolve-SimpleNames -Ids $applications.excludeApplications -Map $applicationMap
            }
            if ($locations) {
                $resolved['locationsInclude'] = Resolve-SimpleNames -Ids $locations.includeLocations -Map $locationNames
                $resolved['locationsExclude'] = Resolve-SimpleNames -Ids $locations.excludeLocations -Map $locationNames
            }
            # Raw data stays untouched; resolution is an additive extra field.
            Add-Member -InputObject $policy -NotePropertyName '_tlResolved' -NotePropertyValue ([pscustomobject]$resolved) -Force
        }

        , @($policies)
    }
}