Suspicious application consent for offline access
Id | 3533f74c-9207-4047-96e2-0eb9383be587 |
Rulename | Suspicious application consent for offline access |
Description | This will alert when a user consents to provide a previously-unknown Azure application with offline access via OAuth. Offline access will provide the Azure App with access to the listed resources without requiring two-factor authentication. Consent to applications with offline access and read capabilities should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome! For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities. |
Severity | Low |
Tactics | CredentialAccess |
Techniques | T1528 |
Required data connectors | AzureActiveDirectory |
Kind | Scheduled |
Query frequency | 1d |
Query period | 14d |
Trigger threshold | 0 |
Trigger operator | gt |
Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/SuspiciousOAuthApp_OfflineAccess.yaml |
Version | 1.0.2 |
Arm template | 3533f74c-9207-4047-96e2-0eb9383be587.json |
let detectionTime = 1d;
let joinLookback = 14d;
AuditLogs
| where TimeGenerated > ago(detectionTime)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Consent to application"
| where TargetResources has "offline"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppDisplayName = tostring(TargetResource.displayName),
AppClientId = tolower(tostring(TargetResource.id))
)
| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))
| mv-apply Properties=ModifiedProperties on
(
where Properties.displayName =~ "ConsentAction.Permissions"
| extend ConsentFull = tostring(Properties.newValue)
| extend ConsentFull = trim(@'"',tostring(ConsentFull))
)
| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *
| where ConsentFull has "offline_access" and ConsentFull has_any ("Files.Read", "Mail.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read", "Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All")
| where GrantConsentType != "AllPrincipals" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally
| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))
| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
| join kind = leftouter (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add service principal"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppClientId = tolower(TargetResource.id)
)
| mv-apply ModifiedProperties=TargetResource.modifiedProperties on
(
where ModifiedProperties.displayName =~ "AppAddress" and ModifiedProperties.newValue has "AddressType"
| extend AppReplyURLs = ModifiedProperties.newValue
)
| distinct AppClientId, tostring(AppReplyURLs)
)
on AppClientId
| join kind = innerunique (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
| extend GrantAuthentication = tostring(TargetResource.displayName)
)
| extend GrantOperation = OperationName
| project GrantAuthentication, GrantOperation, CorrelationId
) on CorrelationId
| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull
| extend Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
status: Available
id: 3533f74c-9207-4047-96e2-0eb9383be587
query: |
let detectionTime = 1d;
let joinLookback = 14d;
AuditLogs
| where TimeGenerated > ago(detectionTime)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Consent to application"
| where TargetResources has "offline"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppDisplayName = tostring(TargetResource.displayName),
AppClientId = tolower(tostring(TargetResource.id))
)
| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))
| mv-apply Properties=ModifiedProperties on
(
where Properties.displayName =~ "ConsentAction.Permissions"
| extend ConsentFull = tostring(Properties.newValue)
| extend ConsentFull = trim(@'"',tostring(ConsentFull))
)
| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *
| where ConsentFull has "offline_access" and ConsentFull has_any ("Files.Read", "Mail.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read", "Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All")
| where GrantConsentType != "AllPrincipals" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally
| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))
| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
| join kind = leftouter (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add service principal"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppClientId = tolower(TargetResource.id)
)
| mv-apply ModifiedProperties=TargetResource.modifiedProperties on
(
where ModifiedProperties.displayName =~ "AppAddress" and ModifiedProperties.newValue has "AddressType"
| extend AppReplyURLs = ModifiedProperties.newValue
)
| distinct AppClientId, tostring(AppReplyURLs)
)
on AppClientId
| join kind = innerunique (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
| extend GrantAuthentication = tostring(TargetResource.displayName)
)
| extend GrantOperation = OperationName
| project GrantAuthentication, GrantOperation, CorrelationId
) on CorrelationId
| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull
| extend Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/SuspiciousOAuthApp_OfflineAccess.yaml
description: |
'This will alert when a user consents to provide a previously-unknown Azure application with offline access via OAuth.
Offline access will provide the Azure App with access to the listed resources without requiring two-factor authentication.
Consent to applications with offline access and read capabilities should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
name: Suspicious application consent for offline access
relevantTechniques:
- T1528
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: GrantInitiatedByUserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: GrantInitiatedByAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: GrantInitiatedByAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: GrantIpAddress
triggerThreshold: 0
severity: Low
requiredDataConnectors:
- dataTypes:
- AuditLogs
connectorId: AzureActiveDirectory
queryFrequency: 1d
queryPeriod: 14d
version: 1.0.2
kind: Scheduled
tactics:
- CredentialAccess
triggerOperator: gt
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"apiVersion": "2024-01-01-preview",
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/3533f74c-9207-4047-96e2-0eb9383be587')]",
"kind": "Scheduled",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/3533f74c-9207-4047-96e2-0eb9383be587')]",
"properties": {
"alertRuleTemplateName": "3533f74c-9207-4047-96e2-0eb9383be587",
"customDetails": null,
"description": "'This will alert when a user consents to provide a previously-unknown Azure application with offline access via OAuth.\nOffline access will provide the Azure App with access to the listed resources without requiring two-factor authentication.\nConsent to applications with offline access and read capabilities should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!\nFor further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'\n",
"displayName": "Suspicious application consent for offline access",
"enabled": true,
"entityMappings": [
{
"entityType": "Account",
"fieldMappings": [
{
"columnName": "GrantInitiatedByUserPrincipalName",
"identifier": "FullName"
},
{
"columnName": "Name",
"identifier": "Name"
},
{
"columnName": "UPNSuffix",
"identifier": "UPNSuffix"
}
]
},
{
"entityType": "Account",
"fieldMappings": [
{
"columnName": "GrantInitiatedByAadUserId",
"identifier": "AadUserId"
}
]
},
{
"entityType": "Account",
"fieldMappings": [
{
"columnName": "GrantInitiatedByAppServicePrincipalId",
"identifier": "AadUserId"
}
]
},
{
"entityType": "IP",
"fieldMappings": [
{
"columnName": "GrantIpAddress",
"identifier": "Address"
}
]
}
],
"OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/SuspiciousOAuthApp_OfflineAccess.yaml",
"query": "let detectionTime = 1d;\nlet joinLookback = 14d;\nAuditLogs\n| where TimeGenerated > ago(detectionTime)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Consent to application\"\n| where TargetResources has \"offline\"\n| mv-apply TargetResource=TargetResources on \n (\n where TargetResource.type =~ \"ServicePrincipal\"\n | extend ModifiedProperties = TargetResource.modifiedProperties,\n AppDisplayName = tostring(TargetResource.displayName),\n AppClientId = tolower(tostring(TargetResource.id))\n )\n| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@\"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv\"] with (format=\"csv\")))\n| mv-apply Properties=ModifiedProperties on \n (\n where Properties.displayName =~ \"ConsentAction.Permissions\"\n | extend ConsentFull = tostring(Properties.newValue)\n | extend ConsentFull = trim(@'\"',tostring(ConsentFull))\n )\n| parse ConsentFull with * \"ConsentType: \" GrantConsentType \", Scope: \" GrantScope1 \"]\" *\n| where ConsentFull has \"offline_access\" and ConsentFull has_any (\"Files.Read\", \"Mail.Read\", \"Notes.Read\", \"ChannelMessage.Read\", \"Chat.Read\", \"TeamsActivity.Read\", \"Group.Read\", \"EWS.AccessAsUser.All\", \"EAS.AccessAsUser.All\")\n| where GrantConsentType != \"AllPrincipals\" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally\n| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)\n| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)\n| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)\n| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)\n| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))\n| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)\n| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ \"User-Agent\", AdditionalDetails[0].value, \"\"))\n| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId\n| join kind = leftouter (AuditLogs\n| where TimeGenerated > ago(joinLookback)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Add service principal\"\n| mv-apply TargetResource=TargetResources on \n (\n where TargetResource.type =~ \"ServicePrincipal\"\n | extend ModifiedProperties = TargetResource.modifiedProperties,\n AppClientId = tolower(TargetResource.id)\n )\n| mv-apply ModifiedProperties=TargetResource.modifiedProperties on \n (\n where ModifiedProperties.displayName =~ \"AppAddress\" and ModifiedProperties.newValue has \"AddressType\"\n | extend AppReplyURLs = ModifiedProperties.newValue\n )\n | distinct AppClientId, tostring(AppReplyURLs)\n)\non AppClientId\n| join kind = innerunique (AuditLogs\n| where TimeGenerated > ago(joinLookback)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Add OAuth2PermissionGrant\" or OperationName =~ \"Add delegated permission grant\"\n | mv-apply TargetResource=TargetResources on \n (\n where TargetResource.type =~ \"ServicePrincipal\" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)\n | extend GrantAuthentication = tostring(TargetResource.displayName)\n )\n| extend GrantOperation = OperationName\n| project GrantAuthentication, GrantOperation, CorrelationId\n) on CorrelationId\n| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull\n| extend Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])\n",
"queryFrequency": "P1D",
"queryPeriod": "P14D",
"severity": "Low",
"status": "Available",
"subTechniques": [],
"suppressionDuration": "PT1H",
"suppressionEnabled": false,
"tactics": [
"CredentialAccess"
],
"techniques": [
"T1528"
],
"templateVersion": "1.0.2",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0
},
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
}
]
}