Microsoft Graph Application Directory Reconnaissance with Denied Requests


Description

Identifies a Microsoft Graph application querying multiple directory collections and encountering access denials. Adversaries with compromised application credentials may enumerate users, groups, applications, service principals, directory roles, role assignments, and OAuth grants to identify privileged identities and opportunities for further access.

Query · esql

from logs-azure.graphactivitylogs-* metadata _id, _index
| where data_stream.dataset == "azure.graphactivitylogs"
  and azure.graphactivitylogs.properties.c_idtyp == "app"
  and azure.tenant_id is not null and azure.tenant_id != ""
  and azure.graphactivitylogs.properties.service_principal_id is not null
  and azure.graphactivitylogs.properties.service_principal_id != ""
  and http.request.method == "GET"
  and http.response.status_code in (200, 403)
  and url.path in (
      "/v1.0/applications",
      "/v1.0/servicePrincipals",
      "/v1.0/users",
      "/v1.0/groups",
      "/v1.0/directoryRoles",
      "/v1.0/roleManagement/directory/roleAssignments",
      "/v1.0/oauth2PermissionGrants"
)
| eval Esql.time_window = date_trunc(15 minutes, @timestamp)
| stats
    Esql.request_count = count(*),
    Esql.category_count = count_distinct(url.path),
    Esql.denied_count = sum(case(http.response.status_code == 403, 1, 0)),
    Esql.paths = values(url.path),
    Esql.status_codes = values(http.response.status_code),
    Esql.app_ids = values(azure.graphactivitylogs.properties.app_id),
    Esql.source_ips = values(source.ip),
    Esql.first_seen = min(@timestamp),
    Esql.last_seen = max(@timestamp)
  by Esql.time_window, azure.tenant_id, azure.graphactivitylogs.properties.service_principal_id
| where Esql.category_count >= 3 and Esql.denied_count >= 1
| keep azure.tenant_id, azure.graphactivitylogs.properties.service_principal_id, Esql.*

Investigation fields

Pivot points the source recommends for triage.

  • azure.tenant_id
  • azure.graphactivitylogs.properties.service_principal_id
  • Esql.paths
  • Esql.status_codes
  • Esql.source_ips
  • Esql.time_window

Implementation guide

Enable Microsoft Graph Activity Logs and ingest them through the Azure integration into logs-azure.graphactivitylogs-*. Retain the tenant and service-principal IDs, HTTP method, URL path, and response status. The azure.graphactivitylogs.properties.c_idtyp field must be populated with the token actor type; this rule selects app and does not infer app-only authentication from an application ID alone.

The rule evaluates a 30-minute ingestion lookback every five minutes and groups requests into fixed 15-minute event-time windows. Requests split across window boundaries or ingested more than 30 minutes apart may not be combined. Review delivery latency and repeated aggregate alerts when tuning the schedule. The rule covers the direct v1.0 collection paths in the query; beta endpoints and other request paths are outside its scope.

Known false positives

  • Application onboarding, directory synchronization, inventory tools, and permission-validation scripts may query multiple directory collections and encounter expected access denials.

Analyst notes

Investigating Microsoft Graph Application Directory Reconnaissance with Denied Requests

Microsoft Graph exposes directory information that can help an adversary identify accounts, privileged identities, and applications to target. This rule identifies app-only requests to at least three common directory discovery collections (applications, service principals, users, groups, directory roles, role assignments, and OAuth permission grants) within a 15-minute window, with at least one HTTP 403 response. The denial requirement reduces noise from authorized inventory applications that succeed across the collections they are granted. A denied request shows attempted discovery, not successful retrieval of that collection.

Possible investigation steps

  • Identify the calling principal using azure.tenant_id and azure.graphactivitylogs.properties.service_principal_id. Review Esql.app_ids and resolve the application owner and expected directory permissions.
  • Review Esql.time_window, Esql.request_count, Esql.category_count, and Esql.denied_count to establish the scope of the discovery. Use Esql.paths, Esql.status_codes, and Esql.source_ips to review the targeted collections, outcomes, and sources.
  • Examine the original Graph events to distinguish successfully accessed collections from denied requests. Determine whether the queried collections are needed for the application's documented purpose.
  • Correlate the principal with service-principal sign-ins, recent application credential additions, and changes to application permissions. Where available, compare azure.signinlogs.properties.service_principal_credential_key_id with the credential added in audit records.
  • Look for related role-assignment attempts, successful privilege changes, secret access, and access to other services. Distinguish the calling application from applications returned or targeted by its requests.
  • Confirm whether an inventory, directory synchronization, onboarding, or permission-validation workflow explains the activity.

