Detection rules › Kusto

Kusto rules: 0r

AFD WAF - Path Traversal Attack

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

'Identifies a match for a Path Traversal 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/Path_Traversal'

MITRE ATT&CK Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: a4d99328-e4e6-493d-b0d5-57e6f9ddae77
name: AFD WAF - Path Traversal Attack 
description: | 
  'Identifies a match for a Path Traversal 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/Path_Traversal'
severity: High 
status: Available 
requiredDataConnectors: 
  - connectorId: WAF
    dataTypes: 
      - AzureDiagnostics
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
tactics: 
  - DefenseEvasion
  - Execution
  - InitialAccess
  - PrivilegeEscalation
  - Discovery
relevantTechniques:
  - T1548
  - T1203
  - T1190
  - T1548
  - T1087
tags:
  - Path Traversal
query:  | 
 let Threshold = 3;
 AzureDiagnostics
 | where Category =~ "FrontDoorWebApplicationFirewallLog"
 | where action_s =~ "AnomalyScoring"
 | where details_msg_s has "Path Traversal Attack"
 | 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 "Path Traversal Attack"

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.

Output Fields

These fields are emitted when the rule matches.

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

API - Account Takeover

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

'42Crunch API protection against account takeover'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access
Discovery

Rule Body

id: 25c86f99-0a91-4b7f-88f3-599a008e5ab8
name: API - Account Takeover
description: |
  '42Crunch API protection against account takeover'
severity: High
requiredDataConnectors:
  - connectorId: 42CrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
  - connectorId: FortyTwoCrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: SingleAlert
status: Available
tactics:
  - CredentialAccess
  - Discovery
relevantTechniques:
  - T1110
  - T1087
query: |
  let firstRec = FortyTwoCrunchAPIProtection
  | where TimeGenerated >= ago(5m) 
  | project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
  | where (UriPath has "/api/register" or UriPath has "/api/reset") and Status == 403;
  let recCount = iff((toscalar(firstRec | count) >= 20), 1, 0);
  firstRec | take recCount
customDetails:
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InstanceName
version: 3.0.1
kind: Scheduled

Stages and Predicates

let firstRec is inlined into the numbered stages below.

Let Binding: recCount used in Stage 5

let recCount = iff((toscalar(firstRec | count) >= 20), 1, 0);

Stages 1 to 4 define let firstRec as the rule's main pipeline source. Stage 5 uses it.

Stage 1: source

FortyTwoCrunchAPIProtection

Stage 2: where

| where TimeGenerated >= ago(5m)

Stage 3: project-away

| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid

Stage 4: where

| where (UriPath has "/api/register" or UriPath has "/api/reset") and Status == 403

Stage 5: take

firstRec
| take recCount

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Statuseq
  • 403
field:"Status" kind:eq value:"403"
UriPathmatch
  • /api/register transforms: term
  • /api/reset transforms: term
field:"UriPath" kind:match

Application Redirect URL Update

#
Severity
medium
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Detects the redirect URL of an app being changed. Applications associated with URLs not controlled by the organization can pose a security risk. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-configuration-changes

MITRE ATT&CK Coverage

Telemetry Coverage

PlatformRecord / event type
AzureUpdate application

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: a1080fc1-13d1-479b-8340-255f0290d96c
name: Application Redirect URL Update
description: |
  'Detects the redirect URL of an app being changed.
    Applications associated with URLs not controlled by the organization can pose a security risk.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-configuration-changes'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  AuditLogs
    | where Category =~ "ApplicationManagement"
    | where Result =~ "success"
    | where OperationName =~ 'Update Application'
    | where TargetResources has "AppAddress"
    | mv-expand TargetResources
    | mv-expand TargetResources.modifiedProperties
    | where TargetResources_modifiedProperties.displayName =~ "AppAddress"
    | extend Key = tostring(TargetResources_modifiedProperties.displayName)
    | extend NewValue = TargetResources_modifiedProperties.newValue
    | extend OldValue = TargetResources_modifiedProperties.oldValue
    | where isnotempty(Key) and isnotempty(NewValue)
    | project-reorder Key, NewValue, OldValue
    | extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
    | extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
    | extend AddedUrls = set_difference(NewUrls, OldUrls)
    | where array_length(AddedUrls) > 0
    | extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
    | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
    | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
    | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
    | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
    | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
    | extend AddedBy = iif(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)
    | extend TargetAppName = tostring(TargetResources.displayName)
    | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
    | project-reorder TimeGenerated, TargetAppName, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, AddedUrls, AddedBy, UserAgent
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: AddedUrls
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIPAddress
version: 1.1.1
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "ApplicationManagement"

