Detection rules › Kusto

Idira - Multiple Failed Actions Followed by Success (15m)

Severity
medium
Time window
450d
Group by
targetAccount, username
Source
github.com/Azure/Azure-Sentinel

Detects 3+ failed actions against an account followed by a success in a short window, indicating brute-force or credential guessing.

MITRE ATT&CK coverage

TacticTechniques
Credential AccessNo specific technique

Rule body

id: bdf3cf98-d64f-4c55-97e4-43483d6d3237
kind: Scheduled
name: Idira - Multiple Failed Actions Followed by Success (15m)
description: >
  Detects 3+ failed actions against an account followed by a success in a short window,
  indicating brute-force or credential guessing.
severity: Medium
tactics:
  - CredentialAccess
queryFrequency: 5M
queryPeriod: 15M
triggerOperator: GreaterThan
triggerThreshold: 0
query: |
  let window = 15m;
  let fails =
      CyberArk_AuditEvents_CL
      | where TimeGenerated > ago(window)
      | where actionType has_any ("Fail","Failed","Denied","Error")
          or message has_any ("fail","failed","denied","unauthorized")
      | summarize Fails=count(), firstFail=min(TimeGenerated), lastFail=max(TimeGenerated) by username, targetAccount;
  let success =
      CyberArk_AuditEvents_CL
      | where TimeGenerated > ago(window)
      | where actionType has_any ("Success","Succeeded","Allow","Allowed")
          or message has_any ("success","allowed")
      | summarize Successes=count(), firstSuccess=min(TimeGenerated) by username, targetAccount;
  fails
  | where Fails >= 3
  | join kind=inner success on username, targetAccount
  | join kind=inner (
      CyberArk_AuditEvents_CL
      | extend cd = parse_json(customData)
      | extend cd_client_ip = tostring(cd.client_ip_address),
               cd_source_ip = tostring(cd.source_ip_address),
               cd_device_os = tostring(cd.device_os),
               cd_browser = tostring(cd.browser_name),
               cd_geo_city = tostring(cd.geoip_city_name),
               cd_geo_country = tostring(cd.geoip_country_name),
               cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
      | project username, targetAccount, any_cd_client_ip = cd_client_ip, any_cd_source_ip = cd_source_ip,
                any_cd_device_os = cd_device_os, any_cd_browser = cd_browser,
                any_cd_geo_city = cd_geo_city, any_cd_geo_country = cd_geo_country, any_cd_target = cd_target
    ) on username, targetAccount
  | project username, targetAccount, Fails, Successes, firstFail, lastFail, firstSuccess,
            any_cd_source_ip, any_cd_client_ip, any_cd_device_os, any_cd_browser,
            any_cd_geo_city, any_cd_geo_country, any_cd_target
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: username
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: targetAccount
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 0: let

let window = 15m;
let fails = CyberArk_AuditEvents_CL <inlined as stages below>;
let success =
    CyberArk_AuditEvents_CL
    | where TimeGenerated > ago(window)
    | where actionType has_any ("Success","Succeeded","Allow","Allowed")
        or message has_any ("success","allowed")
    | summarize Successes=count(), firstSuccess=min(TimeGenerated) by username, targetAccount;

Stage 1: source

CyberArk_AuditEvents_CL

Stage 2: where

| where TimeGenerated > ago(window)

Stage 3: where

| where actionType has_any ("Fail","Failed","Denied","Error")
        or message has_any ("fail","failed","denied","unauthorized")

Stage 4: summarize

| summarize Fails=count(), firstFail=min(TimeGenerated), lastFail=max(TimeGenerated) by username, targetAccount

Stage 5: where

| where Fails >= 3

Stage 6: join

| join kind=inner success on username, targetAccount

Stage 7: join

| join kind=inner (
    CyberArk_AuditEvents_CL
    | extend cd = parse_json(customData)
    | extend cd_client_ip = tostring(cd.client_ip_address),
             cd_source_ip = tostring(cd.source_ip_address),
             cd_device_os = tostring(cd.device_os),
             cd_browser = tostring(cd.browser_name),
             cd_geo_city = tostring(cd.geoip_city_name),
             cd_geo_country = tostring(cd.geoip_country_name),
             cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
    | project username, targetAccount, any_cd_client_ip = cd_client_ip, any_cd_source_ip = cd_source_ip,
              any_cd_device_os = cd_device_os, any_cd_browser = cd_browser,
              any_cd_geo_city = cd_geo_city, any_cd_geo_country = cd_geo_country, any_cd_target = cd_target
  ) on username, targetAccount

Stage 8: project

project Fails, Successes, any_cd_browser, any_cd_client_ip, any_cd_device_os, any_cd_geo_city, any_cd_geo_country, any_cd_source_ip, any_cd_target, firstFail, firstSuccess, lastFail, targetAccount, username

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Failsge
  • 3
field:"Fails" kind:ge value:"3"
actionTypematch
  • Allow transforms: term
  • Allowed transforms: term
  • Denied transforms: term
  • Error transforms: term
  • Fail transforms: term
  • Failed transforms: term
  • Succeeded transforms: term
  • Success transforms: term
field:"actionType" kind:match
messagematch
  • allowed transforms: term
  • denied transforms: term
  • fail transforms: term
  • failed transforms: term
  • success transforms: term
  • unauthorized transforms: term
field:"message" kind:match

Output fields

These fields are emitted when the rule matches.

FieldSource
Failsproject
Successesproject
any_cd_browserproject
any_cd_client_ipproject
any_cd_device_osproject
any_cd_geo_cityproject
any_cd_geo_countryproject
any_cd_source_ipproject
any_cd_targetproject
firstFailproject
firstSuccessproject
lastFailproject
targetAccountproject
usernameproject