When it comes to auditing conditional access policies, the IdPowerToys' Conditional Access Documenter is a powerful tool. However, the app-consent requirement often hinders its usability by presenting reports filled with GUIDs instead of user-friendly display names. In this blog post, I’ll introduce you to idPowerToysCaDocumentHelper.ps1, a versatile PowerShell script that resolves this issue. Join me as we explore how this script enhances the Conditional Access Documenter, making your auditing process more efficient and intuitive.

Discovering the Potential of the IdPowerToys Conditional Access Documenter:

The Conditional Access Documenter from IdPowerToys offers a comprehensive solution for documenting conditional access policies. Its insights help organizations strengthen their security framework. However, the app-consent hurdle can prevent users from fully benefiting from the tool’s capabilities. But it also has a “Manual Generation”, it’s a feature where we can export the conditional access policies to json and paste them here. But…

Unveiling the Challenge: GUIDs vs. Display Names:

Without granting app-consent, the Conditional Access Documenter reports present GUIDs instead of human-readable display names. This limitation complicates the interpretation of the data and hampers effective analysis, diminishing the value of the tool.

Introducing idPowerToysCaDocumentHelper.ps1: Your Solution:

To overcome this challenge, the idPowerToysCaDocumentHelper.ps1 script was developed. This PowerShell script serves as an enhancement to the Conditional Access Documenter, bridging the gap between GUIDs and display names. With its implementation, you can unlock the true potential of the tool without app-consent and streamline your documenting process.

How do I get set up?

  • Requires at least PowerShell 7 and Azure PS modules Az.Resources and Az.Accounts
  • Uses and requires default valid AzContext
  1. Set up the Az module: Begin by installing the Az module, which provides the necessary cmdlets for interacting with Azure Active Directory. You can install the module by running the command: Install-Module -Name Az -Repository PSGallery -Force

  2. Authenticate with Azure AD: Before accessing Azure AD, you need to authenticate with the appropriate permissions. Use the connect-AzAccount cmdlet to sign in with your Azure AD administrator account. To be able to get all the information you need, you need to be a Global Reader

Leveraging idPowerToysCaDocumentHelper.ps1: A Deep Dive:

In this section, we’ll explore the functionality and inner workings of idPowerToysCaDocumentHelper.ps1. We’ll walk through the script’s key components, including the PowerShell cmdlets and functions utilized to retrieve and associate the appropriate display names with the corresponding GUIDs.

Part 1: Tool and Cache Initialization

# https://idpowertoys.com/ca
$Cache = @{}
$Cache.Add('12345678-BBBb-cCCCC-0000-123456789012', 'BreakGlasAccount 1')
$Cache.Add('12345678-BBBb-cCCCC-0000-123456789013', 'BreakGlasAccount 2')

The script begins by initializing the tool and cache. The $Cache variable is a hashtable used to store mappings of GUIDs to display names. The example shows two manual entries in the cache, where specific GUIDs are associated with corresponding display names. These entries can be customized as needed.

Part 2: Retrieving Conditional Access Policies

$Policies = (Invoke-AzRestMethod -Uri 'https://graph.microsoft.com/beta/policies/conditionalAccessPolicies').Content | ConvertFrom-Json

Part 3: Retrieving Directory Role and Locatioons

