Azure Microsoft Graph Single Session from Multiple IP Addresses


Description

Detects when a user signs in to Microsoft Entra ID and subsequently accesses Microsoft Graph from a different IP address using the same session ID. This behavior may indicate OAuth application abuse, session hijacking, token replay attacks, or adversary-in-the-middle attacks where an attacker has obtained a valid session token and is using it from their own infrastructure.

Query · python

from panther_azuresignin_helpers import azure_signin_alert_context, azure_signin_success

# Whitelisted application IDs (common Microsoft services that may legitimately use multiple IPs)
WHITELISTED_APP_IDS = {
    # Office 365 / Microsoft 365
    "00000003-0000-0ff1-ce00-000000000000",  # Office 365
    "00000006-0000-0ff1-ce00-000000000000",  # Office 365 Exchange Online
    "00b41c95-dab0-4487-9791-b9d2c32c80f2",  # Office 365 Management APIs
    "d3590ed6-52b3-4102-aeff-aad2292ab01c",  # Microsoft Office
    # Microsoft Teams
    "1fec8e78-bce4-4aaf-ab1b-5451cc387264",  # Microsoft Teams
    "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",  # Microsoft Teams Services
    "cc15fd57-2c6c-4117-a88c-83b1d56b4bbe",  # Microsoft Teams - Device Profile Service
    # OneDrive
    "ab9b8c07-8f02-4f72-87fa-80105867a763",  # OneDrive SyncEngine
    # Microsoft Edge
    "ea5a67f6-b6f3-4338-b240-c655ddc3cc8e",  # Microsoft Edge Insider Addons
    "ecd6b820-32c2-49b6-98a6-444530e5a77a",  # Edge Remote Settings
    "f44b1140-bc5e-48c6-8dc0-5cf5a53c0e34",  # Microsoft Edge
    # Azure Infrastructure Services
    "797f4846-ba00-4fd7-ba43-dac1f8f63013",  # Azure Resource Manager
    "8edd93e1-2103-40b4-bd70-6e34e586362d",  # Windows Azure Security Resource Provider
    "c44b4083-3bb0-49c1-b47d-974e53cbdf3c",  # Azure Portal
    "04b07795-8ddb-461a-bbee-02f9e1bf7b46",  # Microsoft Azure CLI
    "4962773b-9cdb-44cf-a8bf-237846a00ab7",  # Microsoft.EventGrid
    # Security & Threat Protection Services
    "fc780465-2017-40d4-a0c5-307022471b92",  # WindowsDefenderATP
    "8ee8fdad-f234-4243-8f3b-15c294843740",  # Microsoft Threat Protection
    "2793995e-0a7d-40d7-bd35-6968ba142197",  # Microsoft Defender for Cloud
    "3f6aecb4-6dbf-4e45-9141-440abdced562",  # PROD Microsoft Defender For Cloud XDR
    "7b7531ad-5926-4f2d-8a1d-38495ad33e17",  # Azure Advanced Threat Protection
    "df77edef-903d-416b-bcc0-cc8b91af54ea",  # Defender for IoT
    "8b3391f4-af01-4ee8-b4ea-9871b2499735",  # O365 Secure Score
    # Microsoft Graph & Identity Services
    "f8f7a2aa-e116-4ba6-8aea-ca162cfa310d",  # Microsoft Graph Connectors Core
    "01fc33a7-78ba-4d2f-a4b7-768e336e890e",  # MS-PIM (Privileged Identity Management)
    "bd11ca0f-4fd6-4bb7-a259-4a36693b6e13",  # MCAPI AAD Bridge Service
    "abc63b55-0325-4305-9e1e-3463b182a6dc",  # TenantSearchProcessors
    "eace8149-b661-472f-b40d-939f89085bd4",  # Substrate Instant Revocation Pipeline
    "b46c3ac5-9da6-418f-a849-0a07a10b3c6c",  # Cloud Infrastructure Entitlement Management
    "0469d4cd-df37-4d93-8a61-f8c75b809164",  # Policy Administration Service
    # Other Microsoft Services
    "18fbca16-2224-45f6-85b0-f7bf2b39b3f3",  # Microsoft Docs
    "98db8bd6-0cc0-4e67-9de5-f187f1cd1b41",  # Microsoft Substrate Management
    # Panther Integrations (distributed infrastructure)
    "6821c7a6-ae62-49ba-8669-3f2e72d8d803",  # GL - Panther Graph Integration
    # Microsoft Account Services
    "7eadcef8-456d-4611-9480-4fff72b8b9e2",  # Microsoft Account Controls V2
}


