LoFP LoFP / developer activity using aiohttp or python http libraries against aad graph for prototyping. rare in production tenants and typically low-volume.

Techniques

Sample rules

Azure AD Graph Potential Enumeration (ROADrecon)

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.*