Detection rules › Kusto

Kusto rules: 15

1Password - Disable MFA factor or type for all user accounts

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

This will alert when the MFA factor or type for all user accounts are disabled. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 92ab0938-1e7c-4671-9810-392e8b9714da
name: 1Password - Disable MFA factor or type for all user accounts
version: 1.0.0
kind: Scheduled
description: |-
  This will alert when the MFA factor or type for all user accounts are disabled. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same.

  Ref: https://1password.com/
  Ref: https://github.com/securehats/
severity: High
requiredDataConnectors:
  - connectorId: 1Password
    dataTypes:
      - OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1556
query: |-
  OnePasswordEventLogs_CL
  | where log_source == "auditevents"
  | where action == "disblmfa"
  | where object_type == "account"
  | extend
      ActorUsername = actor_details.email
      , SrcIpAddr = session.ip
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorUsername
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr

Stages and Predicates

Stage 1: source

OnePasswordEventLogs_CL

Stage 2: where

| where log_source == "auditevents"

Stage 3: where

| where action == "disblmfa"

Stage 4: where

| where object_type == "account"

Stage 5: extend

| extend
    ActorUsername = actor_details.email
    , SrcIpAddr = session.ip

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUsernameextend
SrcIpAddrextend

AFD WAF - Code Injection

#
Status
available
Severity
high
Time window
6h
Group by
action_s, clientIP_s, host_s, trackingReference_s
Source
github.com/Azure/Azure-Sentinel

'Identifies a match for a Code Injection based attack in the AFD WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements. References: https://owasp.org/www-community/attacks/Code_Injection'

MITRE ATT&CK Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: ded8168e-c806-4772-af30-10576e0a7529
name: AFD WAF - Code Injection 
description: | 
  'Identifies a match for a Code Injection based attack in the AFD WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements.
  References: https://owasp.org/www-community/attacks/Code_Injection'
severity: High 
status: Available
requiredDataConnectors: 
  - connectorId: WAF
    dataTypes: 
      - AzureDiagnostics
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
tactics: 
  - DefenseEvasion
  - Execution
  - InitialAccess
  - PrivilegeEscalation
relevantTechniques:
  - T1548
  - T1203
  - T1190
  - T1548
tags:
  - Code Injection
query:  | 
 let Threshold = 3;
 AzureDiagnostics
 | where Category =~ "FrontDoorWebApplicationFirewallLog"
 | where action_s =~ "AnomalyScoring"
 | where details_msg_s has "Injection" or details_msg_s has "File Inclusion"
 | parse details_data_s with MessageText "Matched Data:" MatchedData "AND " * "table_name FROM " TableName " " *
 | project trackingReference_s, host_s, requestUri_s, TimeGenerated, clientIP_s, details_matches_s, details_msg_s, details_data_s, TableName, MatchedData
 | join kind = inner(
 AzureDiagnostics
 | where Category =~ "FrontDoorWebApplicationFirewallLog"
 | where action_s =~ "Block") on trackingReference_s
 | summarize URI_s = make_set(requestUri_s,100), Table = make_set(TableName,100), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), TrackingReference = make_set(trackingReference_s,100), Matched_Data = make_set(MatchedData,100), Detail_Data = make_set(details_data_s,100), Detail_Message = make_set(details_msg_s,100), Total_TrackingReference = dcount(trackingReference_s) by clientIP_s, host_s, action_s
 | where Total_TrackingReference >= Threshold
# The Threshold value above can be changed as per your infrastructure's requirement
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URI_s
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: clientIP_s 
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let Threshold = 3;

Stage 1: source

AzureDiagnostics

Stage 2: where

| where Category =~ "FrontDoorWebApplicationFirewallLog"

Stage 3: where

| where action_s =~ "AnomalyScoring"

Stage 4: where

| where details_msg_s has "Injection" or details_msg_s has "File Inclusion"

Stage 5: parse

| parse details_data_s with MessageText "Matched Data:" MatchedData "AND " * "table_name FROM " TableName " " *

Stage 6: project

| project trackingReference_s, host_s, requestUri_s, TimeGenerated, clientIP_s, details_matches_s, details_msg_s, details_data_s, TableName, MatchedData

Stage 7: join

| join kind = inner(
AzureDiagnostics
| where Category =~ "FrontDoorWebApplicationFirewallLog"
| where action_s =~ "Block") on trackingReference_s

Stage 8: summarize

summarize Detail_Data, Detail_Message, EndTime, Matched_Data, StartTime, Table, Total_TrackingReference, TrackingReference, URI_s by clientIP_s, host_s, action_s

Stage 9: where

where Total_TrackingReference >= 3

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • FrontDoorWebApplicationFirewallLog
field:"Category" kind:eq value:"FrontDoorWebApplicationFirewallLog"
Total_TrackingReferencege
  • 3 transforms: number
field:"Total_TrackingReference" kind:ge value:"3"
action_seq
  • AnomalyScoring
  • Block
field:"action_s" kind:eq
details_msg_smatch
  • File Inclusion transforms: term
  • Injection transforms: term
field:"details_msg_s" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
Detail_Datasummarize
Detail_Messagesummarize
EndTimesummarize
Matched_Datasummarize
StartTimesummarize
Tablesummarize
Total_TrackingReferencesummarize
TrackingReferencesummarize
URI_ssummarize
action_ssummarize
clientIP_ssummarize
host_ssummarize

Azure DevOps Service Connection Abuse

#
Status
available
Severity
medium
Time window
14d
Group by
ActorUPN, IpAddress, OrganizationName, ProjectId, ProjectName
Source
github.com/Azure/Azure-Sentinel

'Flags builds/releases that use a large number of service connections if they aren't manually in the allow list. This is to determine if someone is hijacking a build/release and adding many service connections in order to abuse or dump credentials from service connections.'

MITRE ATT&CK Coverage

Rule Body

id: d564ff12-8f53-41b8-8649-44f76b37b99f
name: Azure DevOps Service Connection Abuse
description: |
  'Flags builds/releases that use a large number of service connections if they aren't manually in the allow list.
  This is to determine if someone is hijacking a build/release and adding many service connections in order to abuse or dump credentials from service connections.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - Impact
relevantTechniques:
  - T1098
  - T1496
query: |
  // How many greater than Service Connections you want to view per build/release
  let ServiceConnectionThreshold = 4;
  let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
  [
  //"103", "Release", "ProjectA",
  //"42", "Release", "ProjectB",
  //"122", "Build", "ProjectB"
  ];
  ADOAuditLogs
  | where OperationName == "Library.ServiceConnectionExecuted"
  | extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
  | parse ScopeDisplayName with OrganizationName ' (Organization)'
  | summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
    by OrganizationName, tostring(DefId), tostring(Type), ProjectId, ProjectName, ActorUPN, IpAddress
  | where CurrentCount > ServiceConnectionThreshold
  | join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName
  | extend link = iif(
    Type == "Build", strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId),
    strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId))
  | extend timestamp = StartTime
  | 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 ServiceConnectionThreshold = 4;

Let Binding: BypassDefIds used in Stage 7

let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
[
];

Stage 1: source

ADOAuditLogs

Stage 2: where

| where OperationName == "Library.ServiceConnectionExecuted"

Stage 3: extend

| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)

Stage 4: parse

| parse ScopeDisplayName with OrganizationName ' (Organization)'

Stage 5: summarize

| summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
  by OrganizationName, tostring(DefId), tostring(Type), ProjectId, ProjectName, ActorUPN, IpAddress

Stage 6: where

| where CurrentCount > ServiceConnectionThreshold

Stage 7: join (negated)

join kind=anti (BypassDefIds)

Stage 8: extend (3 consecutive steps)

extend AccountName, AccountUPNSuffix, link, timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUPNsummarize
ConnectionNamessummarize
CurrentCountsummarize
EndTimesummarize
IpAddresssummarize
OrganizationNamesummarize
ProjectIdsummarize
ProjectNamesummarize
StartTimesummarize
linkextend
timestampextend
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - Large Default Groups With GenericAll 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 GenericAll Privileges

Rule Body

id: 54a6167c-f1a3-45a1-8fd0-6023dbda9446
name: BloodHound Attack Path Finding - Large Default Groups With GenericAll Privileges
description: BloodHound Attack Path Finding - Large Default Groups With GenericAll
  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 == "LargeDefaultGroupsPSRemote"
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 == "LargeDefaultGroupsPSRemote"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Findingeq
  • LargeDefaultGroupsPSRemote corpus 2 (kusto 2)
field:"Finding" kind:eq value:"LargeDefaultGroupsPSRemote"

BloodHound Attack Path Finding - PS Remote Users on Tier Zero Computers

#

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 - PS Remote Users on Tier Zero Computers

Rule Body

id: 25ea90f3-89d6-417f-8e16-2e7fee38ccf6
name: BloodHound Attack Path Finding - PS Remote Users on Tier Zero Computers
description: BloodHound Attack Path Finding - PS Remote Users on Tier Zero Computers
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 == "T0PSRemote"
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 == "T0PSRemote"

Indicators

These rows show field, operator, and value matches.

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

Cisco - firewall block but success logon to Microsoft Entra ID

#
Severity
medium
Time window
1d
Group by
$left.SourceIP, $right.IPAddress, SourceIP
Author
Microsoft Security Resarch
Source
github.com/Azure/Azure-Sentinel

