AI-assisted alert triage for Wazuh.
Every alert your Wazuh manager produces, summarized in plain language — what happened, why it matters, what to do — with a verdict, right inside the dashboard you already use.
Status: alpha. Built and validated end-to-end on a real Wazuh 4.9.2 single-node install (manager + indexer + dashboard, TLS and auth on, real connected agents). It has not been tested on multi-node clusters, Wazuh 5.x, or non-Debian distributions. Run it in a lab first.
- What it does
- What it does not do
- Screenshots
- How it works
- Requirements
- Install
- Verify it's working
- Using it
- Configuration
- Cost
- Uninstall
- Security
- Building from source
- Repo layout
- Known limitations
- Contributing
- License
- Summarizes every new alert. Plain-language "what happened / why it matters / suggested
action", plus a verdict (
true_positive,false_positive,needs_review), a confidence score, key indicators, and the model's reasoning. - Lives in the dashboard you already use. An AI Summary page with severity tiles, a filterable list, a time-range picker, a detail flyout, token/cost usage, and a link back to the raw alert in Discover.
- Prioritizes by severity. Critical alerts are summarized before high, high before medium. A burst of routine noise can never delay a critical alert sitting behind it in the queue.
- Never slows down Wazuh. The summarizer is a separate OS process with a bounded queue and real backpressure. If it dies, or your provider goes down, normal Wazuh alerting is completely unaffected.
- Falls back to a local model. Configure a cloud provider plus a local one (Ollama, vLLM, anything OpenAI-compatible) and it drops to local when the cloud provider hits a quota or goes unavailable.
- Answers questions about your data. A Chat tab with 29 read-only tools across findings, vulnerabilities, FIM, SCA, MITRE, PCI DSS and agent inventory. No mutating tools exist.
- No historical backfill. Only alerts arriving after the service starts are summarized. This is deliberate — otherwise your first install would send every old, already-handled alert to a paid API.
- No redaction. Alert content, including
full_log, reaches your LLM provider as-is. See Security. - No automated response. It reads and summarizes. It never restarts agents, blocks IPs, or changes Wazuh configuration.
- It is not a detection engine. Verdicts are triage assistance. An analyst still decides.
Wazuh manager ai-summarizer (systemd) Wazuh Indexer
┌──────────────┐ tails file ┌────────────────────┐ writes ┌────────────────────┐
│ alerts.json │ ──────────────>│ tail → parse → │──────────────>│ ai-summaries-* │
│ (real alerts)│ │ severity queue → │ (dedicated, │ (never touches │
└──────────────┘ │ LLM call → write │ least-priv │ wazuh-alerts-*) │
└─────────┬──────────┘ account) └────────────────────┘
│ ^
calls out to │ reads (as the
┌─────────▼──────────┐ │ logged-in user)
│ OpenAI / Gemini / │ ┌─────────┴──────────┐
│ local (Ollama…) │ │ wazuh-ai-assistant │
└────────────────────┘ │ dashboard plugin │
^ │ (AI Summary page) │
│ polls prompt config │ │
└──────────────────────────┤ │
└────────────────────┘
Two things worth understanding:
- The summarizer never touches
wazuh-alerts-*. It reads the manager's own log file and only ever writes to its ownai-summaries-*index, using a dedicated account with no access to anything else. - The plugin and the summarizer barely talk. The one exception: when you edit a prompt on the Settings tab, the summarizer picks it up within ~30s over a small authenticated polling route.
| Wazuh | 4.x, single node, standard /var/ossec layout (manager + indexer + dashboard on one host) |
| OS | Debian or Ubuntu. The installer refuses to run elsewhere rather than failing halfway |
| Access | root / sudo on that host |
| Provider | An OpenAI or Gemini API key, or a reachable OpenAI-compatible endpoint (Ollama, vLLM, LiteLLM) |
| Network | Outbound HTTPS to your provider, unless running fully local |
Wazuh 5.x is not supported — its dashboard platform and React version differ, so the pre-built plugin here won't load. See Building from source.
git clone https://github.com/uckix/threatlens.git
cd threatlens
sudo ./install.sh --provider geminiIt prompts for your API key with the input hidden. For automation, use a key file or the environment instead:
sudo ./install.sh --provider openai --api-key-file ./key.txt
AI_SUMMARIZER_API_KEY=sk-... sudo -E ./install.sh --provider openai --non-interactiveThe installer will not accept a key as a command-line argument. Anything in argv is visible in
psto every local user and lands in your shell history.
That single command:
- Refuses immediately if this isn't Debian/Ubuntu, isn't Wazuh 4.x, or the Indexer isn't up.
- Verifies the shipped plugin zip against
dist/SHA256SUMS. - Verifies your API key with a real call before touching anything else.
- Installs Node.js if needed and builds the summarizer.
- Creates a least-privilege Indexer account (
ai_summarizer) with write access toai-summaries-*only — never the admin cert, neverwazuh-alerts-*. - Installs the summarizer as a sandboxed systemd service with its own unprivileged account,
secrets in a
600file, and log rotation. - Backs up
opensearch_dashboards.yml, installs the dashboard plugin, and restarts the dashboard — rolling both back automatically if it fails to come up.
If anything fails partway, the script prints exactly what was already created and how to undo it. Every step is safe to re-run.
Useful flags:
--api-key-file <path> read the key from a file instead of prompting
--model <id> default: gpt-4o-mini (openai) / gemini-flash-lite-latest (gemini)
--skip-plugin install only the backend service, leave the dashboard alone
--rotate-token on reinstall, rewrite the existing dashboard token block
--yes don't ask before restarting wazuh-dashboard
--non-interactive fail instead of prompting
--wazuh-home <path> default: /var/ossec
--indexer-certs <path> default: /etc/wazuh-indexer/certs
--dashboard-config <p> default: /etc/wazuh-dashboard/opensearch_dashboards.yml
Run ./install.sh --help for the full list.
If the dashboard already has a ThreatLens config block, the installer stops and asks before
touching it. A fresh token would silently break prompt sync with the existing one, so pass
--rotate-token to keep the two in step, or run ./uninstall.sh first for a clean slate.
systemctl status ai-summarizer
tail -f /var/log/ai-summarizer/ai-summarizer.logTo check the Indexer directly:
curl --cacert /etc/wazuh-indexer/certs/root-ca.pem \
--cert /etc/wazuh-indexer/certs/admin.pem \
--key /etc/wazuh-indexer/certs/admin-key.pem \
"https://127.0.0.1:9200/ai-summaries-*/_search?pretty&sort=@timestamp:desc&size=5"Nothing showing up? It only processes alerts that arrive after the service starts. Trigger something — a failed SSH login is enough — and give it a minute.
Open your Wazuh dashboard and find AI Summary under Home.
- Overview — severity breakdown, token usage and estimated cost, pending and failed counts, and the summary list. Click any row for the full detail flyout.
- Chat — ask open-ended questions about your alerts and agents. Needs its own provider configured under Settings first; it uses a separate encrypted credential store, independent of the summarizer's key.
- Settings — customize the prompt, per severity if you want. Changes reach the summarizer within ~30 seconds, no restart needed. Also has a health check for the summarizer, the Indexer account and your provider — start there when something looks wrong.
Upgrading an existing install? A hard reload isn't always enough for cached JS. Use a private window or clear site data.
Runtime config lives in /etc/ai-summarizer/summarizer.env (mode 600). Edit and
systemctl restart ai-summarizer to apply.
Rotating the API key:
sudo vi /etc/ai-summarizer/summarizer.env # edit AI_SUMMARIZER_API_KEY
sudo systemctl restart ai-summarizerThe installer sets up OpenAI or Gemini. To point at a local OpenAI-compatible endpoint, edit the env file:
AI_SUMMARIZER_PROVIDER=openai
AI_SUMMARIZER_BASE_URL=http://127.0.0.1:11434/v1
AI_SUMMARIZER_MODEL=llama3.1:8b
AI_SUMMARIZER_API_KEY=ollamaThis is the configuration to use if alert content must not leave your network.
Define multiple providers and the service falls back to a local one when a cloud provider hits a quota or goes unavailable:
AI_SUMMARIZER_PRESET_IDS=gemini,ollama-local
AI_SUMMARIZER_ACTIVE_PRESET=gemini
AI_SUMMARIZER_PRESET_GEMINI_TYPE=gemini
AI_SUMMARIZER_PRESET_GEMINI_API_KEY=AIza...
AI_SUMMARIZER_PRESET_GEMINI_MODEL=gemini-flash-lite-latest
AI_SUMMARIZER_PRESET_OLLAMA_LOCAL_TYPE=openai
AI_SUMMARIZER_PRESET_OLLAMA_LOCAL_BASE_URL=http://127.0.0.1:11434/v1
AI_SUMMARIZER_PRESET_OLLAMA_LOCAL_MODEL=llama3.1:8b
AI_SUMMARIZER_PRESET_OLLAMA_LOCAL_API_KEY=ollamaA provider whose baseUrl points at localhost is classified local automatically, and
failover only ever lands on a local preset. Override with
AI_SUMMARIZER_PRESET_<ID>_KIND=local|cloud.
Every alert is one LLM call. A manager producing 5,000 alerts a day makes 5,000 calls a day.
Before pointing this at anything busy:
- Check your volume first:
wc -l /var/ossec/logs/alerts/alerts.json - Start with a cheap model (
gemini-flash-lite-latest,gpt-4o-mini) or a local one - Tune your Wazuh rules first — this is not a substitute for filtering out noise
- Watch the token counters on the Overview page for the first day
There is currently no built-in spend cap. Set a budget limit on the provider's side.
sudo ./uninstall.sh # keeps existing ai-summaries-* data
sudo ./uninstall.sh --purge-data # also deletes itRemoves the service, its account, the Indexer role and user, the dashboard plugin, and the config block it added.
Read SECURITY.md before deploying anywhere that matters. The short version:
- Alert content leaves your network. Full
full_loggoes to your provider with no redaction. Run a local model if that's a problem. - Prompt injection is a real surface. Someone who can write to a log can try to influence a verdict. Treat verdicts as suggestions, never as authority.
- Least privilege by default. Own unprivileged account, Indexer user scoped to
ai-summaries-*, sandboxed systemd unit, no access to the admin cert orwazuh-alerts-*. - The plugin zip is checksum-verified at install time and in CI.
Found a vulnerability? Please open a private advisory rather than a public issue — see SECURITY.md.
The summarizer builds normally:
cd ai-summarizer
npm install && npm run build && npm testSet AI_SUMMARIZER_PROVIDER=mock to exercise the whole pipeline — tailing, rotation handling,
parsing, idempotency, retry, Indexer writes — with no API key and no cost.
The dashboard plugin ships pre-built as dist/wazuhAiAssistant-2.13.0.zip, because building it
requires a full OpenSearch Dashboards platform checkout — a large, fragile process that hit a
missing upstream source file, an OSD version mismatch, and a React 18-vs-16 incompatibility
(which shipped a blank white page before being caught) during development. None of that belongs
in an install script running on an arbitrary server. install.sh checks the target's actual
bundled React version first and refuses rather than risk repeating that bug.
To build it yourself:
- Get a
wazuh-dashboardcheckout matching your target version. - Use the Node version pinned by that checkout's
.nvmrc. - Copy
wazuh-ai-assistant/intowazuh-dashboard/plugins/wazuh_ai_assistant. - From the
wazuh-dashboardroot:yarn && yarn plugin-helpers build. - Regenerate the checksum:
cd dist && sha256sum wazuhAiAssistant-*.zip > SHA256SUMS.
The source in wazuh-ai-assistant/ is the source the shipped zip is built from, targeting the
same platform. scripts/check-artifact-drift.sh verifies that every source module has a
counterpart in the artifact, and CI runs it on every push.
One known drift: the shipped zip was built before the
summarizer-statusroute existed, and predates the AI Environment panel that reads it. Neither half is in the prebuilt artifact -- not the server route, not the UI -- so the Settings tab renders without that panel rather than showing an empty or failing one. Nothing errors. The panel appears once the plugin is rebuilt from current source. Everything else works. Rebuild per the steps above, or use--skip-pluginand build it yourself if you'd rather not install a binary at all.
Release steps, including how to keep the artifact in step, are in docs/RELEASE.md.
ai-summarizer/ Node/TypeScript background service (tail → queue → LLM → Indexer)
wazuh-ai-assistant/ OpenSearch Dashboards plugin source (AI Summary page, Settings, Chat)
dist/ Pre-built plugin zip + SHA256SUMS
docs/assets/ Logo and screenshots
install.sh One-shot installer for Wazuh 4.x on Debian/Ubuntu
uninstall.sh Removes everything install.sh created
deploy-ai-summary.sh Redeploys code to an already-installed host (dev convenience)
ai-summarizer.service systemd unit
- Wazuh 4.x only; 5.x needs the plugin rebuilt.
- Single-node installs only; multi-node clusters untested.
- Debian/Ubuntu only — the installer refuses elsewhere. RPM support wanted, see CONTRIBUTING.md.
- No historical backfill.
- No redaction before content reaches the provider.
- No spend cap.
- The shipped plugin zip predates the health-check route; rebuild it to enable that panel (see Building from source).
Testing this somewhere that isn't the author's homelab is the single most useful thing you can do right now. See CONTRIBUTING.md.
GPL-2.0, matching Wazuh's own dashboard plugins, which this is designed to sit alongside.
Built by @uckix


