1

Get Azure Conditional Access Policy Changes using PowerShell

I always find it very helpful to be able to use Powershell to automate whatever task needs automating. Knowing how to automate is truly one of the most versatile skills you can have as a Systems Engineer and today I’m going to share a script I wrote to be able to get azure conditional access policy changes using Powershell. This script uses Azure Log Analytics in the backend to track the changes/modified properties.
 

What I love most about this script is that it provides a clear understanding as to who updated the policy, what high level changes were made and probably more importantly, the exact timestamp of when these changes were applied. This will help troubleshoot any issues that may come after a policy was recently changed.
 

Let’s take a look at all the components that are needed to make this work as expected. Feel free to navigate to any portions of the article using the table of contents below.

Requirements

I realize there are Azure alerts to notify you of any changes that are made, but the email that it sends would be a bit more helpful if there were a way to modify the contents of it. This method can also be useful for those that like to use Powershell scripts in a workflow, but whatever the use case is, I think this would a great tool in your Powershell arsenal. With that said, let’s get into the requirements needed.
 

  • Azure AD P1/P2 license
    • Conditional access and Log Analytics require Azure premium licenses
  • Az Powershell Module
  • Azure Log Analytics properly setup with AuditLogs sending to the workspace
    • The Log Analytics Workspace ID you want to query
  • Global Administrator or Security Administrator

 

Note: This script has a dependency for Azure Log Analytics. If you need help getting everything setup, or want to know how to get your Workspace ID, be sure to check out How to query Log Analytics via Powershell. It will walk through everything step by step.

Get Azure Conditional Access Policy Changes using PowerShell

Now that we have the pre-requisites out of the way, let’s go ahead and dive in to the Powershell script itself. Since I only use 1 tenant with a single Workspace ID, I’ve defaulted the Workspace ID in the script itself. This helps so I don’t have to get that information every time I want to check something, it’s just readily available.


