Detection rules › Kusto

Kusto rules: 0o

1Password - User added to privileged group

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

This will alert when a user is added to a privileged group which has been implemented by an actor that was not the target user account. Once the analytics rule is triggered it will group all related future alerts for upto 30 minutes when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

PlatformRecord / event type
1Passwordgm-join: Join Group

Rule Body

id: 849ea271-cd9c-4afe-a13b-ddbbac5fc6d3
name: 1Password - User added to privileged group
version: 1.0.0
kind: Scheduled
description: |-
  This will alert when a user is added to a privileged group which has been implemented by an actor that was not the target user account. Once the analytics rule is triggered it will group all related future alerts for upto 30 minutes when all related entities are the same.

  Ref: https://1password.com/
  Ref: https://github.com/securehats/
severity: Medium
requiredDataConnectors:
  - connectorId: 1Password
    dataTypes:
      - OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098
query: |-
  let watchlist =
      _GetWatchlist("PG1PW")
      | project SearchKey
  ;
  let groups = dynamic([""]);
  OnePasswordEventLogs_CL
  | where log_source == "auditevents"
  | where action == "join"
  | where object_type == "gm"
  | where tostring(actor_details.email) != tostring(aux_details.email)
  // Enable the line below when using the "Privileged Groups - 1PW" watchlist
  | where object_uuid in (watchlist)
  // Enable the line below when using the dynamic groups list within the analytics rule itself
  // | where object_uuid in (groups)
  | extend
      TargetUsername = aux_details.email
      , ActorUsername = actor_details.email
      , SrcIpAddr = session.ip
      , GroupRole = case(
          aux_info == "R", "Group member"
          , aux_info == "A", "Group manager"
          , aux_info
      )
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 30m
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorUsername
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: TargetUsername
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr

Stages and Predicates

Parameters

let groups = dynamic([""]);

Let Binding: watchlist used in Stage 6

let watchlist = _GetWatchlist("PG1PW")
    | project SearchKey;

Stage 1: source

OnePasswordEventLogs_CL

Stage 2: where

| where log_source == "auditevents"

Stage 3: where

| where action == "join"

Stage 4: where

| where object_type == "gm"

Stage 5: where

| where tostring(actor_details.email) != tostring(aux_details.email)

Stage 6: where

| where object_uuid in (watchlist)

Stage 7: extend

| extend
    TargetUsername = aux_details.email
    , ActorUsername = actor_details.email
    , SrcIpAddr = session.ip
    , GroupRole = case(
        aux_info == "R", "Group member"
        , aux_info == "A", "Group manager"
        , aux_info
    )
GroupRole =
ifaux_info == "R""Group member"
elifaux_info == "A""Group manager"
elseaux_info

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUsernameextend
GroupRoleextend
SrcIpAddrextend
TargetUsernameextend

Acronis - Multiple Endpoints Accessing Malicious URLs

#
Severity
medium
Time window
1d
Group by
DeviceName
Source
github.com/Azure/Azure-Sentinel

Multiple endpoints accessing malicious URLs could indicate an ongoing phishing attack, with several employees interacting with those URLs.

MITRE ATT&CK Coverage

Rule Body

id: 1385f0ce-69d9-4abf-8039-52080c8c7017
name: Acronis - Multiple Endpoints Accessing Malicious URLs
description: 'Multiple endpoints accessing malicious URLs could indicate an ongoing phishing attack, with several employees interacting with those URLs.'
severity: Medium
query: |
    CommonSecurityLog
    | where DeviceVendor == "Acronis"
    | where DeviceEventClassID == "MaliciousUrlDetected"
    | summarize MaliciousUrlDetected = count() by DeviceName
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 2
tactics:
    - Execution
relevantTechniques:
    - T1204.001
incidentConfiguration:
    createIncident: true
    groupingConfiguration:
        enabled: true
        reopenClosedIncident: true
        lookbackDuration: P1D
        matchingMethod: AnyAlert
eventGroupingSettings:
    aggregationKind: SingleAlert
