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.

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

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.

FieldKindValuesSearch
Findingeq
  • ASREPRoasting
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.

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

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.

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

Detect CoreBackUp Deletion Activity from related Security Alerts

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

'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

TacticTechniques
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 =
ifisnotempty(tostring(Extprop["Compromised Host"]))tolower(tostring(Extprop["Compromised Host"]))
elsetolower(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 =
ifisnotempty(tostring(Extprop["Machine Domain"]))tostring(Extprop["Machine Domain"])
elsetostring(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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertNameproject
AzureLocationproject
BackupItemFriendlyNameproject
BackupItemUniqueIdproject
DnsDomainproject
DrivesBackedUpproject
HostNameproject
IpAddressproject
NTDomainproject
OperationNameproject
Stateproject
UserNameproject
VaultIdproject
_ResourceIdproject
timestampproject

Doppelpaymer Stop Services

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

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

Telemetry Coverage

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 =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 4: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNameeq
  • powershell.exe corpus 187 (elastic 140, splunk 44, kusto 3)
field:"process_basename" kind:eq value:"powershell.exe"
InitiatingProcessFileNamestarts_with
  • psexe
field:"parent_process_basename" kind:starts_with value:"psexe"
ProcessCommandLinematch
  • msexchange transforms: term
  • sql transforms: term
  • stop-service transforms: term
field:"CommandLine" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostNameextend
DnsDomainextend

GWorkspace - Multiple user agents for single source

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

'Detects requests with different user agents from one source in short timeframe.'

MITRE ATT&CK Coverage

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.

FieldKindValuesSearch
UserAgentOriginalis_not_null
  • (no value, null check)
field:"UserAgentOriginal" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
user_uasummarize
IPCustomEntityextend

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.

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

This rule detects suspicious sync events that occurred to applications using SCIM for user provisioning.

MITRE ATT&CK Coverage

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.

FieldKindValuesSearch
Policymatch
  • Password Exfiltration over SCIM application transforms: term
field:"policy_name_s" kind:match value:"Password Exfiltration over SCIM application"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

Potential DGA detected

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

'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

TacticTechniques
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.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPsummarize
EndTimeUtcsummarize
StartTimeUtcsummarize
sampleNXDomainListsummarize

Potential Ransomware activity related to Cobalt Strike

#
Status
available
Severity
high
Time window
1d
Group by
$left.AlertId, $left.DeviceId, $right.AlertId, $right.DeviceId, AlertId, AlertTime, DeviceId, Title
Source
github.com/Azure/Azure-Sentinel

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

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.

FieldKindValuesSearch
Titlein
  • 'Atosev' malware was detected
  • 'Bynoco' malware was detected
  • 'Cobaltstrike' malware was detected
  • 'Ploty' malware was detected
  • An active 'Cobaltstrike' malware was detected
  • Echo command over pipe on localhost
  • Event log was cleared
  • File backups were deleted
  • Known attack framework activity was observed
  • Suspicious 'CobaltStrike' behavior was prevented
  • Suspicious decoded content
  • Suspicious process launch by Rundll32.exe
field:"Title" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountDomainproject
AccountNameproject
AccountSidproject
AlertIdproject
AlertTimeproject
DeviceIdproject
DeviceNameproject
LogonTypeproject
RemoteDeviceNameproject
RemoteIPproject
TimeGeneratedproject
Titleproject
AccountFullNameextend
HostNameextend
DnsDomainextend

Scheduled Task - Suspicious Network Connection

#
Group by
DeviceId, FileName, InitiatingProcessCreationTime, InitiatingProcessFileName, InitiatingProcessG1ParentCreationTime, InitiatingProcessG1ParentFileName, InitiatingProcessG1ParentId, InitiatingProcessG2ParentCreationTime, InitiatingProcessG2ParentFileName, InitiatingProcessG2ParentId, InitiatingProcessG3ParentCreationTime, InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessId, InitiatingProcessParentCreationTime, InitiatingProcessParentFileName, InitiatingProcessParentId, ProcessCommandLine, join_key
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

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

TacticTechniques
PersistenceNo 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.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize
FileNamesummarize
InitiatingProcessFileNamesummarize
InitiatingProcessG1ParentFileNamesummarize
InitiatingProcessG2ParentFileNamesummarize
InitiatingProcessG3ParentFileNamesummarize
InitiatingProcessParentFileNamesummarize
pattern_countsummarize
N_InitiatingProcessCommandLineextend
N_InitiatingProcessCreationTimeextend
N_InitiatingProcessFileNameextend
N_InitiatingProcessIdextend
join_keyextend

Semperis DSP Failed Logons

#
Status
available
Severity
medium
Time window
30m
Source
github.com/Azure/Azure-Sentinel

Alerts when there are failed logons in the DSP system.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostIPproject
TimeGeneratedproject
TrusteeNameproject
_ResourceIdproject
NTDomainextend
Nameextend

Speculus - Threat intelligence feed outage

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

'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

TacticTechniques
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 =
ifisnull(LastReceived)long(null)
elsedatetime_diff('hour', now(), LastReceived)
Status =
ifisnull(LastReceived)'No data ever received in query period'
else'Feed silent for more than 24 hours'

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LastReceivedis_null
  • (no value, null check)
field:"LastReceived" kind:is_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
LastReceivedsummarize
HoursSinceLastIndicatorextend
Statusextend

TI Map Email entity to EmailEvents

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

Identifies a match in EmailEvents table from any Email IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DeliveryActionmatchBlockedexcludes:DeliveryAction field:"DeliveryAction" value:"Blocked"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Confidenceproject
ConfidenceLevelproject
DeliveryActionproject
DeliveryLocationproject
Descriptionproject
DomainNameproject
EmailDirectionproject
EmailEvents_TimeGeneratedproject
Idproject
RecipientEmailAddressproject
SenderFromAddressproject
Subjectproject
TI_EmailAddressproject
TrafficLightProtocolLevelproject
Typeproject
Urlproject
ValidUntilproject
Nameextend
UPNSuffixextend
timestampextend

vArmour AppController - SMB Realm Traversal

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

'Detects when SMB traffic crosses Production and Non-Production Realms. Possible network share discovery or lateral tool transfer across realms'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
PolicyNameextend

Back to all Kusto rules