A SIEM is very good at telling you things. It is much less good at telling you which of those things matter. After the Wazuh and Suricata setup had been running for a while, the problem was no longer visibility. It was attention: every high-severity alert still needed a human to read it, work out whether it was real and decide what to do.
So I started building a layer that does the first, boring part of that job, and, carefully, some of the last part too. This is a write-up of how it is designed. It is a system in progress, and I will be explicit about which parts are deliberately not switched on.
The pipeline
It begins with a threshold. High-severity Wazuh alerts, level 9 and above, are the ones I want a ticket for, and the severity maps to the ticket’s priority.
- A Wazuh integration forwards the alert to an n8n webhook.
- An n8n workflow opens a Jira ticket in a dedicated threat-hunting project.
- A locally hosted LLM, running in Ollama on a GPU machine at home, writes a first-pass summary and a verdict into the ticket.
- A notification lands in a chat alerts channel.
Running the model locally was a deliberate choice. Alert data describes my own systems, and I would rather it never leave my network just to be summarised.
The summary is a starting point, not a conclusion
Models can hallucinate context. A summary can sound confident and still invent a cause, so the ticket says plainly that the AI text is a starting point. I do the actual triage, using a written playbook. Each ticket gets one of four classifications:
- False positive
- Noise
- Needs investigation
- Vulnerability
The part that makes the system improve is the library of known false positives. For each one I record why it is benign: virtualization artifacts that show up under container process filesystems, unsigned kernel modules from a GPU driver, inter-process files created by a hypervisor cluster. When one recurs, it becomes a tuned Wazuh suppression rule. The alert queue gets quieter, and what remains is more likely to deserve a look.
Council-gated remediation
Vulnerability alerts already carry the CVE and package data, so they flow into a resolve pipeline that asks one question first: is a fix available? If not, the ticket stays open with that finding.
If a fix exists, the interesting question is not “can I patch” but “should this patch be applied unattended?”. Here a council of several models from different providers, a mix of local open-weight models and hosted models, assesses the patch risk against a structured rubric.
How the council’s answers are combined matters more than which models are in it:
- The highest risk verdict from any model wins. Disagreement is treated as a signal, not averaged away.
- If the council cannot be reached, the result is HIGH. It fails safe. It never defaults to “safe”.
Then policy takes over:
- HIGH is never auto-applied. The ticket is flagged with the council’s reasoning for a human to decide.
- LOW or MEDIUM is applied by Ansible, through a GitLab CI pipeline and only through one. The ticket records the before and after versions and is closed, and the decision is posted to a chat thread.
Design principles
Every change is a pipeline run. Nothing runs commands on hosts by hand, so the CI history is the complete audit trail. This is the same rule as in my homelab-as-code setup, applied to patching.
Trust is phased. The path is dry run first, then virtual machines and containers, with hypervisor nodes last. Every patch is preceded by a snapshot, and a failed health check triggers a rollback. Autonomous patching of hypervisors is deliberately not enabled yet.
It is cost-aware. The nightly loop is plain CI compute calling a model proxy, so no expensive frontier-model session sits running overnight.
Assistants stay read-only while I investigate. A custom MCP server for Wazuh lets AI assistants query alerts, agents and CVEs without being able to change anything.
The shortcut that taught me the lesson
An earlier version of this took the obvious fast route: when a critical CVE appeared, patch automatically, with no risk review. It was quick and it was wrong in principle. Severity of a vulnerability says nothing about the risk of the patch, and an update that fixes one problem can break a service, a kernel module or a cluster.
Replacing “fast” with “reviewed by a council and gated by policy” is the core idea of the whole project. The goal was never to remove myself from the loop. It was to make sure the loop only asks for my attention when it should, and that anything done without me was reviewed, conservative and reversible.
Where it stands
I am building this in stages, and I would rather describe it honestly than oversell it. Parts of it are running; the later phases are not switched on. It is not a fully autonomous system, and for hypervisors it is not meant to be yet.
What I find most satisfying is how the pieces reinforce each other. Triage feeds the false-positive library, the library feeds Wazuh rules, quieter alerts make the AI summaries more useful, and the pipeline’s audit trail keeps the whole thing honest.