← Back to SOC feed Coverage →

Service principal credential addition by non-historical actor

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-21T23:00:01Z · Confidence: medium

Hunt Hypothesis

A new service principal credential addition by an actor with no prior history of this activity may indicate an adversary attempting to establish persistent access. SOC teams should proactively hunt for this behavior in Azure Sentinel to detect potential credential compromise and unauthorized access attempts.

KQL Query

let timeframe = 1d;
let lookback   = 90d;
let KnownActors =
    AuditLogs
    | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(timeframe)
    | where OperationName in~ (
          "Add service principal credentials",
          "Update application - Certificates and secrets management"
      )
    | where Result =~ "success"
    | extend Actor = iff(
          isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
          tostring(InitiatedBy.user.userPrincipalName),
          tostring(InitiatedBy.app.displayName))
    | where isnotempty(Actor)
    | distinct Actor;
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName in~ (
      "Add service principal credentials",
      "Update application - Certificates and secrets management"
  )
| where Result =~ "success"
| 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 TargetSP   = tostring(TargetResources[0].displayName)
| extend TargetSpId = tostring(TargetResources[0].id)
| where isnotempty(Actor)
| join kind=leftanti KnownActors on Actor
| extend AccountName      = iff(Actor has "@", tostring(split(Actor, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
| project TimeGenerated, TargetSP, TargetSpId, Actor, AccountName,
          AccountUPNSuffix, ActorIp, OperationName, CorrelationId
| sort by TimeGenerated desc

Analytic Rule Definition

id: 4519bc3b-1849-4f37-b98b-6e8d67b34c71
name: Service principal credential addition by non-historical actor
description: |
  Identifies service principal credential additions or updates by actors with no history
  of this operation in the preceding 90 days. A new actor outside the established
  baseline may indicate credential abuse by a compromised account.
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
relevantTechniques:
  - T1098.001
query: |
  let timeframe = 1d;
  let lookback   = 90d;
  let KnownActors =
      AuditLogs
      | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(timeframe)
      | where OperationName in~ (
            "Add service principal credentials",
            "Update application - Certificates and secrets management"
        )
      | where Result =~ "success"
      | extend Actor = iff(
            isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
            tostring(InitiatedBy.user.userPrincipalName),
            tostring(InitiatedBy.app.displayName))
      | where isnotempty(Actor)
      | distinct Actor;
  AuditLogs
  | where TimeGenerated >= ago(timeframe)
  | where OperationName in~ (
        "Add service principal credentials",
        "Update application - Certificates and secrets management"
    )
  | where Result =~ "success"
  | 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 TargetSP   = tostring(TargetResources[0].displayName)
  | extend TargetSpId = tostring(TargetResources[0].id)
  | where isnotempty(Actor)
  | join kind=leftanti KnownActors on Actor
  | extend AccountName      = iff(Actor has "@", tostring(split(Actor, "@")[0]), Actor)
  | extend AccountUPNSuffix = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
  | project TimeGenerated, TargetSP, TargetSpId, Actor, AccountName,
            AccountUPNSuffix, ActorIp, OperationName, CorrelationId
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Actor
      - 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" ]

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/ServicePrincipalCredentialAdditionByNonHistoricalActor.yaml