Terrance DeJesus

Azure AD Graph Activity Logs: Ingestion and threat detection to close the visibility gap

Azure AD Graph Activity Logs land in Elastic with full ECS parsing. Detect ROADrecon and AADInternals enumeration with ready-to-use detection rules.

AAD Graph Activity Logs are now ingestible into Elastic and usable for threat detection within the SIEM/XDR solution. That sentence shouldn't be exciting, but it is. For most of the past decade, this slice of telemetry simply didn't exist as a customer-accessible log stream. Microsoft Graph Activity Logs (the modern graph.microsoft.com surface) went GA in April 2024. The legacy graph.windows.net surface, the one adversary tooling actually hits, stayed dark until early 2026.

This post walks the loop end-to-end. Why visibility matters, how to ingest the logs into Elastic, how to generate realistic recon manually and with ROADrecon, and how to hunt the result in ES|QL. Everything below was validated against a live tenant.

Key takeaways

  • AAD Graph Activity Logs ride into Elastic through the Azure integration and land in logs-azure.aadgraphactivitylogs-* with full ECS extraction.

  • ROADtools, AADInternals, and friends have been operating in a visibility gap for years. Defenders weren't capturing the calls.

  • AAD Graph is "deprecated" but still queryable in most tenants. The 1.61-internal API version still returns data that Microsoft Graph won't.

  • ECS fields land typed (event.action, event.outcome, http.request.method, source.ip, user.id, user_agent.original). Dataset extras stay queryable under azure.aadgraphactivitylogs.properties.*.

  • Five hunts reliably catch the activity: tooling user-agents, endpoint breadth, *-internal API misuse, FOCI client-ID mismatches, and 4xx surges.

A short history of defender visibility

Defenders have spent years on sign-ins, conditional access, role assignments, and OAuth consent grants. Very little content covers the underlying directory APIs that adversary tooling actually hits. The reason is structural: customer-accessible logs for those APIs didn't exist. Microsoft Graph Activity Logs landed first (preview October 2023, GA April 2024). AzureADGraphActivityLogs finally showed up in early 2026.

For most of the past decade, AAD Graph enumeration was invisible to SOCs, not because the telemetry was hidden, but because it didn't exist. ROADtools, AADInternals, MSOLSpray, Microburst. None of them produced data that anyone could capture, even with a perfect logging configuration.

That changes the day AzureADGraphActivityLogs start landing in your platform-logs index.

AAD Graph is “deprecated” but still very much alive

Quick refresher. Azure AD Graph is the legacy REST API for Entra ID directory objects, hosted at https://graph.windows.net/{tenantId}/{objecttype} with API versions like 1.5, 1.6, and 1.61-internal. Microsoft has been telling everyone to migrate to Microsoft Graph since 2019, and the retirement date has slipped several times.

Deprecation isn’t gone. In 2026, AAD Graph can still answer requests in environments where legacy access paths remain available or where applications have not been explicitly blocked from using it. A few reasons it sticks around as an attacker target:

  • Adversary tooling hasn't been ported. ROADrecon still uses it for gather. AADInternals has dozens of cmdlets wrapping it.

  • The *-internal API versions return more data. 1.61-internal exposes strongAuthenticationDetail inline on the user object during a normal directory walk. The Microsoft Graph equivalent lives behind a separate /authentication/methods endpoint gated by UserAuthenticationMethod.Read.All. That asymmetry is exactly what bulk enumeration tooling exploits.

  • The block isn't a single toggle. The blockAzureADGraphAccess control lives per-app on application.authenticationBehaviors, so blocking tenant-wide means iterating every app registration. Most environments haven't done that because some legacy automation still depends on the API. Microsoft's phased retirement enforcement does the work on Microsoft's timeline, not the defender's.

  • Visibility did not exist, thus red teamers and adversaries could hammer the API endpoints for relevant information.

Legitimate AAD Graph traffic is dominated by a handful of first-party Microsoft callers. In our test tenant, the order, by volume, was Microsoft.OData.Client, Microsoft Azure Graph Client Library, an empty-UA tail from first-party AppIds, Microsoft ADO.NET Data Services, and the Azure portal (Chrome UAs against the portal app ID). Anything outside that recognisable set is either internal tooling or unauthorized activity. That makes it a solid threat hunting/detection dataset. If you're capturing it.

Setting up the ingestion pipeline

If you're already running the Elastic Azure integration with diagnostic settings forwarding to an event hub, skim this section. You probably just need to enable one extra log category. From scratch, it's about a 20-minute path.

Step 1: A stack to receive the logs

