Azure and Azure Active Directory Monitoring Use Cases

Wrangling data exposed by various Azure services is a daunting challenge. Because numerous tables exist with many available data types, finding the table with a particular Azure action or its associated query often proves difficult. Azure Monitor can aid you in this journey. Azure Sentinel comes with several preconfigured analytic detection rules. However, we still strongly feel that users roll up their proverbial sleeves and dig through some of the data available and create their own analytic/detection/informational rules using the available data. We often see clients struggle with monitoring use cases for their Azure environments. This blog post aims to assist in this critical area of Azure monitoring. In it, we cover some basic Azure monitoring use cases and provide queries for them in both (SPL) Splunk and (KQL) Sentinel formats. We have designed these queries as baselines for creating more comprehensive and complex queries. The queries are not meant to cover every use case nor different environment configurations.

The queries below take the following format:

  • [Action] This is the action that needs to occur to generate the logging telemetry
  • [Splunk Query] This is the query in Splunk (SPL) format, the index value will need to be tweaked if the Azure AD data is flowing to a different index
  • [Results] These are the Splunk query results
  • [Sentinel Query] Uses the same logic as the Splunk query, but in KQL format
  • [Results] These are the Sentinel Query results

Getting the Data

For Splunk, the following applications were used:

We used the Azure Active Directory and Azure Activity data connectors for Sentinel.

ATT&CK Mapping

Note that the following attack mappings are a best-effort attempt, given the pool of techniques found in the Azure AD attack matrix does not reflect a one-to-one alignment with the queries below.

This image was generated via ATT&CK Navigator

Action: Creating a user in Azure Active Directory – T1136.003

Splunk Query

index=aad activityDisplayName="Add user"
| spath output=SourceUserAgent path=additionalDetails{0}.value
| spath output=SourceUser path=initiatedBy.user.userPrincipalName
| spath output=DestinationUser path=targetResources{0}.userPrincipalName
| table activityDisplayName,SourceUserAgent,SourceUser,DestinationUser,result

Results

Sentinel Query

AuditLogs 
| where OperationName == "Add user"
| extend SourceUserAgent = tostring(AdditionalDetails[0].value)
| extend SourceUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend DestinationUser = tostring(TargetResources[0].userPrincipalName)
| project OperationName,SourceUserAgent,SourceUser,DestinationUser,Result

Results

Action: Deleting a user in Azure Active Directory – T1098

Splunk Query

index=aad activityDisplayName="Delete user"
| spath output=SourceUserAgent path=additionalDetails{0}.value
| spath output=SourceUser path=initiatedBy.user.userPrincipalName
| spath output=DestinationUser path=targetResources{0}.userPrincipalName
| table activityDisplayName,SourceUserAgent,SourceUser,DestinationUser,result

Results

Sentinel Query

AuditLogs
| where OperationName == "Delete user"
| extend SourceUserAgent = tostring(AdditionalDetails[0].value)
| extend SourceUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend DestinationUser = tostring(TargetResources[0].userPrincipalName)
| project OperationName,SourceUserAgent,SourceUser,DestinationUser,Result

Results

Action: Change Users Password – T1098

Splunk Query

index=aad activityDisplayName = "Reset password (by admin)"
| spath output=SourceUser path=initiatedBy.user.userPrincipalName
| spath output=DestinationUser path=targetResources{0}.userPrincipalName
| table activityDisplayName,SourceUser,DestinationUser,result

Results

Sentinel Query

AuditLogs
| where Category == "UserManagement"
| where OperationName == "Reset password (by admin)"
| extend SourceUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend DestinationUser = tostring(TargetResources[0].userPrincipalName)
| project OperationName,SourceUser,DestinationUser,Result,AADOperationType

Results

Action: Add Role Assignment to a Subscription – T1098.001

Splunk Query

