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

Brute force attack against Azure Portal

Back
Id28b42356-45af-40a6-a0b4-a554cdfd5d8a
RulenameBrute force attack against Azure Portal
DescriptionDetects Azure Portal brute force attacks by monitoring for multiple authentication failures and a successful login within a 20-minute window. Default settings: 10 failures, 25 deviations.

Ref: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.
SeverityMedium
TacticsCredentialAccess
TechniquesT1110
Required data connectorsAzureActiveDirectory
KindScheduled
Query frequency1d
Query period7d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/SigninBruteForce-AzurePortal.yaml
Version2.1.4
Arm template28b42356-45af-40a6-a0b4-a554cdfd5d8a.json
Deploy To Azure
// Set threshold value for deviation
let threshold = 25;
// Set the time range for the query
let timeRange = 24h;
// Set the authentication window duration
let authenticationWindow = 20m;
// Define a reusable function 'aadFunc' that takes a table name as input
let aadFunc = (tableName: string) {
  // Query the specified table
  table(tableName)
  // Filter data within the last 24 hours
  | where TimeGenerated > ago(1d)
  // Filter records related to "Azure Portal" applications
  | where AppDisplayName has "Azure Portal"
  // Extract and transform some fields
  | extend
      DeviceDetail = todynamic(DeviceDetail),
      LocationDetails = todynamic(LocationDetails)
  | extend
      OS = tostring(DeviceDetail.operatingSystem),
      Browser = tostring(DeviceDetail.browser),
      State = tostring(LocationDetails.state),
      City = tostring(LocationDetails.city),
      Region = tostring(LocationDetails.countryOrRegion)
  // Categorize records as Success or Failure based on ResultType
  | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
  // Sort and identify sessions
  | sort by UserPrincipalName asc, TimeGenerated asc
  | extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
  // Summarize data
  | summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName, SessionStartedUtc
  | summarize FailureCountBeforeSuccess = sumif(FailureOrSuccessCount, FailureOrSuccess == "Failure"), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress, 15), make_set(Browser, 15), make_set(City, 15), make_set(State, 15), make_set(Region, 15), make_set(ResultType, 15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type
  // Filter records where "Success" occurs in the middle of a session
  | where array_index_of(list_FailureOrSuccess, "Success") != 0
  | where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
  // Remove unnecessary columns from the output
  | project-away SessionStartedUtc, list_FailureOrSuccess
  // Join with another table and calculate deviation
  | join kind=inner (
      table(tableName)
      | where TimeGenerated > ago(7d)
      | where AppDisplayName has "Azure Portal"
      | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
      | summarize avgFailures = avg(todouble(FailureOrSuccess == "Failure")) by UserPrincipalName
  ) on UserPrincipalName
  | extend Deviation = abs(FailureCountBeforeSuccess - avgFailures) / avgFailures
  // Filter records based on deviation and failure count criteria
  | where Deviation > threshold and FailureCountBeforeSuccess >= 10
  // Expand the IPAddress array
  | mv-expand IPAddress
  | extend IPAddress = tostring(IPAddress)
  | extend timestamp = StartTime
};
// Call 'aadFunc' with different table names and union the results
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
// Additional transformation - Split UserPrincipalName
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
id: 28b42356-45af-40a6-a0b4-a554cdfd5d8a
name: Brute force attack against Azure Portal
requiredDataConnectors:
- connectorId: AzureActiveDirectory
  dataTypes:
  - SigninLogs
- connectorId: AzureActiveDirectory
  dataTypes:
  - AADNonInteractiveUserSignInLogs
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: UserPrincipalName
  - identifier: Name
    columnName: Name
  - identifier: UPNSuffix
    columnName: UPNSuffix
- entityType: Account
  fieldMappings:
  - identifier: AadUserId
    columnName: UserId
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IPAddress
description: |
  Detects Azure Portal brute force attacks by monitoring for multiple authentication failures and a successful login within a 20-minute window. Default settings: 10 failures, 25 deviations.
  Ref: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.  
status: Available
query: |
  // Set threshold value for deviation
  let threshold = 25;
  // Set the time range for the query
  let timeRange = 24h;
  // Set the authentication window duration
  let authenticationWindow = 20m;
  // Define a reusable function 'aadFunc' that takes a table name as input
  let aadFunc = (tableName: string) {
    // Query the specified table
    table(tableName)
    // Filter data within the last 24 hours
    | where TimeGenerated > ago(1d)
    // Filter records related to "Azure Portal" applications
    | where AppDisplayName has "Azure Portal"
    // Extract and transform some fields
    | extend
        DeviceDetail = todynamic(DeviceDetail),
        LocationDetails = todynamic(LocationDetails)
    | extend
        OS = tostring(DeviceDetail.operatingSystem),
        Browser = tostring(DeviceDetail.browser),
        State = tostring(LocationDetails.state),
        City = tostring(LocationDetails.city),
        Region = tostring(LocationDetails.countryOrRegion)
    // Categorize records as Success or Failure based on ResultType
    | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
    // Sort and identify sessions
    | sort by UserPrincipalName asc, TimeGenerated asc
    | extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
    // Summarize data
    | summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName, SessionStartedUtc
    | summarize FailureCountBeforeSuccess = sumif(FailureOrSuccessCount, FailureOrSuccess == "Failure"), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress, 15), make_set(Browser, 15), make_set(City, 15), make_set(State, 15), make_set(Region, 15), make_set(ResultType, 15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type
    // Filter records where "Success" occurs in the middle of a session
    | where array_index_of(list_FailureOrSuccess, "Success") != 0
    | where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
    // Remove unnecessary columns from the output
    | project-away SessionStartedUtc, list_FailureOrSuccess
    // Join with another table and calculate deviation
    | join kind=inner (
        table(tableName)
        | where TimeGenerated > ago(7d)
        | where AppDisplayName has "Azure Portal"
        | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
        | summarize avgFailures = avg(todouble(FailureOrSuccess == "Failure")) by UserPrincipalName
    ) on UserPrincipalName
    | extend Deviation = abs(FailureCountBeforeSuccess - avgFailures) / avgFailures
    // Filter records based on deviation and failure count criteria
    | where Deviation > threshold and FailureCountBeforeSuccess >= 10
    // Expand the IPAddress array
    | mv-expand IPAddress
    | extend IPAddress = tostring(IPAddress)
    | extend timestamp = StartTime
  };
  // Call 'aadFunc' with different table names and union the results
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
  // Additional transformation - Split UserPrincipalName
  | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])  
