﻿---
title: Azure AD Graph Potential Enumeration (ROADrecon)
description: Detects an Azure AD Graph (graph.windows.net) burst from a user-agent identifying as "aiohttp" (the default HTTP library used by ROADrecon's "gather"...
url: https://www.elastic.co/docs/reference/security/prebuilt-rules/rules/integrations/azure/discovery_aad_graph_roadrecon_aiohttp_enumeration
products:
  - Elastic Security
---

# Azure AD Graph Potential Enumeration (ROADrecon)
Detects an Azure AD Graph (graph.windows.net) burst from a user-agent identifying as "aiohttp" (the default HTTP library
used by ROADrecon's "gather" command) where a single calling identity issues many requests in a short window. ROADrecon
walks every interesting directory object type via aiohttp, producing a large volume of requests from one
user / source IP / UA triple. The combination of "aiohttp" UA with a burst threshold is a structural ROADrecon
signature; legitimate first-party Microsoft components do not identify as aiohttp.
**Rule type**: esql
**Rule indices**:
**Rule Severity**: high
**Risk Score**: 73
**Runs every**: 
**Searches indices from**: `now-9m`
**Maximum alerts per execution**: 100
**References**:
- [[https://github.com/dirkjanm/ROADtools](https://github.com/dirkjanm/ROADtools)](https://github.com/dirkjanm/ROADtools)
- [[https://github.com/dirkjanm/ROADtools/blob/master/roadrecon/roadtools/roadrecon/gather.py](https://github.com/dirkjanm/ROADtools/blob/master/roadrecon/roadtools/roadrecon/gather.py)](https://github.com/dirkjanm/ROADtools/blob/master/roadrecon/roadtools/roadrecon/gather.py)
- [[https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview](https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview)](https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview)

**Tags**:
- Domain: Cloud
- Data Source: Azure
- Data Source: Azure AD Graph
- Data Source: Azure AD Graph Activity Logs
- Use Case: Threat Detection
- Tactic: Discovery
- Resources: Investigation Guide
- Noise: Unknown
- Performance: Fast
- Rule Type: ES|QL
- Platform: Entra ID
- Domain: Identity

**Version**: 3
**Rule authors**:
- Elastic

**Rule license**: Elastic License v2

#### Azure AD Graph Activity Logs

Requires Azure AD Graph Activity Logs ingested into `logs-azure.aadgraphactivitylogs-*` via the Elastic Azure integration. Enable the `AzureADGraphActivityLogs` diagnostic-settings category on Entra ID.

## Investigation guide


## Triage and analysis


### Investigating Azure AD Graph Potential Enumeration (ROADrecon)

This is an ES|QL aggregation rule. Alert documents contain summarized fields per burst window: the calling identity, the tenant, and a one-minute bucket. The alert itself is the signal that something resembling ROADrecon's `gather` walk happened against AAD Graph; the actual investigation happens against the raw `logs-azure.aadgraphactivitylogs-*` events for the same identity and window.

### Possible investigation steps

- Confirm the burst by filtering raw AAD Graph activity for the alerting user, tenant, and time window.
  - Filter `logs-azure.aadgraphactivitylogs-*` on the alerting user, tenant, and burst window.
  - ROADrecon's full `gather` walks ~16 directory collections; five or more in a single minute is the structural fingerprint.
- Tool fingerprint: aiohttp UA plus the hardcoded internal API version.
  - `user_agent.original` contains `aiohttp`.
  - `api_version = 1.61-internal` (hardcoded in `gather.py`, returns internal-only fields like `strongAuthenticationDetail`).
  - No first-party Microsoft component identifies as aiohttp or pins `1.61-internal`.
- Calling client + auth method: the typical device-code-flow ROADrecon entrypoint.
  - ROADrecon is usually pointed at the Azure CLI client (`04b07795-…`) via the `-c` flag.
  - Uses a public-client auth method (no client secret or certificate).
- HTTP shape distinguishes enumeration from operator follow-on.
  - `gather` reads only, so GETs dominate.
  - A 403/404 tail indicates the identity probing endpoints it lacks permission for.
  - PATCH / POST / DELETE in the same burst means the operator did more than enumerate.
- Source posture: residential ISP, generic VPS, or anonymising-network egress raises triage priority.
- Pivot to sign-in logs (`logs-azure.signinlogs-*`) via the sign-in correlation ID on each AAD Graph event to land on the originating token-mint.
- Pivot to audit logs (`logs-azure.auditlogs-*`) for any directory writes by the same user near the burst that suggest persistence or modification activity.
- Confirm the activity is not attributable to authorized testing before treating as malicious.
  - Check for red team engagement, penetration test, or internal tooling validation.
  - Validate against the engagement window and the operator's known source range.


### Response and remediation

- Enumerate device registrations created by the user during or around the burst window.
  - `GET /v1.0/users/{id}/registeredDevices` and `GET /v1.0/users/{id}/ownedDevices`.
  - De-register anything not attributable to a known endpoint via `DELETE /v1.0/devices/{deviceObjectId}`.
  - Do this BEFORE session revocation: device-bound PRTs survive `revokeSignInSessions`.
- Revoke refresh tokens and active sessions for the calling user.
  - `POST /v1.0/users/{id}/revokeSignInSessions`.
- Temporarily disable the user if the alert is high-confidence or you need to halt further activity while investigation continues.
  - `PATCH /v1.0/users/{id}` with body `{"accountEnabled": false}`.
- Audit OAuth grants and app role assignments the user holds; revoke anything minted from a kit-egress or otherwise suspicious source.
  - `GET /v1.0/oauth2PermissionGrants?$filter=principalId eq '{id}'`, revoke via `DELETE /v1.0/oauth2PermissionGrants/{grantId}`.
  - `GET /v1.0/users/{id}/appRoleAssignments`, revoke via `DELETE /v1.0/servicePrincipals/{spId}/appRoleAssignedTo/{assignmentId}`.
- Reset the user's password and audit authentication methods added during the window.
  - `GET /v1.0/users/{id}/authentication/methods` to list.
  - Remove anything unexpected via the method-type-specific endpoint.
- Audit directory writes by the user near the burst and roll back unauthorized changes.
  - Query `logs-azure.auditlogs-*` for `Register device`, `Update user`, `User registered security info`, role assignment activity by the same user in the window.
- If the calling application has no legitimate AAD Graph dependency, block further use by that app.
  - `PATCH /beta/applications/{id}` with body `{"authenticationBehaviors": {"blockAzureADGraphAccess": true}}`.
  - This property lives on the Graph beta endpoint, not v1.0.


## Rule Query

```esql
from logs-azure.aadgraphactivitylogs-* metadata _id, _version, _index

| where data_stream.dataset == "azure.aadgraphactivitylogs"
  and to_lower(user_agent.original) like "*aiohttp*"

| eval Esql.target_endpoints = case(
    url.path like "*/eligibleRoleAssignments*", "eligibleRoleAssignments",
    url.path like "*/roleAssignments*",         "roleAssignments",
    url.path like "*/users*",                   "users",
    url.path like "*/groups*",                  "groups",
    url.path like "*/servicePrincipals*",       "servicePrincipals",
    url.path like "*/applications*",            "applications",
    url.path like "*/devices*",                 "devices",
    url.path like "*/directoryRoles*",          "directoryRoles",
    url.path like "*/roleDefinitions*",         "roleDefinitions",
    url.path like "*/administrativeUnits*",     "administrativeUnits",
    url.path like "*/contacts*",                "contacts",
    url.path like "*/oauth2PermissionGrants*",  "oauth2PermissionGrants",
    url.path like "*/authorizationPolicy*",     "authorizationPolicy",
    url.path like "*/settings*",                "settings",
    url.path like "*/policies*",                "policies",
    url.path like "*/tenantDetails*",           "tenantDetails",
    "other"
  )
| where Esql.target_endpoints != "other"

| eval Esql.time_window = date_trunc(1 minutes, @timestamp)

| stats
    Esql.request_count                = count(*),
    Esql.distinct_endpoints           = count_distinct(Esql.target_endpoints),
    Esql.api_versions                 = values(azure.aadgraphactivitylogs.properties.api_version),
    Esql.app_ids                      = values(azure.aadgraphactivitylogs.properties.app_id),
    Esql.user_agent                   = values(user_agent.original),
    Esql.http_methods                 = values(http.request.method),
    Esql.status_codes                 = values(http.response.status_code),
    Esql.source_ips                   = values(source.ip),
    Esql.source_asn_orgs              = values(source.`as`.organization.name),
    Esql.source_countries             = values(source.geo.country_name),
    Esql.actor_types                  = values(azure.aadgraphactivitylogs.properties.actor_type),
    Esql.client_auth_methods          = values(azure.aadgraphactivitylogs.properties.client_auth_method),
    Esql.session_ids                  = values(azure.aadgraphactivitylogs.properties.session_id),
    Esql.sign_in_activity_ids         = values(azure.aadgraphactivitylogs.properties.sign_in_activity_id),
    Esql.scopes                       = values(azure.aadgraphactivitylogs.properties.scopes),
    Esql.first_seen                   = min(@timestamp),
    Esql.last_seen                    = max(@timestamp)
  by
    user.id,
    azure.tenant_id,
    Esql.time_window

| where Esql.distinct_endpoints >= 5

| keep
    user.id,
    azure.tenant_id,
    Esql.*
```

**Framework:** MITRE ATT&CK
- Tactic:
  - Name: Discovery
  - Id: TA0007
  - Reference URL: [[https://attack.mitre.org/tactics/TA0007/](https://attack.mitre.org/tactics/TA0007/)](https://attack.mitre.org/tactics/TA0007/)
- Technique:
  - Name: Permission Groups Discovery
  - Id: T1069
  - Reference URL: [[https://attack.mitre.org/techniques/T1069/](https://attack.mitre.org/techniques/T1069/)](https://attack.mitre.org/techniques/T1069/)
- Sub Technique:
  - Name: Cloud Groups
  - Id: T1069.003
  - Reference URL: [[https://attack.mitre.org/techniques/T1069/003/](https://attack.mitre.org/techniques/T1069/003/)](https://attack.mitre.org/techniques/T1069/003/)
- Technique:
  - Name: Account Discovery
  - Id: T1087
  - Reference URL: [[https://attack.mitre.org/techniques/T1087/](https://attack.mitre.org/techniques/T1087/)](https://attack.mitre.org/techniques/T1087/)
- Sub Technique:
  - Name: Cloud Account
  - Id: T1087.004
  - Reference URL: [[https://attack.mitre.org/techniques/T1087/004/](https://attack.mitre.org/techniques/T1087/004/)](https://attack.mitre.org/techniques/T1087/004/)
- Technique:
  - Name: Cloud Service Discovery
  - Id: T1526
  - Reference URL: [[https://attack.mitre.org/techniques/T1526/](https://attack.mitre.org/techniques/T1526/)](https://attack.mitre.org/techniques/T1526/)