Stage 3: where

| where Result =~ "success"

Stage 4: where

| where OperationName =~ 'Update Application'

Stage 5: where

| where TargetResources has "AppAddress"

Stage 6: mv-expand

| mv-expand TargetResources

Stage 7: mv-expand

| mv-expand TargetResources.modifiedProperties

Stage 8: where

| where TargetResources_modifiedProperties.displayName =~ "AppAddress"

Stage 9: extend (3 consecutive steps)

| extend Key = tostring(TargetResources_modifiedProperties.displayName)
| extend NewValue = TargetResources_modifiedProperties.newValue
| extend OldValue = TargetResources_modifiedProperties.oldValue

Stage 10: where

| where isnotempty(Key) and isnotempty(NewValue)

Stage 11: project-reorder

| project-reorder Key, NewValue, OldValue

Stage 12: extend (3 consecutive steps)

| extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
| extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
| extend AddedUrls = set_difference(NewUrls, OldUrls)

Stage 13: where

| where array_length(AddedUrls) > 0

Stage 14: extend (9 consecutive steps)

| extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend AddedBy = iif(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)
| extend TargetAppName = tostring(TargetResources.displayName)
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Stage 15: project-reorder

| project-reorder TimeGenerated, TargetAppName, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, AddedUrls, AddedBy, UserAgent

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Keyextend
NewValueextend
OldValueextend
NewUrlsextend
OldUrlsextend
AddedUrlsextend
UserAgentextend
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIPAddressextend
AddedByextend
TargetAppNameextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

Caramel Tsunami Actor IOC - July 2021

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

Identifies a match across IOC's related to an actor tracked by Microsoft as Caramel Tsunami

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rule Body

id: 066395ac-ef91-4993-8bf6-25c61ab0ca5a
name: Caramel Tsunami Actor IOC - July 2021
description: | 
  'Identifies a match across IOC's related to an actor tracked by Microsoft as Caramel Tsunami'
severity: High
status: Available 
requiredDataConnectors: 
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
tactics: 
  - Persistence
relevantTechniques:
  - T1546
tags:
  - Caramel Tsunami
