Detection rules › Kusto

Dataverse - Suspicious security role modifications

Status
available
Severity
medium
Time window
14d
Group by
DeletedRoleID, MemberAddedRoleId, MemberRemovedRoleId, RoleId
Source
github.com/Azure/Azure-Sentinel

Identifies an unusual pattern of events whereby a new role is created followed by the creator adding members to the role and subsequently removing the member or deleting the role after a short time period.

MITRE ATT&CK coverage

Rule body kusto

id: e44a58b2-b63a-4eb9-92da-85660d73495c
kind: Scheduled
name: Dataverse - Suspicious security role modifications
description: Identifies an unusual pattern of events whereby a new role is created
  followed by the creator adding members to the role and subsequently removing the
  member or deleting the role after a short time period.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1404
  - T1626
  - T1548
query: |
  let role_create_watch_period = 2d;
  let query_frequency = 1h;
  let role_create_add_events= DataverseActivity
      | where Message == "Create" and EntityName == "role"
      | mv-expand Role = Fields
      | extend RoleName = Role.Value
      | where Role.Name == "name"
      | mv-expand Role = Fields
      | extend RoleCreateTime = TimeGenerated, RoleId = tostring(Role.Value)
      | where Role.Name == "roleid"
      | join kind=inner (
          DataverseActivity
          | where Message == "Associate" and EntityName == "systemuser"
          | mv-expand Role = Fields
          | where Role.Name == "role"
          | extend RoleMemberAddedTime = TimeGenerated, MemberAddedRoleId = tostring(Role.Value))
          on $left.RoleId == $right.MemberAddedRoleId, InstanceUrl, UserId
      | where RoleMemberAddedTime between (RoleCreateTime .. (RoleCreateTime + role_create_watch_period));
  let remove_role_member_events = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "Disassociate" and EntityName == "systemuser"
      | mv-expand Role = Fields
      | where Role.Name == "role"
      | extend ActionTime = TimeGenerated, MemberRemovedRoleId = tostring(Role.Value);
  let role_delete_events = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "Delete" and EntityName == "role"
      | extend DeletedRoleID = EntityId, Action = "Role deleted within defined time window"
      | project Action, ActionTime = TimeGenerated, UserId, ClientIp, DeletedRoleID, InstanceUrl;
  let role_member_removals = role_create_add_events
      | join kind=inner (remove_role_member_events) on $left.RoleId == $right.MemberRemovedRoleId
      | where ActionTime between (RoleCreateTime .. (RoleCreateTime + role_create_watch_period))
      | extend Action = "Role membership removed within defined time window";
  let role_deletions = role_create_add_events
      | join kind=inner (role_delete_events) on $left.RoleId == $right.DeletedRoleID
      | where ActionTime between (RoleCreateTime .. (RoleCreateTime + role_create_watch_period));
  union isfuzzy=true role_member_removals, role_deletions
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      UserId,
      InstanceUrl,
      ClientIp,
      Action,
      RoleCreateTime,
      RoleName,
      ActionTime,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: Dataverse - suspicious role modifications in {{InstanceUrl}}
  alertDescriptionFormat: 'The following action ocurred following role modifications
    changes in {{InstanceUrl}}: {{Action}}.'
  alertSeverityColumnName: Severity
version: 3.2.0

Stages and Predicates

Parameters

let role_create_watch_period = 2d;
let query_frequency = 1h;

Let binding: role_create_add_events

let role_create_add_events = DataverseActivity
    | where Message == "Create" and EntityName == "role"
    | mv-expand Role = Fields
    | extend RoleName = Role.Value
    | where Role.Name == "name"
    | mv-expand Role = Fields
    | extend RoleCreateTime = TimeGenerated, RoleId = tostring(Role.Value)
    | where Role.Name == "roleid"
    | join kind=inner (
        DataverseActivity
        | where Message == "Associate" and EntityName == "systemuser"
        | mv-expand Role = Fields
        | where Role.Name == "role"
        | extend RoleMemberAddedTime = TimeGenerated, MemberAddedRoleId = tostring(Role.Value))
        on $left.RoleId == $right.MemberAddedRoleId, InstanceUrl, UserId
    | where RoleMemberAddedTime between (RoleCreateTime .. (RoleCreateTime + role_create_watch_period));

Derived from role_create_watch_period.

Let binding: remove_role_member_events

let remove_role_member_events = DataverseActivity
    | where TimeGenerated >= ago(query_frequency)
    | where Message == "Disassociate" and EntityName == "systemuser"
    | mv-expand Role = Fields
    | where Role.Name == "role"
    | extend ActionTime = TimeGenerated, MemberRemovedRoleId = tostring(Role.Value);

Derived from query_frequency.

Let binding: role_delete_events

let role_delete_events = DataverseActivity
    | where TimeGenerated >= ago(query_frequency)
    | where Message == "Delete" and EntityName == "role"
    | extend DeletedRoleID = EntityId, Action = "Role deleted within defined time window"
    | project Action, ActionTime = TimeGenerated, UserId, ClientIp, DeletedRoleID, InstanceUrl;

Derived from query_frequency.

Let binding: role_member_removals

let role_member_removals = role_create_add_events
    | join kind=inner (remove_role_member_events) on $left.RoleId == $right.MemberRemovedRoleId
    | where ActionTime between (RoleCreateTime .. (RoleCreateTime + role_create_watch_period))
    | extend Action = "Role membership removed within defined time window";

Derived from role_create_watch_period, role_create_add_events, remove_role_member_events.

Let binding: role_deletions

let role_deletions = role_create_add_events
    | join kind=inner (role_delete_events) on $left.RoleId == $right.DeletedRoleID
    | where ActionTime between (RoleCreateTime .. (RoleCreateTime + role_create_watch_period));

Derived from role_create_watch_period, role_create_add_events, role_delete_events.

union isfuzzy=true (2 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: role_member_removals, role_deletions

Leg 1: role_member_removals

Leg 2: role_deletions

Applied to the combined result

| extend
    CloudAppId = int(32780),
    AccountName = tostring(split(UserId, '@')[0]),
    UPNSuffix = tostring(split(UserId, '@')[1]) | project
    UserId,
    InstanceUrl,
    ClientIp,
    Action,
    RoleCreateTime,
    RoleName,
    ActionTime,
    CloudAppId,
    AccountName,
    UPNSuffix

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
EntityNameeq
  • role transforms: cased
  • systemuser transforms: cased corpus 3 (kusto 3)
Messageeq
  • Associate transforms: cased
  • Create transforms: cased corpus 2 (kusto 2)
  • Delete transforms: cased corpus 2 (kusto 2)
  • Disassociate transforms: cased
Nameeq
  • name transforms: cased
  • role transforms: cased
  • roleid 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
AccountNameproject
Actionproject
ActionTimeproject
ClientIpproject
CloudAppIdproject
InstanceUrlproject
RoleCreateTimeproject
RoleNameproject
UPNSuffixproject
UserIdproject