Community ID
Community ID is a single deterministic hash that identifies the same TCP/UDP flow across every tool in your detection and forensics stack — Zeek, Suricata, Elastic, Splunk, MDE, Arkime, and more. When a Suricata alert fires, Community ID lets you pivot instantly to the matching Zeek conn.log entry and the exact PCAP session in Arkime without manually matching IPs, ports, and timestamps.
A Suricata alert fires: "ET MALWARE Cobalt Strike Beacon Activity." The EVE JSON event contains a source IP, destination IP, source port, and destination port. You need to correlate it with the Zeek conn.log (to get duration, total bytes, and connection state), the Zeek ssl.log (to get the JA3 hash and TLS SNI), the Zeek files.log (to get any file hashes), and the Arkime PCAP session (to download the raw packets). Doing this by matching src_ip + dst_ip + src_port + dst_port across four different log sources takes minutes. Community ID makes it a single copy-paste search.
How Community ID is Computed
Community ID v1 Computation
═══════════════════════════════════════════════════════════════════
Input: 5-tuple + seed (default seed = 0)
├── Source IP (sorted — lower IP is always "source")
├── Destination IP
├── Source port
├── Destination port
└── Protocol number (6 = TCP, 17 = UDP, 1 = ICMP)
Normalization (ensures same hash regardless of which direction
you observe the flow):
If src_ip > dst_ip: swap src and dst IP and ports
If src_ip == dst_ip and src_port > dst_port: swap ports
(This means A→B and B→A for the same flow get the same hash)
Computation:
seed_bytes = seed as 2-byte big-endian (default: \x00\x00)
data = seed_bytes + src_ip_bytes + dst_ip_bytes +
proto_byte + padding_byte +
src_port_bytes + dst_port_bytes
hash = base64(HMAC-SHA1(key=seed, data=data))
Final format: "1:" + base64_hash
Example: 1:hO+sN4H+MG5MY/8hIoss3QXS2+Iw=
The "1:" prefix = version 1 of the spec (current version)
The base64 is padded to 28 characters (20-byte SHA1 = 28 base64 chars)
Same flow seen from different vantage points:
Zeek sees: src=10.0.1.50:49812 dst=185.220.101.47:443
Suricata sees: src=10.0.1.50:49812 dst=185.220.101.47:443
→ Both compute: 1:hO+sN4H+MG5MY/8hIoss3QXS2+Iw=
(Identical — no matching required)
Before Community ID, correlating the same TCP flow across Zeek, Suricata, and Arkime required matching src_ip + dst_ip + src_port + dst_port AND timestamp. Timestamps differ slightly between tools because Zeek logs the start of the session, Suricata logs when the alert fires (mid-session), and Arkime records when the first packet arrived at the capture point. If NTP skew differs by even 100ms, a time-based join fails or produces false matches. Community ID removes the timestamp problem entirely: the hash is derived only from the 5-tuple, so it's identical across every tool regardless of when they observed the flow. In Elastic/Splunk, a search for network.community_id: "1:hO+sN4H..." returns events from every data source that logged that flow — Suricata alert, Zeek conn.log, Zeek ssl.log, Zeek http.log — in one result set.
Community ID in Every Tool
| Tool | Community ID field | How to enable |
|---|---|---|
| Zeek | community_id in conn.log (and all logs) | Install zeek-community-id package via zkg |
| Suricata | community_id in EVE JSON | Enabled by default in Suricata 4.1+; community-id: yes in eve-log outputs |
| Elastic (ECS) | network.community_id | Filebeat Zeek/Suricata modules add this field automatically |
| Arkime | Native support in Arkime 2.0+ | Enabled by default; searchable in the UI |
| Splunk (ES) | flow_id after CIM mapping | Splunk App for Stream or manual field extraction |
| Microsoft Defender for Endpoint | NetworkCommunityId | Available in DeviceNetworkEvents KQL table |
| Python (manual) | Compute with communityid package | pip install communityid |
Computing Community ID in Python
#!/usr/bin/env python3
"""
Community ID implementation from scratch — to understand the spec.
For production use: pip install communityid
"""
import socket
import struct
import hmac
import hashlib
import base64
def community_id(src_ip: str, dst_ip: str,
src_port: int, dst_port: int,
proto: int, seed: int = 0) -> str:
"""
Compute Community ID v1 for a flow.
proto: 6=TCP, 17=UDP, 1=ICMP, 58=ICMPv6
"""
src_addr = socket.inet_aton(src_ip)
dst_addr = socket.inet_aton(dst_ip)
# Normalize direction: lower IP is always "source"
if src_addr > dst_addr:
src_addr, dst_addr = dst_addr, src_addr
src_port, dst_port = dst_port, src_port
elif src_addr == dst_addr and src_port > dst_port:
src_port, dst_port = dst_port, src_port
# Build the hash input
seed_bytes = struct.pack('>H', seed) # 2 bytes, big-endian
proto_byte = struct.pack('B', proto) # 1 byte
pad_byte = b'\x00' # 1 byte padding
sp_bytes = struct.pack('>H', src_port) # 2 bytes big-endian
dp_bytes = struct.pack('>H', dst_port) # 2 bytes big-endian
data = seed_bytes + src_addr + dst_addr + proto_byte + pad_byte + sp_bytes + dp_bytes
# HMAC-SHA1
digest = hmac.new(seed_bytes, data, hashlib.sha1).digest()
return '1:' + base64.b64encode(digest).decode()
# Examples
print(community_id('10.0.1.50', '185.220.101.47', 49812, 443, proto=6))
# → 1:hO+sN4H+MG5MY/8hIoss3QXS2+Iw=
# Verify that reversed direction gives the same hash
print(community_id('185.220.101.47', '10.0.1.50', 443, 49812, proto=6))
# → 1:hO+sN4H+MG5MY/8hIoss3QXS2+Iw= (same — normalized)
# Using the communityid library (production use)
import communityid
cid = communityid.CommunityID()
print(cid.calc(communityid.FlowTuple.make_tcp('10.0.1.50', '185.220.101.47', 49812, 443)))
# Computing from a Zeek conn.log CSV row
import csv
with open('conn.log', 'r') as f:
for row in csv.DictReader(f, delimiter='\t'):
cid_hash = community_id(
row['id.orig_h'], row['id.resp_h'],
int(row['id.orig_p']), int(row['id.resp_p']),
proto=6 if row['proto'] == 'tcp' else 17
)
print(f"{row['ts']} {cid_hash} {row['id.orig_h']}:{row['id.orig_p']}"
f" → {row['id.resp_h']}:{row['id.resp_p']}")
Using Community ID in a Real Investigation
# Scenario: Suricata fired an alert. Pivot to all related log sources.
# Step 1: Get the Community ID from the Suricata EVE alert
ALERT_CID=$(cat /var/log/suricata/eve.json | \
jq -r 'select(.event_type=="alert" and
.alert.signature_id==2028465) | .community_id' | head -1)
echo "Community ID: $ALERT_CID"
# → 1:hO+sN4H+MG5MY/8hIoss3QXS2+Iw=
# Step 2: Find the matching Zeek conn.log entry
grep "$ALERT_CID" /var/log/zeek/current/conn.log | \
awk '{print $1, $4, $5, $6, $7, $9, $10, $11}' # ts,src,dst,proto,service,duration,orig_bytes,resp_bytes
# Step 3: Find the TLS metadata (JA3, SNI, certificate)
grep "$ALERT_CID" /var/log/zeek/current/ssl.log | \
awk '{print $8, $10, $11, $22, $23}' # ja3, server_name, subject, issuer
# Step 4: Find any file transfers in this session
grep "$ALERT_CID" /var/log/zeek/current/files.log | \
awk '{print $4, $5, $8, $11}' # source, depth, mime_type, sha256
# Step 5: Search Arkime for the PCAP session
# Arkime REST API
curl -u analyst:changeme \
"http://localhost:8005/api/sessions?query=communityId:$ALERT_CID" | jq '.data[0]'
# Step 6: In Elasticsearch — search all data sources at once
curl -XGET "http://elastic:9200/_search" -H 'Content-Type: application/json' -d "{
\"query\": {
\"term\": { \"network.community_id\": \"$ALERT_CID\" }
}
}" | jq '.hits.hits[] | {_index, event_type: ._source.event_type, ts: ._source[\"@timestamp\"]}'
Q & A
Q: Two different connections show the same Community ID. How is that possible?
Community ID is computed from the 5-tuple: src_ip, dst_ip, src_port, dst_port, and protocol. Two connections will share the same ID if and only if they have the same 5-tuple — which means either: (1) The same TCP connection was captured twice from different vantage points (this is expected and the whole point of Community ID — it lets you correlate the same flow across different capture points), or (2) A port was reused for a new connection (TCP port reuse — after a connection closes, the OS may reuse the same ephemeral source port for a new connection to the same destination port). This is rare for the same destination but can happen in high-connection-rate environments. If you see two Community ID matches in Zeek conn.log with different ts timestamps and different durations, you're looking at port reuse for two distinct sessions. Use the Zeek uid field to distinguish them — each session has a unique uid even if Community ID is shared due to port reuse.