LoFP LoFP / authorized red team activity exercising roadrecon. document the engagement window and add exceptions on the source ip or calling user.

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