Zeek Core Logs
Zeek generates over 30 different log files, each focused on a specific protocol or category. The five most forensically significant are conn.log, dns.log, ssl.log, http.log, and files.log. Understanding the schema of each — what every field means and which fields are most useful for threat hunting — is essential for writing effective Zeek-based detection queries.
An analyst is hunting for lateral movement across a 3-week Zeek dataset. They start with conn.log to identify hosts making many connections to internal port 445 (SMB), then cross-reference with kerberos.log to see which accounts authenticated to those destinations, then check ssl.log to see if any connections use self-signed certificates. Each log provides a different dimension of the same events — the combination paints a complete picture without touching PCAP at all.
conn.log Reference
conn.log Field Reference (most forensically useful)
═══════════════════════════════════════════════════════════════════
ts Timestamp (Unix epoch, float)
uid Unique connection ID — use this to correlate across logs
id.orig_h Source IP
id.orig_p Source port
id.resp_h Destination IP
id.resp_p Destination port
proto Transport protocol (tcp/udp/icmp)
service Detected application protocol (http/dns/ssl/smtp/...)
"-" if not detected
duration Session duration in seconds (NULL if incomplete)
orig_bytes Bytes sent by originator (payload only, not headers)
resp_bytes Bytes sent by responder
conn_state Connection state (critical for analysis):
S0 SYN seen, no SYN-ACK (scan/blocked)
S1 SYN+SYN-ACK, no FIN (established, still open at log time)
SF SYN+FIN both seen (normal completed connection)
REJ Connection rejected (RST after SYN)
S2 Established, originator FIN, no responder FIN
S3 Established, responder FIN, no originator FIN
RSTO Established, RST from originator
RSTR Established, RST from responder
SHR SYN-ACK with no prior SYN (partial capture)
OTH No SYN (packet capture started mid-connection)
missed_bytes Bytes missed due to packet loss (gaps in capture)
history Brief sequence of flags: S=SYN, A=ACK, D=data, F=FIN, R=RST
ShADadfFR means typical established connection with data
orig_pkts Packet count from originator
resp_pkts Packet count from responder
ssl.log Reference
ssl.log Field Reference ═══════════════════════════════════════════════════════════════════ ts Timestamp uid Connection UID (links to conn.log) id.orig_h/p Source IP/port id.resp_h/p Destination IP/port version TLS version negotiated (TLSv12, TLSv13, etc.) cipher Selected cipher suite curve Named elliptic curve (if ECDHE) server_name SNI from ClientHello (what client thinks it's connecting to) resumed true if session resumed (no full handshake) last_alert Alert type if connection failed next_protocol ALPN value (h2, h3, http/1.1, xmpp, etc.) established true if TLS handshake completed ssl_history Like conn.log history but for TLS events subject Certificate subject (CN=...) issuer Certificate issuer client_subject Client certificate subject (if mTLS) client_issuer Client certificate issuer (if mTLS) validation_status ok / self signed certificate / ... (cert validation result) ocsp_status stapled OCSP status ja3 JA3 client fingerprint (MD5 hash) ja3s JA3S server fingerprint (MD5 hash)
Threat Hunting Queries
#!/bin/bash
LOG_DIR="/opt/zeek/logs/current"
echo "=== 1. Large external data transfers (exfil candidates) ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/conn.log" | \
awk -F'\t' '
$9 ~ /[0-9]/ && $9+0 > 10000000 { # orig_bytes > 10 MB
# Skip private IP ranges
if ($5 !~ /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/)
print $1"\t"$3"\t"$5"\t"$9"\t"$10
}
' | sort -t$'\t' -k4 -rn | head -20
echo ""
echo "=== 2. Internal hosts connecting to many unique external IPs (scanning or C2 pivot) ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/conn.log" | \
awk -F'\t' '$5 !~ /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/ {print $3"\t"$5}' | \
sort -u | awk -F'\t' '{print $1}' | sort | uniq -c | sort -rn | head -20
echo ""
echo "=== 3. Self-signed TLS certificates ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/ssl.log" | \
awk -F'\t' '$NF ~ /self signed/ || $(NF-1) ~ /self signed/ {
print $1"\t"$3"\t"$5"\t"$8"\t"$NF
}' | head -20
echo ""
echo "=== 4. TLS connections with no SNI ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/ssl.log" | \
awk -F'\t' '$7 == "-" && $6 == "443" {print $3"\t"$5}' | \
sort | uniq -c | sort -rn | head -20
echo ""
echo "=== 5. Hosts making DNS queries for high-entropy domains ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/dns.log" | \
awk -F'\t' '$10 == "1" {print $9}' | \ # qtype 1 = A record
sort | uniq | python3 -c "
import sys, math
from collections import Counter
def entropy(s):
c = Counter(s)
total = len(s)
return -sum((v/total)*math.log2(v/total) for v in c.values())
for line in sys.stdin:
name = line.strip()
parts = name.split('.')
if len(parts) >= 2:
sld = parts[-2]
if len(sld) >= 8 and entropy(sld) > 3.5:
print(f'{entropy(sld):.2f}\t{name}')
" | sort -rn | head -30
echo ""
echo "=== 6. Files with known-bad hashes (from files.log) ==="
KNOWN_BAD_MD5="d41d8cd98f00b204e9800998ecf8427e" # Example (empty file MD5)
awk -v bad="$KNOWN_BAD_MD5" 'NR>8 && !/^#/ && $NF == bad {print}' \
"$LOG_DIR/files.log" 2>/dev/null | head -10
echo ""
echo "=== 7. SSH login attempts (many SYN to port 22) ==="
awk 'NR>8 && !/^#/' "$LOG_DIR/conn.log" | \
awk -F'\t' '$6 == "22" && $8 ~ /S0|REJ/ {print $3"\t"$5}' | \
sort | uniq -c | sort -rn | head -10
Every Zeek log entry related to a single network connection shares the same uid value. A TCP connection to an HTTPS server generates entries in conn.log, ssl.log, files.log (if files were transferred), and possibly http.log (if the TLS is decrypted). All of these entries have the same uid. This lets you start with one log, find a suspicious uid, then retrieve all related records: grep uid_value conn.log ssl.log files.log x509.log — instant multi-protocol view of one connection. In SIEM queries: when a Suricata alert fires with a connection uid, look up that same uid in Zeek's ssl.log to get the JA3 hash and certificate, in conn.log for the data volume, and in dns.log for the DNS name that resolved to the destination IP (look for dns log entries near the connection timestamp from the same source). The uid is the glue that makes correlated multi-log analysis practical.
Q & A
Q: conn.log shows duration="-" for many connections. What does that mean and should I be concerned?
Duration "-" in Zeek's conn.log means the connection was still open when Zeek generated the log entry (typically at log rotation time or capture end). Zeek writes a conn.log entry when a connection closes (FIN/RST) or when it times out. For connections that are still active at log rotation time, Zeek writes a partial entry with duration="-". This is normal for: long-lived TLS sessions, persistent TCP connections (keepalive), WebSocket connections, and SSH/RDP sessions. It becomes significant if you see many connections with duration="-" that should have completed: this may indicate the connection is a long-running tunnel (C2 over TLS, SSH tunnel), or that you have packet loss causing connections to appear open when they've actually closed. For hunting: search for connections with duration="-" that have large orig_bytes or resp_bytes — these are long-running connections that transferred significant data and never closed during the observation window. A C2 beacon that maintains a persistent connection to avoid SYN/SYN-ACK noise would show exactly this pattern.