bokamba / logforge / parse / Suricata (eve.json)

$ logforge parse suricata

Parse Suricata (eve.json) logs → regex, Grok, Wazuh & rsyslog

Suricata, the open-source IDS/IPS and network security monitor, emits its richest output as EVE JSON (Extensible Event Format) — one JSON object per line, written to eve.json. Every line shares a common envelope (timestamp in ISO 8601 with microseconds and offset, flow_id, in_iface, src_ip, src_port, dest_ip, dest_port, proto) and then a discriminator field, event_type, that tells you what kind of record it is: alert, dns, http, tls, flow, fileinfo, and more. That event_type is the first thing to branch on, because the meaningful sub-object differs entirely between them — an alert record carries an alert object, a dns record carries a dns object, and so on.

The alert event_type is the one detection cares about most. Alongside the five-tuple (src_ip/src_port/dest_ip/dest_port/proto) it nests an alert object with the signature that fired: alert.signature is the human-readable rule name (e.g. 'ET SCAN Nmap Scripting Engine User-Agent'), alert.signature_id (the sid) is the numeric rule identifier, alert.category is the classtype, and alert.severity is the priority where 1 is the HIGHEST severity (this inversion trips people up — severity 1 is more urgent than severity 3). alert.gid and alert.rev complete the rule identity. The flow_id field is the correlation key that ties related events together: the alert, the http record for the same request, the tls record for the same session, and the eventual flow record all share one flow_id, so you reconstruct a full picture of a connection by grouping on flow_id across event types.

Parsing EVE is JSON-path selection, but the discipline is in the two-stage shape: read event_type first, then reach into the matching sub-object (alert.*, dns.*, http.*, tls.*). Do not assume alert.signature exists on a dns line — it does not. For detection the load-bearing fields are event_type=alert with alert.signature / alert.signature_id / alert.severity (what fired and how urgent), the five-tuple (who talked to whom), and flow_id (to pull in the corresponding http.hostname, http.url, tls.sni, or dns.rrname that gives the alert context). Because everything is keyed and typed, the challenge is not tokenization but knowing the per-event_type sub-schema and respecting that severity 1 outranks severity 3.

Open this in LogForge →

What a Suricata (eve.json) line looks like

The JSON sample below is fed verbatim into the engine to produce every parser on this page.

{"timestamp":"2026-07-03T14:22:15.123456+0300","event_type":"alert","src_ip":"203.0.113.66","src_port":44121,"dest_ip":"192.0.2.30","dest_port":8080,"proto":"TCP","alert":{"signature":"ET EXPLOIT Apache Log4j RCE Attempt","category":"Attempted Administrator Privilege Gain","severity":1}}
{"timestamp":"2026-07-03T14:22:49.774112+0300","event_type":"alert","src_ip":"198.51.100.23","src_port":51002,"dest_ip":"192.0.2.31","dest_port":445,"proto":"TCP","alert":{"signature":"ET SCAN Behavioral Unusual Port 445","category":"Detection of a Network Scan","severity":2}}

Detected fields

The engine classified this sample as json and consolidated 10 fields across 2 lines. Fields marked literal were identical on every sample line, so they are baked into the pattern as anchors rather than captured.

  • timestamp : timestamp
  • event_type : severity · literal
  • src_ip : ipv4
  • src_port : port
  • dest_ip : ipv4
  • dest_port : port
  • proto : quoted_string · literal
  • alert_signature : quoted_string
  • alert_category : quoted_string
  • alert_severity : number

Regex (named capture groups)

# sample: {"timestamp":"2026-07-03T14:22:15.123456+0300","event_type":"alert","src_ip":"203.0.113.66","src_port":44121,"dest_ip":"192.0.2.30","dest_port":8080,"proto":"TCP","alert":{"signature":"ET EXPLOIT Apache Log4j RCE Attempt","category":"Attempted Administrator Privilege Gain","severity":1}}
# groups: timestamp=2026-07-03T14:22:15.123456+0300, src_ip=203.0.113.66, src_port=44121, dest_ip=192.0.2.30, dest_port=8080, alert_signature=ET EXPLOIT Apache Log4j RCE Attempt, alert_category=Attempted Administrator Privilege Gain, alert_severity=1
^(?=.*?"timestamp":"(?<timestamp>[^"]*)")(?=.*?"event_type":"alert")(?=.*?"src_ip":"(?<src_ip>[^"]*)")(?=.*?"src_port":(?<src_port>\d{1,5}))(?=.*?"dest_ip":"(?<dest_ip>[^"]*)")(?=.*?"dest_port":(?<dest_port>\d{1,5}))(?=.*?"proto":"TCP")(?=.*?"signature":"(?<alert_signature>[^"]*)")(?=.*?"category":"(?<alert_category>[^"]*)")(?=.*?"severity":(?<alert_severity>-?\d+(?:\.\d+)?)).*$
  • note input is JSON — use a JSON parser (jq, Logstash json filter, …) instead of a regex where possible
  • note a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead

Grok pattern (Logstash / Elastic)

# custom patterns
SURICATA_NOTDQUOTE [^"]*

\{"timestamp":"%{TIMESTAMP_ISO8601:timestamp}","event_type":"alert","src_ip":"%{IPV4:src_ip}","src_port":%{INT:src_port},"dest_ip":"%{IPV4:dest_ip}","dest_port":%{INT:dest_port},"proto":"TCP","alert":\{"signature":"%{SURICATA_NOTDQUOTE:alert_signature}","category":"%{SURICATA_NOTDQUOTE:alert_category}","severity":%{NUMBER:alert_severity}
  • note json input — consider the Logstash json codec/filter instead of grok
  • note constant field "event_type" embedded as literal anchor "alert" (varying=false)
  • note constant field "proto" embedded as literal anchor "TCP" (varying=false)
  • note custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir

Wazuh decoder (OS_Regex XML)

<!--
  Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
  sample: {"timestamp":"2026-07-03T14:22:15.123456+0300","event_type":"alert","src_ip":"203.0.113.66","src_port":44121,"dest_ip":"192.0.2.30","dest_port":8080,"proto":"TC
  test with: /var/ossec/bin/wazuh-logtest
-->

<decoder name="suricata-json">
  <prematch>^{</prematch>
  <plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>
  • note JSON input: emitted a JSON_Decoder plugin decoder — Wazuh extracts every key automatically as dynamic fields (nested keys become dotted names)
  • note field names above are what the other LogForge generators use; JSON_Decoder will use the raw JSON keys instead
  • note decoder order and prematch specificity may need site-specific tuning (other decoders in your ruleset can shadow these) — validate with /var/ossec/bin/wazuh-logtest

rsyslog template / liblognorm rulebase

version=2
# suricata — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
#   module(load="mmnormalize")
#   action(type="mmnormalize" rulebase="/etc/rsyslog.d/suricata.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=suricata:{"timestamp":"%timestamp:date-rfc5424%","event_type":"alert","src_ip":"%src_ip:ipv4%","src_port":%src_port:number%,"dest_ip":"%dest_ip:ipv4%","dest_port":%dest_port:number%,"proto":"TCP","alert":{"signature":"%alert_signature:char-to{"extradata":"\""}%","category":"%alert_category:char-to{"extradata":"\""}%","severity":%alert_severity:number%}}
  • note json structure: rsyslog mmjsonparse handles CEE/JSON natively — consider action(type="mmjsonparse") instead of this rulebase
  • note trailing literal "}}" reconstructed from line 1
  • note chosen parser types: timestamp=date-rfc5424, src_ip=ipv4, src_port=number, dest_ip=ipv4, dest_port=number, alert_signature=char-to("), alert_category=char-to("), alert_severity=number

FAQ

What is event_type in Suricata EVE JSON and why branch on it?
event_type is the discriminator that names the record kind — alert, dns, http, tls, flow, fileinfo, and others. The meaningful nested object differs per type (an alert has an alert object, a dns record has a dns object), so you read event_type first and then reach into the matching sub-object. Fields from one type do not exist on another.
Why is Suricata alert.severity 1 more serious than 3?
Suricata inverts the scale: alert.severity 1 is the HIGHEST priority and larger numbers are lower priority (3 is routine). This catches people who assume bigger means worse. Triage and paging logic must treat severity 1 as most urgent.
How do I correlate related Suricata events?
Group by flow_id. Suricata assigns one flow_id to a connection, and the alert, http, tls, dns, and flow records for that same connection all carry it. Joining on flow_id lets you enrich an alert with the corresponding http.url, tls.sni, or dns.rrname to understand what actually happened.
Which fields identify the rule that fired?
On an alert record: alert.signature (the human-readable rule name), alert.signature_id (the numeric sid), alert.category (the classtype), plus alert.gid and alert.rev. Together with alert.severity these tell you exactly which rule triggered, how it is classified, and how urgent it is.

Try it on your own Suricata (eve.json) lines

Paste a few real lines, review the detected fields, and copy whichever format your stack needs. Free, no account, nothing uploaded.

Open this sample in LogForge →