On this page
The problem
The goal was a practical project that combines live SIEM telemetry with AI-assisted alert triage.
The original idea was to build something involving “AI agents in cybersecurity.” I deliberately scoped it down into something realistic, functional, and explainable: a Python SOC analyst assistant that retrieves real Wazuh alerts, extracts the important fields, sends a summary to OpenAI, and produces a SOC-style triage report.
The specific challenge: how do you take raw security event data from a live SIEM, extract the meaningful fields, and produce something that reads like a trained analyst’s assessment, automatically and on demand?
The secondary challenge was infrastructure. Before any code could run, the lab environment had to be fully operational, and that took real troubleshooting.
Environment
| Analyst workstation | Windows PC, 172.22.2.119, with the Python project folder |
|---|---|
| SIEM server | Wazuh 4.7.2 on Rocky Linux, hostname wazuh1, 172.22.3.49 |
| Virtualization | Xen Orchestra (XOA) managing the XCP-ng virtual machines |
| Endpoints | 8 Wazuh agents (6 active, 2 disconnected) reporting Windows and Linux telemetry |
| Alert store | Wazuh Indexer (OpenSearch) on port 9200, index pattern wazuh-alerts-* |
| Wazuh API | Port 55000 (agents, manager status, rules, RBAC) |
| AI | OpenAI API (gpt-4o-mini) for the triage analysis |
The Security Events dashboard showed real activity: about 64 alerts, including 13 authentication failures and 11 authentication successes, with MITRE ATT&CK techniques such as Valid Accounts, Password Guessing, Sudo and Sudo Caching, and Brute Force. That meant the project could use real telemetry instead of mock data.
The build, phase by phase
Phase 1Infrastructure discovery & recovery
Start from a sticky note
The only starting information was a sticky note with internal IP addresses, including the Wazuh server at 172.22.3.49 and the Xen Orchestra appliance. It wasn’t clear whether these were physical servers, VMs, powered off, or stale. The first task wasn’t AI development; it was infrastructure discovery.
Every connection attempt to the Wazuh server failed: ping, SSH, and browser access all returned unreachable errors. Instead of assuming the server was broken, I worked layer by layer.
Trace the fault down to NetworkManager
- Checked Xen Orchestra. The Wazuh VM existed but was powered off. I started it, and connections still failed: powered on is not the same as reachable.
- Opened the VM console. Logged in to Rocky Linux as
shamar(hostnamewazuh1) and confirmed full sudo rights. - Ran
ip addr. Theeth0interface existed but had no IPv4 address. - Ran
ip route. The routing table was completely empty. - Checked NetworkManager.
nmcli device statusshowed eth0 as disconnected; a saved profile namedeth0existed but wasn’t active. - Inspected the profile.
nmcli connection show eth0 | grep ipv4revealed a valid static config (172.22.3.49/22, gateway 172.22.0.1). The configuration was correct, just inactive.
Rocky Linux, NetworkManager# Activate the existing connection profile sudo nmcli connection up eth0 Connection successfully activated # Verify the IP assignment ip addr show eth0 inet 172.22.3.49/22 brd 172.22.3.255 scope global eth0 # Set autoconnect so it survives reboots sudo nmcli connection modify eth0 connection.autoconnect yesWith the network restored, the Wazuh dashboard loaded at
https://172.22.3.49. I wrote the full investigation up separately: Restoring an Unreachable Linux Server.
Phase 2Working with real telemetry
Export a real alert and study its structure
I opened a real alert in the dashboard: a Windows logon success event (Event ID 4624, rule level 3, user SYSTEM in the NT AUTHORITY domain, logon type 5, process
services.exe, MITRE T1078 Valid Accounts). The JSON was long, but the important lesson was that Wazuh alert data is structured and can be parsed programmatically.
Phase 3Manual JSON triage (proof of concept)
Parse the alert and ask OpenAI for a triage
I created the project at
C:\Users\shamar\ai-wazuh-triagewith a Python virtual environment, installedopenaiandpython-dotenv, and kept the API key in a.envfile. The first script,triage_alert.py, loaded the exported alert, extracted key fields, sent a structured summary to OpenAI, printed the analysis, and saved a Markdown report.triage_alert.py (core workflow)# Load alert with open("alert.json", "r") as f: alert = json.load(f) # Extract key fields summary = { "agent": alert.get("agent", {}).get("name"), "rule_desc": alert["rule"]["description"], "level": alert["rule"]["level"], "mitre": alert.get("rule", {}).get("mitre", {}) } # Send to OpenAI response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "You are a SOC analyst..."}, {"role": "user", "content": json.dumps(summary)} ] )The AI correctly read the Windows event: Event ID 4624 with logon type 5 for SYSTEM via
services.exeis a likely benign service logon, mapped to T1078 Valid Accounts. Reports were saved toreports/report_YYYYMMDD_HHMMSS.mdwith an extracted alert summary and the AI analysis, which turned a terminal demo into something that produced artifacts.The remaining gapIt worked, but a human still had to export or copy the alert JSON by hand. The next goal was to remove that step.
Phase 4Automated retrieval
Find the right data source
The desired behavior was run-on-demand, not a background service: run the script, pull the latest alert, triage it, save the report, exit. My first attempt used the Wazuh API on port 55000. A test request returned Unauthorized: No authorization token provided, which was actually good news: the API was reachable and responding over HTTPS. The problem was authentication.
The authentication rabbit hole RBAC database deep dive
Credentials I tried (including
shamar,wazuh, andwazuh-wui) kept returning Invalid credentials. The dashboard’s config file listed awazuh-wuiuser that authenticated fine locally (the API log showed successful logins from 127.0.0.1), but the same credentials failed from the Windows PC with a 401.So I inspected the Wazuh RBAC database with SQLite (
/var/ossec/api/configuration/security/rbac.db). The users table showed the accounts with administrator-level roles, and the passwords were stored as PBKDF2 hashes, so the plaintext couldn’t be recovered. That narrowed the problem: the accounts had the right authorization, so the failure was authentication, meaning the password itself was wrong.The fix was the installer-generated credentials file at
/root/wazuh-install-files/wazuh-passwords.txt. Using thewazuhAPI user from that file returned a JWT token, which confirmed API authentication worked.The architectural correctionThe Wazuh API on port 55000 is for agents, manager status, rules, and RBAC. The alert documents shown in the dashboard actually live in the Wazuh Indexer on port 9200, under
wazuh-alerts-*. For pulling the latest alert, the Indexer is the right data source.Indexer query, newest alert firstquery = { "size": 1, "sort": [{"@timestamp": {"order": "desc"}}], "query": {"match_all": {}} } response = requests.post( f"{INDEXER_URL}/wazuh-alerts-*/_search", auth=(INDEXER_USER, INDEXER_PASS), json=query, verify=False # self-signed certificate in the lab )Expose the Indexer to the lab network
The first query from Windows timed out on port 9200 while port 55000 worked. On the server,
sudo ss -lntp | grep 9200showed the Indexer listening only on 127.0.0.1. It wasn’t a firewall problem: the service wasn’t listening on the LAN interface at all./etc/wazuh-indexer/opensearch.yml# Was bound to localhost only. Listen on all interfaces (trusted lab only): network.host: 0.0.0.0 sudo systemctl restart wazuh-indexerThe Indexer also needed its own credentials, separate from the API’s: the
adminuser from the same installer-generated file. The final.envheld the OpenAI key plus the Indexer URL, username, and password.Security notePort 9200 exposes the Wazuh Indexer (OpenSearch) API. Binding it to
0.0.0.0is only appropriate inside a trusted lab network, never directly on the internet.Make the parser work for Windows and Linux alerts
The first parser was built around a Windows alert (
data.win.system.eventID,channel,targetUserName,logonType,processName). When the script pulled a Linux PAM alert, those fields came back asNone, which made sense because Linux alerts don’t have Windows Event Log fields.I extended it to also extract general Wazuh fields:
location,decoder.name,predecoder.program_name,full_log, rule ID, description, and level, agent name, IP, and ID, and the MITRE ID, technique, and tactic. More context made the AI triage better for both platforms.
Challenges & fixes
Problem
The Wazuh VM was unreachable. Ping, SSH, and browser access all failed.
Fix
The VM was powered off. Started it in Xen Orchestra, then activated the inactive NetworkManager profile with nmcli connection up eth0.
Problem
The network fix didn’t survive a reboot. Wazuh became unreachable again after restart.
Fix
Set connection.autoconnect yes on the eth0 profile so it activates automatically at boot.
Problem
PowerShell blocked venv activation: running scripts is disabled on this system.
Fix
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass, which changes the policy only for the current PowerShell window.
Problem
OpenAI returned RateLimitError: insufficient_quota right after the first request.
Fix
ChatGPT Plus and the OpenAI API are billed separately. Added API credits and it resolved immediately.
Problem
ModuleNotFoundError: No module named 'requests', even though it was installed.
Fix
It was installed in one PowerShell window but the script ran in another, outside the venv. Activated the venv in the same window that runs the script.
Problem
Wazuh API credentials on port 55000 kept returning 401 despite multiple attempts.
Fix
Found the installer-generated credentials in /root/wazuh-install-files/wazuh-passwords.txt. The dashboard config credentials weren’t valid for direct API authentication from the script.
Problem
The Wazuh API was the wrong place to pull alert documents from.
Fix
Switched the script to the Wazuh Indexer on port 9200 and queried wazuh-alerts-* for the newest alert by @timestamp.
Problem
Port 9200 timed out from Windows. The Indexer was bound only to localhost.
Fix
Confirmed with ss -lntp | grep 9200 (127.0.0.1:9200), set network.host: 0.0.0.0 in opensearch.yml, and restarted the service.
Problem
API credentials were being mixed up with Indexer credentials.
Fix
Port 55000 (Wazuh API) uses the API user; port 9200 (Wazuh Indexer) uses the Indexer admin user. The final script uses the Indexer credentials.
Results
The final tool runs with one command and performs the whole triage loop:
PS> python .\triage_latest_wazuh_alert.py
Connecting to Wazuh Indexer...
Latest alert pulled successfully.
=== Extracted Wazuh Alert Summary ===
agent_name: wazuh1
rule_description: PAM: Login session opened.
rule_level: 3
mitre_id: ['T1078']
mitre_technique: ['Valid Accounts']
=== AI SOC Triage ===
1. Summary
2. What the event means
3. Suspicious or likely benign
... report and raw alert saved to reports/- Connects to the Wazuh Indexer and retrieves the latest alert from
wazuh-alerts-* - Extracts agent name, rule description, severity level, and MITRE ATT&CK IDs and techniques
- Handles Windows alerts (Event ID, logon type, process name) and Linux alerts (PAM, SSH, syslog)
- Sends a structured summary to OpenAI and receives an analyst-style triage
- Saves the triage report as Markdown and the raw alert as JSON to
reports/
For a PAM login session alert (rule level 3, MITRE T1078 Valid Accounts), the AI classified the event as likely benign, explained the process context, noted why the MITRE technique was relevant, and recommended next verification steps.
Security hygiene
Several real credentials were displayed during troubleshooting. In a real environment, I’d follow through on these:
- Rotate the Wazuh API password
- Rotate the Wazuh Indexer admin password
- Keep secrets out of screenshots
- Keep
.envout of Git with.gitignore - Restrict port 9200 to trusted hosts only
- Never expose the Indexer to the internet
.env
venv/
reports/raw_alert_*.json
__pycache__/Skills demonstrated
Planned next steps
- Add a
--limit Noption to triage the latest N alerts in one run - Filter by severity so only alerts above a rule-level threshold (for example
rule.level >= 7) are analyzed - Track analyzed alert IDs locally to avoid duplicate triage
- Separate parsing for Windows authentication, Linux PAM, SSH failures, sudo, file integrity, and vulnerability events
- A simple Streamlit or Flask dashboard to browse alerts and view saved reports
- Email or Slack notifications for high-severity findings, and optional scheduled runs