Correlate IPs blocked by a Cisco firewall appliance with successful Microsoft Entra ID signins. Because the IP was blocked by the firewall, that same IP logging on successfully to Entra ID is potentially suspect and could indicate credential compromise for the user account.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 157c0cfc-d76d-463b-8755-c781608cdc1a
name: Cisco - firewall block but success logon to Microsoft Entra ID
description: |
  'Correlate IPs blocked by a Cisco firewall appliance with successful Microsoft Entra ID signins.
  Because the IP was blocked by the firewall, that same IP logging on successfully to Entra ID is potentially suspect and could indicate credential compromise for the user account.'
severity: Medium
requiredDataConnectors:
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureActiveDirectory
    dataTypes:
     - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  let aadFunc = (tableName:string){
  CommonSecurityLog
  | where DeviceVendor =~ "Cisco"
  | where DeviceAction =~ "denied"
  | where ipv4_is_private(SourceIP) == false
  | summarize count() by SourceIP
  | join (
      // Successful signins from IPs blocked by the firewall solution are suspect
      // Include fully successful sign-ins, but also ones that failed only at MFA stage
      // as that supposes the password was sucessfully guessed.
    table(tableName)
    | where ResultType in ("0", "50074", "50076")
  ) on $left.SourceIP == $right.IPAddress
  | extend AccountName = tostring(split(Account, "@")[0]), AccountUPNSuffix = tostring(split(Account, "@")[1])
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
version: 1.0.6
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Resarch
    support:
        tier: Community
    categories:
        domains: [ "Security - Network" ]

Stages and Predicates

Parameters

let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let Binding: aadFunc

let aadFunc = (tableName:string){
CommonSecurityLog
| where DeviceVendor =~ "Cisco"
| where DeviceAction =~ "denied"
| where ipv4_is_private(SourceIP) == false
| summarize count() by SourceIP
| join (
  table(tableName)
  | where ResultType in ("0", "50074", "50076")
) on $left.SourceIP == $right.IPAddress
| extend AccountName = tostring(split(Account, "@")[0]), AccountUPNSuffix = tostring(split(Account, "@")[1])
};

Union isfuzzy=true (2 Sources)

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

Leg 1: aadSignin

Leg 2: aadNonInt

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SourceIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:SourceIP

Output Fields

These fields are emitted when the rule matches.

FieldSource
SourceIPsummarize
AccountNameextend
AccountUPNSuffixextend

Cisco Duo - Authentication device new location

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

'Detects new location of authentication device.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 01df3abe-3dc7-40e2-8aa7-f00b402df6f0
name: Cisco Duo - Authentication device new location
description: |
  'Detects new location of authentication device.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CiscoDuoAuthConnectorDefinition
    dataTypes:
      - DuoAuthentication_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  DuoAuthentication_CL
  | where Result =~ 'success'
  | where isnotempty(AuthDeviceCountry)
  | summarize src_c = make_set(AuthDeviceCountry) by AccessDeviceIp
  | join (DuoAuthentication_CL
          | where Result =~ 'success') on AccessDeviceIp
  | where src_c !has AuthDeviceCountry
  | extend IPCustomEntity = AccessDeviceIp, AccountCustomEntity = UserName
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

DuoAuthentication_CL

Stage 2: where

| where Result =~ 'success'

Stage 3: where

| where isnotempty(AuthDeviceCountry)

Stage 4: summarize

| summarize src_c = make_set(AuthDeviceCountry) by AccessDeviceIp

Stage 5: join

| join (DuoAuthentication_CL
        | where Result =~ 'success') on AccessDeviceIp

Stage 6: where

where src_c =~ AuthDeviceCountry

Stage 7: extend

extend AccountCustomEntity, IPCustomEntity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccessDeviceIpsummarize
src_csummarize
AccountCustomEntityextend
IPCustomEntityextend

Cognni Incidents for Medium Sensitivity Financial Information

#

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
5h
Source
github.com/Azure/Azure-Sentinel

'Display incidents in which medium sensitive financial information was placed at risk by user sharing.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: d29b1d66-d4d9-4be2-b607-63278fc4fe6b
name: Cognni Incidents for Medium Sensitivity Financial Information
description: |
  'Display incidents in which medium sensitive financial information was placed at risk by user sharing.'
severity: Medium
requiredDataConnectors:
  - connectorId: CognniSentinelDataConnector
    dataTypes:
      - CognniIncidents_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1530
query: |
  let mediumRisk = 2;
  let financial = 'Financial Information';
  CognniIncidents_CL 
  | where Severity == mediumRisk
  | where informationType_s == financial
  | where TimeGenerated >= ago(5h)
  | extend AccountCustomEntity = userId_s
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let mediumRisk = 2;
let financial = 'Financial Information';

Stage 1: source

CognniIncidents_CL

Stage 2: where

| where Severity == mediumRisk

Stage 3: where

| where informationType_s == financial

Stage 4: where

| where TimeGenerated >= ago(5h)

Stage 5: extend

| extend AccountCustomEntity = userId_s

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Correlate Unfamiliar sign-in properties & atypical travel alerts

#
Status
available
Severity
high
Time window
14d
Group by
$left.AadTenantId, $left.AadUserId, $right.AccountObjectId, $right.AccountTenantId, UserAccount
Source
github.com/Azure/Azure-Sentinel

The combination of an Unfamiliar sign-in properties alert and an Atypical travel alert about the same user within a +10m or -10m window is considered a high severity incident.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: a3df4a32-4805-4c6d-8699-f3c888af2f67
name: Correlate Unfamiliar sign-in properties & atypical travel alerts
description: |
  'The combination of an Unfamiliar sign-in properties alert and an Atypical travel alert about the same user within a +10m or -10m window is considered a high severity incident.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
    // We can use this configuration TimeDeltaInMinutes if you want to chnage the time window that we try to match the alerts
    let TimeDeltaInMinutes = 10;
    let Alert_UnfamiliarSignInProps = 
    SecurityAlert
    | where TimeGenerated > ago(1d)
    | where ProductName =~ "Azure Active Directory Identity Protection"
    | where AlertName =~ "Unfamiliar sign-in properties"
    | mv-expand Entity = todynamic(Entities)
    | where Entity.Type =~ "account"
    | extend AadTenantId = tostring(Entity.AadTenantId)
    | extend AadUserId = tostring(Entity.AadUserId)
    | join kind=inner (
    IdentityInfo
    | distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
    | extend UserName = AccountDisplayName
    | extend UserAccount = AccountUPN
    | where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
    | project AccountTenantId, AccountObjectId, UserAccount, UserName
    )
    on
    $left.AadTenantId == $right.AccountTenantId,
    $left.AadUserId == $right.AccountObjectId
    | extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
    | extend Alert_UnfamiliarSignInProps_Time = TimeGenerated
    | extend Alert_UnfamiliarSignInProps_Name = AlertName
    | extend Alert_UnfamiliarSignInProps_Severity = AlertSeverity
    | project AadTenantId, AadUserId, AccountTenantId, AccountObjectId, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, UserAccount, UserName
    ;
    let Alert_AtypicalTravels = 
    SecurityAlert
    | where TimeGenerated > ago(1d)
    | where ProductName =~ "Azure Active Directory Identity Protection"
    | where AlertName =~ "Atypical travel"
    | mv-expand Entity = todynamic(Entities)
    | where Entity.Type =~ "account"
    | extend AadTenantId = tostring(Entity.AadTenantId)
    | extend AadUserId = tostring(Entity.AadUserId)
    | join kind=inner (
    IdentityInfo
    | distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
    | extend UserName = AccountDisplayName
    | extend UserAccount = AccountUPN
    | where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
    | project AccountTenantId, AccountObjectId, UserAccount, UserName
    )
    on
    $left.AadTenantId == $right.AccountTenantId,
    $left.AadUserId == $right.AccountObjectId
    | extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
    | extend Alert_AtypicalTravels_Time = TimeGenerated
    | extend Alert_AtypicalTravels_Name = AlertName
    | extend Alert_AtypicalTravels_Severity = AlertSeverity
    | extend ExtendedProperties_json= parse_json(ExtendedProperties)
    | extend CurrentLocation = tostring(ExtendedProperties_json.["Current Location"])
    | extend PreviousLocation = tostring(ExtendedProperties_json.["Previous Location"])
    | extend CurrentIPAddress = tostring(ExtendedProperties_json.["Current IP Address"])
    | extend PreviousIPAddress = tostring(ExtendedProperties_json.["Previous IP Address"])
    | extend Comments = tostring(ExtendedProperties_json.Comments)
    | extend RiskDetail = extract(@"(?i)risk detail:\s*(.+)$", 1, Comments)
    | where isempty(RiskDetail) or RiskDetail !contains "admin"
    | project AadTenantId, AadUserId, AccountTenantId, AccountObjectId, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, CurrentIPAddress, PreviousIPAddress, CurrentLocation, Comments, PreviousLocation, UserAccount, UserName, CompromisedEntity
    ;
    Alert_UnfamiliarSignInProps
    | join kind=inner Alert_AtypicalTravels on UserAccount
    | where abs(datetime_diff('minute', Alert_UnfamiliarSignInProps_Time, Alert_AtypicalTravels_Time)) <= TimeDeltaInMinutes
    | extend TimeDelta = Alert_UnfamiliarSignInProps_Time - Alert_AtypicalTravels_Time
    | project UserAccount, AadUserId, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, TimeDelta, CurrentLocation, PreviousLocation, CurrentIPAddress, PreviousIPAddress, Comments, UserName
    | extend UserEmailName = split(UserAccount,'@')[0], UPNSuffix = split(UserAccount,'@')[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserAccount
      - identifier: Name
        columnName: UserEmailName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: AadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CurrentIPAddress
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: PreviousIPAddress
customDetails:
    Alert1_Name: Alert_UnfamiliarSignInProps_Name
    Alert1_Time: Alert_UnfamiliarSignInProps_Time
    Alert1_Severity: Alert_UnfamiliarSignInProps_Severity
    Alert2_Name: Alert_AtypicalTravels_Name
    Alert2_Time: Alert_AtypicalTravels_Time
    Alert2_Severity: Alert_AtypicalTravels_Severity
    TimeDelta: TimeDelta
    CurrentLocation: CurrentLocation
    PreviousLocation: PreviousLocation
    CurrentIPAddress: CurrentIPAddress
    PreviousIPAddress: PreviousIPAddress
version: 1.0.9
kind: Scheduled

Stages and Predicates

Parameters

let TimeDeltaInMinutes = 10;

let Alert_UnfamiliarSignInProps is inlined into the numbered stages below.

Let Binding: Alert_AtypicalTravels used in Stages 2, 14

let Alert_AtypicalTravels = SecurityAlert
| where TimeGenerated > ago(1d)
| where ProductName =~ "Azure Active Directory Identity Protection"
| where AlertName =~ "Atypical travel"
| mv-expand Entity = todynamic(Entities)
| where Entity.Type =~ "account"
| extend AadTenantId = tostring(Entity.AadTenantId)
| extend AadUserId = tostring(Entity.AadUserId)
| join kind=inner (
IdentityInfo
| distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
| extend UserName = AccountDisplayName
| extend UserAccount = AccountUPN
| where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
| project AccountTenantId, AccountObjectId, UserAccount, UserName
)
on
$left.AadTenantId == $right.AccountTenantId,
$left.AadUserId == $right.AccountObjectId
| extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
| extend Alert_AtypicalTravels_Time = TimeGenerated
| extend Alert_AtypicalTravels_Name = AlertName
| extend Alert_AtypicalTravels_Severity = AlertSeverity
| extend ExtendedProperties_json= parse_json(ExtendedProperties)
| extend CurrentLocation = tostring(ExtendedProperties_json.["Current Location"])
| extend PreviousLocation = tostring(ExtendedProperties_json.["Previous Location"])
| extend CurrentIPAddress = tostring(ExtendedProperties_json.["Current IP Address"])
| extend PreviousIPAddress = tostring(ExtendedProperties_json.["Previous IP Address"])
| extend Comments = tostring(ExtendedProperties_json.Comments)
| extend RiskDetail = extract(@"(?i)risk detail:\s*(.+)$", 1, Comments)
| where isempty(RiskDetail) or RiskDetail !contains "admin"
| project AadTenantId, AadUserId, AccountTenantId, AccountObjectId, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, CurrentIPAddress, PreviousIPAddress, CurrentLocation, Comments, PreviousLocation, UserAccount, UserName, CompromisedEntity;

Stage 1: source

let Alert_UnfamiliarSignInProps

Stage 2: source

let Alert_AtypicalTravels

Stage 3: source

SecurityAlert

Stage 4: where

where TimeGenerated > ago(86400s)

Stage 5: where

where ProductName =~ "Azure Active Directory Identity Protection"

Stage 6: where

where AlertName =~ "Unfamiliar sign-in properties"

Stage 7: mv-expand

mv-expand Entity

Stage 8: where

where Entity.Type =~ "account"

Stage 9: extend

extend AadTenantId

Stage 10: extend

extend AadUserId

Stage 11: join

join kind=inner (IdentityInfo) on $left.AadTenantId, $right.AccountTenantId, $left.AadUserId, $right.AccountObjectId

Stage 12: extend (4 consecutive steps)

extend Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, CompromisedEntity
CompromisedEntity =
if(CompromisedEntity == "N/A" or isempty(CompromisedEntity))UserAccount
elseCompromisedEntity

Stage 13: project

project AadTenantId, AadUserId, AccountObjectId, AccountTenantId, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, UserAccount, UserName

Stage 14: join

join kind=inner (Alert_AtypicalTravels) on UserAccount

Stage 15: where

where /* macro: (abs(datetime_diff('minute', Alert_UnfamiliarSignInProps_Time, Alert_AtypicalTravels_Time)) <= 10) */

Stage 16: extend

extend TimeDelta

Stage 17: project

project AadUserId, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, Comments, CurrentIPAddress, CurrentLocation, PreviousIPAddress, PreviousLocation, TimeDelta, UserAccount, UserName

Stage 18: extend

extend UPNSuffix, UserEmailName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccountDisplayNameis_not_null
  • (no value, null check)
field:"AccountDisplayName" kind:is_not_null
AlertNameeq
  • Atypical travel
  • Unfamiliar sign-in properties
field:"AlertName" kind:eq
Entity.Typeeq
  • account transforms: kusto_dynamic_path
field:"Entity.Type" kind:eq value:"account"
ProductNameeq
  • Azure Active Directory Identity Protection
field:"ProductName" kind:eq value:"Azure Active Directory Identity Protection"
RiskDetailis_null
  • (no value, null check)
field:"RiskDetail" kind:is_null
UserAccountis_not_null
  • (no value, null check)
field:"UserAccount" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AadUserIdproject
Alert_AtypicalTravels_Nameproject
Alert_AtypicalTravels_Severityproject
Alert_AtypicalTravels_Timeproject
Alert_UnfamiliarSignInProps_Nameproject
Alert_UnfamiliarSignInProps_Severityproject
Alert_UnfamiliarSignInProps_Timeproject
Commentsproject
CurrentIPAddressproject
CurrentLocationproject
PreviousIPAddressproject
PreviousLocationproject
TimeDeltaproject
UserAccountproject
UserNameproject
UPNSuffixextend
UserEmailNameextend

Creation of expensive computes in Azure

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

Identifies the creation of large size or expensive VMs (with GPUs or with a large number of virtual CPUs) in Azure. An adversary may create new or update existing virtual machines to evade defenses or use them for cryptomining purposes. For Windows/Linux Vm Sizes, see https://docs.microsoft.com/azure/virtual-machines/windows/sizes Azure VM Naming Conventions, see https://docs.microsoft.com/azure/virtual-machines/vm-naming-conventions

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 9736e5f1-7b6e-4bfb-a708-e53ff1d182c3
name: Creation of expensive computes in Azure
description: |
  'Identifies the creation of large size or expensive VMs (with GPUs or with a large number of virtual CPUs) in Azure.
  An adversary may create new or update existing virtual machines to evade defenses or use them for cryptomining purposes.
  For Windows/Linux Vm Sizes, see https://docs.microsoft.com/azure/virtual-machines/windows/sizes 
  Azure VM Naming Conventions, see https://docs.microsoft.com/azure/virtual-machines/vm-naming-conventions'
severity: Low
status: Available 
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 1
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1578
query: |
  let tokens = dynamic(["416","208","192","128","120","96","80","72","64","48","44","40","nc12","nc24","nv24"]);
  let operationList = dynamic(["microsoft.compute/virtualmachines/write", "microsoft.resources/deployments/write"]);
  AzureActivity
  | where OperationNameValue in~ (operationList)
  | where ActivityStatusValue startswith "Accept"
  | where Properties has 'vmSize'
  | extend parsed_property= parse_json(tostring((parse_json(Properties).responseBody))).properties
  | extend vmSize = tostring((parsed_property.hardwareProfile).vmSize)
  | mv-apply token=tokens to typeof(string) on (where vmSize contains token)
  | extend ComputerName = tostring((parsed_property.osProfile).computerName)
  | project TimeGenerated, OperationNameValue, ActivityStatusValue, Caller, CallerIpAddress, ComputerName, vmSize
  | extend Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: ComputerName       
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
version: 2.0.3
kind: Scheduled

Stages and Predicates

Parameters

let tokens = dynamic(["416","208","192","128","120","96","80","72","64","48","44","40","nc12","nc24","nv24"]);
let operationList = dynamic(["microsoft.compute/virtualmachines/write", "microsoft.resources/deployments/write"]);

Stage 1: source

AzureActivity

Stage 2: where

| where OperationNameValue in~ (operationList)

Stage 3: where

| where ActivityStatusValue startswith "Accept"

Stage 4: where

| where Properties has 'vmSize'

Stage 5: extend

| extend parsed_property= parse_json(tostring((parse_json(Properties).responseBody))).properties

Stage 6: extend

| extend vmSize = tostring((parsed_property.hardwareProfile).vmSize)

Stage 7: kusto:mv-apply

| mv-apply token=tokens to typeof(string) on (where vmSize contains token)

Stage 8: extend

| extend ComputerName = tostring((parsed_property.osProfile).computerName)

Stage 9: project

| project TimeGenerated, OperationNameValue, ActivityStatusValue, Caller, CallerIpAddress, ComputerName, vmSize

Stage 10: extend

| extend Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityStatusValuestarts_with
  • Accept corpus 2 (kusto 2)
field:"ActivityStatusValue" kind:starts_with value:"Accept"
OperationNameValuein
  • microsoft.compute/virtualmachines/write corpus 4 (kusto 4)
  • microsoft.resources/deployments/write corpus 4 (kusto 4)
field:"azure_ad::operation_name_value" kind:in
Propertiesmatch
  • vmSize transforms: term corpus 2 (kusto 2)
field:"Properties" kind:match value:"vmSize"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityStatusValueproject
Callerproject
CallerIpAddressproject
ComputerNameproject
OperationNameValueproject
TimeGeneratedproject
vmSizeproject
Nameextend
UPNSuffixextend

CyberArkEPM - Possible execution of Powershell Empire

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

'Detects possible execution of Powershell Empire.'

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

id: eddfd1fd-71df-4cc3-b050-287643bee398
name: CyberArkEPM - Possible execution of Powershell Empire
description: |
  'Detects possible execution of Powershell Empire.'
severity: High
requiredDataConnectors:
  - connectorId: CyberArkEPM
    dataTypes:
      - CyberArkEPM
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: | 
  CyberArkEPM
  | where EventSubType != 'AttackAttempt'
  | where ActingProcessCommandLine has_any ('-NoP -sta -NonI -W Hidden -Enc', '-noP -sta -w 1 -enc', '-NoP -NonI -W Hidden -enc')
  | extend AccountCustomEntity = ActorUsername
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CyberArkEPM

Stage 2: where

| where EventSubType != 'AttackAttempt'

Stage 3: where

| where ActingProcessCommandLine has_any ('-NoP -sta -NonI -W Hidden -Enc', '-noP -sta -w 1 -enc', '-NoP -NonI -W Hidden -enc')

Stage 4: extend

| extend AccountCustomEntity = ActorUsername

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActingProcessCommandLinematch
  • -NoP -NonI -W Hidden -enc transforms: term
  • -NoP -sta -NonI -W Hidden -Enc transforms: term
  • -noP -sta -w 1 -enc transforms: term
field:"ParentCommandLine" kind:match
EventSubTypene
  • AttackAttempt
field:"EventSubType" kind:ne value:"AttackAttempt"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Detect device token stealing with WDAC

#
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This rule uses a WDAC audit policy to ingest missing Microsoft Defender for Endpoint events. By doing this, we can detect PRT token stealing on a device when exploiting the MicrosoftAccountTokenProvider.dll. For more detailed information on the WDAC audit policy, see the blogpost added in the references.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

DeviceEvents
| where ActionType startswith "AppControl"
| where FileName =~ "MicrosoftAccountTokenProvider.dll"
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where GlobalPrevalence < 250

Stages and Predicates

Stage 1: source

DeviceEvents

Stage 2: where

| where ActionType startswith "AppControl"

Stage 3: where

| where FileName =~ "MicrosoftAccountTokenProvider.dll"

Stage 4: invoke

| invoke FileProfile(InitiatingProcessSHA1, 1000)

Stage 5: where

| where GlobalPrevalence < 250

Indicators

These rows show field, operator, and value matches.

Detect instances of multiple server errors occurring within a brief period of time (ASIM Web Session)

#
Status
available
Severity
medium
Time window
5m
Group by
SrcHostname, SrcIpAddr, SrcUsername
Source
github.com/Azure/Azure-Sentinel

'This detection mechanism identifies situations where multiple server errors originate from a single source within a limited time frame.'

MITRE ATT&CK Coverage

Rule Body

id: a59ba76c-0205-4966-948e-3d5640140688
name: Detect instances of multiple server errors occurring within a brief period of time (ASIM Web Session)
description: |
  'This detection mechanism identifies situations where multiple server errors originate from a single source within a limited time frame.'
severity: Medium
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Impact
relevantTechniques:
  - T1190
  - T1133
  - T1498
query: |
  // HTTP response status codes indicate whether a specific HTTP request has been successfully completed.
  // Please refer this for more details: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
  let threshold = 100; // You can update threshold value as per your environment
  _Im_WebSession(starttime=ago(1h))
  | where toint(EventResultDetails) between (500 .. 599)
  | summarize
      TotalErrorCount = count(),
      URLs=make_set(Url, 100),
      EventStartTime = min(TimeGenerated),
      EventEndTime = max(TimeGenerated),
      EventResultDetailsSet=make_set(EventResultDetails,10)
      by SrcIpAddr, bin(TimeGenerated, 5m), SrcUsername, SrcHostname
  | where TotalErrorCount > threshold
  | extend
      Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
      UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), ""), Threshold=threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  TotalErrorCount: TotalErrorCount
  RequestURLs: URLs
  ErrorThreshold: Threshold
  EventResultSet: EventResultDetailsSet
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
alertDetailsOverride:
  alertDisplayNameFormat: "High number of server errors originated by user '{{SrcUsername}}' from IP address '{{SrcIpAddr}}'"
  alertDescriptionFormat: "The client has made a total of '{{TotalErrorCount}}' requests to URLs '{{URLs}}', 
                            which have resulted in server errors. It is recommended to thoroughly investigate this alert to determine the underlying cause behind this 
                            significant number of errors. For detailed information regarding the specific errors encountered, please refer to the following link:
                            https://developer.mozilla.org/en-US/docs/Web/HTTP/Status."
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 100;

