LoFP LoFP / this pattern may occur during legitimate device switching or roaming between networks (e.g., corporate to mobile). developers or power users leveraging multiple environments may also trigger this detection if session persistence spans ip ranges. still, this behavior is rare and warrants investigation when rapid ip switching and graph access are involved.

Techniques

Sample rules

Microsoft Entra ID Session Reuse with Suspicious Graph Access

Description

Identifies potential session hijacking or token replay in Microsoft Entra ID. This rule detects cases where a user signs in and subsequently accesses Microsoft Graph from a different IP address using the same session ID within a short time window. This may indicate the use of a stolen refresh/access token or session cookie to impersonate the user and interact with Microsoft services.

Detection logic

FROM logs-azure.*
| WHERE
    (event.dataset == "azure.signinlogs" AND source.`as`.organization.name != "MICROSOFT-CORP-MSN-AS-BLOCK" AND azure.signinlogs.properties.session_id IS NOT NULL)
    OR
    (event.dataset == "azure.graphactivitylogs" AND source.`as`.organization.name != "MICROSOFT-CORP-MSN-AS-BLOCK" AND azure.graphactivitylogs.properties.c_sid IS NOT NULL)
| EVAL
    session_id = COALESCE(azure.signinlogs.properties.session_id, azure.graphactivitylogs.properties.c_sid),
    user_id = COALESCE(azure.signinlogs.properties.user_id, azure.graphactivitylogs.properties.user_principal_object_id),
    client_id = COALESCE(azure.signinlogs.properties.app_id, azure.graphactivitylogs.properties.app_id),
    source_ip = source.ip,
    event_time = @timestamp,
    event_type = CASE(
        event.dataset == "azure.signinlogs", "signin",
        event.dataset == "azure.graphactivitylogs", "graph",
        "other"
    ),
    time_window = DATE_TRUNC(5 minutes, @timestamp)
| KEEP session_id, source_ip, event_time, event_type, time_window, user_id, client_id
| STATS
    user_id = VALUES(user_id),
    session_id = VALUES(session_id),
    source_ip_list = VALUES(source_ip),
    source_ip_count = COUNT_DISTINCT(source_ip),
    client_id_list = VALUES(client_id),
    application_count = COUNT_DISTINCT(client_id),
    event_type_list = VALUES(event_type),
    event_type_count = COUNT_DISTINCT(event_type),
    event_start = MIN(event_time),
    event_end = MAX(event_time),
    signin_time = MIN(CASE(event_type == "signin", event_time, NULL)),
    graph_time = MIN(CASE(event_type == "graph", event_time, NULL)),
    document_count = COUNT()
  BY session_id, time_window
| EVAL
    duration_minutes = DATE_DIFF("minutes", event_start, event_end),
    signin_to_graph_delay_minutes = DATE_DIFF("minutes", signin_time, graph_time)
| WHERE
    event_type_count > 1 AND
    source_ip_count > 1 AND
    duration_minutes <= 5 AND
    signin_time IS NOT NULL AND
    graph_time IS NOT NULL AND
    signin_to_graph_delay_minutes >= 0