Detection rules › Kusto
Kusto rules: 02
Archive Repository Settings Updated
#Detects when archive repository settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.
Rule Body
id: 74c3bda5-cdae-4af3-ab54-daddfbe8bc70
name: Archive Repository Settings Updated
description: Detects when archive repository settings are updated in Veeam Backup
& Replication. This might indicate configuration changes that require review.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 29800\n| extend Name = extract(\"\
Name=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n Date = format_datetime(TimeGenerated,\
\ 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n EventId = instanceId,\n\
\ UserName = user,\n [\"Object Name\"] = Name,\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 == 29800
Stage 3: extend
| extend Name = extract("Name=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
["Object Name"] = Name,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"29800" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Object Name | project |
Severity | project |
UserName | project |
Authentication Attempt from New Country
#Detects when there is a login attempt from a country that has not seen a successful login in the previous 14 days. Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts. Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
Rule Body
id: ef895ada-e8e8-4cf0-9313-b1ab67fab69f
name: Authentication Attempt from New Country
description: |
Detects when there is a login attempt from a country that has not seen a successful login in the previous 14 days.
Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts.
Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let CombinedSignInLogs = union isfuzzy=True AADNonInteractiveUserSignInLogs, SigninLogs;
// Combine AADNonInteractiveUserSignInLogs and SigninLogs into a single table
// Fetch Azure IP address ranges data from a JSON file hosted on GitHub
let AzureRanges = externaldata(changeNumber: string, cloud: string, values: dynamic)
["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
// Load Azure IP address ranges from the JSON file hosted on GitHub
| mv-expand values
// Expand the values column into separate rows
| extend Name = values.name, AddressPrefixes = tostring(values.properties.addressPrefixes);
// Create additional columns for the name and address prefixes
// Identify known locations to be excluded from analysis
let ExcludedKnownLocations = CombinedSignInLogs
// Filter the combined logs based on the specified time range
| where TimeGenerated between (ago(14d)..ago(1d))
// Filter by specific ResultType
| where ResultType == 0
// Summarize the logs by location
| summarize by Location;
// Find sign-in locations matching specific criteria
let MatchedLocations = materialize(CombinedSignInLogs
// Filter the combined logs based on the specified time range
| where TimeGenerated > ago(1d)
// Exclude specific ResultTypes
| where ResultType !in (50126, 50053, 50074, 70044)
// Exclude known locations
| where Location !in (ExcludedKnownLocations));
// Match IP addresses of matched locations with Azure IP address ranges
let MatchedIPs = MatchedLocations
// Use the 'ipv4_lookup' function to match IP addresses with Azure IP address ranges
| evaluate ipv4_lookup(AzureRanges, IPAddress, AddressPrefixes)
// Project only the IPAddress column
| project IPAddress;
// Exclude IP addresses that are already matched with Azure IP address ranges
let MaxSetSize = 5; // Set the maximum size limit for make_set
let ExcludedIPs = MatchedLocations
// Filter out IP addresses that are already matched
| where not (IPAddress in (MatchedIPs))
// Exclude empty or null Location values
| where isnotempty(Location)
// Handle dynamic and string column values for LocationDetails and DeviceDetail
| extend LocationDetails_dynamic = column_ifexists("LocationDetails_dynamic", "")
| extend DeviceDetail_dynamic = column_ifexists("DeviceDetail_dynamic", "")
| extend LocationDetails = iif(isnotempty(LocationDetails_dynamic), LocationDetails_dynamic, parse_json(LocationDetails_string))
| extend DeviceDetail = iif(isnotempty(DeviceDetail_dynamic), DeviceDetail_dynamic, parse_json(DeviceDetail_string))
// Extract location details (city and state)
| extend City = tostring(LocationDetails.city)
| extend State = tostring(LocationDetails.state)
| extend Place = strcat(City, " - ", State)
| extend DeviceId = tostring(DeviceDetail.deviceId)
| extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
// Summarize the data based on UserPrincipalName, Location, and Category
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated),
make_set(Result, MaxSetSize), make_set(IPAddress, MaxSetSize),
make_set(UserAgent, MaxSetSize), make_set(Place, MaxSetSize),
make_set(DeviceId, MaxSetSize) by UserPrincipalName, Location, Category
// Extract the username prefix and suffix from UserPrincipalName
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0]);
ExcludedIPs // Output the final result set
| extend IP = set_IPAddress[0]
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IP
version: 1.1.2
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let MaxSetSize = 5;
let CombinedSignInLogs, let MatchedLocations and let ExcludedIPs are inlined into the numbered stages below.
Let Binding: AzureRanges
let AzureRanges = externaldata(changeNumber: string, cloud: string, values: dynamic)
["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
| mv-expand values
| extend Name = values.name, AddressPrefixes = tostring(values.properties.addressPrefixes);
Let Binding: ExcludedKnownLocations
let ExcludedKnownLocations = CombinedSignInLogs
| where TimeGenerated between (ago(14d)..ago(1d))
| where ResultType == 0
| summarize by Location;
Let Binding: MatchedIPs
let MatchedIPs = MatchedLocations
| evaluate ipv4_lookup(AzureRanges, IPAddress, AddressPrefixes)
| project IPAddress;
Stages 1 to 11 define let ExcludedIPs as the rule's main pipeline source. Stage 12 uses it.
Stage 1: union
union of 2 branches
Stage 2: source
AADNonInteractiveUserSignInLogs
Stage 3: source
SigninLogs
Stage 4: where
where TimeGenerated > ago(86400s)
Stage 5: where
where not (ResultType in (50053, 50074, 50126, 70044))
Stage 6: where
where not (Location == "ExcludedKnownLocations")
Stage 7: where
where not (IPAddress == "MatchedIPs")
Stage 8: where
where isnotempty(Location)
Stage 9: extend (9 consecutive steps)
extend City, DeviceDetail, DeviceDetail_dynamic, DeviceId, LocationDetails, LocationDetails_dynamic, Place, Result, State
Stage 10: summarize
summarize FirstSeen, LastSeen by UserPrincipalName, Location, Category
Stage 11: extend
extend Name, UPNSuffix
Stage 12: extend
extend IP
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Location | is_not_null | field:"Location" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ResultType | in | 50053, 50074, 50126, 70044 | excludes:ResultType |
Location | eq | ExcludedKnownLocations | excludes:Location field:"Location" value:"ExcludedKnownLocations" |
IPAddress | eq | MatchedIPs | excludes:IPAddress field:"IPAddress" value:"MatchedIPs" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | summarize |
FirstSeen | summarize |
LastSeen | summarize |
Location | summarize |
UserPrincipalName | summarize |
Name | extend |
UPNSuffix | extend |
IP | extend |
Azure DevOps Pull Request Policy Bypassing - Historic allow list
#'This detection builds an allow list of historic PR policy bypasses and compares to recent history, flagging pull request bypasses that are not manually in the allow list and not historically included in the allow list.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Rule Body
id: 4d8de9e6-263e-4845-8618-cd23a4f58b70
name: Azure DevOps Pull Request Policy Bypassing - Historic allow list
description: |
'This detection builds an allow list of historic PR policy bypasses and compares to recent history, flagging pull request bypasses that are not manually in the allow list and not historically included in the allow list.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 3h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098
query: |
let starttime = 14d;
let endtime = 3h;
// Add full UPN (user@domain.com) to Authorized Bypassers to ignore policy bypasses by certain authorized users
let AuthorizedBypassers = dynamic(['foo@baz.com', 'test@foo.com']);
let historicBypassers = ADOAuditLogs
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where OperationName == 'Git.RefUpdatePoliciesBypassed'
| distinct ActorUPN;
ADOAuditLogs
| where TimeGenerated >= ago(endtime)
| where OperationName == 'Git.RefUpdatePoliciesBypassed'
| where ActorUPN !in (historicBypassers) and ActorUPN !in (AuthorizedBypassers)
| parse ScopeDisplayName with OrganizationName '(Organization)'
| project TimeGenerated, ActorUPN, IpAddress, UserAgent, OrganizationName, ProjectName, RepoName = Data.RepoName, AlertDetails = Details, Branch = Data.Name,
BypassReason = Data.BypassReason, PRLink = strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_git/', Data.RepoName, '/pullrequest/', Data.PullRequestId)
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUPN
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
- entityType: URL
fieldMappings:
- identifier: Url
columnName: PRLink
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 14d;
let endtime = 3h;
let AuthorizedBypassers = dynamic(['foo@baz.com', 'test@foo.com']);
Let Binding: historicBypassers
let historicBypassers = ADOAuditLogs
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where OperationName == 'Git.RefUpdatePoliciesBypassed'
| distinct ActorUPN;
Stage 1: source
ADOAuditLogs
Stage 2: where
| where TimeGenerated >= ago(endtime)
Stage 3: where
| where OperationName == 'Git.RefUpdatePoliciesBypassed'
Stage 4: where
| where ActorUPN !in (historicBypassers) and ActorUPN !in (AuthorizedBypassers)
Stage 5: parse
| parse ScopeDisplayName with OrganizationName '(Organization)'
Stage 6: project
| project TimeGenerated, ActorUPN, IpAddress, UserAgent, OrganizationName, ProjectName, RepoName = Data.RepoName, AlertDetails = Details, Branch = Data.Name,
BypassReason = Data.BypassReason, PRLink = strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_git/', Data.RepoName, '/pullrequest/', Data.PullRequestId)
Stage 7: extend
| extend timestamp = TimeGenerated
Stage 8: extend
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"Git.RefUpdatePoliciesBypassed" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ActorUPN | eq | historicBypassers | excludes:ActorUPN field:"ActorUPN" value:"historicBypassers" |
ActorUPN | in | foo@baz.com, test@foo.com | excludes:ActorUPN field:"ActorUPN" value:"foo@baz.com" field:"ActorUPN" value:"test@foo.com" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUPN | project |
AlertDetails | project |
Branch | project |
BypassReason | project |
IpAddress | project |
OrganizationName | project |
PRLink | project |
ProjectName | project |
RepoName | project |
TimeGenerated | project |
UserAgent | project |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
Cisco Cloud Security - Request to blocklisted file type
#'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control |
Rule Body
id: de58ee9e-b229-4252-8537-41a4c2f4045e
name: Cisco Cloud Security - Request to blocklisted file type
description: |
'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_proxy_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CommandAndControl
relevantTechniques:
- T1189
- T1105
query: |
let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);
Cisco_Umbrella
| where EventType == 'proxylogs'
| where DvcAction =~ 'Allowed'
| extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)
| extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)
| where file_ext in (file_ext_blocklist)
| project TimeGenerated, SrcIpAddr, DstIpAddr, Identities, Filename, UrlOriginal
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlOriginal
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);
Stage 1: source
Cisco_Umbrella
Stage 2: where
| where EventType == 'proxylogs'
Stage 3: where
| where DvcAction =~ 'Allowed'
Stage 4: extend
| extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)
Stage 5: extend
| extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)
Stage 6: where
| where file_ext in (file_ext_blocklist)
Stage 7: project
| project TimeGenerated, SrcIpAddr, DstIpAddr, Identities, Filename, UrlOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DvcAction | eq |
| field:"DvcAction" kind:eq value:"Allowed" |
EventType | eq |
| field:"EventType" kind:eq value:"proxylogs" |
file_ext | in |
| field:"file_ext" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstIpAddr | project |
Filename | project |
Identities | project |
SrcIpAddr | project |
TimeGenerated | project |
UrlOriginal | project |
CMMC 2.0 Level 1 (Foundational) Readiness Posture
#'CMMC 2.0 Level 1 (Foundational) assessments have deviated from configured threshold baselines. This alert is triggered when CMMC2.0 policy compliance is assessed below 70% compliance in 7 days.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: fb127436-e5c4-4e31-85a8-d3507128dd09
name: CMMC 2.0 Level 1 (Foundational) Readiness Posture
description: |
'CMMC 2.0 Level 1 (Foundational) assessments have deviated from configured threshold baselines. This alert is triggered when CMMC2.0 policy compliance is assessed below 70% compliance in 7 days.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1082
query: |
SecurityRegulatoryCompliance
| where ComplianceStandard == "NIST-SP-800-171-R2"
| extend Level=iff(ComplianceControl in ("3.1.1","3.1.2","3.1.20","3.1.22","3.4.1","3.5.2","3.5.2","3.8.3","3.13.1","3.13.5","3.14.1","3.14.2","3.14.4","3.14.5"), "Level 1: Foundational","Level 2: Advanced")
| where Level == "Level 1: Foundational"
| summarize arg_max(TimeGenerated, *) by RecommendationName, AssessedResourceId, Level
| summarize Failed=countif(State=="Failed"),Passed=countif(State=="Passed"),Total=countif(State=="Passed" or State == "Failed") by Level
|extend PassedControlsPercentage = (Passed/todouble(Total))*100
| where PassedControlsPercentage < 70
//Adjust Either Passed Thresholds within Organizational Needs
| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
| project Level, Total, PassedControlsPercentage, Passed, Failed, RemediationLink, LastObserved=now()
| extend URLCustomEntity = RemediationLink
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityRegulatoryCompliance
Stage 2: where
| where ComplianceStandard == "NIST-SP-800-171-R2"
Stage 3: extend
| extend Level=iff(ComplianceControl in ("3.1.1","3.1.2","3.1.20","3.1.22","3.4.1","3.5.2","3.5.2","3.8.3","3.13.1","3.13.5","3.14.1","3.14.2","3.14.4","3.14.5"), "Level 1: Foundational","Level 2: Advanced")
Level =ComplianceControl in ("3.1.1", "3.1.2", "3.1.20", "3.1.22", "3.4.1", "3.5.2", "3.5.2", "3.8.3", "3.13.1", "3.13.5", "3.14.1", "3.14.2", "3.14.4", "3.14.5")"Level 1: Foundational""Level 2: Advanced"Stage 4: where
| where Level == "Level 1: Foundational"
Stage 5: summarize
| summarize arg_max(TimeGenerated, *) by RecommendationName, AssessedResourceId, Level
Stage 6: summarize
| summarize Failed=countif(State=="Failed"),Passed=countif(State=="Passed"),Total=countif(State=="Passed" or State == "Failed") by Level
Stage 7: extend
| extend PassedControlsPercentage = (Passed/todouble(Total))*100
Stage 8: where
| where PassedControlsPercentage < 70
Stage 9: extend
| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
Stage 10: project
| project Level, Total, PassedControlsPercentage, Passed, Failed, RemediationLink, LastObserved=now()
Stage 11: extend
| extend URLCustomEntity = RemediationLink
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ComplianceStandard | eq |
| field:"ComplianceStandard" kind:eq value:"NIST-SP-800-171-R2" |
Level | eq |
| field:"Level" kind:eq value:"Level 1: Foundational" |
PassedControlsPercentage | lt |
| field:"PassedControlsPercentage" kind:lt value:"70" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Failed | project |
LastObserved | project |
Level | project |
Passed | project |
PassedControlsPercentage | project |
RemediationLink | project |
Total | project |
URLCustomEntity | extend |
Cortex XDR Incident - Medium
#'A new incident was created in the Cortex XDR portal with a severity "Medium". Click on the events for incident details. '
Rule Body
id: 2e638f3d-611f-4281-81b1-1fd4aa240ffb
name: Cortex XDR Incident - Medium
description: |
'A new incident was created in the Cortex XDR portal with a severity "Medium". Click on the events for incident details. '
severity: Medium
requiredDataConnectors:
- connectorId: CortexXDRDataConnector
dataTypes:
- PaloAltoCortexXDR_Incidents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics: []
relevantTechniques: []
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride: null
customDetails: null
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Users_s
- entityType: URL
fieldMappings:
- identifier: Url
columnName: XdrUrl
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hosts_s
sentinelEntitiesMappings: null
query: |
PaloAltoCortexXDR_Incidents
| where Severity contains "medium"
| where Status !contains "resolved_auto"
// the below line filters for incidents that has at least one user or one host in it. Comment the below line if you want to generated incidents that have 0 user or 0 host as well
| where HostCount > 0 or UserCount > 0
| extend Hosts_s = tostring(Hosts), Users_s = tostring(Users)
| project TimeGenerated, IncidentId, Status, Severity, IncidentSources, Description, XdrUrl, Hosts_s, Users_s, AlertCategories, HostCount, UserCount, AlertCount, CreationTime, ModificationTime
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
PaloAltoCortexXDR_Incidents
Stage 2: where
| where Severity contains "medium"
Stage 3: where
| where Status !contains "resolved_auto"
Stage 4: where
| where HostCount > 0 or UserCount > 0
Stage 5: extend
| extend Hosts_s = tostring(Hosts), Users_s = tostring(Users)
Stage 6: project
| project TimeGenerated, IncidentId, Status, Severity, IncidentSources, Description, XdrUrl, Hosts_s, Users_s, AlertCategories, HostCount, UserCount, AlertCount, CreationTime, ModificationTime
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HostCount | gt |
| field:"HostCount" kind:gt value:"0" |
Severity | contains |
| field:"Severity" kind:contains value:"medium" |
UserCount | gt |
| field:"UserCount" kind:gt value:"0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Status | contains | resolved_auto | excludes:Status field:"Status" value:"resolved_auto" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertCategories | project |
AlertCount | project |
CreationTime | project |
Description | project |
HostCount | project |
Hosts_s | project |
IncidentId | project |
IncidentSources | project |
ModificationTime | project |
Severity | project |
Status | project |
TimeGenerated | project |
UserCount | project |
Users_s | project |
XdrUrl | project |
Credential Dumping Tools - File Artifacts
#This query detects the creation of credential dumping tools files. Several credential dumping tools export files with hardcoded file names. Ref: https://jpcertcc.github.io/ToolAnalysisResultSheet/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 11: FileCreate |
Rule Body
id: 32ffb19e-8ed8-40ed-87a0-1adb4746b7c4
name: Credential Dumping Tools - File Artifacts
description: |
'This query detects the creation of credential dumping tools files. Several credential dumping tools export files with hardcoded file names.
Ref: https://jpcertcc.github.io/ToolAnalysisResultSheet/'
severity: High
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- Event
- connectorId: WindowsSecurityEvents
dataTypes:
- Event
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1003.001
query: |
// Enter a reference list of malicious file artifacts
let MaliciousFileArtifacts = dynamic (["lsass.dmp","test.pwd","lsremora.dll","lsremora64.dll","fgexec.exe","pwdump","kirbi","wce_ccache","wce_krbtkts","wceaux.dll","PwHashes","SAM.out","SECURITY.out","SYSTEM.out","NTDS.out" "DumpExt.dll","DumpSvc.exe","cachedump64.exe","cachedump.exe","pstgdump.exe","servpw64.exe","servpw.exe","pwdump.exe","fgdump-log"]);
Event
| where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID==11
| parse EventData with * 'TargetFilename">' TargetFilename "<" *
| where TargetFilename has_any (MaliciousFileArtifacts)
| parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Image">' Image "<" *
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, Image, ProcessGuid, TargetFilename
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
entityMappings:
- entityType: File
fieldMappings:
- identifier: Name
columnName: TargetFilename
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: Image
version: 1.0.3
kind: Scheduled
Stages and Predicates
Let Binding: MaliciousFileArtifacts
let MaliciousFileArtifacts = dynamic (["lsass.dmp","test.pwd","lsremora.dll","lsremora64.dll","fgexec.exe","pwdump","kirbi","wce_ccache","wce_krbtkts","wceaux.dll","PwHashes","SAM.out","SECURITY.out","SYSTEM.out","NTDS.out" "DumpExt.dll","DumpSvc.exe","cachedump64.exe","cachedump.exe","pstgdump.exe","servpw64.exe","servpw.exe","pwdump.exe","fgdump-log"]);
Stage 1: source
Event
Stage 2: where
| where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID==11
Stage 3: parse
| parse EventData with * 'TargetFilename">' TargetFilename "<" *
Stage 4: where
| where TargetFilename has_any (MaliciousFileArtifacts)
Stage 5: parse
| parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Image">' Image "<" *
Stage 6: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, Image, ProcessGuid, TargetFilename
Stage 7: extend
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"11" |
EventLog | eq |
| field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational" |
TargetFilename | match |
| field:"TargetFilename" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
EndTime | summarize |
EventID | summarize |
Image | summarize |
ProcessGuid | summarize |
StartTime | summarize |
TargetFilename | summarize |
DnsDomain | extend |
HostName | extend |
Dataverse - Bulk record ownership re-assignment or sharing
#Identifies individual record ownership changes including sharing of records with other users/teams or re-assignment of ownership exceeding a pre-defined threshold.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: 6e480329-84bc-409a-b97b-22e8102af3ca
kind: Scheduled
name: Dataverse - Bulk record ownership re-assignment or sharing
description: Identifies individual record ownership changes including sharing of records
with other users/teams or re-assignment of ownership exceeding a pre-defined threshold.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1548
query: |
// Set threshold for number of shared/assigned records
let detection_threshold = 100;
let query_frequency = 1h;
DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| where Message in ("ModifyAccess", "Assign", "GrantAccess")
| summarize
FirstEvent = min(TimeGenerated),
LastEvent = max(TimeGenerated),
Events = count()
by UserId, Message, InstanceUrl, ClientIp
| where Events > detection_threshold
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
FirstEvent,
LastEvent,
Message,
Events,
UserId,
ClientIp,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
alertDetailsOverride:
alertDisplayNameFormat: Dataverse - High number of record access modification events
detected
alertDescriptionFormat: '{{Events}} events of type {{Message}} detected in {{InstanceUrl}}
could indicate suspicious or malicious activity.'
version: 3.2.0
Stages and Predicates
Parameters
let detection_threshold = 100;
let query_frequency = 1h;
Stage 1: source
DataverseActivity
Stage 2: where
| where TimeGenerated >= ago(query_frequency)
Stage 3: where
| where Message in ("ModifyAccess", "Assign", "GrantAccess")
Stage 4: summarize
| summarize
FirstEvent = min(TimeGenerated),
LastEvent = max(TimeGenerated),
Events = count()
by UserId, Message, InstanceUrl, ClientIp
Stage 5: where
| where Events > detection_threshold
Stage 6: extend
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
Stage 7: project
| project
FirstEvent,
LastEvent,
Message,
Events,
UserId,
ClientIp,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Events | gt |
| field:"Events" kind:gt value:"100" |
Message | in |
| field:"Message" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
Events | project |
FirstEvent | project |
InstanceUrl | project |
LastEvent | project |
Message | project |
UPNSuffix | project |
UserId | project |
DDoS attack detected
#Identifies DDoS attack in Azure Firewall IDPS logs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: c3ffdbe6-2e62-4984-9e80-933ed90b2f6a
name: DDoS attack detected
description: |
Identifies DDoS attack in Azure Firewall IDPS logs.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureFirewall
dataTypes:
- AZFWIdpsSignature
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1498
query: |
let TimeWindow = 90d;
let HitThreshold = 10;
let MinSeverity = 2;
let EnableCategoryFilter = true;
let EnableDescriptionFilter = false;
let EnableActionFilter = false;
let CategoriesOfInterest = dynamic([
"Attempted Denial of Service",
"Denial of Service",
"Detection of a Denial of Service Attack"
]);
let DescriptionsOfInterest = dynamic([
"attempted-dos",
"successful-dos",
"denial-of-service"
]);
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([
"Attempted Denial of Service",
"Denial of Service",
"Detection of a Denial of Service Attack"
]);
Let Binding: DescriptionsOfInterest
let DescriptionsOfInterest = dynamic([
"attempted-dos",
"successful-dos",
"denial-of-service"
]);
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 |
GitLab - Abnormal number of repositories deleted
#'This hunting queries identify an unusual increase of repo deletion activities adversaries may want to disrupt availability or compromise integrity by deleting business data.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 3efd09bd-a582-4410-b7ec-5ff21cfad7bd
name: GitLab - Abnormal number of repositories deleted
description: |
'This hunting queries identify an unusual increase of repo deletion activities adversaries may want to disrupt availability or compromise integrity by deleting business data.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1485
query: |
let LearningPeriod = 7d;
let BinTime = 1h;
let RunTime = 1h;
let StartTime = 1h;
let NumberOfStds = 3;
let MinThreshold = 10.0;
let EndRunTime = StartTime - RunTime;
let EndLearningTime = StartTime + LearningPeriod;
let GitLabRepositoryDestroyEvents = (GitLabAudit
| where RemoveAction == "project" or RemoveAction == "repository");
GitLabRepositoryDestroyEvents
| where TimeGenerated between (ago(EndLearningTime) .. ago(StartTime))
| summarize count() by bin(TimeGenerated, BinTime)
| summarize AvgInLearning = avg(count_), StdInLearning = stdev(count_)
| extend LearningThreshold = max_of(AvgInLearning + StdInLearning * NumberOfStds, MinThreshold)
| extend Dummy = 1
| join kind=innerunique (GitLabRepositoryDestroyEvents
| where TimeGenerated between (ago(StartTime) .. ago(EndRunTime))
| summarize CountInRunTime = count() by bin(TimeGenerated, BinTime)
| extend Dummy = 1) on Dummy
| project-away Dummy
| where CountInRunTime > LearningThreshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AuthorName
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let LearningPeriod = 7d;
let BinTime = 1h;
let RunTime = 1h;
let StartTime = 1h;
let NumberOfStds = 3;
let MinThreshold = 10.0;
let EndRunTime = StartTime - RunTime;
let EndLearningTime = StartTime + LearningPeriod;
let GitLabRepositoryDestroyEvents is inlined into the numbered stages below.
Stages 1 to 2 define let GitLabRepositoryDestroyEvents as the rule's main pipeline source. Stages 3 to 10 use it.
Stage 1: source
GitLabAudit
Stage 2: where
| where RemoveAction == "project" or RemoveAction == "repository"
Stage 3: where
GitLabRepositoryDestroyEvents
| where TimeGenerated between (ago(EndLearningTime) .. ago(StartTime))
Stage 4: summarize
| summarize count() by bin(TimeGenerated, BinTime)
Stage 5: summarize
| summarize AvgInLearning = avg(count_), StdInLearning = stdev(count_)
Stage 6: extend
| extend LearningThreshold = max_of(AvgInLearning + StdInLearning * NumberOfStds, MinThreshold)
Stage 7: extend
| extend Dummy = 1
Stage 8: join
| join kind=innerunique (GitLabRepositoryDestroyEvents
| where TimeGenerated between (ago(StartTime) .. ago(EndRunTime))
| summarize CountInRunTime = count() by bin(TimeGenerated, BinTime)
| extend Dummy = 1) on Dummy
Stage 9: project-away
project-away Dummy
Stage 10: where
where CountInRunTime > LearningThreshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
RemoveAction | eq |
| field:"RemoveAction" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AvgInLearning | summarize |
StdInLearning | summarize |
LearningThreshold | extend |
GWorkspace - Possible maldoc file name in Google drive
#'Detects possible maldoc file name in Google drive.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: d80d02a8-5da6-11ec-bf63-0242ac130002
name: GWorkspace - Possible maldoc file name in Google drive
description: |
'Detects possible maldoc file name in Google drive.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: GoogleWorkspaceReportsAPI
dataTypes:
- GWorkspaceActivityReports
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
GWorkspaceActivityReports
| where isnotempty(DocTitle)
| where DocTitle contains "invoice" or DocTitle contains "payment" or DocTitle contains "order" or DocTitle contains "fax" or DocTitle contains "scan" or DocTitle contains "transfer" or DocTitle contains "report" or DocTitle contains "bill"
| extend FileCustomEntity = DocTitle, AccountCustomEntity = ActorEmail
entityMappings:
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileCustomEntity
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
GWorkspaceActivityReports
Stage 2: where
| where isnotempty(DocTitle)
Stage 3: where
| where DocTitle contains "invoice" or DocTitle contains "payment" or DocTitle contains "order" or DocTitle contains "fax" or DocTitle contains "scan" or DocTitle contains "transfer" or DocTitle contains "report" or DocTitle contains "bill"
Stage 4: extend
| extend FileCustomEntity = DocTitle, AccountCustomEntity = ActorEmail
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DocTitle | contains |
| field:"DocTitle" kind:contains |
DocTitle | is_not_null | field:"DocTitle" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
FileCustomEntity | extend |
Hunt domains with Seamless SSO enabled in Entra ID Connect
#With below KQL query you can search through the IdentityLogon events of Microsoft Defender for Identity to find users and devices still using Seamless SSO in Entra ID Connect. This feature has been marked by the community multiple times as a security risk, and should be disabled if not in use. The KQL query returns the domains where Seamless SSO is enabled, allong with the related users and devices. On top of that, devices get enriched to find their OS distribution, version, and join type and tells you if Seamless SSO is expected to be used for the related device or not. If there are no results or if all results are showing 'No' for the 'Seamless SSO Expected' column, it should be save to disable the feature in Entra ID connect. !Important: This query relies on the Domain Controller EventID 4769 and Defender for Identity. Make sure the EventID is being logged and Defender for Identity is healthy. For more information see references!
References
- https://nathanmcnulty.com/blog/2025/08/finding-seamless-sso-usage/#
- https://ourcloudnetwork.com/why-you-should-disable-seamless-sso-in-microsoft-entra-connect/
- https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso-faq#how-can-i-disable-seamless-sso-
- https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso
Telemetry Coverage
Rule Body
// Get all device info we can find
let devices = (
DeviceInfo
// Search for 14 days
| where TimeGenerated > ago(14d)
// Normalize DeviceName
// --> if it is an IP Address we keep it
// --> If it is not an IP Address we only use the hostname for correlation
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
// Only get interesting data
| distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);
IdentityLogonEvents
// Get the last 30 days of logon events on Domain Controllers
| where TimeGenerated > ago(30d)
// Search for Seamless SSO events
| where Application == "Active Directory" and Protocol == "Kerberos"
| where TargetDeviceName == "AZUREADSSOACC"
// Save the domain name of the Domain Controller
| extend OnPremisesDomainName = strcat(split(DestinationDeviceName, ".")[-2], ".", split(DestinationDeviceName, ".")[-1])
// Normalize DeviceName
// --> if it is an IP Address we keep it
// --> If it is not an IP Address we only use the hostname for correlation
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
// Only use interesting data and find more info regarding the source device
| distinct AccountUpn, OnPremisesDomainName, DeviceName
| join kind=leftouter devices on DeviceName
| project-away DeviceName1
// Check if Seamless SSO usage is expected
| extend ['Seamless SSO Expected'] = case(
// Cases where we do not expect Seamless SSO to be used
JoinType == "Hybrid Azure AD Join" or
JoinType == "AAD Joined" or
JoinType == "AAD Registered", "No",
// Cases where we do expect Seamless SSO to be used
JoinType == "Domain Joined" or
(OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0) , "Yes",
// Cases that need to be verified
"Unknown (to verify)"
)
Stages and Predicates
Let Binding: devices
let devices = (
DeviceInfo
| where TimeGenerated > ago(14d)
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
| distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);
Stage 1: source
let devices
Stage 2: source
IdentityLogonEvents
Stage 3: where
where TimeGenerated > ago(2592000s)
Stage 4: where
where Application == "Active Directory" and Protocol == "Kerberos"
Stage 5: where
where TargetDeviceName == "AZUREADSSOACC"
Stage 6: extend
extend OnPremisesDomainName
Stage 7: extend
extend DeviceName
DeviceName =(ipv4_is_in_range(DeviceName, "10.0.0.0/8") or ipv4_is_in_range(DeviceName, "172.16.0.0/12") or ipv4_is_in_range(DeviceName, "192.168.0.0/16") or ipv4_is_in_range(DeviceName, "169.254.0.0/16") or ipv4_is_in_range(DeviceName, "127.0.0.0/8"))DeviceNametolower(split(DeviceName, ".")[0])Stage 8: distinct
distinct AccountUpn, DeviceName, OnPremisesDomainName
Stage 9: join
join kind=leftouter (devices) on DeviceName
Stage 10: project-away
project-away DeviceName1
Stage 11: extend
extend Seamless SSO Expected
Seamless SSO Expected =((JoinType == "Hybrid Azure AD Join" or JoinType == "AAD Joined") or JoinType == "AAD Registered")"No"(JoinType == "Domain Joined" or (OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0))"Yes""Unknown (to verify)"Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Application | eq |
| field:"Application" kind:eq value:"Active Directory" |
Protocol | eq |
| field:"Protocol" kind:eq value:"Kerberos" |
TargetDeviceName | eq |
| field:"TargetDeviceName" kind:eq value:"AZUREADSSOACC" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
OnPremisesDomainName | extend |
DeviceName | extend |
Seamless SSO Expected | extend |
Lookout - High Severity Mobile Threats Detected (v2)
#'Detects high severity mobile threats from Lookout Mobile Risk API v2 with enhanced threat intelligence and device context. This rule leverages the comprehensive v2 field set to provide detailed threat classification, risk assessment, and device compliance status for improved security monitoring.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Evasion | |
| Discovery |
Rule Body
id: 8b4a5c7e-2f91-4d8a-9e3b-1c6f8a2d4e9f
name: Lookout - High Severity Mobile Threats Detected (v2)
description: |
'Detects high severity mobile threats from Lookout Mobile Risk API v2 with enhanced threat intelligence and device context. This rule leverages the comprehensive v2 field set to provide detailed threat classification, risk assessment, and device compliance status for improved security monitoring.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: LookoutAPI
dataTypes:
- LookoutEvents
queryFrequency: 5m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
- DefenseEvasion
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1424
- T1418
- T1629
- T1630
query: |
LookoutEvents
| where EventType == "THREAT"
| where ThreatSeverity in ("CRITICAL", "HIGH")
| where ThreatStatus in ("OPEN", "ACTIVE")
| where ThreatAction == "DETECTED"
| extend
ThreatRiskScore = case(
ThreatSeverity == "CRITICAL", 10,
ThreatSeverity == "HIGH", 8,
ThreatSeverity == "MEDIUM", 5,
ThreatSeverity == "LOW", 2,
1
),
DeviceRiskLevel = case(
DeviceSecurityStatus == "THREATS_HIGH", "High",
DeviceSecurityStatus == "THREATS_MEDIUM", "Medium",
DeviceSecurityStatus == "THREATS_LOW", "Low",
"Unknown"
),
ThreatCategory = case(
ThreatClassifications has "MALWARE", "Malware",
ThreatClassifications has "PHISHING", "Phishing",
ThreatClassifications has "SPYWARE", "Spyware",
ThreatClassifications has "TROJAN", "Trojan",
ThreatClassifications has "ADWARE", "Adware",
"Other"
)
| extend ComplianceImpact = case(
DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 8, "Critical",
DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 5, "High",
DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 8, "High",
DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 5, "Medium",
"Low"
)
| project
TimeGenerated,
EventId,
ThreatId,
ThreatType,
ThreatSeverity,
ThreatRiskScore,
ThreatCategory,
ThreatClassifications,
ThreatStatus,
ThreatDescription,
ThreatApplicationName,
ThreatPackageName,
ThreatPackageSha,
DeviceGuid,
DevicePlatform,
DeviceOSVersion,
DeviceManufacturer,
DeviceModel,
DeviceEmailAddress,
DeviceSecurityStatus,
DeviceRiskLevel,
DeviceComplianceStatus,
ComplianceImpact,
ClientLookoutSDKVersion,
MDMConnectorId,
MDMExternalId,
TargetEmailAddress,
TargetPlatform,
ActorType,
ActorGuid
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: DeviceEmailAddress
- identifier: Name
columnName: TargetEmailAddress
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceGuid
- identifier: OSFamily
columnName: DevicePlatform
- identifier: OSVersion
columnName: DeviceOSVersion
- entityType: FileHash
fieldMappings:
- identifier: Algorithm
columnName: ThreatApplicationName
- identifier: Value
columnName: ThreatPackageSha
customDetails:
ThreatType: ThreatType
ThreatSeverity: ThreatSeverity
ThreatRiskScore: ThreatRiskScore
ThreatCategory: ThreatCategory
ThreatClasses: ThreatClassifications
ThreatStatus: ThreatStatus
DevicePlatform: DevicePlatform
DeviceSecStatus: DeviceSecurityStatus
DeviceRiskLevel: DeviceRiskLevel
ComplianceImpact: ComplianceImpact
MDMConnectorId: MDMConnectorId
alertDetailsOverride:
alertDisplayNameFormat: "High Severity Mobile Threat: {{ThreatType}} on {{DevicePlatform}} Device"
alertDescriptionFormat: "{{ThreatSeverity}} {{ThreatCategory}} threat on {{DevicePlatform}}"
alertTacticsColumnName: ThreatCategory
alertSeverityColumnName: ThreatSeverity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: Selected
groupByEntities:
- Account
- Host
groupByAlertDetails:
- ThreatType
- DeviceGuid
groupByCustomDetails:
- ThreatCategory
- DevicePlatform
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 2.0.5
kind: Scheduled
Stages and Predicates
Stage 1: source
LookoutEvents
Stage 2: where
| where EventType == "THREAT"
Stage 3: where
| where ThreatSeverity in ("CRITICAL", "HIGH")
Stage 4: where
| where ThreatStatus in ("OPEN", "ACTIVE")
Stage 5: where
| where ThreatAction == "DETECTED"
Stage 6: extend
| extend
ThreatRiskScore = case(
ThreatSeverity == "CRITICAL", 10,
ThreatSeverity == "HIGH", 8,
ThreatSeverity == "MEDIUM", 5,
ThreatSeverity == "LOW", 2,
1
),
DeviceRiskLevel = case(
DeviceSecurityStatus == "THREATS_HIGH", "High",
DeviceSecurityStatus == "THREATS_MEDIUM", "Medium",
DeviceSecurityStatus == "THREATS_LOW", "Low",
"Unknown"
),
ThreatCategory = case(
ThreatClassifications has "MALWARE", "Malware",
ThreatClassifications has "PHISHING", "Phishing",
ThreatClassifications has "SPYWARE", "Spyware",
ThreatClassifications has "TROJAN", "Trojan",
ThreatClassifications has "ADWARE", "Adware",
"Other"
)
DeviceRiskLevel =DeviceSecurityStatus == "THREATS_HIGH""High"DeviceSecurityStatus == "THREATS_MEDIUM""Medium"DeviceSecurityStatus == "THREATS_LOW""Low""Unknown"ThreatCategory =ThreatClassifications has "MALWARE""Malware"ThreatClassifications has "PHISHING""Phishing"ThreatClassifications has "SPYWARE""Spyware"ThreatClassifications has "TROJAN""Trojan"ThreatClassifications has "ADWARE""Adware""Other"ThreatRiskScore =ThreatSeverity == "CRITICAL"10ThreatSeverity == "HIGH"8ThreatSeverity == "MEDIUM"5ThreatSeverity == "LOW"21Stage 7: extend
| extend ComplianceImpact = case(
DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 8, "Critical",
DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 5, "High",
DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 8, "High",
DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 5, "Medium",
"Low"
)
ComplianceImpact =DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 8"Critical"DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 5"High"DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 8"High"DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 5"Medium""Low"Stage 8: project
| project
TimeGenerated,
EventId,
ThreatId,
ThreatType,
ThreatSeverity,
ThreatRiskScore,
ThreatCategory,
ThreatClassifications,
ThreatStatus,
ThreatDescription,
ThreatApplicationName,
ThreatPackageName,
ThreatPackageSha,
DeviceGuid,
DevicePlatform,
DeviceOSVersion,
DeviceManufacturer,
DeviceModel,
DeviceEmailAddress,
DeviceSecurityStatus,
DeviceRiskLevel,
DeviceComplianceStatus,
ComplianceImpact,
ClientLookoutSDKVersion,
MDMConnectorId,
MDMExternalId,
TargetEmailAddress,
TargetPlatform,
ActorType,
ActorGuid
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"THREAT" |
ThreatAction | eq |
| field:"ThreatAction" kind:eq value:"DETECTED" |
ThreatSeverity | in |
| field:"ThreatSeverity" kind:in |
ThreatStatus | in |
| field:"ThreatStatus" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorGuid | project |
ActorType | project |
ClientLookoutSDKVersion | project |
ComplianceImpact | project |
DeviceComplianceStatus | project |
DeviceEmailAddress | project |
DeviceGuid | project |
DeviceManufacturer | project |
DeviceModel | project |
DeviceOSVersion | project |
DevicePlatform | project |
DeviceRiskLevel | project |
DeviceSecurityStatus | project |
EventId | project |
MDMConnectorId | project |
MDMExternalId | project |
TargetEmailAddress | project |
TargetPlatform | project |
ThreatApplicationName | project |
ThreatCategory | project |
ThreatClassifications | project |
ThreatDescription | project |
ThreatId | project |
ThreatPackageName | project |
ThreatPackageSha | project |
ThreatRiskScore | project |
ThreatSeverity | project |
ThreatStatus | project |
ThreatType | project |
TimeGenerated | project |
LSASS Dumping using Debug Privileges
#This query searches for a process that requests the SeDebugPrivilege privilege and opens LSASS memory using specific permission 0x1fffff which represents PROCESS_ALL_ACCESS.
Known False Positives
- There are some applications that perform these actions for legitimate purposes. One example is Procmon by Sysinternals.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Credential Access |
References
Telemetry Coverage
Rule Body
let timeframe = 2*1h;
let SeDebugPrivilege = binary_shift_left(1, 20); // Value for SeDebugPrivilege is 2**20 = 0x100000.
let LSASSOpen=materialize (
DeviceEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "OpenProcessApiCall"
| where FileName =~ "lsass.exe"
| extend AccessRights=parse_json(AdditionalFields).DesiredAccess
| where AccessRights == 0x1fffff // PROCESS_ALL_ACCESS.
| summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1
);
DeviceEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "ProcessPrimaryTokenModified"
| where isnotempty(InitiatingProcessSHA1)
// Look for processes that request debug privilege that also opened LSASS
| where InitiatingProcessSHA1 in ((LSASSOpen | project InitiatingProcessSHA1)) // Speeds up the query.
| lookup kind=inner LSASSOpen on DeviceId, InitiatingProcessSHA1, InitiatingProcessId
// Check that debug privilege is enabled.
| extend AdditionalFields=parse_json(AdditionalFields)
| extend CurrentTokenPrivEnabled = toint(AdditionalFields.CurrentTokenPrivEnabled)
| extend OriginalTokenPrivEnabled = toint(AdditionalFields.OriginalTokenPrivEnabled)
// Value for SeDebugPrivilege is 2**20 = 0x100000.
// Refer to https://downloads.volatilityfoundation.org//omfw/2012/OMFW2012_Gurkok.pdf for numeric values for privileges.
| extend DebugPrivCurrent = binary_and(CurrentTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege
| extend DebugPrivOrig = binary_and(OriginalTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege
// Check for processes that have debug privilege after the event, but did not have it before.
| where not(DebugPrivOrig) and DebugPrivCurrent
| extend CleanCmdLine = parse_command_line(InitiatingProcessCommandLine, "windows")
| where not(InitiatingProcessFileName =~ "tasklist.exe" and CleanCmdLine has_any ("/m", "-m"))
| extend HostName=tostring(split(DeviceName,".")[0]),DnsDomain=iif(DeviceName contains ".", substring(DeviceName, indexof(DeviceName, ".") + 1, strlen(DeviceName)),"")
| project-reorder Timestamp, DeviceId, InitiatingProcessFileName
// Begin environment-specific filter.
// End environment-specific filter.
Stages and Predicates
Parameters
let timeframe = 2*1h;
let SeDebugPrivilege = binary_shift_left(1, 20);
Let Binding: LSASSOpen
let LSASSOpen = materialize (
DeviceEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "OpenProcessApiCall"
| where FileName =~ "lsass.exe"
| extend AccessRights=parse_json(AdditionalFields).DesiredAccess
| where AccessRights == 0x1fffff
| summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1
);
Stage 1: source
let LSASSOpen
Stage 2: source
DeviceEvents
Stage 3: where
where ...
Stage 4: where
where ActionType == "ProcessPrimaryTokenModified"
Stage 5: where
where isnotempty(InitiatingProcessSHA1)
Stage 6: where
where /* macro: (InitiatingProcessSHA1 in (LSASSOpen | project)) */
Stage 7: kusto:lookup
lookup kind=inner (LSASSOpen) on DeviceId, InitiatingProcessSHA1, InitiatingProcessId
Stage 8: extend (5 consecutive steps)
extend AdditionalFields, CurrentTokenPrivEnabled, DebugPrivCurrent, DebugPrivOrig, OriginalTokenPrivEnabled
Stage 9: where
where not (DebugPrivOrig == true) and DebugPrivCurrent == true
Stage 10: extend
extend CleanCmdLine
Stage 11: where
where not (((CleanCmdLine has "/m" or CleanCmdLine has "-m") and InitiatingProcessFileName =~ "tasklist.exe"))
Stage 12: extend
extend DnsDomain, HostName
Stage 13: project-reorder
project-reorder
Stage 14: summarize aggregation inside the lookup branch
summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccessRights | eq |
| field:"AccessRights" kind:eq value:"2097151" |
ActionType | eq |
| field:"ActionType" kind:eq |
DebugPrivCurrent | eq |
| field:"DebugPrivCurrent" kind:eq value:"true" |
FileName | eq |
| field:"FileName" kind:eq value:"lsass.exe" |
InitiatingProcessSHA1 | is_not_null | field:"InitiatingProcessSHA1" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DebugPrivOrig | eq | true | excludes:DebugPrivOrig field:"DebugPrivOrig" value:"true" |
CleanCmdLine | match | /m, -m | excludes:CleanCmdLine field:"CleanCmdLine" value:"/m" field:"CleanCmdLine" value:"-m" |
InitiatingProcessFileName | eq | tasklist.exe | excludes:InitiatingProcessFileName field:"InitiatingProcessFileName" value:"tasklist.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |
InitiatingProcessId | summarize |
InitiatingProcessSHA1 | summarize |
Malware Detection Session Finished
#Detects when malware detection session finishes.
Rule Body
id: 88b9223c-29ff-48a9-a745-c553aa0dbae2
name: Malware Detection Session Finished
description: Detects when malware detection session finishes.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "let action_results_lookup = union isfuzzy=true (datatable(JobResult:string,\
\ JobResultMessage:string)[]), (_GetWatchlist(\"action_results_lookup\")); \nVeeam_GetSecurityEvents\n\
| where instanceId == 42210\n| extend Result = extract(\"Result=\\\"([^\\\"]*)\\\
\"\", 1, SyslogMessage)\n| extend SessionID = extract(\"SessionID=\\\"([^\\\"]*)\\\
\"\", 1, SyslogMessage)\n| lookup kind=leftouter (action_results_lookup)\n on\
\ $left.Result == $right.JobResult\n| project\n Date = format_datetime(TimeGenerated,\
\ 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n EventId = instanceId,\n\
\ [\"Session ID\"] = SessionID,\n [\"StateMessage\"] = JobResultMessage,\n\
\ MessageDetails = Description,\n Severity = SeverityDescription\n"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Parameters
let action_results_lookup = union isfuzzy=true (datatable(JobResult:string, JobResultMessage:string)[]), (_GetWatchlist("action_results_lookup"));
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 42210
Stage 3: extend
| extend Result = extract("Result=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: extend
| extend SessionID = extract("SessionID=\"([^\"]*)\"", 1, SyslogMessage)
Stage 5: kusto:lookup
lookup kind=leftouter (action_results_lookup) on $left.Result, $right.JobResult
Stage 6: project
project DataSource, Date, EventId, MessageDetails, Session ID, Severity, StateMessage
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"42210" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Session ID | project |
Severity | project |
StateMessage | project |
Oracle - Put suspicious file
#'Detects PUT or POST of suspicious file'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Exfiltration |
Rule Body
id: edc2f2b4-573f-11ec-bf63-0242ac130002
name: Oracle - Put suspicious file
description: |
'Detects PUT or POST of suspicious file'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- OracleWebLogicServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Exfiltration
relevantTechniques:
- T1190
- T1133
- T1048
query: |
OracleWebLogicServerEvent
| where HttpRequestMethod in~ ("POST", "PUT")
| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
| where isnotempty(File)
| where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"
| extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File, UrlCustomEntity = UrlOriginal
entityMappings:
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileCustomEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
OracleWebLogicServerEvent
Stage 2: where
| where HttpRequestMethod in~ ("POST", "PUT")
Stage 3: extend
| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
Stage 4: where
| where isnotempty(File)
Stage 5: where
| where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"
Stage 6: extend
| extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File, UrlCustomEntity = UrlOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
File | is_not_null | field:"File" kind:is_not_null | |
File | regex_match |
| field:"File" kind:regex_match value:"([a-zA-Z0-9-_]+.)([a-zA-Z0-9-]+.[a-zA-Z0-9-]+)" |
HttpRequestMethod | in |
| field:"HttpRequestMethod" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
File | extend |
FileCustomEntity | extend |
IPCustomEntity | extend |
UrlCustomEntity | extend |
Stopping multiple processes using taskkill
#This query checks for attempts to stop at least 10 separate processes using the taskkill.exe utility. This is a common technique used by ransomware to stop security products and other processes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: 4dd31bd5-11a3-4b9c-a7c5-4927ab4f2a77
name: Stopping multiple processes using taskkill
description: |
This query checks for attempts to stop at least 10 separate processes using the taskkill.exe utility. This is a common technique used by ransomware to stop security products and other processes.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |
// Find attempts to stop processes using taskkill.exe
DeviceProcessEvents
| where FileName =~ "taskkill.exe"
| summarize taskKillCount = dcount(ProcessCommandLine), TaskKillList = make_set(ProcessCommandLine, 10000) by DeviceId, DeviceName, bin(TimeGenerated, 2m)
| where taskKillCount > 10
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
DeviceProcessEvents
Stage 2: where
| where FileName =~ "taskkill.exe"
Stage 3: summarize
| summarize taskKillCount = dcount(ProcessCommandLine), TaskKillList = make_set(ProcessCommandLine, 10000) by DeviceId, DeviceName, bin(TimeGenerated, 2m)
Stage 4: where
| where taskKillCount > 10
Stage 5: extend
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 6: extend
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FileName | eq |
| field:"process_basename" kind:eq value:"taskkill.exe" |
taskKillCount | gt |
| field:"taskKillCount" kind:gt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |
DeviceName | summarize |
TaskKillList | summarize |
taskKillCount | summarize |
HostName | extend |
DnsDomain | extend |
Suspicious SPN logon from workstation (DumpGuard)
#With the DumpGuard tool, attackers are able to dump credetials via Remote Credential Guard on devices that have Credential Guard enabled. Since the DumpGuard tool needs to use an SPN enabled account (in the POC they use a machine account) for two exploitation scenario's, it is interesting to look for TGT requests happening from client devices for SPN enabled accounts.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | IdentityLogonEvents action any: Identity logon activity |
Rule Body
let spn_accounts = toscalar(
// Search for all SPNs we can find in historic logs
IdentityLogonEvents
| where TimeGenerated > ago(14d)
| where Application == "Active Directory"
| where isnotempty(AdditionalFields.Spns)
| extend Spns = split(AdditionalFields.Spns, ",")
| summarize make_set(Spns)
);
let workstation_subnets = toscalar(
DeviceNetworkInfo
| where TimeGenerated > ago(14d)
// Filter out empty device names
| where isnotempty(DeviceName)
// Expand IP Addresses
| mv-expand todynamic(IPAddresses)
// Focus on device name and IP Address info
| distinct DeviceName, tostring(IPAddresses)
// Filter out IPv6 addresses, /32 addresses, and APIPA addresses
| where todynamic(IPAddresses).IPAddress !contains ":"
| where todynamic(IPAddresses).SubnetPrefix != "32"
| where todynamic(IPAddresses).IPAddress !startswith "169.254"
// Find Device Type of the device
| join kind=inner (
DeviceInfo
| where TimeGenerated > ago(30d)
| distinct DeviceName, DeviceType
) on DeviceName
// Only focus on workstations
| where DeviceType == "Workstation"
// Create Network Address based on the host IP Address and create a distinct list
| extend NetworkAddress = format_ipv4_mask(tostring(todynamic(IPAddresses).IPAddress), tolong(todynamic(IPAddresses).SubnetPrefix))
| summarize make_set(NetworkAddress)
);
IdentityLogonEvents
| where TimeGenerated > ago(1h)
// Get AD TGT requests by looking for Kerberos requests to KRBTGT account
| where Application == "Active Directory"
| where Protocol == "Kerberos"
| where AdditionalFields.Spns contains "krbtgt"
// Check for requests to account names with SPNs
| where AccountName in (spn_accounts)
// Check if IP Address is from a client range
| where ipv4_is_in_any_range(IPAddress, workstation_subnets)
// Optional - Ignore failed logins
| where ActionType != "LogonFailed"
Stages and Predicates
Let Binding: spn_accounts
let spn_accounts = toscalar(
IdentityLogonEvents
| where TimeGenerated > ago(14d)
| where Application == "Active Directory"
| where isnotempty(AdditionalFields.Spns)
| extend Spns = split(AdditionalFields.Spns, ",")
| summarize make_set(Spns)
);
Let Binding: workstation_subnets
let workstation_subnets = toscalar(
DeviceNetworkInfo
| where TimeGenerated > ago(14d)
| where isnotempty(DeviceName)
| mv-expand todynamic(IPAddresses)
| distinct DeviceName, tostring(IPAddresses)
| where todynamic(IPAddresses).IPAddress !contains ":"
| where todynamic(IPAddresses).SubnetPrefix != "32"
| where todynamic(IPAddresses).IPAddress !startswith "169.254"
| join kind=inner (
DeviceInfo
| where TimeGenerated > ago(30d)
| distinct DeviceName, DeviceType
) on DeviceName
| where DeviceType == "Workstation"
| extend NetworkAddress = format_ipv4_mask(tostring(todynamic(IPAddresses).IPAddress), tolong(todynamic(IPAddresses).SubnetPrefix))
| summarize make_set(NetworkAddress)
);
Stage 1: source
IdentityLogonEvents
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where Application == "Active Directory"
Stage 4: where
| where Protocol == "Kerberos"
Stage 5: where
| where AdditionalFields.Spns contains "krbtgt"
Stage 6: where
| where AccountName in (spn_accounts)
Stage 7: where
| where ipv4_is_in_any_range(IPAddress, workstation_subnets)
Stage 8: where
| where ActionType != "LogonFailed"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountName | in |
| field:"user" kind:in value:"spn_accounts" |
ActionType | ne |
| field:"ActionType" kind:ne value:"LogonFailed" |
AdditionalFields.Spns | contains |
| field:"AdditionalFields.Spns" kind:contains value:"krbtgt" |
Application | eq |
| field:"Image" kind:eq value:"Active Directory" |
Protocol | eq |
| field:"Protocol" kind:eq value:"Kerberos" |
TI map Domain entity to EmailUrlInfo
#Identifies a match in EmailUrlInfo table from any Domain IOC from TI.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
Rule Body
id: 87cc75df-d7b2-44f1-b064-ee924edfc879
name: TI map Domain entity to EmailUrlInfo
description: |
'Identifies a match in EmailUrlInfo table from any Domain IOC from TI.'
severity: Medium
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- EmailUrlInfo
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let dt_lookBack = 1h; // Define the lookback period for email data as 1 hour
let ioc_lookBack = 14d; // Define the lookback period for threat intelligence data as 14 days
let EmailUrlInfo_ = EmailUrlInfo
| where isnotempty(Url) or isnotempty(UrlDomain) // Filter for non-empty URLs or URL domains
| where TimeGenerated >= ago(dt_lookBack) // Filter for records within the lookback period
| extend Url = tolower(Url), UrlDomain = tolower(UrlDomain) // Convert URLs and domains to lowercase
| extend EmailUrlInfo_TimeGenerated = TimeGenerated; // Create a new column for the time generated
let EmailEvents_ = EmailEvents
| where TimeGenerated >= ago(dt_lookBack); // Filter email events within the lookback period
let TI_Urls = ThreatIntelligenceIndicator
| where TimeGenerated >= ago(ioc_lookBack) // Filter threat intelligence indicators within the lookback period
| where isnotempty(Url) // Filter for non-empty URLs
| extend Url = tolower(Url) // Convert URLs to lowercase
| join kind=innerunique (EmailUrlInfo_) on Url // Join with email URL info on URL
| where Active == true and ExpirationDateTime > now() // Filter for active indicators that haven't expired
| where EmailUrlInfo_TimeGenerated < ExpirationDateTime // Ensure email info was generated before the indicator expired
| summarize EmailUrlInfo_TimeGenerated = arg_max(EmailUrlInfo_TimeGenerated, *) by IndicatorId, Url // Get the latest email info for each indicator
| project EmailUrlInfo_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, UrlLocation, NetworkMessageId; // Select relevant columns
let TI_Domains = ThreatIntelligenceIndicator
| where TimeGenerated >= ago(ioc_lookBack) // Filter threat intelligence indicators within the lookback period
| where isnotempty(DomainName) // Filter for non-empty domain names
| extend DomainName = tolower(DomainName) // Convert domain names to lowercase
| join kind=innerunique (EmailUrlInfo_) on $left.DomainName == $right.UrlDomain // Join with email URL info on domain name
| where Active == true and ExpirationDateTime > now() // Filter for active indicators that haven't expired
| where EmailUrlInfo_TimeGenerated < ExpirationDateTime // Ensure email info was generated before the indicator expired
| summarize EmailUrlInfo_TimeGenerated = arg_max(EmailUrlInfo_TimeGenerated, *) by IndicatorId, UrlDomain // Get the latest email info for each indicator
| project EmailUrlInfo_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, UrlDomain, UrlLocation, NetworkMessageId; // Select relevant columns
union TI_Urls, TI_Domains // Combine URL and domain threat intelligence data
| extend timestamp = EmailUrlInfo_TimeGenerated // Add a timestamp column
| join kind=inner (EmailEvents_) on NetworkMessageId // Join with email events on network message ID
| where DeliveryAction !has "Blocked" // Filter out blocked delivery actions
| extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0]); // Extract name and UPN suffix from recipient email address
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: RecipientEmailAddress
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: EmailUrlInfo_
let EmailUrlInfo_ = EmailUrlInfo
| where isnotempty(Url) or isnotempty(UrlDomain)
| where TimeGenerated >= ago(dt_lookBack)
| extend Url = tolower(Url), UrlDomain = tolower(UrlDomain)
| extend EmailUrlInfo_TimeGenerated = TimeGenerated;
Let Binding: EmailEvents_
let EmailEvents_ = EmailEvents
| where TimeGenerated >= ago(dt_lookBack);
Union (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: TI_Urls, TI_Domains
Leg 1: TI_Urls
Leg 2: TI_Domains
Applied to the Combined Result
| extend timestamp = EmailUrlInfo_TimeGenerated | join kind=inner (EmailEvents_) on NetworkMessageId | where DeliveryAction !has "Blocked" | extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0]);
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 | |
Url | is_not_null | field:"Url" kind:is_not_null | |
UrlDomain | is_not_null | field:"UrlDomain" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DeliveryAction | match | Blocked | excludes:DeliveryAction field:"DeliveryAction" value:"Blocked" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ConfidenceScore | project |
Description | project |
EmailUrlInfo_TimeGenerated | project |
ExpirationDateTime | project |
IndicatorId | project |
NetworkMessageId | project |
ThreatType | project |
UrlDomain | project |
UrlLocation | project |
timestamp | extend |
Name | extend |
UPNSuffix | extend |
VMware SD-WAN Edge - Network Anomaly Detection - RPF Check Failure
#The VMware SD-WAN Edge appliance received packets that failed a Reverse Path Forwarding (RPF) Check. Reverse path forwarding (RPF) check is a network security mechanism that verifies whether the source IP address of a packet is reachable through the incoming interface on which the packet is received. The packet is dropped if the source IP address is not reachable through the incoming interface. RPF checks prevent spoofing attacks, in which an attacker uses a forged source IP address to make it appear that the packets are coming from a trusted source. This can allow the attacker to gain unauthorized network access or launch a denial-of-service attack against a target system. An IP fragmentation attack is a cyberattack that exploits how IP packets are fragmented and reassembled. IP fragmentation is a process by which large IP packets are broken down into smaller packets to transmit them over networks with smaller Maximum Transmission Unit (MTU) sizes. This analytics rule analyzes Syslog streams; these alerts are not reported by default if Search API is used.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 840b050f-842b-4264-8973-d4f9b65facb5
name: VMware SD-WAN Edge - Network Anomaly Detection - RPF Check Failure
version: 1.0.0
kind: Scheduled
description: |-
The VMware SD-WAN Edge appliance received packets that failed a Reverse Path Forwarding (RPF) Check.
Reverse path forwarding (RPF) check is a network security mechanism that verifies whether the source IP address of a packet is reachable through the incoming interface on which the packet is received. The packet is dropped if the source IP address is not reachable through the incoming interface.
RPF checks prevent spoofing attacks, in which an attacker uses a forged source IP address to make it appear that the packets are coming from a trusted source. This can allow the attacker to gain unauthorized network access or launch a denial-of-service attack against a target system.
An IP fragmentation attack is a cyberattack that exploits how IP packets are fragmented and reassembled. IP fragmentation is a process by which large IP packets are broken down into smaller packets to transmit them over networks with smaller Maximum Transmission Unit (MTU) sizes.
This analytics rule analyzes Syslog streams; these alerts are not reported by default if Search API is used.
severity: Low
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
requiredDataConnectors:
- connectorId: VMwareSDWAN
dataTypes:
- SDWAN
tactics:
- Impact
relevantTechniques:
- T1498
query: |+
Syslog
| where SyslogMessage contains "VCF Drop"
| where SyslogMessage contains "Reverse path forwarding check fail"
| project-rename EdgeName=HostName
| project-away Computer, HostIP, SourceSystem, Type
| extend OverlaySegmentName = extract("SEGMENT_NAME=(.+) COUNT=", 1, SyslogMessage)
| extend IpProtocol = extract("PROTO=(.+) SRC=", 1, SyslogMessage)
| extend SrcIpAddress = extract("SRC=(.+) DST=", 1, SyslogMessage)
| extend DstIpAddress = extract("DST=(.+) REASON=", 1, SyslogMessage)
| extend EdgeFwAction = extract("ACTION=(.+) SEGMENT=", 1, SyslogMessage)
| extend SyslogTag = extract("^(.+): ACTION=", 1, SyslogMessage)
| extend pcktCount = extract("COUNT=([0-9]+)$", 1, SyslogMessage)
| project
TimeGenerated,
EdgeFwAction,
EdgeName,
SrcIpAddress,
IpProtocol,
DstIpAddress,
pcktCount,
SyslogTag
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1h
matchingMethod: AllEntities
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
Edge_Name: EdgeName
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddress
suppressionDuration: 5h
Stages and Predicates
Stage 1: source
Syslog
Stage 2: where
| where SyslogMessage contains "VCF Drop"
Stage 3: where
| where SyslogMessage contains "Reverse path forwarding check fail"
Stage 4: project-rename
| project-rename EdgeName=HostName
Stage 5: project-away
| project-away Computer, HostIP, SourceSystem, Type
Stage 6: extend (7 consecutive steps)
| extend OverlaySegmentName = extract("SEGMENT_NAME=(.+) COUNT=", 1, SyslogMessage)
| extend IpProtocol = extract("PROTO=(.+) SRC=", 1, SyslogMessage)
| extend SrcIpAddress = extract("SRC=(.+) DST=", 1, SyslogMessage)
| extend DstIpAddress = extract("DST=(.+) REASON=", 1, SyslogMessage)
| extend EdgeFwAction = extract("ACTION=(.+) SEGMENT=", 1, SyslogMessage)
| extend SyslogTag = extract("^(.+): ACTION=", 1, SyslogMessage)
| extend pcktCount = extract("COUNT=([0-9]+)$", 1, SyslogMessage)
Stage 7: project
| project
TimeGenerated,
EdgeFwAction,
EdgeName,
SrcIpAddress,
IpProtocol,
DstIpAddress,
pcktCount,
SyslogTag
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | contains |
| field:"SyslogMessage" kind:contains |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstIpAddress | project |
EdgeFwAction | project |
EdgeName | project |
IpProtocol | project |
SrcIpAddress | project |
SyslogTag | project |
TimeGenerated | project |
pcktCount | project |