Query Details

RULE 09 SP Credentials Added No Signin

Query

// Rule    : Workload Identity - SP Credentials Added But No Sign-In Within 24 Hours (Dormant Backdoor)
// Severity: Medium
// Tactics : Persistence
// MITRE   : T1098.001 (Account Manipulation: Additional Cloud Credentials)
// Freq    : PT6H   Period: PT2D
//==========================================================================================
// Attacker adds credentials to an existing SP to establish a persistence backdoor,
// then holds off signing in to avoid detection. If credentials were added 24–48h ago
// and still no sign-in has occurred, this warrants hunting.

// --- Credentials added between 48h and 24h ago ---
// ---- Network Allowlist (exclude trusted IPs / CIDR / ranges) --------------
let _allow = materialize(union isfuzzy=true (print R="" | take 0), (_GetWatchlist('NetworkAllowlist') | project R = tostring(IPOrRange)) | where isnotempty(R));
let _allowCIDR  = toscalar(_allow | where not(R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$') | extend R = iff(R has '/', R, strcat(R, '/32')) | summarize make_list(R));
let _allowRange = toscalar(_allow | where R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$' | summarize make_list(R));
let _ExcludeAllowlistedIPs = (T:(IPAddress:string)) {
    T
    | extend IPAddress = tostring(IPAddress)
    | where array_length(_allowCIDR) == 0 or isnull(ipv4_is_in_any_range(IPAddress, _allowCIDR)) or not(ipv4_is_in_any_range(IPAddress, _allowCIDR))
    | mv-apply _r = _allowRange to typeof(string) on (
        extend _lo = tostring(split(_r,'-')[0]), _hi = tostring(split(_r,'-')[1])
        | extend _inRange = ipv4_compare(IPAddress, _lo) >= 0 and ipv4_compare(IPAddress, _hi) <= 0
        | summarize _anyInRange = max(toint(_inRange)))
    | where isnull(_anyInRange) or _anyInRange == 0
    | project-away _anyInRange
};
// ---------------------------------------------------------------------------
let CredentialAdditions = AuditLogs
    | where TimeGenerated between (ago(2d) .. ago(24h))
    | where OperationName has_any (
        "Add service principal credentials",
        "Update application – Certificates and secrets management",
        "Update application")
    | where Result =~ "success"
    | extend SPId      = tostring(TargetResources[0].id)
    | extend SPName    = tostring(TargetResources[0].displayName)
    | extend Initiator = coalesce(tostring(InitiatedBy.user.userPrincipalName),
        tostring(InitiatedBy.app.displayName))
    | where isnotempty(SPId)
    | project AddedTime = TimeGenerated, SPId, SPName, Initiator,
        CredType = OperationName;

// --- Any successful sign-ins by those SPs after credential addition ---
let PostAddSignins = (AADServicePrincipalSignInLogs | invoke _ExcludeAllowlistedIPs())
    | where TimeGenerated > ago(2d)
    | where ResultType == "0"
    | distinct ServicePrincipalId;

// --- SPs with creds added but never signed in after ---
CredentialAdditions
| join kind=leftanti PostAddSignins on $left.SPId == $right.ServicePrincipalId
| summarize
    CredAddCount         = count(),
    CredTypes            = make_set(CredType, 5),
    Initiators           = make_set(Initiator, 5),
    ServicePrincipalName = any(SPName),
    FirstAdded           = min(AddedTime),
    LastAdded            = max(AddedTime)
    by SPId
| extend HoursSinceAdd = datetime_diff("hour", now(), LastAdded)

Explanation

This query is designed to detect potential security threats related to service principal (SP) credentials in a cloud environment. Here's a simple breakdown of what it does:

  1. Purpose: The query aims to identify service principals that have had new credentials added but have not been used to sign in within 24 to 48 hours. This behavior might indicate a dormant backdoor set up by an attacker who is avoiding immediate detection.

  2. Severity and Tactics: The severity level is marked as medium, and the tactic involved is persistence, which is a method attackers use to maintain access to a system. The MITRE technique referenced is T1098.001, which involves account manipulation through additional cloud credentials.

  3. Frequency and Period: The query runs every 6 hours and looks back over the past 2 days.

  4. Network Allowlist: The query first defines a list of trusted IP addresses or ranges (allowlist) to exclude them from further analysis. This helps in focusing on potentially suspicious activities.

  5. Credential Additions: It searches through audit logs to find instances where credentials were added to service principals between 24 and 48 hours ago. It filters for successful operations and extracts relevant details like the service principal ID, name, and the initiator of the action.

  6. Post-Addition Sign-Ins: The query checks for any successful sign-ins by these service principals after the credentials were added, excluding those from trusted IPs.

  7. Detection of Dormant Backdoors: Finally, it identifies service principals that had credentials added but have not signed in since then. It summarizes the findings, including the count of credential additions, types of credentials, initiators, and the time since the last addition.

In summary, this query helps in identifying potentially malicious activities where credentials are added to service principals but not used immediately, which could indicate an attempt to establish a backdoor for future access.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AuditLogsAADServicePrincipalSignInLogs

Keywords

AuditLogsAADServicePrincipalSignInLogsServicePrincipalIdServicePrincipalNameInitiatorOperationNameResultTypeTimeGeneratedTargetResourcesIPAddress

Operators

materializeunionprinttakeprojectwhereisnotemptymatches regexextendiffstrcatsummarizemake_listtoscalarnotipv4_is_in_any_rangemv-applysplitipv4_comparemaxtointproject-awaybetweenhas_any=~coalesceagoinvokedistinctjoin kind=leftantionsummarizecountmake_setanyminmaxbydatetime_diffnow

Actions