Skip to content

ITSEC-Research/xynapt

Repository files navigation

Xynapt

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.


What it does

  1. pcap in — upload a capture file. Suricata and Zeek process it internally.
  2. 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.
  3. 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.
  4. Incident view — graph + timeline + optional enrichment (VirusTotal, Gemini narrative).

What it is not

Not a SIEM, not a rule tester (that's Dalton), not an endpoint tool. Network-only.


Quickstart

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 up

Open http://localhost. Upload a pcap. Done.

Optional enrichment

VIRUSTOTAL_API_KEY=your_key GEMINI_API_KEY=your_key docker compose up

Both are optional — the pipeline runs without them. VT adds verdict lookups (cached in SQLite). Gemini generates a plain-English incident narrative.

Configuring the Gemini model and prompt

GEMINI_API_KEY=your_key GEMINI_MODEL=gemini-3.0-pro GEMINI_PROMPT_PATH=/app/config/narrative_prompt.txt docker compose up
  • GEMINI_MODEL — Gemini model id to call (e.g. gemini-2.5-pro, gemini-3.0-pro). Defaults to gemini-2.5-flash if 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.


Pipeline

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)

Why JA3 as a correlation anchor

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.

Validated fingerprints

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

Anchor quality — alert corroboration

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 MALWARE or ET EXPLOIT (severity 1, confidence 0.9) → bridge edge created ✓
  • Destination IPs matched only by ET INFO observations (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.


Why Leiden over Louvain

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.


Why not RITA / SELKS / Security Onion

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

Repository layout

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

Running tests

python -m pytest backend/tests/ -v

1034 backend tests + 269 frontend tests. Backend tests need no Suricata or Zeek — pipeline logic runs on synthetic data.

# Frontend
cd frontend && npm test

License

MIT — see LICENSE.

About

Correlation engine that tracks TLS fingerprints to detect rotated C2 addresses, then groups them into campaigns using Leiden.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors