jtshor14899/threat-dash

GitHub: jtshor14899/threat-dash

Stars: 0 | Forks: 0

# threat-dash [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Docker](https://img.shields.io/badge/docker-ready-2496ed?logo=docker)](./Dockerfile) [![Python](https://img.shields.io/badge/python-3.12-3776ab?logo=python&logoColor=white)](./Dockerfile) [![stdlib only](https://img.shields.io/badge/python%20deps-stdlib%20%2B%202-success)](./Dockerfile) [![Issues](https://img.shields.io/github/issues/jtshor14899/threat-dash)](https://github.com/jtshor14899/threat-dash/issues) [![Stars](https://img.shields.io/github/stars/jtshor14899/threat-dash?style=social)](https://github.com/jtshor14899/threat-dash/stargazers)

threat-dash hero
(Run scripts/seed-demo-data.py to populate a copy with realistic synthetic data — see SCREENSHOTS.md.)

## Table of contents - [What it does](#what-it-does) - [Screenshots](#screenshots) - [How it compares](#how-it-compares) - [Quick start](#quick-start) - [Block backends](#block-backends) - [Endpoint visibility (Wazuh)](#endpoint-visibility-wazuh) - [Deployment patterns](#deployment-patterns) - [Architecture](#architecture) - [Configuration](#configuration) - [API providers — what's free, what's not](#api-providers--whats-free-whats-not) - [REST + SSE endpoints](#rest--sse-endpoints) - [Performance](#performance) - [Security](#security) - [Roadmap](#roadmap) - [Contributing](#contributing) - [License](#license) ## What it does Threat-dash watches four things and stitches them into a single live view: 1. **Inbound** — what hits your Cloudflare zone. Every WAF event, every blocked / challenged / allowed request, every Cloudflare Access login attempt, GeoIP-enriched and projected onto a 3D globe with animated arcs pointing at your homelab. 2. **Outbound** — what your containers are calling out to. A sidecar reads `/proc/net/tcp` from every container's netns, deduplicates, and feeds destinations into the same map (different color, different direction). 3. **Intelligence** — every outbound destination is auto-enriched against GreyNoise, Censys, Shodan, and (on demand) VirusTotal. The 🛰 Intel page surfaces full host profiles: open services, software stack, TLS certs, SSH banners, WHOIS, DNS, CVEs, threat-score rollup. 4. **Endpoints** — agent inventory, vulnerability findings, and FIM events from a **Wazuh** SIEM/XDR manager. Per-agent CVE drilldown, bucketed by severity; status KPIs across the fleet. See [docs/WAZUH.md](docs/WAZUH.md). You also get: - **Click-to-block** at the Cloudflare edge *or* on a **pfSense** firewall (alias-based, no rule per IP — see [docs/PFSENSE.md](docs/PFSENSE.md)). - **Region attack surface** — pick a region (`country:US state:CA`, `country:DE`, `asn:13335`, anything Shodan-filterable) and see how many hosts expose SSH / RDP / SMB / Redis / Mongo / Kibana / Docker API / Modbus / MQTT / Elasticsearch / VNC / etc. Click any row to drill into facet breakdowns — top organizations, ASNs, products, versions, OSes, cities, CVEs — all on Shodan's **free `oss` tier**. - **Per-container traffic attribution** — which container talked to which IP, when, on what port. - **Drawer-style nav** — horizontal tab strip under the header; clicking a tab opens a slide-out drawer on the right with that section's content. The globe stays full-bleed in the main view until you ask for detail. - **Light & dark themes**, **resizable drawer**, **keyboard shortcuts** (`1`-`6` tabs, `T` theme, `R` refresh, `Esc` close drawer, `F` live feed). - **Stdlib-only Python server** — `http.server` + `sqlite3` + `urllib` + `maxminddb` + `cryptography`. No Flask, no SQLAlchemy, no Celery. - **Vanilla-JS frontend** — globe.gl + three.js for the 3D view. ~150 KB client total. No build step, no node_modules. ## Screenshots All shots below are from a copy seeded with `scripts/seed-demo-data.py` — real Cloudflare / Censys / GreyNoise / Shodan / VT credentials are not required to reproduce them. See [SCREENSHOTS.md](docs/SCREENSHOTS.md) for the capture recipe.
Overview tab — globe centered on the contiguous US with top-attacker country breakdown
Overview — animated globe + top-attacker country list, opened in the right-side drawer
Traffic tab — per-container outbound destinations
Traffic — per-container outbound destinations, ranked
Blocks tab — active rules from both Cloudflare and pfSense backends
Blocks — Cloudflare and pfSense rules in one list, backend-badged
Endpoints tab — Wazuh agent inventory with KPI strip and per-agent drilldown
Endpoints — Wazuh agent fleet, status KPIs, CVE drilldown NEW
Outbound Intel page — enriched destination IPs
Outbound Intel — GreyNoise / Censys / Shodan / VT enrichment
Settings UI showing Cloudflare, pfSense, and Wazuh configuration sections
Settings — every credential & toggle (Cloudflare, Shodan, Censys, GreyNoise, pfSense, Wazuh) editable at runtime, no restart. Env vars win and are marked locked.
## How it compares | Tool | What it does | What threat-dash adds | | --- | --- | --- | | [CrowdSec](https://crowdsec.net/) | Behaviour-based IPS, community feeds | Globe + outbound visibility + click-to-block at the CF edge instead of iptables | | Cloudflare dashboard | The data itself | One pane covering inbound + outbound + intel, with a live feed and per-container attribution | | [Shodan](https://shodan.io/) | Internet-scale scan data | We *consume* Shodan; we don't replace it. Combined with your own outbound list, it's per-destination intel | | [WatchYourLAN](https://github.com/aceberg/WatchYourLAN) | LAN device discovery | Different scope — WYL is L2 inventory, threat-dash is L7 / external | | [Grafana + Cloudflare Loki](https://grafana.com/) | DIY dashboarding | Threat-dash is the dashboard. No PromQL/LogQL to write | ## Quick start # 1. clone git clone https://github.com/jtshor14899/threat-dash.git cd threat-dash # 2. fill in API tokens (Cloudflare required for inbound data; # Censys / GreyNoise / VT / Shodan optional but make the Intel page sing) cp .env.example .env $EDITOR .env # 3. fetch the free GeoIP databases (~50 MB total, monthly refresh) ./scripts/setup-geoip.sh # 4. up docker compose up -d --build # 5. (optional) seed with realistic synthetic data so the UI is interesting # before any real Cloudflare events trickle in: docker compose exec threat-dash python /scripts/seed-demo-data.py The dashboard is now on `http://127.0.0.1:5959/`. **Don't expose 5959 publicly** — front with one of the deployment patterns below. ## Block backends | Backend | Where the rule lives | Modes | Setup | | ------------- | ------------------------ | ---------------------------------- | ----- | | **Cloudflare**| Zone IP Access Rules | block, challenge (3 flavors), allow| Default — uses your existing `CF_DASH_TOKEN`. | | **pfSense** | Host alias on your pfSense| block, allow | [docs/PFSENSE.md](docs/PFSENSE.md) — requires the `pfSense-pkg-RESTAPI` package. | Backends are independent — you can run with one, the other, or both wired up simultaneously. The Blocks tab dropdown lets you pick per rule. ## Endpoint visibility (Wazuh) Threat-dash plugs into a **Wazuh** SIEM/XDR manager to surface agent inventory, vulnerability findings, and file-integrity events in a dedicated **Endpoints** tab. Strictly read-only — block decisions still go through Cloudflare or pfSense; Wazuh is for visibility, not enforcement. | Backend | Surface | What you get | Setup | | -------- | --------------- | ------------------------------------------------------------ | ----- | | **Wazuh**| Manager REST API| agent inventory, status KPIs, per-agent CVE drilldown, FIM | [docs/WAZUH.md](docs/WAZUH.md) — works with any Wazuh 4.x manager (single-node `wazuh-docker` is the quickest path). | A bundled in-memory mock means you can try the Endpoints tab without standing up a real Wazuh: `docker compose -f docker-compose.yml -f docker-compose.mock.yml up -d`. ## Deployment patterns Self-contained recipes for the homelab stacks people actually run. Pick one; all live under [`examples/`](./examples). | Pattern | When to use | TLS | Auth | Public? | | --- | --- | --- | --- | --- | | **[LAN only](./examples/lan-only-no-proxy/)** | First try / trusted LAN+VPN | ❌ | none | no | | **[Tailscale sidecar](./examples/tailscale-sidecar/)** | You already use Tailscale | ✓ | tailnet ACLs | no (yes via Funnel) | | **[Traefik](./examples/traefik/)** | Already on Traefik (the *arr/Jellyfin stack) | ✓ Let's Encrypt | forward-auth | optional | | **[Caddy](./examples/caddy/)** | Already on Caddy | ✓ Let's Encrypt | forward-auth | optional | | **[Nginx Proxy Manager](./examples/nginx-proxy-manager/)** | You like the NPM GUI | ✓ Let's Encrypt | NPM access lists | optional | | **[Cloudflare Tunnel](./examples/cloudflare-tunnel/)** | Public access without port-forwarding | ✓ CF edge | Cloudflare Access SSO | **yes** ✓ | | **[Authelia / Authentik](./examples/authelia-forward-auth/)** | You run your own SSO | depends on proxy | TOTP/WebAuthn | optional | **Recommended for most homelabs**: `tailscale-sidecar` (quickest) or `cloudflare-tunnel + cloudflare-access` (most polished, public-safe). ## Architecture ┌──── Cloudflare edge ────┐ ┌──── Wazuh manager ────┐ │ GraphQL Analytics │ │ REST API :55000 │ │ Firewall events │ │ agents / vulns / FIM │ │ Access audit log │ │ JWT auth, cached │ └────────────┬────────────┘ └──────────┬─────────────┘ │ │ ▼ 60s poll ▼ on-demand ┌──────────────────────────────────────┐ ┌──────────────────┐ │ collector.py (background thread) │ │ wazuh_client.py │ │ + outbound_collector.py (sidecar) │ │ (per-request) │ └────────────────────┬─────────────────┘ └────────┬─────────┘ ▼ │ ┌──────────────────────────────────────┐ │ │ GeoIP / ASN (DB-IP Lite mmdb, free) │ │ │ Auto-enrich (GreyNoise + Censys) │ ← 5/tick │ └────────────────────┬─────────────────┘ │ ▼ │ ┌─── SQLite (WAL, /data) ────┐ │ │ events, blocks, recon │ │ │ vt_cache, scans, settings │ │ └──────────────┬─────────────┘ │ ▼ │ ┌──────────────────────────────────────────────────┴─────┐ │ server.py — stdlib http.server │ │ REST + Server-Sent Events on :5959 │ └─────┬───────────────────────┬───────────────────────────┘ ▼ ▼ ┌────────────────────┐ ┌──────────────────────┐ │ Cloudflare API │ │ pfSense REST API v2 │ │ IP Access Rules │ │ host-alias mutate │ │ (per-IP rules) │ │ + apply │ └────────────────────┘ └──────────────────────┘ ▲ ▲ │ click-to-block │ └───── from any IP row ─┘ your reverse proxy ▼ browser (globe.gl + drawer UI) ## Configuration Two ways to configure threat-dash — pick whichever fits your workflow: 1. **Via the Settings UI** (⚙ in the header, or press `,`). Edit API tokens, homelab branding, self-detection, and outbound rules from the dashboard. Changes apply on the next API call — no container restart. Secrets are **masked on the wire** (only last 4 chars ever leave the server). Per-provider "Test" buttons verify each token at the source. 2. **Via `.env`** (classic). Env vars always win over UI-set values — the Settings page shows them as "locked from env". Existing deployments are unaffected. See [`.env.example`](./.env.example) for the full list. Highlights: | Variable | Required? | Notes | | --- | --- | --- | | `CF_DASH_TOKEN`, `CF_ZONE_ID`, `CF_ACCOUNT_ID` | for inbound | Cloudflare API. Scopes documented in `.env.example`. | | `HOMELAB_LAT`, `HOMELAB_LON`, `HOMELAB_LABEL` | recommended | Globe pin location + brand text in the header | | `VT_API_KEY` | optional | VirusTotal v3 free tier (500 lookups/day) | | `SHODAN_API_KEY` | optional | Free `oss` tier — region surface + facet drill | | `CENSYS_TOKEN` | optional | Censys Platform PAT (per-host enrichment, free) | | `CENSYS_API_ID` + `CENSYS_API_SECRET` | optional | Censys Search v2 (250 queries/month, free) | | `GREYNOISE_API_KEY` | optional | Community works keyless; auth bumps rate limit | | `SELF_EMAILS`, `SELF_IPS`, `SELF_ASN_PREFIXES` | optional | Hide your own traffic | | `OUTBOUND_EXTRA_RANGES` | optional | Comma-separated CIDRs to treat as internal | | `DEMO_MODE` | optional | `1` disables all live polling — for screenshots | ## API providers — what's free, what's not | Provider | Free | Paid required for | | --- | --- | --- | | **VirusTotal** | 500 v3 lookups/day | Higher quota, hunting | | **Censys Platform** | per-host enrichment (PAT) | Search, aggregate | | **Censys Search v2** | 250 search queries/mo | Bulk results, higher quota | | **GreyNoise** | community classify (50/day keyless) | RIOT context, hunt | | **Shodan** | `/host/count` + facets (`oss` plan) | `/host/`, `/host/search` | Anything you don't have a key for is hidden in the UI with a tooltip explaining why. Threat-dash always degrades gracefully — you can run with just Cloudflare + DB-IP and still get a useful dashboard. ## REST + SSE endpoints The UI is a thin client over a small REST API: GET /api/stats?range=24h&self=hide&direction=in|out GET /api/arcs?range=24h&limit=400 GET /api/events?range=24h&limit=200 GET /api/intel/outbound?range=24h&limit=250 GET /api/signins?range=24h GET /api/containers?container=&range=24h GET /api/blocks POST /api/block {ip, mode?, reason?, expires_at?} DEL /api/blocks/ PATCH /api/blocks/ {mode?, reason?, expires_at?} GET /api/recon/status GET /api/shodan/surface?base= GET /api/shodan/drill?base=&category= GET /api/shodan/host/ GET /api/censys/host/ GET /api/recon/search?q=&per_page=25 GET /api/greynoise/ GET /api/vt/{ip,domain,hash,url}/ POST /api/scan {ip, profile=common|extended, force?} GET /stream Server-Sent Events GET /healthz 200 ok (used by HEALTHCHECK) Useful for scripting threat-dash into bigger SOAR-y workflows — e.g. block a list of IPs from a Slack slash-command. ## Performance The frontend is opinionated about not melting your browser: - SSE events are batched on a 200 ms rAF tick (no per-event re-renders). - Globe redraws throttled to ~3 fps regardless of feed velocity. - `renderStats` short-circuits when the data signature is unchanged. - Periodic refresh slows to 2 min when `document.hidden`. - All list updates use `DocumentFragment` + `replaceChildren`. The Python server: - One process, threaded `HTTPServer`. SSE clients each get a bounded `queue.Queue` so a slow consumer can't back up the broadcast. - SQLite WAL with a single `threading.Lock` for writes; reads are unsynchronised. - Outbound enrichment is rate-limited to 5 IPs/tick (every 60 s) so the free-tier API quotas can't be blown by a sudden burst of new destinations. On a modest VM (2 vCPU / 2 GB) with ~60 k events/day, RSS sits around 80 MB. ## Security Threat-dash is a security tool. It deserves a high bar. - **Never expose port 5959 publicly without auth in front.** There is no user model. Anyone with the URL can block IPs at your Cloudflare edge. - Runs as **non-root** (UID 1000) in a multi-stage image with all Linux capabilities dropped and `no-new-privileges:true`. - `HEALTHCHECK` on `/healthz` so orchestrators notice when it's wedged. - Outbound TCP scanner is **operator-triggered**, connect+banner only — no exploitation paths. Confirm scanning is OK in your jurisdiction. - `.env` is git-ignored; rotate the Cloudflare token quarterly. Full threat model + hardening checklist in [`docs/SECURITY-HARDENING.md`](./docs/SECURITY-HARDENING.md). Reporting a vulnerability: [`SECURITY.md`](./SECURITY.md). ## Roadmap - [ ] Built-in basic-auth / proxy-header auth for users not fronting with their own proxy - [ ] Saved query bundles ("weekly region attack-surface diff") - [ ] ntfy / Slack / Discord webhook on threat-score thresholds - [ ] Cross-reference outbound CVEs with Trivy SBOM scans of your images - [ ] Image published to GHCR for pull-not-build deploys - [ ] More providers: AbuseIPDB, AlienVault OTX, IPinfo Lite Open an issue if you want to vote one up or propose a different one. ## License [MIT](./LICENSE). The runtime DB-IP Lite databases are CC-BY 4.0 by [db-ip.com](https://db-ip.com). Built for homelab operators by a homelab operator. If this saved you from getting credential-stuffed by a botnet, [⭐ star the repo][stars] — that's the only marketing budget we have.