Lesson 1 of 5 · 25 min read

The Network Hunter's Data Model — What Each Log Answers

Hunting requires knowing which questions each data source answers. A hunter who reaches for dns.log for beacon detection, or conn.log for URL analysis, wastes time and may miss the finding. The data model is the map — use it before every hypothesis.


The Network Hunter’s Log Map

                    ┌─────────────────────────────────────┐
                    │           NETWORK TRAFFIC            │
                    └──────┬───────────────┬──────────────┘
                           │               │
              ┌────────────▼─────────┐  ┌──▼─────────────────┐
              │     conn.log         │  │   Protocol Logs     │
              │  (all connections)   │  │  (if protocol seen) │
              │                      │  │                     │
              │  Who talked to who   │  │  dns.log  (queries) │
              │  For how long        │  │  http.log (URLs)    │
              │  How many bytes      │  │  ssl.log  (TLS meta)│
              │  Service detected?   │  │  files.log (hashes) │
              └──────────────────────┘  └─────────────────────┘

Log-to-Question Map

Hunter QuestionPrimary LogKey Fields
Is this host beaconing?conn.logts, duration, orig_bytes, resp_bytes, id.resp_h, id.resp_p
Is a host doing unusual DNS?dns.logquery, qtype_name, rcode_name, answers, TTL
Is TLS suspicious?ssl.logja3, server_name, validation_status, cipher
Was a file downloaded?files.logsha256, MIME type, tx_hosts, rx_hosts, conn_uids
What URLs was a host visiting?http.loghost, uri, user_agent, method, status_code
Is a host scanning internally?conn.logmany S0/REJ entries, single src, many dsts
Did the host use a rare JA3?ssl.logja3, count aggregated over time
Is DNS traffic carrying data?dns.loglong subdomain labels, TXT qtype, high volume

The Power-Law of Connection Frequency

Normal enterprise traffic follows a power-law distribution. A small number of destinations get most of the connections; a long tail of destinations get very few.

Connections to office365.com:      14,200 per day
Connections to google.com:         8,400 per day
Connections to cdn.example.com:    1,200 per day
...
Connections to analytics-cdn.io:   2 per day
Connections to update-cdn.net:     1 per day   ← hunter territory
Connections to cdn-metrics.io:     1 ever      ← high suspicion

Hunter insight: Attacker C2 lives in the tail. Query for:


Baseline First, Hunt Second

Before running any hunt, establish what “normal” is for your environment. Without a baseline, everything looks suspicious. With a baseline, you know what to explain away.

Three network baselines every hunter needs:

1. Destination Frequency Baseline

// Establish "well-known" external IPs and domains (seen by >10 hosts, every week)
Zeek_conn_CL
| where local_orig_b == true and local_resp_b == false
| where TimeGenerated > ago(30d)
| summarize 
    host_count = dcount(id_orig_h_s),
    conn_count = count()
  by id_resp_h_s
| where host_count >= 10  // seen by 10+ internal hosts = likely legitimate
| project id_resp_h_s as known_good_ip

Save this as your “known good” destination set. Any connection to an IP NOT in this set is a hunting candidate.

2. JA3 Frequency Baseline

// Most common JA3 fingerprints in your environment
Zeek_ssl_CL
| where TimeGenerated > ago(30d)
| summarize count() by ja3_s
| order by count_ desc
// Top 20 JA3 values = your expected browser/OS fingerprints
// Anything not in this set = anomalous TLS client

3. Per-Host Connection Profile

// For each internal host: how many unique external destinations per day?
Zeek_conn_CL
| where local_orig_b == true and local_resp_b == false
| summarize unique_dsts = dcount(id_resp_h_s) by id_orig_h_s, bin(TimeGenerated, 1d)
| summarize avg_unique_dsts = avg(unique_dsts), p99_unique_dsts = percentile(unique_dsts, 99) by id_orig_h_s
// A server that contacts 2 external IPs per day suddenly contacting 200 = anomaly

Long-Tail Hunting: First Principles

The simplest and most productive network hunt: find internal hosts talking to external destinations that no other internal host has seen.

// "Lone wolf" external destinations — seen by exactly 1 internal host ever
Zeek_conn_CL
| where TimeGenerated > ago(30d)
| where local_orig_b == true and local_resp_b == false
| summarize 
    host_count = dcount(id_orig_h_s),
    total_bytes = sum(orig_bytes_l + resp_bytes_l),
    first_seen = min(TimeGenerated),
    last_seen = max(TimeGenerated)
  by id_resp_h_s
| where host_count == 1              // only 1 internal host ever talked to this IP
| where total_bytes > 100000         // >100KB total (not a brief failed connection)
| order by total_bytes desc

Each result is a hypothesis: “Why is this host the only one that ever talked to this IP, and why did they exchange this much data?”


Hunt Planning: Data Source Validation

Before spending hours on a hunt, validate that the data exists in your environment:

Hunt: "Find hosts beaconing to C2 on 60-second intervals"

Data required:
  - conn.log with timestamps (ts field) — ✓ available?
  - External destination IP/port (id.resp_h, id.resp_p) — ✓ available?
  - Zeek retention period covers the hunt window — ✓ 30 days available?
  
Data gap:
  - If conn.log has been truncated or sampled, beacon math is unreliable
  - If only flow data (NetFlow, sFlow) is available without packet-level timing,
    connection timestamps may be rounded to 5-minute windows (not usable for 60s beacon)

Resolution: Confirm with infra team whether Zeek conn.log is full-fidelity or sampled.
If sampled: switch hypothesis to long-connection hunt (duration > 4h) which is
more robust to sampling artifacts.

Never start a hunt without validating the data source. A hunt that refutes a hypothesis because the data was sampled is worse than not hunting — it creates false confidence.