An OAuth app with application permissions such as Mail.Read or Files.Read.All can read every mailbox or every site in a Microsoft 365 tenant without a user ever signing in, and most tenants cannot tell a normal integration run from a bulk theft. The app authenticates as itself, so per-user signals such as risky sign-ins and MFA prompts never fire. This guide is about one narrow job: detecting bulk mail and file pulls made by apps and service principals through Microsoft Graph and Exchange Web Services. It covers logs, fields, baselines, detections, app scoping and response. It is written for detection engineers, Microsoft 365 administrators and identity teams. For the wider exfiltration picture across endpoints, networks and other clouds, see the Data Exfiltration Guide.
Key takeaways
- Graph activity logs are the only per-request record of app traffic. The MicrosoftGraphActivityLogs table records AppId, ServicePrincipalId, RequestUri, ResponseSizeBytes and IPAddress for each call, but it needs Entra ID P1 or P2 and a diagnostic setting that most tenants have never created.
- Mailbox and file audit events give the content view. MailItemsAccessed and SharePoint and OneDrive file events in the unified audit log say which items were touched; Graph activity logs say how much was pulled, by which app, from where.
- Baseline per app, not per user. An integration that reads 40 mailboxes a day and suddenly reads 4,000, from a new IP range or user agent, is the core signal. Volume, breadth (distinct mailboxes and sites) and novelty (new endpoints, new IPs) together beat any single threshold.
- Credential changes come before the pull. A new secret or certificate on an existing app, a fresh admin consent or a new app role assignment is often the first event in the chain. Correlate them with the volume that follows.
- Scoping shrinks the problem. RBAC for Applications in Exchange Online limits an app to named mailboxes, so a stolen app credential can reach only what the integration genuinely needs, and anything outside that scope becomes an obvious anomaly.
Why app-driven pulls are hard to see
Three properties set app exfiltration apart from a user downloading files.
- No interactive sign-in: with the client credentials flow an app signs in with a client secret or certificate. The event lands in the service principal sign-in log, not the user sign-in logs that most alert rules watch, and there is no MFA challenge to fail.
- Tenant-wide reach by design: application permissions such as Mail.Read, Mail.ReadWrite, Files.Read.All and Sites.Read.All apply to every mailbox or site in the tenant unless something narrows them. ATT&CK describes this pattern under Remote Email Collection (T1114.002), Data from Cloud Storage (T1530) and Data from Information Repositories: Sharepoint (T1213.002).
- Legitimate-looking transport: requests go to the same Graph endpoint as Outlook, Teams and the admin portals, often from cloud hosting or residential proxy addresses. Microsoft's January 2024 guidance on Midnight Blizzard noted that residential proxies made "traditional indicators of compromise (IOC)-based detection infeasible due to the high changeover rate of IP addresses".
The Microsoft Midnight Blizzard breach shows the full chain: a legacy test OAuth app with elevated access was taken over, new apps were created and consented, and the Exchange Online full_access_as_app role was used to read executive mailboxes through Exchange Web Services. No user mailbox credential was needed after the app was in hand.
The telemetry you need
No single log answers "which app took what". Detection needs four sources joined on app and service principal identifiers.
| Source | What it shows for app pulls | Key fields | Prerequisites and limits |
|---|---|---|---|
| Microsoft Graph activity logs (MicrosoftGraphActivityLogs) | Every HTTP request Graph processed for the tenant, including app-only calls | AppId, ServicePrincipalId, UserId, RequestUri, RequestMethod, ResponseStatusCode, ResponseSizeBytes, IPAddress, UserAgent, Roles, Scopes, ClientAuthMethod, OperationId, SignInActivityId | Entra ID P1 or P2; diagnostic setting to Log Analytics, Storage or Event Hubs; resource tenant only; usually delivered within 30 minutes, occasionally up to 2 hours |
| Unified audit log: Exchange mailbox auditing | Which mailbox items were accessed, by which client and session | MailItemsAccessed with MailAccessType (Bind or Sync), OperationCount, InternetMessageId, ClientInfoString, ClientIPAddress, SessionId, MailboxUPN | Enabled by default for users with Office 365 or Microsoft 365 E3 or E5; Audit (Standard) retention 180 days |
| Unified audit log: SharePoint and OneDrive | File and page downloads, searches and sharing by users and apps | FileDownloaded, FileAccessed, SearchQueryPerformed, AnonymousLinkCreated with the app and user identifiers | Audit (Premium) keeps Exchange, SharePoint, OneDrive and Entra ID records for one year by default |
| Entra ID audit and service principal sign-in logs | Credential added to an app, consent granted, app role assigned, app sign-ins and source IPs | Add service principal credentials, Consent to application, Add app role assignment to service principal; AADServicePrincipalSignInLogs | Sign-in logs need P1 or P2 to stream; see the Entra ID Sign-In and Audit Log Detection Guide |
Unified audit log records from non-user entities such as service principals are kept for a fixed one year. For background on the audit pipeline itself, see the Microsoft 365 Unified Audit Log Guide for Detection.
Fields in the Graph activity log that carry the detection
- AppId and ServicePrincipalId: the primary keys for every app baseline. A request with an AppId and no UserId is app-only.
- Roles and Scopes: the permissions in the token. Roles holds application permissions (for example Mail.Read), Scopes holds delegated ones.
- ClientAuthMethod: 0 for a public client, 1 for client ID and secret, 2 for client certificate. A confidential app that has always used a certificate and suddenly authenticates with a secret deserves attention.
- ResponseSizeBytes: the volume signal. Sum it per app per hour and per day.
- RequestUri: normalise the path (strip the version, replace GUIDs and UPNs with placeholders) to get the operation, such as users/{id}/messages, users/{id}/mailFolders/{id}/messages/delta, drives/{id}/items/{id}/content or search/query. Count distinct mailboxes and drives from the raw URI.
- OperationId: shared by all requests in one JSON batch. Graph allows up to 20 requests per batch and evaluates each one individually against throttling, so count inner requests rather than batch calls.
- SignInActivityId and UniqueTokenId: join keys to the sign-in logs, so each request can be tied to the authentication that produced its token, including the token's source IP.
What MailItemsAccessed adds, and its limits
Microsoft Purview records a Bind record for each individual message accessed over POP, IMAP, MAPI, EWS, Exchange ActiveSync and REST, which includes Graph. Bind operations inside a 2-minute window are aggregated into one record, with OperationCount giving the number, and duplicate binds for the same message within one hour are filtered out. Sync records, which treat a whole folder as accessed, are produced only by desktop Outlook for Windows or Mac, so app pulls show up as Bind records. Mailbox auditing must not be disabled at organisation level: check Get-OrganizationConfig for AuditDisabled.
Turning the logs on
- Create the Graph activity log diagnostic setting: in Entra ID diagnostic settings, select MicrosoftGraphActivityLogs and send it to a Log Analytics workspace (for KQL and Microsoft Sentinel), an Event Hub (for another SIEM) or a Storage account (for cheap long-term retention). Security Administrator is the least privileged role that can configure it.
- Size the cost before you switch it on: Microsoft's own estimate is about 15 GiB a month in Azure Monitor Logs for a 1,000-user tenant and about 1,200 GiB for 100,000 users. Diagnostic settings cannot filter these logs, so reduce cost with a workspace transformation that drops unneeded columns or rows, or with the Basic log plan, and send a full copy to Storage.
- Stream the companion logs: AADServicePrincipalSignInLogs, AADManagedIdentitySignInLogs, AuditLogs and the unified audit log (through the Office 365 Management Activity API or the Microsoft Sentinel Microsoft 365 connector).
- Confirm mailbox audit coverage: check that MailItemsAccessed is in the default Owner, Delegate and Admin action sets for licensed mailboxes, and identify mailboxes without an E3 or E5 class licence, which are blind spots.
- Set retention to at least a year: app abuse is often found months later. Keep Graph activity logs in Storage for 12 months or more even if hot retention is shorter.
Detections that hold up
Build an inventory first: every app with application permissions to mail, files or sites, its owner, the mailboxes or sites it should touch, and its normal daily volume. The How to find high-privilege Microsoft Graph application permissions walkthrough produces that list. Then layer the detections below.
| Detection | Logic | Main source | ATT&CK |
|---|---|---|---|
| App volume spike | Daily sum of ResponseSizeBytes or count of message and file content requests per AppId exceeds the 30-day baseline by a set multiple (start at 3x and tune) | Graph activity logs | T1114.002, T1530 |
| Breadth spike | Distinct mailboxes or drives touched per AppId per day exceeds baseline, or an app reaches mailboxes outside its expected group | Graph activity logs, MailItemsAccessed | T1114.002, T1213.002 |
| Credential change then pull | New secret, certificate or federated credential on an app (T1098.001), followed within 7 days by a volume or breadth spike for that AppId | Entra audit log joined to Graph activity logs | T1098.001, T1550.001 |
| New consent then pull | Admin consent to Mail.Read, Mail.ReadWrite, Files.Read.All, Sites.Read.All or full_access_as_app, followed by first-ever content reads | Entra audit log, Graph activity logs | T1528, T1671 |
| Dormant app wakes up | An AppId with no Graph requests for 30 days or more begins reading mail or files | Graph activity logs | T1550.001 |
| New network origin or client | An app's requests or token sign-ins come from an IP range, hosting provider or UserAgent never seen for that app | Graph activity logs, service principal sign-ins | T1078.004 |
| Delta and export patterns | First use by an app of messages delta queries across many mailboxes, mailbox export permissions, or drive-wide item enumeration followed by content downloads | Graph activity logs | T1119, T1020 |
A baseline query, described
The first useful analytic is a per-app daily summary. In KQL against MicrosoftGraphActivityLogs: filter to app-only calls (UserId empty), filter RequestUri to message, attachment, drive item content and search paths, then summarise by AppId and day: request count, sum of ResponseSizeBytes, distinct mailbox or drive identifiers extracted from the URI, distinct IPAddress and distinct UserAgent. Store the result in a summary table and compare each day against the app's trailing 30-day median. Repeated 429 (throttled) responses for an app are also a volume signal.
Built-in analytics
- App governance in Microsoft Defender for Cloud Apps: its threat detections include "App made anomalous Graph calls to read e-mail", "App made unusual email search activities", "App with EWS application permissions accessing numerous emails", "Anomalous SharePoint activity by app that just updated or added new credentials", "Anomalous OneDrive activity by app that just updated or added new credentials" and "Unused app newly accessing APIs". Use them alongside your own baselines.
Shrinking the attack surface for detection
Detection works best when each app has a narrow, documented scope. Three controls make anomalies stand out.
| Control | What it does | Effect on detection |
|---|---|---|
| RBAC for Applications (Exchange Online) | Assigns application roles such as Application Mail.Read, Application MailboxItem.Export or Application EWS.AccessAsApp to a service principal, limited to a management scope or administrative unit. It replaces Application Access Policies. | An app can only reach in-scope mailboxes; any attempt outside scope fails and is visible as errors |
| Remove unscoped Entra grants | Exchange combines Entra and RBAC permissions additively, so an unscoped Mail.Read in Entra ID overrides a scoped role | Without this step the scope is cosmetic |
| Consent restrictions and app ownership | Restrict user consent, require admin consent workflow for high-impact permissions, and give every app a named owner | Fewer apps, each with a known purpose and expected volume |
Use Test-ServicePrincipalAuthorization to confirm which mailboxes a service principal can reach under RBAC; note that it does not include permissions granted directly in Entra ID, and that role changes take 30 minutes to 2 hours to apply because of caching. Microsoft's guidance after Midnight Blizzard also recommends reviewing any app holding EWS.AccessAsUser.All or full_access_as_app, and any identity holding ApplicationImpersonation. See the Microsoft 365 Application Access Policies and RBAC for Applications Guide and the How to review Microsoft 365 app registrations with mail permissions walkthrough.
Where identity fits
Every Graph pull is made by a non-human identity holding a credential: a client secret, a certificate, a federated credential or a managed identity. The quality of that identity's lifecycle decides how much a thief can take and how quickly you notice.
- Credential type: prefer certificates or workload identity federation over client secrets, and alert when ClientAuthMethod changes for an app.
- Third-party apps holding your data access: backup, archiving, e-discovery and CRM sync tools often hold tenant-wide mail and file permissions. After the Commvault Metallic breach, CISA's advice to affected customers centred on customer-side controls: watch Entra audit logs for credential changes, restrict the app's sign-ins to the provider's published IP ranges, and rotate the secrets.
- Stale apps: remove dormant apps with live credentials, as described in How to find and remove stale service principals in Entra ID.
Choosing your approach
- Small tenant, no SIEM: turn on the Graph activity log to a Storage account for retention, use App governance detections and the unified audit log search for MailItemsAccessed, and review app permissions quarterly.
- Mid-size tenant with Microsoft Sentinel: Log Analytics with a transformation that keeps app-only traffic and the fields in this guide, per-app daily summaries and the detections table above.
- Large or regulated tenant: full ingestion to the SIEM through Event Hubs, per-app baselines owned by detection engineering, RBAC for Applications for every mail integration, and app inventory reconciled against the Graph activity log monthly (apps calling Graph that are not in the inventory are findings).
Responding to a suspected app pull
- Confirm and scope: list the AppId's requests over the past 30 days, the mailboxes and drives touched, total ResponseSizeBytes and source IPs. Cross-check MailItemsAccessed InternetMessageId values to identify the messages read.
- Contain the identity: disable the service principal or remove its credentials. Removing a secret does not invalidate access tokens already issued, which remain valid until they expire, so expect some activity to continue briefly and watch for it.
- Find persistence: other apps created or consented at the same time, credentials added to other service principals, and inbox rules or mailbox permissions changed by the app.
- Preserve evidence: export the Graph activity log rows, unified audit log records and Entra audit entries before retention ages them out.
- Assess notification: mailbox and file contents usually include personal data; involve legal and privacy early, because under GDPR Article 33 a controller must notify the supervisory authority without undue delay and, where feasible, within 72 hours of becoming aware of a personal data breach, unless it is unlikely to result in a risk to individuals.
- Recover: reissue a narrowly scoped app if needed and add the pattern to your detections.
The OAuth Token Theft Incident Playbook and the Microsoft 365 Tenant Compromise Playbook cover the wider response.
Measuring whether it works
- Coverage: percentage of apps with mail, file or site application permissions that have a named owner, a documented scope and a baseline (target 100%).
- Scoping: percentage of mail-reading apps restricted by RBAC for Applications with no unscoped Entra grant remaining.
- Log health: hours since the last MicrosoftGraphActivityLogs record arrived, checked continuously, and retention achieved in days.
- Unknown callers: number of AppIds calling Graph for mail or files that are not in the inventory (target zero).
- Time to detect: time from the start of an app volume spike to an alert, measured in purple team exercises with a test app.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| MITRE ATT&CK v20 | TA0009 Collection, TA0010 Exfiltration; T1114.002, T1530, T1213.002, T1119, T1020; T1528, T1550.001, T1098.001, T1671 | Technique mapping for detections and purple team tests |
| NIST CSF 2.0 | DE.CM-03, DE.AE-02, PR.AA-05 | Monitoring technology usage, analysing adverse events, least privilege for app entitlements |
| NIST SP 800-53 Rev. 5 | AU-12, AU-6, AU-11, SI-4, AC-6, AC-2 | Generating, reviewing and retaining audit records; system monitoring; least privilege and account management for service principals |
| CIS Controls v8.1 | Control 8 Audit Log Management; Control 6 Access Control Management | Collecting and retaining the logs; scoping app access |
| ISO/IEC 27001:2022 Annex A | A.8.15 Logging, A.8.16 Monitoring activities, A.5.15 Access control | Logging and monitoring obligations and access restriction |
Common failure patterns
- Graph activity logs never enabled: the diagnostic setting is missing, so the only record of app reads is MailItemsAccessed, which says little about volume and nothing for file pulls by apps through Graph.
- Counting records instead of operations: MailItemsAccessed aggregation and batching mean raw record counts understate volume. Use OperationCount, distinct InternetMessageId and inner batch requests.
- Scoping that does not scope: an app is placed under RBAC for Applications but keeps its unscoped Entra Mail.Read grant, so it can still read every mailbox.
- Alerting on users only: rules watch user sign-ins and interactive downloads while service principals, which hold the broadest standing access, have no baseline at all, as the Microsoft Midnight Blizzard breach illustrates.
- Ignoring credential events: a new secret on a trusted backup or sync app is treated as routine maintenance and never correlated with what the app did next.
Practitioner checklist
- Create the MicrosoftGraphActivityLogs diagnostic setting and confirm records arrive with AppId and ServicePrincipalId.
- Stream service principal sign-ins, Entra audit logs and the unified audit log to the same platform.
- Keep at least 12 months of Graph activity logs, in Storage if not in the SIEM.
- Inventory every app with mail, file or site application permissions, with owner, purpose, scope and normal volume.
- Build per-app daily summaries of request count, bytes, distinct mailboxes or drives, IPs and user agents.
- Alert on volume and breadth spikes, credential-change-then-pull, consent-then-pull and dormant apps waking up.
- Apply RBAC for Applications to mail integrations and remove unscoped Entra grants.
- Review apps holding full_access_as_app, EWS.AccessAsUser.All or ApplicationImpersonation.
Standards and references
- Microsoft Learn: Access Microsoft Graph activity logs
- Microsoft Learn: Use MailItemsAccessed to investigate compromised accounts
- Microsoft Learn: Manage mailbox auditing
- Microsoft Learn: Auditing solutions in Microsoft Purview
- Microsoft Learn: Role Based Access Control for Applications in Exchange Online
- Microsoft Learn: Investigate threat detection alerts in app governance
- Microsoft Learn: Combine multiple HTTP requests using JSON batching
- Microsoft Security: Midnight Blizzard, guidance for responders on nation-state attack
Related NHI Mgmt Group resources: Data Exfiltration Guide · Microsoft 365 Unified Audit Log Guide for Detection · Consent Phishing and Illicit OAuth Grants Guide · SaaS and OAuth App Governance Guide