{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2024-01-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/67802748-435b-4f80-9f61-b9a9ac6ea15c')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/67802748-435b-4f80-9f61-b9a9ac6ea15c')]",
      "properties": {
        "alertDetailsOverride": {
          "alertDynamicProperties": []
        },
        "alertRuleTemplateName": "67802748-435b-4f80-9f61-b9a9ac6ea15c",
        "customDetails": null,
        "description": "Detects repeated use of the same OAuth token across different IPs or locations over time. This can indicate token theft or session abuse.\n",
        "displayName": "[Entra ID] Suspicious Continuous OAuth Token Usage",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "Source_Identity_FullName",
                "identifier": "Name"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "Source_Network_IPAddress",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/eDCRule/Analytic%20Rules/%5BEntra%20ID%5D%20Suspicious%20Continuous%20OAuth%20Token%20Usage.yaml",
        "query": "// =====================\n// Low-noise Token Reuse Detector (fixed timespan calc)\n// =====================\nlet GapDays = 1;\nlet MinIPChanges = 2;\nlet MinLocChanges = 2;\nlet ExcludeApps = dynamic([\n    \"Microsoft Authentication Broker\",\n    \"Microsoft Teams\",\n    \"Office 365\"\n    ]);\nlet Src =\n    union isfuzzy=true\n        AADNonInteractiveUserSignInLogs,\n        SigninLogs\n    | where ResultType == 0\n    | where isnotempty(UniqueTokenIdentifier);\nlet Past =\n    Src\n    | where TimeGenerated between (ago(14d) .. ago(1d))\n    | summarize\n        PastLastSeen = max(TimeGenerated),\n        PastUPNs     = make_set(UserPrincipalName, 20),\n        PastIPs      = make_set(IPAddress, 50),\n        PastLocs     = make_set(tostring(Location), 50),\n        PastApps     = make_set(AppDisplayName, 50)\n        by UniqueTokenIdentifier;\nlet Last24h =\n    Src\n    | where TimeGenerated >= ago(1d)\n    //| where AppDisplayName !in (ExcludeApps)\n    | summarize\n        CurrentFirstSeen = min(TimeGenerated),\n        CurrentLastSeen  = max(TimeGenerated),\n        CurrentUPNs      = make_set(UserPrincipalName, 20),\n        CurrentIPs       = make_set(IPAddress, 50),\n        CurrentLocs      = make_set(tostring(Location), 50),\n        CurrentApps      = make_set(AppDisplayName, 50),\n        IPCount          = dcount(IPAddress),\n        LocCount         = dcount(tostring(Location))\n        by UniqueTokenIdentifier, ResultType;\nLast24h\n| join kind=inner Past on UniqueTokenIdentifier\n| extend Gap = CurrentFirstSeen - PastLastSeen\n| extend GapThreshold = totimespan(strcat(GapDays, \"d\"))\n| where Gap >= GapThreshold\n| where IPCount >= MinIPChanges or LocCount >= MinLocChanges\n| extend NewIPs  = set_difference(CurrentIPs, PastIPs)\n| extend NewLocs = set_difference(CurrentLocs, PastLocs)\n| where array_length(NewIPs) > 0 or array_length(NewLocs) > 0\n| extend\n    CurrentUPNCount = array_length(CurrentUPNs),\n    PastUPNCount = array_length(PastUPNs)\n| project\n    UniqueTokenIdentifier,\n    PastLastSeen,\n    CurrentFirstSeen,\n    CurrentLastSeen,\n    Gap,\n    GapThreshold,\n    CurrentUPNs,\n    CurrentUPNCount,\n    CurrentIPs,\n    IPCount,\n    NewIPs,\n    CurrentLocs,\n    LocCount,\n    NewLocs,\n    CurrentApps,\n    PastApps,\n    PastUPNs,\n    ResultType\n| order by Gap desc, CurrentLastSeen desc\n| extend UserNames = strcat_array(CurrentUPNs, \",\")\n| extend NewIP = strcat_array(NewIPs, \",\")\n| extend FirstNewIP = tostring(NewIPs[0])\n| extend NewLoc = strcat_array(NewLocs, \",\")\n| extend PastUPN = strcat_array(PastUPNs, \",\")\n| extend Source_Network_IPLocation = \"\"\n| project\n    Alert_Time_TW = datetime_utc_to_local(CurrentLastSeen, 'Asia/Taipei'),\n    Alert_Time_UTC0 = CurrentLastSeen,\n        Alert_Category_en = \"Entra ID\",\n    Alert_SubCategory_en = \"Anomaly Network Access User\",\n    Alert_Name_en = \"Abnormal Sign-in Token Reuse\",\n    Alert_Description_en=strcat(\n                         \"At Taiwan time: \",\n                         format_datetime(datetime_utc_to_local(CurrentLastSeen, \"Asia/Taipei\"), \"yyyy-MM-dd HH:mm:ss\"),\n                         \", detected suspected Token Reuse behavior (UniqueTokenIdentifier appeared repeatedly and the interval reached the threshold).\",\n                         \"Token:\",\n                         iff(isnotempty(UniqueTokenIdentifier), UniqueTokenIdentifier, \"<NoTokenId>\"),\n                         \", users (last 2 days): \",\n                         iff(isnotempty(tostring(UserNames)), tostring(UserNames), \"<NoUserNames>\"),\n                         \", previous last seen: \",\n                         format_datetime(datetime_utc_to_local(PastLastSeen, \"Asia/Taipei\"), \"yyyy-MM-dd HH:mm:ss\"),\n                         \", current first seen: \",\n                         format_datetime(datetime_utc_to_local(CurrentFirstSeen, \"Asia/Taipei\"), \"yyyy-MM-dd HH:mm:ss\"),\n                         \", gap: \",\n                         tostring(Gap),\n                         \" days\",\n                         \", IP count: \",\n                         tostring(IPCount),\n                         \", current sign-in IP: \",\n                         iff(isnotempty(tostring(NewIP)), tostring(NewIP), \"<NoNewIP>\"),\n                         \", location count: \",\n                         tostring(LocCount),\n                         \", current sign-in location: \",\n                         iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), \"<NoNewLoc>\"),\n                         \".\"\n                     ),\n    Alert_TriageStep_en=strcat(\n                        \" 1. Check whether this is truly token reuse. The reused IP is: \",\n                        iff(isnotempty(tostring(NewIP)), tostring(NewIP), \"<NoNewIPs>\"),\n                        \", current sign-in location: \",\n                        iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), \"<NoNewLocs>\"),\n                        \"  to determine whether it is an uncommon source, cross-country/cross-region, or anonymous cloud egress.\",\n                        \" 2. Check whether multiple accounts share the same token. Current triggering account count: \",\n                        tostring(CurrentUPNCount),\n                        \", accounts that previously triggered this token: \",\n                        tostring(PastUPN),\n                        \"; if the UPN count is greater than 1, prioritize suspicion of token leakage or proxy/automation abuse.\",\n                        \" 3. Check the applications currently involved in sign-in: \",\n                        iff(isnotempty(tostring(CurrentApps)), tostring(CurrentApps), \"<NoCurrentApps>\"),\n                        \"previous sign-in applications: \",\n                        iff(isnotempty(tostring(PastApps)), tostring(PastApps), \"<NoPastApps>\"),\n                        \"  to determine whether they include administrative/highly sensitive applications or abnormally newly added apps.\"\n                    ),\n    Alert_Containment_en=strcat(\n                         \"1. Immediately revoke sign-in tokens/sessions and force re-sign-in for users (last 2 days): \",\n                         iff(isnotempty(tostring(UserNames)), tostring(UserNames), \"<NoUserNames>\"),\n                         \"  to block continued access using the reused token.  \",\n                         \"2. If the current sign-in IP or location is abnormal, immediately block the current sign-in IP: \",\n                         iff(isnotempty(tostring(NewIP)), tostring(NewIP), \"<NoNewIP>\"),\n                         \", current sign-in location: \",\n                         iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), \"<NoNewLoc>\"),\n                         \", and tighten Conditional Access (MFA/compliant device/named location).  \",\n                         \"3. Immediately require account security recovery: reset the password, re-register MFA, and check for suspicious devices or added authentication methods.  \"\n                     ),\n    Alert_Remediation_en=strcat(\n                         \"1. Strengthen risk-based access: establish Conditional Access policies to block new IPs/new locations.  \",\n                         \"2. Implement token protection and endpoint governance: promote compliant devices, reduce long-lived token risk, and restrict access from unmanaged devices or legacy clients.  \",\n                         \"3. Inventory automation and applications: regularly check for abnormally added applications and high-privilege applications, and remove unnecessary permissions and old credentials.  \",\n                         \"4. Establish automated response: automatically revoke tokens, block IPs, notify users for confirmation, and create incident tickets for follow-up investigation when alerts trigger.\"\n                     ),\n            Event_Code = ResultType,\n    //Event_Description = ResultDescription,\n    Event_TimeRange_Start_UTC0 = CurrentFirstSeen,\n    Event_TimeRange_End_UTC0 = CurrentLastSeen,\n    Event_TimeRange_Start_TW = datetime_utc_to_local(CurrentFirstSeen, 'Asia/Taipei'),\n    Event_TimeRange_End_TW = datetime_utc_to_local(CurrentFirstSeen, 'Asia/Taipei'),\n    Source_Identity_FullName = UserNames,\n    Source_Network_IPAddress = NewIP,\n    Source_Network_IPLocation = Source_Network_IPLocation,\n    Target_Identity_FullName = UniqueTokenIdentifier,\n    //Target_Network_IPAddress = UniqueTokenIdentifier,\n    //Target_Resource_ID = \"\",\n    Target_Resource_Name = \"Microsoft Entra ID\",\n    Target_Resource_Type = \"Microsoft Entra ID\"\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P1D",
        "severity": "High",
        "status": "Available",
        "subTechniques": [],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CredentialAccess"
        ],
        "techniques": [
          "T1606"
        ],
        "templateVersion": "1.0.0",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}
