Zion Boggan

In-depth vulnerability research, detection engineering & applied cryptography.

● Open to security-research & detection roles
GitHub · LinkedIn · Email
← All work
SOC AUTOMATION

SOC Automation Lab

An end-to-end detection-to-response pipeline wiring Wazuh detection into Shuffle SOAR and TheHive case management, deployed and validated live against a replayed SSH brute force.

WazuhTheHiveShuffleCortexMITRE ATT&CKSOARVirusTotalAlienVault OTXDockerSysmon
7
Services in the SIEM stack
11
Custom detection rules
340
Alerts in the live test
53
SSH auth failures ingested
10
Integration handoff level
1
Agent enrolled (forge)
Threat Hunting dashboard from the live test: 340 total alerts, 53 SSH authentication failures from the enrolled agent, broken down by MITRE ATT&CK technique.
Threat Hunting dashboard from the live test: 340 total alerts, 53 SSH authentication failures from the enrolled agent, broken down by MITRE ATT&CK technique.

Architecture and data flow

The pipeline runs detection, orchestration, and case management as three decoupled layers, with the SIEM only needing to know one webhook URL:

  1. Agents on Windows and Linux endpoints ship logs and Sysmon events to the Wazuh manager over an encrypted channel on 1514/tcp.
  2. The manager decodes events against the bundled ruleset plus the custom rules in local_rules.xml.
  3. Any alert at level 10 or higher matches the <integration> block and the manager invokes custom-thehive, which forwards a normalized payload to the Shuffle webhook.
  4. Shuffle resolves the indicator of interest (source or destination IP), pulls reputation from VirusTotal and OTX, and computes a verdict score.
  5. A TheHive case is opened with the score-derived severity, the indicator is attached as an observable flagged as an IOC, and the analyst channel gets a message linking the case.

The handoff lives in the manager's ossec.conf - level 10 is the line between “stays in the dashboard” and “worth a case”:

<integration>
 <name>custom-thehive</name>
 <hook_url>SET_FROM_ENV_SHUFFLE_WEBHOOK_URL</hook_url>
 <level>10</level>
 <alert_format>json</alert_format>
</integration>

The stack

The SIEM and case-management side is one Compose project of seven services: the three Wazuh components, plus TheHive backed by Cassandra and Elasticsearch with Cortex available for observable analyzers. Versions are pinned across the board - Wazuh 4.9.0, TheHive 5.4, Cortex 3.1.8, Cassandra 4.1, Elasticsearch 7.17.20. A trimmed excerpt:

services:
 wazuh.indexer:
 image: wazuh/wazuh-indexer:4.9.0
 ports:
 - "9200:9200"
 environment:
 - OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g
 - bootstrap.memory_lock=true

 wazuh.manager:
 image: wazuh/wazuh-manager:4.9.0
 depends_on:
 - wazuh.indexer
 ports:
 - "1514:1514"
 - "1515:1515"
 - "55000:55000"

 thehive:
 image: strangebee/thehive:5.4.0-1
 depends_on:
 - cassandra
 - elasticsearch
 - cortex
 ports:
 - "${THEHIVE_PORT}:9000"

Shuffle runs as its own four-service Compose project (shuffle-database on OpenSearch 2.14.0, shuffle-backend, shuffle-frontend, shuffle-orborus, all pinned to 1.4.0) so it survives a teardown of the SIEM side.

Custom detections

local_rules.xml adds eleven detections on top of the Wazuh ruleset, each mapped to a MITRE ATT&CK technique so cases arrive tagged:

RuleDetectionTechnique
100101Process launched from a user-writable pathT1059
100102Office application spawns a scripting hostT1566 / T1059.001
100110Suspicious LSASS access (credential dumping)T1003.001
100120 / 100121New service and scheduled-task persistenceT1543.003 / T1053.005
100200 / 100201SSH and RDP brute forceT1110
100210–100212CTI watchlist hits (IP, domain, hash)T1071 / T1204

The LSASS-access rule is the kind of thing the bundled ruleset doesn't ship - it keys off a Sysmon process-access event against lsass.exe with one of the granted-access masks that credential dumpers request:

<rule id="100110" level="12">
 <if_sid>61609</if_sid>
 <field name="win.eventdata.targetImage" type="pcre2">(?i)\\lsass\.exe$</field>
 <field name="win.eventdata.grantedAccess" type="pcre2">0x1010|0x1410|0x143a|0x1fffff</field>
 <description>Suspicious LSASS access - possible credential dumping</description>
 <mitre>
 <id>T1003.001</id>
 </mitre>
</rule>

The SOAR workflow

The exported Shuffle workflow (wazuh-thehive-enrichment.json) is a webhook trigger fanning into seven actions: a router that picks the indicator, parallel VirusTotal and OTX lookups, a Python scoring step, TheHive case creation, observable attachment, and a Slack notification. The router and both lookups feed the scoring node, which gates everything downstream. The scoring step turns reputation counts into a TheHive severity:

vt = int($action_vt.last_analysis_stats.malicious or 0)
otx = int($action_otx.pulse_info.count or 0)
score = vt * 2 + otx
severity = 3 if score >= 6 else (2 if score >= 2 else 1)
return {"score": score, "severity": severity, "vt_malicious": vt, "otx_pulses": otx}

The case node carries the agent, the rule, the reputation counts, and the raw log into the description, and tags the case with the rule's MITRE id. A missing VirusTotal result is coerced to zero rather than failing the case creation, so the free tier's 4 req/min limit never drops an alert. The indicator is then attached as an observable flagged as an IOC, so it flows into TheHive's observable history and can be swept against other cases.

Live validation

The lab was stood up single-node with a Linux endpoint (forge, Ubuntu 22.04) enrolled and reporting in, then an SSH brute force was replayed against it. The Threat Hunting dashboard showed 340 total alerts with 53 real SSH authentication failures ingested from the agent, every detection auto-mapped to MITRE ATT&CK across Password Guessing, SSH, and Brute Force categories. The escalation rule that fired collapses a burst of Wazuh's per-failure 5710 events into one level-10 brute-force alert:

<rule id="100200" level="10" frequency="8" timeframe="120">
 <if_matched_sid>5710</if_matched_sid>
 <same_source_ip />
 <description>SSH brute force - 8 failed logins from $(srcip) in 120s</description>
 <mitre>
 <id>T1110</id>
 </mitre>
</rule>

For CTI-list hits, rule 100210 is also wired to active response: the manager issues a firewall-drop on the endpoint for 600 seconds while the analyst confirms. The block is scoped to a single rule on purpose - auto-blocking on a noisier rule would be a fast way to firewall yourself out of your own hosts.

Agent enrollment

The manager runs registration on 1515/tcp with force enabled so a re-enrolling host reclaims its slot rather than piling up duplicates. Linux endpoints enroll through a helper that adds the Wazuh apt repo and installs the agent pinned to the manager's version:

curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --no-default-keyring \
 --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import
echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" \
 > /etc/apt/sources.list.d/wazuh.list
apt-get update
WAZUH_MANAGER="${WAZUH_MANAGER}" WAZUH_AGENT_GROUP="${WAZUH_AGENT_GROUP}" \
 apt-get install -y "wazuh-agent=${WAZUH_VERSION}"
systemctl enable --now wazuh-agent

Windows hosts use a PowerShell counterpart that pulls the MSI and registers it into the Sysmon-aware windows group, so process-creation and network events arrive with enough context for the rules above to be useful:

WAZUH_MANAGER=10.0.0.10 ./scripts/enroll-agent.sh
.\scripts\enroll-agent.ps1 -Manager 10.0.0.10 -Group windows

Deployment notes

A few things that bit during bring-up and are worth knowing before running this somewhere real:

  • Indexer compatibility. Filebeat 7.10 refuses to publish to an indexer that reports an OpenSearch 2.x version, so the indexer config sets compatibility.override_main_response_version: true. Without it the manager produces alerts but nothing reaches the indexer and the dashboard stays empty.
  • Memory locking under LXC. The committed config locks memory (bootstrap.memory_lock=true, memlock: -1), which is correct for bare metal. Inside an unprivileged LXC the kernel caps locked memory and the indexer and Elasticsearch fail with an rlimit error; the fix is a docker-compose.override.yml that disables locking rather than editing the committed file.
  • vm.max_map_count. Must be at least 262144 on the host for both the indexer and Elasticsearch; in an LXC it's inherited from the node, so it has to be set there.
  • CTI watchlists. The cti-malicious-* CDB lists are seeded by deploy.sh after the stack is up - they have to be writable so the manager can compile them, which rules out bind-mounting them read-only.

This is a lab build. Production would split the Wazuh indexer and TheHive's Cassandra and Elasticsearch onto their own nodes with real resource headroom.

Repository · github.com/zionsworking/soc-automation-lab