customDetails:
  DeviceName: DeviceName
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
version: 1.0.0
kind: Scheduled
requiredDataConnectors: []

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor == "Acronis"

Stage 3: where

| where DeviceEventClassID == "MaliciousUrlDetected"

Stage 4: summarize

| summarize MaliciousUrlDetected = count() by DeviceName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceNamesummarize
MaliciousUrlDetectedsummarize

BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName Privileges

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName Privileges

Rule Body

id: 8bbc3980-8b18-40c5-829e-d282afa52284
name: BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName
  Privileges
description: BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName
  Privileges
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "LargeDefaultGroupsWriteSPN"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "LargeDefaultGroupsWriteSPN"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Findingeq
  • LargeDefaultGroupsWriteSPN
field:"Finding" kind:eq value:"LargeDefaultGroupsWriteSPN"

Cloudflare - WAF Allowed threat

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

'Detects WAF "Allowed" action on threat events.'

MITRE ATT&CK Coverage

Rule Body

id: f53fe2a9-96b5-454c-827e-cf1764a67fb0
name: Cloudflare - WAF Allowed threat
description: |
  'Detects WAF "Allowed" action on threat events.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  Cloudflare
  | where isnotempty(WAFRuleID) or isnotempty(WAFRuleMessage)
  | where WAFAction =~ 'Allow'
  | 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

Stage 1: source

Cloudflare

Stage 2: where

| where isnotempty(WAFRuleID) or isnotempty(WAFRuleMessage)

Stage 3: where

| where WAFAction =~ 'Allow'

Stage 4: extend

| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CompleteUrlextend

SAP BTP - Malware detected in BAS dev space

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

Identifies instances of malware detected using SAP internal malware agent within Business Application Studio dev spaces.

MITRE ATT&CK Coverage

Rule Body

id: 31997e9a-7447-47f3-8208-4f5d7efe497c
kind: Scheduled
name: SAP BTP - Malware detected in BAS dev space
description: Identifies instances of malware detected using SAP internal malware agent
  within Business Application Studio dev spaces.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SAPBTPAuditEvents
    dataTypes:
      - SAPBTPAuditLog_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - ResourceDevelopment
  - Execution
  - Persistence
relevantTechniques:
  - T1584
  - T1072
  - T0873
query: |
  SAPBTPAuditLog_CL
  | where Message has "malware"
  | extend MessageData = parse_json(tostring(Message.data))
  | extend MalwareData = parse_json(tostring(MessageData.message))
  | extend
      ClusterID = tostring(MessageData.clusterID),
      WorkspaceID = tostring(MessageData.wsID),
      DevSpaceId = tostring(MalwareData.dev_space_id),
      User = tostring(MalwareData.user),
      Malware = strcat_array(MalwareData.findings, ", "),
      MessageText = tostring(MalwareData.message)
  | extend
      AccountName = tostring(split(User, '@')[0]),
      UPNSuffix = tostring(split(User, '@')[1])
  | where isnotempty(User) and isnotempty(Malware)
  | project
      UpdatedOn,
      ClusterID,
      WorkspaceID,
      DevSpaceId,
      MessageText,
      User,
      Malware,
      Tenant,
      SpaceId,
      Category,
      CloudApp = "SAP BTP",
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: CloudApp
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: Malware
alertDetailsOverride:
  alertDisplayNameFormat: SAP BTP - Malware detected in Business Apps Studio dev space
  alertDescriptionFormat: 'Malware was found in the following subaccount: {{Tenant}}'
version: 3.0.8

Stages and Predicates

Stage 1: source

SAPBTPAuditLog_CL

Stage 2: where

| where Message has "malware"

Stage 3: extend (4 consecutive steps)

| extend MessageData = parse_json(tostring(Message.data))
| extend MalwareData = parse_json(tostring(MessageData.message))
| extend
    ClusterID = tostring(MessageData.clusterID),
    WorkspaceID = tostring(MessageData.wsID),
    DevSpaceId = tostring(MalwareData.dev_space_id),
    User = tostring(MalwareData.user),
    Malware = strcat_array(MalwareData.findings, ", "),
    MessageText = tostring(MalwareData.message)
