Skip to content

clickhouse

ElastAlert is dead, long live Clickdetect

Hey, souzo here.

ElastAlert has been around for a long time and served the security community well. But the ecosystem has changed — new datasources emerged, new integrations became standard, and the expectations around alerting tools grew significantly. ElastAlert has struggled to keep up.

This post introduces Clickdetect as a modern alternative: more datasources, more webhooks, more control over your queries, and a simpler operational model. If you are tired of working around ElastAlert's limitations, this is for you.

Clickdetect is a generic alerting and detection engine that supports any datasource you have and integrates with any webhook you want.

Check it out on GitHub: https://github.com/clicksiem/clickdetect.

You can also read my previous blog post about Building a powerful SIEM with Clickhouse and Clickdetect.

Clickdetect vs ElastAlert

Why replace ElastAlert with Clickdetect?

You may think — who is this guy trying to tell me that Clickdetect is better than ElastAlert when I've been using ElastAlert for years?

I'm just someone trying to build the best open source security project of my life.

Let me show you why this approach is better.

First of all, let me explain why you should choose Clickhouse instead of Elasticsearch.

Why replace Elasticsearch

See this Clickhouse post: Clickhouse Vs Elasticsearch I tested and I'm current running in a closed environment. I can guarantee, everything Clickhouse posted is real, and it's even better.

Scalability

  • Clickhouse scales better than OpenSearch/Elasticsearch with Distributed tables.
  • You can use big data schemas to centralize your data without being limited to your current storage.

Big data standard schemas | Data lakehouse

You can create your own big data table using Apache Iceberg, Delta Lake, Hudi, etc.

This approach gives you a better way to store your data (I recommend Delta Lake by Databricks).

You can also integrate any data lake you already have with big data tables!

Better storage cost

  • You can cut your disk usage by 90% with Clickhouse while maintaining the same (or better) performance, thanks to its built-in compression.
  • 90% of disk usage reduction, are you crazy, this means your 1 Petabyte storage will be 100 terabyte usage storage!

More control

  • You have full control over your data — Clickhouse is like PostgreSQL, but built for your dreams.

More, more, more

  • Reading the Clickhouse documentation is a piece of heaven — you will discover functionalities you never knew existed!
  • WARNING: You could become addicted!

Ok, Clickhouse is better — but why Clickdetect over ElastAlert?

I built Clickdetect to be as generic as possible. You can even use Clickdetect with Elasticsearch if you are not ready to switch to Clickhouse as your datasource.

Datasource integrations

  • Elasticsearch/Opensearch: Yes, Clickdetect has integration with it.
  • Clickhouse: Of course.
  • Loki: Grafana Loki datasource integration — great if you want to replace Loki or the Grafana alerting engine.
  • VictoriaLogs: VictoriaLogs can match the performance of Elasticsearch and Clickhouse. I simply didn't choose it as the primary datasource because it is still very recent.
  • PostgreSQL: PostgreSQL integration — you can search through your database. If you use TimescaleDB or TigerData, this works great too.
  • Databricks (not implemented yet, but on my roadmap).

Webhooks

  • Generic: Generic integration — send to any webhook, including N8N.
  • DFIR Iris: Send alerts to DFIR Iris.
  • Forgejo/Gitea: Create issues from your alerts.
  • Email: Send alerts via e-mail.
  • Microsoft Teams: Send alerts to Microsoft Teams.
  • Slack: Send alerts to Slack.
  • Telegram: Send alerts to a Telegram bot.
  • Whatever you want: The Clickdetect documentation will show you how to implement your own webhook — or just open an issue!

Runtime management

  • You can use the reload option in Clickdetect to hot-reload your rules whenever a new rule is added to disk.
  • API: Clickdetect has an API to manage it programmatically.

You write your rules

  • Clickdetect is built to be simple. You write your query, and Clickdetect handles the rest.

ElastAlert rules vs Clickdetect rules

Let's make this concrete. Here is the same detection — multiple failed logins in a short window — written in both tools. This example uses Elasticsearch/OpenSearch as the datasource, so if you are already an ElastAlert user, you can migrate without touching your stack.

ElastAlert

name: Multiple Failed Logins
type: frequency
index: wazuh-alerts-*
num_events: 15
timeframe:
  minutes: 5
filter:
  - term:
      rule.groups: "authentication_failed"
  - term:
      data.win.system.eventID: "4625"
alert:
  - slack
