Collecting security alerts is one challenge. Turning an alert into a useful incident note—without giving an AI model control over infrastructure—is another.
In an earlier lab, I connected an Azure Ubuntu VM to a local Wazuh SIEM over Tailscale and verified one controlled SSH authentication failure. On November 14, 2026, I completed the next step: I retrieved that same historical Azure event from the Wazuh Indexer, passed it through a Python SOC assistant and a local Qwen model, and saved a validated incident report for human review.
The result is a manually operated laboratory proof of concept, not autonomous incident response or a production SOC.
What changed since the earlier Azure-to-Wazuh test?
The previous Azure-to-on-premises Wazuh experiment verified the transport and detection path: OpenSSH → journald → Wazuh Agent → Tailscale → local Wazuh Manager → rule 5710 → Dashboard. It did not claim AI analysis of that specific Azure alert.
This follow-up adds the missing downstream path: Wazuh Indexer / OpenSearch → restricted read-only retrieval → Python → local Qwen → validated Markdown report → human analyst.
The Azure VM remained deallocated during the November 14 analysis. I used the indexed historical event rather than restarting cloud compute just to repeat a known test.
Architecture and trust boundaries
Azure Ubuntu 24.04 (historical SSH event) | v Wazuh Agent -- Tailscale private network | v Local Wazuh Manager | v Filebeat / Wazuh Indexer (OpenSearch) | | TLS verification + read-only soc_reader | SSH port-forward to local loopback v Python SOC Assistant (Windows) | | | +--> SQLite event ledger v LM Studio / Qwen 3.5 4B Instruct | v Schema-validated incident report | v Human analyst review The Python program cannot automatically change Azure resources, block IP addresses, alter firewall rules, or execute model-recommended remediation. The model's role is limited to producing structured investigation notes.
1. A traceable, controlled Azure alert
The historical event originated from one intentionally invalid SSH username against the Azure VM's own loopback interface. It was an authorized test, not an external intrusion.
| Evidence field | Verified result |
|---|---|
| Wazuh agent | azure-devops-lab (ID 002) |
| Detection rule | 5710 — login attempt using a non-existent user |
| Alert level | 5 |
| Alert timestamp | 2026-11-08T00:30:25.167+0000 |
| Source context | 127.0.0.1 (laboratory loopback) |
The alert was retained in the Wazuh archive and the Indexer. I confirmed that the Indexer returned one matching document for this agent and rule. A single intentionally failed login does not demonstrate brute force, lateral movement, or compromise.
2. Effective read-only access matters
The assistant accessed OpenSearch through a local SSH tunnel and validated the Indexer's TLS certificate against a trusted CA. It authenticated with a restricted Indexer account, soc_reader, rather than administrator credentials.
During the check, the query initially returned HTTP 403. The intended role already had search permissions for Wazuh alert indices, but the user had not been mapped to it. Correcting that mapping restored the expected read-only search.
The lesson is practical: role definitions are not enough; effective user-to-role mapping must also be tested. No broad administrative rights were needed for alert retrieval.
3. Model output failed closed, then passed
The first end-to-end analysis fetched the event but did not produce an accepted report. Qwen reached its generation limit, yielding an incomplete response:
finish_reason=length completed=0 failed=1 The assistant rejected that output instead of treating partial JSON as a completed security assessment.
After changing the local model configuration, a manual retry succeeded:
status=success fetched=1 selected=1 completed=1 failed=0 duration=11.172s The program wrote a validated Markdown incident report locally and recorded processing state in SQLite. No private report, password, raw production log, SSH private key, or local database was committed to the public project.
4. AI classification is not the analyst's conclusion
The generated incident note assessed the event as:
| Field | Model output |
|---|---|
| Disposition | SUSPICIOUS |
| Severity | LOW |
| Self-reported confidence | 0.7 |
| Suggested actions | Read-only investigation and verification |
My final human disposition was BENIGN because I had deliberately generated this test on an authorized host. The model saw a failed SSH authentication and could describe reasons to investigate it; it lacked the operational context that explains why it occurred.
This is precisely why I did not allow the model to decide firewall actions or declare a confirmed attack. An alert can be technically correct while an AI interpretation remains incomplete.
5. Repeatability, state, and automated tests
The assistant stores processed event identities in SQLite and uses a bounded, manually triggered run. After the successful report, I repeated the invocation; the follow-up did not select the already processed Azure event for a duplicate report.
Relevant evidence from this implementation:
- 35 Python offline tests passed, including validation and state-handling scenarios.
- GitHub Actions checks passed on Python 3.10 and 3.12 for the SOC project.
- Truncated model output was rejected and then retried successfully.
- One matching Azure event produced one validated report.
- Human review corrected the AI's suspicious classification to benign authorized activity.
These results establish behavior within the tested scenarios only. They do not prove general model accuracy, complete prompt-injection resistance, or production availability.
6. A separate Terraform CI milestone
On November 15, I also finalized a private Terraform infrastructure repository for the Azure lab and added a validation-only GitHub Actions workflow.
That workflow checks Terraform formatting, initializes required providers without a remote backend, and runs terraform validate. It succeeded in GitHub Actions after addressing a Windows-versus-Linux provider checksum issue.
It has no Azure credentials and no plan, apply, or destroy stage. Validation of source configuration is deliberately separate from deployment.
Engineering takeaway
The important result is not simply that a language model generated a report. It is that I verified a traceable chain, identified real failure modes, and constrained what the model could do:
Azure security telemetry → private SIEM ingestion → indexed event retrieval → local LLM analysis → validated report → human decision.
This experiment exercised Linux log collection, private networking, Wazuh detection, OpenSearch permissions, TLS, SSH forwarding, Python state management, local AI integration, test automation, and incident-review discipline.
The most useful lesson was straightforward: reliable AI-assisted security workflows need conventional engineering safeguards before they need more autonomy.
Code and earlier work
- AI-Powered SOC Lab — source code and tests
- Verified Azure-to-Qwen end-to-end runbook
- Azure-to-on-premises Wazuh over Tailscale
- Azure Terraform, Ubuntu and Docker
- Azure Ansible, Tomcat and security monitoring
- Building a local Wazuh and Qwen SOC lab
Validated November 14–15, 2026 (Michigan). All security events were created in an authorized lab. No real-world compromise or autonomous remediation is claimed.