Stage 1: source

_Im_WebSession(starttime=ago(1h))

Stage 2: where

| where toint(EventResultDetails) between (500 .. 599)

Stage 3: summarize

| summarize
    TotalErrorCount = count(),
    URLs=make_set(Url, 100),
    EventStartTime = min(TimeGenerated),
    EventEndTime = max(TimeGenerated),
    EventResultDetailsSet=make_set(EventResultDetails,10)
    by SrcIpAddr, bin(TimeGenerated, 5m), SrcUsername, SrcHostname

Stage 4: where

| where TotalErrorCount > threshold

Stage 5: extend

| extend
    Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
    UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), ""), Threshold=threshold

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventEndTimesummarize
EventResultDetailsSetsummarize
EventStartTimesummarize
SrcHostnamesummarize
SrcIpAddrsummarize
SrcUsernamesummarize
TotalErrorCountsummarize
URLssummarize
Nameextend
Thresholdextend
UPNSuffixextend

Excessive Login Attempts (Microsoft Defender for IoT)

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

'This alert leverages Defender for IoT to detect excessive login attempts that may indicate improper service configuration, human error, or malicious activity on the network such as a cyber threat attempting to manipulate the SCADA network.'

MITRE ATT&CK Coverage

TacticTechniques
Impair Process Control

