Writing Your First Suricata Rule: Syntax, a CVE Example, and False-Positive Testing
A practical guide to writing a Suricata rule: header structure, modern sticky buffers, a before/after CVE example, pcap testing, and the false-positive trap.
A scanner sweeps the perimeter, a CVE lands on an exposed component, or the team simply wants to detect a specific behavior on internal HTTP traffic. In all three cases, the answer comes down to the same skill: writing a Suricata rule. It is the single most common request in network detection work, yet few teams master the syntax beyond copy-pasting examples found online.
This guide walks through the full structure of a rule, a concrete example of detecting a CVE in the URI using modern sticky buffers, how to test before deploying, and the number-one trap that fills logs with false positives: forgetting flow:established.
The anatomy of a Suricata rule
A Suricata rule fits on one line, but every keyword carries a specific role. Here is the base skeleton for an HTTP detection:
alert http $EXTERNAL_NET any -> $HOME_NET any (
msg:"Description of the detection";
flow:established,to_server;
content:"pattern to match";
sid:9000001;
rev:1;
metadata:created_at 2026_07_17;
)Breaking it down.
- The header (
alert http $EXTERNAL_NET any -> $HOME_NET any) sets the action (alert), the application protocol (http, already parsed by Suricata), and the direction: traffic coming from outside toward the protected network. Swapping the variables or usingany -> anythrows away all directional meaning, which is essential to tell an attack request apart from a server response. msgis the human-readable label that shows up in alerts. It needs to be descriptive and stable over time: analysts triage in the SOC on this exact text.flow:established,to_serverrestricts the match to fully established TCP connections, client to server. It is a context keyword, not a content keyword, but leaving it out is the number-one source of false positives (more on that below).contentis the actual pattern being matched. Used alone against the raw payload, it works but stays fragile and costly, which is exactly where modern sticky buffers come in.sid(signature ID) andrev(revision) uniquely identify the rule. Internal sids should use a dedicated range so they never collide with public feeds (ET Open, for instance, reserves sids under 2000000).metadatacarries traceability: creation date, CVE reference, classification. This is not decoration, it is what lets you sort, correlate, and age out a rule base later.
A concrete case: detecting a CVE in the URI
Take an illustrative scenario: a CVE affecting a web portal, exploitable through a cmd= parameter sent to a specific endpoint, /api/v1/import. (The example is illustrative: the principle matters more than the chosen CVE.)
Before: the naive version, which matches almost anything.
alert tcp any any -> any any (msg:"ACME Portal RCE Attempt"; content:"/api/v1/import"; content:"cmd="; sid:9000001; rev:1;)This rule has four problems: it completely ignores traffic direction (any any -> any any), it never checks that the connection is established, it scans the raw TCP stream instead of the already-parsed HTTP buffer, and it has neither fast_pattern nor metadata.
After: the version built on sticky buffers.
alert http $EXTERNAL_NET any -> $HOME_NET any (
msg:"EXPLOIT ACME Portal CVE-2024-0000 RCE Attempt via cmd Parameter";
flow:established,to_server;
http.method; content:"POST"; startswith;
http.uri; content:"/api/v1/import"; nocase; fast_pattern;
content:"cmd="; nocase; distance:0;
reference:cve,2024-0000;
classtype:attempted-admin;
metadata:created_at 2026_07_17, cve CVE_2024_0000;
sid:9000001;
rev:1;
)http.uri is a sticky buffer: it redirects the content matches that follow toward the URI already normalized and decoded by the engine, rather than the raw packet. That is both more reliable (URL decoding is done once by Suricata, not reinvented inside the rule) and faster (the engine does not rescan the whole payload). The other common sticky buffers follow the same logic: tls.sni to match a domain name inside a TLS ClientHello without ever decrypting the traffic, or dns.query to match a specific DNS lookup, for instance a command-and-control domain. In all three cases, the rule reasons over a protocol field the engine has already parsed, not over raw bytes.
Testing before you deploy
A rule that has never run against real traffic is a hypothesis, not a detection. Two steps, in order.
1. Check the syntax.
suricata -T -S ma_regle.rules -c /etc/suricata/suricata.yamlThe -T flag runs a configuration test, -S loads the rule file to validate alongside the normal load. This step catches syntax errors (a missing parenthesis, a misspelled keyword), but it says nothing about whether the rule actually detects anything.
2. Replay a pcap and confirm the match.
suricata -r sample.pcap -S ma_regle.rules -k none -l /tmp/suricata-testThen open /tmp/suricata-test/fast.log or eve.json and confirm the alert with the expected sid is present, and just as importantly, absent when run against a benign traffic capture or a patched target. It is that double proof, not syntax validation alone, that separates a rule the team can trust from one that merely "compiles."
The number-one trap: forgetting flow:established
This is the most frequent and most costly mistake in terms of noise. Without flow:established,to_server, the engine evaluates the rule against any packet matching the pattern, including raw payloads sent by a port scan that never completes the TCP handshake, or malformed packets outside a real session. The result: the rule fires on scans (nmap, masscan) that represent no real exploitation attempt, drowning the team in alerts with zero value.
Two additional refinements further cut the false-positive risk:
- Anchor with
http.method:http.method; content:"POST"; startswith;guarantees only a POST request triggers the alert, consistent with the actual exploitation method described in the advisory. - Handle case with
nocase: an attacker who triesCMD=instead ofcmd=to dodge a case-sensitive rule slips through ifnocaseis missing. On the other hand, adding it everywhere without thought can widen the match beyond what is needed: apply it where case variation is plausible, not as a reflex.
Performance: fast_pattern and buffer choice
At high throughput, every poorly built rule adds load to the engine. Two levers matter here.
fast_pattern tells the engine which pattern to use in the multi-pattern matcher for the first filtering pass, before the rest of the rule is even evaluated. Placing it on the longest, most distinctive pattern (/api/v1/import, fifteen characters) rather than a short, common one (cmd=, four characters) drastically cuts the number of packets that trigger a full rule evaluation.
Buffer choice matters just as much. Matching against http.uri rather than the raw TCP payload spares the engine from rescanning text the HTTP parser has already extracted and normalized. On a multi-gigabit link, the difference between a rule built on protocol buffers and one built on raw content shows up directly as CPU load and as packet-drop risk under pressure.
Prioritize: not every CVE deserves a rule
Writing a rule has a cost, however small. Two free, public sources help decide where to spend it:
- CISA KEV (Known Exploited Vulnerabilities): the catalog of actively exploited flaws. A CVE on that list is no longer a hypothesis, it is an operational emergency.
- EPSS (Exploit Prediction Scoring System): a 30-day exploitation probability, useful for arbitrating between two CVEs before either one is actually exploited.
Crossing "this CVE is in KEV" with "it hits one of my exposed assets" produces a short, actionable list: those are the rules to write first, not the forty thousand CVEs published this year.
In summary
A reliable Suricata rule rests on disciplined structure (header, flow:established, sticky buffers, metadata), on empirical proof rather than assumption (syntax validated, then a confirmed match on a vulnerable pcap and silence on a patched target), and on prioritizing by KEV and EPSS instead of by CVE arrival order.
Keeping a NIDS rule base up to date, tested, and low on false positives takes time few teams have to spare internally. That is exactly what the ThreatClaw NIDS rule feed delivers: rules validated on the real engine, prioritized by active exploitation, shipped ready to use instead of written under pressure on a Friday evening.
Related articles
ET Open is free, ET Pro and Talos are paid, and curated feeds sit in between. A practical comparison of Suricata and Snort rulesets for network detection, with the criteria that matter for a SOC or MSSP.
JA3 struggles against TLS 1.3. Configure JA4 in Suricata, write a ja4.hash detection rule, and correlate with SNI to catch encrypted C2 and data exfiltration.
Suricata vs Snort compared on architecture, rule compatibility, ICS/OT coverage, and migration steps, to help you pick a NIDS engine on technical merit.
Network intrusion detection NIDS: where Suricata sees what an EDR cannot, the anatomy of a rule, and why a curated rule pack beats a raw, noisy rule feed.