severity: Medium
triggerThreshold: 0
queryPeriod: 7d
queryFrequency: 1d
triggerOperator: gt
kind: Scheduled
tactics:
- CredentialAccess
relevantTechniques:
- T1110
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/SigninBruteForce-AzurePortal.yaml
version: 2.1.4
{
  "$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/28b42356-45af-40a6-a0b4-a554cdfd5d8a')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/28b42356-45af-40a6-a0b4-a554cdfd5d8a')]",
      "properties": {
        "alertRuleTemplateName": "28b42356-45af-40a6-a0b4-a554cdfd5d8a",
        "customDetails": null,
        "description": "Detects Azure Portal brute force attacks by monitoring for multiple authentication failures and a successful login within a 20-minute window. Default settings: 10 failures, 25 deviations.\nRef: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.\n",
        "displayName": "Brute force attack against Azure Portal",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "UserPrincipalName",
                "identifier": "FullName"
              },
              {
                "columnName": "Name",
                "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/SigninBruteForce-AzurePortal.yaml",
        "query": "// Set threshold value for deviation\nlet threshold = 25;\n// Set the time range for the query\nlet timeRange = 24h;\n// Set the authentication window duration\nlet authenticationWindow = 20m;\n// Define a reusable function 'aadFunc' that takes a table name as input\nlet aadFunc = (tableName: string) {\n  // Query the specified table\n  table(tableName)\n  // Filter data within the last 24 hours\n  | where TimeGenerated > ago(1d)\n  // Filter records related to \"Azure Portal\" applications\n  | where AppDisplayName has \"Azure Portal\"\n  // Extract and transform some fields\n  | extend\n      DeviceDetail = todynamic(DeviceDetail),\n      LocationDetails = todynamic(LocationDetails)\n  | extend\n      OS = tostring(DeviceDetail.operatingSystem),\n      Browser = tostring(DeviceDetail.browser),\n      State = tostring(LocationDetails.state),\n      City = tostring(LocationDetails.city),\n      Region = tostring(LocationDetails.countryOrRegion)\n  // Categorize records as Success or Failure based on ResultType\n  | extend FailureOrSuccess = iff(ResultType in (\"0\", \"50125\", \"50140\", \"70043\", \"70044\"), \"Success\", \"Failure\")\n  // Sort and identify sessions\n  | sort by UserPrincipalName asc, TimeGenerated asc\n  | extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == \"Success\")\n  // Summarize data\n  | summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName, SessionStartedUtc\n  | summarize FailureCountBeforeSuccess = sumif(FailureOrSuccessCount, FailureOrSuccess == \"Failure\"), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress, 15), make_set(Browser, 15), make_set(City, 15), make_set(State, 15), make_set(Region, 15), make_set(ResultType, 15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type\n  // Filter records where \"Success\" occurs in the middle of a session\n  | where array_index_of(list_FailureOrSuccess, \"Success\") != 0\n  | where array_index_of(list_FailureOrSuccess, \"Success\") == array_length(list_FailureOrSuccess) - 1\n  // Remove unnecessary columns from the output\n  | project-away SessionStartedUtc, list_FailureOrSuccess\n  // Join with another table and calculate deviation\n  | join kind=inner (\n      table(tableName)\n      | where TimeGenerated > ago(7d)\n      | where AppDisplayName has \"Azure Portal\"\n      | extend FailureOrSuccess = iff(ResultType in (\"0\", \"50125\", \"50140\", \"70043\", \"70044\"), \"Success\", \"Failure\")\n      | summarize avgFailures = avg(todouble(FailureOrSuccess == \"Failure\")) by UserPrincipalName\n  ) on UserPrincipalName\n  | extend Deviation = abs(FailureCountBeforeSuccess - avgFailures) / avgFailures\n  // Filter records based on deviation and failure count criteria\n  | where Deviation > threshold and FailureCountBeforeSuccess >= 10\n  // Expand the IPAddress array\n  | mv-expand IPAddress\n  | extend IPAddress = tostring(IPAddress)\n  | extend timestamp = StartTime\n};\n// Call 'aadFunc' with different table names and union the results\nlet aadSignin = aadFunc(\"SigninLogs\");\nlet aadNonInt = aadFunc(\"AADNonInteractiveUserSignInLogs\");\nunion isfuzzy=true aadSignin, aadNonInt\n// Additional transformation - Split UserPrincipalName\n| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P7D",
        "severity": "Medium",
        "status": "Available",
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CredentialAccess"
        ],
        "techniques": [
          "T1110"
        ],
        "templateVersion": "2.1.4",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}