Detection rules › Kusto
Suspicious granting of permissions to an account
Identifies IPs from which users grant access to other users on Azure resources and alerts when a previously unseen source IP address is used.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | T1098 Account Manipulation |
| Privilege Escalation | T1098 Account Manipulation, T1548 Abuse Elevation Control Mechanism |
Event coverage
Rule body kusto
id: b2c15736-b9eb-4dae-8b02-3016b6a45a32
name: Suspicious granting of permissions to an account
description: |
'Identifies IPs from which users grant access to other users on Azure resources and alerts when a previously unseen source IP address is used.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActivity
dataTypes:
- AzureActivity
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098
- T1548
query: |
let starttime = 14d;
let endtime = 1d;
// The number of operations above which an IP address is considered an unusual source of role assignment operations
let alertOperationThreshold = 5;
let AzureBuiltInRole = externaldata(Role:string,RoleDescription:string,ID:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/AzureBuiltInRole.csv"] with (format="csv", ignoreFirstRecord=True);
let createRoleAssignmentActivity = AzureActivity
| where OperationNameValue =~ "microsoft.authorization/roleassignments/write";
let RoleAssignedActivity = createRoleAssignmentActivity
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| summarize count() by CallerIpAddress, Caller, bin(TimeGenerated, 1d)
| where count_ >= alertOperationThreshold
// Returns all the records from the right side that don't have matches from the left.
| join kind = rightanti (
createRoleAssignmentActivity
| where TimeGenerated > ago(endtime)
| extend parsed_property = tostring(parse_json(Properties).requestbody)
| extend PrincipalId = case(parsed_property has_cs 'PrincipalId',parse_json(parsed_property).Properties.PrincipalId, parsed_property has_cs 'principalId',parse_json(parsed_property).properties.principalId,"")
| extend PrincipalType = case(parsed_property has_cs 'PrincipalType',parse_json(parsed_property).Properties.PrincipalType, parsed_property has_cs 'principalType',parse_json(parsed_property).properties.principalType, "")
| extend Scope = case(parsed_property has_cs 'Scope',parse_json(parsed_property).Properties.Scope, parsed_property has_cs 'scope',parse_json(parsed_property).properties.scope,"")
| extend RoleAddedDetails = case(parsed_property has_cs 'RoleDefinitionId',parse_json(parsed_property).Properties.RoleDefinitionId,parsed_property has_cs 'roleDefinitionId',parse_json(parsed_property).properties.roleDefinitionId,"")
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_set(TimeGenerated), ActivityStatusValue = make_set(ActivityStatusValue), CorrelationId = make_set(CorrelationId), ActivityCountByCallerIPAddress = count()
by ResourceId, CallerIpAddress, Caller, OperationNameValue, Resource, ResourceGroup, PrincipalId, PrincipalType, Scope, RoleAddedDetails
) on CallerIpAddress, Caller
| extend timestamp = StartTimeUtc, AccountCustomEntity = Caller, IPCustomEntity = CallerIpAddress;
let RoleAssignedActivitywithRoleDetails = RoleAssignedActivity
| extend RoleAssignedID = tostring(split(RoleAddedDetails, "/")[-1])
// Returns all matching records from left and right sides.
| join kind = inner (AzureBuiltInRole
) on $left.RoleAssignedID == $right.ID;
let CallerIPCountSummary = RoleAssignedActivitywithRoleDetails | summarize AssignmentCountbyCaller = count() by Caller, CallerIpAddress;
let RoleAssignedActivityWithCount = RoleAssignedActivitywithRoleDetails | join kind = inner (CallerIPCountSummary | project Caller, AssignmentCountbyCaller, CallerIpAddress) on Caller, CallerIpAddress;
RoleAssignedActivityWithCount
| summarize arg_max(StartTimeUtc, *) by PrincipalId, RoleAssignedID
// Returns all the records from the left side and only matching records from the right side.
| join kind = leftouter( IdentityInfo
| summarize arg_max(TimeGenerated, *) by AccountObjectId
) on $left.PrincipalId == $right.AccountObjectId
// Check if assignment count is greater than the threshold.
| where AssignmentCountbyCaller >= alertOperationThreshold
| project ActivityTimeStamp, OperationNameValue, Caller, CallerIpAddress, PrincipalId, RoleAssignedID, RoleAddedDetails, Role, RoleDescription, AccountUPN, AccountCreationTime, GroupMembership, UserType, ActivityStatusValue, ResourceGroup, PrincipalType, Scope, CorrelationId, timestamp, AccountCustomEntity, IPCustomEntity, AssignmentCountbyCaller
| extend Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Caller
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
version: 2.0.2
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 14d;
let endtime = 1d;
let alertOperationThreshold = 5;
Let binding: AzureBuiltInRole
let AzureBuiltInRole = externaldata(Role:string,RoleDescription:string,ID:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/AzureBuiltInRole.csv"] with (format="csv", ignoreFirstRecord=True);
Let binding: CallerIPCountSummary
let CallerIPCountSummary = RoleAssignedActivitywithRoleDetails | summarize AssignmentCountbyCaller = count() by Caller, CallerIpAddress;
Derived from RoleAssignedActivitywithRoleDetails.
The stages below define let RoleAssignedActivityWithCount (the rule's main pipeline source).
Stage 1: source
AzureActivity
Stage 2: where
| where OperationNameValue =~ "microsoft.authorization/roleassignments/write"
Stage 3: where
| where TimeGenerated between (ago(starttime) .. ago(endtime))
Stage 4: summarize
| summarize count() by CallerIpAddress, Caller, bin(TimeGenerated, 1d)
Stage 5: where
| where count_ >= alertOperationThreshold
Stage 6: join (negated)
| join kind = rightanti (
createRoleAssignmentActivity
| where TimeGenerated > ago(endtime)
| extend parsed_property = tostring(parse_json(Properties).requestbody)
| extend PrincipalId = case(parsed_property has_cs 'PrincipalId',parse_json(parsed_property).Properties.PrincipalId, parsed_property has_cs 'principalId',parse_json(parsed_property).properties.principalId,"")
| extend PrincipalType = case(parsed_property has_cs 'PrincipalType',parse_json(parsed_property).Properties.PrincipalType, parsed_property has_cs 'principalType',parse_json(parsed_property).properties.principalType, "")
| extend Scope = case(parsed_property has_cs 'Scope',parse_json(parsed_property).Properties.Scope, parsed_property has_cs 'scope',parse_json(parsed_property).properties.scope,"")
| extend RoleAddedDetails = case(parsed_property has_cs 'RoleDefinitionId',parse_json(parsed_property).Properties.RoleDefinitionId,parsed_property has_cs 'roleDefinitionId',parse_json(parsed_property).properties.roleDefinitionId,"")
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_set(TimeGenerated), ActivityStatusValue = make_set(ActivityStatusValue), CorrelationId = make_set(CorrelationId), ActivityCountByCallerIPAddress = count()
by ResourceId, CallerIpAddress, Caller, OperationNameValue, Resource, ResourceGroup, PrincipalId, PrincipalType, Scope, RoleAddedDetails
) on CallerIpAddress, Caller
Stage 7: extend
| extend timestamp = StartTimeUtc, AccountCustomEntity = Caller, IPCustomEntity = CallerIpAddress
Stage 8: extend
| extend RoleAssignedID = tostring(split(RoleAddedDetails, "/")[-1])
Stage 9: join
| join kind = inner (AzureBuiltInRole
) on $left.RoleAssignedID == $right.ID
Stage 10: join
| join kind = inner (CallerIPCountSummary | project Caller, AssignmentCountbyCaller, CallerIpAddress) on Caller, CallerIpAddress
The stages below run on RoleAssignedActivityWithCount (the outer pipeline).
Stage 11: summarize
RoleAssignedActivityWithCount
| summarize arg_max(StartTimeUtc, *) by PrincipalId, RoleAssignedID
Stage 12: join
| join kind = leftouter( IdentityInfo
| summarize arg_max(TimeGenerated, *) by AccountObjectId
) on $left.PrincipalId == $right.AccountObjectId
Stage 13: where
| where AssignmentCountbyCaller >= alertOperationThreshold
Stage 14: project
| project ActivityTimeStamp, OperationNameValue, Caller, CallerIpAddress, PrincipalId, RoleAssignedID, RoleAddedDetails, Role, RoleDescription, AccountUPN, AccountCreationTime, GroupMembership, UserType, ActivityStatusValue, ResourceGroup, PrincipalType, Scope, CorrelationId, timestamp, AccountCustomEntity, IPCustomEntity, AssignmentCountbyCaller
Stage 15: extend
| extend Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
OperationNameValue | eq | microsoft.authorization/roleassignments/write |
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 |
|---|---|---|
AssignmentCountbyCaller | ge |
|
OperationNameValue | eq |
|
count_ | ge |
|
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 |
|---|---|
AccountCreationTime | project |
AccountCustomEntity | project |
AccountUPN | project |
ActivityStatusValue | project |
ActivityTimeStamp | project |
AssignmentCountbyCaller | project |
Caller | project |
CallerIpAddress | project |
CorrelationId | project |
GroupMembership | project |
IPCustomEntity | project |
OperationNameValue | project |
PrincipalId | project |
PrincipalType | project |
ResourceGroup | project |
Role | project |
RoleAddedDetails | project |
RoleAssignedID | project |
RoleDescription | project |
Scope | project |
UserType | project |
timestamp | project |
Name | extend |
UPNSuffix | extend |