| extend
    AccountName = tostring(split(User, '@')[0]),
    UPNSuffix = tostring(split(User, '@')[1])

Stage 4: where

| where isnotempty(User) and isnotempty(Malware)

Stage 5: project

| project
    UpdatedOn,
    ClusterID,
    WorkspaceID,
    DevSpaceId,
    MessageText,
    User,
    Malware,
    Tenant,
    SpaceId,
    Category,
    CloudApp = "SAP BTP",
    AccountName,
    UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Malwareis_not_null
  • (no value, null check)
field:"Malware" kind:is_not_null
Messagematch
  • malware transforms: term
field:"Message" kind:match value:"malware"
Useris_not_null
  • (no value, null check)
field:"user" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
Categoryproject
CloudAppproject
ClusterIDproject
DevSpaceIdproject
Malwareproject
MessageTextproject
SpaceIdproject
Tenantproject
UPNSuffixproject
UpdatedOnproject
Userproject
WorkspaceIDproject

Tenable.ad DCShadow

#
Severity
high
Time window
2h
Source
github.com/Azure/Azure-Sentinel

'Searches for DCShadow attacks.'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 861044f3-6eef-4f79-8609-e3764abb02f4
name: Tenable.ad DCShadow
description: |
    'Searches for DCShadow attacks.'
severity: High
requiredDataConnectors:
  - connectorId: Tenable.ad
    dataTypes:
      - Tenable_ad_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1207
query: |
  // For the query to work properly, make sure you have imported the afad_parser.kql parser into the workspace
  // Retrieve the parser here: https://raw.githubusercontent.com/tenable/Azure-Sentinel/Tenable.ad-connector/Solutions/TenableAD/Parsers/afad_parser.kql
  // Then, create the Kusto Function with alias afad_parser
  afad_parser
    | where MessageType == 2 and Codename == "DCShadow"
    | extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 2 and Codename == "DCShadow"

Stage 3: extend

| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Codenameeq
  • DCShadow corpus 3 (kusto 3)
field:"Codename" kind:eq value:"DCShadow"
MessageTypeeq
  • 2 corpus 21 (kusto 21)
field:"MessageType" kind:eq value:"2"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

TI Map Domain Entity to DeviceNetworkEvents

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

This query identifies any Domain indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: c308b2f3-eebe-4a20-905c-cb8293b062db
name: TI Map Domain Entity to DeviceNetworkEvents
description: |
  'This query identifies any Domain indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.'
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(RemoteUrl)
    | where TimeGenerated >= ago(dt_lookBack)
    | where ActionType !has "ConnectionFailed"
    | extend Domain = tostring(parse_url(tolower(RemoteUrl)).Host)
    | where isnotempty(Domain)
    | project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;
  let DeviceNetworkEventDomains = DeviceNetworkEvents_
    | distinct Domain
    | summarize make_list(Domain);
  ThreatIntelligenceIndicator
  | where isnotempty(DomainName)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend TI_Domain = tolower(DomainName)
  | where TI_Domain in (DeviceNetworkEventDomains)
  | 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_Domain == $right.Domain
  | where DeviceNetworkEvents_TimeGenerated < ExpirationDateTime
  | summarize DeviceNetworkEvents_TimeGenerated = arg_max(DeviceNetworkEvents_TimeGenerated, *) by IndicatorId, TI_Domain
  | project DeviceNetworkEvents_TimeGenerated, IndicatorId, TI_Domain, Url = RemoteUrl, ConfidenceScore, Description, ThreatType, Tags, TrafficLightProtocolLevel, ActionType, DeviceId, DeviceName, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort
  | extend Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
  | extend timestamp = DeviceNetworkEvents_TimeGenerated, UserPrincipalName = InitiatingProcessAccountUpn
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: InitiatingProcessCommandLine
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(RemoteUrl)
  | where TimeGenerated >= ago(dt_lookBack)
  | where ActionType !has "ConnectionFailed"
  | extend Domain = tostring(parse_url(tolower(RemoteUrl)).Host)
  | where isnotempty(Domain)
  | project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;

