Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

Azure Portal sign in from another Azure Tenant

Back
Id87210ca1-49a4-4a7d-bb4a-4988752f978c
RulenameAzure Portal sign in from another Azure Tenant
DescriptionThis query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant,

and the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look

to pivot to other tenants leveraging cross-tenant delegated access in this manner.
SeverityMedium
TacticsInitialAccess
TechniquesT1199
Required data connectorsAzureActiveDirectory
KindScheduled
Query frequency1h
Query period1h
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/AzurePortalSigninfromanotherAzureTenant.yaml
Version2.0.2
Arm template87210ca1-49a4-4a7d-bb4a-4988752f978c.json
Deploy To Azure
// Get details of current Azure Ranges (note this URL updates regularly so will need to be manually updated over time)
// You may find the name of the new JSON here: https://www.microsoft.com/download/details.aspx?id=56519
// On the downloads page, click the 'details' button, and then replace just the filename in the URL below
let azure_ranges = externaldata(changeNumber: string, cloud: string, values: dynamic)
["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
| mv-expand values
| mv-expand values.properties.addressPrefixes
| mv-expand values_properties_addressPrefixes
| summarize by tostring(values_properties_addressPrefixes)
| extend isipv4 = parse_ipv4(values_properties_addressPrefixes)
| extend isipv6 = parse_ipv6(values_properties_addressPrefixes)
| extend ip_type = case(isnotnull(isipv4), "v4", "v6")
| summarize make_list(values_properties_addressPrefixes) by ip_type
;
SigninLogs
// Limiting to Azure Portal really reduces false positives and helps focus on potential admin activity
| where ResultType == 0
| where AppDisplayName =~ "Azure Portal"
| extend isipv4 = parse_ipv4(IPAddress)
| extend ip_type = case(isnotnull(isipv4), "v4", "v6")
 // Only get logons where the IP address is in an Azure range
| join kind=fullouter (azure_ranges) on ip_type
| extend ipv6_match = ipv6_is_in_any_range(IPAddress,  list_values_properties_addressPrefixes)
| extend ipv4_match = ipv4_is_in_any_range(IPAddress,  list_values_properties_addressPrefixes)
| where ipv4_match or ipv6_match 
// Limit to where the user is external to the tenant
| where HomeTenantId != ResourceTenantId
// Further limit it to just access to the current tenant (you can drop this if you wanted to look elsewhere as well but it helps reduce FPs)
| where ResourceTenantId == AADTenantId
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), make_set(ResourceDisplayName) by UserPrincipalName, IPAddress, UserAgent, Location, HomeTenantId, ResourceTenantId, UserId
| extend AccountName = split(UserPrincipalName, "@")[0]
| extend UPNSuffix = split(UserPrincipalName, "@")[1]
kind: Scheduled
queryPeriod: 1h
description: |
  'This query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant,
   and the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look
   to pivot to other tenants leveraging cross-tenant delegated access in this manner.'  
tactics:
- InitialAccess
id: 87210ca1-49a4-4a7d-bb4a-4988752f978c
requiredDataConnectors:
- connectorId: AzureActiveDirectory
  dataTypes:
  - SigninLogs
relevantTechniques:
- T1199
severity: Medium
version: 2.0.2
status: Available
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: UserPrincipalName
  - identifier: Name
    columnName: AccountName
  - identifier: UPNSuffix
    columnName: UPNSuffix
- entityType: Account
  fieldMappings:
  - identifier: AadUserId
    columnName: UserId
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IPAddress
name: Azure Portal sign in from another Azure Tenant
triggerOperator: gt
query: |
  // Get details of current Azure Ranges (note this URL updates regularly so will need to be manually updated over time)
  // You may find the name of the new JSON here: https://www.microsoft.com/download/details.aspx?id=56519
  // On the downloads page, click the 'details' button, and then replace just the filename in the URL below
  let azure_ranges = externaldata(changeNumber: string, cloud: string, values: dynamic)
  ["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
  | mv-expand values
  | mv-expand values.properties.addressPrefixes
  | mv-expand values_properties_addressPrefixes
  | summarize by tostring(values_properties_addressPrefixes)
  | extend isipv4 = parse_ipv4(values_properties_addressPrefixes)
  | extend isipv6 = parse_ipv6(values_properties_addressPrefixes)
  | extend ip_type = case(isnotnull(isipv4), "v4", "v6")
  | summarize make_list(values_properties_addressPrefixes) by ip_type
  ;
  SigninLogs
  // Limiting to Azure Portal really reduces false positives and helps focus on potential admin activity
  | where ResultType == 0
  | where AppDisplayName =~ "Azure Portal"
  | extend isipv4 = parse_ipv4(IPAddress)
  | extend ip_type = case(isnotnull(isipv4), "v4", "v6")
   // Only get logons where the IP address is in an Azure range
  | join kind=fullouter (azure_ranges) on ip_type
  | extend ipv6_match = ipv6_is_in_any_range(IPAddress,  list_values_properties_addressPrefixes)
  | extend ipv4_match = ipv4_is_in_any_range(IPAddress,  list_values_properties_addressPrefixes)
  | where ipv4_match or ipv6_match 
  // Limit to where the user is external to the tenant
  | where HomeTenantId != ResourceTenantId
  // Further limit it to just access to the current tenant (you can drop this if you wanted to look elsewhere as well but it helps reduce FPs)
  | where ResourceTenantId == AADTenantId
  | summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), make_set(ResourceDisplayName) by UserPrincipalName, IPAddress, UserAgent, Location, HomeTenantId, ResourceTenantId, UserId
  | extend AccountName = split(UserPrincipalName, "@")[0]
  | extend UPNSuffix = split(UserPrincipalName, "@")[1]  
