← Back to SOC feed Coverage →

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

kql MEDIUM Azure-Sentinel
T1098.001
AuditLogs
backdoorcredential-thefthuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-05-26T11:00:01Z · Confidence: medium

Hunt Hypothesis

A user with recently granted privileged roles may have added a service principal credential to exfiltrate data or establish persistence. SOC teams should proactively hunt for this behavior as it indicates potential post-compromise activity leveraging elevated privileges.

KQL 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

Analytic Rule Definition

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: 

Required Data Sources

Sentinel TableNotes
AuditLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/AuditLogs/FreshRoleGrantedActorSpCredentialAdded.yaml