Tenant default user permissions (authorization policy) changed


Description

Identifies changes to the tenant authorization policy, which sets what ordinary users can do by default (create apps, groups or tenants, invite guests, consent to applications), with the old and new value of each setting that changed.

Query · kql

let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
// Property names of the Microsoft Graph authorizationPolicy resource that widen what ordinary users can do
let SensitiveSettings = @"AllowedToCreate|PermissionGrant|GuestUserRole|AllowInvitesFrom|AllowedToSignUpEmailBasedSubscriptions|AllowEmailVerifiedUsersToJoinOrganization|BlockMsolPowerShell";
AuditLogs
| where TimeGenerated between (starttime .. endtime)
| where OperationName =~ "Update authorization policy"
| where Result =~ "success"
// Every TargetResources entry and every modified property is expanded instead of reading a fixed index
| mv-apply TargetResource = TargetResources on (
      mv-expand Property = TargetResource.modifiedProperties
      | extend PropertyName = tostring(Property.displayName)
      | extend OldValue = trim('"', tostring(Property.oldValue))
      | extend NewValue = trim('"', tostring(Property.newValue))
  )
| where PropertyName !~ "Included Updated Properties"
| where OldValue != NewValue
// "[]" is how the log shows a setting that had no recorded value, except for the permission grant policy list,
// where an empty list is a real value, so going from "[]" to a policy there is a change
| extend ChangeKind = iff(OldValue == "" or (OldValue == "[]" and PropertyName !contains "PermissionGrantPolicyIds"), "First recorded value", "Changed")
// For list values (permission grant policies, roles) show what was added and removed
| extend OldItems = iff(OldValue startswith "[", todynamic(OldValue), dynamic([]))
| extend NewItems = iff(NewValue startswith "[", todynamic(NewValue), dynamic([]))
| extend AddedItems = set_difference(NewItems, OldItems)
| extend RemovedItems = set_difference(OldItems, NewItems)
| extend SensitiveSetting = PropertyName matches regex strcat("(?i)", SensitiveSettings)
| extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend Actor    = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend ActorIp  = iff(
      isnotempty(tostring(InitiatedBy.user.ipAddress)),
      tostring(InitiatedBy.user.ipAddress),
      tostring(InitiatedBy.app.ipAddress))
| extend AccountName      = tostring(split(ActorUpn, "@")[0])
| extend AccountUPNSuffix = tostring(split(ActorUpn, "@")[1])
| project
    TimeGenerated,
    OperationName,
    PropertyName,
    ChangeKind,
    SensitiveSetting,
    OldValue,
    NewValue,
    AddedItems,
    RemovedItems,
    Actor,
    ActorUpn,
    AccountName,
    AccountUPNSuffix,
    ActorIp,
    CorrelationId
| sort by SensitiveSetting desc, ChangeKind asc, TimeGenerated desc
Raw source Tenant default user permissions (authorization policy) changed · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: 9d54d86c-a3ad-462f-b0fe-7bdf6ff5e0df
name: Tenant default user permissions (authorization policy) changed
description: |
  Identifies changes to the tenant authorization policy, which sets what ordinary users can do by default (create apps, groups or tenants, invite guests, consent to applications), with the old and new value of each setting that changed.