index=aad sourcetype="mscs:azure:audit"
| spath output=Action path=properties.message
| search Action="Microsoft.Authorization/roleAssignments/write"
| spath output=RequestBody path=properties.requestbody
| spath input=RequestBody output=PrincipalType path=Properties.PrincipalType
| spath input=RequestBody output=PrincipalId path=Properties.PrincipalId
| spath input=RequestBody output=RoleDefinitionID path=Properties.RoleDefinitionId
| spath input=RequestBody output=Scope path=Properties.Scope
| spath output=clientIpAddress path=httpRequest.clientIpAddress
| eval RoleDefinitionID_Split = split(RoleDefinitionID,"/")
| eval PrincipalId = mvindex(RoleDefinitionID_Split,4)
| eval TargetUser = case(PrincipalId="acdd72a7-3385-48ef-bd42-f606fba81ae7","User1")
| eval RoleDefinitionIdTranslate = case(RoleDefinitionID="/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7","Reader")
| table caller,PrincipalType,PrincipalId,RoleDefinitionID,Scope,Action,clientIpAddress,TargetUser,RoleDefinitionIdTranslate

Results

Sentinel Query

AzureActivity
| where OperationNameValue == "Microsoft.Authorization/roleAssignments/write"
| extend PrincipalType = tostring(parse_json(tostring(parse_json(tostring(parse_json(Properties).requestbody)).Properties)).PrincipalType)
| extend PrincipalId = tostring(parse_json(tostring(parse_json(tostring(parse_json(Properties).requestbody)).Properties)).PrincipalId)
| extend TargetUser = case(PrincipalId =~ "7d0d811c-aa42-4369-954e-8c99dcd857a0","User1","User1")
| extend RoleDefinitionId = tostring(parse_json(tostring(parse_json(tostring(parse_json(Properties).requestbody)).Properties)).RoleDefinitionId)
| extend RoleDefinitionIdTranslate = case(RoleDefinitionId  =~ "acdd72a7-3385-48ef-bd42-f606fba81ae7","Reader","Reader")
| extend Scope = tostring(parse_json(tostring(parse_json(tostring(parse_json(Properties).requestbody)).Properties)).Scope)
| extend Action = tostring(parse_json(Authorization).action)
| extend AuthScope = tostring(parse_json(Authorization).scope)
| extend clientIpAddress = tostring(parse_json(HTTPRequest).clientIpAddress)
| where PrincipalType == "ServicePrincipal" or  PrincipalType == "User"
| project Caller,PrincipalType,PrincipalId,RoleDefinitionId,Scope,AuthScope,Action,clientIpAddress,TargetUser,RoleDefinitionIdTranslate

Results

Note, the role IDs can be looked up via this page: https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#reader

Action: Show Conditional Access Policies Applied on SignIn – T1078.004

Splunk Query

index=aad sourcetype="azure:aad:signin"
| eval OperationName = "Sign-in Activity"
| spath output=EnforcedGrantControls path=appliedConditionalAccessPolicies{0}.enforcedGrantControls{0}
| spath output=CADisplayName path=appliedConditionalAccessPolicies{0}.displayName
| table OperationName,userDisplayName,clientAppUsed,userPrincipalName,appDisplayName,conditionalAccessStatus,EnforcedGrantControls,CADisplayName

Results

Sentinel Query

SigninLogs 
| extend detail = tostring(parse_json(AuthenticationRequirementPolicies)[0].detail)
| extend CADisplayName = tostring(ConditionalAccessPolicies[0].displayName)
| extend EnforcedGrantControls = tostring(parse_json(tostring(ConditionalAccessPolicies[0].enforcedGrantControls))[0])
| project OperationName,Identity,ClientAppUsed,AlternateSignInName,AppDisplayName,ConditionalAccessStatus,EnforcedGrantControls,CADisplayName

Results

Note: These queries only display the Grant Controls, but the query can be tweaked to show other controls as well.

Action: New Application Registration – T1528

Splunk Query

index=aad activityDisplayName="Add application"
| spath output=UserAgent path=additionalDetails{0}.value
| spath output=UPN path=initiatedBy.user.userPrincipalName
| spath output=AppName path=targetResources{0}.displayName
| spath output=modifiedproperties path=targetResources{0}.modifiedProperties{0}.newValue
| spath input=modifiedproperties output=RedirectURL path={0}.Address
| table activityDisplayName,UserAgent,UPN,AppName,RedirectURL

Results

