Chapter 54

EVE JSON

EVE (Extensible Event Format) JSON is Suricata's structured output format. All event types — alerts, DNS queries, HTTP transactions, TLS handshakes, flow summaries — are written to a single eve.json file as newline-delimited JSON, with an event_type field distinguishing them. EVE JSON is what you ship to your SIEM, query with jq for triage, and analyze in Python for hunting. This chapter covers EVE JSON field schemas and practical query patterns.

Scenario

A Suricata alert fires for SID 2024218 (ET MALWARE Cobalt Strike). The analyst opens eve.json, filters to the alert event, then pivots to the TLS event for the same flow_id to get the JA3 hash and certificate subject. They extract the community_id from the alert and search Zeek's conn.log for the same community_id to get the full connection duration and byte counts. All from one file using jq.

EVE JSON Schema Reference

  EVE JSON — Key Fields by Event Type
  ═══════════════════════════════════════════════════════════════════

  Common fields (all events):
    timestamp        "2024-01-15T14:23:17.123456+0000"
    flow_id          Integer: all events from same connection share this
    event_type       "alert" | "dns" | "http" | "tls" | "flow" | "smtp" | ...
    src_ip / dest_ip
    src_port / dest_port
    proto            "TCP" | "UDP" | "ICMP"
    community_id     Community ID hash (if enabled)

  event_type = "alert":
    alert.signature_id    SID of the matching rule
    alert.signature       Rule msg field
    alert.category        classtype of the rule
    alert.severity        1-3 (1=high, 2=medium, 3=low)
    alert.action          "allowed" | "blocked" (IPS mode)
    payload               Base64-encoded packet payload (if payload:yes in config)
    payload_printable     Printable ASCII version

  event_type = "tls":
    tls.sni              Server Name Indication
    tls.version          "TLS 1.3" etc.
    tls.cipher           Cipher suite
    tls.ja3.hash         JA3 fingerprint
    tls.ja3s.hash        JA3S fingerprint
    tls.subject          Server cert subject
    tls.issuerdn         Server cert issuer
    tls.fingerprint      Cert SHA1 fingerprint
    tls.notbefore        Cert validity start
    tls.notafter         Cert validity end

  event_type = "dns":
    dns.type             "query" | "answer"
    dns.rrname           Domain name queried
    dns.rrtype           "A" | "AAAA" | "MX" | "TXT" | ...
    dns.rcode            "NOERROR" | "NXDOMAIN" | ...
    dns.answers          Array of answer records (if type=answer)

  event_type = "flow":
    flow.pkts_toserver / flow.pkts_toclient
    flow.bytes_toserver / flow.bytes_toclient
    flow.start / flow.end
    flow.state           "established" | "closed" | "new"
    app_proto            Detected application protocol

EVE JSON Queries with jq

basheve-json-queries.sh
#!/bin/bash
EVE="${1:-/var/log/suricata/eve.json}"

echo "=== All alert SIDs with counts ==="
jq -r 'select(.event_type=="alert") | .alert.signature_id' "$EVE" | \
  sort | uniq -c | sort -rn | head -20

echo ""
echo "=== Alerts in last hour ==="
ONE_HOUR_AGO=$(date -u -d '1 hour ago' '+%Y-%m-%dT%H:%M:%S')
jq -r --arg cutoff "$ONE_HOUR_AGO" \
  'select(.event_type=="alert" and .timestamp > $cutoff) |
   [.timestamp, .src_ip, .dest_ip, .dest_port, .alert.signature] | @tsv' \
  "$EVE" | head -50