Rule Body

id: f5217b4c-3f1f-4d89-b4f3-5d7581da1c1c
name: Excessive Login Attempts (Microsoft Defender for IoT)
description: |
  'This alert leverages Defender for IoT to detect excessive login attempts that may indicate improper service configuration, human error, or malicious activity on the network such as a cyber threat attempting to manipulate the SCADA network.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (ASC for IoT)
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - ImpairProcessControl
relevantTechniques:
  - T0806
query: |
  SecurityAlert
  | where ProviderName == "IoTSecurity"
  | where AlertName in ("Excessive Login Attempts","Excessive SMB login attempts","Password Guessing Attempt Detected","Excessive Number of Sessions") 
  | extend ExtendedProperties = parse_json(ExtendedProperties)
  | where tostring(ExtendedProperties.isNew) == "True"
  | extend DeviceId = tostring(ExtendedProperties.DeviceId), 
           SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress), 
           DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress), 
           RemediationSteps = tostring(parse_json(RemediationSteps)[0]), 
           Protocol = tostring(ExtendedProperties.Protocol), 
           AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
  | project
    TimeGenerated,
    DeviceId,
    ProductName,
    ProductComponentName,
    AlertSeverity,
    AlertName,
    Description,
    Protocol,
    SourceDeviceAddress,
    DestDeviceAddress,
    RemediationSteps,
    Tactics,
    Entities,
    VendorOriginalId,
    AlertLink,
    AlertManagementUri,
    Techniques
entityMappings:
sentinelEntitiesMappings:
  - columnName: Entities
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  Sensor: DeviceId
  Protocol: Protocol
  VendorOriginalId: VendorOriginalId
  AlertManagementUri: AlertManagementUri
alertDetailsOverride:
  alertDisplayNameFormat: (MDIoT) {{AlertName}}
  alertDescriptionFormat: (MDIoT) {{Description}}
  alertTacticsColumnName: Tactics
  alertSeverityColumnName: AlertSeverity
  alertDynamicProperties: 
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: RemediationSteps
      value: RemediationSteps
    - alertProperty: Techniques
      value: Techniques
    - alertProperty: ProductComponentName
      value: ProductComponentName
    - alertProperty: AlertLink
      value: AlertLink
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where ProviderName == "IoTSecurity"

Stage 3: where

| where AlertName in ("Excessive Login Attempts","Excessive SMB login attempts","Password Guessing Attempt Detected","Excessive Number of Sessions")

Stage 4: extend

| extend ExtendedProperties = parse_json(ExtendedProperties)

Stage 5: where

| where tostring(ExtendedProperties.isNew) == "True"

Stage 6: extend

| extend DeviceId = tostring(ExtendedProperties.DeviceId), 
         SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress), 
         DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress), 
         RemediationSteps = tostring(parse_json(RemediationSteps)[0]), 
         Protocol = tostring(ExtendedProperties.Protocol), 
         AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)

Stage 7: project

| project
  TimeGenerated,
  DeviceId,
  ProductName,
  ProductComponentName,
  AlertSeverity,
  AlertName,
  Description,
  Protocol,
  SourceDeviceAddress,
  DestDeviceAddress,
  RemediationSteps,
  Tactics,
  Entities,
  VendorOriginalId,
  AlertLink,
  AlertManagementUri,
  Techniques

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AlertNamein
  • Excessive Login Attempts
  • Excessive Number of Sessions
  • Excessive SMB login attempts
  • Password Guessing Attempt Detected
field:"AlertName" kind:in
ExtendedProperties.isNeweq
  • True transforms: tostring, kusto_dynamic_path corpus 15 (kusto 15)
field:"ExtendedProperties.isNew" kind:eq value:"True"
ProviderNameeq
  • IoTSecurity corpus 15 (kusto 15)
field:"ProviderName" kind:eq value:"IoTSecurity"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertLinkproject
AlertManagementUriproject
AlertNameproject
AlertSeverityproject
Descriptionproject
DestDeviceAddressproject
DeviceIdproject
Entitiesproject
ProductComponentNameproject
ProductNameproject
Protocolproject
RemediationStepsproject
SourceDeviceAddressproject
Tacticsproject
Techniquesproject
TimeGeneratedproject
VendorOriginalIdproject

