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.
Table Of Contents
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
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
Example 2 – Displaying FlatObject Output
PS C:\> Get-ConditionalAccessChange -WorkSpaceId $WorkSpaceId -DaysFromToday 5 -FlatObject | select -First 1
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
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.
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