Sentinel Query

AuditLogs 
| where OperationName == "Add application"
| extend UserAgent = tostring(AdditionalDetails[0].value)
| extend UPN = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend AppName = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[3].newValue))[0])
| extend RedirectURL = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue))[0].Address)
| project OperationName,UserAgent,UPN,AppName,RedirectURL

Results

Action: Add Permission to Application – T1528

Splunk Query

index=aad activityDisplayName="Update application"
| spath output=UserAgent path=additionalDetails{0}.value
| spath output=UPN path=initiatedBy.user.userPrincipalName
| spath output=AppDisplayName path=targetResources{0}.displayName
| spath path=targetResources{0}.modifiedProperties{0}.newValue output=modifiedProperties
| spath input=modifiedProperties path={0}.RequiredAppPermissions{0}.EntitlementId output=EntitlementId0
| spath input=modifiedProperties path={0}.RequiredAppPermissions{1}.EntitlementId output=EntitlementId1
| eval EntitlementId0Translate = case(EntitlementId0="e1fe6dd8-ba31-4d61-89e7-88639da4683d","User.Read")
| eval EntitlementId1Translate = case(EntitlementId1="b0afded3-3588-46d8-8b3d-9842eff778da","AuditLog.Read.All")
| eval PermissionsGranted = EntitlementId0Translate + " " +EntitlementId1Translate
| table activityDisplayName,UserAgent,UPN,AppDisplayName,PermissionsGranted,result

Results

Sentinel Query

AuditLogs 
| where OperationName == "Update application"
| extend UserAgent = tostring(AdditionalDetails[0].value)
| extend UPN = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend AppDisplayName = tostring(TargetResources[0].displayName)
| extend EntitlementId0 = tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue))[0].RequiredAppPermissions))[0].EntitlementId)
| extend EntitlementId1 = tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue))[0].RequiredAppPermissions))[1].EntitlementId)
| extend EntitlementId0Translate = case(EntitlementId0 =~ "e1fe6dd8-ba31-4d61-89e7-88639da4683d","User.Read","User.Read")
| extend EntitlementId1Translate = case(EntitlementId1 =~ "b0afded3-3588-46d8-8b3d-9842eff778da","AuditLog.Read.All","AuditLog.Read.All")
| project OperationName,UserAgent,UPN,AppDisplayName,EntitlementId0Translate,EntitlementId1Translate,Result

Results

Note: https://gist.github.com/watahani/cd14196dc858f4c0d60a898b63a402bc can be used to translate the GUID entitlements values into human-readable format

Splunk Query

index=aad activityDisplayName="Consent to application"
| spath output=UserAgent path=additionalDetails{0}.value
| spath output=UPN path=initiatedBy.user.userPrincipalName
| spath output=AdminContext path=targetResources{0}.modifiedProperties{0}.displayName
| spath output=AdminContextValue path=targetResources{0}.modifiedProperties{0}.newValue
| eval AdminContextValue = replace(AdminContextValue,"\W","")
| spath output=OnBehalf path=targetResources{0}.modifiedProperties{2}.displayName
| spath output=OnBehalfValue path=targetResources{0}.modifiedProperties{2}.newValue
| eval OnBehalfValue = replace(OnBehalfValue,"\W","")
| spath output=ConsentType path=targetResources{0}.modifiedProperties{4}.newValue
| eval ConsentTypeSplit = split(ConsentType,",")
| eval ConsentType = mvindex(ConsentTypeSplit,4)
| eval Scope = mvindex(ConsentTypeSplit,5)
| eval Scope = replace(Scope,"]","")
| eval Scope = replace(Scope,"\"","")
| eval Scope = replace(Scope,";","")
| table activityDisplayName,UserAgent,UPN,AdminContext,AdminContextValue,OnBehalf,OnBehalfValue,ConsentType,Scope

Results

Sentinel Query

AuditLogs 
| where OperationName == "Consent to application"
| extend UserAgent = tostring(AdditionalDetails[0].value)
| extend UPN = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend AdminContext = tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].displayName)
| extend AdminContextValue = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue)))
| extend OnBehalf = tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[2].displayName)
| extend OnBehalfValue = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[2].newValue)))
| extend ConsentAction = tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[4].displayName)
| extend ConsentActionValue = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[4].newValue)))
| extend ConsentTypeFields = split(ConsentActionValue, ",")
| extend ConsentType = tostring(ConsentTypeFields[4])
| extend Scope = tostring(ConsentTypeFields[5])
| extend Scope = trim(@"[^\w]+",Scope)
| project OperationName,UserAgent,UPN,AdminContext,AdminContextValue,OnBehalf,OnBehalfValue,ConsentType,Scope

Results

Action: Provisioning a Virtual Machine – T1078.004

Splunk Query

index=aad 
| spath output=OperationNameValue path=operationName.value
| spath output=ActivityStatusValue path=eventName.value
| search OperationNameValue = "Microsoft.Compute/virtualMachines/write" AND ActivityStatusValue="BeginRequest"
| eval UPN = caller
| spath output=CallerIpAddress path=httpRequest.clientIpAddress
| spath output=VMName path=authorization.scope
| spath output=ResourceGroup path=resourceGroupName
| eval VMNameSplit = split(VMName,"/")
| eval VMName = mvindex(VMNameSplit,8)
| eval SubscriptionId = mvindex(VMNameSplit,2)
| table OperationNameValue,UPN,CallerIpAddress,ActivityStatusValue,VMName,SubscriptionId,ResourceGroup

Results

Sentinel Query

AzureActivity 
| where OperationNameValue == "MICROSOFT.COMPUTE/VIRTUALMACHINES/WRITE" and ActivityStatusValue == "Start"
| extend Scope = tostring(parse_json(Authorization).scope)
| extend UPN = tostring(parse_json(Claims).["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"])
| extend ActivityStatusValue = tostring(parse_json(Properties).activityStatusValue)
| extend VMName = tostring(parse_json(Properties).resource)
| extend ResourceGroup = tostring(parse_json(Properties).resourceGroup)
| project OperationNameValue,UPN,CallerIpAddress,ActivityStatusValue,VMName,SubscriptionId,ResourceGroup

Results

Action: User Registers MFA Device – T1098.001

Splunk Query

index=aad
| eval OperationName=activityDisplayName
| search OperationName = "Update user"
| spath output=NewValue path=targetResources{0}.modifiedProperties{1}.newValue
| search NewValue = *StrongAuthenticationPhoneAppDetail*
| spath output=Details path=targetResources{0}.modifiedProperties{0}.newValue
| spath input=Details output=DeviceName path={0}.DeviceName
| spath input=Details output=PhoneAppVersion path={0}.PhoneAppVersion
| spath output=userPrincipalName path=targetResources{0}.userPrincipalName
| table OperationName,NewValue,DeviceName,PhoneAppVersion,userPrincipalName

Results

Sentinel Query

AuditLogs
| where OperationName == "Update user"
| where parse_json(tostring(TargetResources[0].modifiedProperties))[0].displayName == "StrongAuthenticationPhoneAppDetail"
| extend displayName = tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].displayName)
| extend newValue = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].newValue)))
| extend DeviceName = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue))[0].DeviceName)
| extend PhoneAppVersion = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0].newValue))[0].PhoneAppVersion)
| extend userPrincipalName = tostring(TargetResources[0].userPrincipalName)
| project OperationName,displayName,newValue,DeviceName,PhoneAppVersion,userPrincipalName

Results

Monitoring for Abnormal User Behavior

Aside from the queries outlined above that look for specific events, we can also construct queries that look at behaviors. Let us consider the following scenario: a threat actor compromises a set of credentials for a user with SharePoint access. The threat actor then uses these credentials to browse SharePoint, searching various sites for documents containing passwords. Putting our threat hunting hat on, we can hypothesize the following:

  • More than one IP address will be used
  • More than one User Agent will be used
  • A SharePoint query will be performed
  • A file containing sensitive information will be accessed
  • There will be more than 1 SharePoint session per user