Function Get-ConditionalAccessChange {
<#
.SYNOPSIS
    This will display any conditional access changes over a specified amount of time.

.NOTES
    Name: Get-ConditionalAccessChange
    Author: theSysadminChannel
    Version: 2.0
    DateCreated: 2022-Mar-3

.LINK
    https://thesysadminchannel.com/get-azure-conditional-access-policy-changes-using-powershell/ -    

#>

    [CmdletBinding()]

    param(
        [Parameter(
            Mandatory = $false,
            Position = 0
        )]
        [int]    $DaysFromToday = 1,


        [Parameter(
            Mandatory = $false,
            Position = 1
        )]
        [string]    $WorkSpaceId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',


        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet('CreatePolicy', 'UpdatePolicy', 'DeletePolicy')]
        [string[]]    $Operation,


        [Parameter(
            Mandatory = $false
        )]
        [switch]    $FlatObject
    )

    BEGIN {
        $SessionInfo = Get-AzContext -ErrorAction Stop

    }

    PROCESS {
        try {
            $Query = "AuditLogs
                | where TimeGenerated > ago($($DaysFromToday)d)
                | where OperationName contains 'conditional access policy'
                | extend PolicyName = tostring(TargetResources[0].displayName)
                | extend InitiatedByUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
                | extend UserId = tostring(parse_json(tostring(InitiatedBy.user)).id)
                | extend Modifiedproperties = tostring(TargetResources[0].modifiedProperties)
                | extend NewValue = tostring(parse_json(tostring(parse_json(Modifiedproperties)[0].newValue)))
                | extend OldValue = tostring(parse_json(tostring(parse_json(Modifiedproperties)[0].oldValue)))
                | project TimeGenerated, PolicyName, OperationName, InitiatedByUser, OldValue, NewValue, UserId
                | order by TimeGenerated"

            $ResultList = Invoke-AzOperationalInsightsQuery -WorkspaceId $WorkspaceId -Query $Query -ErrorAction Stop | select -ExpandProperty Results


            foreach ($Result in $ResultList) {
                $OldState = $Result.OldValue | ConvertFrom-Json | select -ExpandProperty State
                $NewState = $Result.NewValue | ConvertFrom-Json | select -ExpandProperty State

                if ($OldState -eq 'enabledForReportingButNotEnforced') {
                    $OldState = 'reportOnly'
                }

                if ($NewState -eq 'enabledForReportingButNotEnforced') {
                    $NewState = 'reportOnly'
                }

                $OldConditions = $Result.OldValue | ConvertFrom-Json | select -ExpandProperty conditions
                $NewConditions = $Result.NewValue | ConvertFrom-Json | select -ExpandProperty conditions

                if ($Result.OperationName -eq 'Update conditional access policy') {
                    $ChangesMade = New-Object -TypeName 'System.Collections.ArrayList'
                    $PropertyList = New-Object -TypeName 'System.Collections.ArrayList'

                    $OldConditions | Get-Member -MemberType 'NoteProperty' | select -ExpandProperty Name | ForEach-Object {$PropertyList.Add($_) | Out-Null}
                    $NewConditions | Get-Member -MemberType 'NoteProperty' | select -ExpandProperty Name | ForEach-Object {$PropertyList.Add($_) | Out-Null}

                    $PropertyList = $PropertyList | select -Unique

                    foreach ($Property in $PropertyList) {
                        $SubPropertyList = New-Object -TypeName 'System.Collections.ArrayList'

                        $OldConditions.$($Property) | Get-Member -MemberType 'NoteProperty' -ErrorAction SilentlyContinue | select -ExpandProperty Name | ForEach-Object {$SubPropertyList.Add($_) | Out-Null}
                        $NewConditions.$($Property) | Get-Member -MemberType 'NoteProperty' -ErrorAction SilentlyContinue | select -ExpandProperty Name | ForEach-Object {$SubPropertyList.Add($_) | Out-Null}

                        $SubPropertyList = $SubPropertyList | select -Unique

                        foreach ($SubProperty in $SubPropertyList) {
                            $Compare = Compare-Object -ReferenceObject @($OldConditions.$($Property) | select -ExpandProperty $SubProperty) -DifferenceObject @($NewConditions.$($Property) | select -ExpandProperty $SubProperty) -ErrorAction SilentlyContinue
                            if ($Compare) {
                               $ChangesMade.Add($SubProperty) | Out-Null
                            }

                            switch ($SubProperty) {
                                'includeApplications' {
                                    $includeApplicationsAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $includeApplicationsRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'excludeApplications' {
                                    $excludeApplicationsAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $excludeApplicationsRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'includeUserActions' {
                                    $includeUserActionsAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $includeUserActionsRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'includeAuthenticationContextClassReferences' {
                                    $IncludeAuthContextAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $IncludeAuthContextRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'applicationFilter' {
                                    $applicationFilterAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $applicationFilterRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'excludeGroups' {
                                    $excludeGroupsAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $excludeGroupsRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'excludeRoles' {
                                    $excludeRolesAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $excludeRolesRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'excludeUsers' {
                                    $excludeUsersAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $excludeUsersRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'includeGroups' {
                                    $includeGroupsAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $includeGroupsRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                'includeRoles' {
                                    $includeRolesAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $includeRolesRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }
                                'includeUsers' {
                                    $includeUsersAdd    = $Compare | Where-Object {$_.SideIndicator -eq '=>'} | select -ExpandProperty InputObject
                                    $includeUsersRemove = $Compare | Where-Object {$_.SideIndicator -eq '<='} | select -ExpandProperty InputObject
                                }

                                default {$null}
                            }

                            Remove-Variable compare -ErrorAction SilentlyContinue

                        }
                    }                    

                    if ($OldState -ne $NewState) {
                        $ChangesMade.Add("ChangedState") | Out-Null
                        $ChangedState = "$OldState => $NewState"
                    }

                    $ChangesMade = ($ChangesMade | select -Unique) -join ', '
                    $OperationName   = 'UpdatePolicy'
                } #end Update policy block

                
                if ($Result.OperationName -eq 'Add conditional access policy') {
                    $ChangesMade = 'Policy created'
                    $OperationName   = 'CreatePolicy'
                    $ChangedState = "null => $NewState"
                }

                if ($Result.OperationName -eq 'Delete conditional access policy') {
                    $ChangesMade = 'Policy deleted'
                    $OperationName   = 'DeletePolicy'
                    $ChangedState = "$OldState => null"
                }
              

                if ($PSBoundParameters.ContainsKey('FlatObject')) {
                    #Each property has its own add and remove subproperty in the object
                    $ObjectOutput = [PSCustomObject]@{
                        TimeGenerated              = Get-Date ($Result.TimeGenerated) -Format g
                        PolicyName                 = $Result.PolicyName
                        Operation                  = $OperationName
                        InitiatedByUser            = $Result.InitiatedByUser.Split('@')[0]
                        ChangesMade                = $ChangesMade
                        State                      = $NewState
                        ChangedState               = $ChangedState
                        IncludeApplicationsAdded   = $includeApplicationsAdd
                        IncludeApplicationsRemoved = $includeApplicationsRemove
                        IncludeUserActionsAdded    = $includeUserActionsAdd
                        IncludeUserActionsRemoved  = $includeUserActionsRemove
                        IncludeGroupsAdded         = $includeGroupsAdd
                        IncludeGroupsRemoved       = $includeGroupsRemove
                        IncludeRolesAdded          = $includeRolesAdd
                        IncludeRolesRemoved        = $includeRolesRemove
                        IncludeUsersAdded          = $includeUsersAdd
                        IncludeUsersRemoved        = $includeUsersRemove
                        IncludeAuthContextAdded    = $IncludeAuthContextAdd
                        IncludeAuthContextRemoved  = $IncludeAuthContextRemove
                        ExcludeApplicationsAdded   = $excludeApplicationsAdd
                        ExcludeApplicationsRemoved = $excludeApplicationsRemove
                        ExcludeGroupsAdded         = $excludeGroupsAdd
                        ExcludeGroupsRemoved       = $excludeGroupsRemove
                        ExcludeRolesAdded          = $excludeRolesAdd
                        ExcludeRolesRemoved        = $excludeRolesRemove
                        ExcludeUsersAdded          = $excludeUsersAdd
                        ExcludeUsersRemoved        = $excludeUsersRemove
                        AppFilterAdded             = $applicationFilterAdd
                        AppFilterRemoved           = $applicationFilterRemove
                    }
                } else {
                    #Each add and remove subproperty is nested under the corresponding property. 
                    #I could not decide what would be better/more convenient so I did both :)
                    
                    $ObjectOutput = [PSCustomObject]@{
                        TimeGenerated              = Get-Date ($Result.TimeGenerated) -Format g
                        PolicyName                 = $Result.PolicyName
                        Operation                  = $OperationName
                        InitiatedByUser            = $Result.InitiatedByUser.Split('@')[0]
                        ChangesMade                = $ChangesMade
                        State                      = $NewState
                        ChangedState               = $ChangedState
                        IncludeApplications        = [PSCustomObject]@{'Added' = $includeApplicationsAdd ; 'Removed' = $includeApplicationsRemove}
                        IncludeUserActions         = [PSCustomObject]@{'Added' = $includeUserActionsAdd  ; 'Removed' = $includeUserActionsRemove}
                        IncludeGroups              = [PSCustomObject]@{'Added' = $includeGroupsAdd       ; 'Removed' = $includeGroupsRemove}
                        IncludeRoles               = [PSCustomObject]@{'Added' = $includeRolesAdd        ; 'Removed' = $includeRolesRemove}
                        IncludeUsers               = [PSCustomObject]@{'Added' = $includeUsersAdd        ; 'Removed' = $includeUsersRemove}
                        IncludeAuthContext         = [PSCustomObject]@{'Added' = $IncludeAuthContextAdd  ; 'Removed' = $IncludeAuthContextRemove}
                        ExcludeApplications        = [PSCustomObject]@{'Added' = $excludeApplicationsAdd ; 'Removed' = $excludeApplicationsRemove}
                        ExcludeGroups              = [PSCustomObject]@{'Added' = $excludeGroupsAdd       ; 'Removed' = $excludeGroupsRemove}
                        ExcludeRoles               = [PSCustomObject]@{'Added' = $excludeRolesAdd        ; 'Removed' = $excludeRolesRemove}
                        ExcludeUsers               = [PSCustomObject]@{'Added' = $excludeUsersAdd        ; 'Removed' = $excludeUsersRemove}
                        AppFilter                  = [PSCustomObject]@{'Added' = $applicationFilterAdd   ; 'Removed' = $applicationFilterRemove}
                    }
                }

                if ($PSBoundParameters.ContainsKey('Operation')) {
                    $ObjectOutput | Where-Object {$_.Operation -in $Operation}
                  } else {
                    $ObjectOutput
                }

                #Clear variables before next run
                Remove-Variable OperationName -ErrorAction SilentlyContinue
                Remove-Variable ChangesMade   -ErrorAction SilentlyContinue
                Remove-Variable NewState      -ErrorAction SilentlyContinue
                Remove-Variable ChangedState  -ErrorAction SilentlyContinue
                Remove-Variable include*      -ErrorAction SilentlyContinue
                Remove-Variable exclude*      -ErrorAction SilentlyContinue
                Remove-Variable application*  -ErrorAction SilentlyContinue

            }
        } catch {
            Write-Error $_.Exception.Message
        }
    }

    END {}
}

 

Script Parameters

    -WorkspaceId

DataType: string
Description: This is the Azure Log Analytics Workspace Id.

 

    -DaysFromToday

DataType: Int
Description: This will determine how far back we want to check the logs.
 

    -Operation

DataType: string/array
Description: This will filter for the items you specify. Valid inputs are CreatePolicy, UpdatePolicy, DeletePolicy.
 

    -FlatObject

DataType: switch
Description: This switch will output each property’s add and remove subproperty in the object. When not used, each add and remove subproperty will be nested under the corresponding property.
 

Example 1 – Displaying Default Output

PS C:\> Get-ConditionalAccessChange -WorkSpaceId $WorkSpaceId -DaysFromToday 5 | select -First 1

Get-ConditionalAccessChange - Nested objects
 

Example 2 – Displaying FlatObject Output

PS C:\> Get-ConditionalAccessChange -WorkSpaceId $WorkSpaceId -DaysFromToday 5 -FlatObject | select -First 1

Get-ConditionalAccessChange - FlatObject
 

Example 3 – Displaying Only Created and Deleted Policies

PS C:\> Get-ConditionalAccessChange -WorkSpaceId $WorkSpaceId -DaysFromToday 5 -Operation CreatePolicy, DeletePolicy | `
select TimeGenerated, PolicyName, Operation, InitiatedByUser, ChangesMade, State, ChangedState

Get-ConditionalAccessChange - Operation-update

Conclusion

Hopefully this script to get azure conditional access policy changes using Powershell was helpful for you to see what changes have been made to your environment. It’s useful knowing we have the ability to see who did what, exactly what user, groups or apps were added or removed. Furthermore, since this is all ran from Powershell, you can automate this report to get a weekly or monthly update.

Finally, be sure to follow us on our Youtube Channel if you’re interested in video content.

5/5 - (6 votes)

Paul Contreras

Hi, my name is Paul and I am a Sysadmin who enjoys working on various technologies from Microsoft, VMWare, Cisco and many others. Join me as I document my trials and tribulations of the daily grind of System Administration.

One Comment

  1. Hey Paul
    Great Article my current method to this problem is exporting the conditional access policies (using graph API) and comparing the results when a change is made and I’d love to use workspace analytics, but you seem to have richer data?

    Maybe I don’t have my Conditional Access Audit , diagnostic setting right?
    As i look into the Audit Log I see a rather useless {“displayName”:”Included Updated Properties”,”oldValue”:null,”newValue”:”\”\””}” for the target resources
    additionally I dont see the OperationName being “conditional access policy” just “policy”

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *