Hello community,
I’m sharing a composite detection pattern that took me quite some time to figure it out, because it addresses a limitation in Chronicle composite rules that isn’t clearly documented.
This pattern may help whoever needs to build OR-based composite detections across heterogeneous telemetry (for example, host-based vs user-based signals).
I needed a composite rule that triggers when either of the following detections fires for a watchlist entity:
-
USB File Write activity (host-centric)
-
Outbound personal email attachments (user-centric)
Constraints:
-
The USB rule only has hostname
-
The Email rule only has email address
-
There is no common identity field across the two detections
-
The watchlist contains both email and hostname
-
Chronicle composite rules require all event groups in events: to be joined by equalities
-
Using OR directly between watchlist fields and detection fields results in compiler errors such as:
validating intermediate representation:
event variables are not all joined by equalities
The rules below are not working examples as they need to be adjusted to each environment.
Producer Rule #1
Log Type Characteristics
-
Identity: hostname
-
User identity may be missing or inconsistent
rule USB_Device_Mounted_and_FileWrite {
meta:
description = "Detect USB device usage"
severity = "Low"
events:
$e.metadata.product_event_type = /RemovableMediaVolumeMounted/
$Host = strings.to_lower($e.principal.hostname)
$User = $e.principal.user.userid
match:
$Host over 30m
outcome:
$risk_score = 10
$hostname = $Host
$User = $User
$USBFilesWritten = array_distinct($e.target.file.full_path)
$join = "1"
condition:
$e
}
Producer Rule #2
Log Type Characteristics
-
Identity: email address
-
No hostname information
-
Cannot be directly joined to the USB rule
rule Outbound_Personal_Email_Attachment {
meta:
description = "Detect outbound personal email attachments"
severity = "Low"
events:
$e.metadata.event_type = "EMAIL_TRANSACTION"
$Sender = strings.to_lower($e.network.email.from)
match:
$Sender over 30m
outcome:
$risk_score = 10
$OutboundEmailSender = $Sender
$OutboundEmailSubject = $e.network.email.subject
$OutboundEmailAttachmentMBytes =
cast.as_float($e.additional.fields["totalSizeAttachments"]) / 1000000
$join = "1"
condition:
$e
}
Composite rule: OR logic across USB and Email events for watchlist entities
The solution that worked was:
-
Use a single detection placeholder ($d) to pool both rules
-
Introduce a synthetic join key from the watchlist to satisfy the compiler’s equality-join requirement
-
Apply the real correlation logic (hostname OR email) once the join graph is satisfied
⚠️ This is intentionally a workaround, not a preferred long-term design.
rule Composite_Departing_Employee_Exfiltration {
meta:
description = "Composite OR rule for USB or Email exfiltration scoped to watchlist"
severity = "Medium"
events:
// Watchlist row
$userid = %Watchlist1.userid
$upn = %Watchlist1.upn
$email = %Watchlist1.email_address
$hostname = %Watchlist1.hostname
$key = %Watchlist1.key
// Detection pool (single placeholder)
(
$d.detection.detection.rule_id = "ru_whatever_id_1" // USB
or
$d.detection.detection.rule_id = "ru_whatever_id_2" // Email
)
$Rule_Name = $d.detection.detection.rule_name
// Synthetic join to satisfy Chronicle compiler requirements
// Required because USB and Email detections share no common identity fields.
// Do not remove unless upstream rules emit a shared identity key.
$d.detection.detection.variables["join"] = $key
// Real correlation logic (email OR hostname)
// Since this is scoped to the aforementioned detections there is always
// a "hostname" or an "OutboundEmailSender"
(
strings.to_lower($d.detection.detection.variables["hostname"]) = $hostname
or
strings.to_lower($d.detection.detection.variables["OutboundEmailSender"]) = $email
)
// Context variables (may be empty depending on rule)
$USBHost = $d.detection.detection.variables["hostname"]
$USB_User = $d.detection.detection.variables["User"]
$USB_FilesWritten = $d.detection.detection.variables["USBFilesWritten"]
$USB_DeviceType = $d.detection.detection.variables["DeviceType"]
$EmailSender = $d.detection.detection.variables["OutboundEmailSender"]
$Email_Subject = $d.detection.detection.variables["OutboundEmailSubject"]
$Email_Attachment_MBytes = $d.detection.detection.variables["OutboundEmailAttachmentMBytes"]
match:
$key, $Rule_Name over 59m
condition:
$d
options:
// Allows inspection of all variables during testing
allow_zero_values = true
}
-
This pattern intentionally bypasses the composite join limitation using a synthetic equality.
-
The preferred long-term solution remains refactoring upstream rules to emit a shared identity key (user email, device ID, asset ID, etc.).
-
Until that is possible, this approach allows practical OR-based correlation across heterogeneous telemetry.
Happy to hear if others have found cleaner approaches, or if there are roadmap plans to relax the equality-join requirement in composite detections. (For example allowing event joins through a data table)