def rule(event):
    resource_display_name = event.deep_get("properties", "resourceDisplayName", default="")
    session_id = event.deep_get("properties", "sessionId", default="")
    user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
    source_ip = event.deep_get("properties", "ipAddress", default="")
    app_id = event.deep_get("properties", "appId", default="")

    # Skip if not Microsoft Graph access or whitelisted applications
    if "Microsoft Graph" not in resource_display_name or app_id in WHITELISTED_APP_IDS:
        return False

    is_interactive = event.deep_get("properties", "isInteractive", default=True)
    category = event.get("category", "")
    if not is_interactive or category == "NonInteractiveUserSignInLogs":
        return False

    # Skip if essential fields are missing or failed attempts
    if (
        not session_id
        or not user_principal_name
        or not source_ip
        or not azure_signin_success(event)
    ):
        return False

    return True


def unique(event):
    return event.deep_get("properties", "ipAddress", default="")


def dedup(event):
    session_id = event.deep_get("properties", "sessionId", default="")
    user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
    return f"{session_id}-{user_principal_name}"


def title(event):
    user_principal_name = event.deep_get(
        "properties", "userPrincipalName", default="<UNKNOWN_USER>"
    )
    session_id = event.deep_get("properties", "sessionId", default="<UNKNOWN_SESSION>")

    return (
        f"Microsoft Graph Session Access from Multiple IPs: "
        f"User [{user_principal_name}] in "
        f"session [{session_id}]"
    )


def alert_context(event):
    context = azure_signin_alert_context(event)

    # Add Graph access specific context
    context["session_id"] = event.deep_get("properties", "sessionId", default="<NO_SESSION>")
    context["app_display_name"] = event.deep_get("properties", "appDisplayName", default="<NO_APP>")
    context["app_id"] = event.deep_get("properties", "appId", default="<NO_APP_ID>")
    context["resource_display_name"] = event.deep_get(
        "properties", "resourceDisplayName", default="<NO_RESOURCE>"
    )
    context["authentication_protocol"] = event.deep_get(
        "properties", "authenticationProtocol", default="<NO_PROTOCOL>"
    )
    context["conditional_access_status"] = event.deep_get(
        "properties", "conditionalAccessStatus", default="<NO_CA_STATUS>"
    )
    context["is_interactive"] = event.deep_get("properties", "isInteractive", default=None)
    context["user_agent"] = event.deep_get("properties", "userAgent", default="<NO_USER_AGENT>")

    return context

Analyst notes

  1. Query Azure.Audit logs for all Microsoft Graph access events with properties:sessionId in the 4 hours around this alert to identify all source IP addresses, user agents, and accessed resources
  2. Compare the geographic locations and ASN information for all distinct callerIpAddress values to determine if they represent different countries, cloud providers, or inconsistent network types
  3. Review Azure.Audit logs for properties:userPrincipalName in the 7 days before this session to identify OAuth consent grants, new application registrations, risky sign-ins, or token issuance events that may indicate initial compromise
Raw source Azure Microsoft Graph Single Session from Multiple IP Addresses · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: azure_graph_session_multiple_ips.py
RuleID: "Azure.Audit.GraphSessionMultipleIPs"
DisplayName: "Azure Microsoft Graph Single Session from Multiple IP Addresses"
Enabled: true
Status: Experimental
LogTypes:
  - Azure.Audit
Severity: Medium
Threshold: 2
DedupPeriodMinutes: 60
Description: >
  Detects when a user signs in to Microsoft Entra ID and subsequently accesses Microsoft Graph from
  a different IP address using the same session ID. This behavior may indicate OAuth application abuse,
  session hijacking, token replay attacks, or adversary-in-the-middle attacks where an attacker has
  obtained a valid session token and is using it from their own infrastructure.
