Service principal credential added by user granted privileged role in last 24 hours


Description

Identifies service principal credential additions by users who received Application Administrator or Global Administrator roles within the preceding 24 hours, consistent with immediate post-compromise privilege abuse.

Query · kql

let timeframe = 1d;
let roleWindow = 24h;
let lookback = 7d;
let PrivilegedRoles = dynamic([
    "Application Administrator",
    "Cloud Application Administrator",
    "Global Administrator",
    "Privileged Role Administrator"
]);
let RecentRoleGrants =
    AuditLogs
    | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(0h)
    | where OperationName in~ ("Add member to role.", "Add member to role")
    | where Result =~ "success"
    | extend NewRoleUser = tolower(tostring(TargetResources[0].userPrincipalName))
    | extend RoleName    = tostring(TargetResources[1].displayName)
    | where RoleName in~ (PrivilegedRoles)
    | where isnotempty(NewRoleUser)
    | project NewRoleUser, RoleName, RoleGrantedTime = TimeGenerated;
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName in~ (
      "Add service principal credentials",
      "Update application - Certificates and secrets management"
  )
| where Result =~ "success"
| extend ActorUpn     = tolower(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 TargetSpName = tostring(TargetResources[0].displayName)
| extend TargetSpId   = tostring(TargetResources[0].id)
| where isnotempty(ActorUpn)
| join kind=inner RecentRoleGrants on $left.ActorUpn == $right.NewRoleUser
| where TimeGenerated >= RoleGrantedTime and TimeGenerated <= RoleGrantedTime + roleWindow
| extend AccountName      = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), ActorUpn)
| extend AccountUPNSuffix = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
| project
    TimeGenerated,
    ActorUpn,
    AccountName,
    AccountUPNSuffix,
    RoleName,
    RoleGrantedTime,
    TargetSpName,
    TargetSpId,
    ActorIp,
    CorrelationId
| sort by TimeGenerated desc
Raw source Service principal credential added by user granted privileged role in last 24 hours · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: 661d71d1-98a4-464f-bb6b-fc3c39499b3f
name: Service principal credential added by user granted privileged role in last 24 hours
description: Identifies service principal credential additions by users who received Application Administrator or Global Administrator roles within the preceding 24 hours, consistent with immediate post-compromise privilege abuse.
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
relevantTechniques:
  - T1098.001
query: |
  let timeframe = 1d;
  let roleWindow = 24h;
  let lookback = 7d;
  let PrivilegedRoles = dynamic([
      "Application Administrator",
      "Cloud Application Administrator",
      "Global Administrator",
      "Privileged Role Administrator"
  ]);
  let RecentRoleGrants =
      AuditLogs
      | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(0h)
      | where OperationName in~ ("Add member to role.", "Add member to role")
      | where Result =~ "success"
      | extend NewRoleUser = tolower(tostring(TargetResources[0].userPrincipalName))
      | extend RoleName    = tostring(TargetResources[1].displayName)
      | where RoleName in~ (PrivilegedRoles)
      | where isnotempty(NewRoleUser)
      | project NewRoleUser, RoleName, RoleGrantedTime = TimeGenerated;
  AuditLogs
  | where TimeGenerated >= ago(timeframe)
  | where OperationName in~ (
        "Add service principal credentials",
        "Update application - Certificates and secrets management"
    )
  | where Result =~ "success"
  | extend ActorUpn     = tolower(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 TargetSpName = tostring(TargetResources[0].displayName)
  | extend TargetSpId   = tostring(TargetResources[0].id)
  | where isnotempty(ActorUpn)
  | join kind=inner RecentRoleGrants on $left.ActorUpn == $right.NewRoleUser
  | where TimeGenerated >= RoleGrantedTime and TimeGenerated <= RoleGrantedTime + roleWindow
  | extend AccountName      = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), ActorUpn)
  | extend AccountUPNSuffix = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
  | project
      TimeGenerated,
      ActorUpn,
      AccountName,
      AccountUPNSuffix,
      RoleName,
      RoleGrantedTime,
      TargetSpName,
      TargetSpId,
      ActorIp,
      CorrelationId
  | sort by 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
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.