query:  |
  let iocs = externaldata(DateAdded:string,IoC:string,Type:string,TLP:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/SOURGUM.csv"] with (format="csv", ignoreFirstRecord=True);
  let file_path1 = (iocs | where Type =~ "filepath1" | project IoC);
  let file_path2 = (iocs | where Type =~ "filepath2" | project IoC);
  let file_path3 = (iocs | where Type =~ "filepath3" | project IoC);
  let reg_key = (iocs | where Type =~ "regkey" | project IoC);
  WindowsEvent
  | where EventID == 4688 and (EventData has_any (file_path1) or EventData has_any (file_path2) or  EventData has_any (file_path3) or EventData has_any ('reg add') or EventData has_any (reg_key) )
  | extend CommandLine = tostring(EventData.CommandLine)
  | extend NewProcessName = tostring(EventData.NewProcessName)
  | extend ParentProcessName = tostring(EventData.ParentProcessName)
  | where (CommandLine has_any (file_path1)) or
    (CommandLine has_any (file_path3)) or
    (CommandLine has 'reg add' and CommandLine has_any (reg_key) and CommandLine has_any (file_path2)) or 
    (NewProcessName has_any (file_path1)) or
    (NewProcessName has_any (file_path3)) or
    (ParentProcessName has_any (file_path1)) or 
    (ParentProcessName has_any (file_path3)) 
  | extend Account = strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
  | extend NewProcessId = tostring(EventData.NewProcessId)
  | extend IP = tostring(EventData.IpAddress)
  | project TimeGenerated, Computer, NewProcessName, ParentProcessName, Account, NewProcessId, Type, IP
  | extend Alert = 'SOURGUM IOC detected'
  | extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IP 
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: NewProcessId
version: 1.0.4
kind: Scheduled

Stages and Predicates

Let Binding: iocs

let iocs = externaldata(DateAdded:string,IoC:string,Type:string,TLP:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/SOURGUM.csv"] with (format="csv", ignoreFirstRecord=True);

Let Binding: file_path1 used in Stages 2, 4

let file_path1 = (iocs | where Type =~ "filepath1" | project IoC);

Let Binding: file_path2 used in Stages 2, 4

let file_path2 = (iocs | where Type =~ "filepath2" | project IoC);

Let Binding: file_path3 used in Stages 2, 4

let file_path3 = (iocs | where Type =~ "filepath3" | project IoC);

Let Binding: reg_key used in Stages 2, 4

let reg_key = (iocs | where Type =~ "regkey" | project IoC);

Stage 1: source

WindowsEvent

Stage 2: where

| where EventID == 4688 and (EventData has_any (file_path1) or EventData has_any (file_path2) or  EventData has_any (file_path3) or EventData has_any ('reg add') or EventData has_any (reg_key) )

Stage 3: extend (3 consecutive steps)

| extend CommandLine = tostring(EventData.CommandLine)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend ParentProcessName = tostring(EventData.ParentProcessName)

Stage 4: where

| where (CommandLine has_any (file_path1)) or
  (CommandLine has_any (file_path3)) or
  (CommandLine has 'reg add' and CommandLine has_any (reg_key) and CommandLine has_any (file_path2)) or 
  (NewProcessName has_any (file_path1)) or
  (NewProcessName has_any (file_path3)) or
  (ParentProcessName has_any (file_path1)) or 
  (ParentProcessName has_any (file_path3))

Stage 5: extend (3 consecutive steps)

| extend Account = strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
| extend NewProcessId = tostring(EventData.NewProcessId)
| extend IP = tostring(EventData.IpAddress)

Stage 6: project

| project TimeGenerated, Computer, NewProcessName, ParentProcessName, Account, NewProcessId, Type, IP

Stage 7: extend (4 consecutive steps)

| extend Alert = 'SOURGUM IOC detected'
| extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CommandLinematch
  • file_path1 transforms: term
  • file_path2 transforms: term
  • file_path3 transforms: term
  • reg add transforms: term
  • reg_key transforms: term
field:"CommandLine" kind:match
EventDatamatch
  • file_path1 transforms: term
  • file_path2 transforms: term
  • file_path3 transforms: term
  • reg add transforms: term
  • reg_key transforms: term
field:"EventData" kind:match
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"
NewProcessNamematch
  • file_path1 transforms: term
  • file_path3 transforms: term
field:"NewProcessName" kind:match
ParentProcessNamematch
  • file_path1 transforms: term
  • file_path3 transforms: term
field:"ParentProcessName" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
Computerproject
IPproject
NewProcessIdproject
NewProcessNameproject
ParentProcessNameproject
TimeGeneratedproject
Typeproject
Alertextend
AccountNTDomainextend
AccountNameextend
DomainIndexextend
HostNameextend
HostNameDomainextend

CTERA Mass Permissions Changes Detection Analytic

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

This analytic rule detects and alerts when access denied operations generated by the CTERA Edge Filer goes over a predefined threshold

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Rule Body

id: 90502ac9-19a2-41f0-ba81-e352de90b61b
name: CTERA Mass Permissions Changes Detection Analytic
description: 'This analytic rule detects and alerts when access denied operations generated by the CTERA Edge Filer goes over a predefined threshold'
kind: Scheduled
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CTERA
    dataTypes:
      - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 5000
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1068
query: |
  Syslog
    | where ProcessName == 'gw-audit'
    | extend
        TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
        UserName = extract("user=([^|]*)", 1, SyslogMessage),
        Operation = extract("op=([^|]*)", 1, SyslogMessage),
        EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
        RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
        Share = extract("share=([^|]*)", 1, SyslogMessage),
        LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
        Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))
    | where Operation in ('ACLAdded', 'ACLDeleted', 'ACLProtectionAdded','chown', 'setsd', 'ACLProtectionDeleted', 'ACEChanged', 'setdacl')
    | summarize Count = count() by UserName, bin(Timestamp, 5m)
    | where Count > 5000
incidentConfiguration:
    createIncident: true
    groupingConfiguration:
        enabled: false
        reopenClosedIncident: false
        lookbackDuration: PT5H
        matchingMethod: AllEntities
eventGroupingSettings:
    aggregationKind: SingleAlert
alertDetailsOverride:
    alertnameFormat: 'CTERA Batch Access Denied Detection'
    alertDescriptionFormat: >
      Detected {{Count}} denied access attempts by user {{UserName}} on Edge Filer 
      {{EdgeFiler}} within 5 minutes. Please investigate unauthorized access attempts or misconfigurations.
customDetails:
    TenantName: TenantName
    UserName: UserName
    EdgeFiler: EdgeFiler
    RootPath: RootPath
    Share: Share
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserName
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: EdgeFiler
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Syslog