Flare chat results

#

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

'This query searches for conversations and posts from real-time messaging environments used by threat actors and fraud communities.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: 76210211-3ade-47b6-b7f2-c871cd05ec43
name: Flare chat results
description: |
    'This query searches for conversations and posts from real-time messaging environments used by threat actors and fraud communities.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Flare
    dataTypes:
      - FireworkV2_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
relevantTechniques:
  - T1593
query: |
  FireworkV2_CL
  | where notempty(uid) and RiskScore >= 3
  | extend index_name = split(uid, "/")[0]
  | where index_name == "chat_message"
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

FireworkV2_CL

Stage 2: where

| where notempty(uid) and RiskScore >= 3

Stage 3: extend

| extend index_name = split(uid, "/")[0]

Stage 4: where

| where index_name == "chat_message"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RiskScorege
  • 3 transforms: number
field:"RiskScore" kind:ge value:"3"
index_nameeq
  • chat_message
field:"index_name" kind:eq value:"chat_message"
uidis_not_null
  • (no value, null check)
field:"uid" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
index_nameextend

HackerView - Any Issue Detected

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

Generic alert that triggers when ANY HackerView issue/incident is detected in the logs. Extracts nested metadata from RawPayload.

MITRE ATT&CK Coverage

Rule Body

id: abe1a663-d00d-482e-aa68-9394622ae03e
name: HackerView - Any Issue Detected
description: Generic alert that triggers when ANY HackerView issue/incident is detected in the logs. Extracts nested metadata from RawPayload.
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: CTM360HackerViewConnectorDefinition
    dataTypes:
      - HackerViewLog_AzureV2_CL
tactics:
- Reconnaissance
- Discovery
relevantTechniques:
- T1592
- T1598
query: |
  HackerViewLog
  | extend RawPayloadDyn = todynamic(RawPayload)
  | extend 
    AssetName = coalesce(AssetName, tostring(RawPayloadDyn.meta.asset), tostring(RawPayloadDyn.meta.host)),
    AssetType = coalesce(AssetType, tostring(RawPayloadDyn.meta.asset_type)),
    IpAddress = coalesce(IpAddress, tostring(RawPayloadDyn.meta.resolved_ip)),
    ExternalLink = coalesce(ExternalLink, tostring(RawPayloadDyn.hackerview_link))
  | project TimeGenerated, Title, Severity, AssetName, AssetType, IpAddress, ExternalLink
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  AssetName: AssetName
  IpAddress: IpAddress
  Severity: Severity
alertDetailsOverride:
  alertDisplayNameFormat: HackerView Alert - {{Title}}
  alertDescriptionFormat: HackerView detected {{Severity}} issue {{Title}} on asset {{AssetName}}
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: AssetName
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IpAddress
kind: Scheduled
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

HackerViewLog

Stage 2: extend

| extend RawPayloadDyn = todynamic(RawPayload)

Stage 3: extend

| extend 
  AssetName = coalesce(AssetName, tostring(RawPayloadDyn.meta.asset), tostring(RawPayloadDyn.meta.host)),
  AssetType = coalesce(AssetType, tostring(RawPayloadDyn.meta.asset_type)),
  IpAddress = coalesce(IpAddress, tostring(RawPayloadDyn.meta.resolved_ip)),
  ExternalLink = coalesce(ExternalLink, tostring(RawPayloadDyn.hackerview_link))

Stage 4: project

| project TimeGenerated, Title, Severity, AssetName, AssetType, IpAddress, ExternalLink

Output Fields

These fields are emitted when the rule matches.

FieldSource
AssetNameproject
AssetTypeproject
ExternalLinkproject
IpAddressproject
Severityproject
TimeGeneratedproject
Titleproject

Medium severity malicious activity detected

#
Status
available
Severity
medium
Time window
1d
Group by
SourceIp, ThreatCategory
Source
github.com/Azure/Azure-Sentinel

Identifies medium severity malicious activity in Azure Firewall IDPS logs.

MITRE ATT&CK Coverage

Rule Body

id: dfbe3963-42fb-4ebe-a00c-1cc44e2aa9f0
name: Medium severity malicious activity detected
description: |
  Identifies medium severity malicious activity in Azure Firewall IDPS logs.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureFirewall
    dataTypes:
      - AZFWIdpsSignature
queryFrequency: 1h
queryPeriod: 24h     
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - DefenseEvasion
  - Impact
relevantTechniques:
  - T1496
  - T1204
  - T1036
query: |
  let TimeWindow   = 90d;    
  let HitThreshold = 10;
  let MinSeverity  = 2;
  let EnableCategoryFilter    = true;
  let EnableDescriptionFilter = false;
  let EnableActionFilter      = false;
  let CategoriesOfInterest = dynamic([
      "Possibly Unwanted Program Detected",
      "Possible Social Engineering Attempted",
      "Crypto Currency Mining Activity Detected",
      "A suspicious filename was detected",
      "A system call was detected"
  ]);
  let DescriptionsOfInterest = dynamic([
      "pup-activity",
      "social-engineering",
      "coin-mining",
      "suspicious-filename-detect",
      "system-call-detect"
  ]);
  let MatchActions = dynamic(["Deny", "alert"]);
  AZFWIdpsSignature
  | where TimeGenerated >= ago(TimeWindow)
  | where Severity >= MinSeverity
  | where (EnableCategoryFilter == false) or (Category has_any (CategoriesOfInterest))
  | where (EnableDescriptionFilter == false) or (Description has_any (DescriptionsOfInterest))
  | where (EnableActionFilter == false) or (Action in~ (MatchActions))
  | summarize
      StartTime   = min(TimeGenerated),
      EndTime     = max(TimeGenerated),
      TotalHits   = count(),
      MaxSeverity = max(Severity),
      Actions     = make_set(Action, 5),
      Signatures  = make_set(SignatureId, 20),
      Description = make_set(substring(tostring(Description), 0, 120), 3)
      by SourceIp, ThreatCategory = Category
  | where TotalHits >= HitThreshold
  | project
      StartTime,
      EndTime,
      SourceIp,
      ThreatCategory,
      TotalHits,
      MaxSeverity,
      Actions,
      Signatures,
      Description
  | order by MaxSeverity desc, TotalHits desc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let TimeWindow = 90d;
let HitThreshold = 10;
let MinSeverity = 2;
let EnableCategoryFilter = true;
let EnableDescriptionFilter = false;
let EnableActionFilter = false;
let MatchActions = dynamic(["Deny", "alert"]);

Let Binding: CategoriesOfInterest used in Stage 4

let CategoriesOfInterest = dynamic([
    "Possibly Unwanted Program Detected",
    "Possible Social Engineering Attempted",
    "Crypto Currency Mining Activity Detected",
    "A suspicious filename was detected",
    "A system call was detected"
]);

Let Binding: DescriptionsOfInterest used in Stage 5

let DescriptionsOfInterest = dynamic([
    "pup-activity",
    "social-engineering",
    "coin-mining",
    "suspicious-filename-detect",
    "system-call-detect"
]);

Stage 1: source

AZFWIdpsSignature

Stage 2: where

| where TimeGenerated >= ago(TimeWindow)

Stage 3: where

| where Severity >= MinSeverity

Stage 4: where

| where (EnableCategoryFilter == false) or (Category has_any (CategoriesOfInterest))

Stage 5: where

| where (EnableDescriptionFilter == false) or (Description has_any (DescriptionsOfInterest))

Stage 6: where

| where (EnableActionFilter == false) or (Action in~ (MatchActions))

Stage 7: summarize

| summarize
    StartTime   = min(TimeGenerated),
    EndTime     = max(TimeGenerated),
    TotalHits   = count(),
    MaxSeverity = max(Severity),
    Actions     = make_set(Action, 5),
    Signatures  = make_set(SignatureId, 20),
    Description = make_set(substring(tostring(Description), 0, 120), 3)
    by SourceIp, ThreatCategory = Category

Stage 8: where

| where TotalHits >= HitThreshold

Stage 9: project

| project
    StartTime,
    EndTime,
    SourceIp,
    ThreatCategory,
    TotalHits,
    MaxSeverity,
    Actions,
    Signatures,
    Description

Stage 10: sort

| order by MaxSeverity desc, TotalHits desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categorymatch
  • A suspicious filename was detected transforms: term
  • A system call was detected transforms: term
  • Crypto Currency Mining Activity Detected transforms: term
  • Possible Social Engineering Attempted transforms: term
  • Possibly Unwanted Program Detected transforms: term
field:"Category" kind:match
Severityge
  • 2 transforms: number
field:"Severity" kind:ge value:"2"
TotalHitsge
  • 10 transforms: number
field:"TotalHits" kind:ge value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionsproject
Descriptionproject
EndTimeproject
MaxSeverityproject
Signaturesproject
SourceIpproject
StartTimeproject
ThreatCategoryproject
TotalHitsproject

Pathlock TDnR - ABAP Runtime Dumps

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

Detects ABAP runtime dumps (short dumps) in SAP, forwarded by Pathlock Threat Detection and Response. Abnormal dump activity may indicate exploitation attempts, injection of malformed inputs, or application-layer attacks targeting SAP business processes.

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c02
name: Pathlock TDnR - ABAP Runtime Dumps
kind: Scheduled
description: >-
  Detects ABAP runtime dumps (short dumps) in SAP, forwarded by Pathlock Threat Detection and Response.
  Abnormal dump activity may indicate exploitation attempts, injection of malformed inputs,
  or application-layer attacks targeting SAP business processes.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  Pathlock_TDnR_CL
  | where DataSource == "ABAP_DUMPS"
  | 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 == "ABAP_DUMPS"

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

