CybersecuritySIEM & SOC Operations7 min read

AI-Powered SOC Alert Triage

A Python SOC analyst assistant that retrieves live security alerts from a Wazuh SIEM indexer, extracts the fields an analyst cares about, including MITRE ATT&CK context, and generates an analyst-style triage report with OpenAI. Before any code could run, I had to bring the SIEM server itself back online.

Role
Designed, built, troubleshot, and documented end to end
Year
2026
Environment
Wazuh 4.7.2 on Rocky Linux, Xen Orchestra, Windows workstation
Status
Functional, run on demand

Final architecture: one command runs the whole pipeline on demandpython triage_latest_wazuh_alert.py

Summary

The challenge

Turn raw security events from a live SIEM into a structured, analyst-style triage report, automatically and on demand. Real telemetry, not simulated data. At the start, the SIEM server wasn’t even reachable.

What I did

Restored the Wazuh VM’s networking, reviewed real alerts, built a Python triage script against an exported alert, then removed the manual step by querying the Wazuh Indexer directly. Along the way I solved nine infrastructure and integration problems.

The outcome

One command pulls the newest alert from wazuh-alerts-*, parses Windows and Linux fields with MITRE context, and saves a Markdown triage report plus the raw alert JSON. No manual export.

Skills

  • Wazuh SIEM
  • Alert triage
  • MITRE ATT&CK
  • Python
  • REST APIs
  • OpenAI API
  • Linux administration
  • NetworkManager
  • Root cause analysis
On this page
  1. The problem
  2. Environment
  3. The build, phase by phase
    1. Phase 1: Infrastructure discovery & recovery
    2. Phase 2: Working with real telemetry
    3. Phase 3: Manual JSON triage (proof of concept)
    4. Phase 4: Automated retrieval
  4. Challenges & fixes
  5. Results
  6. Security hygiene
  7. Skills demonstrated
  8. Planned next steps

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 workstationWindows PC, 172.22.2.119, with the Python project folder
SIEM serverWazuh 4.7.2 on Rocky Linux, hostname wazuh1, 172.22.3.49
VirtualizationXen Orchestra (XOA) managing the XCP-ng virtual machines
Endpoints8 Wazuh agents (6 active, 2 disconnected) reporting Windows and Linux telemetry
Alert storeWazuh Indexer (OpenSearch) on port 9200, index pattern wazuh-alerts-*
Wazuh APIPort 55000 (agents, manager status, rules, RBAC)
AIOpenAI 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

  1. 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.

  2. Trace the fault down to NetworkManager

    1. 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.
    2. Opened the VM console. Logged in to Rocky Linux as shamar (hostname wazuh1) and confirmed full sudo rights.
    3. Ran ip addr. The eth0 interface existed but had no IPv4 address.
    4. Ran ip route. The routing table was completely empty.
    5. Checked NetworkManager. nmcli device status showed eth0 as disconnected; a saved profile named eth0 existed but wasn’t active.
    6. Inspected the profile. nmcli connection show eth0 | grep ipv4 revealed 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 yes

    With 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

  1. 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)

  1. Parse the alert and ask OpenAI for a triage

    I created the project at C:\Users\shamar\ai-wazuh-triage with a Python virtual environment, installed openai and python-dotenv, and kept the API key in a .env file. 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.exe is a likely benign service logon, mapped to T1078 Valid Accounts. Reports were saved to reports/report_YYYYMMDD_HHMMSS.md with an extracted alert summary and the AI analysis, which turned a terminal demo into something that produced artifacts.

    The remaining gap

    It 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

  1. 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, and wazuh-wui) kept returning Invalid credentials. The dashboard’s config file listed a wazuh-wui user 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 the wazuh API user from that file returned a JWT token, which confirmed API authentication worked.

    The architectural correction

    The 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 first
    query = {
        "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
    )
  2. 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 9200 showed 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-indexer

    The Indexer also needed its own credentials, separate from the API’s: the admin user from the same installer-generated file. The final .env held the OpenAI key plus the Indexer URL, username, and password.

    Security note

    Port 9200 exposes the Wazuh Indexer (OpenSearch) API. Binding it to 0.0.0.0 is only appropriate inside a trusted lab network, never directly on the internet.

  3. 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 as None, 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:

Example run of triage_latest_wazuh_alert.py
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/
Example output

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 .env out of Git with .gitignore
  • Restrict port 9200 to trusted hosts only
  • Never expose the Indexer to the internet
.gitignore
.env
venv/
reports/raw_alert_*.json
__pycache__/

Skills demonstrated

Linux administrationNetworkManager / nmcliIP addressing & routingVirtualization (Xen Orchestra)SIEM conceptsWazuh API & IndexerBind addresses & service portsSQLite / RBACPythonPython venvREST API integrationJSON parsingOpenAI APIAlert triageMITRE ATT&CKCredential managementMethodical troubleshooting

Planned next steps

  • Add a --limit N option 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

Hiring for this skill set? I’m open to SOC, security analyst, and IT roles, and I usually reply within 24 hours.