A BloodHound-style visualizer for packet captures. Drop in a .pcap / .pcapng
file and explore who's talking to whom as an interactive, drill-down node-link
graph — built for rapid triage of a capture and for spotting malicious IP /
domain indicators fast.
Sibling project to RDPGraph (which does the same for Windows RDP event logs).
This started on the threat-hunting/IR side of the house. When the team is handed a packet capture for investigation, the goal is rapid triage — quickly answer "is there anything bad in here, and who touched it?"
Wireshark is superb for deep packet analysis, but it's not built for rapid response: you're reading traffic flow-by-flow, and reconstructing "which internal host talked to which external thing, and is that thing known-bad?" is slow manual work. It's especially weak at the triage questions that matter first:
- Which external IPs / domains did we actually contact?
- Are any of them known-malicious per our CTI (Cyber Threat Intelligence)?
- Which internal host(s) reached the bad indicator, and how?
PCAPGraph exists to answer those in minutes, not hours — turn the capture into a graph you can click through, label destinations with the domains they really are, and check indicators against threat intel (offline blocklists) and VirusTotal right from the node you're looking at.
- Drill-down graph. The canvas starts with your internal hosts only (plus internal↔internal lateral-movement edges). Click a host to expand it into its application/protocol boxes (Web, DNS, SSH, NTP, SMB/RPC, RDP, Push/FCM, ICMP, Unknown, …) and a 🌍 Countries box. Click an app box to reveal the destination hosts for that protocol; click a destination to inspect it.
- Domain labels. Pulls TLS SNI and HTTP Host (and reverse-maps observed
DNS) so destinations read as
www.example.cominstead of a bare IP. - Reverse lookup (🔎). Already have an indicator? Type a malicious IP or domain and PCAPGraph finds every internal host that contacted it (full conversation table, broken down by protocol / port / bytes), and draws the source→destination path in red on the graph.
- Find Malicious IoCs (🛡). One click checks all the capture's public IPs and domains against VirusTotal and highlights the top malicious ones in the graph. (Confirms first — see below.)
- VirusTotal on click. Click any external IP/domain to see its detection count (how many engines flag it malicious/suspicious) inline. Optional API key.
- Geo / country enrichment. Click a public IP for country / ASN / org (via ip-api.com), and group a host's external destinations by country in the Countries box.
- Offline threat-intel matching. Matches IPs / domains / JA3 against local blocklists (abuse.ch Feodo/SSLBL/URLhaus, Tor exits) — fully offline. Matched nodes turn red with a ☠.
- Visual cues, always on. Threat-intel hits = red ☠, beaconing destinations = ⏱ amber (with a beacon-timeline sparkline in the detail panel), lateral-movement edges = amber. Click a node to spotlight it (everything else dims). Port numbers are shown on the edges and destination rows.
- Layout + export. Force or Tree (L→R) layout; ⬇ Export dumps every host → application → destination as CSV.
Note on the Findings panel: the older rule-based "Findings" list is currently disabled while it's being redesigned (the previous heuristics produced too many false positives on normal traffic). The detection engine still exists in the code and the graph still color-codes threat-intel / beaconing / lateral movement — see the Roadmap.
PCAPGraph is a Python 3.8+ app. Create a virtual environment, install the dependencies, run the server, and open it in a browser.
Linux / macOS:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python app.py
# open http://127.0.0.1:5000Windows (PowerShell):
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r requirements.txt
python app.py
# open http://127.0.0.1:5000Then drag-and-drop (or click to choose) a .pcap / .pcapng / .cap file.
Run deactivate to leave the virtual environment when you're done.
- Drop the capture in. The graph shows your internal hosts.
- Click a host → see what it did, by protocol. Open 🌍 Countries to see where its traffic went geographically; open Web/DNS/… to see the actual destinations.
- Hit 🛡 Find Malicious IoCs to scan everything against VirusTotal and light up the worst offenders — or, if you already have an indicator from CTI, paste it into 🔎 reverse to see who in the environment touched it.
- Click any suspicious destination for VirusTotal detections, country/ASN, and the exact ports / volumes involved.
The graph is progressive drill-down, so even a busy capture stays readable:
- Top level: internal hosts only, plus internal ↔ internal edges (lateral movement / general visibility, drawn amber when over SMB/RDP/WinRM/etc.).
- Host → applications: clicking a host spawns one box per application/protocol it used, classified by port + TLS SNI / HTTP Host / JA3.
- Application → destinations: clicking an app box reveals its destination hosts, paginated (20 at a time + a "+N more" node) so a chatty protocol doesn't flood the canvas.
- Host → 🌍 Countries → destinations: an alternate grouping of the same external destinations, by country.
Destinations are colored by risk (☠ threat-intel, ⏱ beaconing) and clicking any node opens a detail panel; clicking empty space returns to the capture overview.
The parser doesn't draw packets — it folds both directions of each TCP/UDP session into a single conversation (5-tuple), so memory scales with the number of unique conversations, not the file size.
Click a public IP to see its country, ASN, org, and reverse DNS, sourced from ip-api.com (no API key required). This is gated by the Online lookups toggle in the header (on by default) because it sends the IP to a third party — turn it off if an investigation must stay fully local.
The per-host 🌍 Countries box uses ip-api.com's batch endpoint, so geolocating a host with dozens of destinations is a single fast request. Internal / RFC1918 addresses are never enriched.
Offline MaxMind GeoLite2 and the RDAP / AbuseIPDB lookups are present in the code but disabled for now — ip-api.com is the only enrichment source currently wired up. They can be re-enabled later.
Click an external IP or domain and its panel shows the VirusTotal detection count — engines flagging it malicious/suspicious, total engines, reputation, and a link to the full VirusTotal report.
This needs a (free) VirusTotal API key, but it is completely optional:
- No key configured → the panel shows "VirusTotal API token not added" and no request is ever sent to VirusTotal.
- Key configured → detections are fetched automatically when you click an IP/domain.
To add a key, copy the example config and fill it in (config.ini is gitignored, so
your key is never committed — keep the .example blank):
cp config.ini.example config.ini
# edit config.ini and set api_key under [virustotal][virustotal]
api_key = <your VirusTotal API key>
timeout = 8
scan_max = 40Get a free key at https://www.virustotal.com/gui/my-apikey. The
VIRUSTOTAL_API_KEY environment variable also works and takes precedence over the
file.
⚠ A VirusTotal lookup sends the IP/domain to VirusTotal. Only use it when it's safe to query a third party about your indicators. Results are cached for the process lifetime to respect the free-tier rate limit.
The 🛡 Find Malicious IoCs button checks all of the capture's public IPs and domains against VirusTotal in one pass and highlights the top 5 malicious indicators on the graph (source→app→destination paths turn red), with a side-panel summary of detections and which internal hosts contacted each one.
Because this can fire many API calls, an Attention dialog first tells you exactly
how many indicators will be sent. To respect the free tier it scans at most
scan_max indicators (default 40, configurable); already-checked indicators are
cached, so re-running is fast and fills any rate-limited gaps. On the free tier
(~4 lookups/min) a large scan can take a few minutes — raise scan_max or use a
higher VirusTotal tier for bigger captures.
PCAPGraph matches IPs, domains, and JA3 hashes against local blocklist files — fully offline, nothing queried at match time. Populate the feeds when you have internet:
./download_feeds.sh # abuse.ch Feodo / SSLBL / URLhaus + Tor exit nodes → ./threatintelMatched destinations turn red with a ☠ in the graph and panels. This degrades
gracefully if ./threatintel is empty.
Most settings are environment variables; API keys live in config.ini (see above).
| Variable | Default | Meaning |
|---|---|---|
MAX_UPLOAD_MB |
500 |
Max upload size; larger files are rejected cleanly |
MAX_PACKETS |
5000000 |
Stop after this many packets (reported, never silent) |
PARSE_TIMEOUT |
120 |
Max seconds to spend parsing one capture |
THREATINTEL_DIR |
./threatintel |
Folder holding offline blocklist feed files (*.txt) |
VIRUSTOTAL_API_KEY |
(unset) | VirusTotal key — enables detections (or config.ini) |
VT_SCAN_MAX |
40 |
Max indicators per "Find Malicious IoCs" scan (or config.ini) |
For captures bigger than the limit, pre-slice before uploading:
editcap -c 1000000 big.pcapng chunk.pcapng # split by packet count
tshark -r big.pcapng -Y "ip.addr==10.1.5.0/24" -w focused.pcap # filter to a subnetThe near-term priority is a redesigned, low-false-positive detection surface to replace the disabled Findings panel, tuned for endpoint malware triage. Planned / wanted measures:
Core detections to harden and resurface
- Tunneling — DNS tunneling (long/encoded qnames, heavy TXT, high subdomain cardinality), ICMP tunneling (oversized/steady ICMP payloads), and protocol-on- wrong-port tunneling.
- C2 beaconing — jitter-tolerant periodicity, but with a known-good allowlist (push/telemetry/NTP, major cloud ASNs) so benign services like FCM stop tripping it.
- Data exfiltration — outbound-heavy byte ratios and volume to a single destination, plus DNS- and HTTP-based exfil, scored against destination reputation.
Suggestions worth adding
- CTI / known-good context layer — classify destinations by ASN/org (Google, Cloudflare, Microsoft, Apple, Akamai, AWS, Fastly) to suppress false positives, and pull in more CTI feeds (newly-registered-domain lists, threat feeds, AbuseIPDB).
- Confidence scoring + two tiers — a high-confidence Findings list separate from an Informational list, so analysts see noise without being alarmed by it.
- JA3 / JA3S malware-family matching against fingerprint sets.
- Encrypted-traffic heuristics — self-signed / anomalous certs, SNI↔DNS mismatch, rare TLS parameters.
- Low-and-slow / long-lived connection detection and beacon clustering.
- Timeline / time-slider to replay when contacts happened, and an "expand all flagged paths" button to auto-reveal every route to a malicious node.
- Reverse pivot (external → which internal hosts), GeoIP flags on dest nodes, and IPv6 support.
Some of these (beaconing, DNS/ICMP tunneling, exfil heuristics, lateral movement) already exist in
detect.py; the work is making them accurate and context-aware before surfacing them again.
- Parser: streaming raw-header parse over Scapy's
RawPcapReader(no per-packet dissection) — pure Python, no Wireshark/tshark dependency, fast (tens of thousands of packets/sec). Memory scales with unique conversations, not file size. - Both directions of a TCP/UDP session merge into one conversation, with the well-known/lower port treated as the server side.
- IPv4 only for now — IPv6 is skipped. Multicast / broadcast / unspecified
addresses (e.g. SSDP
239.255.255.250) are filtered out as noise. - Supported link types: Ethernet, Linux cooked (SLL), raw IPv4, and null/loopback.
- The uploaded capture file is parsed then deleted; it is never stored on disk. The parsed conversation table for the most-recent upload is kept in memory (so reverse-lookup can scan every flow without a re-upload); it's replaced on the next upload and gone when the server stops.
- Local tool: runs on
127.0.0.1, single-user, not hardened for multi-user or public deployment.
