Report on Named Locations using PowerShell

Named Locations are convenient to use, but obtaining a clear overview from within the Conditional Access panes can be challenging. In this blog post, I will show how you can create a nice overview and report of them 🙂

What are Named Locations?

“Admins can create policies that target specific network locations as a signal along with other conditions in their decision making process. They can include or exclude these network locations as part of their policy configuration. These network locations might include public IPv4 or IPv6 network information, countries or regions, unknown areas that don’t map to specific countries or regions, or Global Secure Access compliant network.”

Source: Conditional Access Policy: Using Network Signals – Microsoft Entra ID | Microsoft Learn

What does the script do?

For auditing of existing environments, I regularly have to create an overview of Named Locations and their properties (Countries, IP Ranges, Trusted or not, etc.) That’s why I created this script to provide a clear overview while documenting in a filterable Out-ConsoleGridView or in an Excel sheet for the report I have to write. It checks all Conditional Access policies to determine if they’re used as an include or an exclude, what IP Ranges were used, and so on.

This script is based on and inspired by the one Ali Tajran created, which can be found here: https://www.alitajran.com/export-conditional-access-named-locations-powershell/.

By default, without using the -FileName Parameter, it will output the results to the screen like this: (This is just an example, the IP Addresses are not my home IP Addresses 😀 )

When using the -FileName Parameter, you can specify an .XLSX file, and the output will look like this:

To run this, you need “Policy.Read.All” permissions. Also, you need to be a member of one of the following Entra ID Roles: Security Reader, Company Administrator, Security Administrator, Conditional Access Administrator, Global Reader, Devices Admin, or Entra Network Access Administrator.

Note: Because I use the Out-ConsoleGridView cmdlet, which is part of the Microsoft.PowerShell.ConsoleGuiTools Module, the script only works in PowerShell v7 and higher.

Wrapping up

And that’s how you can create a nice, filterable overview of your Named Locations, which is easy to search in, or an Excel sheet that you can use for documentation. Have a lovely weekend!

The script

Save the contents of the scripts below to, for example, c:\scripts\Get-NamedLocations.ps1 and run it in a PowerShell v7 session.

#Inspired by and based on https://www.alitajran.com/export-conditional-access-named-locations-powershell/
param (
    [Parameter(Mandatory = $false)][string]$FileName
)

#Check if required Microsoft.Graph.Authentication, and Microsoft.Graph.Identity.SignIns Modules are installed
#and install them if needed
if (-not (Get-InstalledModule Microsoft.Graph.Authentication, Microsoft.Graph.Identity.SignIns)) {
    try {
        Install-Module -Name Microsoft.Graph.Authentication, Microsoft.Graph.Identity.SignIns -Scope CurrentUser -ErrorAction Stop
    }
    catch {
        Write-Warning ("Error installing required modules, exiting...")
        return
    }
}

#Connect using Microsoft Graph
try {
    Connect-MgGraph -Scopes Policy.Read.All -NoWelcome -ErrorAction Stop
} 
catch {
    Write-Warning ("Error connecting, check connection/permissions. Exiting...")
    return
}

#Retrieve all Named Locations and Conditional Access Policies
try {
    $NamedLocations = Get-MgIdentityConditionalAccessNamedLocation -All -ErrorAction Stop
    $Policies = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction Stop
}
catch {
    Write-Warning ("Could not retrieve Conditional Access Policies, check permissions!")
    Write-Warning ("Specified account should be member of one of the following roles:")
    Write-Warning ("Security Reader, Company Administrator, Security Administrator, Conditional Access Administrator, Global Reader, Devices Admin, Entra Network Access Administrator")
    Write-Warning ("Exiting...")
    return
}

#Exit if no Named Locations were found
if ($null -eq $NamedLocations) {
    Write-Warning ("No Named Locations were found in this tenant, exiting...")
    return
}

#Get all specific cultures and store them in $cultures
$cultures = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::SpecificCultures)

#Create a dictionary for country code to full country name mapping
$countryNames = @{}
foreach ($culture in $cultures) {
    $region = [System.Globalization.RegionInfo]::new($culture.Name)
    if (-not $countryNames.ContainsKey($region.TwoLetterISORegionName)) {
        $countryNames[$region.TwoLetterISORegionName] = $region.EnglishName
    }
}