Let Binding: DeviceNetworkEventDomains used in Stages 2, 7

let DeviceNetworkEventDomains = DeviceNetworkEvents_
  | distinct Domain
  | summarize make_list(Domain);

Stage 1: source

let DeviceNetworkEvents_

Stage 2: source

let DeviceNetworkEventDomains

Stage 3: source

ThreatIntelligenceIndicator

Stage 4: where

where isnotempty(DomainName)

Stage 5: where

where TimeGenerated >= ago(1209600s)

Stage 6: extend

extend TI_Domain

Stage 7: where

where TI_Domain == "DeviceNetworkEventDomains"

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_Domain, $right.Domain

Stage 12: where

where DeviceNetworkEvents_TimeGenerated < ExpirationDateTime

Stage 13: summarize

summarize DeviceNetworkEvents_TimeGenerated by IndicatorId, TI_Domain

Stage 14: project

project ActionType, ConfidenceScore, Description, DeviceId, DeviceName, DeviceNetworkEvents_TimeGenerated, IndicatorId, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort, TI_Domain, Tags, ThreatType, TrafficLightProtocolLevel, Url

Stage 15: extend

extend Name, UPNSuffix

Stage 16: extend

extend UserPrincipalName, 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
ActionTypeproject
ConfidenceScoreproject
Descriptionproject
DeviceIdproject
DeviceNameproject
DeviceNetworkEvents_TimeGeneratedproject
IndicatorIdproject
InitiatingProcessAccountUpnproject
InitiatingProcessCommandLineproject
RemoteIPproject
RemotePortproject
TI_Domainproject
Tagsproject
ThreatTypeproject
TrafficLightProtocolLevelproject
Urlproject
Nameextend
UPNSuffixextend
UserPrincipalNameextend
timestampextend

TI Map Domain entity to EmailEvents

#
Severity
medium
Time window
14d
Group by
$left.DomainName, $right.RecipientEmailDomain, $right.SenderFromDomain, Id, ObservableValue, RecipientEmailAddress
Source
github.com/Azure/Azure-Sentinel

Identifies a match in EmailEvents table from any Domain IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: bc3bb047-70b8-4a4b-ac21-e3b1172881a4
name: TI Map Domain entity to EmailEvents
description: |
  Identifies a match in EmailEvents table from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - EmailEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename  EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromDomain = tolower(SenderFromDomain) | extend RecipientEmailDomain = tolower(tostring(split(RecipientEmailAddress, '@', 1))));
  let SenderDomains = EmailEvents_ | distinct  SenderFromDomain | summarize make_list(SenderFromDomain);
  let RecipientDomains = EmailEvents_ | distinct  RecipientEmailDomain | summarize make_list(RecipientEmailDomain);
  let TI = materialize(ThreatIntelIndicators
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "domain-name"
  | extend DomainName = tolower(ObservableValue)
  | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | where DomainName in (SenderDomains) or DomainName in (RecipientDomains)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | extend Description = tostring(parse_json(Data).description)
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
  (union
    (TI | join kind=innerunique (EmailEvents_) on $left.DomainName == $right.SenderFromDomain),
    (TI | join kind=innerunique (EmailEvents_) on $left.DomainName == $right.RecipientEmailDomain))
  | where EmailEvents_TimeGenerated < ValidUntil
  | extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
  | summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by Id, RecipientEmailAddress
  | project EmailEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Type, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
  | extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
  | extend timestamp = EmailEvents_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: RecipientEmailAddress
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: EmailEvents_

let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename  EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromDomain = tolower(SenderFromDomain) | extend RecipientEmailDomain = tolower(tostring(split(RecipientEmailAddress, '@', 1))));

