Detection rules › Kusto
Rare application consent
This will alert when the "Consent to application" operation occurs by a user that has not done this operation before or rarely does this. This could indicate that permissions to access the listed Azure App were provided to a malicious actor. Consent to application, Add service principal and Add OAuth2PermissionGrant should typically be rare events. This may help detect the Oauth2 attack that can be initiated by this publicly available tool - https://github.com/fireeye/PwnAuth For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | T1136 Create Account |
| Privilege Escalation | T1068 Exploitation for Privilege Escalation |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Entra-AuditLogs | _catch_all | Entra ID audit event (any operation) |
Rule body kusto
id: 83ba3057-9ea3-4759-bf6a-933f2e5bc7ee
name: Rare application consent
description: |
'This will alert when the "Consent to application" operation occurs by a user that has not done this operation before or rarely does this.
This could indicate that permissions to access the listed Azure App were provided to a malicious actor.
Consent to application, Add service principal and Add OAuth2PermissionGrant should typically be rare events.
This may help detect the Oauth2 attack that can be initiated by this publicly available tool - https://github.com/fireeye/PwnAuth
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 3
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1136
- T1068
query: |
let current = 1d;
let auditLookback = 7d;
// Setting threshold to 3 as a default, change as needed.
// Any operation that has been initiated by a user or app more than 3 times in the past 7 days will be excluded
let threshold = 3;
// Gather initial data from lookback period, excluding current, adjust current to more than a single day if no results
let AuditTrail = AuditLogs | where TimeGenerated >= ago(auditLookback) and TimeGenerated < ago(current)
// 2 other operations that can be part of malicious activity in this situation are
// "Add OAuth2PermissionGrant" and "Add service principal", extend the filter below to capture these too
| where OperationName has "Consent to application"
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName))
)
| summarize max(TimeGenerated), OperationCount = count() by OperationName, InitiatedBy, TargetResourceName
// only including operations initiated by a user or app that is above the threshold so we produce only rare and has not occurred in last 7 days
| where OperationCount > threshold;
// Gather current period of audit data
let RecentConsent = AuditLogs | where TimeGenerated >= ago(current)
| where OperationName has "Consent to application"
| extend IpAddress = case(
isnotempty(tostring(InitiatedBy.user.ipAddress)) and tostring(InitiatedBy.user.ipAddress) != 'null', tostring(InitiatedBy.user.ipAddress),
isnotempty(tostring(InitiatedBy.app.ipAddress)) and tostring(InitiatedBy.app.ipAddress) != 'null', tostring(InitiatedBy.app.ipAddress),
'Not Available')
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName)),
props = TargetResource.modifiedProperties
)
| parse props with * "ConsentType: " ConsentType "]" *
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
| project TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type;
// Exclude previously seen audit activity for "Consent to application" that was seen in the lookback period
// First for rare InitiatedBy
let RareConsentBy = RecentConsent | join kind= leftanti AuditTrail on OperationName, InitiatedBy
| extend Reason = "Previously unseen user consenting";
// Second for rare TargetResourceName
let RareConsentApp = RecentConsent | join kind= leftanti AuditTrail on OperationName, TargetResourceName
| extend Reason = "Previously unseen app granted consent";
RareConsentBy | union RareConsentApp
| summarize Reason = make_set(Reason,100) by TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type
| extend timestamp = TimeGenerated, Name = tolower(tostring(split(InitiatedBy,'@',0)[0])), UPNSuffix = tolower(tostring(split(InitiatedBy,'@',1)[0]))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatedBy
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: TargetResourceName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.1.5
kind: Scheduled
Stages and Predicates
Parameters
let current = 1d;
let auditLookback = 7d;
let threshold = 3;
Let binding: AuditTrail
let AuditTrail = AuditLogs | where TimeGenerated >= ago(auditLookback) and TimeGenerated < ago(current)
| where OperationName has "Consent to application"
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName))
)
| summarize max(TimeGenerated), OperationCount = count() by OperationName, InitiatedBy, TargetResourceName
| where OperationCount > threshold;
Derived from current, auditLookback, threshold.
Let binding: RareConsentApp
let RareConsentApp = RecentConsent | join kind= leftanti AuditTrail on OperationName, TargetResourceName
| extend Reason = "Previously unseen app granted consent";
Derived from AuditTrail, RecentConsent.
The stages below define let RareConsentBy (the rule's main pipeline source).
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated >= ago(current)
Stage 3: where
| where OperationName has "Consent to application"
Stage 4: extend
| extend IpAddress = case(
isnotempty(tostring(InitiatedBy.user.ipAddress)) and tostring(InitiatedBy.user.ipAddress) != 'null', tostring(InitiatedBy.user.ipAddress),
isnotempty(tostring(InitiatedBy.app.ipAddress)) and tostring(InitiatedBy.app.ipAddress) != 'null', tostring(InitiatedBy.app.ipAddress),
'Not Available')
IpAddress =ipAddress != "null"tostring(InitiatedBy.user.ipAddress)ipAddress != "null"tostring(InitiatedBy.app.ipAddress)'Not Available'Stage 5: extend
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
InitiatedBy =/* macro: isnotempty(tostring(InitiatedBy.user.userPrincipalName)) */tostring(InitiatedBy.user.userPrincipalName)tostring(InitiatedBy.app.displayName)Stage 6: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName)),
props = TargetResource.modifiedProperties
)
Stage 7: parse
| parse props with * "ConsentType: " ConsentType "]" *
Stage 8: kusto:mv-apply
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
Stage 9: project
| project TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type
Stage 10: join (negated)
| join kind= leftanti AuditTrail on OperationName, InitiatedBy
Stage 11: extend
| extend Reason = "Previously unseen user consenting"
The stages below run on RareConsentBy (the outer pipeline).
Stage 12: union
RareConsentBy
| union
Stage 13: source
AuditLogs
Stage 14: where
| where TimeGenerated >= ago(current)
Stage 15: where
| where OperationName has "Consent to application"
Stage 16: extend
| extend IpAddress = case(
isnotempty(tostring(InitiatedBy.user.ipAddress)) and tostring(InitiatedBy.user.ipAddress) != 'null', tostring(InitiatedBy.user.ipAddress),
isnotempty(tostring(InitiatedBy.app.ipAddress)) and tostring(InitiatedBy.app.ipAddress) != 'null', tostring(InitiatedBy.app.ipAddress),
'Not Available')
IpAddress =ipAddress != "null"tostring(InitiatedBy.user.ipAddress)ipAddress != "null"tostring(InitiatedBy.app.ipAddress)'Not Available'Stage 17: extend
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
InitiatedBy =/* macro: isnotempty(tostring(InitiatedBy.user.userPrincipalName)) */tostring(InitiatedBy.user.userPrincipalName)tostring(InitiatedBy.app.displayName)Stage 18: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName)),
props = TargetResource.modifiedProperties
)
Stage 19: parse
| parse props with * "ConsentType: " ConsentType "]" *
Stage 20: kusto:mv-apply
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
Stage 21: project
| project TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type
Stage 22: join (negated)
| join kind= leftanti AuditTrail on OperationName, TargetResourceName
Stage 23: extend
| extend Reason = "Previously unseen app granted consent"
Stage 24: summarize
| summarize Reason = make_set(Reason,100) by TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type
Stage 25: extend
| extend timestamp = TimeGenerated, Name = tolower(tostring(split(InitiatedBy,'@',0)[0])), UPNSuffix = tolower(tostring(split(InitiatedBy,'@',1)[0]))
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
OperationCount | gt | 3 |
OperationName | match | Consent to application |
type | eq | ServicePrincipal |
OperationCount | gt | 3 |
OperationName | match | Consent to application |
type | eq | ServicePrincipal |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
OperationName | match |
|
key | eq |
|
type | eq |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
Category | summarize |
ConsentType | summarize |
CorrelationId | summarize |
InitiatedBy | summarize |
IpAddress | summarize |
OperationName | summarize |
Reason | summarize |
TargetResourceName | summarize |
TimeGenerated | summarize |
Type | summarize |
UserAgent | summarize |
Name | extend |
UPNSuffix | extend |
timestamp | extend |