False positive analysis

  • Directory inventory and synchronization applications may access several collections and encounter expected permission denials. Validate the activity with the application owner and compare it with normal sources and access patterns.
  • Onboarding and permission-validation scripts may generate similar requests. Scope any justified exception to the tenant, principal, and expected workflow, and reassess it when permissions or ownership change.

Response and remediation

  • If compromise is confirmed, restrict the affected principal and remove the specific compromised credential and unnecessary permissions according to the incident response process.
  • Review and remediate unauthorized privilege changes or downstream resource access associated with the principal.
  • Verify that subsequent access is denied after containment; credential removal alone does not establish that previously issued access tokens are unusable.
  • Preserve the original Graph activity, audit, and sign-in events for investigation.
Raw source Microsoft Graph Application Directory Reconnaissance with Denied Requests · Elastic TOML
Esc
Published by elastic/detection-rules ↗, licensed under Elastic License 2.0 ↗. Reproduced here unmodified.
[metadata]
creation_date = "2026/11/03"
integration = ["azure"]
maturity = "production"
updated_date = "2026/11/04"

[rule]
author = ["Elastic"]
description = """
Identifies a Microsoft Graph application querying multiple directory collections and encountering access denials.
Adversaries with compromised application credentials may enumerate users, groups, applications, service principals,
directory roles, role assignments, and OAuth grants to identify privileged identities and opportunities for further
access.
"""
false_positives = [
    """
    Application onboarding, directory synchronization, inventory tools, and permission-validation scripts may query
    multiple directory collections and encounter expected access denials.
    """,
]
from = "now-30m"
interval = "5m"
language = "esql"
license = "Elastic License v2"
name = "Microsoft Graph Application Directory Reconnaissance with Denied Requests"
note = """## Triage and analysis

### Investigating Microsoft Graph Application Directory Reconnaissance with Denied Requests

Microsoft Graph exposes directory information that can help an adversary identify accounts, privileged identities, and applications to target. This rule identifies app-only requests to at least three common directory discovery collections (applications, service principals, users, groups, directory roles, role assignments, and OAuth permission grants) within a 15-minute window, with at least one HTTP 403 response. The denial requirement reduces noise from authorized inventory applications that succeed across the collections they are granted. A denied request shows attempted discovery, not successful retrieval of that collection.

### Possible investigation steps

- Identify the calling principal using `azure.tenant_id` and `azure.graphactivitylogs.properties.service_principal_id`. Review `Esql.app_ids` and resolve the application owner and expected directory permissions.
- Review `Esql.time_window`, `Esql.request_count`, `Esql.category_count`, and `Esql.denied_count` to establish the scope of the discovery. Use `Esql.paths`, `Esql.status_codes`, and `Esql.source_ips` to review the targeted collections, outcomes, and sources.
- Examine the original Graph events to distinguish successfully accessed collections from denied requests. Determine whether the queried collections are needed for the application's documented purpose.
- Correlate the principal with service-principal sign-ins, recent application credential additions, and changes to application permissions. Where available, compare `azure.signinlogs.properties.service_principal_credential_key_id` with the credential added in audit records.
- Look for related role-assignment attempts, successful privilege changes, secret access, and access to other services. Distinguish the calling application from applications returned or targeted by its requests.
- Confirm whether an inventory, directory synchronization, onboarding, or permission-validation workflow explains the activity.

### False positive analysis

- Directory inventory and synchronization applications may access several collections and encounter expected permission denials. Validate the activity with the application owner and compare it with normal sources and access patterns.
- Onboarding and permission-validation scripts may generate similar requests. Scope any justified exception to the tenant, principal, and expected workflow, and reassess it when permissions or ownership change.

### Response and remediation

- If compromise is confirmed, restrict the affected principal and remove the specific compromised credential and unnecessary permissions according to the incident response process.
- Review and remediate unauthorized privilege changes or downstream resource access associated with the principal.
- Verify that subsequent access is denied after containment; credential removal alone does not establish that previously issued access tokens are unusable.
- Preserve the original Graph activity, audit, and sign-in events for investigation.
"""
references = [
    "https://www.microsoft.com/en-us/security/blog/2026/10/21/storm-0940-uses-artex-agentic-ai-connectwise-supply-chain-compromise/",
    "https://learn.microsoft.com/en-us/graph/microsoft-graph-activity-logs-overview",
]
risk_score = 47
rule_id = "ed66918f-5fb0-4cb2-a684-47e5d72e0a50"
setup = """#### Microsoft Graph Activity Logs

Enable Microsoft Graph Activity Logs and ingest them through the Azure integration into
`logs-azure.graphactivitylogs-*`. Retain the tenant and service-principal IDs, HTTP method, URL path, and response
status. The `azure.graphactivitylogs.properties.c_idtyp` field must be populated with the token actor type;
this rule selects `app` and does not infer app-only authentication from an application ID alone.

The rule evaluates a 30-minute ingestion lookback every five minutes and groups requests into fixed 15-minute
event-time windows. Requests split across window boundaries or ingested more than 30 minutes apart may not be
combined. Review delivery latency and repeated aggregate alerts when tuning the schedule. The rule covers
the direct v1.0 collection paths in the query; beta endpoints and other request paths are outside its scope.
"""
severity = "medium"
tags = [
    "Domain: Cloud",
    "Domain: Identity",
    "Data Source: Azure",
    "Data Source: Microsoft Graph Activity Logs",
    "Use Case: Threat Detection",
    "Tactic: Discovery",
    "Resources: Investigation Guide",
    "Rule Type: ES|QL",
    "Platform: Microsoft Graph",
    "Platform: Azure",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-azure.graphactivitylogs-* metadata _id, _index
| where data_stream.dataset == "azure.graphactivitylogs"
  and azure.graphactivitylogs.properties.c_idtyp == "app"
  and azure.tenant_id is not null and azure.tenant_id != ""
  and azure.graphactivitylogs.properties.service_principal_id is not null
  and azure.graphactivitylogs.properties.service_principal_id != ""
  and http.request.method == "GET"
  and http.response.status_code in (200, 403)
  and url.path in (
      "/v1.0/applications",
      "/v1.0/servicePrincipals",
      "/v1.0/users",
      "/v1.0/groups",
      "/v1.0/directoryRoles",
      "/v1.0/roleManagement/directory/roleAssignments",
      "/v1.0/oauth2PermissionGrants"
)
| eval Esql.time_window = date_trunc(15 minutes, @timestamp)
| stats
    Esql.request_count = count(*),
    Esql.category_count = count_distinct(url.path),
    Esql.denied_count = sum(case(http.response.status_code == 403, 1, 0)),
    Esql.paths = values(url.path),
    Esql.status_codes = values(http.response.status_code),
    Esql.app_ids = values(azure.graphactivitylogs.properties.app_id),
    Esql.source_ips = values(source.ip),
    Esql.first_seen = min(@timestamp),
    Esql.last_seen = max(@timestamp)
  by Esql.time_window, azure.tenant_id, azure.graphactivitylogs.properties.service_principal_id
| where Esql.category_count >= 3 and Esql.denied_count >= 1
| keep azure.tenant_id, azure.graphactivitylogs.properties.service_principal_id, Esql.*
'''

[rule.alert_suppression]
group_by = [
    "azure.tenant_id",
    "azure.graphactivitylogs.properties.service_principal_id",
]
missing_fields_strategy = "suppress"

[rule.alert_suppression.duration]
unit = "m"
value = 30

[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1069"
name = "Permission Groups Discovery"
reference = "https://attack.mitre.org/techniques/T1069/"
[[rule.threat.technique.subtechnique]]
id = "T1069.003"
name = "Cloud Groups"
reference = "https://attack.mitre.org/techniques/T1069/003/"


[[rule.threat.technique]]
id = "T1087"
name = "Account Discovery"
reference = "https://attack.mitre.org/techniques/T1087/"
[[rule.threat.technique.subtechnique]]
id = "T1087.004"
name = "Cloud Account"
reference = "https://attack.mitre.org/techniques/T1087/004/"



[rule.threat.tactic]
id = "TA0007"
name = "Discovery"
reference = "https://attack.mitre.org/tactics/TA0007/"

[rule.investigation_fields]
field_names = [
    "azure.tenant_id",
    "azure.graphactivitylogs.properties.service_principal_id",
    "Esql.paths",
    "Esql.status_codes",
    "Esql.source_ips",
    "Esql.time_window",
]

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.