tshark Scripting
tshark becomes a forensics automation platform when combined with bash pipelines, Python subprocess wrappers, and jq for JSON parsing. This chapter builds reusable scripts for bulk PCAP analysis, automated triage reports, and programmatic investigation pipelines that process directories of captures without human intervention.
You've received 23 PCAP files from an incident — ring buffer files spanning 72 hours. You need to: process all 23 files uniformly, extract IOCs from each, detect any beaconing patterns, identify the first time each suspicious IP was seen (to establish initial compromise time), and produce a summary report. Doing this manually in Wireshark would take hours. A Python script wrapping tshark processes all 23 files in parallel and produces the report in 12 minutes.
Bash Pipeline Patterns
PCAP="merged.pcap"
# ── TOP EXTERNAL DESTINATIONS ─────────────────────────────────────────
# Extract dst IPs, filter external, count frequency
tshark -r $PCAP -n \
-Y "not ip.dst in {10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16}" \
-T fields -e ip.dst 2>/dev/null | \
sort | uniq -c | sort -rn | head -20
# ── DNS QUERY FREQUENCY ───────────────────────────────────────────────
# Find domains queried more than 50 times (beaconing/DGA activity)
tshark -r $PCAP -n \
-Y "dns.flags.response==0" \
-T fields -e dns.qry.name 2>/dev/null | \
sort | uniq -c | sort -rn | awk '$1 > 50 {print $0}' | head -30
# ── FIRST SEEN TIMESTAMPS ─────────────────────────────────────────────
# For each unique external IP, find the earliest packet timestamp
# This establishes the dwell time start point
tshark -r $PCAP -n \
-Y "not ip.dst in {10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16}" \
-T fields -e frame.time_epoch -e ip.dst 2>/dev/null | \
sort -k2,2 -k1,1n | \ # sort by IP, then by time
awk '!seen[$2]++ {print $1, $2}' | \ # first occurrence per IP
sort -k1,1n | \ # sort by timestamp
awk '{cmd="date -d @"$1" +%Y-%m-%dT%H:%M:%SZ"; cmd | getline ts; close(cmd); print ts, $2}' | \
head -20
# ── HTTP USER AGENT ANOMALIES ─────────────────────────────────────────
# Find unusual user agents (not Chrome, Firefox, Edge, Safari)
tshark -r $PCAP -n \
-Y "http.user_agent" \
-T fields -e http.user_agent 2>/dev/null | \
sort | uniq -c | sort -rn | \
grep -v "Chrome\|Firefox\|Safari\|Edge\|MSIE 11\|Trident" | head -20
# ── LARGE DNS RESPONSES (tunneling candidate) ─────────────────────────
tshark -r $PCAP -n \
-Y "dns and frame.len > 200" \
-T fields -e frame.time_epoch -e ip.src -e dns.qry.name -e frame.len 2>/dev/null | \
sort -k4 -rn | head -20
Python subprocess Wrapper
#!/usr/bin/env python3
"""
Automated PCAP processor — runs tshark on all pcap files in a directory,
extracts IOCs, and produces a consolidated triage report.
Usage: python3 pcap-processor.py /path/to/pcaps/ /output/dir/
"""
import subprocess, sys, os, json, glob
from pathlib import Path
from collections import defaultdict
from datetime import datetime
PCAP_DIR = Path(sys.argv[1])
OUT_DIR = Path(sys.argv[2])
OUT_DIR.mkdir(parents=True, exist_ok=True)
PRIVATE_NETS = "not ip.dst in {10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8}"
def run_tshark(pcap: str, display_filter: str, fields: list[str]) -> list[list[str]]:
"""Run tshark and return list of [field1, field2, ...] rows."""
cmd = [
"tshark", "-r", pcap, "-n",
"-Y", display_filter,
"-T", "fields",
"-E", "separator=\t",
]
for field in fields:
cmd += ["-e", field]
result = subprocess.run(cmd, capture_output=True, text=True)
rows = []
for line in result.stdout.splitlines():
if line.strip():
rows.append(line.split("\t"))
return rows
all_ips = defaultdict(set) # ip → set of pcap files it appeared in
all_domains = defaultdict(int) # domain → count across all files
all_user_agents = defaultdict(int)
timeline = []
pcap_files = sorted(glob.glob(str(PCAP_DIR / "*.pcap")) + glob.glob(str(PCAP_DIR / "*.pcapng")))
print(f"Processing {len(pcap_files)} PCAP files...")
for pcap in pcap_files:
fname = os.path.basename(pcap)
print(f" → {fname}")
# Extract external destination IPs
rows = run_tshark(pcap, PRIVATE_NETS, ["frame.time_epoch", "ip.dst"])
for row in rows:
if len(row) >= 2:
ts, ip = row[0], row[1]
all_ips[ip].add(fname)
timeline.append({"ts": float(ts), "ip": ip, "file": fname})
# Extract DNS queries
rows = run_tshark(pcap, "dns.flags.response==0", ["dns.qry.name"])
for row in rows:
if row and row[0]:
all_domains[row[0]] += 1
# Extract User-Agents
rows = run_tshark(pcap, "http.user_agent", ["http.user_agent"])
for row in rows:
if row and row[0]:
all_user_agents[row[0]] += 1
# Sort timeline and find first-seen per IP
timeline.sort(key=lambda x: x["ts"])
first_seen = {}
for entry in timeline:
ip = entry["ip"]
if ip not in first_seen:
first_seen[ip] = {
"ip": ip,
"first_ts": datetime.utcfromtimestamp(entry["ts"]).strftime("%Y-%m-%dT%H:%M:%SZ"),
"file": entry["file"],
"pcap_count": len(all_ips[ip])
}
# Build report
report = {
"total_pcap_files": len(pcap_files),
"total_external_ips": len(all_ips),
"top_ips_by_file_count": sorted(
[{"ip": ip, "file_count": len(files), "files": sorted(files)}
for ip, files in all_ips.items()],
key=lambda x: -x["file_count"]
)[:20],
"first_seen_ips": sorted(first_seen.values(), key=lambda x: x["first_ts"])[:20],
"top_domains": sorted(
[{"domain": d, "count": c} for d, c in all_domains.items()],
key=lambda x: -x["count"]
)[:30],
"unusual_user_agents": [
{"ua": ua, "count": c} for ua, c in sorted(all_user_agents.items(), key=lambda x: -x[1])
if not any(k in ua for k in ["Chrome", "Firefox", "Safari", "Edge", "MSIE 11"])
][:15]
}
report_file = OUT_DIR / "triage_report.json"
with open(report_file, "w") as f:
json.dump(report, f, indent=2)
print(f"\nReport written: {report_file}")
print(f"Unique external IPs: {report['total_external_ips']}")
print(f"Unique domains: {len(all_domains)}")
print(f"Unusual user agents: {len(report['unusual_user_agents'])}")
Q & A
Q: My Python subprocess tshark call hangs indefinitely on a corrupted PCAP. How do I handle this?
Always add a timeout to subprocess.run when wrapping tshark: subprocess.run(cmd, capture_output=True, text=True, timeout=300) — a 5-minute timeout is generous for any single PCAP under a few GB. Catch the TimeoutExpired exception and log the file as "processing timed out" before moving on. Also handle the case where tshark exits with a non-zero return code (corrupted PCAP, unsupported format, truncated file): check result.returncode and log the stderr output (in result.stderr) which typically contains tshark's error message about why it failed. For a batch of 20+ PCAPs, 1-2 files will often have issues — robust error handling ensures the rest of the batch completes successfully.