After initial access, attackers register their own app in the tenant, grant it Graph permissions and give it a secret that lasts years. The app then reads mail and files over the API without any user session, and ordinary containment steps such as password resets and token revocation do not remove it. Detection combines registration events, credential lifetime and what the app does with its access.
Signals at a Glance
| Signal | Where | ATT&CK |
|---|---|---|
| New app registration followed quickly by a secret or certificate | Entra AuditLogs | T1098.001 |
| Client secret or certificate valid for a year or more | Graph inventory, AuditLogs | T1098.001 |
| Application permissions or consent for Mail, Files or Directory scopes | Entra AuditLogs | T1550.001 |
| App-only Graph calls reading mailboxes or listing users in bulk | Microsoft Graph activity logs | T1114.002 |
| App sign-ins from hosting or VPS ranges unrelated to the tenant | AADServicePrincipalSignInLogs | T1078.004 |
Registration and Credential Lifetime
The persistence pattern is a creation followed by a credential addition, often by the same actor within minutes. Join the two events and surface apps where the credential came straight after the registration:
// Microsoft Sentinel (KQL) - app registered then credentialed within an hour
let created = AuditLogs
| where OperationName == "Add application" and Result =~ "success"
| extend AppObjectId = tostring(TargetResources[0].id), App = tostring(TargetResources[0].displayName),
Creator = coalesce(tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| project CreatedTime = TimeGenerated, AppObjectId, App, Creator;
AuditLogs
| where OperationName in ("Update application - Certificates and secrets management",
"Update application – Certificates and secrets management", "Add service principal credentials")
| extend AppObjectId = tostring(TargetResources[0].id), CredTime = TimeGenerated,
Actor = coalesce(tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| join kind=inner created on AppObjectId
| where CredTime between (CreatedTime .. CreatedTime + 1h)
| project CreatedTime, CredTime, App, Creator, Actor, AppObjectId
// tune/validate: developer self-service apps match this pattern; prioritise by the permissions granted afterwards
Credential lifetime is not logged as a field you can alert on in every tenant, so inventory it. A scheduled Graph script lists every secret and certificate that outlives your policy:
# PowerShell (Microsoft Graph) - app credentials valid for more than 365 days
Connect-MgGraph -Scopes "Application.Read.All"
$limit = (Get-Date).AddDays(365)
Get-MgApplication -All | ForEach-Object {
$app = $_
@($app.PasswordCredentials) + @($app.KeyCredentials) | Where-Object { $_.EndDateTime -gt $limit } |
ForEach-Object { [pscustomobject]@{ App = $app.DisplayName; AppId = $app.AppId; Expires = $_.EndDateTime } }
}
# tune/validate: pair with an app owner list and expire exceptions through your app lifecycle process
- Set an app management policy that caps credential lifetime so long-lived secrets cannot be created at all.
- Review every app with application permissions for Mail.Read, Mail.ReadWrite, Files.Read.All or Directory.ReadWrite.All against a business owner.
What the App Does With Access
Microsoft Graph activity logs show app-only calls by AppId and caller IP. An unfamiliar app reading many mailboxes is the clearest sign that a registered app is being used for collection:
// KQL - app-only Graph mail and file reads at volume from one app and IP
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1d)
| where RequestMethod == "GET"
| where RequestUri has_any ("/messages", "/mailFolders", "/drive/", "/users?")
| where isempty(UserId) // app-only token
| summarize Calls = count(), Targets = dcount(RequestUri), First = min(TimeGenerated), Last = max(TimeGenerated)
by AppId, IPAddress
| where Calls > 500
| order by Calls desc
// tune/validate: backup, eDiscovery and archival apps do exactly this; allowlist them by AppId and publisher
- To evict, remove the credentials, disable the service principal, delete the app, and revoke consent grants tied to it.
- Search for sibling apps created by the same actor or from the same IP, as attackers rarely register only one.
Use Microsoft guidance on investigating and remediating compromised apps, Entra app management policies, Microsoft Graph activity log documentation, and the SigmaHQ and Elastic Azure application rules. Baseline your own environment first, since integrations and backup products hold broad, long-lived access legitimately.
Build Detections That Fire
Mjolnir Security helps blue teams turn threat intelligence into high-fidelity detections across endpoint, network, identity and cloud telemetry.
- Detection Engineering We build and tune detections mapped to ATT&CK from your own telemetry.
- Threat Hunting as a Service Hypothesis-driven hunts for the behaviours on this page.
- 24/7 Incident Response Containment when a detection fires. Call +1 833 403 5875.
Stay ahead of emerging threats. Get notified when we publish new intelligence reports and advisories.
Subscribe to Alerts