Pros
- • Completely open-source core - zero licensing costs vs. $50K-$300K/year for commercial SOAR
- • Full data sovereignty - all case data, observables, and playbooks remain on-premises
- • 77% average reduction in Mean Time to Respond (MTTR) documented across real SOC deployments
- • 300+ Cortex analyzers for VirusTotal, Shodan, MISP, AbuseIPDB, and every major TIP
- • No-code/low-code Shuffle playbooks mean junior analysts can build automation without Python expertise
- • Native MISP, Wazuh, Elastic, and Splunk integrations - works with your existing stack
- • Multi-tenancy built-in - ideal for MSSPs managing 10 to 100+ client environments
Cons
- • Steeper initial deployment curve than SaaS SOAR - requires Docker/Linux infrastructure expertise
- • TheHive 5 Community license lacks SSO and High Availability - commercial tiers required at scale
- • Shuffle documentation quality is inconsistent - community forums are the real knowledge base
- • No vendor 24/7 support on free tier - you own the operational responsibility
- • Cortex requires Docker daemon access - a potential security surface that requires hardening
Watch a tier-1 analyst work a queue for an hour and count the browser tabs. VirusTotal, AbuseIPDB, a WHOIS lookup, the internal asset spreadsheet, Shodan, back to the SIEM to copy the next indicator because the first one turned out to be the corporate egress IP. That is not analysis. It is a human being used as an HTTP client, and it consumes most of a shift on a normal day.
The fix is obvious and the fix is not the hard part. Enrichment is mechanical, deterministic and perfectly automatable; every one of those lookups is an API call with a stable schema. What is hard is that automating it properly requires case management, an enrichment engine and an orchestrator, and the commercial products that bundle all three arrive with licensing in the tens to hundreds of thousands a year plus an implementation engagement.
TheHive, Cortex and Shuffle are the open-source answer to the same three problems. The stack is genuinely capable and widely deployed, and it costs infrastructure rather than licensing.
What it costs instead is ownership. This is three services, two datastores and a Docker socket you are deliberately exposing, and there is no vendor to escalate to when Cassandra will not join its own cluster on a Sunday. If your team is comfortable there, this is a blueprint. If it is not, the honest recommendation later in this piece is to buy something managed instead.
The Architecture: Three Platforms, One Autonomous SOC
Three tools, three jobs. Most of the fragile deployments I have seen went wrong by putting logic in the wrong one of them:
| Platform | What It Does |
|---|---|
| TheHive 5 | Incident case management—the command centre where every incident begins and ends |
| Cortex | Automated observable enrichment—it looks up IPs, domains, file hashes against threat intelligence sources automatically |
| Shuffle | Orchestration and automation—it decides what happens next based on analysis results, then makes it happen |
The boundaries matter more than the features. Cortex can call out to your firewall, so people put containment logic there. TheHive can trigger webhooks, so people build workflows out of chained notifications. Both work, and both produce a system where the answer to “why did this host get blocked?” is spread across three tools with no execution log you can read end to end.
Keep it simple: Shuffle is the pipe. Cortex is the scalpel. TheHive is the brain. Decision logic belongs in Shuffle, because Shuffle is the only component with a visual execution trace, and during an incident review the ability to replay exactly which branch fired is worth more than architectural elegance.
When your SIEM flags a suspicious IP:
- Shuffle receives the alert, extracts the fields it cares about (source IP, host, severity, rule), and decides whether this is worth pursuing at all
- Cortex enriches the observable against VirusTotal, AbuseIPDB, Shodan, your MISP instance and whatever else you have configured, in parallel
- TheHive receives a case with the enrichment already attached, so the analyst opens it to context rather than to a starting point
The gain is not that lookups happen faster. It is that the analyst’s first decision is made with complete information instead of after fifteen minutes of assembling it — and decision quality at minute zero is what determines whether the next hour is spent on the right incident.
Worth naming the risk this introduces. A pipeline that auto-closes low-scoring alerts is making a judgement no human reviews, and its false negatives are invisible by construction: nothing appears in a queue, no metric moves, and the miss looks exactly like a quiet day. Whatever threshold you choose, sample the auto-closed bucket weekly. If nobody is reading the discard pile, you have not automated triage — you have automated not looking.
TheHive 5: Your Incident Operations Center
TheHive is where the investigation lives, and the reason it matters is not the interface. It is that an incident worked in a ticketing system leaves you with a resolution note, whereas an incident worked in TheHive leaves you with the observables, the enrichment, the tasks, the timeline and the ATT&CK tags — a record you can query three months later when the same infrastructure reappears. Cases you can search across are the difference between handling thirty separate incidents and recognising one campaign.
What TheHive Needs to Run
TheHive is a thin application over three dependencies, and that ratio is the honest description of its operational cost. Nearly every production incident you will have is a Cassandra or Elasticsearch incident wearing a TheHive error message.
| Component | What It Does | Options |
|---|---|---|
| Cassandra | Stores all your case data, investigations, and alerts | ScyllaDB if you need higher performance |
| Elasticsearch | Lets you search and filter through millions of alerts quickly | OpenSearch works just as well |
| File Storage | Holds evidence files, attachments, and observable data | MinIO, NFS, AWS S3, or local disk |
Cassandra is the one to respect. It is the system of record, so a corrupted or unrecoverable keyspace is your entire case history, and it is not a database most SOC teams have operated before. Two consequences worth acting on: back it up with nodetool snapshot on a schedule and actually test a restore, because an untested Cassandra backup is a belief rather than a backup. And treat the single-node Docker deployment further down this page as what it is — a perfectly reasonable starting point with a single point of failure holding every investigation you have ever run.
The file storage choice looks like a footnote and is not. Evidence attachments include malware samples, and localfs on the TheHive host means those samples sit on the same filesystem as the application. Object storage keeps them somewhere you can apply separate access control and lifecycle rules to.
How TheHive Organizes Information
The hierarchy is worth learning properly, because it is what the automation writes into:
Alert (the raw signal from your SIEM)
└── Case (the investigation you open because of that alert)
├── Tasks (work items: verify authenticity, check for lateral movement, etc.)
├── Observables (the actual artefacts: IPs, domains, hashes, email addresses)
│ └── [Cortex analysis results get attached here automatically]
└── TTPs (MITRE ATT&CK technique tags so you know what the attacker was trying to do)
The alert-versus-case distinction is the one that pays off. Alerts are cheap and disposable — your automation can create thousands, and dismissing one costs nothing. Cases are deliberate. Keeping that boundary honest means your case count reflects actual incidents, which is the only reason any of your later metrics mean anything. Teams that let automation open cases directly end up with a case history full of noise and no way to answer “how many real incidents did we have last quarter”.
The observable layer earns its keep on the second sighting. Because observables are first-class objects rather than text in a description field, TheHive will tell you that this hash already appeared in a case six weeks ago — which is the single most useful sentence in an investigation and the thing spreadsheet-based tracking can never produce.
TheHive Licensing: What You Actually Need
TheHive 5 is open-core, so the licence tier is a real architectural decision rather than a billing detail:
| License | Cost | What You Get |
|---|---|---|
| Community | Free | Perfect for single organisations, unlimited users, no SSO, no failover clustering |
| Gold | €500/month | High availability, Active Directory integration, better for teams that need it |
| Platinum | €1,000/month | Everything in Gold, plus some multi-tenancy features |
| MSSP | Custom pricing | Full multi-tenancy for managed security service providers running multiple client environments |
Check the current tiers and prices with StrangeBee before you plan around them — the figures above are indicative and the packaging has changed more than once since TheHive 5 shipped.
The honest answer: for a single organisation, Community is a real production tier and not a crippled demo. You get unlimited users and the full case model.
The two limits that bite are worth understanding as security limits, not feature limits. No SSO means local accounts in TheHive, which means an offboarding process that has to remember one more system, and a set of credentials outside whatever conditional access and MFA you have built everywhere else. For a tool holding your complete incident history, that is a genuine gap rather than an inconvenience. And no HA means the single-node reality described above: the platform your analysts use during an incident is itself a single point of failure, which is a poor property for exactly the moments it matters most.
Neither is a reason to avoid Community. Both are reasons to know which one will force the upgrade, so the decision happens on a planning day rather than during an outage.
TheHive’s Core Strengths
Live collaboration — Task and evidence updates propagate to everyone on the case without a refresh. The value is not convenience; it is that two analysts working the same incident stop duplicating each other’s lookups, which is where a surprising amount of a major incident’s effort actually goes.
TLP and PAP classification — Traffic Light Protocol and Permissible Actions Protocol tags on cases and observables, enforced by the platform. This matters most if you share with an ISAC, a CERT or a client: TLP:Red intelligence leaving via an automated export is the kind of mistake that ends an information-sharing relationship permanently. Be clear on the boundary, though — TheHive enforces the tags within TheHive. Your Shuffle playbooks will happily post a TLP:Red observable into a Slack channel unless you check the tag before sending, and that check is the easiest thing in the world to leave out of a workflow.
Case templates — Define the standard shape of a phishing or malware investigation once — tasks, required observable types, severity defaults — and every case starts there. This is the highest-return feature in TheHive and the most consistently skipped. Templates are how a junior analyst at 03:00 runs the same process as your lead, and how you find out that “check for lateral movement” was never done, because the task is sitting there unclosed.
Notifications and webhooks — Severity changes, task completions and case closures can fire webhooks into Shuffle, Slack or PagerDuty. Keep the logic thin here; a chain of TheHive notifications triggering other TheHive notifications is an automation you cannot trace.
Full REST API — Everything the UI does is available over the API, which is what makes the automation in this stack possible at all. It also means an API key is equivalent to full access to your incident history, so scope keys per integration and rotate them on the schedule set out further down rather than sharing one admin key across three tools.
Cortex: The Enrichment Engine
Cortex is a job runner that normalises a few hundred threat intelligence APIs behind one interface. Every analyzer is a script in a container that takes an observable, calls a service, and returns a structured verdict in the same shape as every other analyzer — which is the property that makes the rest of the automation possible. Without it, every playbook contains bespoke API handling for a dozen services with a dozen response formats and a dozen ways of failing.
That containerised design is also the security cost, and it is a real one: Cortex needs Docker socket access to launch analyzer containers, and the Docker socket is root on the host. Hardening it is not optional — see the socket proxy in the hardening section below.
How Cortex Works in Practice
You’re looking at a case in TheHive. You see an IP address that triggered the alert. You click “Cortex” and select which analyses to run. Cortex takes that IP and simultaneously checks:
- VirusTotal — is this IP’s reputation flagged?
- AbuseIPDB — how many abuse reports?
- Shodan — what services are exposed on this IP?
- Your MISP — have we seen this IP before in our threat intelligence?
- Talos Intelligence, OTX, Spamhaus — other threat feeds
- And 280+ more sources, in parallel
Within seconds you have structured results attached to the observable, and the decision gets made on evidence rather than on the alert text.
Two disciplines keep this useful rather than merely fast. Enrichment is not attribution: a clean VirusTotal result means nobody has submitted the sample, which for targeted malware is the expected result and not reassurance. And a high AbuseIPDB confidence score on a shared cloud or NAT address says something about the address, not about the traffic you are looking at — which is exactly how automated blocking ends up dropping a CDN.
There is also a disclosure question nobody raises until after the fact. Submitting an observable to a third-party service tells that service you are interested in it, and uploading a file sends the file. For a document exfiltrated from your own environment, or a hash unique to an ongoing intrusion, that lookup can tip off the operator watching their own submissions, and public sandbox uploads are frequently readable by others. Configure analyzers to check hashes rather than upload files by default, and decide deliberately which observables are allowed to leave.
Cortex can also take action: responders can block an IP on the firewall, add a domain to a DNS blocklist, isolate an endpoint via CrowdStrike, or open a Jira ticket. Powerful, and the same caution as any automated containment applies — the analyzer that recommended the action is subject to the false positives described above.
Cortex’s Smart Caching
Cortex caches analyzer results for a configurable window, and it exists for a boring reason that turns out to matter enormously: free-tier API quotas.
VirusTotal’s public tier allows a few hundred requests a day. A phishing campaign delivered to forty recipients generates forty alerts referencing the same sender IP, the same attachment hash and the same handful of URLs. Without caching, that single campaign can consume your entire daily quota before lunch — and here is the part that hurts: when the quota is exhausted, the analyzer returns an error, the playbook’s scoring condition evaluates against a missing result, and alerts start getting auto-closed as clean because nothing came back malicious. A rate limit becomes a detection outage, silently.
So the caching is not a nice optimisation. It is load-bearing, and the cache duration is a real tuning decision. Long windows conserve quota and risk serving a stale verdict on an indicator that was reclassified an hour ago; short windows keep verdicts fresh and burn quota faster. Ten minutes is a sane default for a small SOC. Whatever you pick, alert on analyzer job failures explicitly, because a failing analyzer is the one failure in this stack most likely to quietly degrade every downstream decision.
Shuffle: Orchestration and Automation
Shuffle holds the decisions. Everything conditional in this stack — what gets enriched, what escalates, what gets closed, who gets woken up — belongs here, in one place, with an execution log.
What Makes Up Shuffle
The backend (Go) receives triggers and schedules workflow executions, handling many in parallel.
The frontend (React) is a drag-and-drop workflow builder. The no-code claim is half true and worth being precise about: wiring apps together needs no code, but the moment a playbook has to reshape a JSON payload or evaluate a nested field, you are writing expressions and Python snippets. Budget for one person who is comfortable there, because every non-trivial playbook eventually needs it.
Orborus is the worker that spawns containers to execute workflow steps. Like Cortex, it wants Docker socket access — the same hardening requirement, for the same reason.
The app library covers Wazuh, Splunk, AWS, CrowdStrike, Slack, PagerDuty and several hundred more, generated largely from OpenAPI specifications. That generation is why the catalogue is so large and also why quality varies sharply: a heavily used app is solid, while a rarely touched one may be a thin wrapper over an API version that moved on. Test any app your playbook depends on against your actual tenant rather than trusting the connector exists.
The gap you will notice fastest is documentation. Shuffle’s docs lag the product, and the practical knowledge lives in the community Discord and in reading other people’s exported workflows. Factor that into your estimate: the platform is capable, but you will spend more time on discovery than the feature list suggests.
Trigger Types
Four ways a workflow starts, and the third one is the one that separates a mature deployment from a dangerous one:
# Example: Wazuh → Shuffle webhook trigger
triggers:
- type: webhook
name: "Wazuh High-Severity Alert"
description: "Receives Wazuh alerts with rule.level >= 12"
- type: schedule
cron: "0 6 * * 1"
description: "Monday 6AM: Weekly threat summary digest"
- type: user_input
prompt: "Analyst: Should this IP be blocked?"
description: "Human-in-the-loop approval gate"
- type: api
endpoint: "/api/v1/execute_workflow"
description: "Direct programmatic trigger"
The user_input gate is how you keep automation on the right side of the line. Enrichment, case creation, notification and evidence collection are reversible and belong fully automated. Blocking an address, disabling an account, isolating a host and purging mail from inboxes are not, and each has a plausible false-positive path to an outage. Put those behind an approval step that names the blast radius — which host, which account, how many mailboxes — so the analyst approving at 03:00 is agreeing to something specific rather than clicking yes.
Two things about the webhook trigger. The URL is a bearer credential: anything that can reach it can inject arbitrary alerts into your pipeline, which for a workflow ending in a firewall block is a gift. Treat webhook URLs as secrets, restrict them at the network layer, and validate that the payload actually looks like your SIEM’s output before acting on it. And webhooks are fire-and-forget, so if Shuffle is down the alert is simply gone — the SIEM believes it delivered, nothing retries, and the only record is in the SIEM’s own index. Keep the SIEM as the authoritative alert store and treat Shuffle as a consumer that may miss things.
The Hybrid Execution Model
The architecturally interesting option is the hybrid model: webhook traffic is relayed via Shuffler.io’s cloud to an on-premises Shuffle instance, so you get externally reachable webhook endpoints without an inbound firewall rule. For a hardened or heavily segmented network that removes a genuine obstacle.
Be clear about what you are accepting in return. Alert payloads — hostnames, internal IP addresses, usernames, file paths, sometimes the content that triggered the detection — transit a third party’s infrastructure, and your incident pipeline now has an availability dependency on a service you do not run. Fine for many organisations, and a non-starter for anyone with data residency obligations or an air gap. If that trade does not work, run the relay yourself and accept the inbound rule instead.
The Economics: Why This Stack Wins
The Cost Reality
Commercial SOAR pricing is negotiated and usually under NDA, so read the table below as orders of magnitude rather than quotes. The ranges are wide because the reality is wide — the same product lands at very different numbers depending on seat count, action volume and how much the vendor wants the reference customer.
| Platform | Year 1 | Year 3 Total | The Problem |
|---|---|---|---|
| This Open-Source Stack | $0-$12K (infra) | $0-$36K | You need Linux expertise |
| Palo Alto XSOAR | $80K-$350K | $240K-$1M+ | Vendor lock-in, works best with Palo Alto |
| Splunk SOAR | $60K-$250K | $180K-$750K | Cloud-dependent, steep licensing |
| IBM QRadar SOAR | $70K-$300K | $210K-$900K | Complex, expensive support |
| Microsoft Sentinel + Logic Apps | $30K-$150K | $90K-$450K | Azure lock-in |
Note what the open-source row does not include, because the licensing comparison is the easy half. Building the playbooks is engineering work — a first useful phishing workflow is days, and a playbook set that covers your top five alert types is closer to a quarter of an engineer’s year. Commercial platforms charge for that in the licence and in an implementation engagement; you pay it in salary. The gap survives the correction comfortably, but “free” describes the download, not the deployment.
Why It Actually Works Better
No vendor bias. XSOAR’s integrations are excellent for Palo Alto’s own products and merely adequate elsewhere — which is rational for Palo Alto and inconvenient if your estate is Wazuh, Splunk and CrowdStrike. Shuffle has no product to favour. The flip side is that it also has no product to guarantee: nobody is contractually obliged to keep any given connector working.
Realistic scaling for MSSPs. Per-client onboarding in commercial SOAR usually means per-client licensing plus customisation. Here you clone a workflow, swap the variables and go, so the tenth client costs a fraction of the first. What you own instead is tenant isolation — nothing prevents a misconfigured workflow writing client A’s observables into client B’s case, and that failure is a contract-ending event. Separate instances or rigorous per-tenant credential scoping, decided before client two.
Real performance gains. Deployments of this kind commonly report large MTTR reductions, and the mechanism is sound: replacing sequential manual lookups with parallel API calls removes minutes of wall-clock time from every alert. Treat the specific percentages, here and elsewhere, with care — MTTR is trivially gameable, since closing alerts faster improves the metric whether or not anything was investigated, and a pipeline that auto-closes 78% of alerts will improve every average you track by construction. The number worth measuring is not how fast alerts close. It is whether the incidents you escalated were the right ones, which requires periodically sampling what the automation discarded.
Deployment Guide: Production Docker Compose
Docker Compose is the fastest route to a working stack, and the configuration below runs everything on one host — 8 vCPU, 16 GB RAM and 500 GB of SSD is a realistic floor once Elasticsearch and Cassandra both have heap.
Be honest with yourself about what single-host means. Every component, both datastores and your entire case history share one kernel, one disk and one maintenance window, and the machine your analysts depend on during an incident is a machine that reboots for patching. It is the right place to start. It is not where a SOC that has come to rely on this should still be in a year — split the datastores off first, since they are the part whose loss is unrecoverable.
Also note that the Compose file below is a starting point, not a hardened deployment: it binds ports to all interfaces, ships default credentials and disables Elasticsearch security for simplicity. Work through the hardening section before anything reaches a network you do not control.
Prerequisites
# Server requirements: Ubuntu 22.04 LTS
# Docker Engine 24.x + Docker Compose v2
# Increase virtual memory for Elasticsearch
sudo sysctl -w vm.max_map_count=262144
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
# Create directory structure
mkdir -p /opt/soc-stack/{thehive,cortex,shuffle,elasticsearch,cassandra,misp}
cd /opt/soc-stack
Docker Compose Configuration
version: "3.8"
services:
# ─── Cassandra (TheHive Database) ───────────────────────────
cassandra:
image: cassandra:4.1
container_name: cassandra
hostname: cassandra
environment:
- CASSANDRA_CLUSTER_NAME=thp
- MAX_HEAP_SIZE=1G
- HEAP_NEWSIZE=200M
volumes:
- /opt/soc-stack/cassandra/data:/var/lib/cassandra
healthcheck:
test: ["CMD-SHELL", "nodetool status | grep UN"]
interval: 30s
timeout: 10s
retries: 10
# ─── Elasticsearch (TheHive Index) ──────────────────────────
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.1
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms2g -Xmx2g
volumes:
- /opt/soc-stack/elasticsearch/data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -v red"]
interval: 30s
timeout: 10s
retries: 10
# ─── TheHive 5 ──────────────────────────────────────────────
thehive:
image: strangebee/thehive:5.3
container_name: thehive
depends_on:
cassandra: { condition: service_healthy }
elasticsearch: { condition: service_healthy }
ports:
- "9000:9000"
volumes:
- /opt/soc-stack/thehive/config:/etc/thehive
- /opt/soc-stack/thehive/data:/opt/thp/thehive/files
command:
- --storage-provider localfs
- --cassandra-host cassandra
- --es-host http://elasticsearch:9200
# ─── Cortex ─────────────────────────────────────────────────
cortex:
image: thehiveproject/cortex:3.1.7
container_name: cortex
depends_on:
elasticsearch: { condition: service_healthy }
ports:
- "9001:9001"
volumes:
- /opt/soc-stack/cortex/config:/etc/cortex
- /var/run/docker.sock:/var/run/docker.sock # Required for analyzer containers
- /opt/soc-stack/cortex/jobs:/tmp/cortex-jobs
environment:
- JOB_DIRECTORY=/tmp/cortex-jobs
# ─── Shuffle SOAR ───────────────────────────────────────────
shuffle-backend:
image: ghcr.io/shuffle/shuffle-backend:latest
container_name: shuffle-backend
hostname: shuffle-backend
ports:
- "5001:5001"
volumes:
- /opt/soc-stack/shuffle:/shuffle-database
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DATASTORE_EMULATOR_HOST=shuffle-database:8000
- SHUFFLE_APP_HOTLOAD_FOLDER=/shuffle-database/apps
- SHUFFLE_FILE_LOCATION=/shuffle-database/files
- SHUFFLE_DEFAULT_USERNAME=admin
- SHUFFLE_DEFAULT_PASSWORD=changeme_on_first_login
shuffle-frontend:
image: ghcr.io/shuffle/shuffle-frontend:latest
container_name: shuffle-frontend
ports:
- "3001:80"
environment:
- BACKEND_HOSTNAME=shuffle-backend
shuffle-orborus:
image: ghcr.io/shuffle/shuffle-orborus:latest
container_name: shuffle-orborus
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- BASE_URL=http://shuffle-backend:5001
- SHUFFLE_APP_SDK_VERSION=1.1.0
Post-Deployment Configuration Steps
# Step 1: Start the stack
docker compose up -d
# Step 2: Monitor startup (takes 3-5 minutes)
docker compose logs -f thehive cortex
# Step 3: Access TheHive at http://YOUR_SERVER:9000
# Default credentials: admin@thehive.local / secret
# Step 4: Access Cortex at http://YOUR_SERVER:9001
# Create a new org and generate an API key
# Step 5: Link Cortex to TheHive
# TheHive Admin → Platform Management → Cortex
# Paste the Cortex API key and server URL
# Step 6: Access Shuffle at http://YOUR_SERVER:3001
# Complete onboarding wizard
Steps 3, 4 and 6 all involve a default or self-chosen credential on a service that is currently listening on every interface. Change all of them before the host is reachable from anywhere but your own workstation, and put the whole stack behind a reverse proxy with real TLS — analysts will be pasting case data into these interfaces, and http:// on port 9000 means that data crosses your network in clear text.
If startup fails, the cause is almost always one of three things: vm.max_map_count not applied so Elasticsearch dies immediately, insufficient RAM so the JVM heap settings above cannot both be satisfied, or Cassandra still initialising while TheHive gives up waiting for it. Read docker compose logs for the dependency rather than for TheHive — TheHive’s error will describe a symptom, not the cause.
Integration Blueprint: Wazuh → Shuffle → TheHive
This is the pattern most deployments end up running. The full data flow and configuration follow — including the <level> filter, which is the most consequential single line in it.
Step 1: Configure Wazuh to Forward Alerts to Shuffle
Add the following integration block to /var/ossec/etc/ossec.conf on your Wazuh Manager:
<integration>
<name>custom-shuffle</name>
<hook_url>http://YOUR_SHUFFLE_SERVER:5001/api/v1/hooks/webhook_XXXXXXXX</hook_url>
<level>12</level> <!-- Only forward critical/high severity -->
<alert_format>json</alert_format>
</integration>
Restart the Wazuh Manager after the configuration change:
sudo systemctl restart wazuh-manager
That <level>12</level> is doing more work than it looks like. It is the boundary of everything your automation can ever see: alerts below level 12 never reach Shuffle, so no playbook enriches them, no case gets created, and no metric reflects them. Set it too high and you have built a fast pipeline for the alerts that were already obvious. Set it too low and you are firing a Cortex job chain — with its finite API quota — at routine noise.
Start conservative, then widen deliberately by promoting specific rule groups rather than by lowering the global floor. A phishing rule at level 7 that you actually want automated should be forwarded by group, not by dropping the threshold and admitting everything else at level 7 alongside it.
Note also that this integration runs on the Wazuh Manager, which means it is subject to the same silent failure as everything else on that box: if the webhook endpoint is unreachable, Wazuh logs the failure in ossec.log and carries on. Nothing in Shuffle, TheHive or your dashboards will indicate that alerts stopped arriving. Build a heartbeat — a scheduled workflow that expects at least one alert per interval and complains when it sees none.
Step 2: Build the Shuffle Phishing Triage Playbook
[Webhook: Wazuh Alert Received]
│
▼
[Condition: rule.groups contains "syscheck" or "web"]
│
─────┴─────
│ │
YES NO → [Skip/Log]
│
▼
[Regex: Extract observables]
• src_ip from data.srcip
• file_hash from syscheck.sha256_after
• domain from data.url
│
▼
[Cortex: Analyze src_ip with AbuseIPDB]
[Cortex: Analyze file_hash with VirusTotal]
│
▼
[Condition: vtScore > 5 OR abuseConfidence > 75]
│
─────┴─────
│ │
YES NO → [Close: Low Priority]
│
▼
[TheHive: Create Alert]
• title: "Wazuh - [rule.description]"
• severity: Critical
• observables: [ip, hash, Cortex results]
• tags: ["automated", "wazuh", rule.groups]
│
▼
[Slack/Teams: Notify SOC Channel]
• "New critical alert created in TheHive"
• Link to case
Read the two NO branches carefully, because they are where this design succeeds or fails. The second one — vtScore > 5 OR abuseConfidence > 75, else close as low priority — is a detection decision encoded as a threshold, and it fails in a specific direction. Targeted malware and freshly registered infrastructure are precisely the things that score zero on public reputation services, so the alerts this branch discards most confidently are the ones most likely to be a competent adversary. Prefer “deprioritise and retain” over “close” here, and read the discard pile on a schedule.
The other thing missing from this flow is error handling. If Cortex returns nothing — quota exhausted, container failed, service down — then vtScore is absent, the condition is false, and the alert is closed as low priority. An infrastructure failure silently becomes a clean verdict. Add an explicit branch for “analysis did not complete” that escalates to a human rather than falling through to the negative path, and never let a missing result be treated as a benign one.
Finally, note the Slack step. Case links in a chat channel are convenient and they also put incident context into a system with a different access model and a much longer memory than you probably intend. Post the case link and the severity; keep observables, filenames and user identities in TheHive.
Step 3: TheHive Alert Template for Wazuh
The template is where the process gets enforced. Automation can enrich an alert but it cannot notice that nobody checked for lateral movement — an unclosed task can:
{
"title": "Wazuh SIEM - {{alert.title}}",
"description": "## Automated Alert\n\n**Source:** Wazuh SIEM\n**Rule:** {{alert.sourceRef}}\n**Host:** {{alert.source}}\n\n## Observables\n\n{{#observables}}\n- {{dataType}}: `{{data}}`\n{{/observables}}",
"tasks": [
{ "title": "Verify alert is not a false positive", "assignee": null },
{ "title": "Enrich all observables with Cortex analyzers", "assignee": null },
{ "title": "Determine scope - check for lateral movement", "assignee": null },
{ "title": "Execute containment if confirmed malicious", "assignee": null },
{ "title": "Document findings and close case", "assignee": null }
],
"tags": ["wazuh", "automated-triage"]
}
That first task — verify the alert is not a false positive — is the one to keep at the top and the one people delete once the pipeline feels reliable. It is the human check on an automated escalation, and its absence is how a confident pipeline talks a tired analyst into containing a backup agent.
The automated-triage tag is worth more than it looks. It lets you separate machine-created cases from analyst-created ones in every subsequent report, which is the only way to tell whether your automation is finding real incidents or generating tidy-looking volume.
Real-World Case Study: Phishing Campaign Triage at Scale
The Scenario
This is a composite scenario rather than a named engagement, and the figures are illustrative — the shape is representative of how these deployments go, and the numbers are there to show the mechanism, not to be quoted.
The setup: a regional financial services firm, around 1,200 staff, three people in security, several hundred Wazuh alerts a day. The arithmetic is the whole story. Three analysts covering business hours is roughly 24 analyst-hours a day; 450 alerts at eight to twelve minutes each is 60 to 90 hours of work. The queue was never going to be cleared, and everyone knew it.
What happens next is predictable and is the actual harm. A queue that cannot be cleared gets triaged by proximity to the top rather than by severity, and the team develops a fast heuristic for closing things — which works until the day the heuristic is wrong. The three to four missed incidents per quarter in the table below are not a discipline problem. They are the designed output of a system with three times more work than hours.
The Problem (Before)
| Metric | Before Deployment |
|---|---|
| Daily alert volume | 450 average |
| Manual triage time per alert | 8-12 minutes |
| True positive rate (pre-enrichment) | ~12% |
| MTTR (confirmed incidents) | 4.2 hours |
| Analyst hours on L1 triage | 6.5 hours/day |
| Incidents missed (per quarter) | 3-4 (analyst fatigue) |
The Solution Architecture
The team deployed:
- TheHive 5 Community on a dedicated Ubuntu 22.04 VM (8 vCPU, 32GB RAM)
- Cortex 3.1 with 12 configured analyzers (VirusTotal, AbuseIPDB, MISP, Shodan, URLhaus, MalwareBazaar, Hybrid Analysis, PassiveTotal, Talos Intelligence, OTX, Spamhaus, Greynoise)
- Shuffle with two primary playbooks: Phishing Email Triage and Malware Alert Enrichment
- Integration: Wazuh Manager forwarding rules 85, 91xxx (phishing detection), and rule level ≥12 to Shuffle webhook
The Phishing-Specific Playbook
- Wazuh fires on email gateway rule (rule group:
office365orexchange) - Shuffle extracts: sender domain, sender IP, attachment hash, embedded URLs
- Cortex triggered in parallel:
- AbuseIPDB on sender IP (confidence threshold: 50%)
- VirusTotal on attachment hash (detection threshold: 3/72)
- URLhaus on embedded URLs
- SpamHaus on sender domain
- Shuffle evaluates results - if any threshold exceeded, severity escalated to Critical
- TheHive case created with pre-populated observables, all Cortex results attached
- Slack message sent to
#soc-alertswith case link and summary - If the hash has 20+ VirusTotal detections, Shuffle calls the CrowdStrike Falcon API to add a custom IOC block across all endpoints
Step 7 is the only irreversible action in the chain, and the 20-detection threshold is doing the work of a human approval. That is a defensible choice — a file with twenty independent engine detections is not ambiguous — but note the exposure: this workflow can block a hash fleet-wide on the strength of a third-party API response, so a compromised webhook or a poisoned reputation result is a self-inflicted availability incident. At minimum, exclude signed vendor binaries, log every block with the case that caused it, and make sure someone knows how to reverse one at 03:00 without reading documentation.
Step 4’s escalation to Critical also deserves a floor. Severity driven purely by reputation scores means an alert on a domain controller and an alert on a test VM escalate identically, which is how a genuine Critical ends up queued behind noise. Asset criticality has to come from somewhere — usually a CMDB lookup or, failing that, a hard-coded list of hosts that always escalate.
The Results (After 90 Days)
| Metric | Before | After | Change |
|---|---|---|---|
| Daily alert volume | 450 | 450 | - |
| Automated triage rate | 0% | 78% | +78% |
| True positive escalation rate | 12% | 91% | +79% |
| MTTR (confirmed incidents) | 4.2 hours | 58 minutes | -77% |
| Analyst hours on L1 triage | 6.5 hours/day | 1.4 hours/day | -78% |
| Incidents missed (per quarter) | 3-4 | 0 | -100% |
Read this table sceptically, including the rows that flatter it.
The MTTR and analyst-hours numbers are real in the sense that the work genuinely moved from a human to a machine — parallel API calls instead of sequential browser tabs is a mechanical improvement and it does not depend on anyone’s judgement. The “incidents missed: 0” row is the one to distrust. You cannot count what you did not detect, so that figure means no missed incident was subsequently discovered during the window, which is a different and much weaker claim. Ninety days is also short relative to how long an intrusion can sit undiscovered.
The 12% → 91% escalation rate deserves the same care. It says the alerts reaching analysts are far more likely to be real, which is exactly the intended effect. It says nothing about the 78% the pipeline handled without a human — and if any of those were wrong, they are not in this table by definition.
None of which undermines the deployment. The real result is that the team’s work became finishable, and the recovered hours went into hunting, rule tuning and vulnerability work rather than into absorbing more alert volume. That last choice is the one that matters, and it is a management decision, not a technical one: the default outcome of freeing analyst capacity is that the capacity quietly fills with more triage. Automation buys you time. Whether it buys you better security depends on what you spend it on.
Is This the Right Stack for You?
Perfect Fit
MSSPs managing multiple clients — Reusable playbooks plus no per-client licence means the marginal client is nearly free. The work you take on instead is tenant isolation, and it is not optional: cross-tenant data leakage is the failure that loses contracts, so design it before client two rather than after.
Mid-market enterprises growing fast — You need real orchestration and you are not going to fund six figures of SOAR licensing. Spending a fraction of that on infrastructure and engineering time is the rational trade, provided the engineering time is actually allocated rather than assumed.
Highly regulated industries (healthcare, finance, government) — Incident data never leaves your network, there is no vendor telemetry to explain to a reviewer, and you control retention completely. For air-gapped environments this is frequently the only workable option. Note the exception: the hybrid webhook relay described earlier does involve a third party, so if residency is the reason you are here, do not switch it on.
Teams with real Linux and Docker depth — Two datastores, a Docker socket and three services that fail in each other’s logs. If that is comfortable territory, the stack is a pleasure. If it is not, it is a liability.
Lean security teams — Two to eight analysts against a queue sized for three times that. Automation is not an efficiency project here; it is the only way the work fits in the hours. Just be clear-eyed that building it is itself a project competing with the same scarce hours — the first playbook is the hardest thing you will ship all quarter.
When to Look Elsewhere
No Linux or Docker depth in-house — If nobody can debug a container that will not start, this becomes a fragile dependency in the middle of your incident process. Splunk SOAR or Sentinel with Logic Apps costs more and neglects itself far less gracefully.
You need a vendor SLA — Nothing here has one. When it breaks during an incident, the escalation path is your own engineer and a community Discord.
All-Microsoft environment — With Azure, Teams and Microsoft 365, Sentinel plus Logic Apps reaches identity and Graph telemetry more deeply than this stack can, and if you are already paying for the licensing, its marginal cost is far lower than list price suggests.
You want playbooks rather than a playbook builder — XSOAR ships hundreds of tuned playbooks. Here you build your own, and they will be better fitted to your estate and considerably slower to arrive.
Nobody owns it — The disqualifier nobody lists. This stack rewards an owner and punishes shared custody: an unmaintained playbook that silently stopped firing three months ago is worse than no automation, because the team has already stopped checking the alerts it was handling. If you cannot name the person, buy managed.
Security Hardening: Production Best Practices
Take this section seriously in a way most deployment guides do not, because of what this stack is. It holds your complete incident history — every investigation, every observable, and often malware samples and credentials recovered during response. It has API access into your firewall, your EDR and your identity platform. And two of its components have root-equivalent access to the host.
An attacker who owns your SOAR does not merely read your investigations. They can see exactly what you have detected about them, and they inherit the containment tooling you built to stop them.
Docker Security
The Docker socket is the sharp edge. Cortex and Orborus both need it to launch containers, and access to /var/run/docker.sock is equivalent to root on the host — a container that can talk to it can start a privileged container mounting the host filesystem. Mounting the raw socket into two internet-adjacent services is therefore a decision, not a default to accept quietly.
The socket proxy below is the mitigation: it sits in front of the socket and permits only the specific API operations these services actually need, so a compromise of Cortex yields container creation rather than the whole host. It reduces the exposure; it does not eliminate it, since container creation is itself a strong primitive. Bind it to localhost, as shown, and never publish it.
# Run containers as non-root where possible
# Cortex requires Docker socket access - harden with socket proxy
# Deploy docker-socket-proxy instead of exposing raw socket to Cortex/Shuffle
docker run -d \
--name dockerproxy \
-e CONTAINERS=1 \
-e IMAGES=1 \
-e INFO=1 \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 127.0.0.1:2375:2375 \
tecnativa/docker-socket-proxy
Network Segmentation
[Internet]
│
[Wazuh Manager] ─── webhook ──► [Shuffle] (VLAN: Automation)
│
┌────────────┼────────────┐
▼ ▼ ▼
[TheHive] [Cortex] [MISP]
(VLAN: SOC) (VLAN: SOC) (VLAN: SOC)
│ │
└────────────┘
│
[Elasticsearch + Cassandra]
(VLAN: Data, no internet access)
The datastore tier having no internet access is the most important line in that diagram, and it is the one that gets relaxed first — usually for a package update, temporarily, in a way that outlives the reason. Cassandra and Elasticsearch have both had authentication-bypass and remote-code-execution issues over the years; an exposed one is your entire case history.
Cortex is the awkward exception to the segmentation. It has to reach the internet, because talking to VirusTotal and AbuseIPDB is its whole function, and it also has to reach TheHive. That makes it the component with both external reachability and internal access — restrict its egress to the specific analyzer endpoints you use rather than allowing general outbound, and treat it as the most likely compromise point in the design.
API Key Rotation Policy
# TheHive API key rotation (run quarterly via cron)
# Generate new API key via TheHive API
NEW_KEY=$(curl -s -X POST http://localhost:9000/api/v1/user/current/credentials \
-H "Authorization: Bearer $CURRENT_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "key"}' | jq -r '.key')
# Update Shuffle with new TheHive API key
curl -X PUT http://localhost:5001/api/v1/apps/authentication \
-H "Authorization: Bearer $SHUFFLE_ADMIN_KEY" \
-d "{\"thehive_api_key\": \"$NEW_KEY\"}"
Two notes before you cron this. Rotation that runs unattended will, on some quarter, update the key in one place and not the other — and the symptom is not an error anyone sees. Case creation starts failing inside a workflow, Shuffle logs it, and alerts stop becoming cases while every dashboard stays green. Any rotation job needs a verification step that creates a test alert end to end and fails loudly if it does not appear.
And give each integration its own service account and key rather than rotating one shared admin credential. A per-integration key can be revoked without breaking everything else, and it tells you which component performed an action when you are reading the audit trail during an incident — which, given that this tooling can block addresses and isolate hosts, is a question you will eventually need answered.
Cortex Analyzers: Recommended Starter Configuration
Configure these in order. Resist the temptation to enable forty analyzers on day one — each one is another API key to manage, another quota to exhaust, and another source of results nobody has calibrated. Four well-understood analyzers beat twenty you cannot interpret.
Check the free-tier limits below against current vendor documentation; they change, usually downwards. And note how small some of them are. Shodan at 100 requests a month is not an automation budget, it is a manual-lookup budget — wire that one to analyst-initiated enrichment rather than to a playbook that fires on every alert, or it will be exhausted by the third day of the month and start failing in the silent way described earlier.
MISP is the one to prioritise beyond its position here. It is self-hosted, so it has no quota, no rate limit and no disclosure problem, and it is the only entry in this table that knows about your environment rather than the internet’s.
| Priority | Analyzer | Data Types | Free Tier |
|---|---|---|---|
| 🔴 Critical | VirusTotal | Hash, IP, Domain, URL | 500 req/day |
| 🔴 Critical | AbuseIPDB | IP | 1,000 req/day |
| 🔴 Critical | MISP | All | Self-hosted, unlimited |
| 🟠 High | Shodan | IP | 100 req/month |
| 🟠 High | URLhaus | URL, Domain | Free |
| 🟠 High | MalwareBazaar | Hash | Free |
| 🟠 High | OTX (AlienVault) | Hash, IP, Domain, URL | Free |
| 🟡 Medium | Hybrid Analysis | Hash, URL | 200 req/day |
| 🟡 Medium | PassiveTotal | IP, Domain | 15 req/day |
| 🟡 Medium | Talos Intelligence | IP, Domain | Free (web) |
| 🟢 Optional | Spamhaus | IP, Domain | Free |
| 🟢 Optional | Greynoise | IP | 100 req/day (free) |
The Real Verdict
From the analyst’s seat, a well-built deployment of this stack and a six-figure commercial SOAR are close to indistinguishable. The case opens with the enrichment already attached either way. That is the honest headline, and it is why the comparison is worth making at all.
What the licence actually buys is not capability. It is convenience, managed infrastructure, someone to call at 02:00, and a library of playbooks you did not have to write. Those are real things. Whether they are worth the money depends entirely on whether you have an engineer who would rather build the playbooks than buy them — and on whether that engineer will still be there in two years, because this stack has a key-person risk that a support contract does not.
Be sceptical of the MTTR figures, including the one earlier in this piece. The underlying mechanism is genuine: parallel machine lookups replace sequential human ones, and that removes minutes from every alert whether or not anyone is measuring. But the metric itself is easy to improve by closing things faster, and a pipeline that auto-handles most of the queue flatters every average you compute. The result that matters is not the response-time chart. It is whether the alerts your analysts saw were the ones worth seeing, and the only way to know is to keep reading what the automation threw away.
The thing worth building here is not a faster queue. It is a queue small enough to finish, so that the hours left over go into hunting and tuning rather than into absorbing more alerts. That last part is a management decision the software cannot make for you.
Additional Resources
| Resource | Description |
|---|---|
| TheHive 5 Documentation | Official deployment and API guides |
| Cortex Documentation | Analyzer/Responder catalogue and configuration |
| Shuffle Documentation | Playbook building and app integration guides |
| Cortex-Analyzers GitHub | 300+ open-source analyzer scripts |
| MISP Integration Guide | Threat intelligence platform integration |
| Docker-Templates (StrangeBee) | Official TheHive + Cortex + Shuffle Compose templates |
| SOC Automation Lab (uruc) | Community-built end-to-end homelab guides |