Skip to main content

Cybersecurity

Cyber attacks unfold as sequences of events across hosts and accounts. Sifting patterns detect multi-stage attack chains by joining events through shared entities -- credentials, hosts, accounts -- with temporal ordering and exception clauses. The same mechanism that finds broken promises in narrative also finds lateral movement in a network.

Time~15 minutes
PrerequisitesWhat is Sifting?

1. Lateral Movement

An attacker authenticates across multiple hosts using the same stolen credential. Three logins, three hosts, one credential -- that is the signal.

Loading playground...

Result: 1 match -- stolen_cred on server1, server2, server3. Jane's logins use different credentials on each host, so the ?credential join never binds across all three stages.

What to notice

The ?credential variable appears in all three stages. This is the join condition: only login sequences that reuse the same credential across distinct hosts will match. Legitimate users rotating credentials naturally evade this pattern.


2. Impossible Travel

A user authenticates from two distant locations with no VPN session between. The VPN event would explain the geographic jump -- its absence is the signal.

Loading playground...

Result: 1 match -- Alice logged in from new_york then tokyo with no VPN between. Bob also jumped cities (london to paris), but his VPN connection at t=4 falls between his two logins, so unless between kills that match.

What to notice

The negation clause binds ?user, so only a VPN event for the same user counts as an exception. A VPN connection by a different user does not suppress the alert. This is the precision that variable-scoped negation provides.


3. Credential Stuffing

Multiple failed logins to the same account from different sources, with no successful login between. The successful login resets the count -- only uninterrupted failure sequences match.

Loading playground...

Result: Matches on admin_account -- failed logins from ip_10_0_0_1 and ip_10_0_0_2 (and from ip_10_0_0_1 and ip_10_0_0_3, etc.) all occur before the successful login at t=4 with no success between consecutive failures. The service_acct pair (t=6, t=9) is split by a successful login at t=8, so unless between blocks that match.

What to notice

The ?account join ensures only failures targeting the same account are correlated. The unless between resets detection when a legitimate login succeeds. Different ?src_a and ?src_b bindings capture the distributed nature of the attack -- multiple source IPs hitting one target.


Pattern Comparison

PatternStagesJoin variableNegationDetection signal
Lateral movement3?credentialnoneSame credential, multiple hosts
Impossible travel2?userunless between (VPN)Location jump without VPN
Credential stuffing2?accountunless between (success)Consecutive failures, no intervening success

Three mechanisms do all the work: stages define the temporal sequence, variable joins correlate events across stages, and negation windows suppress matches when a legitimate explanation exists.


Mapping your data

Windows Event Log and syslog entries map to fabula edges as follows:

Real-world fieldFabula edge
EventRecordIDsource node
EventID or message typelabel
Computer, TargetUserName, SourceAddresstarget values/nodes
TimeCreatedinterval start (point event: [t, t+1))

Most security events are instantaneous, so they become point intervals. The Computer and TargetUserName fields become target nodes, enabling variable joins that correlate events across hosts and accounts.


Limitations and false positives

These patterns are starting points, not production-ready rules. Each has known blind spots:

  • Lateral movement: Credential rotation evades the join on ?credential. The pattern only catches reuse of the same credential -- an attacker who steals a new credential per host is invisible.
  • Impossible travel: VPN and proxy use create false positives. A user routing through a VPN exit node in another country looks identical to impossible travel. You need additional context (e.g., known VPN IP ranges) to filter these.
  • Credential stuffing: Distributed attacks from many source IPs may not trigger the pattern if each IP attempts only once. The pattern requires two failures from different sources hitting the same account -- a single-attempt-per-IP botnet spreads below this threshold.
  • Mitigation: Combine sifting patterns with statistical baselines. Surprise scoring ranks matches by anomaly, so a lateral movement match involving a service account that always authenticates across hosts scores low. Tighten metric gap constraints (gap ..300 for "within 5 minutes") to reduce the window. Add negation for known-safe patterns (unless between for scheduled credential rotation events).

Timestamp resolution

Fabula requires strict temporal ordering between pattern stages (stage_1.start < stage_2.start). Events with identical timestamps cannot be placed in consecutive stages.

Impact on SIEM data: Log sources that batch events with the same millisecond timestamp (common in syslog, Windows Event Log, and batch-import scenarios) need pre-processing. Options:

  1. Add sub-millisecond resolution -- if your source provides sequence numbers or microsecond precision, use those as the time type
  2. Buffer and sort -- collect events in a short window (e.g., 1 second), assign monotonic sequence IDs, then feed to fabula
  3. Use batch evaluation -- evaluate_pattern() sees all events simultaneously and is not affected by timestamp collisions

See Thinking in Time -- The cost of intervals for full details.


How fabula compares

  • vs Splunk correlation searches: Threshold-based ("5 failed logins in 10 minutes"). No graph structure, no variable joins across events, no negation windows. Fabula patterns express multi-stage attack chains with entity correlation.
  • vs Elastic EQL: Supports sequence by [field] with maxspan -- the closest analogue to fabula's staged joins with gap constraints. However, EQL has no interval algebra, limited negation (no unless_after, no unless_global), and no composition operators for reusable pattern fragments.
  • vs Sigma rules: Single-event signatures. Sigma describes what one log line looks like, not temporal sequences across multiple events. Fabula operates at the sequence level with cross-event joins.

Where to go next

  • Getting Started -- Build these patterns in Rust, from cargo new to working alerts.
  • Pattern Cookbook -- More pattern recipes including repeat/range for threshold detection.
  • Scoring Reference -- Rank matched alerts by surprise to prioritize triage.