slack_webhook_url: "https://hooks.slack.com/services/your/webhook/url"

ElastAlert owns the query logic. You configure what you want to detect through its abstraction layer — frequency, spike, flatline — and it builds the query for you. That works until you need something it does not support.

Clickdetect

runner.yml — your datasource and detector configuration:

datasource:
    type: elasticsearch
    url: http://localhost:9200
    index: wazuh-alerts-*
    username: elastic
    password: changeme

webhooks:
    slack:
        type: slack
        url: "https://hooks.slack.com/services/your/webhook/url"

detectors:
    5m_detector:
        for: "5m"
        rules:
            - "rules/windows/*.yml"
        webhooks:
            - slack

rules/windows/multiple_failed_logins.yml — the rule itself:

id: "a1b2c3d4-0000-0000-0000-000000000001"
name: "Windows - Multiple Authentication Failures"
level: 12
size: ">0"
active: true
rule: |-
    {
        "size": 0,
        "query": {
            "bool": {
                "filter": [
                    { "term": { "rule.groups": "authentication_failed" } },
                    { "term": { "data.win.system.eventID": "4625" } },
                    { "range": { "@timestamp": { "gte": "now-5m" } } }
                ]
            }
        },
        "aggs": {
            "by_user": {
                "terms": { "field": "data.win.eventdata.targetUserName", "min_doc_count": 15 }
            }
        }
    }

With Clickdetect, you write the query directly in Elasticsearch Query DSL. No abstraction layer, no hidden magic. If Elasticsearch can do it, Clickdetect can detect it.


The migration path is straightforward: keep your Elasticsearch/OpenSearch stack, swap ElastAlert for Clickdetect, and start writing your own queries. When you are ready to move to Clickhouse, it is just a datasource change in runner.yml.

Conclusion

If you made it this far, you now know why I built Clickdetect and what it can do for you.

Clickdetect is open source, actively maintained, and designed to grow with your infrastructure — whether you are running Elasticsearch today or planning a full move to Clickhouse tomorrow.

If this project helped you or sounds promising, please give it a star on GitHub — it means a lot and helps the project reach more people in the security community.

⭐ Star Clickdetect on GitHub

Have questions, ideas, or want to contribute? Open an issue or a pull request — the door is always open.

Leveraging Wazuh detection and alerting with Clickdetect

Hey, souzo here. In this blog post I'll show you how to extend Wazuh's detection and alerting capabilities using Clickdetect.

Wazuh is a fantastic open-source security platform — it collects logs from agents, parses events, and fires alerts based on its built-in rule engine. However, once you start running it at scale or need more sophisticated detection logic, you quickly hit a wall. The rule engine is static, alert correlation is limited to a single source at a time, and there is no native support for anomaly detection or complex aggregations.

That's where Clickdetect comes in. By storing Wazuh alerts in ClickHouse, we unlock the full power of SQL to build detections that simply aren't possible inside Wazuh alone. If you're not familiar with how to set up ClickHouse and Clickdetect together, check out my previous post: Building a powerful SIEM with ClickHouse and Clickdetect.

Detections like:

  • Multiple source correlation — correlate events across different log sources (e.g., firewall + EDR) in a single query
  • Anomaly detection — detect unusual patterns using statistical functions and time-window aggregations
  • Threshold-based detection — count events per user, IP, or host over sliding time windows
  • Batch alerting — group related alerts before sending notifications, reducing alert fatigue

Why?

Detection engine

Wazuh's (OSSEC-based) alert engine has several fundamental limitations by design. Wazuh 5 may address some of them, but it looks like it will remain tied to OpenSearch.

What Wazuh's alert engine can't do:

  1. Anomaly detection
  2. Multiple source correlation
  3. Complex queries and log searches
  4. Threshold-based detection
  5. Multi-tenant queries
  6. Sending alerts to third-party platforms

There is also the alert fatigue problem: Wazuh sends each alert individually. Clickdetect can batch related alerts into a single notification, dramatically reducing noise.

SQL detections

All queries below run against ClickHouse tables populated with Wazuh alerts. Each detection is scheduled as a Clickdetect rule and fires an alert whenever results are returned.

Multiple source correlation

Users authenticating from too many distinct IPs (Fortigate VPN)

This query detects users who authenticated through the Fortigate VPN from more than 5 distinct source IPs in the last 20 minutes. A single user connecting from many different IPs in a short window is a strong indicator of credential stuffing or a compromised account being used across multiple locations.

