Contents

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.

/aadgraphactivitylogs/images/entra-diagnostic-settings-aadgraph.png
Entra ID Diagnostic Settings page showing AADGraphActivityLogs forwarding configuration to 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.

/aadgraphactivitylogs/images/aadgraphactivitylogs-schema-fields.png
AADGraphActivityLogs schema showing key fields including TimeRequested, RequestMethod, ResponseStatusCode, and UserAgent

Note
TimeGenerated and TimeRequested are important to understand and the median time difference between the two, in my lab environment, is about 7 minutes, with a maximum difference of 70 minutes. Make sure to adjust your queries and detections for those values.
  • 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.

Note
If you run this query in XDR Advanced Hunting you must adjust the size multiplier, as the IdentityInfo tables in Sentinel and XDR have different result sets. You might also want to change the summarize column to 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

User prompt
Analyse the AADGraphLog in the last 14 days for any potential discovery or reconnaissance activity
AI disclosure
The following response was generated by the LLM

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

  1. Treat roadrunner@c4a8korriban.com as potentially compromised until validated.
  2. Revoke sessions and refresh tokens for the account.
  3. Reset credentials and verify MFA methods.
  4. Review whether access from 95.217.58.XXX / Hetzner is expected.
  5. Review Azure CLI and Azure AD PowerShell consent/token usage for this account.
  6. 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.

/aadgraphactivitylogs/images/roadtools-detection-results-llm.png
LLM-based detection analysis showing ROADtools reconnaissance activity with 11,612 AAD Graph requests from account roadrunner

AI disclosure
End of LLM generated response

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)

/aadgraphactivitylogs/images/signin-activity-id-format-comparison.png
KQL query results showing SignInActivityId format comparison with Padded, GUID, and TotalUnique counts

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

  1. Migrate Azure AD Graph apps to Microsoft Graph
  2. Important Update: Azure AD Graph Retirement
  3. AADGraphActivityLogs schema documentation
  4. ROADtools
  5. AADInternals
  6. Compliant Device Bypass using Intune Company Portal
  7. Azure AD Graph default permissions (EntraScopes)
  8. Entra ID Diagnostic Settings
  9. Azure Monitor log reference: AADGraphActivityLogs
  10. Invictus IR: The Missing Link: AADGraphActivityLogs Finally Arrives