Detection rules › Kusto

Suspicious granting of permissions to an account

Status
available
Severity
medium
Time window
1d
Group by
AccountObjectId, Caller, CallerIpAddress, ID, PrincipalId, RoleAssignedID
Source
github.com/Azure/Azure-Sentinel

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

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.

FieldKindExcluded values
OperationNameValueeqmicrosoft.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.

FieldKindValues
AssignmentCountbyCallerge
  • 5 transforms: cased
OperationNameValueeq
  • microsoft.authorization/roleassignments/write
count_ge
  • 5 transforms: cased

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.

FieldSource
AccountCreationTimeproject
AccountCustomEntityproject
AccountUPNproject
ActivityStatusValueproject
ActivityTimeStampproject
AssignmentCountbyCallerproject
Callerproject
CallerIpAddressproject
CorrelationIdproject
GroupMembershipproject
IPCustomEntityproject
OperationNameValueproject
PrincipalIdproject
PrincipalTypeproject
ResourceGroupproject
Roleproject
RoleAddedDetailsproject
RoleAssignedIDproject
RoleDescriptionproject
Scopeproject
UserTypeproject
timestampproject
Nameextend
UPNSuffixextend