Not all these conditions need to be true for us to determine that some kind of “weird” activity took place. Likewise, some aspects of this activity may be more important than others. This is indeed a great use case for “Hyper Queries” – which you can read more about here: https://ateixei.medium.com/siem-hyper-queries-introduction-current-detection-methods-part-i-ii-13330b5137df. Let’s turn this into query form (using Splunk):

index=o365 sourcetype="o365:management:activity" Workload=SharePoint (UserAgent != "MSWAC" AND UserAgent != "ODMTADocCache\/0.0" AND UserAgent != "OneDriveMpc-Transform_Thumbnail\/1.0") AND UserId != "SHAREPOINT\\system"

| eventstats dc(AppAccessContext.AADSessionId) AS user_sessions BY UserId
| eventstats dc(UserAgent) AS user_agent_count BY UserId
| eventstats dc(ClientIP) AS client_ip_count BY UserId

| eval high_ua_count=case(user_agent_count>1,"yes")
| eval high_ip_count=case(client_ip_count>1,"yes")
| eval high_user_session_count=case(user_sessions>1,"yes")

| eval qualifiers=if(match(high_ua_count,"yes"), mvappend(qualifiers,"High User Agent Count Per User # score: 20"),qualifiers)
| eval qualifiers=if(match(high_user_session_count,"yes"), mvappend(qualifiers,"High User Session Count Per User # score: 20"),qualifiers)
| eval qualifiers=if(match(high_ip_count,"yes"), mvappend(qualifiers,"High IP Per User # score: 20"),qualifiers)
| eval qualifiers=if(match(Operation,"SearchQueryPerformed"), mvappend(qualifiers,"SharePoint Query Performed # score: 20"),qualifiers)
| eval qualifiers=if(match(SourceFileName,"SecretPasswords.docx"), mvappend(qualifiers,"Honeypot file viewed # score: 100"),qualifiers)

| rex field=qualifiers "(?<=score: )(?<score>(.*)(?=))"

| eventstats sum(score) AS score_total BY UserId

| search qualifiers=* AND high_ua_count=yes

| stats values(qualifiers),values(UserId),values(Operation),values(user_agent_count),values(SourceFileName),values(UserAgent),values(ClientIP),values(score_total) AS score BY UserId

And looking at our results, we see the various qualifiers that the query flagged, as well as all the SharePoint operations utilized.

Looking another example, this time looking at Azure application consent, we can – and have – alerted on a user registering an application and assigning permissions to it. But what if we don’t want to alert on this activity every time it happens, but we do want to know if an application is registered, admin consent is granted and a sensitive permission is used. Let’s take a look at the following query:

index=azure (activityDisplayName="Add application" OR activityDisplayName = "Consent to application" OR activityDisplayName="Update application")

| bin _time span=1h

| spath output=AppName path=targetResources{0}.displayName
| spath output=IsAdminConsent path=targetResources{0}.modifiedProperties{0}.newValue
| spath output=ConsentOnBehalfAll path=targetResources{0}.modifiedProperties{2}.newValue
| spath output=Permissions path=targetResources{0}.modifiedProperties{0}.newValue

| eval qualifiers=if(match(IsAdminConsent,"True"), mvappend(qualifiers,"Admin Consent Granted # score: 20"),qualifiers)
| eval qualifiers=if(match(ConsentOnBehalfAll,"True"), mvappend(qualifiers,"On behalf of All Consent Granted # score: 20"),qualifiers)
| eval qualifiers=if(match(Permissions,"810c84a8-4a9e-49e6-bf7d-12d183f40d01"), mvappend(qualifiers,"Sensitive Permissions (Mailbox Read All) Granted # score: 300"),qualifiers)

| rex field=qualifiers "(?<=score: )(?<score>(.*)(?=))"

| eventstats sum(score) AS score_total BY _time

| search qualifiers=*

| stats values(qualifiers),values(score_total),values(initiatedBy.user.userPrincipalName) AS src_user BY _time

Looking at the results, we see that our query flagged the various qualifiers, including granting admin consent, consenting on behalf of the organization and including a sensitive permissions:

These types of queries offer detection engineers with many “levers” to push and tweak in order to bubble up and highlight malicious activity. This dynamic is especially evident in cloud workloads like Azure and Office 365.

References: