H3AD-REF / GUIDES / SURICATA & SNORT RULE WRITING

Network Rules.
Suricata & Snort, One Syntax Mostly.

A static reference for writing network IDS/IPS rules: header anatomy, sticky buffers versus the deprecated content-modifier syntax, the detection keywords that do the real work, and the judgment calls that keep a rule from going blind against encrypted traffic. No rule engine here, no live testing, just the reference. For the port/service context behind a rule's dport/sport match, see the Protocol & Port Reference.

Rule Anatomy

Action, header, options. Suricata and Snort share this shape; Suricata's app-layer protocols and sticky buffers are the parts that moved on.

The Three Parts, In Order

1. Action
alert | drop | reject | pass
├── alert: logs and generates an event, never blocks traffic
├── drop (IPS mode only): blocks and logs, silently
└── reject: blocks and logs, and sends a TCP RST or ICMP unreachable back
    // Suricata runs IDS and IPS from the same binary; Snort needs inline mode configured for drop/reject to block

2. Header
protocol src_ip src_port -> dst_ip dst_port
├── protocol: tcp, udp, icmp, or an app-layer protocol like http, tls, dns
├── src_ip / dst_ip: an IP, a CIDR block, a variable ($HOME_NET, $EXTERNAL_NET), or a negation (!$HOME_NET)
└── -> is directional; <> matches traffic in either direction
    // $HOME_NET / $EXTERNAL_NET are defined once in suricata.yaml or snort.conf, not re-typed per rule

3. Options (everything inside the parens)
(msg:"..."; content:"..."; sid:1000001; rev:1;)
├── every option ends in a semicolon; order matters for sticky buffers, not much else
├── msg, sid, rev, classtype, reference are metadata and required bookkeeping
└── content, pcre, flow, and sticky buffers like http.uri are the actual matching logic
    // sid must be unique across the whole ruleset; local rules conventionally start at 1000000+

Sticky Buffers

A sticky buffer sets which part of the traffic every content/pcre keyword after it inspects, until the next sticky buffer or the end of the rule.

BUFFER

Modern Sticky Buffer Syntax

The buffer name comes first, as its own keyword, then content: keywords apply to it
  • http.uri; content:"/gate.php"; — matches only inside the request URI, not the whole packet
  • http.host; content:"evil.example"; — matches only the HTTP Host header
  • http.user_agent; content:"curl"; — matches only the User-Agent header value
  • Everything after a sticky buffer keyword stays scoped to it until another sticky buffer appears
BUFFER

Deprecated Content Modifiers

The old Snort-style syntax, still parsed but not how new rules should be written
content:"/gate.php"; http_uri; puts the modifier after the content match instead of before it. Suricata still accepts this form, but every current ruleset and the official documentation write new rules with sticky buffers instead — write new rules the modern way and only expect to read the old form in inherited rulesets.
BUFFER

Common Buffers Worth Knowing

The ones that show up in most real-world rules
  • tls.sni — the SNI hostname from a TLS ClientHello, readable even though the session is encrypted
  • dns.query — the queried domain name in a DNS request
  • file.data — reassembled, decompressed file content extracted from the stream (HTTP body, SMB file transfer, etc.)

Detection Keywords

The options that decide whether a rule fires, and how expensive it is to evaluate on every packet.

KEYWORD

content & nocase

The base string match, binary-safe
content:"malware"; nocase; is the cheapest and most common match. Binary bytes can be mixed in with pipe syntax, e.g. content:"|4D 5A|"; for the MZ header. Without nocase, the match is case-sensitive.
KEYWORD

pcre

Regex matching, meaningfully slower than content
pcre:"/pattern/i"; is far more expressive than a fixed string but costs more CPU per packet. Pair it after a content match narrows the candidate packets first, rather than running pcre against every packet on the wire.
KEYWORD

flow

Restricts a rule to a TCP state and a direction
flow:established,to_server; only inspects packets in an established session flowing toward the server. Direction values are to_server, to_client, from_client, from_server — matching the wrong direction is one of the most common reasons a rule never fires.
KEYWORD

flowbits

Lets one rule set a flag another rule in the same flow can check
flowbits:set,seen_login; on one rule and flowbits:isset,seen_login; on a later one builds multi-stage detection logic, where a second rule only fires because an earlier rule already matched something in the same session.
KEYWORD

fast_pattern

Tells the multi-pattern matcher which content to index first
Suricata pre-filters packets using one content match per rule before evaluating the rest of the rule's logic. Put fast_pattern; on the longest, least-common string in the rule, not necessarily the first one written, since a short or common string makes a poor pre-filter.
KEYWORD

threshold / detection_filter

Rate-limits a rule that's real but noisy by nature
threshold:type limit, track by_src, count 5, seconds 60; only alerts after 5 matches from the same source within a minute. Built for patterns like brute-force login attempts, where every individual match is genuine but alerting on each one drowns the analyst.

Best Practices

The habits that keep a network rule fast, accurate, and maintainable once someone else inherits it.

PRACTICE

Write New Rules With Sticky Buffers

Not the deprecated content-modifier form
Use http.uri, tls.sni, and the other sticky buffer keywords in anything new you write. The old content:"x"; http_uri; form still parses, but it's the legacy syntax, not the one to reach for by default.
PRACTICE

Put fast_pattern On The Most Selective Content

Not the first content keyword written
The pre-filter match should be the longest, least-common string in the rule. A short or generic string like content:"GET"; makes a poor fast_pattern candidate since it pre-filters almost nothing.
PRACTICE

Anchor Every Rule With flow

A content match with no flow keyword checks every packet in both directions
Adding flow:established,to_server; (or whichever direction actually applies) cuts both the CPU cost and the false-positive surface, since the rule stops inspecting traffic it was never meant to match.
PRACTICE

Give Every Rule A Real classtype And reference

Same discipline as meta.reference in YARA and tags in Sigma
An analyst triaging a hit months later needs to know why the rule exists without reverse-engineering it. reference:url,attack.mitre.org/techniques/T1059/001/; maps a hit straight to an ATT&CK technique.
PRACTICE

Test Against A Clean Pcap Before Deploying

Validate the rule outside production first
suricata -r sample.pcap -S your.rules -l /tmp/out confirms a rule fires, or doesn't, against a known pcap without touching live traffic or an existing production ruleset.
PRACTICE

Keep A Dedicated sid Range For Local Rules

Collisions between custom and vendor rules are a real, common failure
Reserve a block, commonly 1000000-1999999, for local and custom rules so they never collide with an imported ruleset like ET Open or a commercial feed.

Common Pitfalls

Mistakes that don't show up as an error, just as a rule that silently never fires or fires on everything.

PITFALL

Missing Flow Direction

A rule meant for responses that also inspects requests
A rule that should only fire on server responses but has no flow:to_client; will also inspect client-to-server traffic, doubling both the match surface and the false-positive risk.
PITFALL

content Without nocase On Case-Varying Input

HTTP headers and URIs aren't reliably one case
Different clients and proxies inflect case differently. A bare content:"POST"; can silently miss a lowercase or mixed-case variant that a real client or a deliberately evasive one sends.
PITFALL

Encrypted Payloads Defeat content/pcre Entirely

TLS 1.3 encrypts almost everything past the ClientHello
Matching inside an HTTPS session requires TLS termination or decryption upstream of Suricata. Without that, detection has to fall back to metadata that's still visible in cleartext, like tls.sni or a JA3/JA4 TLS fingerprint.
PITFALL

Duplicate Or Reused sid Values

Two rules, one identity
Suricata will refuse to load, or silently prefer one of, two rules that share a sid. Always check for collisions before deploying custom rules alongside a vendor ruleset like ET Open.

Full Example

Every keyword from the sections above, in one rule, doing real work.

Suspicious PowerShell EncodedCommand Over HTTP POST

alert http $HOME_NET any -> $EXTERNAL_NET any (
    msg:"SUSPICIOUS PowerShell EncodedCommand over HTTP POST";
    flow:established,to_server;
    http.method; content:"POST"; nocase;
    http.uri; content:"/gate.php"; fast_pattern; nocase;
    file.data; content:"-EncodedCommand"; nocase;
    classtype:trojan-activity;
    reference:url,attack.mitre.org/techniques/T1059/001/;
    sid:1000042; rev:1;
)

// http.method, http.uri, and file.data are sticky buffers — each content: that follows applies
// to the buffer named just before it, not the raw packet payload. fast_pattern sits on the URI
// content because it's the most unique string in the rule, so Suricata's matcher indexes on it first.