Let Binding: SenderDomains

let SenderDomains = EmailEvents_ | distinct  SenderFromDomain | summarize make_list(SenderFromDomain);

Let Binding: RecipientDomains

let RecipientDomains = EmailEvents_ | distinct  RecipientEmailDomain | summarize make_list(RecipientEmailDomain);

Union (2 Sources)

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

Leg 1: TI

TI | join kind=innerunique (EmailEvents_) on $left.DomainName == $right.SenderFromDomain

Leg 2: TI

TI | join kind=innerunique (EmailEvents_) on $left.DomainName == $right.RecipientEmailDomain

Applied to the Combined Result

| where EmailEvents_TimeGenerated < ValidUntil
| extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by Id, RecipientEmailAddress
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Type, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
| extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
| extend timestamp = EmailEvents_TimeGenerated

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;"
DeliveryActionmatchBlockedexcludes:DeliveryAction field:"DeliveryAction" value:"Blocked"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Confidenceproject
ConfidenceLevelproject
DeliveryActionproject
DeliveryLocationproject
Descriptionproject
DomainNameproject
EmailDirectionproject
EmailEvents_TimeGeneratedproject
Idproject
RecipientEmailAddressproject
SenderFromAddressproject
Subjectproject
TrafficLightProtocolLevelproject
Typeproject
ValidUntilproject
Nameextend
UPNSuffixextend
timestampextend

UniFi Site Manager: New WAN2 (secondary) issue recorded

#
Status
available
Severity
medium
Time window
45m
Group by
SiteName, siteId_s
Source
github.com/Azure/Azure-Sentinel

Identifies a new issue index on the secondary WAN failover link. Fires only on newly-recorded issue indexes, not while existing issues persist.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: b16f13ae-343b-9513-e684-469cdf9471b2
name: "UniFi Site Manager: New WAN2 (secondary) issue recorded"
description: |
  Identifies a new issue index on the secondary WAN failover link. Fires only on newly-recorded issue indexes, not while existing issues persist.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Sites_CL
queryFrequency: 15m
queryPeriod: 45m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
subTechniques:
  - T1498.001
query: |
  // UniFi reports a monotonically-rising wanIssues.count rather than discrete events.
  // Detect a NEW occurrence by checking whether the per-site max(count) has grown
  // in the most recent 15-min window vs the prior 15-min window.
  Unifi_SiteManager_Sites_CL
  | where TimeGenerated > ago(45m)
  | extend siteId_s = tostring(SiteId)
  | where isnotempty(tostring(SiteStatistics.wans.WAN2))
  | mv-expand issue = todynamic(tostring(SiteStatistics.wans.WAN2.wanIssues))
  | extend issueCount = tolong(issue.count), idx = tolong(issue.index)
  | summarize
      PrevMaxCount = maxif(issueCount, TimeGenerated < ago(15m)),
      CurrentMaxCount = maxif(issueCount, TimeGenerated >= ago(15m)),
      LatestIndex = maxif(idx, TimeGenerated >= ago(15m))
      by siteId_s, SiteName
  | extend PrevMaxCount = coalesce(PrevMaxCount, tolong(0))
  | where CurrentMaxCount > PrevMaxCount
  | extend
      TimeGenerated = now(),
      Delta = CurrentMaxCount - PrevMaxCount,
      Activity = strcat('New WAN2 issue: count rose from ', PrevMaxCount, ' to ', CurrentMaxCount)
  | project TimeGenerated, SiteId = siteId_s, SiteName, Activity, IssueIndex = LatestIndex, PrevMaxCount, CurrentMaxCount, Delta
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteId
      - identifier: DnsDomain
        columnName: SiteName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

Unifi_SiteManager_Sites_CL

Stage 2: where

| where TimeGenerated > ago(45m)

Stage 3: extend

| extend siteId_s = tostring(SiteId)

Stage 4: where

| where isnotempty(tostring(SiteStatistics.wans.WAN2))

Stage 5: mv-expand

