Now You See Me: AADGraphActivityLogs
In my series “Detect threats using *GraphActivityLogs” I covered a lot of the basics on how to use different methods to detect certain reconnaissance tooling based on fingerprinting the specific sequence of requests or the volume of requests made to the Microsoft Graph endpoints. But one of the biggest detection gaps in all this was the Azure AD Graph, the old API on a retirement path that started before some of you might work in cyber security. All this changed in early May 2026 with the introduction of the AADGraphActivityLogs as a new log source. And don’t get me wrong, this table was in the official documentation since May 2025 but no data was flowing. After a long private preview Microsoft, almost silently released the log to all their customers and gave them crucial insights in one of the most abused protocols for reconnaissance. Notably toolkits like ROADtools and AADInternals use this API to gather deep insights into the tenant and attack vectors like the Intune Company portal Conditional Access bypass rely on the default grant to this resource.
Enable logs
Like all Entra ID logs you can configure the log forwarding of the AADGraphActivityLogs in the Diagnostic Settings of Entra ID. For my purpose I forward them to a Sentinel enabled Log Analytics workspace.
Log schema
While the log schema itself is very extensive, let’s concentrate on the fields that are crucial for detection engineers and defenders.
- TimeRequested The actual time the AAD Graph API call was made
- RequestMethod The REST API method used (e.g. POST, GET, PATCH, DELETE)
- ResponseStatusCode Response from the API itself (200 - 204, 302, 303, 400, 403, 404)
- ResponseSizeBytes The total size of the Graph response
- SignInActivityId The related signin activity Id that can be used to map this to the signin logs
- TokenIssuedAt Date and time when the access token was issued by Entra ID
- ActorType Either User or Application
- ServicePrincipalId or UserId The object Id of the identity
- AppId The app id that was used to connect to Azure AD graph
- UserAgent The UserAgent the client provided
- RequestUri The actual API endpoint requested
- CallerIpAddress The IP address from where the API call was made
Detection opportunities
UserAgent
Especially the UserAgent can be used for simple detections, since attackers did not have to give too much thought about opsec for this API, they might forget to change the source code and looking for AADInternals or aiohttp might expose adversaries already.
AADGraphActivityLogs
| where UserAgent has "aiohttp"
| extend ObjectId = iff(isempty(UserId), ServicePrincipalId, UserId)
| extend ObjectType = iff(isempty(UserId), "ServicePrincipalId", "UserId")
| project-reorder TimeGenerated, TimeRequested, ObjectId, ObjectType, RequestUri, RequestMethod, ResponseStatusCode, UserAgent
RequestUri
Gathering information for a whole tenant requires a lot of requests to different endpoints, and this is your chance to find abusers. The AAD Graph API has only a few official use cases left, so the requests to this API are by far more normalised and uniform than what you see in the Microsoft Graph API. This helps immensely baselining activity.
As in my last blog posts about the Graph API, I created a hunting query which you can use to find ROADtools based on the endpoints the default gather parameter requests.
let ToolGraphQueries = dynamic([
"servicePrincipals",
"groups",
"roleAssignments",
"eligibleRoleAssignments",
"applicationRefs",
"directoryRoles",
"directoryObjects",
"applications",
"policies",
"oauth2PermissionGrants",
"administrativeUnits",
"roleDefinitions",
"authorizationPolicy",
"devices",
"contacts",
"settings",
"tenantDetails",
"users"
]);
AADGraphActivityLogs
| extend ObjectId = iff(isempty(UserId), ServicePrincipalId, UserId)
| extend ObjectType = iff(isempty(UserId), "ServicePrincipalId", "UserId")
| extend Endpoint = extract(@'^/(?:v2/)?[^/]+/([A-Za-z0-9$]+)', 1, RequestUri)
| summarize
GraphEndpointsCalled = make_set(Endpoint, 1000),
TotalCount=count(),
TimeGenerated=min(TimeGenerated),
UniqueCount=dcount(Endpoint),
CallerIpAddress=take_any(CallerIpAddress)
by ObjectId, ObjectType
| project
TimeGenerated,
ObjectId,
ObjectType,
TotalCount,
UniqueCount,
CallerIpAddress,
MatchingQueries=set_intersect(ToolGraphQueries, GraphEndpointsCalled)
| extend ConfidenceScore = round(todouble(array_length(MatchingQueries)) / todouble(array_length(ToolGraphQueries)), 1)
| where ConfidenceScore > 0.8
| where TotalCount > 1000
ResponseSizeBytes
Dumping Entra ID to disk is a resource intensive task. You need to query all the information and transfer it. And this is where the amount of bytes transferred to a user or service principal can be a big tell. In the following hunting query I use the IdentityInfo table to calculate the estimated size of your Entra ID environment. Make sure to baseline your environment, as there could be legitimate use cases for such a dump, which you might not want to alert.
let IdentitiesInEntra = toscalar(IdentityInfo
| where TimeGenerated > ago(14d)
| summarize dcount(AccountObjectId));
AADGraphActivityLogs
| where TimeGenerated > ago(30d)
| extend ObjectId = coalesce(UserId, ServicePrincipalId)
| summarize sum(ResponseSizeBytes) by ObjectId
| extend DirectorySize = IdentitiesInEntra * 55000
| where sum_ResponseSizeBytes > DirectorySize
This will even find an attacker that takes a slow approach to data gathering. Even if they dump Entra ID within a month, the total size is larger than the directory size.
IdentityId.You should recalculate the directory size based on your environment, as the IdentityInfo only includes identities, but not groups and other elements. The easiest way is to use ROADtools and run it against your environment and then use the total size of this action divided by the total amount of identities in your environment.
Ping Castle
In my first blog post I also mentioned Ping Castle as one of the applications that is using the AADGraph for recon purposes. In my research with the latest version (v3.5.0.44) I discovered that Ping Castle has moved away from using the AAD Graph directly but rather is using Microsoft Graph instead. You will still find three AAD endpoints that are used as part of the recon activity, but all three, getEffectiveAccess, tenantDetails and policies are actually backend calls by Microsoft services. They use user agent kiota-dotnet/1.16.4 in Microsoft Graph.
Detour: LLM based hunting for ROADtools activity
After I wrote the detections and hunting queries above, I wanted to do something fun and test how an AI agent, without any prior knowledge of the data would handle this task.
Environment information
- Lab tenant, no real user activity
- Harness: opencode v1.14.25
- Model: GPT-5.5 on Microsoft Foundry
- Skills:
- XDR Hunting How to query XDR advanced hunting data including information about how to request schema information about tables and a list of available tables
Hunting session
Assessment
High-confidence discovery/reconnaissance activity found in AADGraphActivityLogs over the last 14 days.
Key Finding
Account roadrunner@c4a8korriban.com / object ID 12e3576c-665e-4c9e-96df-a2f34ca7eaf0 performed large-scale Azure AD Graph enumeration from 95.217.58.XXX.
Evidence:
| Indicator | Value |
|---|---|
| Time window | 2026-05-04 21:32:38Z to 2026-05-05 21:30:47Z |
| Total AAD Graph requests | 11,612 |
| Successful requests | 11,316 |
| Failed requests | 296 |
| Unique URIs | 3,284 |
| Method | GET only |
| User agent | Python/3.8 aiohttp/3.10.11 |
| Source IP | 95.217.58.XXX |
| IP owner | Hetzner Online GmbH, Finland-hosted range |
| Identity risk | Medium in IdentityInfo |
Observed Enumeration The requests covered directory-wide discovery targets:
| Category | Requests | Unique URIs |
|---|---|---|
servicePrincipals |
5,946 |
1,818 |
applicationRefs / other app objects |
4,005 |
933 |
groups |
1,246 |
391 |
directoryRoles |
370 |
116 |
applications |
16 |
1 |
devices |
10 |
1 |
users |
6 |
1 |
oauth2PermissionGrants |
5 |
1 |
tenantDetails |
5 |
1 |
appRoleAssignments |
3 |
1 |
Sample paths included:
/servicePrincipals/{id}/appRoleAssignedTo/servicePrincipals/{id}/appRoleAssignments/groups/{id}/$links/members/groups/{id}/$links/owners/directoryRoles/{id}/$links/members/oauth2PermissionGrants/applications/devices/users/tenantDetails
This pattern is consistent with automated tenant mapping: service principals, group membership, role membership, application references, OAuth grants, and app role assignments.
Sign-In Context Related sign-ins were found from the same IP:
| Application | AppId | Notes |
|---|---|---|
| Microsoft Azure CLI | 04b07795-8ddb-461a-bbee-02f9e1bf7b46 |
Main source of successful Graph enumeration |
| Azure Active Directory PowerShell | 1b730954-1685-4b74-9bfd-dac224a7b894 |
Initial probing, mostly 403 failures |
Sign-in user agents included browser-based interactive auth and python-requests/2.32.4, followed by Graph activity using Python/3.8 aiohttp/3.10.11. |
Timing Activity occurred in three short automated bursts:
| Time UTC | Requests |
|---|---|
2026-05-04 21:30-21:45 |
3,859 |
2026-05-05 07:10-07:25 |
3,787 |
2026-05-05 21:05-21:35 |
3,966 |
Other Activity
Other actors showed much lower discovery-like volume and looked closer to routine service principal access. None approached the breadth, automation indicators, or success volume of roadrunner@c4a8korriban.com.
Recommended Actions
- Treat
roadrunner@c4a8korriban.comas potentially compromised until validated. - Revoke sessions and refresh tokens for the account.
- Reset credentials and verify MFA methods.
- Review whether access from
95.217.58.XXX/ Hetzner is expected. - Review Azure CLI and Azure AD PowerShell consent/token usage for this account.
- Hunt downstream for use of discovered objects, especially role assignments, app credentials, service principal changes, and OAuth grant changes after
2026-05-04 21:32Z.
Which apps use AAD Graph
According to the data in my lab tenant there are 31 apps that use the Azure AD Graph API. I mapped the AppIds to the respective app names, as not all are easily resolvable.
| AppId | App name |
|---|---|
00000003-0000-0ff1-ce00-000000000000 |
Office 365 SharePoint Online |
00000006-0000-0ff1-ce00-000000000000 |
Microsoft Office 365 Portal |
00000014-0000-0000-c000-000000000000 |
Microsoft.Azure.SyncFabric |
01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9 |
Device Registration Service |
0469d4cd-df37-4d93-8a61-f8c75b809164 |
Policy Administration Service |
04b07795-8ddb-461a-bbee-02f9e1bf7b46 |
Microsoft Azure CLI |
14d82eec-204b-4c2f-b7e8-296a70dab67e |
Microsoft Graph Command Line Tools |
18ed3507-a475-4ccb-b669-d66bc9f2a36e |
Microsoft_AAD_RegisteredApps |
1950a258-227b-4e31-a9cf-717495945fc2 |
Microsoft Azure PowerShell |
1b730954-1685-4b74-9bfd-dac224a7b894 |
Azure Active Directory PowerShell |
1e2ca66a-c176-45ea-a877-e87f7231e0ee |
Microsoft B2B Admin Worker |
4430986e-63a0-4cfb-9414-5e4016e62005 |
Unknown |
4660504c-45b3-4674-a709-71951a6b0763 |
Microsoft Invitation Acceptance Portal |
47ee738b-3f1a-4fc7-ab11-37e4822b007e |
Azure AD Application Proxy |
65d91a3d-ab74-42e6-8a2f-0add61688c74 |
Microsoft Approval Management |
66244124-575c-4284-92bc-fdd00e669cea |
IAMTenantCrawler |
66a88757-258c-4c72-893c-3e8bed4d6899 |
QueryFormulationService |
74658136-14ec-4630-ad9b-26e160ff0fc6 |
ADIbizaUX |
80ccca67-54bd-44ab-8625-4b79c4dc7775 |
Microsoft 365 Security and Compliance Center |
93625bc8-bfe2-437a-97e0-3d0060024faa |
Microsoft password reset service |
972bb84a-1d27-4bd3-8306-6b8e57679e8c |
Microsoft Defender for Cloud Apps - APIs |
9d4afbbc-06a4-49e0-8005-4e5afd1d4fec |
ZTNA Network Access Control Plane |
a3dfc3c6-2c7d-4f42-aeec-b2877f9bce97 |
Microsoft Azure AD Identity Protection |
bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4 |
CPIM Service |
bb8f18b0-9c38-48c9-a847-e1ef3af0602d |
Microsoft.Azure.ActiveDirectoryIUX |
c40dfea8-483f-469b-aafe-642149115b3a |
Microsoft_AAD_Devices |
d9d5c99e-b0b4-4bad-92cc-5a6eb5421985 |
M365 Lighthouse Service |
de8bc8b5-d9f9-48b1-a8ad-b748da725064 |
Graph Explorer |
ea890292-c8c8-4433-b5ea-b09d0668e1a6 |
Azure Credential Configuration Endpoint Service |
f0ae4899-d877-4d3c-ae25-679e38eea492 |
AAD App Management |
fd14a986-6fe4-409a-883e-cdec1009cd54 |
Intune Grouping and Targeting Client Prod |
Other notes
As Invictus IR pointed out in their own post about the logs, the SignInActivityId is different between the actual sign-in logs and the activity logs. But not only the mentioned padding (==) is different. For around 55% of my logs this GUID style if present and not available in any other log source I have connected to my Sentinel.
AADGraphActivityLogs
| where TimeGenerated > ago(90d)
| summarize by SignInActivityId
| summarize
Padded=countif(SignInActivityId endswith "=="),
GUID=countif(SignInActivityId matches regex @"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"),
TotalUnique=dcount(SignInActivityId)
Conclusion
First of all a big thank you to the responsible people at Microsoft who made this possible, the additional insights into this crucial but deprecated API will help many companies to detect attacks early and maybe stop the attackers in their tracks. As with the other Graph Activity Logs the required information for threat hunting is available and with the link to other logs like Sign-in logs this helps to identify the initial access and allows for informed follow up hunting.
As to my AI experiment. It’s great to see how fast and good the response to this prompt was. The reasoning came to the same conclusion as I came about the attacker and enriched the hunt with additional data from whois and AADSignInEventsBeta. While it missed the volume based approach, it was enough to pinpoint all my ROADtool runs without problems. Using these capabilities as an additional tool in your tool-belt, while still using you own brain to challenge the findings will make your research faster. And with cost at under 1 USD it’s definitely not very expensive. But of course since this environment is not noisy at all, the conditions were primed for the LLM to succeed. There were no noisy custom apps or users doing what users do best, deviate from the norm.
To all of you reading this blog: Have fun hunting through this dataset, try to establish baselines, discover unused endpoints that are not triggered by normal behaviour anymore and build alerts that are actionable for your Analysts. Happy hunting.
Further reading
- Migrate Azure AD Graph apps to Microsoft Graph
- Important Update: Azure AD Graph Retirement
- AADGraphActivityLogs schema documentation
- ROADtools
- AADInternals
- Compliant Device Bypass using Intune Company Portal
- Azure AD Graph default permissions (EntraScopes)
- Entra ID Diagnostic Settings
- Azure Monitor log reference: AADGraphActivityLogs
- Invictus IR: The Missing Link: AADGraphActivityLogs Finally Arrives