Power Platform - Account added to privileged Microsoft Entra roles

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

Identifies changes to privileged directory roles impacting Power Platform: - Dynamics 365 Admins - Power Platform Admins - Fabric Admins

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 71d829d6-eb50-4a17-8a64-655fae8d71e1
kind: Scheduled
name: Power Platform - Account added to privileged Microsoft Entra roles
description: |
  Identifies changes to privileged directory roles impacting Power Platform:
  - Dynamics 365 Admins
  - Power Platform Admins
  - Fabric Admins
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078
  - T1068
  - T1548
query: |
  // 44367163-eba1-44c3-98af-f5787879f96a = Dynamics 365 Administrator
  // 11648597-926c-4cf3-9c36-bcebb0ba8dcc = Power Platform Administrator
  // a9ea8996-122f-4c74-9520-8edcd192826c = Fabric Administrator
  let query_frequency = 1h;
  let role_template_ids = dynamic(["44367163-eba1-44c3-98af-f5787879f96a", "11648597-926c-4cf3-9c36-bcebb0ba8dcc", "a9ea8996-122f-4c74-9520-8edcd192826c"]);
  let monitored_activities = dynamic(["Assign", "AssignGrantedRole", "AssignPermanentGrantedRole", "AssignPermanentEligibleRole", "RoleElevatedOutsidePimAlert"]);
  AuditLogs
  | where TimeGenerated >= ago(query_frequency)
  | where Category == "RoleManagement"
      and TargetResources has_any (role_template_ids)
      and AADOperationType in (monitored_activities)
      and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax"
  | extend
      UserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName),
      AadUserId = tostring(parse_json(tostring(InitiatedBy.user)).id),
      IPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
      RoleName = tostring(TargetResources[0].displayName),
      UserAdded = tostring(TargetResources[2].userPrincipalName)
  | extend
      RoleName = iif(isempty(RoleName), tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].newValue), RoleName),
      UserAdded = iif(isempty(UserAdded), tostring(parse_json(tostring(TargetResources[0].userPrincipalName))), UserAdded),
      CloudAppId = int(32780),
      AccountName = tostring(split(UserPrincipalName, '@')[0]),
      UPNSuffix = tostring(split(UserPrincipalName, '@')[1])
  | project
      TimeGenerated,
      Identity,
      UserPrincipalName,
      AadUserId,
      RoleName,
      OperationName,
      UserAdded,
      TargetResources,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
alertDetailsOverride:
  alertDisplayNameFormat: Power Platform - Account added to privileged role {{RoleName}}
  alertDescriptionFormat: 'A user {{UserAdded}} was added to one of the Power Platform
    administrative roles: {{{RoleName}}'
version: 3.2.1

Stages and Predicates

Parameters

let query_frequency = 1h;
let role_template_ids = dynamic(["44367163-eba1-44c3-98af-f5787879f96a", "11648597-926c-4cf3-9c36-bcebb0ba8dcc", "a9ea8996-122f-4c74-9520-8edcd192826c"]);
let monitored_activities = dynamic(["Assign", "AssignGrantedRole", "AssignPermanentGrantedRole", "AssignPermanentEligibleRole", "RoleElevatedOutsidePimAlert"]);

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated >= ago(query_frequency)

Stage 3: where

| where Category == "RoleManagement"
    and TargetResources has_any (role_template_ids)
    and AADOperationType in (monitored_activities)
    and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax"

Stage 4: extend

| extend
    UserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName),
    AadUserId = tostring(parse_json(tostring(InitiatedBy.user)).id),
    IPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
    RoleName = tostring(TargetResources[0].displayName),
    UserAdded = tostring(TargetResources[2].userPrincipalName)

Stage 5: extend

| extend
    RoleName = iif(isempty(RoleName), tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].newValue), RoleName),
    UserAdded = iif(isempty(UserAdded), tostring(parse_json(tostring(TargetResources[0].userPrincipalName))), UserAdded),
    CloudAppId = int(32780),
    AccountName = tostring(split(UserPrincipalName, '@')[0]),
    UPNSuffix = tostring(split(UserPrincipalName, '@')[1])

Stage 6: project

| project
    TimeGenerated,
    Identity,
    UserPrincipalName,
    AadUserId,
    RoleName,
    OperationName,
    UserAdded,
    TargetResources,
    CloudAppId,
    AccountName,
    UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AADOperationTypein
  • Assign corpus 6 (kusto 6)
  • AssignGrantedRole
  • AssignPermanentEligibleRole
  • AssignPermanentGrantedRole
  • RoleElevatedOutsidePimAlert
field:"AADOperationType" kind:in
Categoryeq
  • RoleManagement
field:"Category" kind:eq value:"RoleManagement"
Identityne
  • MS-PIM corpus 3 (kusto 3)
  • MS-PIM-Fairfax corpus 3 (kusto 3)
field:"Identity" kind:ne
TargetResourcesmatch
  • 11648597-926c-4cf3-9c36-bcebb0ba8dcc transforms: term
  • 44367163-eba1-44c3-98af-f5787879f96a transforms: term
  • a9ea8996-122f-4c74-9520-8edcd192826c transforms: term
field:"azure_ad::target_resources" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
AadUserIdproject
AccountNameproject
CloudAppIdproject
Identityproject
OperationNameproject
RoleNameproject
TargetResourcesproject
TimeGeneratedproject
UPNSuffixproject
UserAddedproject
UserPrincipalNameproject

Suspicious Service Principal creation activity

#
Status
available
Severity
low
Time window
1h
Group by
AppID
Source
github.com/Azure/Azure-Sentinel

This alert will detect creation of an SPN, permissions granted, credentials created, activity and deletion of the SPN in a time frame (default 10 minutes)

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Privilege Escalation
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 6852d9da-8015-4b95-8ecf-d9572ee0395d
name: Suspicious Service Principal creation activity
description: |
  'This alert will detect creation of an SPN, permissions granted, credentials created, activity and deletion of the SPN in a time frame (default 10 minutes)'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
      - AADServicePrincipalSignInLogs
queryFrequency: 1h
queryPeriod: 1h10m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - PrivilegeEscalation
  - InitialAccess
relevantTechniques:
  - T1078
  - T1528
query: |
  let queryfrequency = 1h;
  let wait_for_deletion = 10m;
  let account_created =
    AuditLogs 
    | where ActivityDisplayName == "Add service principal"
    | where Result == "success"
    | extend AppID = tostring(AdditionalDetails[1].value)
    | extend creationTime = ActivityDateTime
    | extend CreatorUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend CreatorIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress);
  let account_activity =
    AADServicePrincipalSignInLogs
    | extend Activities = pack("ActivityTime", TimeGenerated ,"IpAddress", IPAddress, "ResourceDisplayName", ResourceDisplayName)
    | extend AppID = AppId
    | summarize make_list(Activities) by AppID;
  let account_deleted =
    AuditLogs 
    | where OperationName == "Remove service principal"
    | where Result == "success"
    | extend AppID = tostring(AdditionalDetails[1].value)
    | extend deletionTime = ActivityDateTime
    | extend DeleterUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend DeleterIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress);
  let account_credentials =
    AuditLogs
    | where OperationName has_all ("Update application", "Certificates and secrets management")
    | where Result == "success"
    | extend AppID = tostring(AdditionalDetails[1].value)
    | extend credentialCreationTime = ActivityDateTime;
  let roles_assigned =
    AuditLogs
    | where ActivityDisplayName == "Add app role assignment to service principal"
    | extend AppID = tostring(TargetResources[1].displayName)
    | extend AssignedRole =  iff(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].displayName)=="AppRole.Value", tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].newValue))),"")
    | extend AssignedRoles = pack("Role", AssignedRole)
    | summarize make_list(AssignedRoles) by AppID;
  account_created
  | where TimeGenerated between (ago(wait_for_deletion+queryfrequency)..ago(wait_for_deletion))
  | join kind= inner (account_activity) on AppID
  | join kind= inner (account_deleted) on AppID
  | join kind= inner (account_credentials) on AppID
  | join kind= inner (roles_assigned) on AppID
  | where deletionTime - creationTime between (time(0s)..wait_for_deletion)
  | extend AliveTime = deletionTime - creationTime
  | project AADTenantId, AppID, creationTime, deletionTime, CreatorUserPrincipalName, DeleterUserPrincipalName, CreatorIPAddress, DeleterIPAddress, list_Activities, list_AssignedRoles, AliveTime
  | extend CreatorName = tostring(split(CreatorUserPrincipalName, "@")[0]), CreatorUPNSuffix = tostring(split(CreatorUserPrincipalName, "@")[1])
  | extend DeleterName = tostring(split(DeleterUserPrincipalName, "@")[0]), DeleterSuffix = tostring(split(DeleterUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CreatorUserPrincipalName
      - identifier: Name
        columnName: CreatorName
      - identifier: UPNSuffix
        columnName: CreatorUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: DeleterUserPrincipalName
      - identifier: Name
        columnName: DeleterName
      - identifier: UPNSuffix
        columnName: DeleterSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CreatorIPAddress
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DeleterIPAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let queryfrequency = 1h;
let wait_for_deletion = 10m;

let account_created is inlined into the numbered stages below.

Let Binding: account_activity used in Stage 6

let account_activity = AADServicePrincipalSignInLogs
  | extend Activities = pack("ActivityTime", TimeGenerated ,"IpAddress", IPAddress, "ResourceDisplayName", ResourceDisplayName)
  | extend AppID = AppId
  | summarize make_list(Activities) by AppID;

Let Binding: account_deleted used in Stage 7

let account_deleted = AuditLogs 
  | where OperationName == "Remove service principal"
  | where Result == "success"
  | extend AppID = tostring(AdditionalDetails[1].value)
  | extend deletionTime = ActivityDateTime
  | extend DeleterUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend DeleterIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress);

Let Binding: account_credentials used in Stage 8

let account_credentials = AuditLogs
  | where OperationName has_all ("Update application", "Certificates and secrets management")
  | where Result == "success"
  | extend AppID = tostring(AdditionalDetails[1].value)
  | extend credentialCreationTime = ActivityDateTime;

Let Binding: roles_assigned used in Stage 9

let roles_assigned = AuditLogs
  | where ActivityDisplayName == "Add app role assignment to service principal"
  | extend AppID = tostring(TargetResources[1].displayName)
  | extend AssignedRole =  iff(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].displayName)=="AppRole.Value", tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].newValue))),"")
  | extend AssignedRoles = pack("Role", AssignedRole)
  | summarize make_list(AssignedRoles) by AppID;