Tags:
  - Initial Access
  - Valid Accounts
  - Use Alternate Authentication Material
Reports:
  MITRE ATT&CK:
    - TA0001:T1078
    - TA0001:T1078.004
    - TA0005:T1550
    - TA0005:T1550.001
Runbook: |
  1. Query Azure.Audit logs for all Microsoft Graph access events with properties:sessionId in the 4 hours around this alert to identify all source IP addresses, user agents, and accessed resources
  2. Compare the geographic locations and ASN information for all distinct callerIpAddress values to determine if they represent different countries, cloud providers, or inconsistent network types
  3. Review Azure.Audit logs for properties:userPrincipalName in the 7 days before this session to identify OAuth consent grants, new application registrations, risky sign-ins, or token issuance events that may indicate initial compromise
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/initial_access_entra_id_graph_single_session_from_multiple_addresses.toml
SummaryAttributes:
  - properties:userPrincipalName
  - callerIpAddress
  - properties:sessionId
  - properties:appDisplayName
Tests:
  - Name: Multiple IP Access to Same Microsoft Graph Session
    ExpectedResult: true
    Log:
      {
        "time": "2025-01-15 09:35:20.456",
        "resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
        "operationName": "Sign-in activity",
        "operationVersion": "1.0",
        "category": "SignInLogs",
        "tenantId": "tenant-123",
        "resultType": "Success",
        "resultSignature": "SUCCESS",
        "durationMs": 0,
        "callerIpAddress": "5.5.5.5",
        "correlationId": "graph-session-002",
        "Level": "4",
        "properties":
          {
            "createdDateTime": "2025-01-15T09:35:20.4567890Z",
            "userPrincipalName": "gandalf@lotr.com",
            "userId": "user-123",
            "ipAddress": "5.5.5.5",
            "sessionId": "session-abc-123",
            "resourceDisplayName": "Microsoft Graph",
            "resourceId": "00000003-0000-0000-c000-111111111111",
            "appDisplayName": "Custom Graph App",
            "appId": "app-custom-789",
            "userAgent": "python-requests/2.28.1",
            "isInteractive": true,
            "authenticationProtocol": "oAuth2",
            "conditionalAccessStatus": "success",
          },
        "p_event_time": "2025-01-15 09:35:20.456",
        "p_log_type": "Azure.Audit",
        "p_parse_time": "2025-01-15 09:35:21.000",
        "p_row_id": "row-456",
      }
  - Name: Non-Interactive Sign-In via NonInteractiveUserSignInLogs Category
    ExpectedResult: false
    Log:
      {
        "time": "2026-02-18 07:34:37.443",
        "resourceId": "/tenants/18360841-3f87-44a6-8c9a-3ffc680611a0/providers/Microsoft.aadiam",
        "operationName": "Sign-in activity",
        "operationVersion": "1.0",
        "category": "NonInteractiveUserSignInLogs",
        "tenantId": "18360841-3f87-44a6-8c9a-3ffc680611a0",
        "resultType": "0",
        "resultSignature": "SUCCESS",
        "durationMs": 0,
        "callerIpAddress": "203.0.113.10",
        "correlationId": "6d657aae-f797-466f-8b35-31eb5b7ffde2",
        "Level": "4",
        "properties":
          {
            "createdDateTime": "2026-02-18T07:32:50.552947800Z",
            "userPrincipalName": "jane.doe@example.com",
            "userId": "3b4e2783-2b5d-4f1f-a3d9-ddc0ed1c623f",
            "ipAddress": "203.0.113.10",
            "sessionId": "00baf379-e510-523f-1c0e-346163d4a768",
            "resourceDisplayName": "Microsoft Graph",
            "resourceId": "00000003-0000-0000-c000-000000000000",
            "appDisplayName": "Custom App",
            "appId": "app-noninteractive-test-999",
            "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
            "isInteractive": false,
            "signInEventTypes": ["nonInteractiveUser"],
            "authenticationProtocol": "none",
            "conditionalAccessStatus": "notApplied",
          },
        "p_event_time": "2026-02-18 07:34:37.443",
        "p_log_type": "Azure.Audit",
      }
  - Name: Whitelisted Office 365
    ExpectedResult: false
    Log:
      {
        "time": "2025-01-15 10:00:30.789",
        "resourceId": "/tenants/tenant-456/providers/Microsoft.aadiam",
        "operationName": "Sign-in activity",
        "operationVersion": "1.0",
        "category": "SignInLogs",
        "tenantId": "tenant-456",
        "resultType": "Success",
        "resultSignature": "SUCCESS",
        "durationMs": 0,
        "callerIpAddress": "192.0.2.100",
        "correlationId": "office365-001",
        "Level": "4",
        "properties":
          {
            "createdDateTime": "2025-01-15T10:00:30.7890123Z",
            "userPrincipalName": "employee@company.com",
            "userId": "user-456",
            "ipAddress": "192.0.2.100",
            "sessionId": "session-office-456",
            "resourceDisplayName": "Microsoft Graph",
            "resourceId": "00000003-0000-0000-c000-000000000000",
            "appDisplayName": "Office 365",
            "appId": "00000003-0000-0ff1-ce00-000000000000",
            "userAgent": "Microsoft Office/16.0",
            "isInteractive": true,
            "authenticationProtocol": "oAuth2",
            "conditionalAccessStatus": "success",
          },
        "p_event_time": "2025-01-15 10:00:30.789",
        "p_log_type": "Azure.Audit",
      }

