Listen to this Post

Introduction:
Microsoft finally pulled back the curtain on the legacy Azure AD Graph API telemetry. For years, tools like ROADtools and AADInternals abused this deprecated, poorly logged channel to silently map your entire EntraID tenant — users, groups, roles, applications, and service principals — without triggering a single alert in traditional SignIn or Audit logs. The introduction of the `AADGraphActivityLogs` table (released broadly in early May 2026) closes that gap, handing defenders a powerful, forensic-grade data source to back‑hunt past compromises and detect ongoing reconnaissance in real time.
Learning Objectives:
- Detect low‑ and‑slow enumeration campaigns that use the deprecated Azure AD Graph API.
- Write KQL (Kusto Query Language) hunting queries to spot ROADtools and AADInternals by their endpoint sequences, user‑agent fingerprints, and/or total data volume exfiltrated.
- Operationalise the new log source: enable diagnostic settings, build alerting rules, and run an incident‑response playbook when a tenant‑wide dump is detected.
You Should Know:
1. Enable and Understand AADGraphActivityLogs
After years of blind spots, Microsoft began populating the `AADGraphActivityLogs` table in mid‑2026. These logs sit alongside your existing EntraID diagnostics and capture every legacy request to graph.windows.net.
Step‑by‑step enabling:
- Go to Microsoft Entra admin centre → Diagnostic settings under the Monitoring section.
- Click + Add diagnostic setting, give it a meaningful name, and under Category details select
AADGraphActivityLogs. - Choose your destination – for hunting, forward the logs to a Log Analytics workspace that is connected to Microsoft Sentinel or to a storage account for long‑term retention.
- Click Save.
Key fields for defenders:
| Field | Why it matters |
|-|-|
| `RequestUri` | The exact AAD Graph endpoint (e.g., /servicePrincipals, /users, /groups). |
| `RequestMethod` | Almost exclusively `GET` for reconnaissance. |
| `UserAgent` | Often leaks the tool (e.g., `aiohttp/3.10.11` for ROADtools, AADInternals). |
| `ResponseStatusCode` | 200–204 = success; 403 = token lacking permission; 404 = object not found. |
| `ResponseSizeBytes` | Total bytes returned – crucial for detecting full‑tenant dumps. |
| `CallerIpAddress` | Source IP of the API call (can be a corporate NAT, VPN, or attacker IP). |
| `SignInActivityId` | Cross‑references with `SigninLogs` to trace the original authentication. |
- Hunt Like a Pro: Two KQL Queries That Catch ROADtools
Once the logs are flowing, use KQL to pinpoint the toolkit’s signature. Below are two battle‑tested queries – one based on endpoint‑set matching, the other on total data volume.
A. Endpoint‑set (signature) hunting – this finds ROADtools’ default `gather` operation across a short time window.
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
What this does:
It groups requests by the identity (user or service principal) making the calls, extracts the API endpoint from each RequestUri, and compares the set of distinct endpoints against the 19 endpoints that ROADtools enumerates by default. A `ConfidenceScore` above 0.8 combined with more than 1,000 total requests is a very strong indicator of automated tenant‑wide mapping.
B. Volume‑based hunting – detects an attacker who spreads the dump over weeks.
let IdentitiesInEntra = toscalar(IdentityInfo | where TimeGenerated > ago(14d) | summarize dcount(AccountObjectId) ); AADGraphActivityLogs | where TimeGenerated > ago(30d) | extend ObjectId = coalesce(UserId, ServicePrincipalId) | summarize TotalBytes = sum(ResponseSizeBytes) by ObjectId | extend EstimatedDirSize = IdentitiesInEntra 55000 // ~55KB per identity | where TotalBytes > EstimatedDirSize
How to use it:
Replace `55000` with the average bytes actually returned per identity in your tenant (run a single ROADtools dump once in a lab and measure). This query catches anyone who, over a 30‑day window, retrieves more total data than the full directory would occupy – regardless of how slowly they spread the requests.
3. User‑Agent Canary: Flag AADInternals and aiohttp
Attackers often forget to change the default `UserAgent` strings, making detection trivial.
AADGraphActivityLogs
| where UserAgent has_any ("aiohttp", "AADInternals", "python-requests")
| extend ObjectId = iff(isempty(UserId), ServicePrincipalId, UserId)
| extend ObjectType = iff(isempty(UserId), "ServicePrincipalId", "UserId")
| project-reorder TimeGenerated, TimeRequested, ObjectId, ObjectType,
RequestUri, RequestMethod, ResponseStatusCode, UserAgent,
CallerIpAddress
This single query will catch nearly every out‑of‑the‑box run of ROADtools (uses aiohttp) and AADInternals (sets its own agent string). Create a scheduled alert for any occurrence that originates from outside your authorised IP ranges.
- Live Demo: Run a ROADtools Recon (and Watch It Light Up)
To understand the logs you are hunting, run a small, authorised enumeration in your own lab tenant.
On Linux / macOS / WSL:
pip3 install roadrecon roadrecon auth --tenant YOUR_TENANT_ID roadrecon gather roadrecon export
On Windows (PowerShell as administrator):
cd C:\AzAD\Tools\ python -m venv roadtools_env .\roadtools_env\Scripts\activate pip install roadrecon roadrecon auth --tenant YOUR_TENANT_ID roadrecon gather
After running the `gather` command, immediately run the first KQL query from Section 2 in your Log Analytics workspace. You will see the 19 endpoints, the `aiohttp` user agent, and – if you calculated the volume query – the total bytes transferred will match your tenant size.
5. Incident Response: What To Do After Detection
The moment you confirm a full‑tenant dump from an unexpected source, follow this IR checklist:
Step 1: Revoke the compromised tokens immediately.
Connect to Microsoft Graph PowerShell Connect-MgGraph -Scopes "User.Read.All", "UserAuthenticationMethod.ReadWrite.All" Revoke all refresh tokens for the compromised identity (replace objectId) Revoke-MgUserSignInSession -UserId "12345678-1234-1234-1234-123456789012"
Step 2: Force password reset and re‑evaluate MFA.
- Reset the user’s password (Entra admin centre → Users → select the user → Reset password).
- Require re‑registration of MFA methods (
Authentication methods→Require re‑register MFA).
Step 3: Hunt for second‑stage activity.
The attacker now knows every user, group, role, and application. Check for:
– New service principal credentials created after the dump time.
– Role assignments (especially `Global Administrator` or Application Administrator).
– OAuth consent grants added to high‑privileged apps.
Step 4: Block legacy protocol access (long‑term mitigation).
Use Microsoft Graph to block legacy authentication for the tenant Update-MgPolicyAuthorizationPolicy -BlockAzureADGraphAccess $true
This setting prevents any application from using the Azure AD Graph API – effectively killing the attack vector for all future attempts.
6. Extend Detection to Microsoft Graph Activity Logs
Because some tools (e.g., newer versions of Ping Castle) have moved to the Microsoft Graph, you should also monitor MicrosoftGraphActivityLogs. The same endpoint‑set technique applies – adapt the `ToolGraphQueries` list to contain the Microsoft Graph equivalents (e.g., /v1.0/users, /v1.0/servicePrincipals).
A combined detection rule in Sentinel that checks both tables will give you coverage for both legacy and modern API reconnaissance.
What Undercode Say:
- The blind spot is finally closed. `AADGraphActivityLogs` is a game‑changer for identity threat detection. Any organisation that continues to ignore it is effectively allowing attackers to operate in plain sight.
- But do not rely on the logs alone. The queries shown catch ROADtools’ default behaviour; determined adversaries will change user‑agents, randomise endpoints, and throttle requests. Combine these KQL hunts with network logs, UEBA, and periodic compliance checks that audit which apps still use the deprecated Graph API.
- Act on the logs immediately. The median delay between an API call and the log appearing is about 7 minutes – fast enough to build semi‑real‑time alerts. Turn these queries into scheduled alerts in Microsoft Sentinel, and include automated playbooks that revoke sessions when a volume anomaly is confirmed.
Prediction:
Within 12 months, Microsoft will accelerate the full retirement of the Azure AD Graph API, but until then, abused apps and old custom code will keep the endpoint alive. The real battle will shift – attackers will no longer rely on the legacy API itself, but will instead exploit the transition phase: confusing logs that partially write to both tables, misconfigurations where only one source is monitored, and the rare legitimate app that still requires legacy access. Defenders who master both `AADGraphActivityLogs` and `MicrosoftGraphActivityLogs` today will have a decisive head‑start.
▶️ Related Video (70% Match):
🎯Let’s Practice For Free:
IT/Security Reporter URL:
Reported By: Https: – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅


