Detection rules › Kusto

Kusto rules: 2p

Cisco Umbrella - Request to blocklisted file type

#
Severity
medium
Time window
10m
Source
github.com/Azure/Azure-Sentinel

'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'

MITRE ATT&CK Coverage

TacticTechniques
Initial AccessNo specific technique

Rule Body

id: de58ee9e-b229-4252-8537-41a4c2f4045e
name: Cisco Umbrella - Request to blocklisted file type
description: |
  'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'
severity: Medium
requiredDataConnectors:
  - connectorId: CiscoUmbrellaDataConnector
    dataTypes:
      - Cisco_Umbrella_proxy_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
query: |
  let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);
  let lbtime = 10m;
  Cisco_Umbrella
  | where TimeGenerated > ago(lbtime)
  | where EventType == 'proxylogs'
  | where DvcAction =~ 'Allowed'
  | extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)
  | extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)
  | where file_ext in (file_ext_blocklist)
  | project TimeGenerated, SrcIpAddr, Identities, Filename
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Identities
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);
let lbtime = 10m;

Stage 1: source

Cisco_Umbrella

Stage 2: where

| where TimeGenerated > ago(lbtime)

Stage 3: where

| where EventType == 'proxylogs'

Stage 4: where

| where DvcAction =~ 'Allowed'

Stage 5: extend

| extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)

Stage 6: extend

| extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)

Stage 7: where

| where file_ext in (file_ext_blocklist)

Stage 8: project

| project TimeGenerated, SrcIpAddr, Identities, Filename

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Filenameproject
Identitiesproject
SrcIpAddrproject
TimeGeneratedproject

Cisco WSA - Suspected protocol abuse

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Detects possible protocol abuse.'

MITRE ATT&CK Coverage

Rule Body

id: 6f756792-4888-48a5-97cf-40d9430dc932
name: Cisco WSA - Suspected protocol abuse
description: |
  'Detects possible protocol abuse.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  CiscoWSAEvent
  | where DstPortNumber !in ('80', '443')
  | where NetworkApplicationProtocol in~ ('HTTP', 'HTTPs')
  | extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoWSAEvent

Stage 2: where

| where DstPortNumber !in ('80', '443')

Stage 3: where

| where NetworkApplicationProtocol in~ ('HTTP', 'HTTPs')

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
IPCustomEntityextend

Cloudflare - XSS probing pattern in request

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Detects XSS probing patterns.'

MITRE ATT&CK Coverage

Rule Body

id: 4d9d00b9-31a6-49e4-88c1-9e68277053ad
name: Cloudflare - XSS probing pattern in request
description: |
  'Detects XSS probing patterns.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let s_threshold = 3;
  Cloudflare
  | where HttpRequestMethod in~ ('POST', 'PUT')
  | extend susp_ch = countof(ClientRequestURI, '%00')
  | where ClientRequestURI matches regex @'(alert\()|(alert\%28)|(String\.fromCharCode\()|(expression\(alert)' or susp_ch > s_threshold 
  | extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: CompleteUrl
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let s_threshold = 3;

Stage 1: source

Cloudflare

Stage 2: where

| where HttpRequestMethod in~ ('POST', 'PUT')

Stage 3: extend

| extend susp_ch = countof(ClientRequestURI, '%00')

Stage 4: where

| where ClientRequestURI matches regex @'(alert\()|(alert\%28)|(String\.fromCharCode\()|(expression\(alert)' or susp_ch > s_threshold

Stage 5: extend

| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ClientRequestURIregex_match
    • (alert()
    • (alert\%28)
    • (String.fromCharCode()
    • (expression(alert)
field:"ClientRequestURI" kind:regex_match
HttpRequestMethodin
  • POST
  • PUT
field:"HttpRequestMethod" kind:in
susp_chgt
  • 3 transforms: number
field:"susp_ch" kind:gt value:"3"

Output Fields

These fields are emitted when the rule matches.

FieldSource
susp_chextend
CompleteUrlextend

DDoS Attack IP Addresses - PPS Threshold

#
Status
available
Severity
medium
Time window
1s
Group by
destPublicIpAddress_s, sec, sourcePublicIpAddress_s
Source
github.com/Azure/Azure-Sentinel

Identifies IP addresses that generates maximal traffic rate over 10k PPS during DDoS attack mitigation

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 6e76fd9d-8104-41eb-bad3-26054a3ad5f0
kind: Scheduled
name: DDoS Attack IP Addresses - PPS Threshold
description: Identifies IP addresses that generates maximal traffic rate over 10k PPS during DDoS attack mitigation
severity: Medium
status : Available
requiredDataConnectors:
  - connectorId: DDOS
    dataTypes:
      - AzureDiagnostics
queryPeriod: 2h
queryFrequency: 2h
triggerOperator: gt
triggerThreshold: 0
query: |
  AzureDiagnostics 
  | where ResourceType == 'PUBLICIPADDRESSES' and Category == "DDoSMitigationFlowLogs"
  //sample rate of mitigation device 1:1000 so the PPS of the source is estimated to be x1000.
  | summarize PPS = 1000 * count() by destPublicIpAddress_s, sec = bin(TimeGenerated, 1s), sourcePublicIpAddress_s
  | summarize maxPPS = max(PPS) by destPublicIpAddress_s, sourcePublicIpAddress_s
  | order by destPublicIpAddress_s, maxPPS desc
  | where maxPPS > 10000
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: sourcePublicIpAddress_s
tactics:
  - Impact
relevantTechniques:
  - T1498
version: 1.0.1

Stages and Predicates

Stage 1: source

AzureDiagnostics

Stage 2: where

| where ResourceType == 'PUBLICIPADDRESSES' and Category == "DDoSMitigationFlowLogs"

Stage 3: summarize

| summarize PPS = 1000 * count() by destPublicIpAddress_s, sec = bin(TimeGenerated, 1s), sourcePublicIpAddress_s

Stage 4: summarize

| summarize maxPPS = max(PPS) by destPublicIpAddress_s, sourcePublicIpAddress_s

Stage 5: sort

| order by destPublicIpAddress_s, maxPPS desc

Stage 6: where

| where maxPPS > 10000

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
destPublicIpAddress_ssummarize
maxPPSsummarize
sourcePublicIpAddress_ssummarize

Detect port misuse by anomaly based detection (ASIM Network Session schema)

#
Status
available
Severity
medium
Time window
14d
Group by
Description, DstPortNumber, DvcAction, Name, NetworkCustomAnalyticsExists, NetworkDirection, NetworkProtocol, NetworkSummary_Protocol_Exists, Severity, Tactic, exists
Source
github.com/Azure/Azure-Sentinel

This rule detects anomalous pattern in port usage. The rule utilize ASIM normalization, and is applied to any source which supports the ASIM Network Session schema. To tune the rule to your environment configure it using the 'NetworkSession_Monitor_Configuration' watchlist. This rule leverages log summaries generated by a Summary Rule or Summarized Playbook. If no such summaries are available, the rule falls back to direct analysis using ASIM function.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: cbf07406-fa2a-48b0-82b8-efad58db14ec
name: Detect port misuse by anomaly based detection (ASIM Network Session schema)
description: |
  'This rule detects anomalous pattern in port usage. The rule utilize [ASIM](https://aka.ms/AboutASIM) normalization, and is applied to any source which supports the ASIM Network Session schema. To tune the rule to your environment configure it using the 'NetworkSession_Monitor_Configuration' watchlist. This rule leverages log summaries generated by a Summary Rule or Summarized Playbook. If no such summaries are available, the rule falls back to direct analysis using ASIM function.'
severity: Medium
status: Available
tags:
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.4
requiredDataConnectors:
  - connectorId: AWSS3
    dataTypes:
      - AWSVPCFlow
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: MicrosoftSysmonForLinux
    dataTypes:
      - Syslog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
  - connectorId: AzureFirewall
    dataTypes:
      - AzureDiagnostics
  - connectorId: AzureNSG
    dataTypes:
      - AzureDiagnostics
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoAsaAma
    dataTypes:
      - CommonSecurityLog
  - connectorId: Corelight
    dataTypes:
      - Corelight_CL
  - connectorId: AIVectraStream
    dataTypes:
      - VectraStream
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoMeraki
    dataTypes:
      - Syslog
      - CiscoMerakiNativePoller

queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - LateralMovement
  - Execution
  - InitialAccess
relevantTechniques:
  - T1095  
  - T1059
  - T1203
  - T1190
query: |
  let lookback = 14d;
  let mapping = _GetWatchlist('NetworkSession_Monitor_Configuration')
      | where Type == "Detection" and ThresholdType == "Anomaly" and Severity != "Disabled"  
      | extend
          Ports = split(Ports, ","),
          App = split(App, ","),
          Protocol = split(Protocol, ","),
          Direction = split(Direction, ","),
          Action = split(Action, ",")
      | project
          Ports,
          App,
          Protocol,
          Direction,
          Action,
          Type,
          ThresholdType,
          Threshold,
          Severity,
          Tactic,
          Name,
          Description
      | mv-expand Ports
      | mv-expand App
      | mv-expand Protocol
      | mv-expand Direction
      | mv-expand Action
      | extend
          Ports = tostring(Ports),
          App = tostring(App),
          Protocol = tostring(Protocol),
          Direction = tostring(Direction),
          Action = tostring(Action),
          Threshold = toint(Threshold)
  ;
  let AnomalyThreshold = 2.5;
  let eps = materialize (_Im_NetworkSession
      | project TimeGenerated
      | where TimeGenerated > ago(5m)
      | count
      | extend Count = Count / 300);
  let maxSummarizedTime = toscalar (
      union isfuzzy=true
          (
          NetworkSummary_Protocol_CL
          | where EventTime > ago(lookback)
          | summarize max_TimeGenerated=max(EventTime)
          | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
          ),
          (
          NetworkCustomAnalytics_protocol_CL
          | where EventTime_t > ago(lookback)
          | summarize max_TimeGenerated=max(EventTime_t)
          | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
          ),
          (
          print(ago(lookback))
          | project max_TimeGenerated = print_0
          )
      | summarize maxTimeGenerated = max(max_TimeGenerated) 
      );
  let NetworkSummary_Protocol = materialize(
      union isfuzzy=true 
          (
          NetworkSummary_Protocol_CL
          | where EventTime > ago(1d)
          | project v = int(2)
          ),
          (
          print int(1) 
          | project v = print_0
          )
      | summarize maxv = max(v)
      | extend
          NetworkSummary_Protocol_Exists = (maxv > 1),
          NetworkCustomAnalyticsExists = false
      );
  let NetworkCustomAnalytics = materialize(
      union isfuzzy=true 
          (
          NetworkCustomAnalytics_protocol_CL
          | where EventTime_t > ago(1d) 
          | project v = int(2)
          ),
          (
          print int(1) 
          | project v = print_0
          )
      | summarize maxv = max(v)
      | extend
          NetworkCustomAnalyticsExists = (maxv > 1),
          NetworkSummary_Protocol_Exists = false
      );
  let allData = union isfuzzy=true 
          (
          (datatable(
      exists: int,
      NetworkSummary_Protocol_Exists: bool,
      NetworkCustomAnalyticsExists: bool
  )[
      1, false, false
  ]
          | where toscalar(eps) > 500
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
          )
          | join (
              _Im_NetworkSession(starttime=todatetime(ago(3d)), endtime=now())
              | where TimeGenerated > maxSummarizedTime
              | summarize Count=count()
                  by
                  NetworkProtocol,
                  DstPortNumber,
                  DstAppName,
                  NetworkDirection,
                  DvcAction,
                  bin(TimeGenerated, 10m)
              | extend
                  EventTime = TimeGenerated,
                  Count = toint(Count),
                  DstPortNumber = toint(DstPortNumber),
                  exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          (datatable(
      exists: int,
      NetworkSummary_Protocol_Exists: bool,
      NetworkCustomAnalyticsExists: bool
  )[
      1, false, false
  ]
          | where toscalar(eps) <= 500
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
          )
          | join (
              _Im_NetworkSession(starttime=todatetime(ago(4d)), endtime=now())
              | where TimeGenerated > maxSummarizedTime
              | summarize Count=count()
                  by
                  NetworkProtocol,
                  DstPortNumber,
                  DstAppName,
                  NetworkDirection,
                  DvcAction,
                  bin(TimeGenerated, 10m)
              | extend
                  EventTime = TimeGenerated,
                  Count = toint(Count),
                  DstPortNumber = toint(DstPortNumber),
                  exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          (datatable(exists: int, NetworkSummary_Protocol_Exists: bool)[1, false]
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          )
          | join (
              NetworkCustomAnalytics_protocol_CL
              | where EventTime_t > ago(lookback)
              | project
                  NetworkProtocol=NetworkProtocol_s,
                  DstPortNumber=DstPortNumber_d,
                  DstAppName=DstAppName_s,
                  NetworkDirection=NetworkDirection_s,
                  DvcAction=DvcAction_s,
                  Count=count__d,
                  EventTime=EventTime_t,
                  TimeGenerated,
                  Type
              | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber), exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          NetworkSummary_Protocol_CL
          | where EventTime > ago(lookback)
          | project
              NetworkProtocol,
              DstPortNumber,
              DstAppName,
              NetworkDirection,
              DvcAction,
              Count=count_,
              EventTime,
              TimeGenerated,
              Type
          | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)
          )
      | project-away exists*, maxv*, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
  ;
  allData
  | where isnotempty(DstPortNumber)
  | make-series Total=sum(Count) on EventTime from ago(lookback) to now() step 1d by DstPortNumber, NetworkProtocol, NetworkDirection, DvcAction
  | extend (anomalies, score, baseline) = series_decompose_anomalies(Total, AnomalyThreshold, -1, 'linefit')
  | mv-expand anomalies, score, baseline, EventTime, Total
  | extend
      anomalies = toint(anomalies),
      score = toint(score),
      baseline = toint(baseline),
      EventTime = todatetime(EventTime),
      Total = tolong(Total)
  | where EventTime >= ago(1d)
  | extend DstPortNumber = tostring(DstPortNumber)
  | where score >= 2 * AnomalyThreshold and baseline > 0
  | join kind=inner ['mapping'] where Ports == DstPortNumber
  | where (Protocol == "*" or Protocol has NetworkProtocol)
      and (Direction == "*" or Direction has NetworkDirection)
      and (Action == "*" or Action has DvcAction)  
  | project
      Name,
      Description,
      NetworkProtocol,
      DstPortNumber,
      NetworkDirection,
      DvcAction,
      Severity,
      Tactic
  | summarize
      NetworkProtocols=make_set_if(NetworkProtocol, isnotempty(NetworkProtocol), 20), 
      NetworkDirections=make_set_if(NetworkDirection, isnotempty(NetworkDirection), 5), 
      DvcActions=make_set_if(DvcAction, isnotempty(DvcAction), 10)
      by Name, Severity, Tactic, DstPortNumber, Description
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  AllNetworkProtocols: NetworkProtocols
  AllNetworkDirections: NetworkDirections
  AllDvcAction: DvcActions
  DstPortNumber: DstPortNumber
alertDetailsOverride:
  alertDisplayNameFormat: Detected {{Name}}
  alertDescriptionFormat: '{{Description}}'
  alertTacticsColumnName: Tactic
  alertSeverityColumnName: Severity
version: 1.3.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 14d;
let AnomalyThreshold = 2.5;

Let Binding: mapping

let mapping = _GetWatchlist('NetworkSession_Monitor_Configuration')
    | where Type == "Detection" and ThresholdType == "Anomaly" and Severity != "Disabled"  
    | extend
        Ports = split(Ports, ","),
        App = split(App, ","),
        Protocol = split(Protocol, ","),
        Direction = split(Direction, ","),
        Action = split(Action, ",")
    | project
        Ports,
        App,
        Protocol,
        Direction,
        Action,
        Type,
        ThresholdType,
        Threshold,
        Severity,
        Tactic,
        Name,
        Description
    | mv-expand Ports
    | mv-expand App
    | mv-expand Protocol
    | mv-expand Direction
    | mv-expand Action
    | extend
        Ports = tostring(Ports),
        App = tostring(App),
        Protocol = tostring(Protocol),
        Direction = tostring(Direction),
        Action = tostring(Action),
        Threshold = toint(Threshold);

Let Binding: eps

let eps = materialize (_Im_NetworkSession
    | project TimeGenerated
    | where TimeGenerated > ago(5m)
    | count
    | extend Count = Count / 300);

Let Binding: maxSummarizedTime

let maxSummarizedTime = toscalar (
    union isfuzzy=true
        (
        NetworkSummary_Protocol_CL
        | where EventTime > ago(lookback)
        | summarize max_TimeGenerated=max(EventTime)
        | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
        ),
        (
        NetworkCustomAnalytics_protocol_CL
        | where EventTime_t > ago(lookback)
        | summarize max_TimeGenerated=max(EventTime_t)
        | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
        ),
        (
        print(ago(lookback))
        | project max_TimeGenerated = print_0
        )
    | summarize maxTimeGenerated = max(max_TimeGenerated) 
    );

Let Binding: NetworkSummary_Protocol

let NetworkSummary_Protocol = materialize(
    union isfuzzy=true 
        (
        NetworkSummary_Protocol_CL
        | where EventTime > ago(1d)
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend
        NetworkSummary_Protocol_Exists = (maxv > 1),
        NetworkCustomAnalyticsExists = false
    );

Let Binding: NetworkCustomAnalytics

let NetworkCustomAnalytics = materialize(
    union isfuzzy=true 
        (
        NetworkCustomAnalytics_protocol_CL
        | where EventTime_t > ago(1d) 
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend
        NetworkCustomAnalyticsExists = (maxv > 1),
        NetworkSummary_Protocol_Exists = false
    );

Union isfuzzy=true (4 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: datatable(, datatable(, datatable(exists:, NetworkSummary_Protocol_CL

Leg 1: datatable(

(datatable(
    exists: int,
    NetworkSummary_Protocol_Exists: bool,
    NetworkCustomAnalyticsExists: bool
)[
    1, false, false
]
        | where toscalar(eps) > 500
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
        )
        | join (
            _Im_NetworkSession(starttime=todatetime(ago(3d)), endtime=now())
            | where TimeGenerated > maxSummarizedTime
            | summarize Count=count()
                by
                NetworkProtocol,
                DstPortNumber,
                DstAppName,
                NetworkDirection,
                DvcAction,
                bin(TimeGenerated, 10m)
            | extend
                EventTime = TimeGenerated,
                Count = toint(Count),
                DstPortNumber = toint(DstPortNumber),
                exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*

Leg 2: datatable(

(datatable(
    exists: int,
    NetworkSummary_Protocol_Exists: bool,
    NetworkCustomAnalyticsExists: bool
)[
    1, false, false
]
        | where toscalar(eps) <= 500
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
        )
        | join (
            _Im_NetworkSession(starttime=todatetime(ago(4d)), endtime=now())
            | where TimeGenerated > maxSummarizedTime
            | summarize Count=count()
                by
                NetworkProtocol,
                DstPortNumber,
                DstAppName,
                NetworkDirection,
                DvcAction,
                bin(TimeGenerated, 10m)
            | extend
                EventTime = TimeGenerated,
                Count = toint(Count),
                DstPortNumber = toint(DstPortNumber),
                exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*

Leg 3: datatable(exists:

(datatable(exists: int, NetworkSummary_Protocol_Exists: bool)[1, false]
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        )
        | join (
            NetworkCustomAnalytics_protocol_CL
            | where EventTime_t > ago(lookback)
            | project
                NetworkProtocol=NetworkProtocol_s,
                DstPortNumber=DstPortNumber_d,
                DstAppName=DstAppName_s,
                NetworkDirection=NetworkDirection_s,
                DvcAction=DvcAction_s,
                Count=count__d,
                EventTime=EventTime_t,
                TimeGenerated,
                Type
            | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber), exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*

Leg 4: NetworkSummary_Protocol_CL

NetworkSummary_Protocol_CL
        | where EventTime > ago(lookback)
        | project
            NetworkProtocol,
            DstPortNumber,
            DstAppName,
            NetworkDirection,
            DvcAction,
            Count=count_,
            EventTime,
            TimeGenerated,
            Type
        | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)

Applied to the Combined Result

| project-away exists*, maxv*, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists* | where isnotempty(DstPortNumber) | make-series Total=sum(Count) on EventTime from ago(lookback) to now() step 1d by DstPortNumber, NetworkProtocol, NetworkDirection, DvcAction | extend (anomalies, score, baseline) = series_decompose_anomalies(Total, AnomalyThreshold, -1, 'linefit') | mv-expand anomalies, score, baseline, EventTime, Total | extend
    anomalies = toint(anomalies),
    score = toint(score),
    baseline = toint(baseline),
    EventTime = todatetime(EventTime),
    Total = tolong(Total) | where EventTime >= ago(1d) | extend DstPortNumber = tostring(DstPortNumber) | where score >= 2 * AnomalyThreshold and baseline > 0 | join kind=inner ['mapping'] where Ports == DstPortNumber | where (Protocol == "*" or Protocol has NetworkProtocol)
    and (Direction == "*" or Direction has NetworkDirection)
    and (Action == "*" or Action has DvcAction) | project
    Name,
    Description,
    NetworkProtocol,
    DstPortNumber,
    NetworkDirection,
    DvcAction,
    Severity,
    Tactic | summarize
    NetworkProtocols=make_set_if(NetworkProtocol, isnotempty(NetworkProtocol), 20), 
    NetworkDirections=make_set_if(NetworkDirection, isnotempty(NetworkDirection), 5), 
    DvcActions=make_set_if(DvcAction, isnotempty(DvcAction), 10)
    by Name, Severity, Tactic, DstPortNumber, Description

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Descriptionsummarize
DstPortNumbersummarize
DvcActionssummarize
Namesummarize
NetworkDirectionssummarize
NetworkProtocolssummarize
Severitysummarize
Tacticsummarize

Failover Plan Failed

#
Status
available
Severity
low
Time window
3h
Source
github.com/Azure/Azure-Sentinel

Detects when a failover plan fails. This might indicate disaster recovery activity or issues with primary systems.

Rule Body

id: 8f339b13-02f0-400f-be18-491ec0ba71f1
name: Failover Plan Failed
description: Detects when a failover plan fails. This might indicate disaster recovery
  activity or issues with primary systems.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 26110\n| extend FailoverPlanName\
  \ =  extract(\"FailoverPlanName=\\\"(?<FailoverPlanName>[^\\\"]*)\\\"\", 1, SyslogMessage)\n\
  | project\n    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource\
  \ = original_host,\n    EventId = instanceId,\n    UserName = user,\n    [\"Failover\
  \ Plan Name\"] = FailoverPlanName,\n    MessageDetails = Description,\n    Severity\
  \ = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 26110

Stage 3: extend

| extend FailoverPlanName =  extract("FailoverPlanName=\"(?<FailoverPlanName>[^\"]*)\"", 1, SyslogMessage)

Stage 4: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    UserName = user,
    ["Failover Plan Name"] = FailoverPlanName,
    MessageDetails = Description,
    Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
Failover Plan Nameproject
MessageDetailsproject
Severityproject
UserNameproject

McAfee ePO - Threat was not blocked

#
Status
available
Severity
high
Time window
15m
Source
github.com/Azure/Azure-Sentinel

'Detects when a threat was not blocked on a host.'

MITRE ATT&CK Coverage

Rule Body

id: 6d70a26a-c119-45b7-b4c6-44ac4fd1bcb7
name: McAfee ePO - Threat was not blocked
description: |
  'Detects when a threat was not blocked on a host.'
severity: High
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - PrivilegeEscalation
  - DefenseEvasion
relevantTechniques:
  - T1562
  - T1070
  - T1068
  - T1189
  - T1195
query: |
  McAfeeEPOEvent
  | where ThreatActionTaken in~ ('none', 'IDS_ACTION_WOULD_BLOCK')
  | extend IPCustomEntity = DvcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

McAfeeEPOEvent

Stage 2: where

| where ThreatActionTaken in~ ('none', 'IDS_ACTION_WOULD_BLOCK')

Stage 3: extend

| extend IPCustomEntity = DvcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ThreatActionTakenin
  • IDS_ACTION_WOULD_BLOCK
  • none
field:"ThreatActionTaken" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

New Agent Added to Pool by New User or Added to a New OS Type

#
Status
available
Severity
medium
Time window
14d
Group by
ActorUPN, AgentPoolName, OsDescription
Source
github.com/Azure/Azure-Sentinel

'As seen in attacks such as SolarWinds attackers can look to subvert a build process by controlling build servers. Azure DevOps uses agent pools to execute pipeline tasks. An attacker could insert compromised agents that they control into the pools in order to execute malicious code. This query looks for users adding agents to pools they have not added agents to before, or adding agents to a pool of an OS that has not been added to that pool before. This detection has potential for false positives so has a configurable allow list to allow for certain users to be excluded from the logic.'

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

id: 4ce177b3-56b1-4f0e-b83e-27eed4cb0b16
name: New Agent Added to Pool by New User or Added to a New OS Type
description: |
  'As seen in attacks such as SolarWinds attackers can look to subvert a build process by controlling build servers. Azure DevOps uses agent pools to execute pipeline tasks. 
  An attacker could insert compromised agents that they control into the pools in order to execute malicious code. This query looks for users adding agents to pools they have not added agents to before, or adding agents to a pool of an OS that has not been added to that pool before. This detection has potential for false positives so has a configurable allow list to allow for certain users to be excluded from the logic.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1053
query: |
  let lookback = 14d;
  let timeframe = 1d;
  // exclude allowed users from query such as the ADO service
  let allowed_users = dynamic(["Azure DevOps Service"]);
  union
  // Look for agents being added to a pool of a OS type not seen with that pool before
  (ADOAuditLogs
  | where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
  | where OperationName =~ "Library.AgentAdded"
  | where ActorUPN !in (allowed_users)
  | extend AgentPoolName = tostring(Data.AgentPoolName)
  | extend OsDescription = tostring(Data.OsDescription)
  | where isnotempty(OsDescription)
  | extend OsDescription = tostring(split(OsDescription, "#", 0)[0])
  | project AgentPoolName, OsDescription
  | join kind=rightanti (ADOAuditLogs
  | where TimeGenerated > ago(timeframe)
  | where OperationName == "Library.AgentAdded"
  | extend AgentPoolName = tostring(Data.AgentPoolName)
  | extend OsDescription = tostring(Data.OsDescription)
  | where isnotempty(OsDescription)
  | extend OsDescription = tostring(split(OsDescription, "#", 0)[0])) on AgentPoolName, OsDescription),
  // Look for users addeing agents to a pool that they have not added agents to before.
  (AzureDevOpsAuditing
  | where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
  | extend AgentPoolName = tostring(Data.AgentPoolName)
  | where ActorUPN !in (allowed_users)
  | project AgentPoolName, ActorUPN
  | join kind=rightanti (AzureDevOpsAuditing
  | where TimeGenerated > ago(timeframe)
  | where OperationName == "Library.AgentAdded"
  | where ActorUPN !in (allowed_users)
  | extend AgentPoolName = tostring(Data.AgentPoolName)
  ) on AgentPoolName, ActorUPN)
  | extend AgentName = tostring(Data.AgentName)
  | extend OsDescription = tostring(Data.OsDescription)
  | extend SystemDetails = Data.SystemCapabilities
  | project-reorder TimeGenerated, OperationName, ScopeDisplayName, AgentPoolName, AgentName, ActorUPN, IpAddress, UserAgent, OsDescription, SystemDetails, Data
  | extend timestamp = TimeGenerated
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 14d;
let timeframe = 1d;
let allowed_users = dynamic(["Azure DevOps Service"]);

Union (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: ADOAuditLogs, AzureDevOpsAuditing

Leg 1: ADOAuditLogs

ADOAuditLogs
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where OperationName =~ "Library.AgentAdded"
| where ActorUPN !in (allowed_users)
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend OsDescription = tostring(Data.OsDescription)
| where isnotempty(OsDescription)
| extend OsDescription = tostring(split(OsDescription, "#", 0)[0])
| project AgentPoolName, OsDescription
| join kind=rightanti (ADOAuditLogs
| where TimeGenerated > ago(timeframe)
| where OperationName == "Library.AgentAdded"
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend OsDescription = tostring(Data.OsDescription)
| where isnotempty(OsDescription)
| extend OsDescription = tostring(split(OsDescription, "#", 0)[0])) on AgentPoolName, OsDescription

Leg 2: AzureDevOpsAuditing

AzureDevOpsAuditing
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| extend AgentPoolName = tostring(Data.AgentPoolName)
| where ActorUPN !in (allowed_users)
| project AgentPoolName, ActorUPN
| join kind=rightanti (AzureDevOpsAuditing
| where TimeGenerated > ago(timeframe)
| where OperationName == "Library.AgentAdded"
| where ActorUPN !in (allowed_users)
| extend AgentPoolName = tostring(Data.AgentPoolName)
) on AgentPoolName, ActorUPN

Applied to the Combined Result

| extend AgentName = tostring(Data.AgentName)
| extend OsDescription = tostring(Data.OsDescription)
| extend SystemDetails = Data.SystemCapabilities
| project-reorder TimeGenerated, OperationName, ScopeDisplayName, AgentPoolName, AgentName, ActorUPN, IpAddress, UserAgent, OsDescription, SystemDetails, Data
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNameeq
  • Library.AgentAdded
  • Library.AgentAdded
field:"OperationName" kind:eq
OsDescriptionis_not_null
  • (no value, null check)
field:"OsDescription" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ActorUPNeqallowed_usersexcludes:ActorUPN field:"ActorUPN" value:"allowed_users"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUPNproject
AgentPoolNameproject
AgentNameextend
OsDescriptionextend
SystemDetailsextend
timestampextend
AccountNameextend
AccountUPNSuffixextend

Pathlock TDnR - SU24 Table USOBT_C Changes

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

Detects changes to the SU24 authorization check table USOBT_C in SAP, forwarded by Pathlock Threat Detection and Response. USOBT_C defines default authorization field values for transactions; unauthorized changes may allow privilege escalation by modifying authorization check behavior.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation
Defense Impairment

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c18
name: Pathlock TDnR - SU24 Table USOBT_C Changes
kind: Scheduled
description: >-
  Detects changes to the SU24 authorization check table USOBT_C in SAP, forwarded by Pathlock Threat
  Detection and Response. USOBT_C defines default authorization field values for transactions;
  unauthorized changes may allow privilege escalation by modifying authorization check behavior.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
relevantTechniques:
  - T1562
  - T1548
query: |
  Pathlock_TDnR_CL
  | where DataSource == "CHANGEDOC_USOBT_C"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "CHANGEDOC_USOBT_C"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Ping Federate - Unexpected authentication URL.

#
Status
available
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Detects unexpected authentication URL.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 9578ef7f-cbb4-4e9a-bd26-37c15c53b413
name: Ping Federate - Unexpected authentication URL.
description: |
  'Detects unexpected authentication URL.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  let known_domains = 
  PingFederateEvent
  | where TimeGenerated between (ago(14d) .. (1d))
  | where isnotempty(DeviceCustomString1)
  | extend url_parsed = parse_url(DeviceCustomString1)
  | extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))
  | summarize makeset(url_domain);
  PingFederateEvent
  | where isnotempty(DeviceCustomString1)
  | extend url_parsed = parse_url(DeviceCustomString1)
  | extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))
  | where url_domain !in (known_domains)
  | extend AccountCustomEntity = DstUserName
  | extend IpCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Let Binding: known_domains used in Stage 5

let known_domains = PingFederateEvent
| where TimeGenerated between (ago(14d) .. (1d))
| where isnotempty(DeviceCustomString1)
| extend url_parsed = parse_url(DeviceCustomString1)
| extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))
| summarize makeset(url_domain);

Stage 1: source

PingFederateEvent

Stage 2: where

| where isnotempty(DeviceCustomString1)

Stage 3: extend

| extend url_parsed = parse_url(DeviceCustomString1)

Stage 4: extend

| extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))

Stage 5: where

| where url_domain !in (known_domains)

Stage 6: extend

| extend AccountCustomEntity = DstUserName

Stage 7: extend

| extend IpCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DeviceCustomString1is_not_null
  • (no value, null check)
field:"DeviceCustomString1" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
url_domaineqknown_domainsexcludes:url_domain field:"url_domain" value:"known_domains"

Output Fields

These fields are emitted when the rule matches.

FieldSource
url_parsedextend
url_domainextend
AccountCustomEntityextend
IpCustomEntityextend

Service principal not using client credentials

#
Status
available
Severity
high
Time window
6h
Source
github.com/Azure/Azure-Sentinel

'This query searches for an service principal is not using a client certificate or secret is not secure. It is recommended that you review your needs and use an Authentication method for sign-in.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: D308318A-B298-4E57-82BD-74AE33C4A539
name: Service principal not using client credentials
description: |
  'This query searches for an service principal is not using a client certificate or secret is not secure. 
   It is recommended that you review your needs and use an Authentication method for sign-in.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SenservaPro
    dataTypes:
      - SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  SenservaPro_CL
  | where ControlName_s == 'ServicePrincipalNotUsingClientCredentials'
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: ControlName_s
      - identifier: AadTenantId
        columnName: TenantId
      - identifier: DisplayName
        columnName: TenantDisplayName_s 
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: DistinguishedName
        columnName: Group_s
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: SourceSystem 
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

SenservaPro_CL

Stage 2: where

| where ControlName_s == 'ServicePrincipalNotUsingClientCredentials'

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ControlName_seq
  • ServicePrincipalNotUsingClientCredentials
field:"ControlName_s" kind:eq value:"ServicePrincipalNotUsingClientCredentials"

Suspicious AWS console logins by credential access alerts

#
Severity
medium
Time window
1m
Group by
$left.EntityIp, $right.SourceIpAddress, AccountObjectId, AlertName, AlertSeverity, AlertTime, AlertTimeGenerated, EntityIp, ProductName, ProviderName, Tactics, Techniques
Source
github.com/Azure/Azure-Sentinel

This query aims to detect instances of successful AWS console logins that align with high-severity credential access or Initial Access alerts generated by Defender Products. Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. The login is considered relevant if it originates from an IP address associated with potential attackers.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: b51fe620-62ad-4ed2-9d40-5c97c0a8231f
name: Suspicious AWS console logins by credential access alerts
description: |
  'This query aims to detect instances of successful AWS console logins that align with high-severity credential access or Initial Access alerts generated by Defender Products.
   Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. The login is considered relevant if it originates from an IP address associated with potential attackers.'
severity: Medium
requiredDataConnectors:
  - connectorId: OfficeATP
    dataTypes:
      - SecurityAlert
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: MicrosoftDefenderAdvancedThreatProtection
    dataTypes:
      - SecurityAlert
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1078
query: |
  SecurityAlert 
  // Filtering alerts based on Microsoft product names
    | where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security","Azure Active Directory Identity Protection", "Microsoft Defender ATP")
  // Narrowing down alerts to specific tactics
    | where Tactics in("CredentialAccess", "InitialAccess")
  // Focusing on high-severity alerts
    | where AlertSeverity == "High"
  // Parsing and extending the 'Entities' column as JSON objects
    | extend Entities = parse_json(Entities) 
  // Exploring IP entities within the alert entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'ip' 
        | extend EntityIp = tostring(Entity.Address) 
        ) 
  // Exploring account entities within the alert entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'account' 
        | extend AccountObjectId = tostring(Entity.AadUserId)
        )
  // Filtering out alerts with missing IP or account information
    | where isnotempty(EntityIp) and isnotempty(AccountObjectId)
  // Summarizing relevant fields for further analysis
    | summarize 
        by 
        AlertName,
        ProductName,
        ProviderName,
        AlertSeverity,
        EntityIp,
        Tactics,
        Techniques,
        AlertTime= bin(TimeGenerated, 1min),
        AccountObjectId,
        AlertTimeGenerated=TimeGenerated
  // Joining with IdentityInfo to obtain additional account details
    | join kind=inner (
        IdentityInfo
        | where TimeGenerated >= ago(1d)
        | distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
        )
        on AccountObjectId 
        |extend Name = tostring(split(AccountUPN,'@')[0]), UPNSuffix =tostring(split(AccountUPN,'@')[1])
  // Joining with AWSCloudTrail data to correlate AWS console logins
    | join kind=inner (
        AWSCloudTrail
        | where EventName == "ConsoleLogin"
        | extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
        | extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)  
        | where ActionType == "Success"
        | extend AWSTime= bin(TimeGenerated, 1min)
        | project
            EventName,
            EventSource,
            EventTypeName,
            RecipientAccountId,
            ResponseElements,
            SessionMfaAuthenticated,
            SourceIpAddress,
            TimeGenerated,
            UserAgent,
            UserIdentityArn,
            UserIdentityType,
            CTUPN,
            AWSTime,
            UserIdentityUserName
        )
        on $left.EntityIp == $right.SourceIpAddress 
  // Filtering login event after the Alert generation time
    | where AlertTimeGenerated >= AWSTime
  // Calculating the time difference between alert generation and AWS login
    | extend timediff = datetime_diff('minute', AlertTimeGenerated, TimeGenerated) 
  // Filtering alerts with a time difference of up to 60 minutes
    | where timediff between ((-60)..(60))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUPN
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  AWSUSerUPN: CTUPN
  AzureUserUPN: AccountUPN
  ComonIp: SourceIpAddress
  UserAgent: UserAgent
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security","Azure Active Directory Identity Protection", "Microsoft Defender ATP")

Stage 3: where

| where Tactics in("CredentialAccess", "InitialAccess")

Stage 4: where

| where AlertSeverity == "High"

Stage 5: extend

| extend Entities = parse_json(Entities)

Stage 6: kusto:mv-apply

| mv-apply Entity = Entities on 
      ( 
      where Entity.Type == 'ip' 
      | extend EntityIp = tostring(Entity.Address) 
      )

Stage 7: kusto:mv-apply

| mv-apply Entity = Entities on 
      ( 
      where Entity.Type == 'account' 
      | extend AccountObjectId = tostring(Entity.AadUserId)
      )

Stage 8: where

| where isnotempty(EntityIp) and isnotempty(AccountObjectId)

Stage 9: summarize

| summarize 
      by 
      AlertName,
      ProductName,
      ProviderName,
      AlertSeverity,
      EntityIp,
      Tactics,
      Techniques,
      AlertTime= bin(TimeGenerated, 1min),
      AccountObjectId,
      AlertTimeGenerated=TimeGenerated

Stage 10: join

| join kind=inner (
      IdentityInfo
      | where TimeGenerated >= ago(1d)
      | distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
      )
      on AccountObjectId

Stage 11: extend

extend Name, UPNSuffix

Stage 12: join

| join kind=inner (
      AWSCloudTrail
      | where EventName == "ConsoleLogin"
      | extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
      | extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)  
      | where ActionType == "Success"
      | extend AWSTime= bin(TimeGenerated, 1min)
      | project
          EventName,
          EventSource,
          EventTypeName,
          RecipientAccountId,
          ResponseElements,
          SessionMfaAuthenticated,
          SourceIpAddress,
          TimeGenerated,
          UserAgent,
          UserIdentityArn,
          UserIdentityType,
          CTUPN,
          AWSTime,
          UserIdentityUserName
      )
      on $left.EntityIp == $right.SourceIpAddress

Stage 13: where

where AlertTimeGenerated >= AWSTime

Stage 14: extend

extend timediff

Stage 15: where

where /* macro: (timediff between ((- 60) .. 60)) */

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccountObjectIdis_not_null
  • (no value, null check)
field:"AccountObjectId" kind:is_not_null
ActionTypeeq
  • Success
field:"ActionType" kind:eq value:"Success"
AlertSeverityeq
  • High corpus 2 (kusto 2)
field:"AlertSeverity" kind:eq value:"High"
Entity.Typeeq
  • account transforms: kusto_dynamic_path
  • ip transforms: kusto_dynamic_path
field:"Entity.Type" kind:eq
EntityIpis_not_null
  • (no value, null check)
field:"EntityIp" kind:is_not_null
EventNameeq
  • ConsoleLogin
field:"EventName" kind:eq value:"ConsoleLogin"
ProductNamein
  • Azure Active Directory corpus 2 (kusto 2)
  • Azure Active Directory Identity Protection corpus 3 (kusto 3)
  • Microsoft 365 Defender corpus 2 (kusto 2)
  • Microsoft Cloud App Security corpus 3 (kusto 3)
  • Microsoft Defender ATP corpus 2 (kusto 2)
  • Microsoft Defender Advanced Threat Protection corpus 2 (kusto 2)
field:"ProductName" kind:in
Tacticsin
  • CredentialAccess
  • InitialAccess
field:"Tactics" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountObjectIdsummarize
AlertNamesummarize
AlertSeveritysummarize
AlertTimesummarize
AlertTimeGeneratedsummarize
EntityIpsummarize
ProductNamesummarize
ProviderNamesummarize
Tacticssummarize
Techniquessummarize
Nameextend
UPNSuffixextend
timediffextend

Suspicious Login from deleted guest account

#
Severity
medium
Time window
1d
Group by
$left.ParsedDeletedUserPrincipalName, $right.ParsedUserPrincipalName, UserPrincipalName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query will detect logins from guest account which was recently deleted. For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: defe4855-0d33-4362-9557-009237623976
name: Suspicious Login from deleted guest account
description: |
  ' This query will detect logins from guest account which was recently deleted. 
  For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078.004
tags:
  - GuestorExternalIdentities
query: |
  let query_frequency = 1h;
  let query_period = 1d;
  AuditLogs
  | where TimeGenerated > ago(query_frequency)
  | where Category =~ "UserManagement" and OperationName =~ "Delete user"
  | mv-expand TargetResource = TargetResources
  | where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"
  | extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))
  | extend
      Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
      InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
      Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
  | project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId
  | join kind=inner (
      SigninLogs
      | where TimeGenerated > ago(query_period)
      | where ResultType == 0
      | summarize take_any(*) by UserPrincipalName
      | extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
      | project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
      ) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName
  | where SigninLogs_TimeGenerated > Delete_TimeGenerated
  | project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName
  | extend
      AccountName = tostring(split(UserPrincipalName, "@")[0]),
      AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let query_frequency = 1h;
let query_period = 1d;

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(query_frequency)

Stage 3: where

| where Category =~ "UserManagement" and OperationName =~ "Delete user"

Stage 4: mv-expand

| mv-expand TargetResource = TargetResources

Stage 5: where

| where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"

Stage 6: extend

| extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))

Stage 7: extend

| extend
    Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
    InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
    Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])

Stage 8: project

| project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId

Stage 9: join

| join kind=inner (
    SigninLogs
    | where TimeGenerated > ago(query_period)
    | where ResultType == 0
    | summarize take_any(*) by UserPrincipalName
    | extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
    | project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
    ) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName

Stage 10: where

where SigninLogs_TimeGenerated > Delete_TimeGenerated

Stage 11: project-away

project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName

Stage 12: extend

extend AccountName, AccountUPNSuffix

Stage 13: summarize aggregation inside the join branch

summarize by UserPrincipalName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • UserManagement corpus 7 (sigma 4, kusto 3)
field:"Category" kind:eq value:"UserManagement"
OperationNameeq
  • Delete user corpus 3 (kusto 3)
field:"OperationName" kind:eq value:"Delete user"
ResultTypeeq
  • 0
field:"ResultType" kind:eq value:"0"
TargetResource["type"]eq
  • User transforms: kusto_dynamic_path
TargetResource["userPrincipalName"]match
  • #EXT# transforms: term, kusto_dynamic_path

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserPrincipalNamesummarize

Tenant Quota Changed

#
Status
available
Severity
informational
Time window
1d
Source
github.com/Azure/Azure-Sentinel

Detects when a tenant quota is updated.

Rule Body

id: da25b390-b97a-4ea4-abe3-5bbd7f90642c
name: Tenant Quota Changed
description: Detects when a tenant quota is updated.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 24160\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\n  MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 24160

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
  MessageDetails = Description,
   Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

TI Map IP Entity to DeviceNetworkEvents

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.RemoteIP, DeviceName, IndicatorId, TI_ipEntity
Source
github.com/Azure/Azure-Sentinel

Identifies a match in DeviceNetworkEvents Event data from any IP Indicator from TI.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: b2df4979-d34a-48b3-a7d9-f473a4bf8058
name: TI Map IP Entity to DeviceNetworkEvents
description: |
  'Identifies a match in DeviceNetworkEvents Event data from any IP Indicator from TI.'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let DeviceNetworkEvents_ = DeviceNetworkEvents
  | where isnotempty(RemoteIP)
  | where TimeGenerated > ago(dt_lookBack)
  | where ActionType !has "ConnectionFailed"
  | extend isPrivate = ipv4_is_private(RemoteIP)
  | where isPrivate != true;
  let IPs = DeviceNetworkEvents_ | distinct RemoteIP | summarize make_list(RemoteIP);
  ThreatIntelligenceIndicator
  | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend TI_ipEntity = coalesce(NetworkIP, EmailSourceIpAddress, NetworkDestinationIP, NetworkSourceIP)
  | where TI_ipEntity in (IPs)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  | join kind=innerunique (DeviceNetworkEvents_) on $left.TI_ipEntity == $right.RemoteIP
  | summarize TimeGenerated = arg_max(TimeGenerated, *) by IndicatorId, TI_ipEntity, DeviceName
  // DeviceName, TI_ipEntity, RemoteUrl, InitiatingProcessAccountUpn
  | extend timestamp = TimeGenerated, Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: TI_ipEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RemoteUrl
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

Let Binding: DeviceNetworkEvents_ used in Stages 1, 11

let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteIP)
| where TimeGenerated > ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend isPrivate = ipv4_is_private(RemoteIP)
| where isPrivate != true;

Let Binding: IPs used in Stages 2, 7

let IPs = DeviceNetworkEvents_ | distinct RemoteIP | summarize make_list(RemoteIP);

Stage 1: source

let DeviceNetworkEvents_

Stage 2: source

let IPs

Stage 3: source

ThreatIntelligenceIndicator

Stage 4: where

where (isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkIP) or isnotempty(NetworkSourceIP))

Stage 5: where

where TimeGenerated >= ago(1209600s)

Stage 6: extend

extend TI_ipEntity

Stage 7: where

where TI_ipEntity == "IPs"

Stage 8: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 9: where

where Active == true

Stage 10: where

where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")

Stage 11: join

join kind=innerunique (DeviceNetworkEvents_) on $left.TI_ipEntity, $right.RemoteIP

Stage 12: summarize

summarize TimeGenerated by IndicatorId, TI_ipEntity, DeviceName

Stage 13: extend

extend Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"
ActionTypematchConnectionFailedexcludes:ActionType field:"ActionType" value:"ConnectionFailed"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceNamesummarize
IndicatorIdsummarize
TI_ipEntitysummarize
TimeGeneratedsummarize
Nameextend
UPNSuffixextend
timestampextend

TI Map IP entity to Workday(ASimAuditEventLogs)

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.DvcIpAddr, Id, ObservableValue
Source
github.com/Azure/Azure-Sentinel

Detects a match in Workday activity from any IP Indicator of Compromise (IOC) provided by Threat Intelligence (TI).

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 92e8e945-6e99-4e4b-bef8-468b4c19fc3a
name: TI Map IP entity to Workday(ASimAuditEventLogs)
description: |
  Detects a match in Workday activity from any IP Indicator of Compromise (IOC) provided by Threat Intelligence (TI).
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: Workday
    dataTypes:
      - Workday
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dtLookBack = 1h; // Define the lookback period for audit events
  let ioc_lookBack = 14d; // Define the lookback period for threat intelligence indicators
  ThreatIntelIndicators 
  | where TimeGenerated >= ago(ioc_lookBack) // Filter threat intelligence indicators within the lookback period
  //extract key part of kv pair
       | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
       | where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
       | extend NetworkSourceIP = toupper(ObservableValue)
       | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
       | where isnotempty(NetworkSourceIP)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue // Get the latest indicator time for each IndicatorId
    | extend TI_ipEntity = coalesce(NetworkSourceIP, NetworkSourceIP, NetworkSourceIP, NetworkSourceIP) // Combine IP fields into a single entity
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil)) // Filter for active indicators that have not expired
    | project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
    | join kind=inner (
        ASimAuditEventLogs
        | where EventVendor == "Workday" // Filter for Workday events
        | where TimeGenerated >= ago(dtLookBack) // Filter events within the lookback period
        | where isnotempty(DvcIpAddr) // Filter for events with a device IP address
        | extend WD_TimeGenerated = EventStartTime // Rename the event start time column
        | project WD_TimeGenerated, ActorUsername, DvcIpAddr, Operation, Object // Select relevant columns
        )
        on $left.TI_ipEntity == $right.DvcIpAddr // Join on the IP entity
        | extend Description = tostring(parse_json(Data).description)
        | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | project
        LatestIndicatorTime,
        Description,
        ActivityGroupNames,
        Id,
        ValidUntil,
        Confidence,
        WD_TimeGenerated,
        ActorUsername,
        DvcIpAddr,
        Operation,
        Object // Select relevant columns after the join
    | extend
        timestamp = WD_TimeGenerated,
        Name = tostring(split(ActorUsername, '@', 0)),
        UPNSuffix = tostring(split(ActorUsername, '@', 1)) // Add additional fields for timestamp, name, and UPN suffix   
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUsername
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DvcIpAddr
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let dtLookBack = 1h;
let ioc_lookBack = 14d;

Stage 1: source

ThreatIntelIndicators

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 4: where

| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")

Stage 5: extend

| extend NetworkSourceIP = toupper(ObservableValue)

Stage 6: extend

| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 7: where

| where isnotempty(NetworkSourceIP)

Stage 8: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 9: extend

| extend TI_ipEntity = coalesce(NetworkSourceIP, NetworkSourceIP, NetworkSourceIP, NetworkSourceIP)

Stage 10: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 11: project-reorder

| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity

Stage 12: join

| join kind=inner (
      ASimAuditEventLogs
      | where EventVendor == "Workday"
      | where TimeGenerated >= ago(dtLookBack)
      | where isnotempty(DvcIpAddr)
      | extend WD_TimeGenerated = EventStartTime
      | project WD_TimeGenerated, ActorUsername, DvcIpAddr, Operation, Object
      )
      on $left.TI_ipEntity == $right.DvcIpAddr

Stage 13: extend

extend Description

Stage 14: extend

extend ActivityGroupNames

Stage 15: project

project ActivityGroupNames, ActorUsername, Confidence, Description, DvcIpAddr, Id, LatestIndicatorTime, Object, Operation, ValidUntil, WD_TimeGenerated

Stage 16: extend

extend Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ActorUsernameproject
Confidenceproject
Descriptionproject
DvcIpAddrproject
Idproject
LatestIndicatorTimeproject
Objectproject
Operationproject
ValidUntilproject
WD_TimeGeneratedproject
Nameextend
UPNSuffixextend
timestampextend

UniFi Site Manager: Pending firmware updates outstanding for 7d+

#
Status
available
Severity
low
Time window
7d
Group by
SiteId, SiteName
Source
github.com/Azure/Azure-Sentinel

Identifies sites with pending firmware updates outstanding continuously for the last 7 days, indicating patch debt that should be scheduled.

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: d943d401-861e-7186-d42c-c505fbf7c619
name: "UniFi Site Manager: Pending firmware updates outstanding for 7d+"
description: |
  Identifies sites with pending firmware updates outstanding continuously for the last 7 days, indicating patch debt that should be scheduled.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Sites_CL
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
relevantTechniques:
  - T1595
subTechniques: []
query: |
  Unifi_SiteManager_Sites_CL
        | where TimeGenerated > ago(7d)
  | extend pendingUpdates = PendingUpdateDevices
        | summarize MinPending = min(pendingUpdates), MaxPending = max(pendingUpdates), arg_max(TimeGenerated, pendingUpdates) by SiteId, SiteName
        | where MinPending > 0
        | extend Activity = strcat('Pending updates sustained - current ', pendingUpdates, ' device(s)')
        | project TimeGenerated, SiteId, SiteName, Activity, PendingDevices = pendingUpdates
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteId
      - identifier: DnsDomain
        columnName: SiteName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

Unifi_SiteManager_Sites_CL

Stage 2: where

| where TimeGenerated > ago(7d)

Stage 3: extend

| extend pendingUpdates = PendingUpdateDevices

Stage 4: summarize

| summarize MinPending = min(pendingUpdates), MaxPending = max(pendingUpdates), arg_max(TimeGenerated, pendingUpdates) by SiteId, SiteName

Stage 5: where

| where MinPending > 0

Stage 6: extend

| extend Activity = strcat('Pending updates sustained - current ', pendingUpdates, ' device(s)')

Stage 7: project

| project TimeGenerated, SiteId, SiteName, Activity, PendingDevices = pendingUpdates

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MinPendinggt
  • 0 transforms: number
field:"MinPending" kind:gt value:"0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
PendingDevicesproject
SiteIdproject
SiteNameproject
TimeGeneratedproject

Zero Networks Segment - New API Token created

#
Status
available
Severity
low
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'Detects when a api token has been created.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 603a6b18-b54a-43b7-bb61-d2b0b47d224a
name: Zero Networks Segment - New API Token created
description: |
  'Detects when a api token has been created.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: ZeroNetworksSegmentAuditFunction
    dataTypes:
      - ZNSegmentAudit_CL
  - connectorId: ZeroNetworksSegmentAuditNativePoller
    dataTypes:
      - ZNSegmentAuditNativePoller_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1528
query: |
  ZNSegmentAudit
  | where AuditTypeId == 25
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: PerformedByName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

ZNSegmentAudit

Stage 2: where

| where AuditTypeId == 25

Indicators

These rows show field, operator, and value matches.

Back to all Kusto rules