Detection rules › Kusto
Kusto rules: 3i
BloodHound Attack Path Finding - AS-REP Roastable User Accounts
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - AS-REP Roastable User Accounts
Rule Body
id: 1f9610b1-f9e0-4da2-9600-12f07ba6f437
name: BloodHound Attack Path Finding - AS-REP Roastable User Accounts
description: BloodHound Attack Path Finding - AS-REP Roastable User Accounts
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "ASREPRoasting"
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "ASREPRoasting"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"ASREPRoasting" |
BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC1 Privileges
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC1 Privileges
Rule Body
id: 1eb72567-2785-44d0-81dd-6a2b08803cdf
name: BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC1 Privileges
description: BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC1
Privileges
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"T0ADCSESC1\""
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "T0ADCSESC1"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0ADCSESC1" |
Detect CoreBackUp Deletion Activity from related Security Alerts
#'The query identifies any efforts by an attacker to delete backup containers, while also searching for any security alerts that may be linked to the same activity, in order to uncover additional information about the attacker's actions.' Though such an activity could be legitimate as part of business operation, some ransomware actors may perform such operation to cause interruption to regular business services.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 011c84d8-85f0-4370-b864-24c13455aa94
name: Detect CoreBackUp Deletion Activity from related Security Alerts
description: |
'The query identifies any efforts by an attacker to delete backup containers, while also searching for any security alerts that may be linked to the same activity, in order to uncover additional information about the attacker's actions.'
Though such an activity could be legitimate as part of business operation, some ransomware actors may perform such operation to cause interruption to regular business services.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureSecurityCenter
dataTypes:
- SecurityAlert
- connectorId: MicrosoftDefenderForCloudTenantBased
dataTypes:
- SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1496
query: |
SecurityAlert
| extend Extprop = parse_json(ExtendedProperties)
| mv-expand todynamic(Entities)
| extend HostName = iff(isnotempty(tostring(Extprop["Compromised Host"])), tolower(tostring(Extprop["Compromised Host"])), tolower(tostring(parse_json(Entities).HostName)))
| where isnotempty(HostName)
| mv-expand todynamic(split(HostName, ','))
| extend DnsDomain = iff(isnotempty(tostring(Extprop["Machine Domain"])), tostring(Extprop["Machine Domain"]), tostring(parse_json(Entities).DnsDomain))
| extend UserName = iff(isnotempty(tostring(Extprop["User Name"])), tostring(Extprop["User Name"]), iff(tostring(parse_json(Entities).Type) == 'account', tostring(parse_json(Entities).Name), ''))
| extend NTDomain = iff(isnotempty(tostring(Extprop["User Domain"])), tostring(Extprop["User Domain"]), tostring(parse_json(Entities).NTDomain))
| extend IpAddress = iff(tostring(parse_json(Entities).Type) == 'ip', tostring(parse_json(Entities).Address), tostring(parse_json(Extprop).["IpAddress"]))
| summarize timestamp = arg_max(TimeGenerated, *) by AlertName, tostring(HostName)
| project timestamp, AlertName, UserName, NTDomain, tostring(HostName), DnsDomain, IpAddress
| join kind=inner
(
CoreAzureBackup
| where State =~ "Deleted"
| where OperationName =~ "BackupItem"
| extend data = split(BackupItemUniqueId, ";")
| extend AzureLocation = data[0], VaultId=data[1], HostName=tolower(tostring(data[2])), DrivesBackedUp=data[3]
| project timestamp = TimeGenerated, AzureLocation, VaultId, HostName, DrivesBackedUp, State, BackupItemUniqueId, _ResourceId, OperationName, BackupItemFriendlyName
)
on HostName
| project timestamp, AlertName, HostName, DnsDomain, UserName, NTDomain, _ResourceId, IpAddress, VaultId, AzureLocation, DrivesBackedUp, State, BackupItemUniqueId, OperationName, BackupItemFriendlyName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserName
- identifier: NTDomain
columnName: NTDomain
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: _ResourceId
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityAlert
Stage 2: extend
| extend Extprop = parse_json(ExtendedProperties)
Stage 3: mv-expand
| mv-expand todynamic(Entities)
Stage 4: extend
| extend HostName = iff(isnotempty(tostring(Extprop["Compromised Host"])), tolower(tostring(Extprop["Compromised Host"])), tolower(tostring(parse_json(Entities).HostName)))
HostName =isnotempty(tostring(Extprop["Compromised Host"]))tolower(tostring(Extprop["Compromised Host"]))tolower(tostring(parse_json(Entities).HostName))Stage 5: where
| where isnotempty(HostName)
Stage 6: mv-expand
| mv-expand todynamic(split(HostName, ','))
Stage 7: extend (4 consecutive steps)
| extend DnsDomain = iff(isnotempty(tostring(Extprop["Machine Domain"])), tostring(Extprop["Machine Domain"]), tostring(parse_json(Entities).DnsDomain))
| extend UserName = iff(isnotempty(tostring(Extprop["User Name"])), tostring(Extprop["User Name"]), iff(tostring(parse_json(Entities).Type) == 'account', tostring(parse_json(Entities).Name), ''))
| extend NTDomain = iff(isnotempty(tostring(Extprop["User Domain"])), tostring(Extprop["User Domain"]), tostring(parse_json(Entities).NTDomain))
| extend IpAddress = iff(tostring(parse_json(Entities).Type) == 'ip', tostring(parse_json(Entities).Address), tostring(parse_json(Extprop).["IpAddress"]))
DnsDomain =isnotempty(tostring(Extprop["Machine Domain"]))tostring(Extprop["Machine Domain"])tostring(parse_json(Entities).DnsDomain)Stage 8: summarize
| summarize timestamp = arg_max(TimeGenerated, *) by AlertName, tostring(HostName)
Stage 9: project
| project timestamp, AlertName, UserName, NTDomain, tostring(HostName), DnsDomain, IpAddress
Stage 10: join
| join kind=inner
(
CoreAzureBackup
| where State =~ "Deleted"
| where OperationName =~ "BackupItem"
| extend data = split(BackupItemUniqueId, ";")
| extend AzureLocation = data[0], VaultId=data[1], HostName=tolower(tostring(data[2])), DrivesBackedUp=data[3]
| project timestamp = TimeGenerated, AzureLocation, VaultId, HostName, DrivesBackedUp, State, BackupItemUniqueId, _ResourceId, OperationName, BackupItemFriendlyName
)
on HostName
Stage 11: project
project AlertName, AzureLocation, BackupItemFriendlyName, BackupItemUniqueId, DnsDomain, DrivesBackedUp, HostName, IpAddress, NTDomain, OperationName, State, UserName, VaultId, _ResourceId, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HostName | is_not_null | field:"HostName" kind:is_not_null | |
OperationName | eq |
| field:"OperationName" kind:eq value:"BackupItem" |
State | eq |
| field:"State" kind:eq value:"Deleted" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertName | project |
AzureLocation | project |
BackupItemFriendlyName | project |
BackupItemUniqueId | project |
DnsDomain | project |
DrivesBackedUp | project |
HostName | project |
IpAddress | project |
NTDomain | project |
OperationName | project |
State | project |
UserName | project |
VaultId | project |
_ResourceId | project |
timestamp | project |
Doppelpaymer Stop Services
#This query was originally published in the threat analytics report, Doppelpaymer: More human-operated ransomware. There is also a related blog. DoppelPaymer is ransomware that is spread manually by human operators. These operators have exhibited extensive knowledge of system administration and common network security misconfigurations. They often use stolen credentials from over-privileged service accounts to turn off security software, run malicious commands, and spread malware throughout an organization. The following query detects attempts to stop security services. The See also section below lists links to other queries associated with DoppelPaymer. References: https://msrc-blog.microsoft.com/2019/11/20/customer-guidance-for-the-dopplepaymer-ransomware/ https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Ransom:Win32/DoppelPaymer.KM!MTB
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: 5bdc1504-880c-4b30-a39c-7c746535928d
name: Doppelpaymer Stop Services
description: |
This query was originally published in the threat analytics report, Doppelpaymer: More human-operated ransomware. There is also a related blog.
DoppelPaymer is ransomware that is spread manually by human operators. These operators have exhibited extensive knowledge of system administration and common network security misconfigurations. They often use stolen credentials from over-privileged service accounts to turn off security software, run malicious commands, and spread malware throughout an organization.
The following query detects attempts to stop security services.
The See also section below lists links to other queries associated with DoppelPaymer.
References:
https://msrc-blog.microsoft.com/2019/11/20/customer-guidance-for-the-dopplepaymer-ransomware/
https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Ransom:Win32/DoppelPaymer.KM!MTB
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- DefenseEvasion
relevantTechniques:
- T1059
- T1562
tags:
- DoppelPaymer
- Ransomware
query: |
// Attempts to stop services and allow ransomware execution
DeviceProcessEvents
| where InitiatingProcessFileName startswith "psexe" and FileName =~ "powershell.exe" and
ProcessCommandLine has "stop-service" and ProcessCommandLine has "sql" and ProcessCommandLine has "msexchange"
| 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 InitiatingProcessFileName startswith "psexe" and FileName =~ "powershell.exe" and
ProcessCommandLine has "stop-service" and ProcessCommandLine has "sql" and ProcessCommandLine has "msexchange"
Stage 3: extend
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 4: 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:"powershell.exe" |
InitiatingProcessFileName | starts_with |
| field:"parent_process_basename" kind:starts_with value:"psexe" |
ProcessCommandLine | match |
| field:"CommandLine" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostName | extend |
DnsDomain | extend |
GWorkspace - Multiple user agents for single source
#'Detects requests with different user agents from one source in short timeframe.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Collection |
Rule Body
id: 6ff0e16e-5999-11ec-bf63-0242ac130002
name: GWorkspace - Multiple user agents for single source
description: |
'Detects requests with different user agents from one source in short timeframe.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: GoogleWorkspaceReportsAPI
dataTypes:
- GWorkspaceActivityReports
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- Collection
relevantTechniques:
- T1185
- T1176
query: |
let threshold = 5;
GWorkspaceActivityReports
| where isnotempty(UserAgentOriginal)
| summarize user_ua = makeset(UserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
| where array_length(user_ua) > threshold
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 5;
Stage 1: source
GWorkspaceActivityReports
Stage 2: where
| where isnotempty(UserAgentOriginal)
Stage 3: summarize
| summarize user_ua = makeset(UserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 4: where
| where array_length(user_ua) > threshold
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
UserAgentOriginal | is_not_null | field:"UserAgentOriginal" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
user_ua | summarize |
IPCustomEntity | extend |
Password Exfiltration over SCIM application
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
This rule detects suspicious sync events that occurred to applications using SCIM for user provisioning.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 2e3c4ad5-8cb3-4b46-88ff-a88367ee7eaa
name: Password Exfiltration over SCIM application
kind: Scheduled
description: This rule detects suspicious sync events that occurred to applications using SCIM for user provisioning.
severity: High
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- InitialAccess
relevantTechniques:
- T1555
- T1040
- T1552
query: |-
Authomize_v2_CL
| where ingestion_time() >= ago(30m)
| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
| where Policy has "Password Exfiltration over SCIM application"
| project EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URL
requiredDataConnectors:
- connectorId: Authomize
dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: AnyAlert
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertnameFormat: Alert from Authomize - Password Exfiltration over SCIM application
alertDescriptionFormat: Password Exfiltration over SCIM application. This policy detects suspicious sync events that occurred to applications using SCIM for user provisioning.
alertSeverity: Severity
alertTactics: Tactics
alertDynamicProperties:
- alertProperty: AlertLink
value: URL
customDetails:
AuthomizeEventID: EventID
EventName: Policy
EventDescription: Description
EventRecommendation: Recommendation
ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2
Stages and Predicates
Stage 1: source
Authomize_v2_CL
Stage 2: where
| where ingestion_time() >= ago(30m)
Stage 3: extend
| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
Stage 4: where
| where Policy has "Password Exfiltration over SCIM application"
Stage 5: project
| project EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| field:"policy_name_s" kind:match value:"Password Exfiltration over SCIM application" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
Potential DGA detected
#'Identifies clients with a high NXDomain count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). Alerts are generated when a new IP address is seen (based on not being associated with NXDomain records in the prior 10-day baseline period).'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: a0907abe-6925-4d90-af2b-c7e89dc201a6
name: Potential DGA detected
description: |
'Identifies clients with a high NXDomain count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live).
Alerts are generated when a new IP address is seen (based on not being associated with NXDomain records in the prior 10-day baseline period).'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: DNS
dataTypes:
- DnsEvents
queryFrequency: 1d
queryPeriod: 10d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1568
- T1008
query: |
let starttime = 10d;
let endtime = 1d;
let threshold = 100;
let nxDomainDnsEvents = DnsEvents
// ResultCode 3 => 'NXDOMAIN'
| where ResultCode == 3
| where QueryType in~ ("A", "AAAA")
| where ipv4_is_match("127.0.0.1", ClientIP) == False
| where Name !has "/"
| where Name has ".";
nxDomainDnsEvents
| where TimeGenerated > ago(endtime)
// sld = Second Level Domain
| extend sld = tostring(split(Name, ".")[-2])
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), dcount(sld), sampleNXDomainList=make_set(Name, 100) by ClientIP
| where dcount_sld > threshold
// Filter out previously seen IPs
// Returns all the records from the left side that don't have matches from the right
| join kind=leftanti (nxDomainDnsEvents
| where TimeGenerated between(ago(starttime)..ago(endtime))
| extend sld = tostring(split(Name, ".")[-2])
| summarize dcount(sld) by ClientIP, bin(TimeGenerated,1d)
| where dcount_sld > threshold
) on ClientIP
| order by dcount_sld desc
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIP
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 10d;
let endtime = 1d;
let threshold = 100;
let nxDomainDnsEvents is inlined into the numbered stages below.
Stages 1 to 6 define let nxDomainDnsEvents as the rule's main pipeline source. Stages 7 to 12 use it.
Stage 1: source
DnsEvents
Stage 2: where
| where ResultCode == 3
Stage 3: where
| where QueryType in~ ("A", "AAAA")
Stage 4: where
| where ipv4_is_match("127.0.0.1", ClientIP) == False
Stage 5: where
| where Name !has "/"
Stage 6: where
| where Name has "."
Stage 7: where
nxDomainDnsEvents
| where TimeGenerated > ago(endtime)
Stage 8: extend
| extend sld = tostring(split(Name, ".")[-2])
Stage 9: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), dcount(sld), sampleNXDomainList=make_set(Name, 100) by ClientIP
Stage 10: where
| where dcount_sld > threshold
Stage 11: join (negated)
join kind=leftanti (nxDomainDnsEvents) on ClientIP
Stage 12: sort
sort by dcount_sld
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientIP | cidr_match |
| field:"ClientIP" kind:cidr_match value:"127.0.0.1" |
Name | match |
| field:"Name" kind:match |
QueryType | in |
| field:"QueryType" kind:in |
ResultCode | eq |
| field:"ResultCode" kind:eq value:"3" |
dcount_sld | gt |
| field:"dcount_sld" kind:gt value:"100" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ClientIP | cidr_match | 127.0.0.1 | excludes:ClientIP field:"ClientIP" value:"127.0.0.1" |
Name | match | / | excludes:Name field:"Name" value:"/" |
Name | match | . | excludes:Name field:"Name" value:"." |
QueryType | in | A, AAAA | excludes:QueryType field:"QueryType" value:"A" field:"QueryType" value:"AAAA" |
ResultCode | eq | 3 | excludes:ResultCode field:"ResultCode" value:"3" |
dcount_sld | gt | 100 | excludes:dcount_sld field:"dcount_sld" value:"100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientIP | summarize |
EndTimeUtc | summarize |
StartTimeUtc | summarize |
sampleNXDomainList | summarize |
Potential Ransomware activity related to Cobalt Strike
#This query searches for alerts related to suspected ransomware and Cobalt Strike activity, a tool used in numerous ransomware campaigns. It looks for alerts that indicate potential ransomware activity, such as attempts to clear security event logs, delete backup files, and execute Cobalt Strike malware.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Persistence | |
| Stealth | |
| Impact |
Telemetry Coverage
Rule Body
id: 4bd9ce9d-8586-4beb-8fdb-bd018cacbe7d
name: Potential Ransomware activity related to Cobalt Strike
description: |
This query searches for alerts related to suspected ransomware and Cobalt Strike activity, a tool used in numerous ransomware campaigns. It looks for alerts that indicate potential ransomware activity, such as attempts to clear security event logs, delete backup files, and execute Cobalt Strike malware.
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- AlertInfo
- AlertEvidence
- DeviceLogonEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- Persistence
- DefenseEvasion
- Impact
relevantTechniques:
- T1059
- T1078
- T1070
- T1490
query: |
// Look for sc.exe disabling services
AlertInfo
// Attempts to clear security event logs.
| where Title in("Event log was cleared",
// List alerts flagging attempts to delete backup files.
"File backups were deleted",
// Potential Cobalt Strike activity - Note that other threat activity can also
// trigger alerts for suspicious decoded content
"Suspicious decoded content",
// Cobalt Strike activity
"\'Atosev\' malware was detected",
"\'Ploty\' malware was detected",
"\'Bynoco\' malware was detected",
"\'Cobaltstrike\' malware was detected",
"Echo command over pipe on localhost",
"Known attack framework activity was observed",
"An active \'Cobaltstrike\' malware was detected",
"Suspicious \'CobaltStrike\' behavior was prevented",
"Suspicious process launch by Rundll32.exe")
| extend AlertTime = TimeGenerated | distinct AlertTime, AlertId, Title
| join AlertEvidence on $left.AlertId == $right.AlertId
| summarize by DeviceId, AlertTime, Title, AlertId
// Get device IDs
| join DeviceLogonEvents on $left.DeviceId == $right.DeviceId
// Creating 10 day Window surrounding alert activity
| where TimeGenerated < AlertTime + 12h and TimeGenerated > AlertTime - 12h // Projecting specific columns
| project Title, DeviceName, DeviceId, TimeGenerated, LogonType, AccountDomain, AccountName, AccountSid, AlertTime, AlertId, RemoteIP, RemoteDeviceName
| extend AccountFullName = tostring(strcat(AccountDomain, "\\", AccountName))
| 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
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountFullName
- identifier: Name
columnName: AccountName
- identifier: DnsDomain
columnName: AccountDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: RemoteIP
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
AlertInfo
Stage 2: where
| where Title in("Event log was cleared",
"File backups were deleted",
"Suspicious decoded content",
"\'Atosev\' malware was detected",
"\'Ploty\' malware was detected",
"\'Bynoco\' malware was detected",
"\'Cobaltstrike\' malware was detected",
"Echo command over pipe on localhost",
"Known attack framework activity was observed",
"An active \'Cobaltstrike\' malware was detected",
"Suspicious \'CobaltStrike\' behavior was prevented",
"Suspicious process launch by Rundll32.exe")
Stage 3: extend
| extend AlertTime = TimeGenerated
Stage 4: distinct
| distinct AlertTime, AlertId, Title
Stage 5: join
| join AlertEvidence on $left.AlertId == $right.AlertId
Stage 6: summarize
summarize by DeviceId, AlertTime, Title, AlertId
Stage 7: join
| join DeviceLogonEvents on $left.DeviceId == $right.DeviceId
Stage 8: where TimeGenerated - AlertTime < 12h
where TimeGenerated > (AlertTime - 12h)
Stage 9: project
project AccountDomain, AccountName, AccountSid, AlertId, AlertTime, DeviceId, DeviceName, LogonType, RemoteDeviceName, RemoteIP, TimeGenerated, Title
Stage 10: extend (3 consecutive steps)
extend AccountFullName, DnsDomain, HostName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Title | in |
| field:"Title" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountDomain | project |
AccountName | project |
AccountSid | project |
AlertId | project |
AlertTime | project |
DeviceId | project |
DeviceName | project |
LogonType | project |
RemoteDeviceName | project |
RemoteIP | project |
TimeGenerated | project |
Title | project |
AccountFullName | extend |
HostName | extend |
DnsDomain | extend |
Scheduled Task - Suspicious Network Connection
#Below query performs process tree analysis for Scheduled Tasks on MDE/MDATP/M365D and displays anomalous trees. Then, it gets all network connections made by every single process in each anomalous process tree. Before using the query, do a quick analysis on commandlines of the processes spawned by Scheduled Tasks. There might be specific processes executing with a unique argument on each device. You need to whitelist them to get better results.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | No specific technique |
Telemetry Coverage
Rule Body
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post: https://mergene.medium.com/hunting-for-the-behavior-scheduled-tasks-9efe0b8ade40
// Hypothesis: The goal of the persistence is keeping the C2 channel active.
// This query performs process tree analysis for Scheduled Tasks on MDE/MDATP/M365D and displays anomalous trees.
// Then, it gets all network connections made by every single process in each anomalous process tree.
// Before using the query, do a quick analysis on commandlines of the processes spawned by Scheduled Tasks.
// There might be specific processes executing with a unique argument on each device. You need to whitelist them to get better results.
let timeframe=7d;
let whitelisted_cmdlines = dynamic(["put_whiltested_commandlines_here"]);
let whitelist_folderpath = dynamic(["put_whitelisted_folderpaths-here"]);
let _process_tree_data = materialize (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| where InitiatingProcessFileName == "svchost.exe" and InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
| where not( ProcessCommandLine has_any (whitelisted_cmdlines ))
| where not (FolderPath has_any (whitelist_folderpath))
| summarize dcount(DeviceId), count() by ProcessCommandLine, FileName
| where dcount_DeviceId <= 5
| join kind=inner (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| where InitiatingProcessFileName == "svchost.exe" and InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
| where not( ProcessCommandLine has_any (whitelisted_cmdlines ))
| where not (FolderPath has_any (whitelist_folderpath))
) on ProcessCommandLine
| project DeviceId,DeviceName, Timestamp,
InitiatingProcessG3ParentFileName=FileName,InitiatingProcessG3ParentSHA1=SHA1,InitiatingProcessG3ParentId=ProcessId, InitiatingProcessG3ParentCommandLine=ProcessCommandLine,InitiatingProcessG3ParentCreationTime=todatetime(ProcessCreationTime),
InitiatingProcessG4ParentFileName=InitiatingProcessFileName,InitiatingProcessG4ParentSHA1=InitiatingProcessSHA1,InitiatingProcessG4ParentId=InitiatingProcessId,InitiatingProcessG4ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG4ParentCreationTime=todatetime(InitiatingProcessCreationTime)
// Start iteration
// 1st iteration of join. From now on, query all processes, rename fields, and join accordingly
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessG2ParentFileName=FileName,InitiatingProcessG2ParentFolderPath=FolderPath,InitiatingProcessG2ParentSHA1=SHA1, InitiatingProcessG2ParentId=ProcessId, InitiatingProcessG2ParentCommandLine=ProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(ProcessCreationTime),
InitiatingProcessG3ParentFileName=InitiatingProcessFileName,InitiatingProcessG3ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG3ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG3ParentId=InitiatingProcessId, InitiatingProcessG3ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG3ParentCreationTime=todatetime(InitiatingProcessCreationTime)
)
on DeviceId , InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentCreationTime
// 2nd iteration of join.
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessG1ParentFileName=FileName,InitiatingProcessG1ParentFolderPath=FolderPath,InitiatingProcessG1ParentSHA1=SHA1, InitiatingProcessG1ParentId=ProcessId, InitiatingProcessG1ParentCommandLine=ProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(ProcessCreationTime),
InitiatingProcessG2ParentFileName=InitiatingProcessFileName,InitiatingProcessG2ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG2ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG2ParentId=InitiatingProcessId, InitiatingProcessG2ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(InitiatingProcessCreationTime)
)
on DeviceId , InitiatingProcessG2ParentFileName , InitiatingProcessG2ParentId, InitiatingProcessG2ParentCreationTime
// 3rd iteration of join.
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessParentFileName=FileName,InitiatingProcessParentFolderPath=FolderPath,InitiatingProcessParentSHA1=SHA1, InitiatingProcessParentId=ProcessId, InitiatingProcessParentCommandLine=ProcessCommandLine, InitiatingProcessParentCreationTime=ProcessCreationTime,
InitiatingProcessG1ParentFileName=InitiatingProcessFileName,InitiatingProcessG1ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG1ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG1ParentId=InitiatingProcessId, InitiatingProcessG1ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(InitiatingProcessCreationTime)
)
on DeviceId , InitiatingProcessG1ParentFileName , InitiatingProcessG1ParentId, InitiatingProcessG1ParentCreationTime
// 4th iteration of join
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessFileName=FileName,InitiatingProcessSHA1=SHA1, InitiatingProcessId=ProcessId, InitiatingProcessCommandLine=ProcessCommandLine, InitiatingProcessCreationTime=ProcessCreationTime,
InitiatingProcessParentFileName=InitiatingProcessFileName,InitiatingProcessParentSHA1=InitiatingProcessSHA1, InitiatingProcessParentId=InitiatingProcessId, InitiatingProcessParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessParentCreationTime=InitiatingProcessCreationTime
)
on DeviceId , InitiatingProcessParentFileName , InitiatingProcessParentId, InitiatingProcessParentCreationTime
// 5th iteration of join
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project Timestamp, DeviceId, FileName,SHA1, ProcessId, ProcessCommandLine, ProcessCreationTime,
InitiatingProcessFileName,InitiatingProcessSHA1, InitiatingProcessId, InitiatingProcessCommandLine, InitiatingProcessCreationTime
)
on DeviceId , InitiatingProcessFileName , InitiatingProcessId, InitiatingProcessCreationTime
);
// Use the cached results and find the rare patterns based on process names.
// New fields "ProcessVersionInfoOriginalFileName" and "InitiatingProcessVersionInfoInternalFileName" can be used as well.
_process_tree_data
| where Timestamp > ago(1d) // get only the trees from last 1d.
// get the last occurence of the rare patterns.
| summarize arg_max(Timestamp,*), pattern_count=count() by DeviceId, InitiatingProcessG3ParentFileName, InitiatingProcessG2ParentFileName, InitiatingProcessG1ParentFileName, InitiatingProcessParentFileName, InitiatingProcessFileName, FileName
// We need to put all process nodes to the same column so that we can apply join for each process node.
| extend N_InitiatingProcessFileName = pack_array(InitiatingProcessG3ParentFileName,InitiatingProcessG2ParentFileName,InitiatingProcessG1ParentFileName,InitiatingProcessParentFileName,InitiatingProcessFileName,FileName),
N_InitiatingProcessCommandLine = pack_array(InitiatingProcessG3ParentCommandLine, InitiatingProcessG2ParentCommandLine, InitiatingProcessG1ParentCommandLine, InitiatingProcessParentCommandLine, InitiatingProcessCommandLine, ProcessCommandLine),
N_InitiatingProcessId = pack_array(InitiatingProcessG3ParentId,InitiatingProcessG2ParentId,InitiatingProcessG1ParentId,InitiatingProcessParentId,InitiatingProcessId,ProcessId),
N_InitiatingProcessCreationTime = pack_array(InitiatingProcessG3ParentCreationTime,InitiatingProcessG2ParentCreationTime,InitiatingProcessG1ParentCreationTime,InitiatingProcessParentCreationTime,InitiatingProcessCreationTime,ProcessCreationTime)
// apply mv-expand so that all process nodes are put into the same columns.
| mv-expand N_InitiatingProcessFileName, N_InitiatingProcessCommandLine, N_InitiatingProcessId, N_InitiatingProcessCreationTime
// generate a key for the join with DeviceNetworkEvents, remove rows if the process info is null(mv-expand results in some null values)
| extend join_key = strcat(DeviceId,'-',N_InitiatingProcessFileName,'-',N_InitiatingProcessId,'-',tostring(N_InitiatingProcessCreationTime))
| where isnotnull(N_InitiatingProcessId)
// generate join key for the DeviceNetworkEvents and apply join and exclude internal trafic(you may want to check internal traffic for lateral movement)
| join kind=inner (DeviceNetworkEvents | where Timestamp > ago(timeframe)| extend join_key = strcat(DeviceId,'-', InitiatingProcessFileName,'-', InitiatingProcessId,'-', tostring(InitiatingProcessCreationTime)) ) on join_key
| where RemoteIP !in ("::1","127.0.0.1","::ffff:127.0.0.1") and RemoteIPType <> "Private"
| where not(RemoteUrl has_any("corel.com","ocsp.digicert.com","avast.com"))
| where ActionType != "ListeningConnectionCreated"
| project-reorder pattern_count, RemoteUrl, RemoteIP, RemotePort
Stages and Predicates
Parameters
let timeframe = 7d;
let whitelisted_cmdlines = dynamic(["put_whiltested_commandlines_here"]);
let whitelist_folderpath = dynamic(["put_whitelisted_folderpaths-here"]);
let _process_tree_data is inlined into the numbered stages below.
Stage 1: source
let _process_tree_data
Stage 2: source
DeviceProcessEvents
Stage 3: where
where Timestamp > ago(604800s)
Stage 4: where
where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule" and InitiatingProcessFileName == "svchost.exe"
Stage 5: where
where not (ProcessCommandLine has "whitelisted_cmdlines")
Stage 6: where
where not (FolderPath has "whitelist_folderpath")
Stage 7: summarize
summarize by ProcessCommandLine, FileName
Stage 8: where
where dcount_DeviceId <= 5
Stage 9: join
join kind=inner (DeviceProcessEvents) on ProcessCommandLine
Stage 10: project
project DeviceId, DeviceName, InitiatingProcessG3ParentCommandLine, InitiatingProcessG3ParentCreationTime, InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentSHA1, InitiatingProcessG4ParentCommandLine, InitiatingProcessG4ParentCreationTime, InitiatingProcessG4ParentFileName, InitiatingProcessG4ParentId, InitiatingProcessG4ParentSHA1, Timestamp
Stage 11: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentCreationTime
Stage 12: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessG2ParentFileName, InitiatingProcessG2ParentId, InitiatingProcessG2ParentCreationTime
Stage 13: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessG1ParentFileName, InitiatingProcessG1ParentId, InitiatingProcessG1ParentCreationTime
Stage 14: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessParentFileName, InitiatingProcessParentId, InitiatingProcessParentCreationTime
Stage 15: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessFileName, InitiatingProcessId, InitiatingProcessCreationTime
Stage 16: where
where Timestamp > ago(86400s)
Stage 17: summarize
summarize pattern_count by DeviceId, InitiatingProcessG3ParentFileName, InitiatingProcessG2ParentFileName, InitiatingProcessG1ParentFileName, InitiatingProcessParentFileName, InitiatingProcessFileName, FileName
Stage 18: extend
extend N_InitiatingProcessCommandLine, N_InitiatingProcessCreationTime, N_InitiatingProcessFileName, N_InitiatingProcessId
Stage 19: mv-expand
mv-expand N_InitiatingProcessFileName
Stage 20: extend
extend join_key
Stage 21: where
where isnotnull(N_InitiatingProcessId)
Stage 22: join
join kind=inner (DeviceNetworkEvents) on join_key
Stage 23: where
where not (RemoteIP in ("127.0.0.1", "::1", "::ffff:127.0.0.1")) and RemoteIPType != "Private"
Stage 24: where
where not ((RemoteUrl has "corel.com" or RemoteUrl has "ocsp.digicert.com" or RemoteUrl has "avast.com"))
Stage 25: where
where ActionType != "ListeningConnectionCreated"
Stage 26: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | ne |
| field:"ActionType" kind:ne value:"ListeningConnectionCreated" |
InitiatingProcessCommandLine | eq |
| field:"InitiatingProcessCommandLine" kind:eq value:"svchost.exe -k netsvcs -p -s Schedule" |
InitiatingProcessFileName | eq |
| field:"InitiatingProcessFileName" kind:eq value:"svchost.exe" |
N_InitiatingProcessId | is_not_null | field:"N_InitiatingProcessId" kind:is_not_null | |
RemoteIPType | ne |
| field:"RemoteIPType" kind:ne value:"Private" |
dcount_DeviceId | le |
| field:"dcount_DeviceId" kind:le value:"5" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ProcessCommandLine | match | whitelisted_cmdlines | excludes:ProcessCommandLine field:"ProcessCommandLine" value:"whitelisted_cmdlines" |
FolderPath | match | whitelist_folderpath | excludes:FolderPath field:"FolderPath" value:"whitelist_folderpath" |
RemoteIP | in | 127.0.0.1, ::1, ::ffff:127.0.0.1 | excludes:RemoteIP field:"RemoteIP" value:"127.0.0.1" field:"RemoteIP" value:"::1" field:"RemoteIP" value:"::ffff:127.0.0.1" |
RemoteUrl | match | corel.com, ocsp.digicert.com, avast.com | excludes:RemoteUrl field:"RemoteUrl" value:"corel.com" field:"RemoteUrl" value:"ocsp.digicert.com" field:"RemoteUrl" value:"avast.com" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |
FileName | summarize |
InitiatingProcessFileName | summarize |
InitiatingProcessG1ParentFileName | summarize |
InitiatingProcessG2ParentFileName | summarize |
InitiatingProcessG3ParentFileName | summarize |
InitiatingProcessParentFileName | summarize |
pattern_count | summarize |
N_InitiatingProcessCommandLine | extend |
N_InitiatingProcessCreationTime | extend |
N_InitiatingProcessFileName | extend |
N_InitiatingProcessId | extend |
join_key | extend |
Semperis DSP Failed Logons
#Alerts when there are failed logons in the DSP system.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Semperis-Operation-Log | Event ID 20002: DSP failed logon |
Rule Body
id: "0e105444-fe13-4ce6-9239-21880076a3f9"
name: Semperis DSP Failed Logons
description: |
'Alerts when there are failed logons in the DSP system.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SemperisDSP
dataTypes:
- dsp_parser
queryFrequency: 30m
queryPeriod: 30m
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
- T1110
triggerOperator: gt
triggerThreshold: 0
query: |
SecurityEvent
| where EventSourceName == 'Semperis-Operation-Log' and EventID == 20002
| sort by TimeGenerated desc
| extend p1Xml = parse_xml(EventData).EventData.Data
| mv-expand bagexpansion=array p1Xml
| evaluate bag_unpack(p1Xml)
| extend Name=column_ifexists('@Name', ''), Value=column_ifexists('#text', '')
| evaluate pivot(Name, any(Value), TimeGenerated, EventSourceName, Channel, Computer, Level, EventLevelName, EventID, Task, Type, _ResourceId)
| extend det = column_ifexists('details', '')
| parse det with * "Trustee Name: " TrusteeName " Correlation ID: " * " Source: " HostIP "WebSite Target" *
| extend host = tostring(HostIP)
| extend HostIP = trim_end(":", HostIP)
| project TimeGenerated, TrusteeName, HostIP, _ResourceId
| extend NTDomain = tostring(split(TrusteeName, '\\', 0)[0]), Name = tostring(split(TrusteeName, '\\', 1)[0])
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: HostIP
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDisplayNameFormat: Failed Logon -- Alert from Semperis Directory Services Protector
alertDescriptionFormat: A failed logon was detected to the DSP system.
version: 2.0.7
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventSourceName == 'Semperis-Operation-Log' and EventID == 20002
Stage 3: sort
| sort by TimeGenerated desc
Stage 4: extend
| extend p1Xml = parse_xml(EventData).EventData.Data
Stage 5: mv-expand
| mv-expand bagexpansion=array p1Xml
Stage 6: evaluate
| evaluate bag_unpack(p1Xml)
Stage 7: extend
| extend Name=column_ifexists('@Name', ''), Value=column_ifexists('#text', '')
Stage 8: evaluate
| evaluate pivot(Name, any(Value), TimeGenerated, EventSourceName, Channel, Computer, Level, EventLevelName, EventID, Task, Type, _ResourceId)
Stage 9: extend
| extend det = column_ifexists('details', '')
Stage 10: parse
| parse det with * "Trustee Name: " TrusteeName " Correlation ID: " * " Source: " HostIP "WebSite Target" *
Stage 11: extend
| extend host = tostring(HostIP)
Stage 12: extend
| extend HostIP = trim_end(":", HostIP)
Stage 13: project
| project TimeGenerated, TrusteeName, HostIP, _ResourceId
Stage 14: extend
| extend NTDomain = tostring(split(TrusteeName, '\\', 0)[0]), Name = tostring(split(TrusteeName, '\\', 1)[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"20002" |
EventSourceName | eq |
| field:"Category" kind:eq value:"Semperis-Operation-Log" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostIP | project |
TimeGenerated | project |
TrusteeName | project |
_ResourceId | project |
NTDomain | extend |
Name | extend |
Speculus - Threat intelligence feed outage
#'Detects when no Speculus threat intelligence indicators have been received for more than 24 hours. The Speculus indexer refreshes continuously, so a silent day usually means an expired API key, a connector misconfiguration, or a feed-side outage rather than an empty feed.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: c4f2a8d1-6e39-4c07-b5a2-1d8e3f7c9a25
name: Speculus - Threat intelligence feed outage
description: |
'Detects when no Speculus threat intelligence indicators have been received for more than 24 hours.
The Speculus indexer refreshes continuously, so a silent day usually means an expired API key,
a connector misconfiguration, or a feed-side outage rather than an empty feed.'
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: SpeculusThreatIntel
dataTypes:
- Speculus_Indicators_CL
queryFrequency: 6h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1565
query: |
Speculus_Indicators_CL
| summarize LastReceived = max(TimeGenerated)
| where isnull(LastReceived) or LastReceived < ago(1d)
| extend
HoursSinceLastIndicator = iff(isnull(LastReceived), long(null), datetime_diff('hour', now(), LastReceived)),
Status = iff(isnull(LastReceived), 'No data ever received in query period', 'Feed silent for more than 24 hours')
suppressionDuration: 1d
suppressionEnabled: true
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1d
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
HoursSinceLast: HoursSinceLastIndicator
Status: Status
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Speculus_Indicators_CL
Stage 2: summarize
| summarize LastReceived = max(TimeGenerated)
Stage 3: where
| where isnull(LastReceived) or LastReceived < ago(1d)
Stage 4: extend
| extend
HoursSinceLastIndicator = iff(isnull(LastReceived), long(null), datetime_diff('hour', now(), LastReceived)),
Status = iff(isnull(LastReceived), 'No data ever received in query period', 'Feed silent for more than 24 hours')
HoursSinceLastIndicator =isnull(LastReceived)long(null)datetime_diff('hour', now(), LastReceived)Status =isnull(LastReceived)'No data ever received in query period''Feed silent for more than 24 hours'Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
LastReceived | is_null | field:"LastReceived" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
LastReceived | summarize |
HoursSinceLastIndicator | extend |
Status | extend |
TI Map Email entity to EmailEvents
#Identifies a match in EmailEvents table from any Email IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | EmailEvents action any: Email processed |
Rule Body
id: 18b61c3f-55fa-4eb9-8721-72dabd1eb3cb
name: TI Map Email entity to EmailEvents
description: |
'Identifies a match in EmailEvents table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- EmailEvents
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromAddress = tolower(SenderFromAddress) | extend RecipientEmailAddress = tolower(RecipientEmailAddress));
let SenderAddresses = EmailEvents_ | distinct SenderFromAddress | summarize make_list(SenderFromAddress);
let RecipientAddresses = EmailEvents_ | distinct RecipientEmailAddress | summarize make_list(RecipientEmailAddress);
let TI = materialize(ThreatIntelIndicators
| where TimeGenerated >= ago(ioc_lookBack)
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "email-addr"
| extend EmailSenderAddress = ObservableValue
| extend EmailSourceDomain = substring(EmailSenderAddress, indexof(EmailSenderAddress, "@") + 1, strlen(EmailSenderAddress) - indexof(EmailSenderAddress, "@") - 1)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| extend TI_EmailAddress = tolower(EmailSenderAddress)
| where TI_EmailAddress in (SenderAddresses) or TI_EmailAddress in (RecipientAddresses)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil)));
(union
(TI | join kind=innerunique (EmailEvents_) on $left.TI_EmailAddress == $right.SenderFromAddress),
(TI | join kind=innerunique (EmailEvents_) on $left.TI_EmailAddress == $right.RecipientEmailAddress))
| where EmailEvents_TimeGenerated < ValidUntil
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by Id, TI_EmailAddress
| extend DomainName = EmailSourceDomain
| extend Url = DomainName
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Url, Type, TI_EmailAddress, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
| extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
| extend timestamp = EmailEvents_TimeGenerated
| project-reorder *, TrafficLightProtocolLevel, DomainName, Type, TI_EmailAddress
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: RecipientEmailAddress
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: EmailEvents_
let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromAddress = tolower(SenderFromAddress) | extend RecipientEmailAddress = tolower(RecipientEmailAddress));
Let Binding: SenderAddresses
let SenderAddresses = EmailEvents_ | distinct SenderFromAddress | summarize make_list(SenderFromAddress);
Let Binding: RecipientAddresses
let RecipientAddresses = EmailEvents_ | distinct RecipientEmailAddress | summarize make_list(RecipientEmailAddress);
Union (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: TI, TI
Leg 1: TI
TI | join kind=innerunique (EmailEvents_) on $left.TI_EmailAddress == $right.SenderFromAddress
Leg 2: TI
TI | join kind=innerunique (EmailEvents_) on $left.TI_EmailAddress == $right.RecipientEmailAddress
Applied to the Combined Result
| where EmailEvents_TimeGenerated < ValidUntil
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by Id, TI_EmailAddress
| extend DomainName = EmailSourceDomain
| extend Url = DomainName
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Url, Type, TI_EmailAddress, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
| extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
| extend timestamp = EmailEvents_TimeGenerated
| project-reorder *, TrafficLightProtocolLevel, DomainName, Type, TI_EmailAddress
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"email-addr" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
RecipientEmailAddress | is_not_null | field:"RecipientEmailAddress" kind:is_not_null | |
SenderFromAddress | is_not_null | field:"SenderFromAddress" kind:is_not_null | |
TI_EmailAddress | in |
| field:"TI_EmailAddress" kind:in |
ValidUntil | is_null | field:"ValidUntil" kind:is_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 |
Confidence | project |
ConfidenceLevel | project |
DeliveryAction | project |
DeliveryLocation | project |
Description | project |
DomainName | project |
EmailDirection | project |
EmailEvents_TimeGenerated | project |
Id | project |
RecipientEmailAddress | project |
SenderFromAddress | project |
Subject | project |
TI_EmailAddress | project |
TrafficLightProtocolLevel | project |
Type | project |
Url | project |
ValidUntil | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
vArmour AppController - SMB Realm Traversal
#'Detects when SMB traffic crosses Production and Non-Production Realms. Possible network share discovery or lateral tool transfer across realms'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery | |
| Lateral Movement |
Rule Body
id: a36de6c3-3198-4d37-92ae-e19e36712c2e
name: vArmour AppController - SMB Realm Traversal
description: |
'Detects when SMB traffic crosses Production and Non-Production Realms. Possible network share discovery or lateral tool transfer across realms'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: vArmourAC
dataTypes:
- CommonSecurityLog
- connectorId: vArmourACAma
dataTypes:
- CommonSecurityLog
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
- LateralMovement
relevantTechniques:
- T1135
- T1570
query: |
CommonSecurityLog
| where DeviceVendor == "vArmour"
| where DeviceProduct == "AC"
| where Activity == "POLICY_VIOLATION"
| extend PolicyName = extract("(SMB_REALM_TRAVERSAL\\w+{.*})", 1, DeviceCustomString1)
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DestinationHostName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor == "vArmour"
Stage 3: where
| where DeviceProduct == "AC"
Stage 4: where
| where Activity == "POLICY_VIOLATION"
Stage 5: extend
| extend PolicyName = extract("(SMB_REALM_TRAVERSAL\\w+{.*})", 1, DeviceCustomString1)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Activity | eq |
| field:"Activity" kind:eq value:"POLICY_VIOLATION" |
DeviceProduct | eq |
| field:"DeviceProduct" kind:eq value:"AC" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"vArmour" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
PolicyName | extend |