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

How-To : Monitoring Conditional Access Policy Changes with PowerShell

If you don't have access to a comprehensive SIEM solution, you end up with a critical operational blind spot regarding what's being changed in your security infrastructure. This lack of visibility means you have no operational awareness of modifications to your Conditional Access policies until someone from your security team manually discovers and advises you of these actions - often too late to prevent potential security incidents.

Conditional Access policies are the backbone of your organization's security posture in Microsoft 365 and Azure AD. When these policies are modified, disabled, or misconfigured, they can create immediate security vulnerabilities or inadvertently lock out legitimate users. Without real-time monitoring, you're essentially flying blind.

This is an example of one of those alerts, where you can see the policy, modification date, perpetrator, policy state and the policy ID: 

Note : This is the 7 day policy that looks back 7 days.

I have an array of policies that cover different timeframes - I need options for 1 day, 7 days and 30 days as you can see below:


The string of code where the date range can be updated is shown below, just udpate the "AddDays" value to the value required.
# Get date range for filtering (last 30 days)
$Today = Get-Date
$SevenDaysAgo = $Today.AddDays(-30)
$TodayString = $Today.ToString("yyyy-MM-dd")
$DateRangeString = "$($SevenDaysAgo.ToString("yyyy-MM-dd")) to $TodayString"
$SecureSecret = ConvertTo-SecureString $ClientSecret -AsPlainText -Force
The Solution: Automated Policy Change Detection

I created a solution that addresses this operational blind spot by building a PowerShell script that proactively monitors your Conditional Access policies. This script checks for policy modifications daily (though you can customize it to monitor any number of previous days), detects what's being changed and who has changed it, then sends you a professional HTML email alert when changes are detected with all the relevant details.

How the Script Works

The script operates in several key phases:

1. Authentication and Connection

The script uses application-only authentication via an Azure AD app registration with client secret credentials:

# Connect to Microsoft Graph using app registration
$SecureSecret = ConvertTo-SecureString $ClientSecret -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential($ClientId, $SecureSecret)
Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $Credential -NoWelcome

2. Policy Discovery and Filtering

The script retrieves all Conditional Access policies and filters them based on the specified time window:

# Get all Conditional Access policies modified in the last 7 days
$Today = Get-Date
$SevenDaysAgo = $Today.AddDays(-7)
$AllPolicies = Get-MgIdentityConditionalAccessPolicy
$ModifiedPolicies = $AllPolicies | Where-Object { 
    $_.ModifiedDateTime -ge $SevenDaysAgo
}

3. Audit Log Correlation

For each modified policy, the script queries the Azure AD audit logs to identify who made the changes:

# Query audit logs for this specific policy
$AuditFilter = "activityDisplayName eq 'Update conditional access policy' and targetResources/any(x:x/displayName eq '$($Policy.DisplayName)')"
$AuditLogs = Get-MgAuditLogDirectoryAudit -Filter $AuditFilter -Top 5 | 
             Where-Object { $_.ActivityDateTime -ge $SevenDaysAgo } |
             Sort-Object ActivityDateTime -Descending |
             Select-Object -First 1

# Extract user information
$ModifiedBy = if ($AuditLogs.InitiatedBy.User.UserPrincipalName) {
    $AuditLogs.InitiatedBy.User.UserPrincipalName
} elseif ($AuditLogs.InitiatedBy.App.DisplayName) {
    "$($AuditLogs.InitiatedBy.App.DisplayName) (Application)"
} else {
    "System/Unknown"
}

4. HTML Email Generation

The script generates a professional, mobile-responsive HTML email with detailed change information:

$HtmlBody = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <style>
        body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 20px; background-color: #f8f9fa; }
        .container { max-width: 800px; margin: 0 auto; background: #ffffff; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
        .policy-item { background: #f8f9fa; border-left: 4px solid #dc3545; margin: 20px 0; padding: 20px; border-radius: 0 6px 6px 0; }
        .policy-name { font-weight: 600; color: #dc3545; font-size: 18px; margin-bottom: 12px; }
    </style>
</head>
<body>
    <!-- Professional email content with policy details -->
</body>
</html>
"@

5. SMTP Delivery

Finally, the script sends the alert via your existing SMTP relay infrastructure:

# Send the email alert
Send-MailMessage -SmtpServer $SmtpServer `
                 -From $FromEmail `
                 -To $ToEmails `
                 -Subject $EmailSubject `
                 -Body $HtmlBody `
                 -BodyAsHtml `
                 -Encoding UTF8

Required API Permissions

For this script to successfully execute, you need to configure an Azure AD app registration with the following Application permissions:

Microsoft Graph API Permissions:

  • Policy-Read.All - Required to read Conditional Access policies
  • AuditLog.Read.All - Required to query audit logs for change tracking

Configuration and Deployment

The script requires minimal configuration at the top:

# Configuration
$TenantId = "your-tenant-id-here"
$ClientId = "your-app-registration-client-id-here" 
$ClientSecret = "your-client-secret-here"
$SmtpServer = "smtp.bear.local"
$FromEmail = "security.alerts@bythepowerofgreyskull.com"
$ToEmails = @("lee@bythepowerofgreyskull.com", "alerting@bythepowerofgreyskull.com")

Creating the Scheduled Task

To automate the monitoring, you need to create a scheduled task that runs this script at your preferred interval. The frequency depends on how quickly you want to be notified of changes to Conditional Access policies - hourly for immediate awareness or daily for routine monitoring.

Setting Up the Scheduled Task

# Create scheduled task to run the monitoring script
$ScriptPath = "C:\Scripts\ConditionalAccessMonitor.ps1"
$TaskName = "ConditionalAccessPolicyMonitor"

# For hourly monitoring (recommended for high-security environments)
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Hours 1) -RepetitionDuration (New-TimeSpan -Days 365)

# Alternative: For daily monitoring at 8 AM
# $Trigger = New-ScheduledTaskTrigger -Daily -At "08:00"

$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ScriptPath`""
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBattery -DontStopIfGoingOnBattery -StartWhenAvailable -RunOnlyIfNetworkAvailable
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $TaskName -Trigger $Trigger -Action $Action -Settings $Settings -Principal $Principal -Description "Monitors Conditional Access policy changes and sends email alerts"

Choosing Your Monitoring Frequency:

Hourly Monitoring - Recommended for:

  • High-security environments
  • Organizations with strict compliance requirements
  • Environments where unauthorized changes need immediate detection
  • Teams that can respond to alerts quickly

Daily Monitoring - Suitable for:

  • Standard business environments
  • Smaller IT teams with limited 24/7 coverage
  • Organizations with established change management processes
  • Cost-conscious deployments wanting to minimize API calls

The script is designed to be quiet when there are no changes, so you won't get unnecessary email spam regardless of frequency.

Conclusion

Don't let Conditional Access policy changes catch you off guard. This automated monitoring solution transforms a critical operational blindspot into a proactive security capability. By implementing this script, you gain the operational awareness necessary to maintain your security posture and respond quickly to unauthorized or unexpected changes.

Previous Post Next Post

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