Detect security policy changes
Who changed my security baseline?
Configuring your tenant with correct security policies that match the needs of your company or customer takes time and effort. But once everything is in place you can sleep on both ears… right?
Unless other admins change the security baseline behind your back. This isn’t necessarily with bad intentions. Security policies are often changed because somebody is troubleshooting an issue, or performing a tenant migration and doesn’t want to be confronted with extra complexity, an emergency situation when the CEO calls and he wants immediate access to his account without MFA for whatever reason.
As a security analyst, IT manager, CISO, or consultant, you want to be informed when somebody changed one of your policies. And let’s be honest, who has a perfect change management system where everything is logged according to the rules?
Does Microsoft provide these detections?
Microsoft doesn’t provide any alerts when critical security policies are changed. You can alter a conditional access policy and exclude the whole company, but no alert or incident will be triggered.
Luckily Microsoft does provide the logs and logs is all we need. With Office Activity logs and Audit logs you can go long way in detecting the most common security policy changes.
Custom detections
Based on common scenario’s I have created KQL queries that will inform you of changes in following the scenario’s:
Azure Active Directory
Conditional Access rule has been deleted ✔️
AuditLogs
| where SourceSystem == "Azure AD"
| where OperationName == "Delete conditional access policy"
| where Result == "success"
| extend Actor = InitiatedBy.user.userPrincipalName
| extend DeletedCAPolicy = TargetResources[0].displayName
| extend CAPolicySettings = TargetResources[0].modifiedProperties[0].oldValue
| project TimeGenerated, Actor, DeletedCAPolicy, CAPolicySettings
Conditional Access rule has been created ✔️
AuditLogs
| where SourceSystem == "Azure AD"
| where OperationName == "Add conditional access policy"
| where Result == "success"
| extend Actor = InitiatedBy.user.userPrincipalName
| extend CreatedCAPolicy = TargetResources[0].displayName
| extend CAPolicySettings = TargetResources[0].modifiedProperties[0].newValue
| project TimeGenerated, Actor, CreatedCAPolicy, CAPolicySettings
Conditional Access rule has been changed ✔️
AuditLogs
| where SourceSystem == "Azure AD"
| where OperationName == "Update conditional access policy"
| where Result == "success"
| extend ChangedCaPolicy = tostring(TargetResources[0].displayName) //check to optimise this line
| extend Actor = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
// get the general settings of the NEW CA policy
| extend NewGeneralSettings = parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue))
// get the general setting of the OLD CA policy
| extend OldGeneralSettings = parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].oldValue))
// get the new conditions
| extend NewConditions = parse_json(tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue)).conditions))
// get the old conditions
| extend OldConditions = parse_json(tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].oldValue)).conditions))
//users
| extend ChangedUser = iff(tostring(OldConditions.users) != tostring(NewConditions.users), true , false)
// applications
| extend ChangedApplications = iff(tostring(OldConditions.applications) != tostring(NewConditions.applications), true , false)
| order by TimeGenerated
// clientAppTypes
| extend ChangedClientAppTypes= iff(tostring(OldConditions.clientAppTypes) != tostring(NewConditions.clientAppTypes), true ,false)
//locations
| extend ChangedLocations= iff(tostring(OldConditions.locations) != tostring(NewConditions.locations), true , false)
| order by TimeGenerated
//platforms
| extend ChangedPlatforms= iff(tostring(OldConditions.platforms) != tostring(NewConditions.platforms), true , false)
//servicePrincipalRiskLevels
| extend ChangedServicePrincipalRiskLevels= iff(tostring(OldConditions.servicePrincipalRiskLevels) != tostring(NewConditions.servicePrincipalRiskLevels), true , false)
// SignInRiskLevels
| extend ChangedSignInRiskLevels= iff(tostring(OldConditions.signInRiskLevels) != tostring(NewConditions.signInRiskLevels), true , false)
//userRiskLevels
| extend ChangedUserRiskLevels= iff(tostring(OldConditions.userRiskLevels) != tostring(NewConditions.userRiskLevels), true , false)
// grantcontrols
| extend ChangedGrantControl = iff(tostring(OldGeneralSettings.grantControls) != tostring(NewGeneralSettings.grantControls), true , false)
// state (policy enabled or not)
| extend ChangedState = iff(tostring(OldGeneralSettings.state) != tostring(NewGeneralSettings.state), true , false)
// sessionControl
| extend ChangedSessionControl = iff(tostring(OldGeneralSettings.sessionControls) != tostring(NewGeneralSettings.sessionControls), true , false)
// show CA policy changes that were True
| where ChangedUser or ChangedApplications or ChangedClientAppTypes or ChangedLocations or ChangedPlatforms or ChangedServicePrincipalRiskLevels or ChangedSignInRiskLevels or ChangedUserRiskLevels or ChangedGrantControl or ChangedState or ChangedSessionControl
| project TimeGenerated, Actor, ChangedCaPolicy, ChangedUser, ChangedApplications, ChangedLocations, ChangedPlatforms, ChangedServicePrincipalRiskLevels, ChangedSignInRiskLevels, ChangedUserRiskLevels, ChangedGrantControl, ChangedState, ChangedSessionControl
Microsft Defender for Office
Anti-Phish
Anti-Phish policy has been changed ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Set-AntiPhishPolicy"
| extend ChangedPolicy = OfficeObjectId
| extend Actor = UserId
| project TimeGenerated, Actor, ChangedPolicy
Anti-Phish policy has been created ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "New-AntiPhishPolicy"
| extend CreatedPolicy = split(OfficeObjectId,"\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, CreatedPolicy, Settings
Anti-Phish policy has been deleted ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Remove-AntiPhishPolicy"
| extend RemoveddPolicy = split(OfficeObjectId,"\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, RemoveddPolicy, Settings
Anti-Spam
Anti-Spam Policy has been changed ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Set-HostedContentFilterRule"
| extend ChangedPolicy = split(OfficeObjectId, "\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, ChangedPolicy, Settings
Anti-Spam Policy has been created ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "New-HostedContentFilterPolicy"
| extend CreatedPolicy = split(OfficeObjectId, "\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, CreatedPolicy, Settings
Anti-Spam Policy has been deleted ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation contains "remove-HostedContentFilterRule"
| extend RemovedPolicy = OfficeObjectId
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, RemovedPolicy, Settings
Anti-Malware
Anti-Malware Policy has been changed ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "New-MalwareFilterPolicy"
| extend CreatedPolicy = split(OfficeObjectId,"\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, CreatedPolicy, Settings
Anti-Malware Policy has been created ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "New-MalwareFilterPolicy"
| extend CreatedPolicy = split(OfficeObjectId,"\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, CreatedPolicy, Settings
Anti-Malware Policy has been removed ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Remove-MalwareFilterPolicy"
| extend RemovedPolicy = OfficeObjectId
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, RemovedPolicy, Settings
Safe Attachments
Safe Attachments Policy has been changed ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Set-SafeAttachmentPolicy"
| extend ChangedPolicy = OfficeObjectId
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, ChangedPolicy, Settings
Safe Attachments Policy has been created ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "New-SafeAttachmentPolicy"
| extend CreatedPolicy = split(OfficeObjectId,"\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, CreatedPolicy, Settings
Safe Attachments Policy has been deleted ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Remove-SafeAttachmentPolicy"
| extend RemovedPolicy = OfficeObjectId
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, RemovedPolicy, Settings
Safe Link Policy
Safe Link Policy has been changed ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Set-SafeLinksPolicy"
| extend ChangedPolicy = OfficeObjectId
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, ChangedPolicy, Settings
Safe Link Policy has been created ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "New-SafeLinksPolicy"
| extend CreatedPolicy = split(OfficeObjectId,"\\",1)
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, CreatedPolicy, Settings
Safe Link Policy has been deleted ✔️
OfficeActivity
| where OfficeWorkload == "Exchange"
| where Operation == "Remove-SafeLinksPolicy"
| extend RemovedPolicy = OfficeObjectId
| extend Actor = UserId
| extend Settings = todynamic(Parameters)
| project TimeGenerated, Actor, RemovedPolicy, Settings
Tenant Allow/Block Lists
Tenant added to block list ✔️
OfficeActivity
| where SourceSystem == "OfficeActivityManager"
| where RecordType == "ExchangeAdmin"
| where ResultStatus == "True"
| where Operation == "New-TenantAllowBlockListItems"
| extend Actor = "UserId"
| extend BlockedDomains = parse_json(Parameters)[0].Value
| project TimeGenerated, Actor, BlockedDomains
Exchange online
Transport rules
Creation of a transport rule ✔️
OfficeActivity
| where Operation == "New-TransportRule"
| where OfficeWorkload == "Exchange"
| where ResultStatus == "True"
| extend Actor = UserId
| extend TransportRuleSettings = todynamic(Parameters)
| extend createdTransportRule = TransportRuleSettings[0].Value
| project TimeGenerated, Actor, createdTransportRule, TransportRuleSettings
Github
All of these KQL queries can be found on my GitHub page. As this will be a place where will update (optimize) and add new KQL queries to this repo as I continue working on this project.
If you have any interesting scenarios that might have a lot of added value for you or your organization, feel free to leave a comment.
6 responses to “Detect security policy changes”
-
Do you know how to also monitor or query for, who edited an existing ASR Rule on an Intune Policy
Example, who changed the, ‘Block Win32 API calls from Office macros’ rule from Block to Audit?
I love the CA ones that you have created and those are going to prove great alerts for the team, so thank for this.
-
Hi Richard,
I just looked in the logs and sadly enough the logs are not the granular enough to track changes.
But I could write a KQL that notifies you when any change is logged (without telling you what the changes is), delete or creation of an ASR rule.-
Honestly, that would be really useful.
Anything that can at least give me a clue on where to start looking etc. Then if the logging abilities in Intune do get more granular in the future, I can use that to build upon.
Thank you.
-
-
-
These are extremally useful – thank you for this.
Just one question please – is there a typo in the ‘Anti-Spam Policy has been deleted’ rule where it says ‘Set-HostedContentFilterRule’? Should it not be ‘Removed-HostedContentFilterRule’?
Also – the KQL for the Safe Link Policy rules are all the same – they should be ‘set’/’new’/’remove’ also?
-
Wow you are totally right!
I must have made a mistake when copy & pasting them!
Thanks Tom, I corrected them in the blog post.The most up-to-date version can always be found on my GitHub page here
But they were definitely wrong in the blog post.
-
-
[…] why I typically depend on generic queries. The query below is developed from examples shared by a colleague and creates a detailed list of the changes made to a policy. With this information, you can […]
Leave a Reply