Stages 1 to 4 define let account_created as the rule's main pipeline source. Stages 5 to 16 use it.

Stage 1: source

AuditLogs

Stage 2: where

| where ActivityDisplayName == "Add service principal"

Stage 3: where

| where Result == "success"

Stage 4: extend (4 consecutive steps)

| extend AppID = tostring(AdditionalDetails[1].value)
| extend creationTime = ActivityDateTime
| extend CreatorUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend CreatorIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)

Stage 5: where

account_created
| where TimeGenerated between (ago(wait_for_deletion+queryfrequency)..ago(wait_for_deletion))

Stage 6: join

| join kind= inner (account_activity) on AppID

Stage 7: join

| join kind= inner (account_deleted) on AppID

Stage 8: join

| join kind= inner (account_credentials) on AppID

Stage 9: join

| join kind= inner (roles_assigned) on AppID

Stage 10: where

where /* macro: ((deletionTime - creationTime) between (time(0s) .. 10m)) */

Stage 11: extend

extend AliveTime

Stage 12: project

project AADTenantId, AliveTime, AppID, CreatorIPAddress, CreatorUserPrincipalName, DeleterIPAddress, DeleterUserPrincipalName, creationTime, deletionTime, list_Activities, list_AssignedRoles

Stage 13: extend

extend CreatorName, CreatorUPNSuffix

Stage 14: extend

extend DeleterName, DeleterSuffix

Stage 15: summarize aggregation inside the join branch

summarize by AppID

Stage 16: summarize aggregation inside the join branch

summarize by AppID

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityDisplayNameeq
  • Add app role assignment to service principal
  • Add service principal
field:"azure_ad::activity_display_name" kind:eq
OperationNameeq
  • Remove service principal
field:"OperationName" kind:eq value:"Remove service principal"
OperationNamematch
  • Certificates and secrets management transforms: term
  • Update application transforms: term
field:"OperationName" kind:match
Resulteq
  • success corpus 4 (kusto 4)
field:"Result" kind:eq value:"success"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppIDsummarize

Tanium Threat Response 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.

Severity
high
Time window
6m
Source
github.com/Azure/Azure-Sentinel

Alerts from Tanium Threat Response (THR) that can be acted upon by Microsoft Sentinel Playbook

Rule Body

id: dd9aa0ff-7ac1-4448-879c-e1a18d5890b4
name: Tanium Threat Response Alerts
description: 'Alerts from Tanium Threat Response (THR) that can be acted upon by Microsoft Sentinel Playbook'
severity: High
queryFrequency: 5m
queryPeriod: 6m
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
requiredDataConnectors: []
query: |
  let cap = (s:string) { strcat(toupper(substring(s,0,1)), substring(s,1))  };
  TaniumThreatResponse_CL
  | extend TaniumUrl = pack("computer_name", Computer_Name_s, "alert_guid", Alert_Id_g, "ip_address", Computer_IP_s, "platform", Match_Details_finding_system_info_platform_s)
  | extend TaniumTHRLabel = strcat(cap(Intel_Type_s)," - ", cap(Intel_Name_s), " - ", cap(Match_Details_match_type_s))
  | where Computer_IP_s !contains "N/A"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: TaniumUrl
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Computer_IP_s
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer_Name_s
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: TaniumTHRLabel
customDetails:
  IntelId: Intel_Id_d
  TaniumAlertId: Alert_Id_g
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "{{TaniumTHRLabel}}"
  alertDescriptionFormat: "Alert from Tanium Threat Response. GUID: {{Alert_Id_g}}; Computer Name: {{Computer_Name_s}}; IP: {{Computer_IP_s}}"
version: 1.2.0
kind: Scheduled

Stages and Predicates

Parameters

let cap = (s:string) { strcat(toupper(substring(s,0,1)), substring(s,1))  };

Stage 1: source

TaniumThreatResponse_CL

Stage 2: extend

| extend TaniumUrl = pack("computer_name", Computer_Name_s, "alert_guid", Alert_Id_g, "ip_address", Computer_IP_s, "platform", Match_Details_finding_system_info_platform_s)

Stage 3: extend

| extend TaniumTHRLabel = strcat(cap(Intel_Type_s)," - ", cap(Intel_Name_s), " - ", cap(Match_Details_match_type_s))

Stage 4: where

| where Computer_IP_s !contains "N/A"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Computer_IP_scontainsN/Aexcludes:Computer_IP_s field:"Computer_IP_s" value:"N/A"

Output Fields

These fields are emitted when the rule matches.

FieldSource
TaniumUrlextend
TaniumTHRLabelextend

Theom - Dev secrets exposed

#

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
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

"Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0012 (Theom has observed developer secrets in a data store that is publicly exposed. As per this requirement, use this information to apply data access control lists or access permissions to secure your data)"

MITRE ATT&CK Coverage

Rule Body

id: 65200844-e161-47a7-a103-f61f7e3afe30
name: Theom - Dev secrets exposed
description: |
   "Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0012 (Theom has observed developer secrets in a data store that is publicly exposed. As per this requirement, use this information to apply data access control lists or access permissions to secure your data)"
severity: High
status : Available
requiredDataConnectors: 
  - connectorId: Theom
    dataTypes:
      - TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1213
  - T1530
query: |
   TheomAlerts_CL
     | where customProps_RuleId_s == "TRIS0012" and (priority_s == "P1" or priority_s == "P2")
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nPlease investigate further on Theom UI at {{deepLink_s}}\n"
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: customProps_AssetName_s 
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: deepLink_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

TheomAlerts_CL

Stage 2: where

| where customProps_RuleId_s == "TRIS0012" and (priority_s == "P1" or priority_s == "P2")

Indicators

These rows show field, operator, and value matches.

TI map Domain entity to DnsEvents

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

Identifies a match in DnsEvents from any Domain IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 85aca4d1-5d15-4001-abd9-acb86ca1786a
name: TI map Domain entity to DnsEvents
description: |
  Identifies a match in DnsEvents from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: DNS
    dataTypes:
      - DnsEvents
  - 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: |
  // Define the lookback periods for time-based filters
  let dt_lookBack = 1h; // Look back 1 hour for DNS events
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to domains
  let Domain_Indicators = ThreatIntelligenceIndicator
    // Filter out indicators without domain names
    | where isnotempty(DomainName)
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now()
    | extend TI_DomainEntity = DomainName;
  // Create a list of TLDs in our threat feed for later validation
  let maxListSize = 100000; // Define the maximum allowed size for each list
  let list_tlds = Domain_Indicators
    | extend parts = split(DomainName, '.')
    | extend tld = parts[(array_length(parts)-1)]
    | summarize count() by tostring(tld)
    | project tld
    | summarize make_list(tld, maxListSize);
  // Perform a join between domain indicators and DNS events to identify potential malicious activity
  Domain_Indicators
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
      DnsEvents
      | where TimeGenerated > ago(dt_lookBack)
      // Extract domain patterns from syslog message
      | where isnotempty(Name)
      | extend parts = split(Name, '.')
      | extend tld = parts[(array_length(parts)-1)]
      // Validate parsed domain by checking if the TLD is in the list of TLDs in our threat feed
      | where tld in~ (list_tlds)
      | extend DNS_TimeGenerated = TimeGenerated
    ) on $left.TI_DomainEntity==$right.Name
    // Filter out DNS events that occurred after the expiration of the corresponding indicator
    | where DNS_TimeGenerated < ExpirationDateTime
    // Group the results by IndicatorId and Name, and keep the DNS event with the latest timestamp
    | summarize DNS_TimeGenerated = arg_max(DNS_TimeGenerated, *) by IndicatorId, Name
    // Select the desired output fields
    | project DNS_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, Computer, ClientIP, Name, QueryType, Type, TI_DomainEntity
    // Extract hostname and DNS domain from the Computer field
    | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
    // Rename the timestamp field
    | extend timestamp = DNS_TimeGenerated
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain 
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.3
kind: Scheduled

Stages and Predicates

Parameters

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

let Domain_Indicators is inlined into the numbered stages below.

Let Binding: list_tlds used in Stage 7

let list_tlds = Domain_Indicators
  | extend parts = split(DomainName, '.')
  | extend tld = parts[(array_length(parts)-1)]
  | summarize count() by tostring(tld)
  | project tld
  | summarize make_list(tld, maxListSize);

Stages 1 to 6 define let Domain_Indicators as the rule's main pipeline source. Stages 7 to 12 use it.

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(DomainName)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 5: where

| where Active == true and ExpirationDateTime > now()

Stage 6: extend

| extend TI_DomainEntity = DomainName

Stage 7: join

Domain_Indicators
| join kind=innerunique (
    DnsEvents
    | where TimeGenerated > ago(dt_lookBack)
    | where isnotempty(Name)
    | extend parts = split(Name, '.')
    | extend tld = parts[(array_length(parts)-1)]
    | where tld in~ (list_tlds)
    | extend DNS_TimeGenerated = TimeGenerated
  ) on $left.TI_DomainEntity==$right.Name

Stage 8: where

where DNS_TimeGenerated < ExpirationDateTime

Stage 9: summarize

summarize DNS_TimeGenerated by IndicatorId, Name

Stage 10: project

project ActivityGroupNames, ClientIP, Computer, ConfidenceScore, DNS_TimeGenerated, Description, ExpirationDateTime, IndicatorId, Name, QueryType, TI_DomainEntity, ThreatType, Type, Url

Stage 11: extend

extend DnsDomain, HostName

Stage 12: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ClientIPproject
Computerproject
ConfidenceScoreproject
DNS_TimeGeneratedproject
Descriptionproject
ExpirationDateTimeproject
IndicatorIdproject
Nameproject
QueryTypeproject
TI_DomainEntityproject
ThreatTypeproject
Typeproject
Urlproject
DnsDomainextend
HostNameextend
timestampextend

TI Map IP Entity to Azure SQL Security Audit Events

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

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SQL Security Audit Events.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 239d987e-ee1b-4c49-b146-e88d682930a4
name: TI Map IP Entity to Azure SQL Security Audit Events
description: |
  This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SQL Security Audit Events.
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for AzureDiagnostics logs
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelIndicators
  //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 TimeGenerated >= ago(ioc_lookBack)
       | extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
       | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
       | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil));
  // Perform a join between IP indicators and AzureDiagnostics logs for SQL Security Audit events
  IP_Indicators
     | project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        AzureDiagnostics
        | where TimeGenerated >= ago(dt_lookBack)
        | where ResourceProvider == 'MICROSOFT.SQL'
        | where Category == 'SQLSecurityAuditEvents'
        | extend SQLSecurityAuditEvents_TimeGenerated = TimeGenerated
        | extend ClientIP = column_ifexists("client_ip_s", "Not Available")
        | extend Action = column_ifexists("action_name_s", "Not Available")
        | extend Application = column_ifexists("application_name_s", "Not Available")
        | extend HostName = column_ifexists("host_name_s", "Not Available")
    )
    on $left.TI_ipEntity == $right.ClientIP
    // Filter out logs that occurred after the expiration of the corresponding indicator
    | where SQLSecurityAuditEvents_TimeGenerated < ValidUntil
    // Group the results by IndicatorId and ClientIP, and keep the log entry with the latest timestamp
    | summarize SQLSecurityAuditEvents_TimeGenerated = arg_max(SQLSecurityAuditEvents_TimeGenerated, *) by Id, ClientIP
    // Select the desired output fields
    | extend Description = tostring(parse_json(Data).description)
    | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | project SQLSecurityAuditEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
      TI_ipEntity, ResourceId, ClientIP, Action, Application, HostName,  NetworkSourceIP, Type
    // Rename the timestamp field
    | extend timestamp = SQLSecurityAuditEvents_TimeGenerated
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
version: 1.3.5
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

Stages 1 to 11 define let IP_Indicators as the rule's main pipeline source. Stages 12 to 19 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

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

Stage 4: extend

| extend NetworkSourceIP = toupper(ObservableValue)

Stage 5: extend

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

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: extend

| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
TI_ipEntity =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: extend

| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =
ifisempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIP
elseTI_ipEntity

Stage 9: where

| where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 10: summarize

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

Stage 11: where

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

Stage 12: project-reorder

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

Stage 13: join

| join kind=innerunique (
      AzureDiagnostics
      | where TimeGenerated >= ago(dt_lookBack)
      | where ResourceProvider == 'MICROSOFT.SQL'
      | where Category == 'SQLSecurityAuditEvents'
      | extend SQLSecurityAuditEvents_TimeGenerated = TimeGenerated
      | extend ClientIP = column_ifexists("client_ip_s", "Not Available")
      | extend Action = column_ifexists("action_name_s", "Not Available")
      | extend Application = column_ifexists("application_name_s", "Not Available")
      | extend HostName = column_ifexists("host_name_s", "Not Available")
  )
  on $left.TI_ipEntity == $right.ClientIP

Stage 14: where

where SQLSecurityAuditEvents_TimeGenerated < ValidUntil

Stage 15: summarize

summarize SQLSecurityAuditEvents_TimeGenerated by Id, ClientIP

Stage 16: extend

extend Description

Stage 17: extend

extend ActivityGroupNames

Stage 18: project

project Action, ActivityGroupNames, Application, ClientIP, Confidence, Description, HostName, Id, NetworkSourceIP, ResourceId, SQLSecurityAuditEvents_TimeGenerated, TI_ipEntity, Type, ValidUntil

Stage 19: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ActivityGroupNamesproject
Applicationproject
ClientIPproject
Confidenceproject
Descriptionproject
HostNameproject
Idproject
NetworkSourceIPproject
ResourceIdproject
SQLSecurityAuditEvents_TimeGeneratedproject
TI_ipEntityproject
Typeproject
ValidUntilproject
timestampextend

Vaikora - High score quarantine

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

Identifies high-confidence phishing or suspected-threat quarantine events from Vaikora for O365. Fires when the classifier returns action_id 4 (Suspected) or 5 (Phishing) with confidence above 0.8 on a Microsoft 365 mailbox.

MITRE ATT&CK Coverage

Rule Body

id: 017031f6-be1d-4c68-b4d3-182fa84378cc
name: Vaikora - High score quarantine
description: |
  Identifies high-confidence phishing or suspected-threat quarantine events from Vaikora for O365. Fires when the classifier returns action_id 4 (Suspected) or 5 (Phishing) with confidence above 0.8 on a Microsoft 365 mailbox.
severity: High
requiredDataConnectors:
  - connectorId: VaikoraO365
    dataTypes:
      - VaikoraO365_Quarantine_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - CredentialAccess
  - Collection
  - Exfiltration
relevantTechniques:
  - T1566
  - T1566.001
  - T1566.002
query: |
  VaikoraO365_Quarantine_CL
  | where TimeGenerated >= ago(15m)
  | where ActionId_d in (4, 5)
  | where Confidence_d >= 0.8
  | extend
      EmailId          = EmailId_s,
      TenantId         = TenantId_s,
      ActionId         = ActionId_d,
      ActionName       = ActionName_s,
      Confidence       = Confidence_d,
      RiskScore        = RiskScore_d,
      SubCategory      = SubCategory_s,
      Reasoning        = Reasoning_s,
      SourceMailbox    = SourceMailbox_s,
      SenderAddress    = SenderAddress_s,
      SenderDomain     = SenderDomain_s,
      Subject          = Subject_s,
      RecipientAddress = RecipientAddress_s,
      ModelVersion     = ModelVersion_s
  | project
      TimeGenerated, EmailId, TenantId, ActionId, ActionName, Confidence, RiskScore,
      SubCategory, Reasoning, SourceMailbox, SenderAddress, SenderDomain, Subject,
      RecipientAddress, ModelVersion
  | order by TimeGenerated desc
entityMappings:
  - entityType: MailMessage
    fieldMappings:
      - identifier: Recipient
        columnName: RecipientAddress
      - identifier: NetworkMessageId
        columnName: EmailId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SenderAddress
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: RecipientAddress
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: SenderDomain
alertDetailsOverride:
  alertDisplayNameFormat: "Vaikora-O365 {{ActionName}} on {{RecipientAddress}}"
  alertDescriptionFormat: "Vaikora-O365 classified this message as {{ActionName}} with confidence {{Confidence}}. The message was quarantined from the recipient mailbox."
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

VaikoraO365_Quarantine_CL

Stage 2: where

| where TimeGenerated >= ago(15m)

Stage 3: where

| where ActionId_d in (4, 5)

Stage 4: where

| where Confidence_d >= 0.8

Stage 5: extend

| extend
    EmailId          = EmailId_s,
    TenantId         = TenantId_s,
    ActionId         = ActionId_d,
    ActionName       = ActionName_s,
    Confidence       = Confidence_d,
    RiskScore        = RiskScore_d,
    SubCategory      = SubCategory_s,
    Reasoning        = Reasoning_s,
    SourceMailbox    = SourceMailbox_s,
    SenderAddress    = SenderAddress_s,
    SenderDomain     = SenderDomain_s,
    Subject          = Subject_s,
    RecipientAddress = RecipientAddress_s,
    ModelVersion     = ModelVersion_s

Stage 6: project

| project
    TimeGenerated, EmailId, TenantId, ActionId, ActionName, Confidence, RiskScore,
    SubCategory, Reasoning, SourceMailbox, SenderAddress, SenderDomain, Subject,
    RecipientAddress, ModelVersion

Stage 7: sort

| order by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionIdproject
ActionNameproject
Confidenceproject
EmailIdproject
ModelVersionproject
Reasoningproject
RecipientAddressproject
RiskScoreproject
SenderAddressproject
SenderDomainproject
SourceMailboxproject
SubCategoryproject
Subjectproject
TenantIdproject
TimeGeneratedproject

ZeroFox Alerts - Low 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
low
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'Detects low severity alerts from ZeroFox'

MITRE ATT&CK Coverage

TacticTechniques
Resource Development
Initial Access

Rule Body

id: e0c7a91a-7aa1-498a-9c20-cd6c721f9345
name: ZeroFox Alerts - Low Severity Alerts
description: |
  'Detects low severity alerts from ZeroFox'
severity: Low
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 (3)
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 (3)

Indicators

These rows show field, operator, and value matches.

Back to all Kusto rules