Detection rules › Kusto
Kusto rules: 0f
AV detections related to SpringShell Vulnerability
#This query looks for Microsoft Defender AV detections related to the SpringShell vulnerability. In Microsoft Sentinel, the SecurityAlerts table includes only the Device Name of the affected device. This query joins the DeviceInfo table to clearly connect other information such as device group, IP, logged-on users, etc. This would allow the Microsoft Sentinel analyst to have more context related to the alert, if available. Reference: https://www.microsoft.com/security/blog/2022/04/04/springshell-rce-vulnerability-guidance-for-protecting-against-and-detecting-cve-2022-22965/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceInfo action any: Device information |
Rule Body
id: 3bd33158-3f0b-47e3-a50f-7c20a1b88038
name: AV detections related to SpringShell Vulnerability
description: |
'This query looks for Microsoft Defender AV detections related to the SpringShell vulnerability. In Microsoft Sentinel, the SecurityAlerts table includes only the Device Name of the affected device.
This query joins the DeviceInfo table to clearly connect other information such as device group, IP, logged-on users, etc. This would allow the Microsoft Sentinel analyst to have more context related to the alert, if available.
Reference: https://www.microsoft.com/security/blog/2022/04/04/springshell-rce-vulnerability-guidance-for-protecting-against-and-detecting-cve-2022-22965/'
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
tags:
- CVE-2022-22965
- SpringShell
- Spring4Shell
query: |
let SpringShell_threats = dynamic(["Trojan:Python/SpringShellExpl", "Exploit:Python/SpringShell", "Backdoor:PHP/Remoteshell.V", "SpringShell"]);
DeviceInfo
| extend DeviceName = tolower(DeviceName)
| join kind=inner ( SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| extend ThreatFamilyName = tostring(parse_json(ExtendedProperties).ThreatFamilyName)
| where ThreatName in~ (SpringShell_threats) or ThreatFamilyName in~ (SpringShell_threats)
| extend CompromisedEntity = tolower(CompromisedEntity)
) on $left.DeviceName == $right.CompromisedEntity
| summarize by DisplayName, ThreatName, ThreatFamilyName, PublicIP, AlertSeverity, Description, tostring(LoggedOnUsers), DeviceId, TenantId , bin(TimeGenerated, 1d), CompromisedEntity, tostring(LoggedOnUsers), ProductName, Entities
| extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: CompromisedEntity
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: PublicIP
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let SpringShell_threats = dynamic(["Trojan:Python/SpringShellExpl", "Exploit:Python/SpringShell", "Backdoor:PHP/Remoteshell.V", "SpringShell"]);
Stage 1: source
DeviceInfo
Stage 2: extend
| extend DeviceName = tolower(DeviceName)
Stage 3: join
| join kind=inner ( SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| extend ThreatFamilyName = tostring(parse_json(ExtendedProperties).ThreatFamilyName)
| where ThreatName in~ (SpringShell_threats) or ThreatFamilyName in~ (SpringShell_threats)
| extend CompromisedEntity = tolower(CompromisedEntity)
) on $left.DeviceName == $right.CompromisedEntity
Stage 4: summarize
summarize by DisplayName, ThreatName, ThreatFamilyName, PublicIP, AlertSeverity, Description, DeviceId, TenantId, CompromisedEntity, ProductName, Entities
Stage 5: extend
extend DomainIndex, HostName
Stage 6: extend
extend HostNameDomain
HostNameDomain =DomainIndex != -1substring(CompromisedEntity, (DomainIndex + 1))CompromisedEntityIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ProviderName | eq |
| field:"ProviderName" kind:eq value:"MDATP" |
ThreatFamilyName | in |
| field:"ThreatFamilyName" kind:in |
ThreatName | in |
| field:"ThreatName" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertSeverity | summarize |
CompromisedEntity | summarize |
Description | summarize |
DeviceId | summarize |
DisplayName | summarize |
Entities | summarize |
ProductName | summarize |
PublicIP | summarize |
TenantId | summarize |
ThreatFamilyName | summarize |
ThreatName | summarize |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
Dev-0270 Malicious Powershell usage
#DEV-0270 heavily uses powershell to achieve their objective at various stages of their attack. To locate powershell related activity tied to the actor, Microsoft Sentinel customers can run the following query.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Exfiltration |
Telemetry Coverage
Rule Body
id: 422ca2bf-598b-4872-82bb-5f7e8fa731e7
name: Dev-0270 Malicious Powershell usage
description: |
'DEV-0270 heavily uses powershell to achieve their objective at various stages of their attack. To locate powershell related activity tied to the actor, Microsoft Sentinel customers can run the following query.'
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Exfiltration
- DefenseEvasion
relevantTechniques:
- T1048
- T1562
tags:
- Dev-0270
query: |
(union isfuzzy=true
(SecurityEvent
| where EventID==4688
| extend FileName=tostring(split(NewProcessName, @'\')[(-1)]), ProcessCommandLine = CommandLine, InitiatingProcessFileName=ParentProcessName
| where (FileName =~ "powershell.exe" and ProcessCommandLine has_all("try", "Add-MpPreference", "-ExclusionPath", "ProgramData", "catch")) or (FileName =~ 'powershell.exe' and ProcessCommandLine has_all('Add-PSSnapin', 'Get-Recipient', '-ExpandProperty', 'EmailAddresses', 'SmtpAddress', '-hidetableheaders') )
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, InitiatingProcessFileName, EventID, Activity, CommandLine, EventSourceName, Type
),
(DeviceProcessEvents
| where (FileName =~ "powershell.exe" and ((ProcessCommandLine has_all("try", "Add-MpPreference", "-ExclusionPath", "ProgramData", "catch")) or (ProcessCommandLine has_all('Add-PSSnapin', 'Get-Recipient', '-ExpandProperty', 'EmailAddresses', 'SmtpAddress', '-hidetableheaders'))))
or ( InitiatingProcessFileName =~ 'powershell.exe' and (((InitiatingProcessCommandLine has_all('$file=', 'dllhost.exe', 'Invoke-WebRequest', '-OutFile')) or ((InitiatingProcessCommandLine has_all('$admins=', 'System.Security.Principal.SecurityIdentifier', 'Translate', '-split', 'localgroup', '/add', '$rdp='))))))
| extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName
)
)
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
version: 1.0.4
kind: Scheduled
Stages and Predicates
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, DeviceProcessEvents
Leg 1: SecurityEvent
SecurityEvent
| where EventID==4688
| extend FileName=tostring(split(NewProcessName, @'\')[(-1)]), ProcessCommandLine = CommandLine, InitiatingProcessFileName=ParentProcessName
| where (FileName =~ "powershell.exe" and ProcessCommandLine has_all("try", "Add-MpPreference", "-ExclusionPath", "ProgramData", "catch")) or (FileName =~ 'powershell.exe' and ProcessCommandLine has_all('Add-PSSnapin', 'Get-Recipient', '-ExpandProperty', 'EmailAddresses', 'SmtpAddress', '-hidetableheaders') )
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, InitiatingProcessFileName, EventID, Activity, CommandLine, EventSourceName, Type
Leg 2: DeviceProcessEvents
DeviceProcessEvents
| where (FileName =~ "powershell.exe" and ((ProcessCommandLine has_all("try", "Add-MpPreference", "-ExclusionPath", "ProgramData", "catch")) or (ProcessCommandLine has_all('Add-PSSnapin', 'Get-Recipient', '-ExpandProperty', 'EmailAddresses', 'SmtpAddress', '-hidetableheaders'))))
or ( InitiatingProcessFileName =~ 'powershell.exe' and (((InitiatingProcessCommandLine has_all('$file=', 'dllhost.exe', 'Invoke-WebRequest', '-OutFile')) or ((InitiatingProcessCommandLine has_all('$admins=', 'System.Security.Principal.SecurityIdentifier', 'Translate', '-split', 'localgroup', '/add', '$rdp='))))))
| extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName
Applied to the Combined Result
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
FileName | eq |
| field:"FileName" kind:eq |
InitiatingProcessCommandLine | match |
| field:"ParentCommandLine" kind:match |
InitiatingProcessFileName | eq |
| field:"parent_process_basename" kind:eq value:"powershell.exe" |
ProcessCommandLine | match |
| field:"CommandLine" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | extend |
AccountDomain | project |
Activity | project |
CommandLine | project |
Computer | extend |
EventID | project |
EventSourceName | project |
InitiatingProcessFileName | project |
ProcessName | project |
ProcessNameFullPath | project |
TimeGenerated | project |
Type | project |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
Hunt for ADWS requests from unknown devices
#This hunting rule searches for incomming ADWS connections on Domain Controllers (DC's need to be onboarded in Defender for Endpoint) from IP Addresses that cannot be linked to MDE onboarded devices.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery | |
| Lateral Movement | |
| Collection |
References
- https://falconforce.nl/soaphound-tool-to-collect-active-directory-data-via-adws/
- https://github.com/FalconForceTeam/FalconFriday/blob/master/Discovery/ADWS_Connection_from_Unexpected_Binary-Win.md
- https://github.com/FalconForceTeam/FalconFriday/blob/master/Discovery/ADWS_Connection_from_Process_Injection_Target-Win.md
- https://cyberlandsec.com/soapy-the-ultimate-stealthy-active-directory-enumeration-tool-via-adws/
- https://github.com/FalconForceTeam/SOAPHound
Telemetry Coverage
Rule Body
let device_info = (
// Get device network info from last 7 days
DeviceNetworkInfo
| where Timestamp > ago(7d)
// Expand the IP Addresses of the devices
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
// Distinct IP address for each device
| distinct DeviceName, DeviceId, IPAddress
// Search for each device if it is onboarded or not
| join kind=inner (
DeviceInfo
| where Timestamp > ago(7d)
| distinct DeviceName, DeviceId, OnboardingStatus
// Get the first timestamp the device was seen
| join kind=inner (
DeviceInfo
| where Timestamp > ago(30d)
| summarize FirstSeen = arg_min(Timestamp, DeviceId) by DeviceId
) on DeviceId
| project-away DeviceId1, DeviceId2
) on DeviceId, DeviceName
| project-away DeviceName1, DeviceId1
);
// Get incomming traffic on ADWS port and save unique remote IP addresses
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where ActionType != "ListeningConnectionCreated"
| where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe"
| where LocalPort == "9389"
| summarize ConnectionTimes=make_list(Timestamp) by RemoteIP, DeviceName
// Get device information of remote IP addresses, results for IP we do not find information for are allowed
| join kind=leftouter device_info on $left.RemoteIP == $right.IPAddress
| project-away IPAddress
// Check if the remote IPs are onboarded devices or not
| where OnboardingStatus != "Onboarded"
// Make output better
| project DeviceName, ConnectionTimes, RemoteIP, RemoteDeviceName = DeviceName1, RemoteDeviceId = DeviceId, RemoteOnboardingStatus = OnboardingStatus, RemoteDeviceFirstSeen = FirstSeen
let device_info = (
// Get device network info from last 7 days
DeviceNetworkInfo
| where TimeGenerated > ago(7d)
// Expand the IP Addresses of the devices
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
// Distinct IP address for each device
| distinct DeviceName, DeviceId, IPAddress
// Search for each device if it is onboarded or not
| join kind=inner (
DeviceInfo
| where TimeGenerated > ago(7d)
| distinct DeviceName, DeviceId, OnboardingStatus
// Get the first timestamp the device was seen
| join kind=inner (
DeviceInfo
| where TimeGenerated > ago(30d)
| summarize FirstSeen = arg_min(TimeGenerated, DeviceId) by DeviceId
) on DeviceId
| project-away DeviceId1, DeviceId2
) on DeviceId, DeviceName
| project-away DeviceName1, DeviceId1
);
// Get incomming traffic on ADWS port and save unique remote IP addresses
DeviceNetworkEvents
| where TimeGenerated > ago(30d)
| where ActionType != "ListeningConnectionCreated"
| where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe"
| where LocalPort == "9389"
| summarize ConnectionTimes=make_list(TimeGenerated) by RemoteIP, DeviceName
// Get device information of remote IP addresses, results for IP we do not find information for are allowed
| join kind=leftouter device_info on $left.RemoteIP == $right.IPAddress
| project-away IPAddress
// Check if the remote IPs are onboarded devices or not
| where OnboardingStatus != "Onboarded"
// Make output better
| project DeviceName, ConnectionTimes, RemoteIP, RemoteDeviceName = DeviceName1, RemoteDeviceId = DeviceId, RemoteOnboardingStatus = OnboardingStatus, RemoteDeviceFirstSeen = FirstSeen
Stages and Predicates
Let Binding: device_info
let device_info = (
DeviceNetworkInfo
| where Timestamp > ago(7d)
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
| distinct DeviceName, DeviceId, IPAddress
| join kind=inner (
DeviceInfo
| where Timestamp > ago(7d)
| distinct DeviceName, DeviceId, OnboardingStatus
| join kind=inner (
DeviceInfo
| where Timestamp > ago(30d)
| summarize FirstSeen = arg_min(Timestamp, DeviceId) by DeviceId
) on DeviceId
| project-away DeviceId1, DeviceId2
) on DeviceId, DeviceName
| project-away DeviceName1, DeviceId1
);
Stage 1: source
let device_info
Stage 2: source
DeviceNetworkEvents
Stage 3: where
where Timestamp > ago(2592000s)
Stage 4: where
where ActionType != "ListeningConnectionCreated"
Stage 5: where
where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe"
Stage 6: where
where LocalPort == 9389
Stage 7: summarize
summarize ConnectionTimes by RemoteIP, DeviceName
Stage 8: join
join kind=leftouter (device_info) on $left.RemoteIP, $right.IPAddress
Stage 9: project-away
project-away IPAddress
Stage 10: where
where OnboardingStatus != "Onboarded"
Stage 11: project
project ConnectionTimes, DeviceName, RemoteDeviceFirstSeen, RemoteDeviceId, RemoteDeviceName, RemoteIP, RemoteOnboardingStatus
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | ne |
| field:"ActionType" kind:ne value:"ListeningConnectionCreated" |
InitiatingProcessFolderPath | eq |
| field:"InitiatingProcessFolderPath" kind:eq value:"c:\windows\adws\microsoft.activedirectory.webservices.exe" |
LocalPort | eq |
| field:"LocalPort" kind:eq value:"9389" |
OnboardingStatus | ne |
| field:"OnboardingStatus" kind:ne value:"Onboarded" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConnectionTimes | project |
DeviceName | project |
RemoteDeviceFirstSeen | project |
RemoteDeviceId | project |
RemoteDeviceName | project |
RemoteIP | project |
RemoteOnboardingStatus | project |
Ingress Tool Transfer - Certutil
#This detection addresses most of the known ways to utilize this binary for malicious/unintended purposes. It attempts to accommodate for most detection evasion techniques, like commandline obfuscation and binary renaming.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | |
| Command & Control |
Telemetry Coverage
Rule Body
id: f0be11a9-ec48-4df6-801d-479556044d4e
name: Ingress Tool Transfer - Certutil
description: |
This detection addresses most of the known ways to utilize this binary for malicious/unintended purposes.
It attempts to accommodate for most detection evasion techniques, like commandline obfuscation and binary renaming.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- DefenseEvasion
relevantTechniques:
- T1105
- T1564.004
- T1027
- T1140
query: |
// Set the time span for the query.
let Timeframe = 1h;
// Set the HashTimeframe for the hash lookup; longer makes it more accurate, but obviously also more resource-intensive.
let HashTimeframe = 14d;
// Get all known SHA1 hashes for certutil executions or renamed files formerly named certutil.
let CertUtilPESha1=materialize(DeviceProcessEvents | where Timestamp > ago(HashTimeframe)| where FileName has "certutil" | where isnotempty(SHA1) | summarize sha1=make_set(SHA1));
let CertUtilFESha1=materialize(DeviceFileEvents | where Timestamp > ago(HashTimeframe)| where PreviousFileName contains "certutil" or FileName contains "certutil" | where isnotempty(SHA1) | summarize sha1=make_set(SHA1));
DeviceProcessEvents
| where Timestamp >= ago(Timeframe)
// Get all executions by processes with a SHA1 hash that is or was named certutil.
| where SHA1 in (CertUtilPESha1) or SHA1 in (CertUtilFESha1) or FileName =~ "certutil.exe" or ProcessCommandLine has "certutil"
// Create a new field called CleanProcessCommandLine which gets populated with the value of ProcessCommandLine as Windows parses it for execution,
// removing any potential command line obfuscation.
| extend CleanProcessCommandLine=parse_command_line(ProcessCommandLine, "windows")
// Search for de-obfuscated commands used.
// Urlcache is the documented attribute. However, url is also accepted.
// Verifyctl is the documented attribute. However, verify is also accepted.
| where CleanProcessCommandLine has_any ("decode", "encode", "verify","url")
| order by Timestamp
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- entityType: Account
fieldMappings:
- identifier: Sid
columnName: AccountSid
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountDomain
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let Timeframe = 1h;
let HashTimeframe = 14d;
Let Binding: CertUtilPESha1
let CertUtilPESha1 = materialize(DeviceProcessEvents | where Timestamp > ago(HashTimeframe)| where FileName has "certutil" | where isnotempty(SHA1) | summarize sha1=make_set(SHA1));
Let Binding: CertUtilFESha1
let CertUtilFESha1 = materialize(DeviceFileEvents | where Timestamp > ago(HashTimeframe)| where PreviousFileName contains "certutil" or FileName contains "certutil" | where isnotempty(SHA1) | summarize sha1=make_set(SHA1));
Stage 1: source
let CertUtilPESha1
Stage 2: source
let CertUtilFESha1
Stage 3: source
DeviceProcessEvents
Stage 4: where
where Timestamp >= ago(3600s)
Stage 5: where
where (FileName =~ "certutil.exe" or ProcessCommandLine has "certutil" or SHA1 == "CertUtilFESha1" or SHA1 == "CertUtilPESha1")
Stage 6: extend
extend CleanProcessCommandLine
Stage 7: where
where (CleanProcessCommandLine has "decode" or CleanProcessCommandLine has "encode" or CleanProcessCommandLine has "verify" or CleanProcessCommandLine has "url")
Stage 8: sort
sort by Timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CleanProcessCommandLine | match |
| field:"CleanProcessCommandLine" kind:match |
FileName | eq |
| field:"FileName" kind:eq value:"certutil.exe" |
ProcessCommandLine | match |
| field:"ProcessCommandLine" kind:match value:"certutil" |
SHA1 | in |
| field:"SHA1" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CleanProcessCommandLine | extend |
OCI - Multiple instances terminated
#'Detects when multiple instances were terminated.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 252e651d-d825-480c-bdeb-8b239354577d
name: OCI - Multiple instances terminated
description: |
'Detects when multiple instances were terminated.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: OracleCloudInfrastructureLogsConnector
dataTypes:
- OCILogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1529
query: |
let threshold = 5;
OCILogs
| where data_eventName_s =~ 'TerminateInstance'
| summarize count() by SrcIpAddr, bin(TimeGenerated, 10m)
| where count_ >= 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
OCILogs
Stage 2: where
| where data_eventName_s =~ 'TerminateInstance'
Stage 3: summarize
| summarize count() by SrcIpAddr, bin(TimeGenerated, 10m)
Stage 4: where
| where count_ >= threshold
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
count_ | ge |
| field:"count_" kind:ge value:"5" |
data_eventName_s | eq |
| field:"data_eventName_s" kind:eq value:"TerminateInstance" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
IPCustomEntity | extend |
Office Policy Tampering
#Identifies if any tampering is done to either auditlog, ATP Safelink, SafeAttachment, AntiPhish or Dlp policy. An adversary may use this technique to evade detection or avoid other policy based defenses. References: https://docs.microsoft.com/powershell/module/exchange/advanced-threat-protection/remove-antiphishrule?view=exchange-ps.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment |
Telemetry Coverage
Rule Body
id: fbd72eb8-087e-466b-bd54-1ca6ea08c6d3
name: Office Policy Tampering
description: |
'Identifies if any tampering is done to either auditlog, ATP Safelink, SafeAttachment, AntiPhish or Dlp policy.
An adversary may use this technique to evade detection or avoid other policy based defenses.
References: https://docs.microsoft.com/powershell/module/exchange/advanced-threat-protection/remove-antiphishrule?view=exchange-ps.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity (Exchange)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- DefenseEvasion
relevantTechniques:
- T1098
- T1562
query: |
let opList = OfficeActivity
| summarize by Operation
//| where Operation startswith "Remove-" or Operation startswith "Disable-"
| where Operation has_any ("Remove", "Disable")
| where Operation contains "AntiPhish" or Operation contains "SafeAttachment" or Operation contains "SafeLinks" or Operation contains "Dlp" or Operation contains "Audit"
| summarize make_set(Operation, 500);
OfficeActivity
// Only admin or global-admin can disable/remove policy
| where RecordType =~ "ExchangeAdmin"
| where UserType in~ ("Admin","DcAdmin")
// Pass in interesting Operation list
| where Operation in~ (opList)
| extend ClientIPOnly = case(
ClientIP has ".", tostring(split(ClientIP,":")[0]),
ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))),
ClientIP
)
| extend Port = case(
ClientIP has ".", (split(ClientIP,":")[1]),
ClientIP has "[", tostring(split(ClientIP,"]:")[1]),
ClientIP
)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = ClientIPOnly, Port, ResultStatus, Parameters
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserId
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIP
version: 2.0.3
kind: Scheduled
Stages and Predicates
Let Binding: opList
let opList = OfficeActivity
| summarize by Operation
| where Operation has_any ("Remove", "Disable")
| where Operation contains "AntiPhish" or Operation contains "SafeAttachment" or Operation contains "SafeLinks" or Operation contains "Dlp" or Operation contains "Audit"
| summarize make_set(Operation, 500);
Stage 1: source
OfficeActivity
Stage 2: where
| where RecordType =~ "ExchangeAdmin"
Stage 3: where
| where UserType in~ ("Admin","DcAdmin")
Stage 4: where
| where Operation in~ (opList)
Stage 5: extend
| extend ClientIPOnly = case(
ClientIP has ".", tostring(split(ClientIP,":")[0]),
ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))),
ClientIP
)
ClientIPOnly =ClientIP has "."tostring(split(ClientIP, ":")[0])ClientIP has "["tostring(trim_start(@'[[]', tostring(split(ClientIP, "]")[0])))ClientIPStage 6: extend
| extend Port = case(
ClientIP has ".", (split(ClientIP,":")[1]),
ClientIP has "[", tostring(split(ClientIP,"]:")[1]),
ClientIP
)
Port =ClientIP has "."split(ClientIP, ":")[1]ClientIP has "["tostring(split(ClientIP, "]:")[1])ClientIPStage 7: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = ClientIPOnly, Port, ResultStatus, Parameters
Stage 8: extend
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Operation | in |
| field:"Operation" kind:in value:"opList" |
RecordType | eq |
| field:"RecordType" kind:eq value:"ExchangeAdmin" |
UserType | in |
| field:"UserType" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientIP | summarize |
EndTimeUtc | summarize |
Operation | summarize |
OperationCount | summarize |
Parameters | summarize |
Port | summarize |
ResultStatus | summarize |
StartTimeUtc | summarize |
UserId | summarize |
UserType | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
Red Sift - Email with URL to previously unseen domain
#'Detects email forensics events containing one or more URLs whose domain has not been seen in the previous 14 days, which may indicate newly observed phishing infrastructure or suspicious delivery patterns.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 8972b513-12a2-4b46-8263-3f091d88a8bc
name: Red Sift - Email with URL to previously unseen domain
description: |
'Detects email forensics events containing one or more URLs whose domain has not been seen in the previous 14 days, which may indicate newly observed phishing infrastructure or suspicious delivery patterns.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: RedSiftPush
dataTypes:
- RedSiftEmailForensics_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let lookback = 14d;
let recentWindow = 1h;
let historicalDomains = RedSiftEmailForensics_CL
| extend EmailUrls = todynamic(EmailUrls)
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailUrls) and array_length(EmailUrls) > 0
| mv-expand Url = EmailUrls
| extend UrlString = tostring(Url.url_string)
| where isnotempty(UrlString)
| extend UrlDomain = tostring(parse_url(UrlString).Host)
| where isnotempty(UrlDomain)
| summarize by UrlDomain;
RedSiftEmailForensics_CL
| extend
EmailFrom = tostring(column_ifexists("EmailFrom", "")),
EmailSubject = tostring(column_ifexists("EmailSubject", "")),
EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
SrcIp = tostring(column_ifexists("SrcIp", "")),
DstHostname = tostring(column_ifexists("DstHostname", "")),
Severity = tostring(column_ifexists("Severity", "")),
Message = tostring(column_ifexists("Message", "")),
CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
EmailUrls = todynamic(EmailUrls)
| where TimeGenerated >= ago(recentWindow)
| where isnotempty(EmailUrls) and array_length(EmailUrls) > 0
| mv-expand Url = EmailUrls
| extend UrlString = tostring(Url.url_string)
| where isnotempty(UrlString)
| extend UrlDomain = tostring(parse_url(UrlString).Host)
| where isnotempty(UrlDomain)
| join kind=leftanti (historicalDomains) on UrlDomain
| summarize
NewUrlDomains = make_set(UrlDomain, 50),
NewUrls = make_set(UrlString, 50),
NewDomainCount = dcount(UrlDomain),
UrlCount = dcount(UrlString),
RepresentativeUrlDomain = take_any(UrlDomain)
by TimeGenerated,
EmailFrom,
EmailSubject,
EmailReturnPath,
EmailMessageUid,
SrcIp,
DstHostname,
Severity,
Message,
CorrelationUid
| extend
NewUrlDomainList = strcat_array(NewUrlDomains, ", "),
NewUrlList = strcat_array(NewUrls, ", ")
| project
TimeGenerated,
EmailFrom,
EmailSubject,
EmailReturnPath,
EmailMessageUid,
SrcIp,
DstHostname,
RepresentativeUrlDomain,
NewDomainCount,
UrlCount,
NewUrlDomainList,
NewUrlList,
Severity,
Message,
CorrelationUid
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: EmailFrom
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIp
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: RepresentativeUrlDomain
customDetails:
EmailSubject: EmailSubject
ReturnPath: EmailReturnPath
NewDomainCount: NewDomainCount
NewUrlDomainList: NewUrlDomainList
NewUrlList: NewUrlList
CorrelationUid: CorrelationUid
alertDetailsOverride:
alertDisplayNameFormat: "RedSift - URL to new domain in email from {{EmailFrom}}"
alertDescriptionFormat: "Email from {{EmailFrom}} contains {{NewDomainCount}} previously unseen URL domain(s): {{NewUrlDomainList}}."
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: Selected
groupByEntities:
- Account
- DNS
groupByCustomDetails:
- EmailSubject
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 14d;
let recentWindow = 1h;
Let Binding: historicalDomains
let historicalDomains = RedSiftEmailForensics_CL
| extend EmailUrls = todynamic(EmailUrls)
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailUrls) and array_length(EmailUrls) > 0
| mv-expand Url = EmailUrls
| extend UrlString = tostring(Url.url_string)
| where isnotempty(UrlString)
| extend UrlDomain = tostring(parse_url(UrlString).Host)
| where isnotempty(UrlDomain)
| summarize by UrlDomain;
Stage 1: source
RedSiftEmailForensics_CL
Stage 2: extend
| extend
EmailFrom = tostring(column_ifexists("EmailFrom", "")),
EmailSubject = tostring(column_ifexists("EmailSubject", "")),
EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
SrcIp = tostring(column_ifexists("SrcIp", "")),
DstHostname = tostring(column_ifexists("DstHostname", "")),
Severity = tostring(column_ifexists("Severity", "")),
Message = tostring(column_ifexists("Message", "")),
CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
EmailUrls = todynamic(EmailUrls)
Stage 3: where
| where TimeGenerated >= ago(recentWindow)
Stage 4: where
| where isnotempty(EmailUrls) and array_length(EmailUrls) > 0
Stage 5: mv-expand
| mv-expand Url = EmailUrls
Stage 6: extend
| extend UrlString = tostring(Url.url_string)
Stage 7: where
| where isnotempty(UrlString)
Stage 8: extend
| extend UrlDomain = tostring(parse_url(UrlString).Host)
Stage 9: where
| where isnotempty(UrlDomain)
Stage 10: join (negated)
join kind=leftanti (historicalDomains) on UrlDomain
Stage 11: summarize
summarize NewDomainCount, NewUrlDomains, NewUrls, RepresentativeUrlDomain, UrlCount by TimeGenerated, EmailFrom, EmailSubject, EmailReturnPath, EmailMessageUid, SrcIp, DstHostname, Severity, Message, CorrelationUid
Stage 12: extend
extend NewUrlDomainList, NewUrlList
Stage 13: project
project CorrelationUid, DstHostname, EmailFrom, EmailMessageUid, EmailReturnPath, EmailSubject, Message, NewDomainCount, NewUrlDomainList, NewUrlList, RepresentativeUrlDomain, Severity, SrcIp, TimeGenerated, UrlCount
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EmailUrls | is_not_null | field:"EmailUrls" kind:is_not_null | |
UrlDomain | is_not_null | field:"UrlDomain" kind:is_not_null | |
UrlString | is_not_null | field:"UrlString" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
EmailUrls | gt | 0 | excludes:EmailUrls field:"EmailUrls" value:"0" |
EmailUrls | is_not_null | excludes:EmailUrls | |
UrlDomain | is_not_null | excludes:UrlDomain | |
UrlString | is_not_null | excludes:UrlString |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CorrelationUid | project |
DstHostname | project |
EmailFrom | project |
EmailMessageUid | project |
EmailReturnPath | project |
EmailSubject | project |
Message | project |
NewDomainCount | project |
NewUrlDomainList | project |
NewUrlList | project |
RepresentativeUrlDomain | project |
Severity | project |
SrcIp | project |
TimeGenerated | project |
UrlCount | project |
SAP ETD - No new data received
#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.
Identifies a complete gap in the SAP Enterprise Threat Detection (ETD) feed when no records have been ingested into the SAPETDAlerts_CL table within the configured time window (default 1 hour). A full-feed blackout may indicate that an adversary is tampering with the security telemetry pipeline (for example by stopping the SAP ETD collector, disabling the data connector, or blocking network egress to Microsoft Sentinel) to hide follow-on activity in the SAP landscape. Benign causes such as a service outage, connector failure, or maintenance window are also possible and should be ruled out during triage. This rule is complementary to the per-SAP-system rule "SAP ETD - SAP system stopped reporting data", which can help distinguish a targeted silencing of a single system from a full-feed blackout. To change the freshness threshold, update the LookbackPeriod variable in the query and align queryFrequency / queryPeriod accordingly.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: a9206c5a-3e72-4c10-807f-313a56075b20
kind: Scheduled
name: SAP ETD - No new data received
description: |
Identifies a complete gap in the SAP Enterprise Threat Detection (ETD) feed when no records have been ingested into the SAPETDAlerts_CL table within the configured time window (default 1 hour). A full-feed blackout may indicate that an adversary is tampering with the security telemetry pipeline (for example by stopping the SAP ETD collector, disabling the data connector, or blocking network egress to Microsoft Sentinel) to hide follow-on activity in the SAP landscape. Benign causes such as a service outage, connector failure, or maintenance window are also possible and should be ruled out during triage. This rule is complementary to the per-SAP-system rule "SAP ETD - SAP system stopped reporting data", which can help distinguish a targeted silencing of a single system from a full-feed blackout. To change the freshness threshold, update the `LookbackPeriod` variable in the query and align `queryFrequency` / `queryPeriod` accordingly.
severity: High
status: Available
requiredDataConnectors:
- connectorId: SAPETDAlerts
dataTypes:
- SAPETDAlerts_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
- T1562.006
query: |
// Configurable freshness threshold for the entire SAP ETD data feed.
// When changing this value also update queryFrequency and queryPeriod accordingly.
let LookbackPeriod = 1h;
SAPETDAlerts_CL
| summarize
LastIngestionTime = max(TimeGenerated),
RecordsInWindow = countif(TimeGenerated > ago(LookbackPeriod))
| where RecordsInWindow == 0 or isnull(LastIngestionTime)
| extend
LookbackPeriod = LookbackPeriod,
TimeSinceLastIngestion = now() - coalesce(LastIngestionTime, datetime(null)),
FeedName = "SAPETD",
Reason = iff(isnull(LastIngestionTime),
"No SAPETDAlerts_CL records have ever been ingested.",
strcat("No SAPETDAlerts_CL records ingested in the last ", tostring(LookbackPeriod), " (last ingestion: ", tostring(LastIngestionTime), ")."))
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: FeedName
alertDetailsOverride:
alertDisplayNameFormat: 'SAP ETD - No new data received in the last {{LookbackPeriod}}'
alertDescriptionFormat: |
{{Reason}}
A complete gap in the SAP ETD feed may indicate that an adversary is tampering with the security telemetry pipeline (for example by stopping the SAP ETD collector, disabling the data connector, or blocking network egress to Microsoft Sentinel) in order to hide malicious activity in the SAP landscape. Treat the silence as suspicious until proven otherwise: validate the integrity and runtime state of the SAP ETD data connector, the SAP ETD service, and the network path between them, and review recent change / admin activity on those components before concluding the cause is a benign outage.
customDetails:
LookbackPeriod: LookbackPeriod
LastIngestion: LastIngestionTime
LastIngestionGap: TimeSinceLastIngestion
version: 1.0.0
Stages and Predicates
Parameters
let LookbackPeriod = 1h;
Stage 1: source
SAPETDAlerts_CL
Stage 2: summarize
| summarize
LastIngestionTime = max(TimeGenerated),
RecordsInWindow = countif(TimeGenerated > ago(LookbackPeriod))
Stage 3: where
| where RecordsInWindow == 0 or isnull(LastIngestionTime)
Stage 4: extend
| extend
LookbackPeriod = LookbackPeriod,
TimeSinceLastIngestion = now() - coalesce(LastIngestionTime, datetime(null)),
FeedName = "SAPETD",
Reason = iff(isnull(LastIngestionTime),
"No SAPETDAlerts_CL records have ever been ingested.",
strcat("No SAPETDAlerts_CL records ingested in the last ", tostring(LookbackPeriod), " (last ingestion: ", tostring(LastIngestionTime), ")."))
Reason =isnull(LastIngestionTime)"No SAPETDAlerts_CL records have ever been ingested."strcat("No SAPETDAlerts_CL records ingested in the last ", tostring(LookbackPeriod), " (last ingestion: ", tostring(LastIngestionTime), ").")Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
LastIngestionTime | is_null | field:"LastIngestionTime" kind:is_null | |
RecordsInWindow | eq |
| field:"RecordsInWindow" kind:eq value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
LastIngestionTime | summarize |
RecordsInWindow | summarize |
FeedName | extend |
LookbackPeriod | extend |
Reason | extend |
TimeSinceLastIngestion | extend |
Speculus - Network traffic to or from high-risk IP indicator
#'Matches network events in CommonSecurityLog against high-risk Speculus IP indicators (risk score >= 75) received in the last 14 days. These IPs are associated with malicious activity such as command and control, botnets, ransomware infrastructure, or brute-force campaigns.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: a7c3f9e2-4d18-4b6a-9f0c-2e5d8b1a6c43
name: Speculus - Network traffic to or from high-risk IP indicator
description: |
'Matches network events in CommonSecurityLog against high-risk Speculus IP indicators (risk score >= 75)
received in the last 14 days. These IPs are associated with malicious activity such as command and control,
botnets, ransomware infrastructure, or brute-force campaigns.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: SpeculusThreatIntel
dataTypes:
- Speculus_Indicators_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- InitialAccess
relevantTechniques:
- T1071
- T1090
query: |
let indicators = Speculus_Indicators_CL
| where TimeGenerated > ago(14d)
| where isnotempty(IndicatorValue)
| where RiskScore >= 75
| summarize arg_max(Modified, *) by IndicatorValue
| project
IndicatorValue,
RiskScore,
SpeculusActivity = Activity,
Attribution,
Labels,
IndicatorDescription = Description;
CommonSecurityLog
| where TimeGenerated > ago(1h)
| where isnotempty(SourceIP) or isnotempty(DestinationIP)
| extend MatchIP = iff(isnotempty(DestinationIP), DestinationIP, SourceIP)
| join kind=inner indicators on $left.MatchIP == $right.IndicatorValue
| project
TimeGenerated,
MatchIP,
SourceIP,
DestinationIP,
DeviceVendor,
DeviceProduct,
RiskScore,
SpeculusActivity,
Attribution,
Labels,
IndicatorDescription
suppressionDuration: 1h
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1d
matchingMethod: Selected
groupByEntities:
- IP
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
RiskScore: RiskScore
Attribution: Attribution
Labels: Labels
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: MatchIP
version: 1.0.0
kind: Scheduled
Stages and Predicates
Let Binding: indicators
let indicators = Speculus_Indicators_CL
| where TimeGenerated > ago(14d)
| where isnotempty(IndicatorValue)
| where RiskScore >= 75
| summarize arg_max(Modified, *) by IndicatorValue
| project
IndicatorValue,
RiskScore,
SpeculusActivity = Activity,
Attribution,
Labels,
IndicatorDescription = Description;
Stage 1: source
CommonSecurityLog
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where isnotempty(SourceIP) or isnotempty(DestinationIP)
Stage 4: extend
| extend MatchIP = iff(isnotempty(DestinationIP), DestinationIP, SourceIP)
MatchIP =isnotempty(DestinationIP)DestinationIPSourceIPStage 5: join
| join kind=inner indicators on $left.MatchIP == $right.IndicatorValue
Stage 6: project
project Attribution, DestinationIP, DeviceProduct, DeviceVendor, IndicatorDescription, Labels, MatchIP, RiskScore, SourceIP, SpeculusActivity, TimeGenerated
Stage 7: summarize aggregation inside the join branch
summarize by IndicatorValue
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationIP | is_not_null | field:"DestinationIP" kind:is_not_null | |
IndicatorValue | is_not_null | field:"IndicatorValue" kind:is_not_null | |
RiskScore | ge |
| field:"RiskScore" kind:ge value:"75" |
SourceIP | is_not_null | field:"SourceIP" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IndicatorValue | summarize |
Tailscale: OAuth client or API key created with write scopes
#Identifies creation of a Tailscale OAuth client or API access key whose granted scopes include WRITE permissions (anything matching :write). Tokens with write scopes are high-value adversary targets.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Rule Body
id: 7237a848-30f2-499b-9ad5-024aea1288bd
name: "Tailscale: OAuth client or API key created with write scopes"
description: Identifies creation of a Tailscale OAuth client or API access key whose granted scopes include WRITE permissions (anything matching :write). Tokens with write scopes are high-value adversary targets.
description-detailed: |
Detects creation of a Tailscale OAuth client or API access key whose granted scopes include WRITE permissions (anything matching ":write"). Tokens with write scopes can modify tailnet configuration, manage devices, write ACLs, and revoke keys - high-value adversary targets. Compare against the recent actor history; revoke immediately if unexpected.
severity: High
status: Available
requiredDataConnectors:
- connectorId: TailscaleCCF
dataTypes:
- Tailscale_Audit_CL
- connectorId: TailscalePremiumCCF
dataTypes:
- Tailscale_Audit_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098
- T1136
query: |
Tailscale_Audit_CL
| where EventType == "CONFIG"
| where Action == "CREATE"
| where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT")
| extend Scopes = extract(@"scopes\s*-\s*(.+)$", 1, ActionDetails)
| where Scopes contains ":write"
| extend WriteScopes = extract_all(@"([a-zA-Z_]+:write)", Scopes)
| extend ActorLogin = tostring(Actor.loginName)
| extend ActorType = tostring(Actor.type)
| extend TargetName = tostring(Target.name)
| extend TargetId = tostring(Target.id)
| extend TargetType = tostring(Target.type)
| project TimeGenerated, ActorLogin, ActorType, Action, TargetType, TargetName, TargetId, WriteScopes, Scopes, Origin, ActionDetails
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorLogin
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
Stage 1: source
Tailscale_Audit_CL
Stage 2: where
| where EventType == "CONFIG"
Stage 3: where
| where Action == "CREATE"
Stage 4: where
| where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT")
Stage 5: extend
| extend Scopes = extract(@"scopes\s*-\s*(.+)$", 1, ActionDetails)
Stage 6: where
| where Scopes contains ":write"
Stage 7: extend (6 consecutive steps)
| extend WriteScopes = extract_all(@"([a-zA-Z_]+:write)", Scopes)
| extend ActorLogin = tostring(Actor.loginName)
| extend ActorType = tostring(Actor.type)
| extend TargetName = tostring(Target.name)
| extend TargetId = tostring(Target.id)
| extend TargetType = tostring(Target.type)
Stage 8: project
| project TimeGenerated, ActorLogin, ActorType, Action, TargetType, TargetName, TargetId, WriteScopes, Scopes, Origin, ActionDetails
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq value:"CREATE" |
EventType | eq |
| field:"EventType" kind:eq value:"CONFIG" |
Scopes | contains |
| field:"Scopes" kind:contains value:":write" |
Target.type | in |
| field:"Target.type" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
ActionDetails | project |
ActorLogin | project |
ActorType | project |
Origin | project |
Scopes | project |
TargetId | project |
TargetName | project |
TargetType | project |
TimeGenerated | project |
WriteScopes | project |
Tenable.ad Password Spraying
#'Searches for Password spraying attacks.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 29d350db-0ac0-4f4c-92ff-dac0f6335612
name: Tenable.ad Password Spraying
description: |
'Searches for Password spraying attacks.'
severity: High
requiredDataConnectors:
- connectorId: Tenable.ad
dataTypes:
- Tenable_ad_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110.003
query: |
// For the query to work properly, make sure you have imported the afad_parser.kql parser into the workspace
// Retrieve the parser here: https://raw.githubusercontent.com/tenable/Azure-Sentinel/Tenable.ad-connector/Solutions/TenableAD/Parsers/afad_parser.kql
// Then, create the Kusto Function with alias afad_parser
afad_parser
| where MessageType == 2 and Codename == "Password Spraying"
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 2 and Codename == "Password Spraying"
Stage 3: extend
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | eq |
| field:"Codename" kind:eq value:"Password Spraying" |
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
Third party integrated apps
#'This query searches for your services by regulating the access of third-party integrated apps. Only allow access to necessary apps that support robust security controls. Third-party applications are not created by Microsoft, so there is a possibility they could be used for malicious purposes like exfiltrating data from your tenancy. Attackers can maintain persistent access to your services through these integrated apps, without relying on compromised accounts.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: BFA7EE22-B5A9-42C8-BD50-2E95885640BB
name: Third party integrated apps
description: |
'This query searches for your services by regulating the access of third-party integrated apps.
Only allow access to necessary apps that support robust security controls.
Third-party applications are not created by Microsoft, so there is a possibility they could be used for malicious purposes like exfiltrating data from your tenancy.
Attackers can maintain persistent access to your services through these integrated apps, without relying on compromised accounts.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: SenservaPro
dataTypes:
- SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1020
query: |
SenservaPro_CL
| where ControlName_s == 'AzureSecureScoreIntegratedApps'
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: ControlName_s
- identifier: AadTenantId
columnName: TenantId
- identifier: DisplayName
columnName: TenantDisplayName_s
- entityType: SecurityGroup
fieldMappings:
- identifier: DistinguishedName
columnName: Group_s
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: SourceSystem
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
SenservaPro_CL
Stage 2: where
| where ControlName_s == 'AzureSecureScoreIntegratedApps'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ControlName_s | eq |
| field:"ControlName_s" kind:eq value:"AzureSecureScoreIntegratedApps" |
TI map Domain entity to Web Session Events (ASIM Web Session schema)
#'This rule identifies Web Sessions for which the target URL hostname is a known IoC. This rule uses the Advanced Security Information Model (ASIM) and supports any web session source that complies with ASIM.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: b1832f60-6c3d-4722-a0a5-3d564ee61a63
name: TI map Domain entity to Web Session Events (ASIM Web Session schema)
description: |
'This rule identifies Web Sessions for which the target URL hostname is a known IoC. This rule uses the [Advanced Security Information Model (ASIM)](https:/aka.ms/AboutASIM) and supports any web session source that complies with ASIM.'
severity: Medium
requiredDataConnectors:
- connectorId: SquidProxy
dataTypes:
- SquidProxy_CL
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let HAS_ANY_MAX = 10000;
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
//Create a list of TLDs in our threat feed for later validation
let DOMAIN_TI=ThreatIntelligenceIndicator
// Picking up only IOC's that contain the entities we want
| where isnotempty(DomainName)
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now();
let DOMAIN_TI_list= todynamic(toscalar(DOMAIN_TI | summarize NIoCs = dcount(DomainName), Domains = make_set(DomainName)
| project Domains=iff(NIoCs > HAS_ANY_MAX, dynamic([]), Domains) ));
DOMAIN_TI
// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique (
_Im_WebSession(starttime=ago(dt_lookBack), url_has_any= DOMAIN_TI_list )
//Extract domain patterns from syslog message
| extend domain = tostring(parse_url(Url)["Host"])
| where isnotempty(domain)
| extend tld = tostring(split(domain, '.')[-1])
| extend Event_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.domain
| where Event_TimeGenerated < ExpirationDateTime
| summarize Event_TimeGenerated = arg_max(Event_TimeGenerated , *) by IndicatorId, domain
| project Event_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, domain, SrcIpAddr, Url
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
customDetails:
EventTime: Event_TimeGenerated
IoCDescription: Description
ActivityGroupNames: ActivityGroupNames
IndicatorId: IndicatorId
ThreatType: ThreatType
IoCExpirationTime: ExpirationDateTime
IoCConfidenceScore: ConfidenceScore
alertDetailsOverride:
alertDisplayNameFormat: A web request from {{SrcIpAddr}} to hostname {{domain}} matched an IoC
alertDescriptionFormat: A client with address {{SrcIpAddr}} requested the URL {{Url}}, whose hostname is a known indicator of compromise of {{ThreatType}}. Consult the threat intelligence blade for more information on the indicator.
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let HAS_ANY_MAX = 10000;
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let DOMAIN_TI is inlined into the numbered stages below.
Let Binding: DOMAIN_TI_list
let DOMAIN_TI_list = todynamic(toscalar(DOMAIN_TI | summarize NIoCs = dcount(DomainName), Domains = make_set(DomainName)
| project Domains=iff(NIoCs > HAS_ANY_MAX, dynamic([]), Domains) ));
Stages 1 to 5 define let DOMAIN_TI as the rule's main pipeline source. Stages 6 to 9 use it.
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where isnotempty(DomainName)
Stage 3: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 4: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 5: where
| where Active == true and ExpirationDateTime > now()
Stage 6: join
DOMAIN_TI
| join kind=innerunique (
_Im_WebSession(starttime=ago(dt_lookBack), url_has_any= DOMAIN_TI_list )
| extend domain = tostring(parse_url(Url)["Host"])
| where isnotempty(domain)
| extend tld = tostring(split(domain, '.')[-1])
| extend Event_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.domain
Stage 7: where
where Event_TimeGenerated < ExpirationDateTime
Stage 8: summarize
summarize Event_TimeGenerated by IndicatorId, domain
Stage 9: project
project ActivityGroupNames, ConfidenceScore, Description, Event_TimeGenerated, ExpirationDateTime, IndicatorId, SrcIpAddr, ThreatType, Url, domain
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
DomainName | is_not_null | field:"DomainName" kind:is_not_null | |
domain | is_not_null | field:"domain" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ConfidenceScore | project |
Description | project |
Event_TimeGenerated | project |
ExpirationDateTime | project |
IndicatorId | project |
SrcIpAddr | project |
ThreatType | project |
Url | project |
domain | project |
TIE Password Guessing
#'Searches for bruteforce Password Guessing attacks.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: d1416c25-5a56-4a88-8d7c-568e6551a307
name: TIE Password Guessing
description: |
'Searches for bruteforce Password Guessing attacks.'
severity: High
requiredDataConnectors:
- connectorId: TenableIE
dataTypes:
- Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
// For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
// Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
// Then, create the Kusto Function with alias afad_parser
afad_parser
| where MessageType == 2 and Codename == "Password Guessing"
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 2 and Codename == "Password Guessing"
Stage 3: extend
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | eq |
| field:"Codename" kind:eq value:"Password Guessing" |
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
Votiro - File Blocked from Connector
#'The analytic rule is intended to detect when a file is blocked by Votiro Sanitization Engine due to a specific policy, and notify the appropriate parties so that they can take appropriate action. The alert message will state that a file with a specific name and hash value was blocked by Votiro Sanatization Engine due to a specific policy name, and that more details can be found at a specific incident URL.'
MITRE ATT&CK Coverage
Rule Body
id: 17bf3780-ae0d-4cd9-a884-5df8b687f3f5
name: Votiro - File Blocked from Connector
description: |
'The analytic rule is intended to detect when a file is blocked by Votiro Sanitization Engine due to a specific policy, and notify the appropriate parties so that they can take appropriate action. The alert message will state that a file with a specific name and hash value was blocked by Votiro Sanatization Engine due to a specific policy name, and that more details can be found at a specific incident URL.'
severity: Low
requiredDataConnectors:
- connectorId: Votiro
dataTypes:
- CommonSecurityLog
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- Discovery
- Impact
relevantTechniques:
- T1036
- T1083
- T1057
- T1082
- T1565
- T1498
- T0837
query: let Votiro_view = view () {
VotiroEvents
| where sanitizationResult has "Blocked" and passwordProtected == "false" and from =~ "null"
| extend FileWithConnectorDetails = strcat_delim(' ', fileName, 'with the hash', SrcFileSHA256, 'that was sent from connector', connectorName)
| summarize count() by fileName, SrcFileSHA256, FileWithConnectorDetails, policyName, tostring(incidentURL), sanitizationResult, LogSeverity
| extend FileHashAlgo = "SHA256", FileHashValue = SrcFileSHA256};Votiro_view
entityMappings:
- entityType: FileHash
fieldMappings:
- identifier: Algorithm
columnName: FileHashAlgo
- identifier: Value
columnName: FileHashValue
incidentConfiguration:
createIncident: true
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: File with hash {{SrcFileSHA256}} was blocked
alertDescriptionFormat: The {{FileWithConnectorDetails}} was blocked by Votiro due to Policy rules, see more detail in the following link {{incidentURL}}
alertTacticsColumnName: sanitizationResult
alertSeverityColumnName: LogSeverity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Let Binding: Votiro_view
let Votiro_view = view () { VotiroEvents | where sanitizationResult has "Blocked" and passwordProtected == "false" and from =~ "null" | extend FileWithConnectorDetails = strcat_delim(' ', fileName, 'with the hash', SrcFileSHA256, 'that was sent from connector', connectorName) | summarize count() by fileName, SrcFileSHA256, FileWithConnectorDetails, policyName, tostring(incidentURL), sanitizationResult, LogSeverity | extend FileHashAlgo = "SHA256", FileHashValue = SrcFileSHA256};
Stage 1: source
VotiroEvents
Stage 2: where
where from =~ "null" and passwordProtected == false and sanitizationResult has "Blocked"
Stage 3: extend
extend FileWithConnectorDetails
Stage 4: summarize
summarize by fileName, SrcFileSHA256, FileWithConnectorDetails, policyName, sanitizationResult, LogSeverity
Stage 5: extend
extend FileHashAlgo, FileHashValue
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
from | eq |
| field:"from" kind:eq value:"null" |
passwordProtected | eq |
| field:"passwordProtected" kind:eq value:"false" |
sanitizationResult | match |
| field:"sanitizationResult" kind:match value:"Blocked" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
FileWithConnectorDetails | summarize |
LogSeverity | summarize |
SrcFileSHA256 | summarize |
fileName | summarize |
policyName | summarize |
sanitizationResult | summarize |
FileHashAlgo | extend |
FileHashValue | extend |
Zscaler - Unexpected ZPA session duration
#'Detects Unexpected ZPA session duration.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: e07846e0-43ad-11ec-81d3-0242ac130003
name: Zscaler - Unexpected ZPA session duration
description: |
'Detects Unexpected ZPA session duration.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
datatypes:
- ZPA_CL
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
- T1133
query: |
let dt_lookBack = 24h;
let time_treshhold = timespan(08:00:00);
let open_sessions =
ZPAEvent
| where TimeGenerated > ago(dt_lookBack)
| where DvcAction == 'open'
| summarize timestampstart = min(TimeGenerated) by DstUserName, SrcIpAddr, ConnectionID
| sort by timestampstart asc;
let closed_sessions =
ZPAEvent
| where TimeGenerated > ago(dt_lookBack)
| where DvcAction == 'close'
| summarize timestampend = max(TimeGenerated) by DstUserName, SrcIpAddr, ConnectionID
| sort by timestampend asc;
open_sessions
| join (closed_sessions) on DstUserName, SrcIpAddr, ConnectionID
| extend duration = timestampend - timestampstart
| where duration > time_treshhold
| where DstUserName !has "ZPA LSS"
| sort by timestampstart asc
| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = DstUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 24h;
let time_treshhold = timespan(08:00:00);
let open_sessions is inlined into the numbered stages below.
Let Binding: closed_sessions
let closed_sessions = ZPAEvent
| where TimeGenerated > ago(dt_lookBack)
| where DvcAction == 'close'
| summarize timestampend = max(TimeGenerated) by DstUserName, SrcIpAddr, ConnectionID
| sort by timestampend asc;
Stages 1 to 5 define let open_sessions as the rule's main pipeline source. Stages 6 to 11 use it.
Stage 1: source
ZPAEvent
Stage 2: where
| where TimeGenerated > ago(dt_lookBack)
Stage 3: where
| where DvcAction == 'open'
Stage 4: summarize
| summarize timestampstart = min(TimeGenerated) by DstUserName, SrcIpAddr, ConnectionID
Stage 5: sort
| sort by timestampstart asc
Stage 6: join
open_sessions
| join (closed_sessions) on DstUserName, SrcIpAddr, ConnectionID
Stage 7: extend
extend duration
Stage 8: where
where duration > time_treshhold
Stage 9: where
where not (DstUserName has "ZPA LSS")
Stage 10: sort
sort by timestampstart
Stage 11: extend
extend AccountCustomEntity, IPCustomEntity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DvcAction | eq |
| field:"DvcAction" kind:eq |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DstUserName | match | ZPA LSS | excludes:DstUserName field:"DstUserName" value:"ZPA LSS" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConnectionID | summarize |
DstUserName | summarize |
SrcIpAddr | summarize |
timestampstart | summarize |
duration | extend |
AccountCustomEntity | extend |
IPCustomEntity | extend |