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>

<!-- ============================================================
     ALERT RULE (starter) — put this in a RULES file, e.g.
     /var/ossec/etc/rules/local_rules.xml. Decoders and rules live
     in SEPARATE files. The rule matches the decoder above through
     <decoded_as>; set <level> and add <field>/<match> conditions so
     it alerts only on the events you care about. Rule ids 100000+
     are the user range — change them if they collide with yours.
     ============================================================ -->
<group name="suricata,">
  <rule id="100000" level="3">
    <decoded_as>suricata-json</decoded_as>
    <description>suricata: decoded event</description>
  </rule>
</group>
  • 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 added a starter alert <rule> (level 3, matched to the decoder via <decoded_as>) — put it in a RULES file (not the decoders file), set the level, and add <field>/<match> conditions; the commented example child rule shows the pattern
  • 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

Wazuh's OS_Regex is not PCRE — a bare . is a literal dot and \. matches any character. Test Wazuh OS_Regex patterns →

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:char-to{"extradata":"\""}%","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 field "timestamp": timestamp uses an ISO-8601-basic offset (e.g. "+0000", no colon) that liblognorm's date-rfc5424 rejects; captured as a raw token instead of a typed date
  • note chosen parser types: timestamp=char-to("), src_ip=ipv4, src_port=number, dest_ip=ipv4, dest_port=number, alert_signature=char-to("), alert_category=char-to("), alert_severity=number

Splunk

# props.conf  (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = (?=.*?"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+)?)).*

# Quick search-time test in SPL:
# | rex field=_raw "(?=.*?\"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 regex: input is JSON — use a JSON parser (jq, Logstash json filter, …) instead of a regex where possible
  • note regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
  • note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype
  • note a timestamp field was detected: this EXTRACT only makes it a searchable field. To set the event _time at index time, add TIME_PREFIX and TIME_FORMAT to this props.conf stanza (TIME_FORMAT uses Splunk strptime, e.g. %Y-%m-%dT%H:%M:%S) — this generator does not guess the strptime format.

ES ingest

PUT _ingest/pipeline/suricata
{
  "description": "LogForge-generated ingest pipeline for suricata",
  "processors": [
    {
      "json": {
        "field": "message",
        "add_to_root": true
      }
    }
  ]
}
  • note grok: json input — consider the Logstash json codec/filter instead of grok
  • note grok: constant field "event_type" embedded as literal anchor "alert" (varying=false)
  • note grok: constant field "proto" embedded as literal anchor "TCP" (varying=false)
  • note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
  • note json structure: emitted a { json: { field: "message", add_to_root: true } } processor — it parses the JSON line and lifts every field to the top level of the record, so a grok pattern is unnecessary
  • note test in Kibana Dev Tools with: POST _ingest/pipeline/suricata/_simulate (supply a docs[] array whose _source.message holds a sample line)

Graylog

# Grok patterns to add under System > Grok Patterns:
# (Graylog needs these custom patterns installed globally BEFORE the rule/extractor below will work.)
#   SURICATA_NOTDQUOTE [^"]*

# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "suricata-parse"
when
  has_field("message")
then
  let gp = grok(pattern: "\\{\"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}", value: to_string($message.message), only_named_captures: true);
  set_fields(gp);
end

# --- Graylog import-ready extractor JSON (secondary) ---
# Save as a .json file and import under System > Inputs > (input) > Manage extractors > Actions > Import extractors.
{
  "extractors": [
    {
      "title": "suricata",
      "extractor_type": "grok",
      "converters": [],
      "order": 0,
      "cursor_strategy": "copy",
      "source_field": "message",
      "target_field": "",
      "extractor_config": {
        "grok_pattern": "\\{\"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}",
        "named_captures_only": true
      },
      "condition_type": "none",
      "condition_value": ""
    }
  ],
  "version": "5.0.0"
}
  • note grok: json input — consider the Logstash json codec/filter instead of grok
  • note grok: constant field "event_type" embedded as literal anchor "alert" (varying=false)
  • note grok: constant field "proto" embedded as literal anchor "TCP" (varying=false)
  • note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
  • note 1 custom grok pattern(s) (SURICATA_NOTDQUOTE) must be installed globally first under System > Grok Patterns — see the block at the top of the output
  • note primary artifact is the processing-pipeline rule; the extractor JSON is an equivalent import-ready alternative for the classic extractor UI

Datadog

logforge_rule \{"timestamp":"%{date("yyyy-MM-dd'T'HH:mm:ss.SSSZ"):timestamp}","event_type":"alert","src_ip":"%{ipv4:src_ip}","src_port":%{integer:src_port},"dest_ip":"%{ipv4:dest_ip}","dest_port":%{integer:dest_port},"proto":"TCP","alert":\{"signature":"%{quotedString:alert_signature}","category":"%{quotedString:alert_category}","severity":%{number:alert_severity}
  • note emitted rule name is "logforge_rule"; rename it to match your "suricata" convention if desired
  • note json input — Datadog can parse JSON logs automatically; a Grok Parser is only needed for non-JSON message bodies
  • note constant field "event_type" embedded as literal anchor "alert" (varying=false)
  • note constant field "proto" embedded as literal anchor "TCP" (varying=false)
  • note paste this line into a Grok Parser processor in a Datadog Log Pipeline; matchers are anchored left-to-right and rule whitespace matches log whitespace. Complex or multi-shape logs may need Helper Rules.

Fluent Bit

[PARSER]
    Name        suricata
    Format      regex
    Regex       ^(?=.*?"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+)?)).*$
    Time_Key    timestamp
    Time_Format %Y-%m-%dT%H:%M:%S.%L%z
# Fluentd <parse> block:
#   <parse>
#     @type regexp
#     expression /^(?=.*?"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+)?)).*$/
#     time_key timestamp
#     time_format %Y-%m-%dT%H:%M:%S.%L%z
#   </parse>
  • note regex: input is JSON — use a JSON parser (jq, Logstash json filter, …) instead of a regex where possible
  • note regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
  • note Time_Key set to "timestamp"; Time_Format "%Y-%m-%dT%H:%M:%S.%L%z" is a best-effort strptime derived from the sample shape — verify it against your data (Fluent Bit uses %L for fractional seconds and %z for numeric offsets)

Vector

[transforms.suricata_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. = parse_json!(.message)
'''
  • note the reused regex needs lookahead, which the Rust regex crate behind parse_regex rejects; used parse_json! on .message (also the idiomatic Vector parser for JSON logs)

Loki

# promtail pipeline for "suricata" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
#   scrape_configs:
#     - job_name: suricata
#       pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
  - json:
      expressions:
        timestamp: 'timestamp'
        src_ip: 'src_ip'
        src_port: 'src_port'
        dest_ip: 'dest_ip'
        dest_port: 'dest_port'
        alert_signature: 'alert.signature'
        alert_category: 'alert.category'
        alert_severity: 'alert.severity'
  • note regex: input is JSON — use a JSON parser (jq, Logstash json filter, …) instead of a regex where possible
  • note regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
  • note JSON input: the reused regex uses lookaheads (RE2 cannot compile them), so a native `- json:` stage is emitted instead — it extracts each field by key without a regex

syslog-ng

parser p_suricata {
    regexp-parser(
        prefix(".suricata.")
        patterns("(?=.*?\"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 regex: input is JSON — use a JSON parser (jq, Logstash json filter, …) instead of a regex where possible
  • note regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
  • note captured fields are stored as name-value pairs under the prefix ".suricata." (e.g. a group (?<srcip>…) becomes ".suricata.srcip")
  • note json structure: syslog-ng has a dedicated json-parser() that handles nested objects natively — consider json-parser(prefix(".logforge.")) instead of the emitted regexp-parser

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 →