| name | suricata-http-detection-rules |
| description | Use when writing or repairing a Suricata / Snort-syntax IDS signature that must fire on one specific HTTP request shape and must stay silent on near-miss traffic — a custom exfil pattern, a C2 beacon, a data-theft POST, a suspicious header. Fires on "write a Suricata rule", "update local.rules", "alert with sid:NNNNNNN", "must not false-positive", "run suricata offline against these pcaps", or a rule graded by replaying positive and negative pcaps. Carries sticky-buffer scoping, the parameter-anchoring regex that separates true positives from lookalikes, content byte-escaping, and the offline verification loop.
|
Suricata HTTP detection rules
A detection rule is graded on two sides at once: it must alert on every true positive and stay
silent on every lookalike. A rule that satisfies only the positive side is the normal failure, and
it is invisible unless you build the negatives yourself.
What transfers: the rule grammar, buffer scoping, the anchoring and escaping rules, and the
offline verification loop. What is per-task: the URI path, header name and value, parameter names,
length bounds, and the required sid. Read every one of those from the task statement — never
carry a literal from an example.
When to use this skill
- The deliverable is a rules file that Suricata will load, and a checker replays pcaps and looks for
a specific signature id in
eve.json.
- The task lists a conjunction of conditions ("alert only when all of the following are true") over
an HTTP request.
- An existing rule fires on benign traffic, or fails to fire on a variant.
Not for firewall/ACL authoring, packet capture triage without rule output, or log parsing.
Procedure
-
Write the conjunction down as separate clauses, then write the near-miss set. For every
clause, name the traffic that satisfies all the other clauses but not this one. The five that
graders reliably include:
| Near miss | What defeats it |
|---|
Same strings in a GET query string, empty body | constrain http.method and put body matches in the body buffer |
The parameter name appears inside another parameter's value (note=blob=AAAA…) | anchor the key: `(?:^ |
| Value one character below the length bound | exact quantifier plus a trailing anchor |
| A required parameter missing | one match per required clause, never one combined regex |
| Header present but with the wrong value | match name and value, with nocase |
-
Scope every match to a sticky buffer. Buffers are stateful: a content or pcre binds to the
most recently named buffer, and an unscoped one matches the raw reassembled stream, where header
text, URI text and body text are all visible at once — that is how "POST" matches inside a body.
| Buffer | Holds |
|---|
http.method | GET, POST, … |
http.uri | normalized path plus query string |
http.header | normalized Name: Value lines |
http.request_body (legacy http_client_body) | request body bytes |
http.host, http.cookie, http.user_agent | the named field only |
-
Constrain direction and state: flow:established,to_server;. Without it the rule can be
evaluated on partial or server-side data.
-
Escape bytes that the rule parser owns. Inside a content string, write ; as |3b|, " as
|22|, \ as |5c|, | as |7c|; : is conventionally written |3a| to avoid parser
surprises. Header names and values are case-insensitive on the wire — add nocase.
-
Anchor form parameters in the regex. This is the single highest-value line in the rule:
pcre:"/(?:^|&)blob=[A-Za-z0-9+\/]{80,}={0,2}(?:&|$)/";
(?:^|&) forces the key to start the body or follow a separator, which is what rejects
note=blob=…. (?:&|$) closes the value, which is what makes {64} mean exactly 64 rather
than at least 64. / inside a /…/-delimited PCRE must be escaped as \/. Base64 alphabets
include + and /, so a class of [A-Za-z0-9] silently misses real payloads; allow ={0,2}
for padding.
-
Give the rule the exact sid the task names, plus a rev and a specific msg. A checker
looks for the numeric signature id, not the message.
-
Verify offline against both directions before finishing. Run Suricata over the supplied pcaps
and over pcaps you build yourself for each near miss:
suricata --runmode single -c suricata.yaml -S local.rules -k none -r case.pcap -l ./out
python3 -c "import json,sys;
print([json.loads(l)['alert']['signature_id'] for l in open('out/eve.json')
if json.loads(l).get('event_type')=='alert'])"
When building a negative pcap with Scapy, emit a complete TCP session — SYN / SYN-ACK / ACK,
the request split across two or three PSH segments, a minimal HTTP/1.1 200 OK response, then
FIN. A lone packet carrying HTTP bytes never populates the http.* buffers, so the rule appears
to pass a negative it never actually evaluated. Splitting the request across segments also proves
the rule survives stream reassembly rather than depending on packet boundaries.
Worked example
Requirement: alert only on a POST to /telemetry/v2/report carrying header X-TLM-Mode: exfil, a
body parameter blob= whose value is Base64-like and at least 80 characters, and a body parameter
sig= of exactly 64 hex characters.
alert http any any -> any any ( \
msg:"Custom exfil telemetry"; \
flow:established,to_server; \
http.method; content:"POST"; \
http.uri; content:"/telemetry/v2/report"; \
http.header; content:"X-TLM-Mode|3a| exfil"; nocase; \
http.request_body; content:"blob="; \
pcre:"/(?:^|&)blob=[A-Za-z0-9+\/]{80,}={0,2}(?:&|$)/"; \
pcre:"/(?:^|&)sig=[0-9a-fA-F]{64}(?:&|$)/"; \
sid:1000001; rev:1;)
Clause-by-clause: http.method + content:"POST" kills the GET lookalike whose strings live in the
URL. The http.header content plus nocase accepts x-tlm-mode and X-TLM-MODE alike; if the
value may carry extra leading whitespace, replace that content with
pcre:"/^X-TLM-Mode:\s*exfil/mi"; on the same buffer. The two anchored PCREs carry the whole
false-positive burden: without (?:^|&) the rule alerts on note=blob=…, and without (?:&|$) a
65-hex-character signature satisfies "exactly 64".
Then confirm, per case, that the sid appears for each positive and is absent for each negative
before treating the rule as finished.
References