echo ""
echo "=== TLS events without SNI (potential C2) ==="
jq -r 'select(.event_type=="tls") |
  select(.tls.sni == null or .tls.sni == "") |
  [.timestamp, .src_ip, .dest_ip, .dest_port, (.tls.ja3.hash // "no-ja3")] | @tsv' \
  "$EVE" | head -20

echo ""
echo "=== JA3 matching known-bad hashes ==="
KNOWN_BAD='["51c64c77e60f3980eea90869b68c58a8","de9f2c7fd25e1b3afad3e85a0226a4c4"]'
jq -r --argjson bad "$KNOWN_BAD" \
  'select(.event_type=="tls") |
   select([.tls.ja3.hash] | inside($bad)) |
   [.timestamp, .src_ip, .dest_ip, .tls.sni, .tls.ja3.hash] | @tsv' \
  "$EVE" | head -20

echo ""
echo "=== DNS NXDOMAIN rate by source (DGA detection) ==="
jq -r 'select(.event_type=="dns") |
  select(.dns.type=="answer" and .dns.rcode=="NXDOMAIN") |
  .src_ip' "$EVE" | sort | uniq -c | sort -rn | head -10

echo ""
echo "=== Large outbound flows (>10MB) ==="
jq -r 'select(.event_type=="flow") |
  select(.flow.bytes_toserver > 10000000) |
  [.src_ip, .dest_ip, .dest_port, .flow.bytes_toserver, .flow.bytes_toclient] | @tsv' \
  "$EVE" | sort -t$'\t' -k4 -rn | head -10

echo ""
echo "=== Pivot: from alert to TLS context ==="
# Find a specific alert's flow_id, then get TLS metadata for that flow
TARGET_SID=2024218
FLOW_ID=$(jq -r --argjson sid "$TARGET_SID" \
  'select(.event_type=="alert" and .alert.signature_id==$sid) | .flow_id' \
  "$EVE" | head -1)

if [ -n "$FLOW_ID" ]; then
    echo "Alert flow_id: $FLOW_ID"
    echo "TLS context for this flow:"
    jq -r --argjson fid "$FLOW_ID" \
      'select(.flow_id==$fid and .event_type=="tls") |
       [.tls.sni, .tls.ja3.hash, .tls.subject, .tls.issuerdn, .tls.fingerprint] | @tsv' \
      "$EVE"
fi
Mental model: flow_id is Suricata's uid — use it to pivot between event types

Every EVE JSON event from the same network flow shares the same flow_id integer. When you see a Suricata alert, the flow_id in that alert lets you immediately retrieve all other protocol events from that same connection: the TLS handshake metadata (JA3, SNI, cert), the HTTP request details (URI, User-Agent, headers), the DNS query that preceded the connection, and the flow summary with total byte counts. This is how you go from "rule fired on SID X" to "here's the complete network context for that event" in one query: jq -r --argjson fid "$FLOW_ID" 'select(.flow_id==$fid)' eve.json. If you also correlate with Zeek via Community ID (both Suricata and Zeek can compute it), you get the cross-tool join: Suricata alert → same flow in Zeek ssl.log → same connection in Zeek conn.log → same source IP in Zeek dns.log. The Community ID is the bridge that makes multi-tool correlated investigation practical.

Q & A

Q: Suricata's eve.json is growing at 50 GB/day. How do I manage this?

At 50 GB/day, direct file storage becomes a problem within days. Production solutions: (1) Selective event types: in suricata.yaml's eve-log section, disable event types you don't need. If you only care about alerts and TLS, disable dns/http/flow output from eve.json — each of these can generate 5-10× as many events as alerts alone. DNS logging is especially high-volume; put it in a separate file or disable it if Zeek covers it. (2) Kafka as the primary sink: configure Suricata to write to a Kafka topic instead of a file. Kafka handles the buffering and consumers (Elasticsearch, Splunk) pull from it at their own rate. No file rotation to manage, and you can add multiple consumers without changing Suricata. (3) Flow events to a separate shorter-retention store: flow events are voluminous but less valuable after 24 hours. Write them to a separate daily index with 7-day retention, while keeping alert events for 90 days. (4) Compression: if writing to files, enable filesystem-level compression (ZFS with LZ4, or gzip via logrotate). EVE JSON compresses to about 15-20% of original size with gzip due to highly repetitive field names. (5) Alert-only mode: for high-traffic environments where you have Zeek for protocol logging, configure Suricata for alert events only — Suricata does the detection, Zeek does the logging, and you avoid duplication.