Microsoft Graph Application Directory Reconnaissance with Denied Requests
Description
Identifies a Microsoft Graph application querying multiple directory collections and encountering access denials. Adversaries with compromised application credentials may enumerate users, groups, applications, service principals, directory roles, role assignments, and OAuth grants to identify privileged identities and opportunities for further access.
Query · esql
from logs-azure.graphactivitylogs-* metadata _id, _index
| where data_stream.dataset == "azure.graphactivitylogs"
and azure.graphactivitylogs.properties.c_idtyp == "app"
and azure.tenant_id is not null and azure.tenant_id != ""
and azure.graphactivitylogs.properties.service_principal_id is not null
and azure.graphactivitylogs.properties.service_principal_id != ""
and http.request.method == "GET"
and http.response.status_code in (200, 403)
and url.path in (
"/v1.0/applications",
"/v1.0/servicePrincipals",
"/v1.0/users",
"/v1.0/groups",
"/v1.0/directoryRoles",
"/v1.0/roleManagement/directory/roleAssignments",
"/v1.0/oauth2PermissionGrants"
)
| eval Esql.time_window = date_trunc(15 minutes, @timestamp)
| stats
Esql.request_count = count(*),
Esql.category_count = count_distinct(url.path),
Esql.denied_count = sum(case(http.response.status_code == 403, 1, 0)),
Esql.paths = values(url.path),
Esql.status_codes = values(http.response.status_code),
Esql.app_ids = values(azure.graphactivitylogs.properties.app_id),
Esql.source_ips = values(source.ip),
Esql.first_seen = min(@timestamp),
Esql.last_seen = max(@timestamp)
by Esql.time_window, azure.tenant_id, azure.graphactivitylogs.properties.service_principal_id
| where Esql.category_count >= 3 and Esql.denied_count >= 1
| keep azure.tenant_id, azure.graphactivitylogs.properties.service_principal_id, Esql.*
Investigation fields
Pivot points the source recommends for triage.
azure.tenant_idazure.graphactivitylogs.properties.service_principal_idEsql.pathsEsql.status_codesEsql.source_ipsEsql.time_window
Implementation guide
Enable Microsoft Graph Activity Logs and ingest them through the Azure integration into
logs-azure.graphactivitylogs-*. Retain the tenant and service-principal IDs, HTTP method, URL path, and response
status. The azure.graphactivitylogs.properties.c_idtyp field must be populated with the token actor type;
this rule selects app and does not infer app-only authentication from an application ID alone.
The rule evaluates a 30-minute ingestion lookback every five minutes and groups requests into fixed 15-minute event-time windows. Requests split across window boundaries or ingested more than 30 minutes apart may not be combined. Review delivery latency and repeated aggregate alerts when tuning the schedule. The rule covers the direct v1.0 collection paths in the query; beta endpoints and other request paths are outside its scope.
Known false positives
- Application onboarding, directory synchronization, inventory tools, and permission-validation scripts may query multiple directory collections and encounter expected access denials.
Analyst notes
Investigating Microsoft Graph Application Directory Reconnaissance with Denied Requests
Microsoft Graph exposes directory information that can help an adversary identify accounts, privileged identities, and applications to target. This rule identifies app-only requests to at least three common directory discovery collections (applications, service principals, users, groups, directory roles, role assignments, and OAuth permission grants) within a 15-minute window, with at least one HTTP 403 response. The denial requirement reduces noise from authorized inventory applications that succeed across the collections they are granted. A denied request shows attempted discovery, not successful retrieval of that collection.
Possible investigation steps
- Identify the calling principal using
azure.tenant_idandazure.graphactivitylogs.properties.service_principal_id. ReviewEsql.app_idsand resolve the application owner and expected directory permissions. - Review
Esql.time_window,Esql.request_count,Esql.category_count, andEsql.denied_countto establish the scope of the discovery. UseEsql.paths,Esql.status_codes, andEsql.source_ipsto review the targeted collections, outcomes, and sources. - Examine the original Graph events to distinguish successfully accessed collections from denied requests. Determine whether the queried collections are needed for the application's documented purpose.
- Correlate the principal with service-principal sign-ins, recent application credential additions, and changes to application permissions. Where available, compare
azure.signinlogs.properties.service_principal_credential_key_idwith the credential added in audit records. - Look for related role-assignment attempts, successful privilege changes, secret access, and access to other services. Distinguish the calling application from applications returned or targeted by its requests.
- Confirm whether an inventory, directory synchronization, onboarding, or permission-validation workflow explains the activity.
False positive analysis
- Directory inventory and synchronization applications may access several collections and encounter expected permission denials. Validate the activity with the application owner and compare it with normal sources and access patterns.
- Onboarding and permission-validation scripts may generate similar requests. Scope any justified exception to the tenant, principal, and expected workflow, and reassess it when permissions or ownership change.
Response and remediation
- If compromise is confirmed, restrict the affected principal and remove the specific compromised credential and unnecessary permissions according to the incident response process.
- Review and remediate unauthorized privilege changes or downstream resource access associated with the principal.
- Verify that subsequent access is denied after containment; credential removal alone does not establish that previously issued access tokens are unusable.
- Preserve the original Graph activity, audit, and sign-in events for investigation.