SELECT 
    alert.data.srcuser::String as srcuser, 
    count(DISTINCT alert.data.srcip) as unique_ip
FROM wazuh_alerts
PREWHERE timestamp >= now() - INTERVAL 20 MINUTE
WHERE has(rule_groups, 'fortigate')
GROUP BY srcuser
HAVING unique_ip > 5
LIMIT 100
CrowdStrike endpoint alerts

This query surfaces the most recent CrowdStrike endpoint detections ingested via Wazuh, returning the alert description and the local IP of the affected host. Useful for correlating EDR detections with other log sources like firewall or authentication logs.

SELECT
    rule_description,
    alert.data.event.LocalIP::String as LocalIP,
    alert.data.event.Description::String as Description
FROM
    wazuh_alerts_dist
PREWHERE timestamp >= now() - INTERVAL 10 MINUTE
WHERE has(rule_groups, 'crowdstrike')
LIMIT 10
SSH brute force followed by successful login

This query correlates two types of events on the same host: a burst of SSH authentication failures followed by a successful login — a classic brute force pattern. Both events must happen within the same 10-minute window on the same agent to trigger the alert.

SELECT
    agent_name,
    countIf(rule_id = '5710') as failed_logins,
    countIf(rule_id = '5715') as successful_logins
FROM wazuh_alerts
PREWHERE timestamp >= now() - INTERVAL 10 MINUTE
WHERE has(rule_groups, 'syslog') AND has(rule_groups, 'authentication')
GROUP BY agent_name
HAVING failed_logins >= 10 AND successful_logins >= 1
LIMIT 100
New admin user created after a privilege escalation alert

This query looks for hosts where a privilege escalation alert (e.g., a sudo rule) and a user account creation event both occurred in the last 15 minutes. The combination of these two signals on the same host is a strong indicator of post-exploitation activity.

SELECT
    agent_name,
    countIf(has(rule_groups, 'sudo')) as sudo_events,
    countIf(has(rule_groups, 'account_changes')) as account_changes
FROM wazuh_alerts
PREWHERE timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY agent_name
HAVING sudo_events >= 1 AND account_changes >= 1
LIMIT 50
Lateral movement: same source IP hitting multiple hosts

This query identifies source IPs that have triggered authentication alerts on more than 3 distinct agents in the last 10 minutes. A single IP probing many hosts in a short window is a strong indicator of lateral movement or internal scanning.

SELECT
    alert.data.srcip::String as srcip,
    count(DISTINCT agent_name) as targeted_hosts,
    groupArray(DISTINCT agent_name) as hosts
FROM wazuh_alerts
PREWHERE timestamp >= now() - INTERVAL 10 MINUTE
WHERE has(rule_groups, 'authentication_failed')
  AND alert.data.srcip::String != ''
GROUP BY srcip
HAVING targeted_hosts > 3
ORDER BY targeted_hosts DESC
LIMIT 50

Anomaly detection

Spike in rule triggers

This query counts how many times each rule fired in the last 5 minutes and compares it against the same rule's volume in the previous hour. Rules where the recent count is more than 3x the historical average are flagged — catching alert storms from malware spreading, scanning activity, or a misconfigured agent before they flood your notification channels.

SELECT
    rule_id,
    rule_description,
    countIf(timestamp >= now() - INTERVAL 5 MINUTE) AS recent_count,
    countIf(timestamp < now() - INTERVAL 5 MINUTE) / 11 AS baseline_avg,
    round(recent_count / nullIf(baseline_avg, 0), 2) AS spike_ratio
FROM wazuh_alerts
PREWHERE timestamp >= now() - INTERVAL 1 HOUR
GROUP BY rule_id, rule_description
HAVING recent_count >= 5 AND spike_ratio > 3
ORDER BY spike_ratio DESC
LIMIT 50
Agents that stopped sending alerts

Silence can be just as suspicious as noise. This query returns agents that were active in the last hour but have sent no alerts in the last 10 minutes — which may indicate a downed agent, a network issue, or an attacker disabling the Wazuh service.

SELECT DISTINCT agent_name
FROM wazuh_alerts
WHERE timestamp >= now() - INTERVAL 1 HOUR
  AND agent_name NOT IN (
      SELECT DISTINCT agent_name
      FROM wazuh_alerts
      WHERE timestamp >= now() - INTERVAL 10 MINUTE
  )
