Detection rules › Kusto
Kusto rules: 15
1Password - Disable MFA factor or type for all user accounts
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment | |
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| 1Password | account-disblmfa: Disable Multi-Factor Authentication For All Users |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"disblmfa" |
log_source | eq |
| field:"log_source" kind:eq value:"auditevents" |
object_type | eq |
| field:"object_type" kind:eq value:"account" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUsername | extend |
SrcIpAddr | extend |
AFD WAF - Code Injection
#'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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Privilege Escalation |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"FrontDoorWebApplicationFirewallLog" |
Total_TrackingReference | ge |
| field:"Total_TrackingReference" kind:ge value:"3" |
action_s | eq |
| field:"action_s" kind:eq |
details_msg_s | match |
| field:"details_msg_s" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Detail_Data | summarize |
Detail_Message | summarize |
EndTime | summarize |
Matched_Data | summarize |
StartTime | summarize |
Table | summarize |
Total_TrackingReference | summarize |
TrackingReference | summarize |
URI_s | summarize |
action_s | summarize |
clientIP_s | summarize |
host_s | summarize |
Azure DevOps Service Connection Abuse
#'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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Impact |
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
CurrentCount | gt |
| field:"CurrentCount" kind:gt value:"4" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Library.ServiceConnectionExecuted" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUPN | summarize |
ConnectionNames | summarize |
CurrentCount | summarize |
EndTime | summarize |
IpAddress | summarize |
OrganizationName | summarize |
ProjectId | summarize |
ProjectName | summarize |
StartTime | summarize |
link | extend |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| 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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0PSRemote" |
Cisco - firewall block but success logon to Microsoft Entra ID
#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
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Detect non-admin requesting token for admin applications (Kusto)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceAction | eq |
| field:"DeviceAction" kind:eq value:"denied" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Cisco" |
ResultType | in |
| field:"ResultType" kind:in |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SourceIP | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:SourceIP |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SourceIP | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
Cisco Duo - Authentication device new location
#'Detects new location of authentication device.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
AuthDeviceCountry | is_not_null | field:"AuthDeviceCountry" kind:is_not_null | |
Result | eq |
| field:"Result" kind:eq value:"success" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccessDeviceIp | summarize |
src_c | summarize |
AccountCustomEntity | extend |
IPCustomEntity | extend |
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.
'Display incidents in which medium sensitive financial information was placed at risk by user sharing.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity | eq |
| field:"Severity" kind:eq value:"2" |
informationType_s | eq |
| field:"informationType_s" kind:eq value:"Financial Information" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Correlate Unfamiliar sign-in properties & atypical travel alerts
#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
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | IdentityInfo action any: Identity information |
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
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 =(CompromisedEntity == "N/A" or isempty(CompromisedEntity))UserAccountCompromisedEntityStage 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.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountDisplayName | is_not_null | field:"AccountDisplayName" kind:is_not_null | |
AlertName | eq |
| field:"AlertName" kind:eq |
Entity.Type | eq |
| field:"Entity.Type" kind:eq value:"account" |
ProductName | eq |
| field:"ProductName" kind:eq value:"Azure Active Directory Identity Protection" |
RiskDetail | is_null | field:"RiskDetail" kind:is_null | |
UserAccount | is_not_null | field:"UserAccount" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AadUserId | project |
Alert_AtypicalTravels_Name | project |
Alert_AtypicalTravels_Severity | project |
Alert_AtypicalTravels_Time | project |
Alert_UnfamiliarSignInProps_Name | project |
Alert_UnfamiliarSignInProps_Severity | project |
Alert_UnfamiliarSignInProps_Time | project |
Comments | project |
CurrentIPAddress | project |
CurrentLocation | project |
PreviousIPAddress | project |
PreviousLocation | project |
TimeDelta | project |
UserAccount | project |
UserName | project |
UPNSuffix | extend |
UserEmailName | extend |
Creation of expensive computes in Azure
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityStatusValue | starts_with |
| field:"ActivityStatusValue" kind:starts_with value:"Accept" |
OperationNameValue | in |
| field:"azure_ad::operation_name_value" kind:in |
Properties | match |
| field:"Properties" kind:match value:"vmSize" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityStatusValue | project |
Caller | project |
CallerIpAddress | project |
ComputerName | project |
OperationNameValue | project |
TimeGenerated | project |
vmSize | project |
Name | extend |
UPNSuffix | extend |
CyberArkEPM - Possible execution of Powershell Empire
#'Detects possible execution of Powershell Empire.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActingProcessCommandLine | match |
| field:"ParentCommandLine" kind:match |
EventSubType | ne |
| field:"EventSubType" kind:ne value:"AttackAttempt" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Detect device token stealing with WDAC
#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
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceEvents action any: Defender event |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | starts_with |
| field:"ActionType" kind:starts_with value:"AppControl" |
FileName | eq |
| field:"file_name" kind:eq value:"MicrosoftAccountTokenProvider.dll" |
GlobalPrevalence | lt |
| field:"GlobalPrevalence" kind:lt value:"250" |
Detect instances of multiple server errors occurring within a brief period of time (ASIM Web Session)
#'This detection mechanism identifies situations where multiple server errors originate from a single source within a limited time frame.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Impact |
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.
| Field | Source |
|---|---|
EventEndTime | summarize |
EventResultDetailsSet | summarize |
EventStartTime | summarize |
SrcHostname | summarize |
SrcIpAddr | summarize |
SrcUsername | summarize |
TotalErrorCount | summarize |
URLs | summarize |
Name | extend |
Threshold | extend |
UPNSuffix | extend |
Excessive Login Attempts (Microsoft Defender for IoT)
#'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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertName | in |
| field:"AlertName" kind:in |
ExtendedProperties.isNew | eq |
| field:"ExtendedProperties.isNew" kind:eq value:"True" |
ProviderName | eq |
| field:"ProviderName" kind:eq value:"IoTSecurity" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertLink | project |
AlertManagementUri | project |
AlertName | project |
AlertSeverity | project |
Description | project |
DestDeviceAddress | project |
DeviceId | project |
Entities | project |
ProductComponentName | project |
ProductName | project |
Protocol | project |
RemediationSteps | project |
SourceDeviceAddress | project |
Tactics | project |
Techniques | project |
TimeGenerated | project |
VendorOriginalId | project |
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.
'This query searches for conversations and posts from real-time messaging environments used by threat actors and fraud communities.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
RiskScore | ge |
| field:"RiskScore" kind:ge value:"3" |
index_name | eq |
| field:"index_name" kind:eq value:"chat_message" |
uid | is_not_null | field:"uid" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
index_name | extend |
HackerView - Any Issue Detected
#Generic alert that triggers when ANY HackerView issue/incident is detected in the logs. Extracts nested metadata from RawPayload.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
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.
| Field | Source |
|---|---|
AssetName | project |
AssetType | project |
ExternalLink | project |
IpAddress | project |
Severity | project |
TimeGenerated | project |
Title | project |
Medium severity malicious activity detected
#Identifies medium severity malicious activity in Azure Firewall IDPS logs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Stealth | |
| Impact |
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
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | match |
| field:"Category" kind:match |
Severity | ge |
| field:"Severity" kind:ge value:"2" |
TotalHits | ge |
| field:"TotalHits" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Actions | project |
Description | project |
EndTime | project |
MaxSeverity | project |
Signatures | project |
SourceIp | project |
StartTime | project |
ThreatCategory | project |
TotalHits | project |
Pathlock TDnR - ABAP Runtime Dumps
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"ABAP_DUMPS" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
Power Platform - Account added to privileged Microsoft Entra roles
#Identifies changes to privileged directory roles impacting Power Platform: - Dynamics 365 Admins - Power Platform Admins - Fabric Admins
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
AADOperationType | in |
| field:"AADOperationType" kind:in |
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
Identity | ne |
| field:"Identity" kind:ne |
TargetResources | match |
| field:"azure_ad::target_resources" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AadUserId | project |
AccountName | project |
CloudAppId | project |
Identity | project |
OperationName | project |
RoleName | project |
TargetResources | project |
TimeGenerated | project |
UPNSuffix | project |
UserAdded | project |
UserPrincipalName | project |
Suspicious Service Principal creation activity
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Privilege Escalation | |
| Credential Access |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
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
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
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
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | eq |
| field:"azure_ad::activity_display_name" kind:eq |
OperationName | eq |
| field:"OperationName" kind:eq value:"Remove service principal" |
OperationName | match |
| field:"OperationName" kind:match |
Result | eq |
| field:"Result" kind:eq value:"success" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppID | summarize |
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.
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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Computer_IP_s | contains | N/A | excludes:Computer_IP_s field:"Computer_IP_s" value:"N/A" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
TaniumUrl | extend |
TaniumTHRLabel | extend |
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.
"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
| Tactic | Techniques |
|---|---|
| Collection |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
customProps_RuleId_s | eq |
| field:"customProps_RuleId_s" kind:eq value:"TRIS0012" |
priority_s | eq |
| field:"priority_s" kind:eq |
TI map Domain entity to DnsEvents
#Identifies a match in DnsEvents from any Domain IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
DomainName | is_not_null | field:"DomainName" kind:is_not_null | |
Name | is_not_null | field:"Name" kind:is_not_null | |
tld | in |
| field:"tld" kind:in value:"list_tlds" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ClientIP | project |
Computer | project |
ConfidenceScore | project |
DNS_TimeGenerated | project |
Description | project |
ExpirationDateTime | project |
IndicatorId | project |
Name | project |
QueryType | project |
TI_DomainEntity | project |
ThreatType | project |
Type | project |
Url | project |
DnsDomain | extend |
HostName | extend |
timestamp | extend |
TI Map IP Entity to Azure SQL Security Audit Events
#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
| Tactic | Techniques |
|---|---|
| Command & Control |
Rules Detecting the Same Action
These rules filter on the same operation.
- Affected rows stateful anomaly on database (Kusto)
- Credential errors stateful anomaly on database (Kusto)
- Drop attempts stateful anomaly on database (Kusto)
- Execution attempts stateful anomaly on database (Kusto)
- Firewall errors stateful anomaly on database (Kusto)
- Firewall rule manipulation attempts stateful anomaly on database (Kusto)
- OLE object manipulation attempts stateful anomaly on database (Kusto)
- Outgoing connection attempts stateful anomaly on database (Kusto)
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 =isnotempty(NetworkSourceIP)NetworkSourceIPNetworkSourceIPStage 8: extend
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =isempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIPTI_ipEntityStage 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"SQLSecurityAuditEvents" |
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ResourceProvider | eq |
| field:"ResourceProvider" kind:eq value:"MICROSOFT.SQL" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
ActivityGroupNames | project |
Application | project |
ClientIP | project |
Confidence | project |
Description | project |
HostName | project |
Id | project |
NetworkSourceIP | project |
ResourceId | project |
SQLSecurityAuditEvents_TimeGenerated | project |
TI_ipEntity | project |
Type | project |
ValidUntil | project |
timestamp | extend |
Vaikora - High score quarantine
#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
| Tactic | Techniques |
|---|---|
| Initial Access |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionId_d | in |
| field:"ActionId_d" kind:in |
Confidence_d | ge |
| field:"Confidence_d" kind:ge value:"0.8" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActionId | project |
ActionName | project |
Confidence | project |
EmailId | project |
ModelVersion | project |
Reasoning | project |
RecipientAddress | project |
RiskScore | project |
SenderAddress | project |
SenderDomain | project |
SourceMailbox | project |
SubCategory | project |
Subject | project |
TenantId | project |
TimeGenerated | project |
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.
'Detects low severity alerts from ZeroFox'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity | in |
| field:"Severity" kind:in value:"3" |