# ------ paired body: azure_graph_session_multiple_ips.py ------

from panther_azuresignin_helpers import azure_signin_alert_context, azure_signin_success

# Whitelisted application IDs (common Microsoft services that may legitimately use multiple IPs)
WHITELISTED_APP_IDS = {
    # Office 365 / Microsoft 365
    "00000003-0000-0ff1-ce00-000000000000",  # Office 365
    "00000006-0000-0ff1-ce00-000000000000",  # Office 365 Exchange Online
    "00b41c95-dab0-4487-9791-b9d2c32c80f2",  # Office 365 Management APIs
    "d3590ed6-52b3-4102-aeff-aad2292ab01c",  # Microsoft Office
    # Microsoft Teams
    "1fec8e78-bce4-4aaf-ab1b-5451cc387264",  # Microsoft Teams
    "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",  # Microsoft Teams Services
    "cc15fd57-2c6c-4117-a88c-83b1d56b4bbe",  # Microsoft Teams - Device Profile Service
    # OneDrive
    "ab9b8c07-8f02-4f72-87fa-80105867a763",  # OneDrive SyncEngine
    # Microsoft Edge
    "ea5a67f6-b6f3-4338-b240-c655ddc3cc8e",  # Microsoft Edge Insider Addons
    "ecd6b820-32c2-49b6-98a6-444530e5a77a",  # Edge Remote Settings
    "f44b1140-bc5e-48c6-8dc0-5cf5a53c0e34",  # Microsoft Edge
    # Azure Infrastructure Services
    "797f4846-ba00-4fd7-ba43-dac1f8f63013",  # Azure Resource Manager
    "8edd93e1-2103-40b4-bd70-6e34e586362d",  # Windows Azure Security Resource Provider
    "c44b4083-3bb0-49c1-b47d-974e53cbdf3c",  # Azure Portal
    "04b07795-8ddb-461a-bbee-02f9e1bf7b46",  # Microsoft Azure CLI
    "4962773b-9cdb-44cf-a8bf-237846a00ab7",  # Microsoft.EventGrid
    # Security & Threat Protection Services
    "fc780465-2017-40d4-a0c5-307022471b92",  # WindowsDefenderATP
    "8ee8fdad-f234-4243-8f3b-15c294843740",  # Microsoft Threat Protection
    "2793995e-0a7d-40d7-bd35-6968ba142197",  # Microsoft Defender for Cloud
    "3f6aecb4-6dbf-4e45-9141-440abdced562",  # PROD Microsoft Defender For Cloud XDR
    "7b7531ad-5926-4f2d-8a1d-38495ad33e17",  # Azure Advanced Threat Protection
    "df77edef-903d-416b-bcc0-cc8b91af54ea",  # Defender for IoT
    "8b3391f4-af01-4ee8-b4ea-9871b2499735",  # O365 Secure Score
    # Microsoft Graph & Identity Services
    "f8f7a2aa-e116-4ba6-8aea-ca162cfa310d",  # Microsoft Graph Connectors Core
    "01fc33a7-78ba-4d2f-a4b7-768e336e890e",  # MS-PIM (Privileged Identity Management)
    "bd11ca0f-4fd6-4bb7-a259-4a36693b6e13",  # MCAPI AAD Bridge Service
    "abc63b55-0325-4305-9e1e-3463b182a6dc",  # TenantSearchProcessors
    "eace8149-b661-472f-b40d-939f89085bd4",  # Substrate Instant Revocation Pipeline
    "b46c3ac5-9da6-418f-a849-0a07a10b3c6c",  # Cloud Infrastructure Entitlement Management
    "0469d4cd-df37-4d93-8a61-f8c75b809164",  # Policy Administration Service
    # Other Microsoft Services
    "18fbca16-2224-45f6-85b0-f7bf2b39b3f3",  # Microsoft Docs
    "98db8bd6-0cc0-4e67-9de5-f187f1cd1b41",  # Microsoft Substrate Management
    # Panther Integrations (distributed infrastructure)
    "6821c7a6-ae62-49ba-8669-3f2e72d8d803",  # GL - Panther Graph Integration
    # Microsoft Account Services
    "7eadcef8-456d-4611-9480-4fff72b8b9e2",  # Microsoft Account Controls V2
}


def rule(event):
    resource_display_name = event.deep_get("properties", "resourceDisplayName", default="")
    session_id = event.deep_get("properties", "sessionId", default="")
    user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
    source_ip = event.deep_get("properties", "ipAddress", default="")
    app_id = event.deep_get("properties", "appId", default="")

    # Skip if not Microsoft Graph access or whitelisted applications
    if "Microsoft Graph" not in resource_display_name or app_id in WHITELISTED_APP_IDS:
        return False

    is_interactive = event.deep_get("properties", "isInteractive", default=True)
    category = event.get("category", "")
    if not is_interactive or category == "NonInteractiveUserSignInLogs":
        return False

    # Skip if essential fields are missing or failed attempts
    if (
        not session_id
        or not user_principal_name
        or not source_ip
        or not azure_signin_success(event)
    ):
        return False

    return True


def unique(event):
    return event.deep_get("properties", "ipAddress", default="")


def dedup(event):
    session_id = event.deep_get("properties", "sessionId", default="")
    user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
    return f"{session_id}-{user_principal_name}"


def title(event):
    user_principal_name = event.deep_get(
        "properties", "userPrincipalName", default="<UNKNOWN_USER>"
    )
    session_id = event.deep_get("properties", "sessionId", default="<UNKNOWN_SESSION>")

    return (
        f"Microsoft Graph Session Access from Multiple IPs: "
        f"User [{user_principal_name}] in "
        f"session [{session_id}]"
    )


def alert_context(event):
    context = azure_signin_alert_context(event)

    # Add Graph access specific context
    context["session_id"] = event.deep_get("properties", "sessionId", default="<NO_SESSION>")
    context["app_display_name"] = event.deep_get("properties", "appDisplayName", default="<NO_APP>")
    context["app_id"] = event.deep_get("properties", "appId", default="<NO_APP_ID>")
    context["resource_display_name"] = event.deep_get(
        "properties", "resourceDisplayName", default="<NO_RESOURCE>"
    )
    context["authentication_protocol"] = event.deep_get(
        "properties", "authenticationProtocol", default="<NO_PROTOCOL>"
    )
    context["conditional_access_status"] = event.deep_get(
        "properties", "conditionalAccessStatus", default="<NO_CA_STATUS>"
    )
    context["is_interactive"] = event.deep_get("properties", "isInteractive", default=None)
    context["user_agent"] = event.deep_get("properties", "userAgent", default="<NO_USER_AGENT>")

    return context

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.