Query Details

HUNT 09 SP Created Never Used 30d

Query

// Hunt     : Workload Identity - Service Principals Created But Never Used (30 days)
// Tactics  : Persistence
// MITRE    : T1136.003 (Create Cloud Account), T1098.001
// Purpose  : Surfaces Service Principals created in the last 30 days that have had ZERO
//            successful sign-in activity. Ghost SPs are a persistence risk — attackers
//            create them as backdoors that sit dormant until needed, making them invisible
//            to activity-based alerting. Also catches misconfigured or abandoned SPs.
//            Correlates AuditLogs (creation) with AADServicePrincipalSignInLogs (usage).
//==========================================================================================

// --- All SPs created in the last 30 days ---
let CreatedSPs = AuditLogs
    | where TimeGenerated > ago(30d)
    | where OperationName =~ "Add service principal"
    | where Result =~ "success"
    | extend SPId        = tostring(TargetResources[0].id)
    | extend SPName      = tostring(TargetResources[0].displayName)
    | extend Creator     = coalesce(
        tostring(InitiatedBy.user.userPrincipalName),
        tostring(InitiatedBy.app.displayName))
    | extend CreatorId   = coalesce(
        tostring(InitiatedBy.user.id),
        tostring(InitiatedBy.app.servicePrincipalId))
    | extend CreatorType = iff(isnotempty(InitiatedBy.user.id), "User", "Application")
    | project CreatedTime = TimeGenerated, SPId, SPName, Creator, CreatorId,
        CreatorType, CorrelationId;

// --- SPs that have signed in at least once ---
let UsedSPs = (AADServicePrincipalSignInLogs | invoke ExcludeAllowlistedIPs())
    | where TimeGenerated > ago(30d)
    | where ResultType == "0"
    | distinct ServicePrincipalId;

// --- SPs created but never used ---
CreatedSPs
| join kind=leftanti UsedSPs on $left.SPId == $right.ServicePrincipalId
// Also check for credential additions (SP may have been prepared for future use)
| join kind=leftouter (
    AuditLogs
    | where TimeGenerated > ago(30d)
    | where OperationName has_any (
        "Add service principal credentials",
        "Update application – Certificates and secrets management")
    | where Result =~ "success"
    | extend SPId = tostring(TargetResources[0].id)
    | summarize
        CredentialAdditions = count(),
        CredAddTypes        = make_set(OperationName, 3),
        LastCredAdd         = max(TimeGenerated)
        by SPId
) on SPId
| extend DaysSinceCreation   = datetime_diff("day", now(), CreatedTime)
| extend HasCredentials       = CredentialAdditions > 0
| extend RiskAssessment = case(
    HasCredentials == true and DaysSinceCreation > 14, "High — Credentialed but Dormant",
    HasCredentials == false and DaysSinceCreation > 14, "Medium — No Credentials, Abandoned",
    HasCredentials == true,                              "Medium — New, Credentialed, Unused",
                                                         "Low — New, No Credentials Yet")
| project
    CreatedTime, SPId, SPName, Creator, CreatorType,
    DaysSinceCreation, HasCredentials, CredentialAdditions,
    LastCredAdd, CredAddTypes, RiskAssessment
| order by DaysSinceCreation desc

Explanation

This query is designed to identify service principals (SPs) that were created in the last 30 days but have not been used for any sign-ins. These unused SPs can pose a security risk, as attackers might create them as backdoors that remain dormant until needed, making them difficult to detect through activity-based alerts. The query also helps identify misconfigured or abandoned SPs.

Here's a simplified breakdown of the query:

  1. Identify Recently Created SPs: It first gathers all service principals created in the last 30 days by checking the audit logs for successful "Add service principal" operations.

  2. Identify Used SPs: It then identifies SPs that have signed in at least once in the last 30 days by checking the Azure Active Directory sign-in logs.

  3. Find Unused SPs: By comparing the two lists, it identifies SPs that were created but never used.

  4. Check for Credential Additions: It checks if any credentials have been added to these unused SPs, which might indicate preparation for future use.

  5. Assess Risk: It evaluates the risk level of each unused SP based on whether they have credentials and how long they have been dormant:

    • High Risk: SPs with credentials that have been dormant for more than 14 days.
    • Medium Risk: New SPs with credentials or SPs without credentials that have been dormant for more than 14 days.
    • Low Risk: New SPs without credentials.
  6. Output: The query outputs details such as creation time, SP ID, SP name, creator details, days since creation, credential information, and risk assessment, sorted by the number of days since creation.

This helps security teams identify potential security risks and take appropriate actions to secure their environment.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AuditLogsAADServicePrincipalSignInLogs

Keywords

AuditLogsAADServicePrincipalSignInLogsServicePrincipalUserApplicationCredentialsCertificatesSecretsManagementRiskAssessment

Operators

let|where>ago=~extendtostringcoalesceiffisnotemptyprojectinvokedistinctjoinonhas_anysummarizecountmake_setmaxdatetime_diffnowcase==>order bydesc

Actions