| mv-expand issue = todynamic(tostring(SiteStatistics.wans.WAN2.wanIssues))

Stage 6: extend

| extend issueCount = tolong(issue.count), idx = tolong(issue.index)

Stage 7: summarize

| summarize
    PrevMaxCount = maxif(issueCount, TimeGenerated < ago(15m)),
    CurrentMaxCount = maxif(issueCount, TimeGenerated >= ago(15m)),
    LatestIndex = maxif(idx, TimeGenerated >= ago(15m))
    by siteId_s, SiteName

Stage 8: extend

| extend PrevMaxCount = coalesce(PrevMaxCount, tolong(0))

Stage 9: where

| where CurrentMaxCount > PrevMaxCount

Stage 10: extend

| extend
    TimeGenerated = now(),
    Delta = CurrentMaxCount - PrevMaxCount,
    Activity = strcat('New WAN2 issue: count rose from ', PrevMaxCount, ' to ', CurrentMaxCount)

Stage 11: project

| project TimeGenerated, SiteId = siteId_s, SiteName, Activity, IssueIndex = LatestIndex, PrevMaxCount, CurrentMaxCount, Delta

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SiteStatistics.wans.WAN2is_not_null
  • (no value, null check)
field:"SiteStatistics.wans.WAN2" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
CurrentMaxCountproject
Deltaproject
IssueIndexproject
PrevMaxCountproject
SiteIdproject
SiteNameproject
TimeGeneratedproject

User Added to Admin Role

#
Severity
low
Time window
1d
Source
github.com/Azure/Azure-Sentinel

Detects a user being added to a new privileged role. Monitor these additions to ensure the users are made eligible for these roles are intended to have these levels of access. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#changes-to-privileged-accounts

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 2a09f8cb-deb7-4c40-b08b-9137667f1c0b
name: User Added to Admin Role
description: |
  'Detects a user being added to a new privileged role. Monitor these additions to ensure the users are made eligible for these roles are intended to have these levels of access.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#changes-to-privileged-accounts'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  AuditLogs
    | where OperationName in ("Add eligible member (permanent)", "Add eligible member (eligible)", "Add member to role")
    | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend Target = tostring(TargetResource.userPrincipalName),
                 props = TargetResource.modifiedProperties
    )
    | mv-apply Property = props on 
    (
        where Property.displayName =~ "Role.DisplayName"
        | extend RoleName = trim('"',tostring(Property.newValue))
    )
    | where RoleName contains "admin"
    | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
    | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
    | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
    | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
    | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
    | extend InitiatedBy = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
    | extend TargetUserPrincipalName = iff(OperationName=="Add member to role",tostring(TargetResources[0].userPrincipalName),tostring(TargetResources[2].userPrincipalName))
    | extend TargetAadUserId = iff(OperationName=="Add member to role", tostring(TargetResources[0].id), tostring(TargetResources[2].id))
    | extend AddedUser = TargetUserPrincipalName
    | extend TargetAccountName = tostring(split(TargetUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(TargetUserPrincipalName, "@")[1])
    | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
    | project-reorder TimeGenerated, AddedUser, RoleName, InitiatedBy
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUserPrincipalName
      - identifier: Name
        columnName: TargetAccountName
      - identifier: UPNSuffix
        columnName: TargetAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: TargetAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName   
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName in ("Add eligible member (permanent)", "Add eligible member (eligible)", "Add member to role")

Stage 3: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend Target = tostring(TargetResource.userPrincipalName),
               props = TargetResource.modifiedProperties
  )

Stage 4: kusto:mv-apply

| mv-apply Property = props on 
  (
      where Property.displayName =~ "Role.DisplayName"
      | extend RoleName = trim('"',tostring(Property.newValue))
  )

Stage 5: where

| where RoleName contains "admin"

