Microsoft Graph Application Directory Reconnaissance with Denied Requests

edit
IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

Microsoft Graph Application Directory Reconnaissance with Denied Requests

edit

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.

Rule type: esql

Rule indices: None

Severity: medium

Risk score: 47

Runs every: 5m

Searches indices from: now-30m (Date Math format, see also Additional look-back time)

Maximum alerts per execution: 100

References:

Tags:

  • Domain: Cloud
  • Domain: Identity
  • Data Source: Azure
  • Data Source: Microsoft Graph Activity Logs
  • Use Case: Threat Detection
  • Tactic: Discovery
  • Resources: Investigation Guide
  • Rule Type: ES|QL
  • Platform: Microsoft Graph
  • Platform: Azure

Version: 1

Rule authors:

  • Elastic

Rule license: Elastic License v2

Investigation guide

edit

Triage and analysis

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_id and azure.graphactivitylogs.properties.service_principal_id. Review Esql.app_ids and resolve the application owner and expected directory permissions.
  • Review Esql.time_window, Esql.request_count, Esql.category_count, and Esql.denied_count to establish the scope of the discovery. Use Esql.paths, Esql.status_codes, and Esql.source_ips to 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_id with 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.

Setup

edit

Microsoft Graph Activity Logs

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.

Rule query

edit
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.*

Framework: MITRE ATT&CKTM