Any Elastic deployment works. An Elastic Cloud trial is the lowest-friction option for prototyping. Another option is the Elastic Container Project for getting started. The Azure integration already handles AzureADGraphActivityLogs once it's enabled.

Step 2: Add the Azure integration

In Kibana, Integrations > Azure Logs > Add Azure Logs. Plug in your Event Hub connection string, the Event Hub name, and a Storage account for offset checkpointing, all on an event hub in the same subscription as your tenant.

Enable the Azure logs v2 data stream specifically. That's the entry point for AAD Graph Activity Logs. The events router matches category == "AzureADGraphActivityLogs" and reroutes documents to logs-azure.aadgraphactivitylogs-*, where the dataset pipeline applies full ECS extraction.

We've also broken Azure AD Graph Activity Logs out into its own integration item, so you can search for "Azure AD Graph Activity Logs" and install via the policy template directly.

Step 3: Enable diagnostic settings on Entra ID

This is the step most defenders miss. AzureADGraphActivityLogs as a diagnostic-settings category is newer. Even if your Entra ID diagnostic settings have been configured for a while now, the new category needs a fresh tick. Otherwise, the data lives and dies in Microsoft's tenant boundary.

In the Azure portal:

  1. Entra ID > Monitoring > Diagnostic settings > + Add diagnostic setting.
  2. Name it.
  3. Under Logs, check AzureADGraphActivityLogs. While you're there, MicrosoftGraphActivityLogs, SignInLogs, and AuditLogs are worth turning on if they aren't already. The integration handles all of them.
  4. Under Destination details, Stream to an event hub (the same one from step 2).
  5. Save.

Step 4: Verify data is flowing

Within a few minutes, you should start seeing events. Fastest sanity check:

FROM logs-azure.aadgraphactivitylogs-*
| LIMIT 20

If documents come back with a populated event.action, http.request.method, and zure.aadgraphactivitylogs.properties.* fields, you're good. If nothing shows up, the usual suspects are a forgotten event hub permission, a typo in the connection string, or the AAD Graph category just not being ticked.

To force a few events, sign in to the Azure portal and click around Users or Applications. The portal still calls AAD Graph internally for some object details. If that doesn't generate anything, this curl loop will:

TOKEN=$(az account get-access-token --resource https://graph.windows.net --query accessToken -o tsv)
TID=$(az account show --query tenantId -o tsv)
for obj in users groups servicePrincipals applications tenantDetails; do
  curl -sS -o /dev/null -H "Authorization: Bearer $TOKEN" \
    "https://graph.windows.net/$TID/$obj?api-version=1.6&\$top=5"
done

Field shape

Once data is flowing, properties land as typed, top-level fields. The ones that matter for hunting:

  • ECS, populated directly: event.action (a semantic verb derived from method + collection, e.g., users-read, batch-execute), event.outcome, event.duration, http.request.method, http.response.status_code, source.ip, and source.geo.*, user.id, user_agent.original (plus parsed sub-fields), url.path, azure.tenant_id, cloud.service.name = "Azure AD Graph".
  • Dataset-specific under zure.aadgraphactivitylogs.properties.*: app_id,, app_id, api_version, actor_type, roles, scopes, wids, identity_provider, client_auth_method, sign_in_activity_id, token_issued_at.
  • related.user gets both user.id and properties.app_id, so pivots on the OAuth-client dimension work alongside the user pivot.

Raw JSON stays in event.original for forensic replay. You shouldn't need to reach into it for normal hunting. If you do, ES|QL's JSON_EXTRACT() is the lever.

AAD Graph enumeration with ROADrecon

To know what to hunt, you need to know what the activity looks like. The two toolkits below are the most common sources of AAD Graph traffic in red team and security research workflows. I ran both against our testing tenant.

Note: I take no responsibility for misuse of this code. Run these tools only against tenants you own or have explicit written authorization to test.

ROADrecon: Bulk enumeration test

ROADrecon is the data-collection module of ROADtools, Dirk-jan Mollema's Entra ID research framework. Highly recommended if you haven't used it. gather walks every interesting object type in the directory (users, groups, service principals, applications, devices, directory roles, role assignments, eligible role assignments, OAuth2 permission grants, administrative units) and writes the result to SQLite.

Setup is the standard workflow:

pip install roadrecon
roadrecon auth --device-code -c 04b07795-8ddb-461a-bbee-02f9e1bf7b46 -r https://graph.windows.net

The device-code flow hands you a URL and a code. We use the Microsoft Azure CLI as the default (1b730954-1685-4b74-9bfd-dac224a7b894 - AAD PowerShell), which returned 403s in our tenant. After signing in:

roadrecon gather

Running roadrecon gather with the resulting token completed cleanly. From the tenant's perspective, the run produced just over ~2,000 AAD Graph calls and logs in roughly 1 minute. Bulk enumeration across every object type ROADrecon knows.

From this, we can form some initial detections to start flagging these anomalies.

Key fields for AAD Graph threat detection

Before the hunts, here are some solid starting fields for detecting anomalies.

FieldDescriptionWhat you can find
event.actionSemantic verb (HTTP method + collection, e.g.,`users-read, batch-execute)A cheap filter to isolate AAD Graph activity by intent
http.request.methodGET, POST, PATCH`, DELETEReads (recon) vs writes (modification, credential injection, persistence)
http.response.status_codeHTTP status returnedSuccessful vs blocked recon; bursts of 4xx indicate permission-probing or brute-forcing
user.idCalling user's directory object IDIdentity attribution; pivot to that user's other activity in SignInLogs / AuditLogs
user_agent.originalFull UA string of the callerWhether the caller is a first-party Microsoft library, a developer tool (curl, Python aiohttp), or known offensive tooling
url.pathResource path (/users, /policies, /servicePrincipals, ...)Which directory object types are being touched; breadth across distinct paths indicates bulk enumeration
azure.aadgraphactivitylogs.properties.app_idOAuth client ID that issued the tokenWhether traffic comes from a legitimate first-party client or from a FOCI-swap-style abuse path
azure.aadgraphactivitylogs.properties.api_version1.5, 1.6, 1.61-internal, etc.Whether the caller is asking for internal-only fields (strongAuthenticationDetail, full CAP set) that adversary tooling specifically targets
azure.aadgraphactivitylogs.properties.actor_typeUser, Application, ServicePrincipalHuman caller vs service-principal / app-only flow
azure.aadgraphactivitylogs.properties.roles / widsDirectory role display names and well-known role template GUIDs held by the callerWhether a privileged role (Global Admin, Application Administrator, etc.) is being exercised at the moment of the call
azure.aadgraphactivitylogs.properties.scopesOAuth scopes on the calling tokenWhich directory permissions the token actually grants the caller
azure.aadgraphactivitylogs.properties.client_auth_methodHow the client authenticated (PRT, certificate, secret, ...)Fingerprints for PRT abuse, device-PRT exploitation, or stolen client-credential use
azure.aadgraphactivitylogs.properties.sign_in_activity_idCorrelation ID to the originating sign-inPivot from an AAD Graph call back to the sign-in event that produced the calling token
azure.aadgraphactivitylogs.properties.token_issued_atTimestamp the token was mintedToken-age analysis; calls riding on a token issued days ago can indicate stale-token / refresh-token abuse

Detection and prevention

Detection

The prerequisite for any AAD Graph detection is having the logs in the first place. The AzureADGraphActivityLogs diagnostic category needs to be enabled in Entra ID and routed to a destination you can query (at minimum a Log Analytics workspace, ideally also forwarded to an event hub for Elastic ingestion as described in the setup section above). Until that's done, the calls described in this post happen entirely off-camera, and none of the hunts below will fire.

If you can't ingest into Elastic right now, enable the diagnostic setting anyway and send to Log Analytics. The KQL equivalents of the hunts below are straightforward, and the data accumulates with retention even without further processing.

Prevention

There's no single tenant-wide AAD Graph kill-switch in the portal. The actual application-layer control is:

  • application.authenticationBehaviors.blockAzureADGraphAccess

A per-app Boolean on the application resource (Microsoft Graph beta, docs). Blocking at scale means walking through every app registration and flipping it manually or programmatically. Microsoft's own phased retirement is doing this on their timeline regardless. The further along that gets, the less surface there is to defend.

Defenders can move on the same axes in the meantime:

  • Audit applications in your tenant that still hold tokens for graph.windows.net. Set blockAzureADGraphAccess = true on the ones that don't need it. Anything still depending on AAD Graph breaks loudly, which surfaces legacy automation you didn't know you had.

  • Apply Conditional Access with Azure AD Graph as a target resource. The Azure AD Graph service principal (00000002-0000-0000-c000-000000000000) doesn't show in the standard CA app picker, but it's covered by All resources policies and is individually targetable via the custom security attribute filter approach. Microsoft's March 2026 enforcement change makes this more practical: low-privilege scopes (User.Read, People.Read, etc.) that used to be auto-excluded from CA enforcement are now treated as AAD Graph access, so all resource policies actually gate them. CA evaluates at token issuance, so already-valid tokens keep working until expiry.

  • Apply CA to the FOCI clients adversary tooling rides on (Microsoft Teams, Microsoft Office, OneDrive, Azure PowerShell, etc.). Require managed and compliant devices. The swap path collapses if the underlying client can't sign in.

  • For service-principal callers, Workload Identities Premium adds CA scoped to service principals. Conditions are limited to location, Identity Protection risk, and authentication context; the only grant control is Block. Useful for collapsing external- and risky-context paths, not for scoping an SP to specific cloud apps the way user CA does.

  • Disable device-code flow for users who don't need it. roadrecon auth --device-code is the path of least resistance into the entire pipeline above and is extremely common in OAuth phishing.

Behavior detection

We shipped detection rules covering the AAD Graph recon shapes documented above. Each lives in the Elastic detection-rules repository and runs natively against the parsed logs-azure.aadgraphactivitylogs-* data stream.

Azure AD Graph Access with Suspicious User-Agent - KQL match rule. Triggers when AAD Graph receives traffic from user-agent strings matching offensive tooling families (Python, aiohttp, curl, Go-http-client, axios, AzureHound, BloodHound, AADIntenals, etc.). Solid baseline signal because no first-party Microsoft component identifies as any of these, while default tooling does. 

Azure AD Graph High 4xx Error Ratio from User - ES|QL aggregation. Triggers when a single caller produces an unusually high ratio of 4xx responses against AAD Graph in a short window. Recon and brute-force token usage leave a tail of 403s and 404s as tools walk endpoints they don't have permission for, ask for object IDs they don't have, or use a client ID unauthorized for AAD Graph. 

Azure AD Graph Access with Unusual Client and User - KQL new_terms rule, medium severity. Fires when a (calling OAuth client, signed-in user) pair appears on AAD Graph for the first time in the prior 14 days. Catches FOCI swaps, phished refresh tokens redeemed for clients the user doesn't normally use, and stolen tokens used under unfamiliar clients. Ignores known first-party applications that were commonly observed interacting with Azure AD that are backend owned by Microsoft.

Azure AD Graph Access with Unusual User and ASN - KQL match rule. Excludes the common Microsoft / AWS / GCP / Akamai / Cloudflare ASN organisations and flags AAD Graph traffic originating outside that set. Adversary tooling typically rides on residential ISPs, VPS providers, or anonymising networks that produce a different ASN distribution than legitimate first-party callers. Tunable per tenant by adjusting the excluded ASN list.

Azure AD Graph Potential Enumeration (ROADrecon) - ES|QL aggregation, high severity. Requires both an aiohttp user-agent and a burst of 500+ AAD Graph requests from a single identity. ROADrecon's gather command uses aiohttp by default and walks every directory object type, so the combination is essentially a tool fingerprint. Higher severity than the generic non-Microsoft UA rule because the additional burst requirement removes the developer-prototype false-positive class.

Entra ID OAuth Device Code Sign-in to Azure AD Graph Enumeration - EQL sequence, high severity. Joins a successful device-code sign-in to the legacy AAD Graph audience (00000002-0000-0000-c000-000000000000) on an unmanaged device with directory enumeration against graph.windows.net by the same user within five minutes. Device-code phishing lands an OAuth token without touching the user's password or MFA, so immediate Graph reads of users, service principals, applications, role assignments, policies, or tenant details under that token are the compromised identity being driven by the attacker. Cross-data-stream sequence removes the single-event false-positive class that the other AAD Graph rules carry.

AAD Graph visibility: what comes next

For most of the past decade, AAD Graph activity was the telemetry equivalent of dark matter. We knew it was there because adversary tooling kept pointing at it, but customers had no diagnostic stream to subscribe to and no logs to query. Microsoft Graph Activity Logs closed half the gap when they went GA in April 2024. AzureADGraphActivityLogs finally closed the other half in early 2026.

Now that the data exists, the rest is on us. Add the new diagnostic setting, point it at an event hub, ingest into your stack, turn detections on (or create your own) and get to monitoring.

The detections in this post are a starting point. Once you have AAD Graph traffic landing in your stack and a baseline of what normal looks like in your tenant, the same patterns generalize. Legitimate first-party Microsoft callers form a small, recognisable set, and anything outside that set deserves a closer look.

The activity was always there. The visibility finally is too.

Happy hunting!

References

The following were referenced throughout the above research:

About Elastic Security Labs

Elastic Security Labs is the threat intelligence branch of Elastic Security dedicated to creating positive change in the threat landscape. Elastic Security Labs provides publicly available research on emerging threats with an analysis of strategic, operational, and tactical adversary objectives, then integrates that research with the built-in detection and response capabilities of Elastic Security.Follow Elastic Security Labs on Twitter @elasticseclabs and check out our research at www.elastic.co/security-labs/.

Share this article