Stage 2: where

| where ProcessName == 'gw-audit'

Stage 3: extend

| extend
      TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
      UserName = extract("user=([^|]*)", 1, SyslogMessage),
      Operation = extract("op=([^|]*)", 1, SyslogMessage),
      EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
      RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
      Share = extract("share=([^|]*)", 1, SyslogMessage),
      LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
      Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))

Stage 4: where

| where Operation in ('ACLAdded', 'ACLDeleted', 'ACLProtectionAdded','chown', 'setsd', 'ACLProtectionDeleted', 'ACEChanged', 'setdacl')

Stage 5: summarize

| summarize Count = count() by UserName, bin(Timestamp, 5m)

Stage 6: where

| where Count > 5000

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Countgt
  • 5000 transforms: number
field:"Count" kind:gt value:"5000"
Operationin
  • ACEChanged
  • ACLAdded
  • ACLDeleted
  • ACLProtectionAdded
  • ACLProtectionDeleted
  • chown
  • setdacl
  • setsd
field:"Operation" kind:in
ProcessNameeq
  • gw-audit
field:"process_name" kind:eq value:"gw-audit"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Countsummarize
UserNamesummarize

Datawiza - massive errors detected

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

"This rule is designed to identify when the system is experiencing abnormal errors."

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: "ddee1398-cf0b-46af-b583-78c3c29156dc"
name: "Datawiza - massive errors detected"
description: |
  "This rule is designed to identify when the system is experiencing abnormal errors."
severity: Medium
requiredDataConnectors:
  - connectorId: DatawizaDapSolution
    dataTypes:
      - datawizaserveraccess_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: 
  - Discovery
relevantTechniques:
  - T1082
query: |
    let timeFrame = 10m;
    datawizaserveraccess_CL
    | where TimeGenerated between (ago(timeFrame) .. now())
      and Status_d >= 500
    | summarize Count = count()
    | where Count > 100
eventGroupingSettings:
    aggregationKind: SingleAlert
alertDetailsOverride:
    alertDisplayNameFormat: 'Datawiza Massive Error Detection'
    alertDescriptionFormat: >
      Detected {{Count}} errors within 10 minutes. Please investigate unauthorized access attempts or misconfigurations.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let timeFrame = 10m;

Stage 1: source

datawizaserveraccess_CL

Stage 2: where

| where TimeGenerated between (ago(timeFrame) .. now())
  and Status_d >= 500

Stage 3: summarize

| summarize Count = count()

Stage 4: where

| where Count > 100

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Countgt
  • 100 transforms: number
field:"Count" kind:gt value:"100"
Status_dge
  • 500 transforms: number
field:"Status_d" kind:ge value:"500"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Countsummarize

Detect first time Azure Custom Script or Run Command deployment

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

This detection rule flags using UEBA of Defender XDR and Microsoft Sentinel if it is the first time that an account is deploying Custom Scripts or Run Commands on Azure and Azure Arc machines. Since UEBA uses a baseline of 180 days, it might indicate that an account is being abused to compormise Azure or Azure Arc machines.

MITRE ATT&CK Coverage

References

Rule Body

BehaviorAnalytics
| where TimeGenerated > ago(1h)
| extend ActivityInsights = parse_json(ActivityInsights)
| where ActivityInsights.EventMessage has_any ('runCommand/action', 'extensions/write')
| where ActivityInsights.FirstTimeUserPerformedAction == "True"

Stages and Predicates

Stage 1: source

BehaviorAnalytics

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: extend

| extend ActivityInsights = parse_json(ActivityInsights)

Stage 4: where

| where ActivityInsights.EventMessage has_any ('runCommand/action', 'extensions/write')

Stage 5: where

| where ActivityInsights.FirstTimeUserPerformedAction == "True"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityInsights.EventMessagematch
  • extensions/write transforms: term, kusto_dynamic_path
  • runCommand/action transforms: term, kusto_dynamic_path
field:"ActivityInsights.EventMessage" kind:match
ActivityInsights.FirstTimeUserPerformedActioneq
  • True transforms: kusto_dynamic_path
field:"ActivityInsights.FirstTimeUserPerformedAction" kind:eq value:"True"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityInsightsextend

McAfee ePO - File added to exceptions

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

Detects when file was added to exception list on a host.

MITRE ATT&CK Coverage

TacticTechniques
Stealth
Defense Impairment

Telemetry Coverage

Rule Body

