Chapter 5

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.

Scenario

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)
Why Community ID is the single most important cross-tool correlation key

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

ToolCommunity ID fieldHow to enable
Zeekcommunity_id in conn.log (and all logs)Install zeek-community-id package via zkg
Suricatacommunity_id in EVE JSONEnabled by default in Suricata 4.1+; community-id: yes in eve-log outputs
Elastic (ECS)network.community_idFilebeat Zeek/Suricata modules add this field automatically
ArkimeNative support in Arkime 2.0+Enabled by default; searchable in the UI
Splunk (ES)flow_id after CIM mappingSplunk App for Stream or manual field extraction
Microsoft Defender for EndpointNetworkCommunityIdAvailable in DeviceNetworkEvents KQL table
Python (manual)Compute with communityid packagepip install communityid

Computing Community ID in Python

Pythoncommunity-id.py
#!/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

Bashcommunity-id-pivot.sh
# 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.