queryFrequency: 1h
alertDetailsOverride:
  alertDescriptionFormat: |
    This query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant,
    and the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look
    to pivot to other tenants leveraging cross-tenant delegated access in this manner.
    In this instance {{UserPrincipalName}} logged in at {{FirstSeen}} from IP Address {{IPAddress}}.    
  alertDisplayNameFormat: Azure Portal sign in by {{UserPrincipalName}} from another Azure Tenant with IP Address {{IPAddress}}
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/AzurePortalSigninfromanotherAzureTenant.yaml
triggerThreshold: 0
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2023-02-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/87210ca1-49a4-4a7d-bb4a-4988752f978c')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/87210ca1-49a4-4a7d-bb4a-4988752f978c')]",
      "properties": {
        "alertDetailsOverride": {
          "alertDescriptionFormat": "This query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant,\nand the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look\nto pivot to other tenants leveraging cross-tenant delegated access in this manner.\nIn this instance {{UserPrincipalName}} logged in at {{FirstSeen}} from IP Address {{IPAddress}}.\n",
          "alertDisplayNameFormat": "Azure Portal sign in by {{UserPrincipalName}} from another Azure Tenant with IP Address {{IPAddress}}"
        },
        "alertRuleTemplateName": "87210ca1-49a4-4a7d-bb4a-4988752f978c",
        "customDetails": null,
        "description": "'This query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant,\n and the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look\n to pivot to other tenants leveraging cross-tenant delegated access in this manner.'\n",
        "displayName": "Azure Portal sign in from another Azure Tenant",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "UserPrincipalName",
                "identifier": "FullName"
              },
              {
                "columnName": "AccountName",
                "identifier": "Name"
              },
              {
                "columnName": "UPNSuffix",
                "identifier": "UPNSuffix"
              }
            ]
          },
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "UserId",
                "identifier": "AadUserId"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "IPAddress",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/AzurePortalSigninfromanotherAzureTenant.yaml",
        "query": "// Get details of current Azure Ranges (note this URL updates regularly so will need to be manually updated over time)\n// You may find the name of the new JSON here: https://www.microsoft.com/download/details.aspx?id=56519\n// On the downloads page, click the 'details' button, and then replace just the filename in the URL below\nlet azure_ranges = externaldata(changeNumber: string, cloud: string, values: dynamic)\n[\"https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json\"] with(format='multijson')\n| mv-expand values\n| mv-expand values.properties.addressPrefixes\n| mv-expand values_properties_addressPrefixes\n| summarize by tostring(values_properties_addressPrefixes)\n| extend isipv4 = parse_ipv4(values_properties_addressPrefixes)\n| extend isipv6 = parse_ipv6(values_properties_addressPrefixes)\n| extend ip_type = case(isnotnull(isipv4), \"v4\", \"v6\")\n| summarize make_list(values_properties_addressPrefixes) by ip_type\n;\nSigninLogs\n// Limiting to Azure Portal really reduces false positives and helps focus on potential admin activity\n| where ResultType == 0\n| where AppDisplayName =~ \"Azure Portal\"\n| extend isipv4 = parse_ipv4(IPAddress)\n| extend ip_type = case(isnotnull(isipv4), \"v4\", \"v6\")\n // Only get logons where the IP address is in an Azure range\n| join kind=fullouter (azure_ranges) on ip_type\n| extend ipv6_match = ipv6_is_in_any_range(IPAddress,  list_values_properties_addressPrefixes)\n| extend ipv4_match = ipv4_is_in_any_range(IPAddress,  list_values_properties_addressPrefixes)\n| where ipv4_match or ipv6_match \n// Limit to where the user is external to the tenant\n| where HomeTenantId != ResourceTenantId\n// Further limit it to just access to the current tenant (you can drop this if you wanted to look elsewhere as well but it helps reduce FPs)\n| where ResourceTenantId == AADTenantId\n| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), make_set(ResourceDisplayName) by UserPrincipalName, IPAddress, UserAgent, Location, HomeTenantId, ResourceTenantId, UserId\n| extend AccountName = split(UserPrincipalName, \"@\")[0]\n| extend UPNSuffix = split(UserPrincipalName, \"@\")[1]\n",
        "queryFrequency": "PT1H",
        "queryPeriod": "PT1H",
        "severity": "Medium",
        "status": "Available",
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "InitialAccess"
        ],
        "techniques": [
          "T1199"
        ],
        "templateVersion": "2.0.2",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}