((Invoke-AzRestMethod 'https://graph.microsoft.com/v1.0/directoryRoleTemplates').Content | ConvertFrom-Json).value | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
((Invoke-AzRestMethod 'https://graph.microsoft.com/v1.0/directoryRoles').Content | ConvertFrom-Json).value | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
((Invoke-AzRestMethod 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations').Content | ConvertFrom-Json).value | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }

In this part, directory role templates are retrieved using a REST API call. The response content is converted from JSON format, and for each role template, the corresponding GUID ($.id) and display name ($.DisplayName) are added to the $Cache hashtable.

And then, “directory roles” and “named locations for conditional access policies” are retrieved the same way.

Part 4: Comment Block for MgGraph (Optional)

<#
    #If using MgGraph
    Connect-MgGraph -AccessToken (Get-AzAccessToken -ResourceUrl 'https://graph.microsoft.com').Token
    $Policies = Invoke-GraphRequest -Uri 'https://graph.microsoft.com/beta/policies/conditionalAccessPolicies'
    Get-MgDirectoryRoleTemplate -All | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
    Get-MgDirectoryRole -All | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
    Get-MgIdentityConditionalAccessNamedLocation -All | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
#>

This section provides an optional code snippet using MgGraph, an alternative to Azure AD cmdlets, for retrieving the policies and related data. It demonstrates how to connect to MgGraph, retrieve policies, and populate the cache hashtable using MgGraph cmdlets.

Part 4: Test-IsGuid function

Source for function https://morgantechspace.com

The Test-IsGuid function is responsible for determining whether a given string is a valid GUID (Globally Unique Identifier) format. It takes a string parameter $StringGuid representing the value to be tested.

This function is used within the script to check if a string represents a GUID before further processing. It helps ensure that only valid GUIDs are considered for resolving display names.

Part 5: Resolve-Objects function

function Resolve-Objects {
    param (
        [string[]]$Guids,
        [Parameter(Mandatory)]
        [ValidateSet('User', 'Role', 'Application', 'Group', 'Location')]
        [string]$ObjType
    )

    if ($Guids.Count -eq 0) {
        # Make sure to return an empty array instead of null
        , @()
    } else {
        $tempRes = foreach ($id in $Guids) {
            if ($Cache.ContainsKey($id)) {
                $Cache[$id]
            } elseif (Test-IsGuid -StringGuid $id) {
                # Code for resolving display names based on object type
            } else {
                # Return clear text if input was not a GUID
                $id
            }
        }
        , @($tempRes | Sort-Object)
    }
}

The Resolve-Objects function is responsible for resolving display names based on a given array of GUIDs. It takes two parameters: $Guids, an array of GUIDs to be resolved, and $ObjType, a string specifying the type of objects associated with the GUIDs (e.g., User, Role, Application, Group, Location).

The function first checks if the $Guids array is empty. If it is, the function returns an empty array to ensure consistent output.

If the $Guids array is not empty, the function iterates over each GUID. It checks if the $Cache hashtable contains the GUID as a key. If it does, the corresponding display name is retrieved from the $Cache hashtable.

If the GUID is not found in the $Cache hashtable, the function calls the Test-IsGuid function to verify whether the input is a valid GUID. If it is a valid GUID, the function performs additional logic to resolve the display name based on the specified object type ($ObjType).

If the input is not a valid GUID, the function returns the original value (clear text).

The resolved display names or original values are collected in the $tempRes array. Finally, the function returns the sorted $tempRes array as the result.

This function is used in the script to resolve display names for various object types associated with conditional access policies, such as users, roles, applications, groups, and locations.

Part 6: The concluding parts

foreach ($Policy in $Policies.value) {
    $Policy.Conditions.users.excludeUsers = Resolve-Objects -Guids $Policy.Conditions.users.excludeUsers -ObjType User
    $Policy.Conditions.users.includeUsers = Resolve-Objects -Guids $Policy.Conditions.users.includeUsers -ObjType User

    $Policy.Conditions.users.includeRoles = Resolve-Objects -Guids $Policy.Conditions.users.includeRoles -ObjType Role
    $Policy.Conditions.users.excludeRoles = Resolve-Objects -Guids $Policy.Conditions.users.excludeRoles -ObjType Role

    $Policy.Conditions.users.excludeGroups = Resolve-Objects -Guids $Policy.Conditions.users.excludeGroups -ObjType Group
    $Policy.Conditions.users.includeGroups = Resolve-Objects -Guids $Policy.Conditions.users.includeGroups -ObjType Group

    if ($Policy.Conditions.Locations) {
        $Policy.Conditions.Locations.excludeLocations = Resolve-Objects -Guids $Policy.Conditions.Locations.excludeLocations -ObjType Location
        $Policy.Conditions.Locations.includeLocations = Resolve-Objects -Guids $Policy.Conditions.Locations.includeLocations -ObjType Location
    }

    $Policy.Conditions.applications.excludeApplications = Resolve-Objects -Guids $Policy.Conditions.applications.excludeApplications -ObjType Application
    $Policy.Conditions.applications.includeApplications = Resolve-Objects -Guids $Policy.Conditions.applications.includeApplications -ObjType Application
}

$Policies | ConvertTo-Json -Depth 9 -Compress | Set-Clipboard

In this part of the script, there is a loop that iterates over each conditional access policy retrieved earlier from the $Policies variable.

For each policy, the script calls the Resolve-Objects function to resolve the display names of various objects associated with the policy’s conditions, such as users, roles, groups, locations, and applications. The resolved display names are then assigned back to the corresponding properties of the policy’s Conditions object.

Here’s a breakdown of the lines:

  • Resolve the display names for the users' exclusion and inclusion lists, roles' exclusion and inclusion lists, and groups' exclusion and inclusion lists using the Resolve-Objects function. The GUID arrays are passed as arguments, and the object type (User, Role, or Group) is specified to correctly resolve the display names.

  • If the policy has location conditions, resolve the display names for the exclusion and inclusion lists of locations using the Resolve-Objects function. The GUID arrays are passed as arguments, and the object type Location is specified.

  • Resolve the display names for the exclusion and inclusion lists of applications using the Resolve-Objects function. The GUID arrays are passed as arguments, and the object type Application is specified.

Finally, the updated policies are converted to JSON format using ConvertTo-Json and compressed. The resulting JSON is then copied to the clipboard using Set-Clipboard. This allows you to easily paste the JSON representation of the policies elsewhere, such as in a text editor or the Conditional Access Documenter from IdPowerToys.

Full script

# https://idpowertoys.com/ca
$Cache = @{}
# If You have any objects that you want to have a specific Displayname you can add them here
$Cache.Add('12345678-BBBb-cCCCC-0000-123456789012', 'BreakGlasAccount 1')
$Cache.Add('12345678-BBBb-cCCCC-0000-123456789013', 'BreakGlasAccount 2')

$Policies = (Invoke-AzRestMethod -Uri 'https://graph.microsoft.com/beta/policies/conditionalAccessPolicies').Content | ConvertFrom-Json 

((Invoke-AzRestMethod 'https://graph.microsoft.com/v1.0/directoryRoleTemplates').Content | ConvertFrom-Json).value | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
((Invoke-AzRestMethod 'https://graph.microsoft.com/v1.0/directoryRoles').Content | ConvertFrom-Json).value | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
((Invoke-AzRestMethod 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations').Content | ConvertFrom-Json).value | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }

<#
    #If using MgGraph 
    Connect-MgGraph -AccessToken (Get-AzAccessToken -ResourceUrl 'https://graph.microsoft.com').Token
    $Policies = Invoke-GraphRequest -Uri 'https://graph.microsoft.com/beta/policies/conditionalAccessPolicies'
    Get-MgDirectoryRoleTemplate -All | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
    Get-MgDirectoryRole -All | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
    Get-MgIdentityConditionalAccessNamedLocation -All | ForEach-Object { $Cache.Add($_.id, $_.DisplayName) }
#>

function Test-IsGuid {
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]$StringGuid
    )
 
    $ObjectGuid = [System.Guid]::empty
    return [System.Guid]::TryParse($StringGuid, [System.Management.Automation.PSReference]$ObjectGuid) # Returns True if successfully parsed
}

function Resolve-Objects {
    param (
        [string[]]$Guids,
        [Parameter(Mandatory)]
        [ValidateSet('User', 'Role', 'Application', 'Group', 'Location')]
        [string]$ObjType
    )

    if ($Guids.Count -eq 0) {
        # Make sure to return an empty array instead of null
        , @()
    } else {
        $tempRes = foreach ($id in $Guids) {
            if ($Cache.ContainsKey($id)) {
                $Cache[$id] 
            } elseif (Test-IsGuid -StringGuid $id) {
                # There are some cases where the values is not a guid like usertype
                switch ($ObjType) {
                    'User' { 
                        $Test = Get-AzADUser -ObjectId $id -ErrorAction SilentlyContinue
                        $DisplayName = $Test ? $Test.DisplayName :  $id
                    }
                    'Group' {
                        $Test = Get-AzADGroup -ObjectId $id -ErrorAction SilentlyContinue
                        $DisplayName = $Test ? $Test.DisplayName :  $id
                    }
                    'Role' {
                        $DisplayName = $id
                    }
                    'Location' {
                        $DisplayName = $id
                    }
                    'Application' {
                        $Test = Get-AzADApplication -ApplicationId $id -ErrorAction SilentlyContinue
                        if ([string]::IsNullOrEmpty($Test)) {
                            $Test = Get-AzADServicePrincipal -ApplicationId $id -ErrorAction SilentlyContinue
                        }
                        $DisplayName = $Test ? $Test.DisplayName : "Unknown - $id"
                    }
                    Default {}
                }
                $DisplayName
                # Add data to cache so we dont need to check it more times
                $Cache.Add($id, $DisplayName)
            } else {
                # Return cleear text if input was not a guid
                $id
            }
        }
        , @($tempRes | Sort-Object)
    }
}

foreach ($Policy in $Policies.value) {
    $Policy.Conditions.users.excludeUsers = Resolve-Objects -Guids $Policy.Conditions.users.excludeUsers -ObjType User
    $Policy.Conditions.users.includeUsers = Resolve-Objects -Guids $Policy.Conditions.users.includeUsers -ObjType User
    
    $Policy.Conditions.users.includeRoles = Resolve-Objects -Guids $Policy.Conditions.users.includeRoles -ObjType Role
    $Policy.Conditions.users.excludeRoles = Resolve-Objects -Guids $Policy.Conditions.users.excludeRoles -ObjType Role
    
    $Policy.Conditions.users.excludeGroups = Resolve-Objects -Guids $Policy.Conditions.users.excludeGroups -ObjType Group
    $Policy.Conditions.users.includeGroups = Resolve-Objects -Guids $Policy.Conditions.users.includeGroups -ObjType Group
    
    if ($Policy.Conditions.Locations) {
        $Policy.Conditions.Locations.excludeLocations = Resolve-Objects -Guids $Policy.Conditions.Locations.excludeLocations -ObjType Location
        $Policy.Conditions.Locations.includeLocations = Resolve-Objects -Guids $Policy.Conditions.Locations.includeLocations -ObjType Location
    }
    
    $Policy.Conditions.applications.excludeApplications = Resolve-Objects -Guids $Policy.Conditions.applications.excludeApplications -ObjType Application
    $Policy.Conditions.applications.includeApplications = Resolve-Objects -Guids $Policy.Conditions.applications.includeApplications -ObjType Application
}

$Policies | ConvertTo-Json -Depth 9 -Compress | Set-Clipboard

Conclusion:

By incorporating the idPowerToysCaDocumentHelper.ps1 script into your workflow, you can overcome the limitations posed by app-consent in the IdPowerToys' Conditional Access Documenter. The script’s ability to translate GUIDs into meaningful display names elevates the tool’s usability, making your auditing process more efficient and user-friendly. Embrace the power of PowerShell and optimize your conditional access policy management with this invaluable script.

Disclaimer 1

It’s important to note that the “Conditional Access Documenter” tool itself has the capability to translate GUIDs to display names seamlessly and efficiently. If you are able and willing to grant the necessary app-consent, utilizing the tool directly is the recommended approach. The script, idPowerToysCaDocumentHelper.ps1, is intended as a workaround for situations where app-consent cannot or will not be granted. It serves as a helpful alternative to bridge the gap and provide the desired translation of GUIDs to display names within the Conditional Access Documenter reports. Please use this script responsibly and in accordance with your organization’s policies and guidelines.

Disclaimer 2

I would like to acknowledge that ChatGPT, an AI language model, assisted me in generating this blog post. While the content has been carefully reviewed and tailored to the best of my abilities, it is essential to recognize that the information provided here is based on the knowledge and understanding available up until September 2021. The efficacy and relevance of the idPowerToysCaDocumentHelper.ps1 script and the Conditional Access Documenter tool may vary over time, as updates and changes are made to the software. Therefore, it is advisable to ensure that you have the latest version of both the tool and the script before implementation. I encourage readers to exercise their discretion and conduct additional research or consult with relevant experts for specific and up-to-date guidance.