Attackers rotate C2 IPs. Xynapt follows the TLS fingerprint, not the address.
Drop in a pcap, get back an incident graph — Suricata and Zeek process it internally, JA3/JA4 fingerprints link rotated C2 infrastructure across distinct destination IPs, and Leiden community detection groups related infections into campaign clusters.
- pcap in — upload a capture file. Suricata and Zeek process it internally.
- JA3 anchoring — TLS fingerprints are treated as first-class correlation entities, not blocklist lookups. Two alerts with different destination IPs but the same JA3 hash get linked. This is what connects rotated C2 infrastructure that every other tool logs as separate incidents.
- Leiden community detection — entity graphs are clustered into campaign groups. JA3/JA4 anchor nodes bridge infections sharing the same TLS fingerprint, letting Leiden merge them into a single campaign cluster even when they hit different C2 IPs.
- Incident view — graph + timeline + optional enrichment (VirusTotal, Gemini narrative).
Not a SIEM, not a rule tester (that's Dalton), not an endpoint tool. Network-only.
Requires Docker and Docker Compose. No local Suricata or Zeek install needed.
git clone https://github.com/haekal-alg/xynapt.git
cd xynapt
docker compose upOpen http://localhost. Upload a pcap. Done.
VIRUSTOTAL_API_KEY=your_key GEMINI_API_KEY=your_key docker compose upBoth are optional — the pipeline runs without them. VT adds verdict lookups (cached in SQLite). Gemini generates a plain-English incident narrative.
GEMINI_API_KEY=your_key GEMINI_MODEL=gemini-3.0-pro GEMINI_PROMPT_PATH=/app/config/narrative_prompt.txt docker compose upGEMINI_MODEL— Gemini model id to call (e.g.gemini-2.5-pro,gemini-3.0-pro). Defaults togemini-2.5-flashif unset.GEMINI_PROMPT_PATH— path (inside the container) to a text file containing the narrative prompt template. Must include an{incident_json}placeholder — it's filled in with the slimmed incident data at generation time. Falls back to the built-in prompt if unset or unreadable.
Both are read fresh at enrichment time, so no rebuild is needed — just set the env var and restart the container, or re-trigger enrichment via POST /api/sessions/{session_id}/enrich.
pcap upload
│
▼
Suricata (alerts + JA3 on TLS flows)
Zeek (conn.log, ssl.log, dns.log, http.log — JA3 on ALL TLS, not just alerting flows)
│
▼
Normalize → NormalizedEvent
Dedup + suppression
│
▼
Windowed correlation
- community_id anchor (exact 5-tuple match)
- JA3/JA4 anchor (links events across IP rotation)
- IP/port fallback
│
▼
Leiden community detection → incident clusters
(co-occurrence weighted — common entities like 8.8.8.8 don't super-connect everything)
│
▼
Two-condition emission gate
(rule_confidence ≥ 0.8 AND entity join ≥ 2 events)
│
▼
Enrichment: VirusTotal (SQLite-cached) + Gemini narrative
│
▼
Incident List → Incident Detail (graph + timeline + enrichment)
Every existing tool treats JA3 hashes as indicators — match against a blocklist, get a binary hit/miss. Xynapt treats them as correlation anchors.
Attackers rotate C2 infrastructure (change IPs) trivially. They cannot trivially change their TLS client hello parameters without recompiling the implant. So:
- Two alerts, different destination IPs, same JA3 → same C2 framework, same campaign.
- A Zeek-logged TLS flow (no Suricata rule fired) sharing a JA3 with an alerting flow → surfaces a connection Suricata missed entirely.
Zeek advantage: Suricata only logs JA3 on flows that triggered a rule. Zeek logs JA3 on every TLS connection regardless. This means you can correlate a silent C2 beacon with a known-bad alert purely via fingerprint match.
| Malware | JA3 Hash | Confirmed across |
|---|---|---|
| Latrodectus v2 beacon | 3c293bdf2a25c07559b560ba86debc77 |
5 distinct C2 IPs, single pcap |
| Lumma Stealer (all variants) | 2800f914a7a4ba98aa9df62d316a460c |
8 infections, Jun 2025–Apr 2026 |
| Rsockstun C2 | 8bee49baa010986785a9e74d688ed7e9 |
2 deployments, different C2 IPs |
| GhostSocks/Go Backdoor | 075b102409434354fe8cc2a05af8465e |
2 IPs in single pcap |
Some JA3 hashes are high-prevalence: the same TLS client hello parameters appear in both malware C2 traffic and legitimate services that happen to use the same TLS library. Xynapt avoids false campaign merges by requiring alert corroboration before promoting a destination IP to a Leiden bridge node: a bridge edge (ja3_hash → dest_ip) is only synthesised when at least one Suricata alert with rule_confidence ≥ 0.7 (severity 1 or 2) targets that destination.
This means:
- Destination IPs matched by
ET MALWAREorET EXPLOIT(severity 1, confidence 0.9) → bridge edge created ✓ - Destination IPs matched only by
ET INFOobservations (severity 3, confidence 0.5) → bridge edge skipped ✓
The filter is corroboration-based, not destination-based. t.me and api.ipify.org are intentional malware behaviour (Telegram C2 exfil, victim IP discovery) — they remain visible in the incident graph and are never suppressed as benign. They are only excluded as Leiden bridge anchors when no high-confidence Suricata rule fires on them.
The same corroboration check is exposed on each incident's ja3_anchors/ja4_anchors (corroborated: bool) and enforced again in the frontend campaign map — two separate incidents sharing a JA3 hash are only linked visually when the hash is corroborated on both sides. This prevents the campaign map from re-creating a false merge that the Leiden bridge filter already keeps split at the backend level.
Louvain can produce internally disconnected communities — two unrelated incidents merged into one cluster with no path between them. Leiden guarantees every community is fully connected. For incident grouping, disconnected means wrong.
Co-occurrence weighting is applied so common entities (shared CDN IPs, DNS resolvers) don't super-connect unrelated incidents.
Academic reference: Traag, V.A., Waltman, L. & van Eck, N.J. (2019). From Louvain to Leiden: guaranteeing well-connected communities. Scientific Reports 9, 5233.
| Tool | What it does | Xynapt difference |
|---|---|---|
| RITA | Beaconing detection + JA3 as a blocklist indicator | Xynapt treats JA3/JA4 as graph anchor nodes — two flows sharing a fingerprint get a graph edge, not just a binary hit/miss |
| SELKS | Suricata-based NSM platform | Xynapt produces campaign clusters from pcap evidence, not raw alert dashboards |
| Zeek + Kibana | Log exploration and search | Xynapt emits enriched incident clusters, not raw telemetry to query manually |
| Slips | Behavioral network IDS | Xynapt focuses on pcap-to-campaign reconstruction across multiple infections |
| Security Onion | Full NSM stack (Zeek, Suricata, Elastic, Kibana) | Xynapt is a single upload-and-go tool — no SIEM pipeline, no index management, no query language |
xynapt/
├── backend/
│ ├── mvp/ # Correlation engine (normalizer, pipeline, Leiden, enrichment)
│ ├── api/ # Starlette HTTP service
│ └── tests/ # 1034 pytest tests
├── frontend/ # React 18 + Vite + Mantine UI
├── config/
│ └── benign_chains.json # Suppression rules
├── Dockerfile.backend # Includes Suricata + Zeek + ET Open rules
├── Dockerfile.frontend
└── docker-compose.yml
python -m pytest backend/tests/ -v1034 backend tests + 269 frontend tests. Backend tests need no Suricata or Zeek — pipeline logic runs on synthetic data.
# Frontend
cd frontend && npm testMIT — see LICENSE.