Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Expanding Conditional Access Monitoring: Adding Named Location Change Detection


In my previous post about monitoring Conditional Access policy changes, I demonstrated how to close operational blind spots by proactively detecting policy modifications.

This is a critical oversight I needed to address. Named Locations are just as crucial to your Conditional Access security posture as the policies themselves. When someone modifies your trusted IP ranges or changes your approved country lists, you need to know about it immediately.

Why Named Locations Matter

Named Locations in Microsoft Entra ID define the network boundaries for your Conditional Access policies. They come in two types:

  • IP Named Locations: Define trusted or untrusted IP address ranges (corporate offices, VPN endpoints, etc.)
  • Country Named Locations: Define approved or blocked countries and continents for access

These locations are referenced by your Conditional Access policies to make trust decisions. If an attacker modifies your "Trusted Corporate IPs" location to include their malicious infrastructure, they can bypass your MFA requirements. Similarly, if someone accidentally removes a country from your approved list, legitimate users might be locked out.

The operational blindspot is the same: without monitoring, you won't know these critical security boundaries have changed until someone reports an access issue or you discover a breach.

The Enhanced Solution

I've updated the original script to include comprehensive Named Location monitoring alongside Conditional Access policy detection. The beauty of this approach is that it leverages the same Microsoft Graph APIs and audit log infrastructure, requiring only additional API calls and enhanced email formatting.

Code Additions: Dual Object Monitoring

The first enhancement expands the monitoring scope to include both object types:

# Get all Named Locations modified in the last 7 days
try {
    $AllNamedLocations = Get-MgIdentityConditionalAccessNamedLocation
    $ModifiedNamedLocations = $AllNamedLocations | Where-Object { 
        $_.ModifiedDateTime -ge $SevenDaysAgo
    }
    
    Write-Log "Found $($AllNamedLocations.Count) total Named Locations, $($ModifiedNamedLocations.Count) modified in the last 7 days" "INFO"
}
catch {
    Write-Log "Failed to retrieve Named Locations: $($_.Exception.Message)" "ERROR"
}

# Exit only if BOTH policies and locations are unchanged
if ($ModifiedPolicies.Count -eq 0 -and $ModifiedNamedLocations.Count -eq 0) {
    Write-Log "No Conditional Access policies or Named Locations were modified in the last 7 days. No alert needed." "INFO"
    exit 0
}

The logic change here is subtle but important: the script now continues if either policies or locations have changed, providing comprehensive coverage of your Conditional Access infrastructure.

Enhanced Audit Log Correlation

Named Locations have their own audit log activity type, requiring separate queries to identify who made changes:

# Process Named Location changes
foreach ($Location in $ModifiedNamedLocations) {
    try {
        Write-Log "Processing Named Location: $($Location.DisplayName)" "INFO"
        
        # Query audit logs for this specific named location
        $AuditFilter = "activityDisplayName eq 'Update named location' and targetResources/any(x:x/displayName eq '$($Location.DisplayName)')"
        
        $AuditLogs = Get-MgAuditLogDirectoryAudit -Filter $AuditFilter -Top 5 | 
                     Where-Object { $_.ActivityDateTime -ge $SevenDaysAgo } |
                     Sort-Object ActivityDateTime -Descending |
                     Select-Object -First 1
        
        # Extract user information (same logic as policies)
        $ModifiedBy = if ($AuditLogs.InitiatedBy.User.UserPrincipalName) {
            $AuditLogs.InitiatedBy.User.UserPrincipalName
        } elseif ($AuditLogs.InitiatedBy.App.DisplayName) {
            "$($AuditLogs.InitiatedBy.App.DisplayName) (Application)"
        } else {
            "System/Unknown"
        }

The key difference is the audit filter: 'Update named location' instead of 'Update conditional access policy'. The user attribution logic remains identical, ensuring consistent identification of who made changes across both object types.

Intelligent Location Type Detection

Named Locations require special handling to provide meaningful context in alerts:

# Determine location type and additional info
$LocationType = "Unknown"
$AdditionalInfo = ""

if ($Location.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') {
    $LocationType = "IP Location"
    $TrustedStatus = if ($Location.IsTrusted) { "Trusted" } else { "Untrusted" }
    $AdditionalInfo = $TrustedStatus
}
elseif ($Location.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') {
    $LocationType = "Country Location"
    $CountryCount = ($Location.CountriesAndRegions | Measure-Object).Count
    $AdditionalInfo = "$CountryCount countries/regions"
}

This code examines the OData type to distinguish between IP and Country locations, then extracts relevant details:

  • IP Locations: Shows whether the location is marked as "Trusted" or "Untrusted"
  • Country Locations: Shows the count of countries/regions included

Visual Enhancement in Email Alerts

The email formatting has been enhanced to clearly distinguish between different types of changes:

# Different styling based on type
$TypeIcon = switch ($Change.Type) {
    { $_ -like "Conditional Access Policy" } { "🛡️" }
    { $_ -like "Named Location (IP Location)" } { "🌐" }
    { $_ -like "Named Location (Country Location)" } { "🗺️" }
    default { "📍" }
}

$BorderColor = switch ($Change.Type) {
    { $_ -like "Conditional Access Policy" } { "#dc3545" }  # Red
    { $_ -like "Named Location*" } { "#0078d4" }          # Blue
    default { "#17a2b8" }
}

This creates a visual hierarchy in the email:

  • 🛡️ Red border: Conditional Access Policies (highest security impact)
  • 🌐 Blue border: IP Named Locations (network security)
  • 🗺️ Blue border: Country Named Locations (geographic security)

No Additional Permissions Required

The enhanced monitoring uses the same Microsoft Graph permissions as the original solution:

  • Policy.Read.All - Covers both Conditional Access policies AND Named Locations
  • AuditLog.Read.All - Provides audit trail for both object types

This means you can upgrade your existing monitoring without additional administrative approval or security reviews.

Conclusion

The enhanced Conditional Access monitoring solution now provides complete visibility into your security infrastructure changes. By extending monitoring to include Named Locations, you eliminate another critical operational blind spot that attackers could exploit.

Your Conditional Access policies are only as strong as the locations they trust. Now you'll know immediately when either changes, enabling rapid response to both legitimate administrative changes and potential security threats

Previous Post Next Post

نموذج الاتصال