id: b9d9fdfe-bc17-45ce-a70d-67a5cfd119f4
name: McAfee ePO - File added to exceptions
description: |
  'Detects when file was added to exception list on a host.'
severity: Medium
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
  - T1070
query: |
  McAfeeEPOEvent
  | where EventId in ('1029', '2005', '2015')
  | project DvcIpAddr, DstFileName
  | extend IPCustomEntity = DvcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

McAfeeEPOEvent

Stage 2: where

| where EventId in ('1029', '2005', '2015')

Stage 3: project

| project DvcIpAddr, DstFileName

Stage 4: extend

| extend IPCustomEntity = DvcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIdin
  • 1029
  • 2005
  • 2015
field:"EventId" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstFileNameproject
DvcIpAddrproject
IPCustomEntityextend

Office365 Sharepoint File transfer above threshold

#
Severity
medium
Time window
15m
Group by
ClientIP, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies Office365 Sharepoint File Transfers above certain threshold in a 15min time period. Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 8b4f03e7-3460-4401-824d-e65a8dd464f0
name: Office365 Sharepoint File transfer above threshold
description: |
  'Identifies Office365 Sharepoint File Transfers above certain threshold in a 15min time period.
   Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (SharePoint)
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1020
query: |
  let threshold = 5000;
  OfficeActivity
  | where EventSource == "SharePoint" and OfficeWorkload has_any("SharePoint", "OneDrive") and Operation has_any ("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded")
  | summarize count_distinct_OfficeObjectId=dcount(OfficeObjectId), fileslist=make_set(OfficeObjectId, 10000) by UserId,ClientIP,bin(TimeGenerated, 15m)
  | where count_distinct_OfficeObjectId >= threshold
  | extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist)))))
  | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileSample
customDetails:
  TransferCount: count_distinct_OfficeObjectId
  FilesList: fileslist
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: Selected
    groupByEntities:
    - Account
    groupByAlertDetails: []
    groupByCustomDetails: []
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 5000;

Stage 1: source

OfficeActivity

Stage 2: where

| where EventSource == "SharePoint" and OfficeWorkload has_any("SharePoint", "OneDrive") and Operation has_any ("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded")

Stage 3: summarize

| summarize count_distinct_OfficeObjectId=dcount(OfficeObjectId), fileslist=make_set(OfficeObjectId, 10000) by UserId,ClientIP,bin(TimeGenerated, 15m)

Stage 4: where

| where count_distinct_OfficeObjectId >= threshold

Stage 5: extend

| extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist)))))
FileSample =
iffileslist == 1tostring(fileslist[0])
elsestrcat("SeeFilesListField", "_", tostring(hash(tostring(fileslist))))

Stage 6: extend

| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventSourceeq
  • SharePoint corpus 2 (kusto 2)
field:"aws::eventSource" kind:eq value:"SharePoint"
OfficeWorkloadmatch
  • OneDrive transforms: term corpus 2 (kusto 2)
  • SharePoint transforms: term corpus 2 (kusto 2)
field:"m365::Workload" kind:match
Operationmatch
  • FileDownloaded transforms: term corpus 2 (kusto 2)
  • FileSyncDownloadedFull transforms: term corpus 2 (kusto 2)
  • FileSyncUploadedFull transforms: term corpus 2 (kusto 2)
  • FileUploaded transforms: term corpus 2 (kusto 2)
field:"Operation" kind:match
count_distinct_OfficeObjectIdge
  • 5000 transforms: number
field:"count_distinct_OfficeObjectId" kind:ge value:"5000"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPsummarize
UserIdsummarize
count_distinct_OfficeObjectIdsummarize
fileslistsummarize
FileSampleextend
AccountNameextend
AccountUPNSuffixextend

Potential communication with a Domain Generation Algorithm (DGA) based hostname (ASIM Web Session schema)

#
Severity
medium
Time window
6h
Group by
DGADomain, Name
Author
Yaron
Source
github.com/Azure/Azure-Sentinel