#Loop through all Named Locations and store information in $Total
$total = foreach ($NamedLocation in $NamedLocations) {
    #Determine if a lookup method is being used
    $LookupMethod = if ($NamedLocation.AdditionalProperties.countryLookupMethod -eq 'authenticatorAppGps') { "Authenticator App GPS" } else { "Client IP Address" }
    if ($null -eq $LookupMethod) { $LookupMethod = 'N.A.' }

    #Determine if IP Ranges were used
    $IPRanges = if ($NamedLocation.AdditionalProperties.ipRanges) { "$($NamedLocation.AdditionalProperties.ipRanges.cidrAddress)" } else { "N.A." }

    #Determine in which Conditional Access Policy the Named Location was used, if any
    $UsedExclude = @()
    $UsedInclude = @()
    foreach ($Policy in $Policies) {
        if ($Policy.Conditions.Locations.ExcludeLocations | Select-String $NamedLocation.Id) {
            $UsedExclude += $Policy.DisplayName
        }
        if ($Policy.Conditions.Locations.IncludeLocations | Select-String $NamedLocation.Id) {
            $UsedInclude += $Policy.DisplayName
        }
    }

    #Prepare a list to hold country names
    $Countries = [System.Collections.Generic.List[string]]::new()
    foreach ($CountryCode in $NamedLocation.AdditionalProperties.countriesAndRegions) {
        if ($CountryNames.ContainsKey($CountryCode)) {
            $Countries.Add($CountryNames[$CountryCode])
        }
        else {
            $Countries.Add($CountryCode)
        }
    }

    #Create a list of findings
    [PSCustomObject]@{
        Name                            = $NamedLocation.DisplayName
        Type                            = if ($NamedLocation.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { "Country" } else { "IP Ranges" }
        Trusted                         = if ($NamedLocation.AdditionalProperties.isTrusted) { "True" } else { "False" }
        LookupMethod                    = $LookupMethod
        'Unknown Countries and Regions' = if (-not ($NamedLocation.AdditionalProperties.includeUnknownCountriesAndRegions)) { "False" } else { "True" }
        "IP Ranges"                     = $IPRanges -replace ' ', ', '
        Countries                       = if ($Countries.Length -gt 0) { $Countries -join ', ' } else { "None" }
        "Excluded in CA Policy"         = if ($UsedExclude.Length -gt 0) { $UsedExclude -join ', ' } else { "N.A." }
        "Included in CA Policy"         = if ($UsedInclude.Length -gt 0) { $UsedInclude -join ', ' } else { "N.A." }
        Created                         = $NamedLocation.CreatedDateTime
        Modified                        = $NamedLocation.ModifiedDateTime
    }
}

#Display results in a Console GridView, output to an XLSX file is $Filename was used
try {
    Import-Module Microsoft.PowerShell.ConsoleGuiTools -ErrorAction Stop
    $Total | Sort-Object Name | Out-ConsoleGridView -Title 'List of all Named Locations, press Esc to exit'
}
catch {
    Write-Warning ("The Microsoft.PowerShell.ConsoleGuiTools was not found, installing now...")
    try {
        Install-Module -Name Microsoft.PowerShell.ConsoleGuiTools -Scope CurrentUser -Force:$true -ErrorAction Stop 
        $Total | Sort-Object Name | Out-ConsoleGridView -Title 'List of all Named Locations, press Esc to exit'
    }
    catch {
        Write-Warning ("Could not install the Microsoft.PowerShell.ConsoleGuiTools Module, results will not be outputted on screen")
        Write-Warning ("Alternatively, you can use the -FileName Parameter to export the results to Excel")
    }
}

if ($FileName) {
    if ($FileName.EndsWith('.xlsx')) {
        try {
            #Test path and remove empty file afterwards because the XLSX is corrupted if not
            New-Item -Path $FileName -ItemType File -Force:$true -Confirm:$false -ErrorAction Stop | Out-Null
            Remove-Item -Path $FileName -Force:$true -Confirm:$false | Out-Null
            
            #Install ImportExcel module if needed
            if (-not (Get-Module -ListAvailable | Where-Object Name -Match ImportExcel)) {
                try {
                    Write-Warning ("`nImportExcel PowerShell Module was not found, installing...")
                    Install-Module ImportExcel -Scope CurrentUser -Force:$true -ErrorAction Stop
                    Import-Module ImportExcel -ErrorAction Stop
                }
                catch {
                    Write-Warning ("Could not install ImportExcel PowerShell Module, exiting...")
                    return
                }
            }
        
            #Export results to path
            $Total | Sort-Object Name | Export-Excel -AutoSize -AutoFilter -Path $FileName
            Write-Host ("`nExported above results to {0}" -f $FileName) -ForegroundColor Green
        }
        catch {
            Write-Warning ("`nCould not export results to {0}, check path and permissions" -f $FileName)
            return
        }
    }
    else {
        Write-Warning ("Specified Filename {0} doesn't end with .xlsx, skipping creation of Excel file" -f $FileName)
        return
    }
}

Download the script(s) from GitHub here.

Leave a ReplyCancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.