A self-hosted, real-time intrusion detection dashboard for Suricata.
Reads eve.json, streams live alerts, and presents everything in a fast single-page UI with no external dependencies at runtime.
| Category | Details |
|---|---|
| Live streaming | Server-Sent Events push alerts, flows, DNS, and HTTP events to all connected browsers the instant Suricata writes them |
| Alert management | Acknowledge, investigate, or mark as false positive β individually or in bulk. Full audit history per alert |
| Threat Intel | Custom per-SID or per-category explanations written by your team. Coverage gap view shows your top-firing unexplained signatures |
| AI Explain | Auto-generated executive summaries for every unique signature β DeepSeek, OpenAI, Claude, or NVIDIA NIM |
| Suppression rules | Silence known-noisy signatures by SID, source IP, or category β with optional expiry dates |
| Charts | Alert trend, top talkers, severity distribution, category breakdown β across 24h / 7d / 30d / 60d / 90d |
| Flow events | Full Suricata flow records with bytes, packets, duration, app-proto |
| DNS events | Every query and response with answers, TTL, rcode |
| HTTP events | Hostname, URL, method, status code, user-agent per transaction |
| Webhooks | Slack, Discord, or generic JSON β per-severity filtering and per-signature cooldown |
| RBAC | Three roles: admin (full), analyst (read + ack), viewer (stream only) |
| Themes | Night Β· Light Β· Midnight Blue Β· Solarized Dark Β· Dracula Β· Nord |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Suricata (IDS engine) β
β /var/log/suricata/eve.json βββ continuous append β
βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
β inotify-style readline loop
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β tail.py (tail_thread β daemon) β
β β
β parse_eve_line() β
β βββ alert β suppression check β DB insert β SSE broadcast β webhook β
β βββ flow β DB insert β SSE broadcast β
β βββ dns β DNS DB insert β SSE broadcast β
β βββ http β DB insert β SSE broadcast β
β β
β purge_thread (daemon) β hourly retention sweep + WAL checkpoint β
ββββββββββ¬ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββ ββββββββββββββββββββββββ
β events.db β β config.db β
β βββββββββββββ β β βββββββββββββββββ β
β alerts β β users / sessions β
β flows β β webhooks β
β http_events β β threat_intel β
β ack_history β β suppression_rules β
βββββββββββββββββββ ββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββ
β dns.db β β separated: high write-volume DNS events
β βββββββββββββ β never hold events.db WAL writer lock
β dns_events β
βββββββββββββββββββ
tail_thread βββΊ registry.broadcast()
ββββΊ client queue 1 βββΊ browser tab A
ββββΊ client queue 2 βββΊ browser tab B
ββββΊ client queue N βββΊ browser tab N
(max 500 items; dead clients pruned automatically)
tail_thread βββΊ dispatch() βββΊ [severity filter] βββΊ [cooldown check]
β
βΌ
_queue (max 1 000 items)
β
delivery_worker (daemon)
βββ attempt 1 β ok? β done
βββ fail β re-enqueue(retry_after + 5 s)
βββ 3 failures β give up, log error
Browser ββββΊ Python HTTP server (handlers.py β ThreadedHTTPServer)
β
βββ GET / β frontend/index.html (React SPA)
βββ GET /frontend/* β static assets (pre-built by Vite)
βββ GET /events β SSE stream (persistent connection)
β
βββ GET /alerts β alert list, search, pagination
βββ POST /alerts/bulk-ack β bulk acknowledge
βββ POST /alerts/delete-selected
βββ GET /alerts/explain β AI Explain (full build only)
β
βββ GET /flows β flow records
βββ GET /dns β DNS events
βββ GET /http β HTTP events
βββ GET /charts β aggregated chart data
β
βββ GET /health β stats + uptime
βββ GET /me β current user + role
β
βββ GET|POST|PUT|DELETE /users β user management
βββ GET|POST|PUT|DELETE /webhooks β webhook CRUD
βββ GET|POST|PUT|DELETE /suppression β suppression rules
β
βββ GET|POST|PUT|DELETE /threat-intel β TI CRUD
βββ GET /threat-intel/lookup β explain lookup
βββ GET /threat-intel/gaps β coverage gaps
βββ GET /threat-intel/stats β statistics
βββ GET /threat-intel/export β JSON export
βββ POST /threat-intel/import β bulk import
β
βββ GET|POST /settings/explain β AI Explain config
βββ POST /admin/replay β reimport eve.json into DB
βββ POST /admin/flush β wipe all event data
| Thread | Name | Role |
|---|---|---|
| Main | server |
Accepts TCP connections, spawns one thread per request |
| Daemon | tail |
Tails eve.json, inserts events, drives SSE + webhooks |
| Daemon | purge |
Hourly: deletes old rows, checkpoints WAL, runs PRAGMA optimize |
| Daemon | delivery_worker |
Drains webhook queue, retries with back-off (non-blocking) |
| Daemon | explain-{sid} |
Spawned per new SID to call AI provider (full build only) |
./build-deb.sh 1.7.3
β
βββ Step 1: npm run build (frontend-src β frontend/)
β
βββ Step 2: strip-ai.py (full source β AI-free source tree)
β removes: explain.py, LLM routes, AI settings panel
β keeps: Explain button, Threat Intel tab, all data views
β
βββ Step 3: watcher-ids_1.7.3_all.deb (full β AI Explain included)
βββ Step 4: watcher-ids_1.7.3-noai_all.deb (AI-free β smaller footprint)
βββ Step 5: watcher-ids-src_1.7.3.zip (source archive)
No Node.js on the server. The frontend is compiled once at build time and shipped as plain JS/CSS. The Python server serves static files only.
watcher-ids/
βββ backend/ Python server β all source files
β βββ server.py Entry point, argument parsing, wires all components
β βββ handlers.py HTTP routing and all API endpoints
β βββ tail.py eve.json tail + replay + suppression check
β βββ database.py AlertDB β alerts, flows, http, ack history
β βββ database_dns.py DnsDB β high-volume DNS events
β βββ config_db.py ConfigDB β SQLite wrapper for config tables
β βββ auth.py Session management
β βββ users.py RBAC user management
β βββ webhooks.py Webhook engine β delivery queue, Slack/Discord/generic
β βββ explain.py AI Explain engine (full build only)
β βββ threat_intel.py Threat Intel database
β βββ suppression.py Suppression rules β in-memory cached engine (30 s TTL)
β βββ registry.py SSE client fan-out
β βββ password_utils.py PBKDF2-SHA256 hashing
β βββ migrate.py One-time DB migration tool
β βββ config.py Runtime constants and default paths
β
βββ frontend-src/ React source β edit this, then `npm run build`
β βββ src/
β β βββ main.jsx Entry point
β β βββ App.jsx Root component β all state and SSE wiring
β β βββ Detail.jsx Alert detail panel (Details / History / Raw JSON)
β β βββ Charts.jsx All SVG chart components
β β βββ FlowsDns.jsx Flow and DNS/HTTP views
β β βββ Settings.jsx Users, Webhooks, AI Explain settings
β β βββ ThreatIntel.jsx Explain dialog + Threat Intel panel
β β βββ Suppression.jsx Suppression rules panel
β β βββ components.jsx Shared: Clock, Sparkline, Timeline, AckBadge
β β βββ themes.jsx Theme definitions and ThemePicker
β β βββ utils.js fmtTime, fmtBytes, fmtDur, constants
β β βββ styles.css All CSS (theme vars + layout + components)
β βββ public/
β β βββ login.html Login page
β β βββ login.js Login page logic
β βββ index.html Vite entry HTML
β βββ vite.config.js Vite config (base: /frontend/, dev proxy β :8765)
β βββ package.json
β
βββ packaging/ .deb packaging support files
β βββ postinst Runs after install: create user, systemd, seed admin
β βββ prerm Runs before remove: stop service
β βββ postrm Runs after purge: clean up data directories
β βββ watcher.service systemd unit with security hardening
β βββ watcher.conf Default config file (/etc/watcher/watcher.conf)
β
βββ .github/workflows/
β βββ build.yml GitHub Actions: build both .deb variants on tag push
β
βββ build-deb.sh Dual-build script β produces full .deb, noai .deb, source .zip
βββ strip-ai.py Strips LLM engine from source tree to produce AI-free variant
βββ README.md
Download the latest .deb from the Releases page:
sudo apt install ./watcher-ids_1.7.3_all.debThat's it. The installer:
- Creates a locked-down
watchersystem user - Adds
watcherto thesuricatagroup (eve.json read access) - Starts
watcher.servicevia systemd - Seeds the admin account on first install (password printed to the install banner)
Retrieve your credentials:
journalctl -u watcher | grep -A5 "First-run credentials"Open the dashboard: http://your-server:8765/
Prerequisites: Node.js 18+, dpkg-deb
git clone https://github.com/yourname/watcher-ids.git
cd watcher-ids
./build-deb.sh 1.7.3
sudo apt install ./packaging/build/watcher-ids_1.7.3_all.debThe build script compiles the frontend with Vite, strips the AI engine for the noai variant, assembles both package trees, and calls dpkg-deb. Three artifacts are produced per run:
| Artifact | Description |
|---|---|
watcher-ids_1.7.3_all.deb |
Full build β includes AI Explain (DeepSeek / OpenAI / Claude / NVIDIA) |
watcher-ids_1.7.3-noai_all.deb |
AI-free build β LLM engine removed, Threat Intel and Explain button kept |
watcher-ids-src_1.7.3.zip |
Source archive for distribution |
# Clone and install frontend deps once
git clone https://github.com/yourname/watcher-ids.git
cd watcher-ids
cd frontend-src && npm install && npm run build && cd ..
# Run the server (from the backend directory)
cd backend
python3 server.py
# With options
python3 server.py --eve /var/log/suricata/eve.json --port 8765 --retain-days 90Edit /etc/watcher/watcher.conf (preserved across upgrades):
# Uncomment and customise ONE WATCHER_ARGS line
WATCHER_ARGS=--eve /var/log/suricata/eve.json --port 8765 --retain-days 30
systemctl restart watcher| Flag | Default | Description |
|---|---|---|
--eve |
/var/log/suricata/eve.json |
Path to Suricata eve.json |
--port |
8765 |
TCP port to listen on |
--host |
0.0.0.0 |
Bind address |
--retain-days |
90 |
Days to keep events in SQLite |
--db |
/var/lib/watcher/events.db |
Events database path |
--dns-db |
/var/lib/watcher/dns.db |
DNS database path |
--config-db |
/var/lib/watcher/config.db |
Config database path |
--password |
β | Set/change admin password, then exit |
| Permission | Admin | Analyst | Viewer |
|---|---|---|---|
| View alerts / flows / DNS / HTTP / charts | β | β | β |
| Alert detail panel | β | β | β |
| Acknowledge / bulk-ack alerts | β | β | β |
| Explain (Threat Intel lookup) | β | β | β |
| Add / edit Threat Intel | β | β | β |
| Delete Threat Intel entries | β | β | β |
| Clear alerts / flows / DNS | β | β | β |
| Manage webhooks | β | β | β |
| Manage suppression rules | β | β | β |
| Manage users | β | β | β |
The Explain button appears in the alert toolbar whenever an alert is selected.
Clicking it opens a dialog showing your team's saved explanation for that signature.
Explanations can be scoped to:
- Exact SID β applies only to one specific Suricata signature (highest priority)
- Category β applies to all alerts of that category (fallback)
Each entry supports free-text explanation, tags, and reference URLs.
Manage entries at Settings β Threat Intel. The Coverage Gaps tab shows your most-fired signatures that have no explanation yet, sorted by fire count.
The full build includes an auto-explain engine that generates an executive summary the first time each unique signature ID fires. Summaries are cached in the database and never re-fetched.
Supported providers: DeepSeek, OpenAI, Claude (Anthropic), NVIDIA NIM.
Configure at Settings β AI Explain or via watcher.conf. The noai build (-noai deb) has the LLM engine removed entirely β the Explain button and Threat Intel panel remain fully functional.
Suppression silences alerts before they are stored or broadcast. Rules match on any combination of:
sig_idβ exact Suricata signature IDsrc_ipβ exact source IP addresscategoryβ alert category (case-insensitive)
All specified conditions must match (AND logic). Rules can have an optional expiry date β expired rules are kept for audit purposes but no longer applied.
Rules are cached in memory and refreshed from the database every 30 seconds, so changes take effect quickly without a restart.
Manage at Settings β Suppression (admin only).
Watcher supports Slack, Discord, and Generic JSON webhooks.
Each webhook has its own severity filter and a 60-second per-signature cooldown to prevent alert storms.
Deliveries are asynchronous β a background worker drains the queue with non-blocking retry (up to 3 attempts, 5-second back-off). A failed or slow endpoint never stalls other webhooks.
Test any webhook from the Settings panel without waiting for a real alert.
sudo apt install ./watcher-ids_1.7.3_all.debdpkg stops the running service, replaces files, restarts. Databases survive untouched. /etc/watcher/watcher.conf is preserved as a dpkg conffile.
sudo apt remove watcher-ids # removes files, keeps databases and config
sudo apt purge watcher-ids # removes everything including /var/lib/watcher# Terminal 1 β Python backend
cd backend && python3 server.py
# Terminal 2 β Vite dev server
cd frontend-src && npm run devOpen http://localhost:5173/ β Vite proxies all API calls to port 8765.
Note: the session cookie is scoped to port 8765, so log in at http://localhost:8765/ once before switching to the Vite URL.
Changes to any .jsx or .css file appear in the browser instantly.
When satisfied, build for production:
cd frontend-src && npm run buildPushing a tag triggers an automatic build and GitHub Release:
git tag v1.7.3
git push origin v1.7.3The workflow installs Node, builds the frontend, assembles both .deb variants (full + noai), and attaches them to the release. No secrets needed β only the default GITHUB_TOKEN.
Server (runtime)
- Debian / Ubuntu (any recent release)
- Python 3.10 or later (standard library only β no pip installs)
- Suricata writing
eve.json
Build machine (one-time, not needed on server)
- Node.js 18+ and npm (to compile the frontend)
dpkg-deb(pre-installed on Debian/Ubuntu)
AGPL-3.0 β see LICENSE.
- S-03 Β· Alert ID collision under high traffic β Alert IDs are now constructed as
{flow_id}-{epoch_ms}-{4-byte-hex}. The 32-bit entropy suffix makes same-millisecond collisions on the same flow statistically impossible, preventing the silentINSERT OR IGNOREdrops that could occur at high event rates or during replay.
- S-04 Β· Replay must not fire live webhooks β
replay_eve()no longer accepts awdbparameter and never calls the webhook dispatcher. Importing 90 days ofeve.jsonhistory no longer floods Slack / Discord / Teams endpoints with stale notifications or triggers provider rate-limit bans.
- P-01 Β· Webhook config DB query on every alert β
dispatch()now callswdb.get_cached()instead ofwdb.get_all(). The webhook list is held in memory with a 30-second TTL and invalidated immediately on anycreate/update/delete. On a busy sensor (1 000 alerts/s) this eliminates ~999 redundantSELECTqueries per second. - P-04 Β· Single delivery worker blocking on retry sleep β The webhook delivery worker no longer calls
time.sleep(RETRY_DELAY)inside its loop. Failed deliveries are re-enqueued with aretry_aftertimestamp; the worker picks up the next ready item and only yields for 100 ms when all pending items are in their back-off window. Two simultaneously-down webhook endpoints no longer stack their 15-second stalls.
- Fix:
0 found in DBbadge rendered as a large 200 px box (CSS class-name collision resolved) - All search-status badges now identical in size:
Searchingβ¦/N found in DB/0 found in DB
- Full-database alert search by SID, IP, or signature text
- Search queries the whole DB β not just loaded rows
- Debounced search (400 ms) with
N found in DBresult count - Load-more support for search result pagination
- β clear button in search input
dst_ipindex for faster destination searches
- AI Explain β executive summaries on every alert (DeepSeek / OpenAI / Claude / NVIDIA NIM)
- Auto-generate summary on each new unique signature ID
- Settings β AI Explain: enable, pick provider, manage API keys via UI or
watcher.conf - Fix: webhook Test now respects Allow Local IPs setting
- Fix: stale SSRF-blocked error cleared when Local IPs enabled
- Fully air-gapped β zero external font/CDN dependencies
- Full-database alert search β queries entire retention window, not just loaded rows
- Search across
sig_id(index-backed),src_ip,dst_ip,sig_msg,category idx_a_dst_ipindex added for destination IP search performance- Admin Data Control panel: Replay (reimport
eve.jsonwithout firing webhooks) and Flush (wipe all event data) - Replay is async β returns immediately, pollable via
GET /admin/replay
- Dual-build system: single
./build-deb.shrun produces full.deb, noai.deb, and source.zip strip-ai.pyβ new tool that surgically removes the LLM engine from the source tree- AI-free variant keeps the Explain button, ExplainDialog, and Threat Intel tab fully functional
build-deb.shrewritten: Step 1 builds frontend, Step 2 strips AI, Steps 3β5 package and archive
- NVIDIA NIM added as fourth AI provider (
deepseek-ai/deepseek-v4-proviaintegrate.api.nvidia.com) - Uses existing OpenAI-compatible
_call_openai_compat()path β no new network code NVIDIA_API_KEYenv var support + Settings UI cardwatcher.confupdated with NVIDIA key comment and link tobuild.nvidia.com
- Backend: 3 new SQLite PRAGMAs on
events.dbanddns.db(cache, mmap, temp_store) - Backend: 2 new indexes β
idx_a_sig_id(GROUP BY),idx_a_ack(bulk-ack filter) - Backend:
fetch_recent()stripsraw_jsonby default; lazyfetch_raw(id)for Detail Raw tab - Backend:
registry.pyβjson.dumps()moved outside lock; snapshot-then-iterate pattern - Backend:
dispatchimport moved to module level β no per-alert attribute lookup - Backend:
_stats_cacheβstats()result cached 5 s; avoids 7 DB queries per/healthpoll - Backend:
GET /alerts/<id>/rawendpoint for single-alert raw JSON - Frontend:
alertIdsRefSet replaces O(n)prev.some()scan β O(1) dedup per SSE event - Frontend:
aiEnabledfetched once on mount and passed as prop - Frontend: Raw tab lazy-fetches JSON only on open
- Packaging:
LICENSEfile bundled inside.deb;AGPL-3.0-or-laterinDEBIAN/control
- Multi-provider AI: OpenAI (
gpt-4o-mini) and Anthropic Claude Haiku added alongside DeepSeek - Global enable/disable toggle β when off, no API calls made and AI tab hidden
- Per-provider key storage, masked hint display, and key-management links in Settings
watcher.confupdated withOPENAI_API_KEYandANTHROPIC_API_KEYcomments
- Auto-explain: every new unique
sig_idtriggers a background thread that pre-fetches the summary - Prompt rewritten to strict 3-sentence executive summary;
MAX_TOKENSreduced 700 β 220 _explained_sidsset prevents duplicate API calls within a session- License changed from MIT to AGPL-3.0-or-later; all backend files updated with SPDX headers
- AI-powered alert explanations via DeepSeek β on-demand, cached by
sig_id explain.pyβ newExplainDB(SQLite cache) +ExplainEngine(stdliburllib)POST /alerts/explain,GET/PUT /settings/explainAPI endpoints- ExplainDialog in UI: AI Explanation tab + Threat Intel tab; Cached/Fresh badge; Regenerate button
- Settings β AI Explain tab: key status, save/clear, How It Works card
- Self-hosted fonts: Google Fonts CDN replaced with bundled
woff2files (ibm-plex-mono/sans, inter, jetbrains-mono) - Install-time credential bootstrap:
postinstseedsconfig.dbwith PBKDF2-SHA256 admin hash before service start; password printed in install banner