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

Detection logic

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