description-detailed: |
  The authorization policy is the tenant-wide switchboard for default user permissions. Each setting is a
  persistence or privilege path for anyone who controls a single ordinary account once it is loosened: letting
  users register applications gives them a place to hold credentials, widening the permission grant policies
  assigned to the default user role lets users consent to applications, and relaxing who may invite guests
  opens the tenant to external accounts. The audit log records each save as an "Update authorization policy"
  event, with the old and new value of the changed setting in the modified properties, for example
  "DefaultUserRolePermissions.AllowedToCreateApps" going from true to false.
  Two behaviors seen in a lab tenant shape the query. First, the first save after a long time can return a full
  snapshot, with settings whose old value is empty and whose new value is their current value, next to the one
  that actually changed; those rows are labeled "First recorded value" so that they are not read as changes,
  and a change from a previous value is labeled "Changed". Second, saving the user settings page in the portal
  rewrote the permission grant policies assigned to the default user role as a side effect: in the lab the
  list gained the two policies that manage permission grants for owned Teams and chats, which the administrator
  had not touched. For settings whose value is a list, the added and removed elements are therefore shown
  separately, so that a harmless side effect can be told from a policy that lets users consent to any
  application. The meta property "Included Updated Properties" only lists names and is left out. A save is
  also written next to "Add role from template" and "Update role" events by the same actor in the same second;
  they are different operations and are deliberately not matched, but they are useful context when reading
  the result.
  Rows for settings that control what ordinary users or guests can do are marked as sensitive. The match uses the names of
  the properties of the Microsoft Graph authorizationPolicy resource (created apps, security groups and
  tenants, permission grant policies, guest role, who can invite, email sign-up, Msol PowerShell). Only some of
  those names were observed in the log (AllowedToCreateApps, PermissionGrantPolicyIdsAssignedToDefaultUserRole,
  GuestUserRole, BlockMsolPowerShell); the others are taken from the documentation, so a sensitive setting may
  be listed without the mark if the audit log spells it differently. Every returned row is still a change.
  This query applies no allow-list, so a planned change must be ruled out against the change calendar. Treat a
  change made outside a change window, by an actor who does not normally administer the tenant, or close to a
  change in the two companion hunting queries of this pack (authentication methods policy and device
  registration policy), as high priority. The same operation can also be observed in CloudAppEvents; see the
  existing hunting query on policy configuration changes in that table.
  References:
  - https://learn.microsoft.com/entra/identity/enterprise-apps/configure-user-consent
  - https://learn.microsoft.com/graph/api/resources/authorizationpolicy
  - https://learn.microsoft.com/entra/identity/monitoring-health/reference-audit-activities
  - https://attack.mitre.org/techniques/T1098/
  - https://attack.mitre.org/techniques/T1528/
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
  - PrivilegeEscalation
  - CredentialAccess
relevantTechniques:
  - T1098
  - T1528
query: |
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  // Property names of the Microsoft Graph authorizationPolicy resource that widen what ordinary users can do
  let SensitiveSettings = @"AllowedToCreate|PermissionGrant|GuestUserRole|AllowInvitesFrom|AllowedToSignUpEmailBasedSubscriptions|AllowEmailVerifiedUsersToJoinOrganization|BlockMsolPowerShell";
  AuditLogs
  | where TimeGenerated between (starttime .. endtime)
  | where OperationName =~ "Update authorization policy"
  | where Result =~ "success"
  // Every TargetResources entry and every modified property is expanded instead of reading a fixed index
  | mv-apply TargetResource = TargetResources on (
        mv-expand Property = TargetResource.modifiedProperties
        | extend PropertyName = tostring(Property.displayName)
        | extend OldValue = trim('"', tostring(Property.oldValue))
        | extend NewValue = trim('"', tostring(Property.newValue))
    )
  | where PropertyName !~ "Included Updated Properties"
  | where OldValue != NewValue
  // "[]" is how the log shows a setting that had no recorded value, except for the permission grant policy list,
  // where an empty list is a real value, so going from "[]" to a policy there is a change
  | extend ChangeKind = iff(OldValue == "" or (OldValue == "[]" and PropertyName !contains "PermissionGrantPolicyIds"), "First recorded value", "Changed")
  // For list values (permission grant policies, roles) show what was added and removed
  | extend OldItems = iff(OldValue startswith "[", todynamic(OldValue), dynamic([]))
  | extend NewItems = iff(NewValue startswith "[", todynamic(NewValue), dynamic([]))
  | extend AddedItems = set_difference(NewItems, OldItems)
  | extend RemovedItems = set_difference(OldItems, NewItems)
  | extend SensitiveSetting = PropertyName matches regex strcat("(?i)", SensitiveSettings)
  | extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
  | extend ActorApp = tostring(InitiatedBy.app.displayName)
  | extend Actor    = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
  | extend ActorIp  = iff(
        isnotempty(tostring(InitiatedBy.user.ipAddress)),
        tostring(InitiatedBy.user.ipAddress),
        tostring(InitiatedBy.app.ipAddress))
  | extend AccountName      = tostring(split(ActorUpn, "@")[0])
  | extend AccountUPNSuffix = tostring(split(ActorUpn, "@")[1])
  | project
      TimeGenerated,
      OperationName,
      PropertyName,
      ChangeKind,
      SensitiveSetting,
      OldValue,
      NewValue,
      AddedItems,
      RemovedItems,
      Actor,
      ActorUpn,
      AccountName,
      AccountUPNSuffix,
      ActorIp,
      CorrelationId
  | sort by SensitiveSetting desc, ChangeKind asc, TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUpn
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ActorIp
customDetails:
  PropertyName: PropertyName
  ChangeKind: ChangeKind
  SensitiveSetting: SensitiveSetting
version: 1.0.0
metadata:
    source:
        kind: Community
    author:
        name: descambiado
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection", "Identity" ]

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.