Stage 6: extend (11 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
| extend TargetUserPrincipalName = iff(OperationName=="Add member to role",tostring(TargetResources[0].userPrincipalName),tostring(TargetResources[2].userPrincipalName))
| extend TargetAadUserId = iff(OperationName=="Add member to role", tostring(TargetResources[0].id), tostring(TargetResources[2].id))
| extend AddedUser = TargetUserPrincipalName
| extend TargetAccountName = tostring(split(TargetUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(TargetUserPrincipalName, "@")[1])
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Stage 7: project-reorder

| project-reorder TimeGenerated, AddedUser, RoleName, InitiatedBy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNamein
  • Add eligible member (eligible)
  • Add eligible member (permanent)
  • Add member to role
field:"OperationName" kind:in
Property.displayNameeq
  • Role.DisplayName transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"Role.DisplayName"
RoleNamecontains
  • admin
field:"RoleName" kind:contains value:"admin"
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIPAddressextend
InitiatedByextend
TargetUserPrincipalNameextend
TargetAadUserIdextend
AddedUserextend
TargetAccountNameextend
TargetAccountUPNSuffixextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

ZeroFox Alerts - Informational Severity Alerts

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

'Detects informational severity alerts from ZeroFox'

MITRE ATT&CK Coverage

TacticTechniques
Resource Development
Initial Access

Rule Body

id: 6f7a7413-b72f-4361-84ee-897baeb9c6d4
name: ZeroFox Alerts - Informational Severity Alerts
description: |
  'Detects informational severity alerts from ZeroFox'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: ZeroFox_Alert_Polling
    dataTypes:
      - ZeroFoxAlertPoller_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - ResourceDevelopment
  - InitialAccess
relevantTechniques:
  - T1583
  - T1586
  - T1566
query: |
  ZeroFoxAlertPoller_CL
  | where Severity in (1,2)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: entity_name_s
eventGroupingSettings:
  aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

ZeroFoxAlertPoller_CL

Stage 2: where

| where Severity in (1,2)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Severityin
  • 1
  • 2
field:"Severity" kind:in

Zscaler - Unexpected update operation

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

'Detects unexpected version of update operation.'

MITRE ATT&CK Coverage

Rule Body

id: 672e2846-4226-11ec-81d3-0242ac130003
name: Zscaler - Unexpected update operation
description: |
  'Detects unexpected version of update operation.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - ZPA_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  ZPAEvent
  | where isnotempty(AuditOperationType)
  | extend ExpectedVersion1 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditOldValue)
  | extend ExpectedVersion2 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditOldValue)
  | extend NewVersion1 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditNewValue)
  | extend NewVersion2 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditNewValue)
  | extend Comparision1 = iff(tolong(ExpectedVersion2) > tolong(NewVersion2), 'Unexpected version', 'Expected version')
  | extend VersionCheck = iff(tolong(ExpectedVersion1) > tolong(NewVersion1), 'Unexpected version', Comparision1)
  | project-away ExpectedVersion1, ExpectedVersion2, NewVersion1, NewVersion2, Comparision1
  | extend ProcessCustomEntity = AuditOperationType
entityMappings:
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: ProcessCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

ZPAEvent

Stage 2: where

| where isnotempty(AuditOperationType)

Stage 3: extend (6 consecutive steps)

| extend ExpectedVersion1 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditOldValue)
| extend ExpectedVersion2 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditOldValue)
| extend NewVersion1 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditNewValue)
| extend NewVersion2 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditNewValue)
| extend Comparision1 = iff(tolong(ExpectedVersion2) > tolong(NewVersion2), 'Unexpected version', 'Expected version')
| extend VersionCheck = iff(tolong(ExpectedVersion1) > tolong(NewVersion1), 'Unexpected version', Comparision1)

Stage 4: project-away

| project-away ExpectedVersion1, ExpectedVersion2, NewVersion1, NewVersion2, Comparision1

Stage 5: extend

| extend ProcessCustomEntity = AuditOperationType

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AuditOperationTypeis_not_null
  • (no value, null check)
field:"AuditOperationType" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
VersionCheckextend
ProcessCustomEntityextend

Back to all Kusto rules