'This rule identifies communication with hosts that have a domain name that might have been generated by a Domain Generation Algorithm (DGA). DGAs are used by malware to generate rendezvous points that are difficult to predict in advance. This detection uses the top 1 million domain names to build a model of what normal domains look like nad uses the model to identify domains that may have been randomly generated by an algorithm. You can modify the triThreshold and dgaLengthThreshold query parameters to change Analytic Rule sensitivity. The higher the numbers, the less noisy the rule is. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 9176b18f-a946-42c6-a2f6-0f6d17cd6a8a
name: Potential communication with a Domain Generation Algorithm (DGA) based hostname (ASIM Web Session schema)
description: |
  'This rule identifies communication with hosts that have a domain name that might have been generated by a Domain Generation Algorithm (DGA).
  DGAs are used by malware to generate rendezvous points that are difficult to predict in advance. This detection uses the top 1 million domain names to build a model of what normal domains look like nad uses the model to identify domains that may have been randomly generated by an algorithm. You can modify the triThreshold and dgaLengthThreshold query parameters to change Analytic Rule sensitivity. The higher the numbers, the less noisy the rule is.
   This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'
severity: Medium
requiredDataConnectors:
  - connectorId: SquidProxy
    dataTypes:
      - SquidProxy_CL
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
tags:
  - ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/CommonSecurityLog/MultiVendor-PossibleDGAContacts.yaml
    version: 1.0.0
  - Schema: ASIMWebSession
    SchemaVersion: 0.2.0
relevantTechniques:
  - T1568
query: |
    let triThreshold = 500;
    let querystarttime = 6h;
    let dgaLengthThreshold = 8;
    // fetch the cisco umbrella top 1M domains
    let top1M =  (externaldata (Position:int, Domain:string)   [@"http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip"]  with (format="csv", zipPattern="*.csv"));
    // extract tri grams that are above our threshold - i.e. are common
    let triBaseline =   top1M
      | extend Domain = tolower(extract("([^.]*).{0,7}$", 1, Domain))
      | extend AllTriGrams = array_concat(extract_all("(...)", Domain), extract_all("(...)", substring(Domain, 1)), extract_all("(...)", substring(Domain, 2)))
      | mvexpand Trigram=AllTriGrams to typeof(string)
      | summarize triCount=count() by Trigram
      | sort by triCount desc
      | where triCount > triThreshold
      | distinct Trigram;
    // collect domain information from common security log, filter and extract the DGA candidate and its trigrams
    let allDataSummarized =  _Im_WebSession
    | where isnotempty(Url)
    | extend Name = tolower(tostring(parse_url(Url)["Host"]))
    | summarize NameCount=count() by Name
    | where Name has "."
    | where Name !endswith ".home" and Name !endswith ".lan"
    // extract DGA candidate
    | extend DGADomain = extract("([^.]*).{0,7}$", 1, Name)
    | where strlen(DGADomain) > dgaLengthThreshold
    // throw out domains with number in them
    | where DGADomain matches regex "^[A-Za-z]{0,}$"
    // extract the tri grams from summarized data
    | extend AllTriGrams = array_concat(extract_all("(...)", DGADomain), extract_all("(...)", substring(DGADomain, 1)), extract_all("(...)", substring(DGADomain, 2)));
    // throw out domains that have repeating tri's and/or >=3 repeating letters
    let nonRepeatingTris =  allDataSummarized
    | join kind=leftanti
    (
        allDataSummarized
        | mvexpand AllTriGrams
        | summarize count() by tostring(AllTriGrams), DGADomain
        | where count_ > 1
        | distinct DGADomain
    )
    on DGADomain;
    // find domains that do not have a common tri in the baseline
    let dataWithRareTris =  nonRepeatingTris
    | join kind=leftanti
    (
        nonRepeatingTris
        | mvexpand AllTriGrams
        | extend Trigram = tostring(AllTriGrams)
        | distinct Trigram, DGADomain
        | join kind=inner
        (
            triBaseline
        )
        on Trigram
        | distinct DGADomain
    )
    on DGADomain;
    dataWithRareTris
    // join DGAs back on connection data
    | join kind=inner
    (
        _Im_WebSession
        | where isnotempty(Url)
        | extend Url = tolower(Url)
        | summarize arg_max(TimeGenerated, EventVendor,  SrcIpAddr) by Url
        | extend Name=tostring(parse_url(Url)["Host"])
        | summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) by Name, SrcIpAddr, Url
    )
    on Name
    | project StartTime, EndTime, Name, DGADomain, SrcIpAddr, Url, NameCount
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
alertDetailsOverride:
  alertDisplayNameFormat: Potential communication from {{SrcIpAddr}} with a Domain Generation Algorithm (DGA) based host {{Name}}
  alertDescriptionFormat: A client with address {{SrcIpAddr}} communicated with host {{Name}} that have a domain name that might have been generated by a Domain Generation Algorithm (DGA), identified by the pattern {{DGADomain}}. DGAs are used by malware to generate rendezvous points that are difficult to predict in advance. This detection uses the top 1 million domain names to build a model of what normal domains look like and uses the model to identify domains that may have been randomly generated by an algorithm.
customDetails:
  DGAPattern: DGADomain
  NameCount: NameCount
version: 1.1.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Yaron
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Parameters

let triThreshold = 500;
let querystarttime = 6h;
let dgaLengthThreshold = 8;

let allDataSummarized, let nonRepeatingTris and let dataWithRareTris are inlined into the numbered stages below.

Let Binding: top1M

let top1M = (externaldata (Position:int, Domain:string)   [@"http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip"]  with (format="csv", zipPattern="*.csv"));

Let Binding: triBaseline

let triBaseline = top1M
  | extend Domain = tolower(extract("([^.]*).{0,7}$", 1, Domain))
  | extend AllTriGrams = array_concat(extract_all("(...)", Domain), extract_all("(...)", substring(Domain, 1)), extract_all("(...)", substring(Domain, 2)))
  | mvexpand Trigram=AllTriGrams to typeof(string)
  | summarize triCount=count() by Trigram
  | sort by triCount desc
  | where triCount > triThreshold
  | distinct Trigram;

Stages 1 to 12 define let dataWithRareTris as the rule's main pipeline source. Stages 13 to 14 use it.

Stage 1: source

_Im_WebSession

Stage 2: where

where isnotempty(Url)

Stage 3: extend

extend Name

Stage 4: summarize

summarize NameCount by Name

Stage 5: where

where Name has "."

Stage 6: where

where not (Name endswith ".home") and not (Name endswith ".lan")

Stage 7: extend

extend DGADomain

Stage 8: where

where DGADomain > 8

Stage 9: where

where DGADomain matches regex "^[A-Za-z]{0,}$"

Stage 10: extend

extend AllTriGrams

Stage 11: join (negated)

join kind=leftanti (allDataSummarized) on DGADomain

Stage 12: join (negated)

join kind=leftanti (nonRepeatingTris) on DGADomain

Stage 13: join

dataWithRareTris
| join kind=inner
(
    _Im_WebSession
    | where isnotempty(Url)
    | extend Url = tolower(Url)
    | summarize arg_max(TimeGenerated, EventVendor,  SrcIpAddr) by Url
    | extend Name=tostring(parse_url(Url)["Host"])
    | summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) by Name, SrcIpAddr, Url
)
on Name

Stage 14: project

project DGADomain, EndTime, Name, NameCount, SrcIpAddr, StartTime, Url

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DGADomaingt
  • 8 transforms: strlen
field:"DGADomain" kind:gt value:"8"
DGADomainregex_match
  • ^[A-Za-z]{0,}$
field:"DGADomain" kind:regex_match value:"^[A-Za-z]{0,}$"
Nameends_with
  • .home
  • .lan
field:"Name" kind:ends_with
Namematch
  • . transforms: term
field:"Name" kind:match value:"."
Urlis_not_null
  • (no value, null check)
field:"Url" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DGADomainproject
EndTimeproject
Nameproject
NameCountproject
SrcIpAddrproject
StartTimeproject
Urlproject

SailPointIdentityNowAlertForTriggers

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

'Create alerts for SailPoint IdentityNow Event Trigger Service.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Collection

Rule Body

id: 08330c3d-487e-4f5e-a539-1e7d06dea786
name: SailPointIdentityNowAlertForTriggers
description: |
  'Create alerts for SailPoint IdentityNow Event Trigger Service.'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: SailPointIdentityNow
    dataTypes:
      - SailPointIDN_Events_CL
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Collection
relevantTechniques:
  - T1133
  - T1005
query: |
  declare query_parameters(lbperiod:timespan = 14d, triggerId:string = "idn:aggregation-accounts-collected");
    SailPointIDN_Triggers_CL
    | where TimeGenerated > ago(lbperiod)
    | where Metadata_triggerId_s == triggerId
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Metadata_triggertId_s
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

declare query_parameters(lbperiod:timespan = 14d, triggerId:string = "idn:aggregation-accounts-collected");
  SailPointIDN_Triggers_CL

Stage 2: where

| where TimeGenerated > ago(lbperiod)

Stage 3: where

| where Metadata_triggerId_s == triggerId

Semperis DSP Recent sIDHistory changes on AD objects

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

'This indicator detects any recent changes to sIDHistory on AD objects, including changes to non-privileged accounts where privileged SIDs are added.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Privilege Escalation

Rule Body

