UPDATED · 23 JUN 2026 · EDIT ON GITHUB
GUIDES · DETECT

Cross-stream window joins.

The strongest detections correlate across streams: an identity sign-in followed by an endpoint exec on the same entity inside a short window. Here is how to write one without losing event ordering or blowing up the join.

11 min read Advanced By P. Sundar

Join on entity_id

Every stream shares a stable entity_id for both actor and target, so a cross-stream match is a join, not an integration. Name the streams, bind them on the shared entity, and the pipeline correlates them in flight.

YAMLdetections/signin-then-exec.yml
when: join: on: entity.actor # same actor across both streams streams: - stream: identity.signin as: signin match: { result: success, net.src_geo.new: true } - stream: endpoint.process as: exec match: { signed: false } window: 10m

Windows & ordering

A bare window asks “did both happen within 10 minutes?” Often you want more: that one happened before the other. Add a sequence and the pipeline only fires when the order holds.

YAMLrequire order
window: 10m sequence: [signin, exec] # sign-in must precede the exec
ClauseFires when
window onlyBoth legs match within the window, any order.
sequenceLegs match within the window, in the listed order.
withinTighter inter-leg gap, e.g. exec within 90s of sign-in.

Keep joins cheap

A join holds state for every open window. Two rules keep that state small: bound the window tightly, and join on a low-cardinality key. A 24-hour window on a high-volume stream is how a clever rule becomes an expensive one.

Always bound the window. An unbounded or very wide join window forces the pipeline to retain partial matches for every entity for the whole window. Keep it to minutes for high-volume streams; reach for hours only on sparse ones like privileged-role changes.

Where to go next

← PREV Backtesting a detection ALL GUIDES → Browse all guides