ORDER BY agent_name
LIMIT 100

SQL Considerations

When querying large time intervals — 1 hour, 1 day, or more — scanning raw alert tables on every run can become expensive. A refreshable materialized view that pre-aggregates the data is usually a better choice.

Here is an example for the Spike detection use case:

CREATE TABLE IF NOT EXISTS hour_update_table_spike_data
(
    window_start DateTime,
    rule_id      String,
    rule_description String,
    alert_count  UInt64
)
ENGINE = MergeTree()
ORDER BY (window_start, rule_id);

CREATE MATERIALIZED VIEW IF NOT EXISTS hour_update_table_spike_data_mv
REFRESH EVERY 1 HOUR TO hour_update_table_spike_data AS
SELECT
    toStartOfHour(now())  AS window_start,
    rule_id,
    rule_description,
    count()               AS alert_count
FROM wazuh_alerts
WHERE timestamp >= now() - INTERVAL 1 HOUR
GROUP BY rule_id, rule_description;

With the materialized view in place, the spike detection rule can be rewritten to query the pre-aggregated table instead of scanning raw alerts:

SELECT
    rule_id,
    rule_description,
    recent.alert_count AS recent_count,
    round(avg(h.alert_count), 2) AS baseline_avg,
    round(recent.alert_count / nullIf(avg(h.alert_count), 0), 2) AS spike_ratio
FROM (
    SELECT rule_id, rule_description, alert_count
    FROM hour_update_table_spike_data
    WHERE window_start = toStartOfHour(now())
) AS recent
JOIN hour_update_table_spike_data AS h USING (rule_id)
WHERE h.window_start >= toStartOfHour(now()) - INTERVAL 24 HOUR
  AND h.window_start < toStartOfHour(now())
GROUP BY rule_id, rule_description, recent.alert_count
HAVING recent_count >= 5 AND spike_ratio > 3
ORDER BY spike_ratio DESC
LIMIT 50

Using these detections with Clickdetect

Each SQL query above becomes a Clickdetect rule — a YAML file that wraps the query with metadata, a firing condition, and severity information. Clickdetect evaluates the rule on a schedule and fires an alert to your configured webhook whenever the query returns results.

Rule file structure

Take the brute force + successful login detection as an example:

id: "wazuh-001"
name: SSH Brute Force Followed by Successful Login
level: 9
size: ">0"
active: true
author:
    - souzo
group: wazuh
tags:
    - brute_force
    - authentication
    - ssh
description: "Detects hosts with 10+ SSH failures followed by a successful login in the last 10 minutes"
rule: |-
    SELECT
        agent_name,
        countIf(rule_id = '5710') as failed_logins,
        countIf(rule_id = '5715') as successful_logins
    FROM wazuh_alerts
    PREWHERE timestamp >= now() - INTERVAL 10 MINUTE
    WHERE has(rule_groups, 'syslog') AND has(rule_groups, 'authentication')
    GROUP BY agent_name
    HAVING failed_logins >= 10 AND successful_logins >= 1
    LIMIT 100

The size: ">0" condition means the alert fires as soon as the query returns at least one row. For detections where you want a higher threshold before alerting (e.g., only fire if 5 or more agents are affected), you can use size: ">=5".

Organizing rules by detector

A good practice is to group rules by their evaluation interval. Rules that query short time windows (5–10 minutes) should run frequently; rules with longer windows (1 hour) can run less often.

# runner.yml
datasource:
    type: clickhouse
    host: clickhouse
    port: 8123
    username: default
    password: ""
    database: default

webhooks:
    security_alerts:
        type: generic
        url: http://your-alerting-platform/webhook
    teams_integration:
        type: teams
        url: https://xyz.webhook.office.com/...

detectors:
    wazuh_5m:
        name: "Wazuh  5 minute checks"
        for: "5m"
        tenant: "default"
        rules:
            - "rules/wazuh/5m/*.yml"
        webhooks:
            - security_alerts
            - teams_integration

    wazuh_10m:
        name: "Wazuh  10 minute checks"
        for: "10m"
        tenant: "default"
        rules:
            - "rules/wazuh/10m/*.yml"
        webhooks:
            - security_alerts

    wazuh_1h:
        name: "Wazuh  hourly checks"
        for: "1h"
        tenant: "default"
        rules:
            - "rules/wazuh/1h/*.yml"
        webhooks:
            - security_alerts

A suggested layout for the rules directory:

rules/
└── wazuh/
    ├── 5m/
    │   └── spike_detection.yml
    ├── 10m/
    │   ├── ssh_brute_force.yml
    │   ├── lateral_movement.yml
    │   └── privilege_escalation.yml
    └── 1h/
        └── silent_agents.yml

Using startime and endtime for precise windows

Instead of using now() directly in your queries, you can use the {{ startime }} and {{ endtime }} template variables that Clickdetect injects at runtime. These are Unix timestamps corresponding to the detector's evaluation window, which avoids clock drift between query execution and the intended time range.

rule: |-
    SELECT
        alert.data.srcip::String as srcip,
        count(DISTINCT agent_name) as targeted_hosts,
        groupArray(DISTINCT agent_name) as hosts
    FROM wazuh_alerts
    WHERE timestamp >= fromUnixTimestamp({{ startime }})
      AND timestamp <= fromUnixTimestamp({{ endtime }})
      AND has(rule_groups, 'authentication_failed')
      AND alert.data.srcip::String != ''
    GROUP BY srcip
    HAVING targeted_hosts > 3
    ORDER BY targeted_hosts DESC
    LIMIT 50

Tuning thresholds per environment

If your thresholds need to vary between environments or tenants, use the data field on the rule to make them configurable without duplicating the YAML:

id: "wazuh-002"
name: VPN User Authenticating from Too Many IPs
description: "User authenticated from more than {{ rule.data.max_ips }} distinct IPs via Fortigate VPN"
level: 7
size: ">0"
group: wazuh
tags:
    - vpn
    - credential_stuffing
data:
    max_ips: 5
rule: |-
    SELECT
        alert.data.srcuser::String as srcuser,
        count(DISTINCT alert.data.srcip) as unique_ip
    FROM wazuh_alerts
    PREWHERE timestamp >= fromUnixTimestamp({{ startime }})
    WHERE has(rule_groups, 'fortigate')
    GROUP BY srcuser
    HAVING unique_ip > {{ rule.data.max_ips }}
    LIMIT 100

Note: You can use Jinja template in description like the example above. I added this feature while writing this post :)

Conclusion

Wazuh is a solid foundation for endpoint and log visibility, but its detection engine was never designed for the kind of analytical workloads that modern threat detection demands. By routing Wazuh alerts into ClickHouse and wrapping SQL queries with Clickdetect rules, you get a detection layer that scales, correlates across sources, and sends batched notifications to any platform you already use.

The patterns shown here — brute force correlation, lateral movement detection, silent agent monitoring, and spike detection — are just a starting point. Any behavior you can express as a SQL query over time-windowed data can become a Clickdetect rule.

If you have questions or want to share a detection you built, feel free to open an issue or discussion on the Clickdetect repository.

Building a powerful SIEM with Clickhouse and Clickdetect

Hi everyone, souzo here. In this blog post I will walk you through building a base SIEM architecture capable of generating security alerts.

This post will not cover how to collect data into Clickhouse. Instead, I will focus on a base table schema for receiving logs and performing detection with Clickdetect.

In a future post, I will show you how to use Wazuh to send data to any datasource and leverage Clickdetect to power Wazuh detections.

Why Clickhouse instead ElasticSearch

I'm not sponsored by Clickhouse, but I love this database, you can do everything you want and use it in any situation.

I thought about this many times, and this is why I prefer Clickhouse instead of ElasticSearch for log management.

  1. Log data is parsed and decoded, so logs can be stored as JSON. Clickhouse is awesome at searching JSON data: look at this post;
  2. Clickhouse can decrease disk usage by 90% and deliver the same or better performance than Elastic/OpenSearch;
  3. Clickhouse can perform full-text search and you can create better indexes for logs;
  4. More control of data. You can do anything in clickhouse and use it for any situation you might have;
  5. Scalability. Take a look in distributed table;
  6. Storage
    • You can use hybrid storage like Host/Amazon S3 or only Host or S3;
    • You can encrypt data to be compliance;
    • Tables in clickhouse are compressed by default
  7. SQL ( Do I need to say anything? )

Companies that use Clickhouse

  1. Huntress
  2. RunReveal
  3. Exabeam
  4. Fortinet (FortiSIEM)
  5. Cloudflare

Architecture

A basic architecture how this will work.

Diagram overview

  1. Logs are sent by assets to wazuh;
  2. Wazuh decodes and send parsed logs to Clickhouse;
  3. Clickdetect will be scheduled to detect and generate alerts;
  4. Alerts are sent to a webhook.

