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

Telemetry coverage

Rule body

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 createRoleAssignmentActivity, let RoleAssignedActivity, let RoleAssignedActivitywithRoleDetails and let RoleAssignedActivityWithCount are inlined into the numbered stages below.

Let binding: AzureBuiltInRole used in Stage 9

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 used in Stage 10

let CallerIPCountSummary = RoleAssignedActivitywithRoleDetails | summarize AssignmentCountbyCaller = count() by Caller, CallerIpAddress;

Stages 1 to 10 define let RoleAssignedActivityWithCount (the rule's main pipeline source); stages 11 to 15 run on it.

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

| 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

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])

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

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