Techniques
Sample rules
Azure AD Graph Potential Enumeration (ROADrecon)
- source: elastic
- technicques:
- T1069
- T1087
- T1526
Description
Detects a burst of Azure AD Graph (graph.windows.net) directory enumeration by a single identity, keyed on the internal
API version that ROADrecon hardcodes (1.61-internal, with 1.6-internal as a related tooling value) plus a per-user
request burst. ROADrecon’s gather command walks the directory via this internal API version using aiohttp.
This rule also detects the “evade” argument that routes every read through a single “/$batch” OData endpoint under a benign
“Microsoft ADO.NET Data Services” user-agent. A lower burst threshold applies when a scripting user-agent (aiohttp/python/fasthttp)
is present and a higher volume threshold applies otherwise.
Detection logic
from logs-azure.aadgraphactivitylogs-* metadata _id, _version, _index
| where data_stream.dataset == "azure.aadgraphactivitylogs"
and azure.aadgraphactivitylogs.properties.api_version in ("1.61-internal", "1.6-internal")
| eval Esql.ua_lower = to_lower(user_agent.original)
| eval Esql.scripting_user_agent = Esql.ua_lower like "*aiohttp*" or Esql.ua_lower like "python*" or Esql.ua_lower like "*fasthttp*"
| eval Esql.target_endpoint = case(
url.path like "*$batch*", "batch",
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"
)
| eval Esql.time_window = date_trunc(1 minutes, @timestamp)
| stats
Esql.request_count = count(*),
Esql.distinct_endpoints = count_distinct(url.path),
Esql.collections = values(Esql.target_endpoint),
Esql.scripting_user_agent_seen = max(case(Esql.scripting_user_agent, 1, 0)),
Esql.api_versions = values(azure.aadgraphactivitylogs.properties.api_version),
Esql.app_ids = values(azure.aadgraphactivitylogs.properties.app_id),
Esql.user_agents = 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
| eval Esql.enum_detected = case(
Esql.scripting_user_agent_seen == 1 and Esql.distinct_endpoints >= 5, true,
Esql.distinct_endpoints >= 15, true,
Esql.request_count >= 150, true,
false
)
| where Esql.enum_detected == true
| keep
user.id,
azure.tenant_id,
Esql.*