{
  "$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/43df1e4c-61f8-4ab8-bc86-65eca7ecab9a')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/43df1e4c-61f8-4ab8-bc86-65eca7ecab9a')]",
      "properties": {
        "alertRuleTemplateName": "43df1e4c-61f8-4ab8-bc86-65eca7ecab9a",
        "customDetails": null,
        "description": "Identifies an illicit consent grant where a non-admin user consents to an\napplication requesting high-risk delegated mailbox permissions\n(Mail.Read, Mail.ReadWrite, Mail.Send or MailboxSettings.ReadWrite) together\nwith offline_access. offline_access returns a long-lived refresh token, so a\nsingle successful end-user consent gives an attacker-controlled application\ndurable, silent read/send access to the victim mailbox without re-prompting\nfor MFA. This is a common precursor to business email compromise (BEC),\nmailbox exfiltration and internal phishing. This rule complements\n\"Suspicious application consent for offline access\": it drops the\nknown-application allowlist join and instead pins the high-fidelity scope\ncombination, and it additionally covers mailbox write/send scopes\n(Mail.ReadWrite, Mail.Send, MailboxSettings.ReadWrite), not only read.\nFor AuditLogs schema see https://learn.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.\n",
        "displayName": "End-user consent to app with mailbox and offline access delegated scopes",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "Name",
                "identifier": "Name"
              },
              {
                "columnName": "UPNSuffix",
                "identifier": "UPNSuffix"
              },
              {
                "columnName": "GrantInitiatedByAadUserId",
                "identifier": "AadUserId"
              }
            ]
          },
          {
            "entityType": "CloudApplication",
            "fieldMappings": [
              {
                "columnName": "AppDisplayName",
                "identifier": "Name"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "GrantIpAddress",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft%20Entra%20ID/Analytic%20Rules/EndUserConsentMailboxOfflineAccess.yaml",
        "query": "let detectionWindow = 1d;\nAuditLogs\n| where TimeGenerated > ago(detectionWindow)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Consent to application\"\n| where TargetResources has \"offline_access\"\n| mv-apply TargetResource = TargetResources on\n  (\n      where TargetResource.type =~ \"ServicePrincipal\"\n      | extend AppDisplayName = tostring(TargetResource.displayName),\n               ServicePrincipalId = tolower(tostring(TargetResource.id)),\n               ModifiedProperties = TargetResource.modifiedProperties\n  )\n| mv-apply Property = ModifiedProperties on\n  (\n      where Property.displayName =~ \"ConsentAction.Permissions\"\n      | extend ConsentFull = trim(@'\"', tostring(Property.newValue))\n  )\n| where ConsentFull has \"offline_access\"\n| where ConsentFull has_any (\"Mail.Read\", \"Mail.ReadWrite\", \"Mail.Send\", \"MailboxSettings.ReadWrite\")\n| parse ConsentFull with * \"ConsentType: \" GrantConsentType \", Scope: \" GrantScope \"]\" *\n// Exact scope membership. has/has_any above is a cheap term-indexed pre-filter, but\n// term matching tokenizes \"Mail.Read\" to mail/read and would over-match scopes like\n// Mail.ReadBasic. Split the parsed scope string and require the exact scope names.\n| extend GrantScopes = split(GrantScope, \" \")\n| where set_has_element(GrantScopes, \"offline_access\")\n| where set_has_element(GrantScopes, \"Mail.Read\")\n    or set_has_element(GrantScopes, \"Mail.ReadWrite\")\n    or set_has_element(GrantScopes, \"Mail.Send\")\n    or set_has_element(GrantScopes, \"MailboxSettings.ReadWrite\")\n// End-user (delegated) consent only. Tenant-wide admin consent for all users\n// surfaces as ConsentType \"AllPrincipals\" and is scoped out here on purpose.\n| where GrantConsentType != \"AllPrincipals\"\n| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)\n| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)\n| extend GrantIpAddress = iff(isnotempty(tostring(InitiatedBy.user.ipAddress)), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))\n// AuditLogs does not guarantee the order of AdditionalDetails, so AdditionalDetails[0] silently\n// misses the user agent whenever another key comes first. User agent is enrichment here and must\n// never drop a consent event, so the array is padded before expanding it and the key is looked\n// up by name in a property bag.\n| extend AdditionalDetailPairs = iff(array_length(AdditionalDetails) > 0, AdditionalDetails, pack_array(bag_pack(\"key\", \"None\", \"value\", \"\")))\n| mv-apply AdditionalDetail = AdditionalDetailPairs on (summarize AdditionalDetailsBag = make_bag(bag_pack(tolower(tostring(AdditionalDetail.key)), tostring(AdditionalDetail.value))))\n| extend GrantUserAgent = tostring(AdditionalDetailsBag[\"user-agent\"])\n| extend Name = tostring(split(GrantInitiatedByUserPrincipalName, '@')[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName, '@')[1])\n| project TimeGenerated, GrantConsentType, GrantScope, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, AppDisplayName, ServicePrincipalId, GrantIpAddress, GrantUserAgent, OperationName, ConsentFull, CorrelationId, Name, UPNSuffix\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P1D",
        "severity": "High",
        "status": "Available",
        "subTechniques": [],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CredentialAccess"
        ],
        "techniques": [
          "T1528"
        ],
        "templateVersion": "1.0.0",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}