id: 64796da3-6383-4de2-9c97-866c83c459ae
name: Semperis DSP Recent sIDHistory changes on AD objects
description: |
  'This indicator detects any recent changes to sIDHistory on AD objects, including changes to non-privileged accounts where privileged SIDs are added.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SemperisDSP
    dataTypes:
      - dsp_parser
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1098
query: |
  dsp_parser
  | where EventID == 9212
  | where SecurityIndicatorName == "Recent sIDHistory changes on objects"
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 2.0.7
kind: Scheduled

Stages and Predicates

Stage 1: source

dsp_parser

Stage 2: where

| where EventID == 9212

Stage 3: where

| where SecurityIndicatorName == "Recent sIDHistory changes on objects"

Stage 4: extend

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

Tailscale Premium: Mass fan-out from single node

#
Status
available
Severity
high
Time window
15m
Group by
NodeId, Src, SrcNodeName, SrcOs, SrcTags, SrcUser
Source
github.com/Azure/Azure-Sentinel

Identifies when a single node initiates flows to 25 or more unique destinations within a 15-minute window. Sudden fan-out is consistent with port scanning, lateral discovery, or worm-style propagation. Requires Tailscale Premium or Enterprise.

MITRE ATT&CK Coverage

Rule Body

id: f4a5b6c7-4d5e-6f7a-8b9c-0d1e2f3a4b5c
name: "Tailscale Premium: Mass fan-out from single node"
description: |
  Identifies when a single node initiates flows to 25 or more unique destinations within a 15-minute window. Sudden fan-out is consistent with port scanning, lateral discovery, or worm-style propagation. Requires Tailscale Premium or Enterprise.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Network_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
  - LateralMovement
relevantTechniques:
  - T1018
  - T1021
  - T1046
query: |
  let dstThreshold = 25;
  Tailscale_Network_CL
  | where TimeGenerated > ago(15m)
  | where HasVirtualTraffic
  | mv-expand t = VirtualTraffic
  | extend Src = tostring(t.src), Dst = tostring(t.dst)
  | summarize UniqueDestinations = dcount(Dst), TopDestinations = make_set(Dst, 25) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags), Src
  | where UniqueDestinations >= dstThreshold
  | order by UniqueDestinations desc
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcNodeName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUser
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Src
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let dstThreshold = 25;

Stage 1: source

Tailscale_Network_CL

Stage 2: where

| where TimeGenerated > ago(15m)

Stage 3: where

| where HasVirtualTraffic

Stage 4: mv-expand

| mv-expand t = VirtualTraffic

Stage 5: extend

| extend Src = tostring(t.src), Dst = tostring(t.dst)

Stage 6: summarize

| summarize UniqueDestinations = dcount(Dst), TopDestinations = make_set(Dst, 25) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags), Src

Stage 7: where

| where UniqueDestinations >= dstThreshold

Stage 8: sort

| order by UniqueDestinations desc

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
NodeIdsummarize
Srcsummarize
SrcNodeNamesummarize
SrcOssummarize
SrcTagssummarize
SrcUsersummarize
TopDestinationssummarize
UniqueDestinationssummarize

Tape Server Deleted

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

Detects when a tape server is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.

Rule Body

id: fda79d00-9e6a-46f8-b7a2-2bdccd5f598e
name: Tape Server Deleted
description: Detects when a tape server is deleted from Veeam Backup & Replication.
  This might indicate unauthorized removal of critical components.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 28800\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = user,\n  MessageDetails = Description,\n\
  \    Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 28800

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

Ubiquiti - connection to non-corporate DNS server

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

'Detects connections to non-corporate DNS servers.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control
Exfiltration

Rule Body

id: fe232837-9bdc-4e2b-8c08-cdac2610eed3
name: Ubiquiti - connection to non-corporate DNS server
description: |
  'Detects connections to non-corporate DNS servers.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - Ubiquiti_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Exfiltration
relevantTechniques:
  - T1572
  - T1041
query: |
  let corp_dns = dynamic(['127.0.0.2']);
  UbiquitiAuditEvent
  | where DstPortNumber == '53'
  | where DstIpAddr !in (corp_dns)
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let corp_dns = dynamic(['127.0.0.2']);

Stage 1: source

UbiquitiAuditEvent

Stage 2: where

| where DstPortNumber == '53'

Stage 3: where

| where DstIpAddr !in (corp_dns)

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DstIpAddreqcorp_dnsexcludes:DstIpAddr field:"DstIpAddr" value:"corp_dns"

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Back to all Kusto rules