Author: Chad Cox (Microsoft)
Created: January 2023
Updated: February 2023
Disclaimer This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys` fees, that arise or result from the use or distribution of the Sample Code..
Deploy the query pack that contains all the queries from this solution into the Log Analytics Workspace that contains the Azure AD Audit / Signin logs
How to use this guide
- Below is a list of Conditional Access Policies that Microsoft recommends in an Azure AD Tenant.
- Each link in the table of content contains information about a policy with notes about how to evaluate the impact of the policy.
- Use this method to shorten the amount of time it takes to deploy Conditional Access Policies in Azure AD, by proactively leveraging existing signinlogs and filtering to show the users that could be impacted.
- Yes it is posible and every organization should still use conditional access policies even if they have a third party IDP. More information in the requirements section.
Table of Content
- Goals
- Requirements
- Introduction
- How to run a Log Analytics Query
- Import the policies from templates
- Find IPAddress not defined as trusted
- Applications not being protected by Conditional Access Policies
- Percentage of MFA / Compliant Device / Trusted Device / Trusted Location / Conditional Access Policies by Applications
- Create list of privileged users with powershell for the kql statement for findings related to privileged user impact
- Conditional Access Policy Matrix
- Protect Privileged Credentials
- Require trusted devices
- Do not depend on trusted networks / locations
- Always require multifactor
- Minimize the use of filters
- Know the potential impact of a policy.
- The best way to do this is sending the Azure AD Sign In Logs to Azure Monitor (LogAnalytics).
- Instructions on how to set up: Integrate Azure AD logs with Azure Monitor logs
- Azure AD Premium 1 License are required for:
- Conditional Access Policies
- Sign in Logs to be sent to Log Analytics
- Ability to query Sign in logs via microsoft graph
- If a third party IDP or ADFS is used to federate the tenant and mfa is being performed there instead of AAD, it must send the multiauthn claim when it performs mfa, so that Azure AD knows a mfa was performed and is reflcted in the logs and bypasses MFA. Here is more info about the settings that needs to be done for this: Set federatedIdpMfaBehavior to enforceMfaByFederatedIdp. Without this data the queries will not provide to much value and Azure AD will have no idea
- Third Party IDP notes for MFA and Conditional Access Policies
- Use Okta MFA for Azure Active Directory - OKTA does have some known issues though.
- PingID as on premises MFA for federated Office 365 users
- Risk Policies require P2 License.
- Workload Identity License is required to view those risk.
While working with organizations to determine impact using read only and the built-in reporting, I found this was taking longer than needed and could be done another way. I have thrown together a few PowerShell Scripts and Log Analytics Queries (KQL) in addition to notes about each policy that will help identify potential impact before a particular policy is created or applied. The idea is to use the results to determine impact of a policy and put in the exclusions or policy adjustments needed to minimize impact and get the desired affect of the policy.
Not everything here is perfect and is being updated as I learn new things or new guidance becomes published. Also do not hesitate to comment and let me know what is not working or is working.
- In the Azure AD Portal
- Navigate to the Log Analytics Tab
- Copy the example code from the section you want to review the possible impact
- Replace the existing text in the query window or open a new query tab and paste in the new one.
- Then select Run and wait for the results.
Or Deploy the query pack that contains all the queries from this solution into the Log Analytics Workspace that contains the Azure AD Audit / Signin logs
- After the query pack is deployed
- In the Azure AD Portal
- Navigate to the Log Analytics Tab
- Select the Queries and change the group by to Label
I have put together a script that will import all of the policies from this github.
The scipt can be found here click here
Instructions
- Copy the contents of the script locally onto a machine.
- Run the script in PowerShell
- Select the number of the policy you want to import.
- Review the results They are always in read-only and have a prefix
- Run this in PowerShell
Connect-MgGraph
Select-MgProfile -Name beta
$roles = @("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator")
(Get-MgDirectoryRole -ExpandProperty members -all | where {$_.displayname -In $roles} | select -ExpandProperty members).id -join('","') | out-file .\privuser.txt
- The results of this will be in a file called privuser.txt
- Open the txt file. Should look something like this
8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b
- Next in the sections titled Log Analytics AAD SigninLogs Query (KQL) needs results from the PowerShell script from the section you are reviewing. Will want to copy the kql statement, and paste in Log Analytics.
- on line 1 replace the phrase replace this with the results from the privuser.txt found from the powershell cmdlets
let privusers = pack_array("**replace this with the results from the privuser.txt found from the powershell cmdlets**");
- to look like
let privusers = pack_array("8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b","8f47d5a6-a36b-4d99-b6bc-c023cf23ae9b");
Log Analytics AAD SigninLogs Query (KQL)
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == "0"
| where HomeTenantId == ResourceTenantId
| where NetworkLocationDetails !contains "trustedNamedLocation"
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| extend isIPv6 = tostring(iff(IPAddress matches regex @"(([\d|\w]{1,4}\:){7}[\d|\w]{1,4})",'Yes','No'))
| distinct IPAddress, TrustedLocation, UserPrincipalName, isIPv6
| summarize uniqueusercountbyip = count() by IPAddress, TrustedLocation, isIPv6
| where uniqueusercountbyip >= 4
| sort by uniqueusercountbyip desc
Comment
This query returns IP addresses where 4 or more unique users have authenticated against Azure AD. You will want to research each IP and determine if they are owned by the organization or if they belong to something like a public proxy cloud solution like zscaler or umbrella. Legit ones will need to be defined as a trusted network in Azure AD to make sure any location filtered policy works correctly and to help remediate false positives in Azure Identity Protection
Instructions on how to create named locations can be viewed here Named locations
The field uniqueusercountbyip is count of unique list of users. It is possible to see ipv6 addresses which usually comes from Azure Networks and will be normal in the near future from the internet.
Log Analytics AAD SigninLogs Query (KQL)
//https://github.com/reprise99/Sentinel-Queries/blob/main/Azure%20Active%20Directory/Identity-Top20AppswithnoCA.kql
//This query shows applications that are not protected by conditional access policies.
let apps=
SigninLogs
| where TimeGenerated > ago (30d)
| project TimeGenerated, ConditionalAccessPolicies, AppDisplayName
//Exclude native Microsoft apps that you can't enforce policy on or that are covered natively in Office 365
| where AppDisplayName !in ("Microsoft Office Web Apps Service", "Microsoft App Access Panel", "Office Online Core SSO", "Microsoft Authentication Broker", "Microsoft Account Controls V2", "Microsoft 365 Support Service","Office Online Maker SSO","My Apps","My Profile")
| mv-expand ConditionalAccessPolicies
| extend CAResult = tostring(ConditionalAccessPolicies.result)
| summarize ResultSet=make_set(CAResult) by AppDisplayName
| where ResultSet !has "success" or ResultSet !has "failure"
| project AppDisplayName;
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0
| where AppDisplayName in (apps)
| summarize Count=count()by AppDisplayName
| top 20 by Count
Comment
The image below, shows the applications and the logon count of those apps that is not being protected by some sort of Conditional Access Policy. Ideally every application will have a mfa requirement or a trusted/compliant policy requirement.
Percentage of MFA / Compliant Device / Trusted Device / Trusted Location / Conditional Access Policies by Applications
Log Analytics AAD SigninLogs Query (KQL)
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0 and AppDisplayName <> 'Windows Sign In' and UserType <> "Guest"
| where ResourceTenantId == HomeTenantId and AADTenantId == HomeTenantId
| extend trustType = tostring(DeviceDetail.trustType)
| extend isCompliant = tostring(DeviceDetail.isCompliant)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| summarize
['Total Signin Count']=count(),
['Total MFA Count']=countif(AuthenticationRequirement == "multiFactorAuthentication"),
['Total non MFA Count']=countif(AuthenticationRequirement == "singleFactorAuthentication"),
['Total Trusted device']=countif(trustType == "Hybrid Azure AD joined"),
['Total Compliant device']=countif(isCompliant == 'true'),
['Total Trusted Location']=countif(TrustedLocation == 'trustedNamedLocation'),
['Total CAP Applied']=countif(ConditionalAccessStatus == 'success')
by AppDisplayName
| project
AppDisplayName,TotalSigninCount = ['Total Signin Count'],
MFAPercentage=(todouble(['Total MFA Count']) * 100 / todouble(['Total Signin Count'])),
TrustedDevicePercentage=(todouble(['Total Trusted device']) * 100 / todouble(['Total Signin Count'])),
CompliantDevicePercentage=(todouble(['Total Compliant device']) * 100 / todouble(['Total Signin Count'])),
TrustedLocationPercentage=(todouble(['Total Trusted Location']) * 100 / todouble(['Total Signin Count'])),
ConditionalPolicyAppliedPercentage=(todouble(['Total CAP Applied']) * 100 / todouble(['Total Signin Count']))
| where MFAPercentage <> 100
| sort by MFAPercentage desc
Comment
My lab has no good data for this. This query will show the percentage of each of the major things we are looking for. The idea here is to look and make sure applications actually have the desired protections whether its from a compliant device or MFA.
This is what I would expect to see.
- Every Application is 100% convered by Conditional Access Policies.
- At minimum the MFAPercentage + TrustedLocationPercentage should equal 100%
- And in a true zero trust envrionment the CompliantDevicePercentage should be 100%
- Im most cases if MFAPercentage + TrustedDevicePercentage + CompliantDevicePercentage = 100%, then a Org is doing a decent job.
- Link to Microsoft Documentation: Common Conditional Access policy: Require MFA for all users
- This policy will require all users logging into any application to MFA.
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group, Directory Role (Directory Sync Accounts), Guest
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: Windows Store
- Conditions
- Grant
- Grant Access
- Require Multi-Factor Authentication
- Require all the selected controls
Note: this policy will more than likely break on premise sync accounts, make sure the Directory Sync Accounts Role is in the exclusion group.
Log Analytics AAD SigninLogs Query (KQL)
let thisTenantId = SigninLogs | take 1 | distinct AADTenantId;
let guests = AADNonInteractiveUserSignInLogs | union SigninLogs | where TimeGenerated > ago(14d) | where HomeTenantId !in (thisTenantId) and HomeTenantId <> '' | distinct UserId;
let excludeapps = pack_array("Windows Sign In","Microsoft Authentication Broker","Microsoft Account Controls V2","Microsoft Intune Company Portal","Microsoft Mobile Application Management");
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(14d)
| where Status !contains "MFA requirement satisfied by claim in the token"
| union SigninLogs
| where TimeGenerated > ago(14d)
| where UserType <> "Guest" and UserId !in (guests)
| where HomeTenantId == ResourceTenantId
| where ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName !in (excludeapps) and AppDisplayName <> ''
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, Category
| summarize apps=make_list(AppDisplayName) by UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement,Category
Comment
This policy is a harder policy to implement. This query will return a unique list of users and applications that are not hitting up against a conditional access policy and not providing multifactor authentication. Things to look for in the KQL results are applications that might have problems like the Windows Store and accounts that need to be excluded such as faceless user objects or "service accounts".
Expect to see most of the users in a org in this list. The goal is to find the users and applications that need to be excluded because it would cause impact.
Looking at the image below. I would make sure to exclude the breakglass account and the sync account as those are accounts that should not have this policy applied to it.
- Link to Microsoft Documentation: Common Conditional Access policy: Require MFA for all users
- This policy will require all users logging into any application to MFA when signing in from networks not flagged as trusted.
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group, Guest
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: Windows Store
- Conditions
- Locations
- Include: Any Location
- Exclude: All trusted locations
- Grant
- Grant Access
- Require Multi-Factor Authentication
- Require all the selected controls
Log Analytics AAD SigninLogs Query (KQL)
let thisTenantId = SigninLogs | take 1 | distinct AADTenantId;
let guests = AADNonInteractiveUserSignInLogs | union SigninLogs | where TimeGenerated > ago(14d) | where HomeTenantId !in (thisTenantId) and HomeTenantId <> '' | distinct UserId;
let excludeapps = pack_array("Windows Sign In","Microsoft Authentication Broker","Microsoft Account Controls V2","Microsoft Intune Company Portal","Microsoft Mobile Application Management");
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(14d)
| where Status !contains "MFA requirement satisfied by claim in the token"
| where NetworkLocationDetails !contains "trustedNamedLocation"
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| union SigninLogs
| where TimeGenerated > ago(14d)
| where UserType <> "Guest" and UserId !in (guests)
| where HomeTenantId == ResourceTenantId
| where NetworkLocationDetails !contains "trustedNamedLocation"
| where ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| where AppDisplayName !in (excludeapps) and AppDisplayName <> ''
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement,TrustedLocation
| summarize apps=make_list(AppDisplayName) by UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement,TrustedLocation
Comment
This policy is not required if you were able to implement: Always require MFA
Also this policy has a network based filter which means if someone was able to "trick" the ip they would bypass important protections.
This query will return a unique list of users and applications that are not hitting up against a conditional access policy and not providing multifactor authentication. Things to look for in the KQL results are applications that might have problems like the Windows Store and accounts that need to be excluded such as faceless user objects or "service accounts".
The goal is to find the users and applications that need to be excluded because it would cause impact. Also note if users are in this list that never access outside of the org then there is a good chance the IP that user is coming from is not trusted.
Looking at the image below. I would make sure to exclude the breakglass account from the policy and I would research the sync account to figure out why its being used outside a trusted network.
- Link to Microsoft Documentation: Common Conditional Access policy: Require a compliant device, hybrid Azure AD joined device, or multifactor authentication for all users
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group, Directory Role (Directory Sync Accounts), Guest
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: Windows Store
- Conditions
- Grant
- Grant Access
- Require Multi-Factor Authentication, Require Hybrid Azure AD joined device, and Require device to be marked as compliant
- Require one of the selected controls
Log Analytics AAD SigninLogs Query (KQL)
let thisTenantId = SigninLogs | take 1 | distinct AADTenantId;
let guests = AADNonInteractiveUserSignInLogs | union SigninLogs | where TimeGenerated > ago(14d) | where HomeTenantId !in (thisTenantId) and HomeTenantId <> '' | distinct UserId;
let excludeapps = pack_array("Windows Sign In","Microsoft Authentication Broker","Microsoft Account Controls V2","Microsoft Intune Company Portal","Microsoft Mobile Application Management");
//query the non interactive logs
let AADNon = AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where Status !contains "MFA requirement satisfied by claim in the token"
| where UserId !in (guests)
| where AppDisplayName !in (excludeapps)
| extend trustType = tostring(parse_json(DeviceDetail).trustType)
| extend isCompliant = tostring(parse_json(DeviceDetail).isCompliant)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| extend os = tostring(parse_json(DeviceDetail).operatingSystem)
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation, trustType,isCompliant,os, Category;
//query the interactive logs
let AAD = SigninLogs
| where TimeGenerated > ago(14d) and UserType <> "Guest" and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName !in (excludeapps)
| where UserType <> "Guest" and UserId !in (guests)
| where HomeTenantId == ResourceTenantId
| extend trustType = tostring(DeviceDetail.trustType)
| extend isCompliant = tostring(DeviceDetail.isCompliant)
| extend os = tostring(DeviceDetail.operatingSystem)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation, trustType,isCompliant,os,Category;
//combine the results
AADNon
| union AAD
| where AppDisplayName <> ''
| summarize apps=make_set(AppDisplayName),ostypes=make_set(os) by UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation,trustType,isCompliant
Comment
This policy is not required if you were able to implement: Always require MFA
- Link to Microsoft Documentation: Common Conditional Access policy: Require a compliant device, hybrid Azure AD joined device, or multifactor authentication for all users
- Link to Microsoft Documentation: Named locations
- This policy will require all trusted networks to be defined.
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group, Directory Role (Directory Sync Accounts), Guest
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: Windows Store
- Conditions
- Locations
- Include: Any Location
- Exclude: All trusted locations
- Grant
- Grant Access
- Require Multi-Factor Authentication, Require Hybrid Azure AD joined device, and Require device to be marked as compliant
- Require one of the selected controls
Log Analytics AAD SigninLogs Query (KQL)
let thisTenantId = SigninLogs | take 1 | distinct AADTenantId;
let guests = AADNonInteractiveUserSignInLogs | union SigninLogs | where TimeGenerated > ago(14d) | where HomeTenantId !in (thisTenantId) and HomeTenantId <> '' | distinct UserId;
let excludeapps = pack_array("Windows Sign In","Microsoft Authentication Broker","Microsoft Account Controls V2","Microsoft Intune Company Portal","Microsoft Mobile Application Management");
let AADNon = AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName !in (excludeapps)
| where UserId !in (guests)
| where Status !contains "MFA requirement satisfied by claim in the token"
| where NetworkLocationDetails !contains "trustedNamedLocation"
| extend trustType = tostring(parse_json(DeviceDetail).trustType)
| extend isCompliant = tostring(parse_json(DeviceDetail).isCompliant)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| extend os = tostring(parse_json(DeviceDetail).operatingSystem)
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation, trustType,isCompliant,os, Category;
//query the interactive logs
let AAD = SigninLogs
| where TimeGenerated > ago(14d) and UserType <> "Guest" and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName !in (excludeapps)
| where NetworkLocationDetails !contains "trustedNamedLocation"
| where UserType <> "Guest" and UserId !in (guests)
| where HomeTenantId == ResourceTenantId
| extend trustType = tostring(DeviceDetail.trustType)
| extend isCompliant = tostring(DeviceDetail.isCompliant)
| extend os = tostring(DeviceDetail.operatingSystem)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation, trustType,isCompliant,os,Category;
//combine the results
AADNon
| union AAD
| where AppDisplayName <> ''
| summarize apps=make_set(AppDisplayName),ostypes=make_set(os) by UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation,trustType,isCompliant
Comment
This policy is not required if you were able to implement: Always require MFA
- Link to Microsoft Documentation: Blocking PowerShell for EDU Tenants
- May not be available for China or Gov Cloud
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud Apps
- Include: Microsoft Graph PowerShell, Graph Explorer
- Conditions
- Grant
- Grant Access
- Require Multi-Factor Authentication
- Require all the selected controls
Log Analytics AAD SigninLogs Query (KQL)
let includeapps = pack_array("Graph Explorer","Microsoft Graph PowerShell");
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName in (includeapps)
| union SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName in (includeapps)
| distinct AppDisplayName, UserPrincipalName, ConditionalAccessStatus, AuthenticationRequirement
Comment
This policy is great for organizations that have trusted network based filters on the base conditional access policy. This will make sure users that use tools that can be used to perform queries or changes agaisnt the tenant must require MFA from both trusted and non trusted networks.
Revew the list of users in the results. in the example image below, the breakglass account is the only account being used to signin to those end points. That particular account should be excluded from the policy. But also shouldnt be used. Any other account such as possible service accounts used for azure ad automation will need to be excluded from the policy and should eventually transition to using a service principal instead.
- Link to Microsoft Documentation: Common Conditional Access policy: Require MFA for Azure management
- This may not be available for Gov or China Tenant
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud Apps
- Include: Microsoft Azure Management / (Gov Tenant) Azure Government Cloud Management API
- Conditions
- Grant
- Grant Access
- Require Multi-Factor Authentication
- Require all the selected controls
Log Analytics AAD SigninLogs Query (KQL)
//https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-azure-management
//Common Conditional Access policy: Require MFA for Azure management
let includeapps = pack_array("Windows Azure Service Management API","Azure Resource Manager","Azure portal","Azure Data Lake","Application Insights API","Log Analytics API");
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AADTenantId == ResourceTenantId
| where ResourceDisplayName in (includeapps) or AppDisplayName in (includeapps)
| distinct AppDisplayName, UserPrincipalName, ConditionalAccessStatus, AuthenticationRequirement, ResourceDisplayName
Comment
This policy is great for organizations that have trusted network based filters on the base conditional access policy. This will make sure users that use tools that can be used to perform queries or changes against Azure subscriptions must require MFA from both trusted and non trusted networks.
Revew the list of users in the results. in the example image below, the breakglass account is the only account being used to signin to those end points. That particular account should be excluded from the policy. But also shouldnt be used. Any other account such as possible service accounts used for azure automation will need to be excluded from the policy and should eventually transition to using a service principal instead.
- Link to Microsoft Documentation: Common Conditional Access policy: Block legacy authentication
- This policy will require
- Ideally use a block over MFA
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud Apps
- Include: All Cloud Apps
- Conditions
- Client apps
- Exchange ActiveSync clients
- Other clients
- Grant
- Block Access
Log Analytics AAD SigninLogs Query (KQL)
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0
| extend ClientAppUsed = iff(isempty(ClientAppUsed) == true, "Unknown", ClientAppUsed)
| extend isLegacyAuth = case(ClientAppUsed contains "Browser", "No", ClientAppUsed contains "Mobile Apps and Desktop clients", "No", ClientAppUsed contains "Exchange ActiveSync", "Yes", ClientAppUsed contains "Exchange Online PowerShell","Yes", ClientAppUsed contains "Unknown", "Unknown", "Yes")
| where isLegacyAuth == "Yes"
| distinct UserDisplayName, UserPrincipalName, AppDisplayName, ClientAppUsed, isLegacyAuth, UserAgent, Category
Comment
No example image to show what these results look like. Review the results from the query which pulls from both the interactive and non interactive logs. work with the users to remove the dependancy. The sooner this policy is in place the better.
- Link to Microsoft Documentation: Common Conditional Access policy: Require MFA for administrators
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Directory Roles (Application Administrator,Authentication Administrator,Cloud Application Administrator,Conditional Access Administrator,Exchange Administrator,Global Administrator,Helpdesk Administrator,Hybrid Identity Administrator,Password Administrator,Privileged Authentication Administrator,Privileged Role Administrator,Security Administrator,SharePoint Administrator,User Administrator)
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Grant
- Grant Access
- Require Multi-Factor Authentication
- Require all the selected controls
- Session
- Sign-in frequency 2 Hours
Get list of Privileged Users using PowerShell to use in the kql below
Connect-MgGraph
Select-MgProfile -Name beta
$roles = @("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator")
(Get-MgDirectoryRole -ExpandProperty members -all | where {$_.displayname -In $roles} | select -ExpandProperty members).id -join('","') | out-file .\privuser.txt
Log Analytics AAD SigninLogs Query (KQL) needs results from the PowerShell script
let privusers = pack_array("**replace this with the results from the privuser.txt found from the powershell cmdlets**");
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and UserId in~ (privusers) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName <> "Windows Sign In" and AppDisplayName <> "Microsoft Authentication Broker" and AppDisplayName <> 'Microsoft Account Controls V2'
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, Category
Log Analytics AAD SigninLogs and AuditLogs PIM Query (KQL)
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = AuditLogs
| where TimeGenerated > ago(60d) and ActivityDisplayName == 'Add member to role completed (PIM activation)' and Category == "RoleManagement"
| extend Caller = tostring(InitiatedBy.user.userPrincipalName)
| extend Role = tostring(TargetResources[0].displayName)
| where Role in (privroles)
| distinct Caller;
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName <> "Windows Sign In" and AppDisplayName <> "Microsoft Authentication Broker" and AppDisplayName <> 'Microsoft Account Controls V2'
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, Category
Sentinel AAD SigninLogs Query (KQL) requires UEBA turned on
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = IdentityInfo | where TimeGenerated > ago(60d) and AssignedRoles != "[]" | mv-expand AssignedRoles | extend Roles = tostring(AssignedRoles) | where Roles in (privroles) | distinct AccountUPN;
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AppDisplayName <> "Windows Sign In" and AppDisplayName <> "Microsoft Authentication Broker" and AppDisplayName <> 'Microsoft Account Controls V2'
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, Category
Comment
The results of these queries will show privileged users that are not using MFA when signing in. Review the results and look for for user based service accounts that will be affected by this policy and exclude them.
- Link to Microsoft Documentation: Common Conditional Access policy: Block legacy authentication
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Include: Directory Roles (Application Administrator,Authentication Administrator,Cloud Application Administrator,Conditional Access Administrator,Exchange Administrator,Global Administrator,Helpdesk Administrator,Hybrid Identity Administrator,Password Administrator,Privileged Authentication Administrator,Privileged Role Administrator,Security Administrator,SharePoint Administrator,User Administrator)
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Client apps
- Exchange ActiveSync clients
- Other clients
- Client apps
- Grant
- Block Access
Get list of Privileged Users using PowerShell to use in the kql below
Connect-MgGraph
Select-MgProfile -Name beta
$roles = @("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator")
(Get-MgDirectoryRole -ExpandProperty members -all | where {$_.displayname -In $roles} | select -ExpandProperty members).id -join('","') | out-file .\privuser.txt
Log Analytics AAD SigninLogs Query (KQL) needs results from the PowerShell script
let privusers = pack_array("**replace this with the results from the privuser.txt found from the powershell cmdlets**");
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and UserId in~ (privusers) and ResultType == 0
| extend ClientAppUsed = iff(isempty(ClientAppUsed) == true, "Unknown", ClientAppUsed)
| extend isLegacyAuth = case(ClientAppUsed contains "Browser", "No", ClientAppUsed contains "Mobile Apps and Desktop clients", "No", ClientAppUsed contains "Exchange ActiveSync", "Yes", ClientAppUsed contains "Exchange Online PowerShell","Yes", ClientAppUsed contains "Unknown", "Unknown", "Yes")
| where isLegacyAuth == "Yes"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, isLegacyAuth
Log Analytics AAD SigninLogs and AuditLogs PIM Query (KQL)
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = AuditLogs
| where TimeGenerated > ago(60d) and ActivityDisplayName == 'Add member to role completed (PIM activation)' and Category == "RoleManagement"
| extend Caller = tostring(InitiatedBy.user.userPrincipalName) | extend Role = tostring(TargetResources[0].displayName) | where Role in (privroles) | distinct Caller;
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and ResultType == 0
| extend ClientAppUsed = iff(isempty(ClientAppUsed) == true, "Unknown", ClientAppUsed)
| extend isLegacyAuth = case(ClientAppUsed contains "Browser", "No", ClientAppUsed contains "Mobile Apps and Desktop clients", "No", ClientAppUsed contains "Exchange ActiveSync", "Yes", ClientAppUsed contains "Exchange Online PowerShell","Yes", ClientAppUsed contains "Unknown", "Unknown", "Yes")
| where isLegacyAuth == "Yes"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, isLegacyAuth
Sentinel AAD SigninLogs Query (KQL) requires UEBA turned on
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = IdentityInfo | where TimeGenerated > ago(60d) and AssignedRoles != "[]" | mv-expand AssignedRoles | extend Roles = tostring(AssignedRoles) | where Roles in (privroles) | distinct AccountUPN
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and ResultType == 0
| extend ClientAppUsed = iff(isempty(ClientAppUsed) == true, "Unknown", ClientAppUsed)
| extend isLegacyAuth = case(ClientAppUsed contains "Browser", "No", ClientAppUsed contains "Mobile Apps and Desktop clients", "No", ClientAppUsed contains "Exchange ActiveSync", "Yes", ClientAppUsed contains "Exchange Online PowerShell","Yes", ClientAppUsed contains "Unknown", "Unknown", "Yes")
| where isLegacyAuth == "Yes"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, isLegacyAuth
Comment
The list from the results are privileged users that would be affected by this policy. I have found it easier for orgs to apply these restrictions to privileged users before applying to all users. The goal of this policy is to make sure no privileged user is actively using basic authentication.
- Link to Microsoft Documentation: Named locations
- Requires Named Locations to be created and trusted
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Directory Role (Directory Sync Account)
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Locations
- Include: Any Location
- Exclude: All trusted locations
- Grant
- Block Access
Log Analytics AAD SigninLogs Query (KQL)
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName startswith "Sync_"
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| distinct IPAddress, TrustedLocation, UserPrincipalName
Comment
This account if compromised is limited to a particular endpoint, however it is possible to compromise the account. The goal is to make sure it is limited to trusted networks. This query will show if any of the accounts are being used from untrusted networks. the idea is to review the ip address and define it if it is trusted.
- Link to Microsoft Documentation: Common Conditional Access policy: Require MFA for Azure management
- Link to Microsoft Documentation: Conditional Access: Block access
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All guest and external users
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: Microsoft Azure Management / (Gov Tenant) Azure Government Cloud Management API
- Exclude: None
- Conditions
- Grant
- Block Access
Log Analytics AAD SigninLogs Query (KQL)
let includeapps = pack_array("Windows Azure Service Management API","Azure Resource Manager","Azure portal","Azure Data Lake","Application Insights API","Log Analytics API");
SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication" and UserType == "Guest"
| where ResourceDisplayName in (includeapps) or AppDisplayName in (includeapps)
| where AADTenantId == ResourceTenantId
| distinct AppDisplayName, UserPrincipalName, ConditionalAccessStatus, AuthenticationRequirement, ResourceDisplayName
Comment
This policy is designed to stop guest accounts from using management portals. The idea is guest should be restricted from doing admin type work.
The results from this query are straight forward. The goal is to provide a list of users that might be affected by applying this policy. Review the results of this and put in exclusions as needed.
- Link to Microsoft Documentation: change me
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Guest
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Grant
- Grant Access
- Require Multi-Factor Authentication
- Require all the selected controls
Log Analytics AAD SigninLogs Query (KQL)
// URL: https://learn.microsoft.com/en-us/azure/active-directory/external-identities/b2b-tutorial-require-mfa
let excludeapps = pack_array("Windows Sign In","Microsoft Authentication Broker","Microsoft Account Controls V2","Microsoft Intune Company Portal","Microsoft Mobile Application Management");
SigninLogs
| where TimeGenerated > ago(14d) and UserType == "Guest" and AppDisplayName !in (excludeapps)
| where ResultType == 0 and AuthenticationRequirement == "singleFactorAuthentication"
| where AADTenantId == ResourceTenantId
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement,Category, UserType
| summarize apps=make_list(AppDisplayName) by UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, Category, UserType
Comment
Guest users should be treated no differently than regular users. Having a guest register and use mfa adds an additional layer of security as there is no way to know if the guest's email is as secure. The results from this query show users that did not get prompted for MFA when logging into the tenant, review the list and exclude guest or applications that need to be excluded. One thing to note is the guest user may need a way outside of self service to reset their MFA.
- Link to Microsoft Documentation: Require compliant PCs and mobile devices
- This policy will require Intune
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group, Directory Role (Directory Sync Accounts), Guest
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps (or at minimum Office 365)
- Conditions
- Grant
- Grant Access
- Require device to be marked as compliant
- Require one of the selected controls
Log Analytics AAD SigninLogs Query (KQL)
let includeapps = pack_array("Exchange Online","Microsoft 365 Search Service","Microsoft Forms","Microsoft Planner","Microsoft Stream","Microsoft Teams","Microsoft To-Do","Microsoft Flow","Microsoft Office 365 Portal","Microsoft Office client application","Microsoft Stream","Microsoft To-Do WebApp","Microsoft Whiteboard Services","Office Delve","Office Online","OneDrive","Power Apps","Power Automate","Security & compliance portal","SharePoint Online","Skype for Business Online","Skype and Teams Tenant Admin API","Sway","Yammer");
AADNonInteractiveUserSignInLogs
| extend trustType = tostring(parse_json(DeviceDetail).trustType)
| extend isCompliant = tostring(parse_json(DeviceDetail).isCompliant)
| project-away DeviceDetail
| union SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0
| extend os = tostring(parse_json(DeviceDetail).operatingSystem)
| extend trustType = tostring(parse_json(DeviceDetail).trustType)
| extend isCompliant = tostring(parse_json(DeviceDetail).isCompliant)
| where AADTenantId == ResourceTenantId
| where ResourceDisplayName in (includeapps) or AppDisplayName in (includeapps)
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| extend os = tostring(parse_json(DeviceDetail).operatingSystem)
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, trustType,isCompliant,os,Category;
Comment
These results show users logging in with non compliant devices. The initial goal is to have it only apply to users using office 365, ultimately requiring it for all applications. Will want to review these results and determine the impact of this policy.
- Link to Microsoft Documentation: Common Conditional Access policy: Require reauthentication and disable browser persistence
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group,
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All cloud apps
- Exclude: None
- Conditions
- Filter for device
- device.isCompliant -ne True -or device.trustType -ne "ServerAD"
- Session
- Sign-in frequency: 1 Hour
- Persistent browser session: Never persistent
Log Analytics AAD SigninLogs Query (KQL)
SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and UserType <> "Guest"
| extend trustType = tostring(DeviceDetail.trustType)
| extend isCompliant = tostring(DeviceDetail.isCompliant)
| extend deviceName = tostring(DeviceDetail.displayName)
| extend os = tostring(DeviceDetail.operatingSystem)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined" and ClientAppUsed == "Browser"
| distinct UserPrincipalName, os, deviceName, trustType, isCompliant, TrustedLocation
Comment
This policy is to make sure that token session are limited on non trusted devices. This is to help prevent tokens from being compromised and replayed. The results show users that will be affected by this policy. Do not put this in if the device / compliance journey is still under works as users will be prompted frequently.
- Link to Microsoft Documentation: Common Conditional Access policy: Block legacy authentication
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud Apps
- Include: All Cloud Apps
- Conditions
- Client apps
- Exchange ActiveSync clients
- Other clients
- Grant
- Block Access
Log Analytics AAD SigninLogs Query (KQL)
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0
| extend ClientAppUsed = iff(isempty(ClientAppUsed) == true, "Unknown", ClientAppUsed)
| extend isLegacyAuth = case(ClientAppUsed contains "Browser", "No", ClientAppUsed contains "Mobile Apps and Desktop clients", "No", ClientAppUsed contains "Exchange ActiveSync", "Yes", ClientAppUsed contains "Exchange Online PowerShell","Yes", ClientAppUsed contains "Unknown", "Unknown", "Yes")
| where isLegacyAuth == "Yes"
| distinct UserDisplayName, UserPrincipalName, AppDisplayName, ClientAppUsed, isLegacyAuth, UserAgent, Category
Comment
These results show users using basic auth. Find users being called out in this and put them in as an exception while working with the account owners to stop using basic authentication.
- Link to Microsoft Documentation: Common Conditional Access policy: Require compliant or hybrid Azure AD joined device for administrators
- This policy will require Intune
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Directory Roles (Application Administrator,Authentication Administrator,Cloud Application Administrator,Conditional Access Administrator,Exchange Administrator,Global Administrator,Helpdesk Administrator,Hybrid Identity Administrator,Password Administrator,Privileged Authentication Administrator,Privileged Role Administrator,Security Administrator,SharePoint Administrator,User Administrator)
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Grant
- Grant Access
- Require device to be marked as compliant
- Require Hybrid Azure AD joined device
- Require one of the selected controls Get list of Privileged Users using PowerShell to use in the kql below
Connect-MgGraph
Select-MgProfile -Name beta
$roles = @("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator")
(Get-MgDirectoryRole -ExpandProperty members -all | where {$_.displayname -In $roles} | select -ExpandProperty members).id -join('","') | out-file .\privuser.txt
Log Analytics AAD SigninLogs Query (KQL) needs results from the PowerShell script
let privusers = pack_array("**replace this with the results from the privuser.txt found from the powershell cmdlets**");
SigninLogs
| where TimeGenerated > ago(14d) and UserId in~ (privusers) and ResultType == 0
| extend trustType = tostring(parse_json(DeviceDetail).trustType)
| extend isCompliant = tostring(parse_json(DeviceDetail).isCompliant)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| extend os = tostring(parse_json(DeviceDetail).operatingSystem)
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation, trustType,isCompliant,os, Category
Log Analytics AAD SigninLogs and AuditLogs PIM Query (KQL)
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = AuditLogs
| where TimeGenerated > ago(60d) and ActivityDisplayName == 'Add member to role completed (PIM activation)' and Category == "RoleManagement"
| extend Caller = tostring(InitiatedBy.user.userPrincipalName) | extend Role = tostring(TargetResources[0].displayName) | where Role in (privroles) | distinct Caller;
SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and ResultType == 0
| extend trustType = tostring(parse_json(DeviceDetail).trustType)
| extend isCompliant = tostring(parse_json(DeviceDetail).isCompliant)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| extend os = tostring(parse_json(DeviceDetail).operatingSystem)
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation, trustType,isCompliant,os, Category
Sentinel AAD SigninLogs Query (KQL) requires UEBA turned on
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = IdentityInfo | where TimeGenerated > ago(60d) and AssignedRoles != "[]" | mv-expand AssignedRoles | extend Roles = tostring(AssignedRoles) | where Roles in (privroles) | distinct AccountUPN
SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and ResultType == 0
| extend trustType = tostring(parse_json(DeviceDetail).trustType)
| extend isCompliant = tostring(parse_json(DeviceDetail).isCompliant)
| extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation',''))
| extend os = tostring(parse_json(DeviceDetail).operatingSystem)
| where isCompliant <> 'true' and trustType <> "Hybrid Azure AD joined"
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, TrustedLocation, trustType,isCompliant,os, Category
Comment
This shows privileged users that are logging in without a compliant devices. I have found most orgs are not to a point to be able to require this for all users. the goal is to focus on the privileged accounts to make sure an admin is not logging into random machines where tokens could be exploited.
- Link to Microsoft Documentation: Common Conditional Access policy: User risk-based password change
- This policy will require
- Ideally use a block over Change Password
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Guests, Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- User risk: High
- Grant
- Block Access
- Session
- Sign-in frequency
- Every time
Log Analytics AAD SigninLogs Query (KQL)
SigninLogs
| where TimeGenerated > ago(14d)
| where RiskState == "atRisk" and RiskLevelAggregated == "high"
| project AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail,IsRisky, RiskEventTypes_V2, MfaDetail, ConditionalAccessStatus, AuthenticationRequirement, ResultType
Comment
Having two conditional access policies one blocking high sign-in risk and one blocking high user risk is really important. The image below shows the importance of this. I took a user and logged into a tenant with a tor browser, This action alone only had a sign in risk of medium. Next I went to add a MFA into my profile and immediately the user risk went up to high and would have instantly blocked me if the policy was enabled preventing the "bad actor" from registering their own MFA creds.
- Link to Microsoft Documentation: Common Conditional Access policy: Sign-in risk-based multifactor authentication
- This policy will require
- Ideally use a block over MFA
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Guest, Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Sign-in risk: High
- Grant
- Block Access
- Session
- Sign-in frequency
- Every time
Log Analytics AAD SigninLogs Query (KQL)
SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and UserType <> "Guest"
| where RiskLevelDuringSignIn in ("high")
| project ResultType, ResultDescription,AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement
Comment
Microsoft documents say to require the user to MFA, But all of Microsoft actual security documents say that this should be a block instead. From personal experience and the guidance from the lapsus incidents is that this needs be a block action. Specially if spammable MFA is present and who knows what admins could be easily convinced to accept a mfa prompt for a price.
No image available, the results to this will be very similiar to the results from the Require MFA when sign-in risk is low, medium, or high query. The users with a high sign-in risk will be blocked. The goal will be to have a user adjust how they are logging in to make sure it does not come in as high. An orgonization should not want to leverage any exclusion on this and I find that most orgs have no issues putting this one in place.
- Link to Microsoft Documentation: change me
- This policy will require
- Ideally use a block over Change Password.
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All Users
- Exclude: Guest, Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Sign-in risk: High, Medium, Low
- Grant
- Require MFA
- Session
- Sign-in frequency
- Every time
Log Analytics AAD SigninLogs Query (KQL)
SigninLogs
| where TimeGenerated > ago(14d) and ResultType == 0 and UserType <> "Guest"
| where RiskLevelDuringSignIn in ("high","medium","low")
| distinct AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement
Comment
This policy will require anyone with a risky sign-in to have to provide mfa, The High will be blocked due to the other policy so the mfa will only occur for low and medium. The image below is an example of the results, It will more than likely be a lot more users in an actual production tenant.
- Link to Microsoft Documentation: change me
- This policy will require
- Ideally use a block over MFA
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Directory Roles (Application Administrator,Authentication Administrator,Cloud Application Administrator,Conditional Access Administrator,Exchange Administrator,Global Administrator,Helpdesk Administrator,Hybrid Identity Administrator,Password Administrator,Privileged Authentication Administrator,Privileged Role Administrator,Security Administrator,SharePoint Administrator,User Administrator)
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- User risk: High, Medium, Low
- Grant
- Block Access
- Session
- Sign-in frequency
- Every time
Get list of Privileged Users using PowerShell to use in the kql below
Connect-MgGraph
Select-MgProfile -Name beta
$roles = @("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator")
(Get-MgDirectoryRole -ExpandProperty members -all | where {$_.displayname -In $roles} | select -ExpandProperty members).id -join('","') | out-file .\privuser.txt
Log Analytics AAD SigninLogs Query (KQL) needs results from the PowerShell script
let privusers = pack_array("**replace this with the results from the privuser.txt found from the powershell cmdlets**");
SigninLogs
| where TimeGenerated > ago(14d) and UserId in~ (privusers) and RiskLevelAggregated in ("high","medium","low")
| project AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement, Category
Log Analytics AAD SigninLogs and AuditLogs PIM Query (KQL)
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = AuditLogs
| where TimeGenerated > ago(60d) and ActivityDisplayName == 'Add member to role completed (PIM activation)' and Category == "RoleManagement"
| extend Caller = tostring(InitiatedBy.user.userPrincipalName)
| extend Role = tostring(TargetResources[0].displayName)
| where Role in (privroles)
| distinct Caller;
SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and RiskLevelAggregated in ("high","medium","low")
| project AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement, Category
Sentinel AAD SigninLogs Query (KQL) requires UEBA turned on
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = IdentityInfo | where TimeGenerated > ago(60d) and AssignedRoles != "[]" | mv-expand AssignedRoles | extend Roles = tostring(AssignedRoles) | where Roles in (privroles) | distinct AccountUPN
SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and RiskLevelAggregated in ("high","medium","low")
| project AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement, Category
Comment
Privileged Role Members should not be allowed to log in when Identity Protection finds the user to be risky. These accounts are highly sensitive and there should be no question if they are secure or not.
The results from thw query finds users that would have triggered this issue. This is a harder query as every company is not using pim or sentinal. may need to use the all users query and research which ones are in the roles.
- Ideally use a block over MFA
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Directory Roles (Application Administrator,Authentication Administrator,Cloud Application Administrator,Conditional Access Administrator,Exchange Administrator,Global Administrator,Helpdesk Administrator,Hybrid Identity Administrator,Password Administrator,Privileged Authentication Administrator,Privileged Role Administrator,Security Administrator,SharePoint Administrator,User Administrator)
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Sign-in risk: High, Medium, Low
- Grant
- Block Access
- Session
- Sign-in frequency
- Every time
- Sign-in frequency
Get list of Privileged Users using PowerShell to use in the kql below
Connect-MgGraph
Select-MgProfile -Name beta
$roles = @("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator")
(Get-MgDirectoryRole -ExpandProperty members -all | where {$_.displayname -In $roles} | select -ExpandProperty members).id -join('","') | out-file .\privuser.txt
Log Analytics AAD SigninLogs Query (KQL) needs results from the PowerShell script
let privusers = pack_array("**replace this with the results from the privuser.txt found from the powershell cmdlets**");
SigninLogs
| where TimeGenerated > ago(14d) and UserId in~ (privusers) and RiskLevelDuringSignIn in ("high","medium","low")
| project AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement, Category
Log Analytics AAD SigninLogs and AuditLogs PIM Query (KQL)
//https://github.com/chadmcox/Azure_Active_Directory/blob/master/Log%20Analytics/Conditional%20Access%20Policy%20Impact%20KQL/Possible%20impact%20of%20Block%20privileged%20user%20if%20sign-in%20risk%20is%20low%20medium%20or%20high.kql
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = AuditLogs
| where TimeGenerated > ago(60d) and ActivityDisplayName == 'Add member to role completed (PIM activation)' and Category == "RoleManagement"
| extend Caller = tostring(InitiatedBy.user.userPrincipalName) | extend Role = tostring(TargetResources[0].displayName) | where Role in (privroles) | distinct Caller;
SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and RiskLevelDuringSignIn in ("high","medium","low")
| project AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement, Category
Sentinel AAD SigninLogs Query (KQL) requires UEBA turned on
let privroles = pack_array("Application Administrator","Authentication Administrator","Cloud Application Administrator","Conditional Access Administrator","Exchange Administrator","Global Administrator","Helpdesk Administrator","Hybrid Identity Administrator","Password Administrator","Privileged Authentication Administrator","Privileged Role Administrator","Security Administrator","SharePoint Administrator","User Administrator");
let privusers = IdentityInfo | where TimeGenerated > ago(60d) and AssignedRoles != "[]" | mv-expand AssignedRoles | extend Roles = tostring(AssignedRoles) | where Roles in (privroles) | distinct AccountUPN
SigninLogs
| where TimeGenerated > ago(14d) and UserPrincipalName in~ (privusers) and RiskLevelDuringSignIn in ("high","medium","low")
| project AppDisplayName, UserPrincipalName, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, RiskDetail, RiskEventTypes_V2, ConditionalAccessStatus, AuthenticationRequirement, Category
Comment
Privileged Role Members should not be allowed to log in when Identity Protection finds the sign-in to be risky. These accounts are highly sensitive and there should be no question if they are secure or not.
The results from thw query finds users that would have triggered this issue. This is a harder query as every company is not using pim or sentinal. may need to use the all users query and research which ones are in the roles.
- Link to Microsoft Documentation: NA
- This policy will require P2 License
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: Directory Roles (Directory Sync Account)
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Sign-in risk: High, Medium, Low
- Grant
- Block Access
- Session
- Sign-in frequency
- Every time
- Sign-in frequency
Log Analytics AAD SigninLogs Query (KQL)
//if an account is used other than the default one created by azure ad connect you will need to
//update the syncaccount variable with the other account name instead of sync_
let syncaccount = "sync_";
AADNonInteractiveUserSignInLogs
| union SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName startswith syncaccount
| where RiskLevelDuringSignIn in ("high","medium","low")
| project AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement,Category,RiskLevelDuringSignIn,RiskDetail
Comment
This query looks in the logs to see if the Azure AD Connect Sync Account is experiencing any sign in risk. Hopefully it is not.
No example to show with this one.
- Link to Microsoft Documentation: NA
- This policy will require P2 License
- Ideally use a block over MFA
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users
- Include: All guest and external users
- Exclude: Breakglass, Exclusion Group
- Cloud Apps or Actions
- Select what this policy applies to: Cloud apps
- Include: All Cloud Apps
- Exclude: None
- Conditions
- Sign-in risk: High, Medium, Low
- Grant
- Block Access
Ideally use a block over MFA, but MFA can be used if non spammable MFA is used
- Block Access
Log Analytics AAD SigninLogs Query (KQL)
SigninLogs | where TimeGenerated > ago(14d) and UserType == "Guest" and ResultType == 0
| where AADTenantId <> HomeTenantId
| where RiskLevelDuringSignIn in ("high","medium")
| distinct AppDisplayName,UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement,Category,RiskLevelDuringSignIn,RiskDetail
| summarize apps=make_list(AppDisplayName) by UserPrincipalName,ConditionalAccessStatus,AuthenticationRequirement, RiskLevelDuringSignIn,RiskDetail
Comment
The results of this query show guest from other tenants that may be impacted by this policy. The goal is if there is any chance an external guest account is trying to access a resource with any kind of risk that they need to bbe blocked. The guest user should be able to change how they are logging in or from and try again. This policy only looks at the risk during signin. This particular sign in risk was due to this guest account using a tor browser.
The results below show a guest account trying to sign into the Azure Portal with a signin risk of medium. Review the results and determine if this policy is going to cause any problems.
- Link to Microsoft Documentation: NA
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users or Workload identities
- What does this apply to? Workload identities
- Include: All owned service principals
- Exclude:
- Cloud Apps or Actions
- Select what this policy applies to: Cloud Apps
- Include: All cloud apps
- Conditions
- Locations
- Include: Any Location
- Exclude: All trusted locations
- Grant
- Block Access
Log Analytics AAD SigninLogs Query (KQL)
let trustedNamedLocations = SigninLogs | where TimeGenerated > ago(30d) | where ResultType == "0" | extend TrustedLocation = tostring(iff(NetworkLocationDetails contains 'trustedNamedLocation', 'trustedNamedLocation','')) | where TrustedLocation == "trustedNamedLocation" | distinct IPAddress;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0
| extend TrustedLocation = tostring(iff(IPAddress in (trustedNamedLocations), 'trustedNamedLocation',''))
| extend City = tostring(parse_json(LocationDetails).city)
| extend State = tostring(parse_json(LocationDetails).state)
| extend Country = tostring(parse_json(LocationDetails).countryOrRegion)
| distinct IPAddress, ServicePrincipalName, City, State, Country, TrustedLocation
| summarize spcountbyip = count() by IPAddress, City, State, Country, TrustedLocation
Comment
the AADServicePrincipalSignInLogs only have a subset of the useful properties provided unlike the user signinlogs.
In order to get the current list of trusted location, Had to pull in a unique list of IP's from the user Signinlogs. Then compare them to the list returned from the serviceprincipal logs. The results do very and some of the ip not showing as trusted could actually be trusted so you will want to research and confirm.
The goal is to look at the ones that are showing that they are coming from outside the trusted network and determine impact if they where blocked.
If trustedlocation column is empty that means the query was unable to find a matching ip from the user signin logs that were marked as trusted.
- Link to Microsoft Documentation: Conditional Access for workload identities
- This policy will require Workload Identities Premium licenses
Conditional Access Policy Setup
- Create Conditional Access Policy:
- Users or Workload identities
- What does this apply to? Workload identities
- Include: All owned service principals
- Exclude:
- Cloud Apps or Actions
- Select what this policy applies to: Cloud Apps
- Include: All cloud apps
- Conditions
- Service principal risk
- Include: High, Medium, Low
- Grant
- Block Access
Log Analytics AAD SigninLogs Query (KQL)
//nothing has been written yet to look into these logs
//ServicePrincipalRiskEvents
//RiskyServicePrincipals
-
CISA - Microsoft Azure Active Directory M365 Minimum Viable Secure Configuration Baseline
-
DEV-0537 criminal actor targeting organizations for data exfiltration and destruction
-
Advice for incident responders on recovery from systemic identity compromises
-
Token tactics: How to prevent, detect, and respond to cloud token theft
-
Understanding "Solorigate"'s Identity IOCs - for Identity Vendors and their customers.
-
Security roadmap - Top priorities for the first 30 days, 90 days, and beyond
-
Configure Conditional Access in Microsoft Defender for Endpoint
-
Recommended Microsoft Defender for Cloud Apps policies for SaaS apps
-
Policy recommendations for securing SharePoint sites and files
-
Policy recommendations for securing Teams chats, groups, and files
-
Enable Azure multifactor authentication for Azure Virtual Desktop
-
excludes "Azure Windows VM Sign-In" for Windows virtual machine in Azure
-
Recommendations for conditional access and multi-factor authentication in Microsoft Flow