Clickhouse Wazuh Schema

This schema is for Wazuh Alerts. In this case, Wazuh is only a log collector — we will only need Wazuh's decoder capabilities.

Database
CREATE DATABASE IF NOT EXISTS siem
Table
CREATE TABLE IF NOT EXISTS siem.wazuh_alerts (
    id UUID default generateUUIDv7() CODEC(ZSTD(1)),
    timestamp DateTime64(6) DEFAULT now() CODEC(DoubleDelta),
    retention UInt16 DEFAULT 30,
    tenant LowCardinality(String) CODEC(ZSTD(1)),
    rule_id UInt32 CODEC(Delta(8), ZSTD(1)),
    rule_description String CODEC(ZSTD(1)),
    rule_groups Array(LowCardinality(String)) CODEC(ZSTD(1)),
    rule_level UInt8,
    agent_id UInt16,
    agent_name String CODEC(ZSTD(1)),
    manager LowCardinality(String) CODEC(ZSTD(1)),
    agent_ip String CODEC(ZSTD(1)),
    full_log String CODEC(ZSTD(22)),
    message String CODEC(ZSTD(22)),
    srcuser String CODEC(ZSTD(1)),
    dstuser String CODEC(ZSTD(1)),
    srcip String CODEC(ZSTD(1)),
    dstip String CODEC(ZSTD(1)),
    hostname String CODEC(ZSTD(1)),
    location String CODEC(ZSTD(1)),
    decoder LowCardinality(String) CODEC(ZSTD(1)),
    action LowCardinality(String) CODEC(ZSTD(1)),
    protocol LowCardinality(String) CODEC(ZSTD(1)),
    status LowCardinality(String) CODEC(ZSTD(1)),
    alert JSON CODEC(ZSTD(1)),

    INDEX idx_full_log full_log TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1,
    INDEX idx_rule_description rule_description TYPE tokenbf_v1(8192, 3, 0) GRANULARITY 1,
    INDEX idx_rule_groups rule_groups TYPE bloom_filter(0.01) GRANULARITY 1
)
engine = MergeTree
partition by (toYYYYMMDD(timestamp), tenant)
order by (tenant, toUnixTimestamp(timestamp), id)
TTL timestamp + toIntervalDay(retention)
settings
    index_granularity = 4096,
    ttl_only_drop_parts = 1,
    storage_policy = 'your_s3_policy'

Performing detection with clickdetect

To perform detection in clickhouse, we need to create our runner

Runner

Define datasource
datasource:
    type: clickhouse
    host: <clickhouse ip>
    port: 8123
    verify: false
    username: default
    password: default
    database: siem
Define webhook
webhooks:
    webhook_name:
        type: generic
        url: http://<webhook_host>/
Define detector
detectors:
    detector_N:
        name: "Detector name"
        description: "Detector description"
        for: 5m # detector time (s, m, h, d)
        rules:
            - "detect_test.yml" # you can use * for match directory
        data:
            var1: "my var"

Rule

id: "00000000-0000-0000-0000-000000000000"
name: "Detect all data in clickhouse"
level: 1
size: ">0"
author: 
    - Vinicius Morais <[email protected]>
rule: |-
    SELECT * FROM wazuh_alerts LIMIT 100;

Clickdetect

uv run clickdetect -r runner.yml

Conclusion

With just a Clickhouse table, a runner configuration, and a detection rule, you have the foundation of a functional SIEM. This architecture is lightweight, cost-effective, and scales well — whether you're running it on a single node or a distributed Clickhouse cluster.

The key advantage over traditional SIEM solutions is control: you own the data, you define the schema, and you write the detections in plain SQL. There are no vendor lock-ins, no per-GB ingestion fees, and no black-box detection engines.

Rule: more rules examples you can found here

Next Steps

This post covered the base architecture. Here is what comes next:

  • Part 2 — Wazuh + Clickdetect: How to configure Wazuh to forward decoded alerts directly to Clickhouse, and how to write detection rules that leverage Wazuh's decoded fields.
  • Alerting pipelines: Routing alerts to Slack, PagerDuty, or a ticketing system using Clickdetect webhooks.
  • Multi-tenancy: Using the tenant field to isolate data between clients or business units in a single Clickhouse cluster.

Follow along on GitHub and feel free to open issues or contribute detection rules.