Lewati ke konten utama
Panduan Implementasi Wazuh sebagai Solusi SIEM Open-Source
  1. Semua Artikel/

Panduan Implementasi Wazuh sebagai Solusi SIEM Open-Source

·19 menit
 Author
Penulis
Hasbi Mizan Azzami
DevOps Enthusiast

Wazuh adalah platform security open-source yang memiliki kapabilitas sebagai Security Information and Event Management (SIEM).

Sebagai SIEM, Wazuh bisa menjadi tempat penyimpanan dan pengolahan log yang tersentralisasi pada sistem SMSCrops, baik itu log dari sisi server maupun database. Log tersebut menyimpan informasi yang dianalisa Wazuh secara realtime untuk mendeteksi adanya security event.

Wazuh memiliki kapabilitas untuk meningkatkan visibility terhadap security event pada sistem SMSCrops. Kapabilitas tersebut didukung oleh fitur-fitur berikut:

  1. Malware Detection Untuk mendeteksi file atau proses berbahaya di dalam server.
  2. Vulnerability Detection Untuk mendeteksi celah keamanan pada package dan software di dalam server.
  3. Security Configuration Assessment Untuk mendeteksi miskonfigurasi dan memberikan saran remediasi sesuai best practices.
  4. Docker Monitoring Untuk memantau malicious activity pada docker container.
  5. File Integrity Monitoring (FIM) Untuk mendeteksi adanya pembuatan, modifikasi, atau penghapusan file.
  6. Who-Data Untuk mencatat pelaku yang membuat/memodifikasi/menghapus suatu file.
  7. Database Monitoring Untuk memantau eksekusi query pada database seperti MySQL.
  8. Email Alerting Untuk mengirimkan alert via email secara otomatis jika terdapat security event.

Dengan adanya kedelapan fitur Wazuh tersebut, sistem SMSCrops bisa mendapatkan benefit sebagai berikut:

  • Mean Time to Detect (MTTD) dan Mean Time to Respond (MTTR) yang jauh lebih baik. Sebagai contoh, Wazuh dapat mendeteksi secara realtime query mencurigakan atau malicious file yang diupload ke server, dan segera mengirimkan alert ke tim terkait untuk segera ditangani.
  • Forensik yang lebih baik, secara Wazuh mencatat what, who, when, where, dan how terkait suatu security event.
  • Mengurangi attack surface dari segi teknologi dengan adanya rekomendasi system hardening dan patching dari Wazuh.
  • Visibility dan traceability yang lebih baik terhadap security event. Wazuh mencatat setiap security event secara tersentralisasi, yang sangat membantu untuk menemukan attack pattern.

Prasyarat Wazuh
#

Kebutuhan CPU & RAM
#

KomponenSpek MinimumSpek Rekomendasi
Wazuh Indexer2vCPU 4GB RAM8vCPU 16GB RAM
Wazuh Server2vCPU 2GB RAM4vCPU 8GB RAM
Wazuh Dashboard2vCPU 4GB RAM4vCPU 8GB RAM
Wazuh Agent--

Wazuh agent hanya mengonsumsi 35mb ram on average untuk beroperasi

Wazuh memberikan rekomendasi spesifikasi 4vCPU 8GB RAM untuk menajalankan ketiga komponen dalam satu server. Rekomendasi tersebut memiliki kapasitas berikut:

  • Monitoring up to 100 endpoints
  • Storing and indexing last 90 days logs
  • Handling up to 25 agents

Kebutuhan Penyimpanan Disk
#

Program Wazuh server, Wazuh indexer, dan Wazuh dashboard membutuhkan +-16GB ruang penyimpanan di dalam disk.

Jumlah ruang penyimpanan yang dibutuhkan untuk menyimpan log tergantung pada generated alerts per second (APS) dan jenis endpoint yang dimonitor.

  • Wazuh indexer

Untuk setiap endpoint berjenis server (0.25 APS), storage yang dibutuhkan untuk menyimpan data di indexer selama 90 hari adalah 3.7GB.

Penyimpanan indexer (per server/90 hari)Jumlah serverTotal
3.7GB311.1GB
  • Wazuh server

Untuk setiap endpoint berjenis server (0.25 APS), storage yang dibutuhkan untuk menyimpan data di indexer selama 90 hari adalah 0.1GB.

Penyimpanan indexer (per server/90 hari)Jumlah serverTotal
0.1GB30.3 GB

Sebagai contoh, untuk menjalankan sistem Wazuh setidaknya selama 1 tahun, maka diperlukan penyimpanan sebanyak:

(11.1GB + 0.3GB) x 4 + 16GB = 61.6GB

Kebutuhan Sistem Operasi
#

Komponen wazuh membutuhkan 64-bit Intel, AMD, atau ARM Linux processor (x86_64/AMD64 atau AARCH64/ARM64) to run. Wazuh merekomendasikan beberapa sistem operasi berikut:

  • Amazon Linux 2, Amazon Linux 2023
  • CentOS Stream 10
  • Red Hat Enterprise Linux 7, 8, 9, 10
  • Ubuntu 16.04, 18.04, 20.04, 22.04, 24.04

Instalasi Wazuh Manager
#

Wazuh menyediakan preconfigured Docker compose untuk memudahkan proses instalasi, baik itu untuk deployment single-node maupun multi-node. Oleh karena itu, Wazuh manager akan dipasang sebagai Docker container pada dokumentasi ini.

Wazuh indexer butuh untuk membuat banyak virtual memory area (VMA). VMA digunakan oleh Wazuh indexer untuk mengakses file pada disk tanpa harus memindahkan file tersebut ke dalam RAM terlebih dahulu. Maka dari itu, jumlah VMA pada host harus diperbesar dari 65530 menjadi 262144.

sudo sysctl -w vm.max_map_count=262144

Example output:

image.png

Clone repositori wazuh-docker dengan versi spesifik. Versi terbaru saat dokumentasi ini dibuat adalah v4.14.3.

git clone https://github.com/wazuh/wazuh-docker.git -b v4.14.3

Example output:

image.png

Working directory untuk melakukan deployment Wazuh manager single-node ada di folder bernama single-node.

cd wazuh-docker/single-node

Example output:

image.png

Generate self-signed certificate untuk SSL, karena komunikasi antar komponen Wazuh dilakukan menggunakan protokol yang secure. Termasuk dashboard yang akan digunakan nantinya diakses menggunakana HTTPS.

docker compose -f generate-indexer-certs.yml run --rm generator

image.png

Setelah selesai generate self-signed certificate, bisa langsung docker compose up.

docker compose up -d

image.png

Setelah semua container up and running, butuh waktu 1-3 menit untuk Wazuh indexer selesai diinisiasi dan siap untuk beroperasi. Akses IP server menggunakan protokol https pada port yang telah ditentukan (defaultnya 443). Kredensial default untuk Wazuh dashboard adalah admin:SecretPassword.

image.png

Instalasi Wazuh Agent
#

Server yang akan dimonitor harus dipasangi Wazuh agent untuk mengumpulkan data-data yang dibutuhkan dan kemudian diforward ke Wazuh manager.

Meskipun Wazuh menyediakan preconfigured Docker compose untuk deployment agent, Wazuh agent akan mendapatkan visibilitas yang baik pada server host jika dideploy as a service. Wazuh agent memiliki beberapa keterbatasan jika dideploy sebagai docker container.

Pertama-tama, daftarkan repositori Wazuh pada package manager host.

sudo apt install gnupg apt-transport-https
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | sudo gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import && sudo chmod 644 /usr/share/keyrings/wazuh.gpg
echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" | sudo tee -a /etc/apt/sources.list.d/wazuh.lis

Example output:

image.png

Instal package wazuh-agent ditambah dengan membuat environment variable WAZUH_MANAGER yang berisi IP address dari Wazuh manager.

WAZUH_MANAGER="x.x.x.x" sudo -E apt install wazuh-agent

Example output:

image.png

Sebelum mengaktifkan service Wazuh agent, sesuaikan konfigurasinya terlebih dahulu. File konfigurasi Wazuh agent secara default terdapat di /var/ossec/etc/ossec.conf.

sudo nano /var/ossec/etc/ossec.conf

Contoh konfigurasi dapat dilihat pada bagian Contoh Konfigurasi Default Wazuh Agent untuk Ubuntu 22.04 di bawah.

Aktifkan dan jalankan service wazuh agent untuk mulai menginisiasi komunikasi dengan Wazuh manager.

sudo systemctl daemon-reload
sudo systemctl enable wazuh-agent
sudo systemctl start wazuh-agent

Example output:

image.png

Contoh Konfigurasi Default Wazuh Agent untuk Ubuntu 22.04
#

Berikut adalah contoh konfigurasi ossec.conf di server Ubuntu 22.04 (sesuaikan MANAGER_IP terlebih dahulu):

<!--
  Wazuh - Agent - General Configuration for Ubuntu 22.04 LTS (Jammy Jellyfish)
-->

<ossec_config>

  <client>
    <server>
      <address>MANAGER_IP</address>
      <port>1514</port>
      <protocol>tcp</protocol>
    </server>
    <config-profile>ubuntu, ubuntu22, ubuntu2204</config-profile>
    <notify_time>20</notify_time>
    <time-reconnect>60</time-reconnect>
    <auto_restart>yes</auto_restart>
    <crypto_method>aes</crypto_method>
  </client>

  <client_buffer>
    <disabled>no</disabled>
    <queue_size>5000</queue_size>
    <events_per_second>500</events_per_second>
  </client_buffer>

  <rootcheck>
    <disabled>no</disabled>
    <check_files>yes</check_files>
    <check_trojans>yes</check_trojans>
    <check_dev>yes</check_dev>
    <check_sys>yes</check_sys>
    <check_pids>yes</check_pids>
    <check_ports>yes</check_ports>
    <check_if>yes</check_if>

    <frequency>43200</frequency>

    <rootkit_files>etc/shared/rootkit_files.txt</rootkit_files>
    <rootkit_trojans>etc/shared/rootkit_trojans.txt</rootkit_trojans>

    <skip_nfs>yes</skip_nfs>
  </rootcheck>

  <wodle name="cis-cat">
    <disabled>yes</disabled>
    <timeout>1800</timeout>
    <interval>1d</interval>
    <scan-on-start>yes</scan-on-start>
    <java_path>wodles/java</java_path>
    <ciscat_path>wodles/ciscat</ciscat_path>
  </wodle>

  <wodle name="osquery">
    <disabled>yes</disabled>
    <run_daemon>yes</run_daemon>
    <log_path>/var/log/osquery/osqueryd.results.log</log_path>
    <config_path>/etc/osquery/osquery.conf</config_path>
    <add_labels>yes</add_labels>
  </wodle>

  <wodle name="syscollector">
    <disabled>no</disabled>
    <interval>1h</interval>
    <scan_on_start>yes</scan_on_start>
    <hardware>yes</hardware>
    <os>yes</os>
    <network>yes</network>
    <packages>yes</packages>
    <ports all="yes">yes</ports>
    <processes>yes</processes>
    <users>yes</users>
    <groups>yes</groups>
    <services>yes</services>

    <synchronization>
      <max_eps>10</max_eps>
    </synchronization>
  </wodle>

  <sca>
    <enabled>yes</enabled>
    <scan_on_start>yes</scan_on_start>
    <interval>12h</interval>
    <skip_nfs>yes</skip_nfs>
  </sca>

  <syscheck>
    <disabled>no</disabled>
    <frequency>43200</frequency>
    <scan_on_start>yes</scan_on_start>

    <!-- Direktori sistem dasar — relevan untuk semua Ubuntu 22.04 -->
    <directories realtime="yes">/etc,/usr/bin,/usr/sbin</directories>
    <directories realtime="yes">/bin,/sbin,/boot</directories>
    <directories realtime="yes">/root</directories>
    <directories realtime="yes">/var/spool/cron</directories>

    <ignore>/etc/mtab</ignore>
    <ignore>/etc/hosts.deny</ignore>
    <ignore>/etc/mail/statistics</ignore>
    <ignore>/etc/random-seed</ignore>
    <ignore>/etc/random.seed</ignore>
    <ignore>/etc/adjtime</ignore>
    <ignore>/etc/httpd/logs</ignore>
    <ignore>/etc/utmpx</ignore>
    <ignore>/etc/wtmpx</ignore>
    <ignore>/etc/cups/certs</ignore>
    <ignore>/etc/dumpdates</ignore>
    <ignore>/etc/svc/volatile</ignore>
    <ignore>/etc/apparmor.d/cache.d</ignore>

    <ignore type="sregex">.log$|.swp$</ignore>

    <nodiff>/etc/ssl/private.key</nodiff>

    <skip_nfs>yes</skip_nfs>
    <skip_dev>yes</skip_dev>
    <skip_proc>yes</skip_proc>
    <skip_sys>yes</skip_sys>

    <process_priority>10</process_priority>
    <max_eps>50</max_eps>

    <synchronization>
      <enabled>yes</enabled>
      <interval>5m</interval>
      <max_eps>10</max_eps>
    </synchronization>
  </syscheck>

  <active-response>
    <disabled>no</disabled>
    <ca_store>etc/wpk_root.pem</ca_store>
    <ca_verification>yes</ca_verification>
  </active-response>

  <logging>
    <log_format>plain</log_format>
  </logging>

</ossec_config>

<ossec_config>

  <localfile>
    <log_format>journald</log_format>
    <location>journald</location>
  </localfile>

  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/syslog</location>
  </localfile>

  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/auth.log</location>
  </localfile>

  <localfile>
    <log_format>syslog</log_format>
    <location>/var/ossec/logs/active-responses.log</location>
  </localfile>

  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/dpkg.log</location>
  </localfile>

</ossec_config>

Konfigurasi ini sudah mencakup hal-hal berikut:

  1. Koneksi ke Wazuh manager, termasuk IP address, port, enkripsi yang dipakai, dan konfigurasi buffering koneksi.
  2. Threat detecting configuration, yaitu rootcheck untuk mendeteksi rootkit, trojan, file mencurigakan, port tersembunyi, dan perubahan di /dev dan /sys.
  3. Security Configuration Assessment (SCA) untuk mengecek apakah konfigurasi sistem sesuai best practices CIS benchmark.
  4. File Integrity Monitoring (FIM) untuk memantau perubahan file secara real-time pada folder-folder penting, yaitu /etc, /usr/bin, /usr/sbin, /bin, /sbin, /boot, /root, dan /var/spool/cron. File yang sering berubah seperti /etc/mtab, /etc/adjtime, file .log dan .swp diignore saja supaya lognya tidak overflow.
  5. Log monitoring dari journald, /var/log/syslog, /var/log/auth.log, /var/log/dpkg.log, dan active-response.log.
  6. Syscollector untuk inventarisasi hardware, OS, packet, port, proses, dan user yang ada pada server.

Panduan Konfigurasi Wazuh Agent
#

Konfigurasi sebelumnya masih dapat diextend untuk disesuaikan di setiap environment. Berikut hal-hal yang dapat ditambahkan pada konfigurasi Wazuh agent:

  1. Monitoring Log File

Gunakan elemen untuk memberitau Wazuh agent bahwa ada file log yang perlu dibaca dan dikirim ke Manager untuk dianalisis.

<localfile>
	<log_format>FORMAT</log_format>
	<location>/path/ke/file.log</location>
</localfile>

FORMAT bisa berupa syslog, json, apache, audit, multi-line, journald, command, dan full-command.

  1. Scheduled Command Execution

Gunakan dengan log_format bernilai command atau full_command. Wazuh akan menjalankan command ini secara berkala dan mengirim hasilnya ke Manager.

<localfile>
	<log_format>full_command</log_format>
	<command>COMMAND</command>
	<frequency>SECONDS</frequency>
</localfile>
  1. File Integrity Monitoring

Tambahkan elemen di dalam blok yang sudah ada untuk memonitor direktori tersebut menggunakan FIM.

<syscheck>
	<directories realtime="yes">/path/ke/direktori</directories>
</syscheck>
  1. Ignore Element

Tambahkan elemen di dalam blok atau supaya file/folder tersebut tidak dimonitoring oleh FIM atau rootcheck. Sehingga tidak terjadi false positive dari direktori yang isinya memang terus berubah seperti cache dan runtime data.

<syscheck>
	<ignore>/path/ke/file/direktori</ignore>
	<ignore type="sregex">REGEX</ignore>
</syscheck>

<rootcheck>
  <ignore>/path/ke/file/direktori</ignore>
  	<ignore type="sregex">REGEX</ignore>
</rootcheck>
  1. Wodle

Wodle adalah modul opsional Wazuh agent. Aktifkan dengan mengubah menjadi no.

<wodle name="docker-listener">
  <disabled>no</disabled>
</wodle>

<wodle name="osquery">
  <disabled>no</disabled>
  <run_daemon>yes</run_daemon>
  <log_path>/var/log/osquery/osqueryd.results.log</log_path>
  <config_path>/etc/osquery/osquery.conf</config_path>
  <add_labels>yes</add_labels>
</wodle>

<wodle name="cis-cat">
  <disabled>no</disabled>
  <timeout>1800</timeout>
  <interval>1d</interval>
  <scan-on-start>yes</scan-on-start>
  <java_path>wodles/java</java_path>
  <ciscat_path>wodles/ciscat</ciscat_path>
</wodle>
  1. Malware Detection

Tambahkan element pada konfigurasi agent untuk mengaktifkan malware detection. Berikut adalah contoh konfigurasi default rootcheck untuk Linux.

<rootcheck>
  <disabled>no</disabled>
  <check_unixaudit>yes</check_unixaudit>
  <check_files>yes</check_files>
  <check_trojans>yes</check_trojans>
  <check_dev>yes</check_dev>
  <check_sys>yes</check_sys>
  <check_pids>yes</check_pids>
  <check_ports>yes</check_ports>
  <check_if>yes</check_if>
  <ignore type="sregex">^/etc/</ignore>

  <frequency>43200</frequency>

  <rootkit_files>etc/shared/rootkit_files.txt</rootkit_files>
  <rootkit_trojans>etc/shared/rootkit_trojans.txt</rootkit_trojans>

  <skip_nfs>yes</skip_nfs>
</rootcheck>
  1. Security Configuration Assessment

Gunakan element untuk mengaktifkan modul configuration assessment. Berikut adalah contoh konfigurasi default untuk sca.

  <sca>
    <enabled>yes</enabled>
    <scan_on_start>yes</scan_on_start>
    <interval>12h</interval>
    <skip_nfs>yes</skip_nfs>
  </sca>

Konfigurasi FIM + Who-Data
#

Modul File Integrity Monitoring pada defaultnya tidak mencatat informasi tentang siapa yang melakukan modifikasi pada file yang dimonitor. Konfigurasi who-data membantu mencatat informasi user, program, dan proses yang terlibat dalam modifikasi suatu file.

Salah satu implementasi who-data adalah menggunakan audit mode, yaitu ekstensi FIM untuk Realtime Monitoring dengan Linux Audit subsystem. Konfigurasi ini dilakukan pada tiap wazuh agent.

Pertama, pasang package auditd terlebih dahulu.

sudo apt install auditd

Example output:

image.png

Pasang juga package plugin audispd af_unix melalui package audispd-plugins dan restart service auditd.

sudo apt install audispd-plugin

Example output:

image.png

Pada /var/ossec/etc/ossec.conf milik agent, tambahkan element provider dengan value audit di dalam element whodata. Bagian ini diletakkan di dalam element syscheck.

sudo nano /var/ossec/etc/ossec.conf 
<syscheck>

...

  <whodata>
    <provider>audit</provider>
  </whodata>

</syscheck>

Untuk setiap direktori yang akan dimonitor, tambahkan konfigurasi realtime=yes dan whodata=yes pada element directori

<directories realtime="yes" whodata="yes">/path/ke/direktori</directories>

Restart service wazuh agent untuk apply perubahan.

sudo systemctl restart wazuh-agent

Example output:

image.png

Gunakan auditctl untuk memverifikasi rules dari konfigurasi wazuh agent sudah masuk ke auditd. Apabila sudah sesuai, maka akan ditampilkan output “-w -p wa -k wazuh_fim” untuk setiap direktori yang dimonitor.

sudo auditctl -l | grep wazuh_fim

Example output:

image.png

Konfigurasi Email Alerting untuk FIM
#

Server yang disediakan oleh cloud provider biasanya tidak memperbolehkan traffic SMTP outbound. Alternatif yang dapat digunakan untuk mengirim email adalah external API integration. Wazuh mendukung beberapa external API secara native, seperti Slack, Virustotal, dll. Wazuh juga mendukung custom external API integration menggunakan script python.

Dokumentasi ini menggunakan Brevo sebagai Email Service Providernya karena mudah digunakan dan free limitnya banyak (300 email/hari). Pertama, buat akun di https://onboarding.brevo.com/account/register. Selesaikan proses pengisian data diri dan pilih free tier.

image.png

Kemudian, pada Settings > SMTP & API > API keys & MCP, pilih Generate a new API key. Kemudian generate dan simpan API key-nya untuk digunakana nanti.

image.png

Selanjutnya, buat file dengan nama custom-brevo yang berisi script untuk memparsing dan mengirim email ke API Brevo. Script untuk custom external API integration harus dimulai dengan prefic custom-. Letakkan scriptnya di folder wazuh-docker/single-node/config/wazuh_cluster/integrations/

cd wazuh-docker/single-node
sudo nano config/wazuh_cluster/integrations/custom-brevo

Berikut adalah contoh script custom-brevo. Jangan lupa untuk menyesuaikan EMAIL_BREVO, EMAIL_TUJUAN, dan NAMA_PENERIMA.

#!/usr/bin/env python3
import json
import sys
import time

from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

SENDER_EMAIL = "EMAIL_BREVO"
SENDER_NAME = "Wazuh Alert"

RECIPIENT_LIST = [
    {"email": "EMAIL_TUJUAN1", "name": "NAMA_PENERIMA"},
    {"email": "EMAIL_TUJUAN2", "name": "NAMA_PENERIMA"},
]

SUBJECT_PREFIX = "[Wazuh]"

LOG_FILE = "/var/ossec/logs/integrations.log"

def log(msg):
    timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    entry = "{} custom-brevo: {}\n".format(timestamp, msg)
    try:
        with open(LOG_FILE, "a") as f:
            f.write(entry)
    except Exception:
        pass

def read_alert(alert_file_path):
    try:
        with open(alert_file_path) as f:
            alert = json.load(f)
        return alert
    except Exception as e:
        log("ERROR: Gagal membaca alert file {}: {}".format(alert_file_path, e))
        sys.exit(1)

def build_subject(alert):
    rule = alert.get("rule", {})
    rule_level = rule.get("level", 0)
    description = rule.get("description", "Wazuh Alert")

    if rule_level >= 12:
        severity = "CRITICAL"
    elif rule_level >= 8:
        severity = "HIGH"
    elif rule_level >= 5:
        severity = "MEDIUM"
    else:
        severity = "LOW"

    return "{} [{}] {}".format(SUBJECT_PREFIX, severity, description)

def build_html(alert):
    rule = alert.get("rule", {})
    agent = alert.get("agent", {})
    syscheck = alert.get("syscheck", {})
    audit = syscheck.get("audit", {})

    timestamp = alert.get("timestamp", "-")
    rule_level = rule.get("level", 0)
    rule_desc = rule.get("description", "-")

    agent_name = agent.get("name", "-")
    agent_ip = agent.get("ip", "-")

    file_path = syscheck.get("path", "")
    file_event = syscheck.get("event", "")
    changed_attrs = syscheck.get("changed_attributes", [])
    size_before = syscheck.get("size_before", "")
    size_after = syscheck.get("size_after", "")
    diff = syscheck.get("diff", "")

    login_user = audit.get("login_user", {}).get("name", "")
    effective_user = audit.get("effective_user", {}).get("name", "")
    process_name = audit.get("process", {}).get("name", "")
    process_cwd = audit.get("process", {}).get("cwd", "")

    if rule_level >= 12:
        color = "#c0392b"
    elif rule_level >= 8:
        color = "#d35400"
    elif rule_level >= 5:
        color = "#e67e22"
    else:
        color = "#2980b9"

    ROW = '<tr{bg}><td style="padding:6px 12px;font-weight:bold;width:130px;color:#555;">{label}</td><td style="padding:6px 12px;">{value}</td></tr>'
    ZEBRA = ' style="background:#f9f9f9;"'
    SEPARATOR = '<tr><td colspan="2" style="border-top:1px solid #eee;padding:0;"></td></tr>'

    rows = []

    if file_path:
        rows.append(ROW.format(bg="", label="File", value="<code>{}</code>".format(file_path)))
        rows.append(ROW.format(bg=ZEBRA, label="Event", value=file_event))
        if changed_attrs:
            display_attrs = [a for a in changed_attrs if a not in ("md5", "sha1", "sha256")]
            if display_attrs:
                rows.append(ROW.format(bg="", label="Changed", value=", ".join(display_attrs)))
        if size_before and size_after:
            rows.append(ROW.format(bg=ZEBRA, label="Size", value="{} &rarr; {} bytes".format(size_before, size_after)))
        if diff:
            rows.append('<tr><td style="padding:6px 12px;font-weight:bold;color:#555;">Diff</td><td style="padding:6px 12px;"><pre style="background:#2d2d2d;color:#f8f8f2;padding:8px;border-radius:4px;font-size:12px;overflow-x:auto;">{}</pre></td></tr>'.format(diff))

    if login_user or process_name:
        rows.append(SEPARATOR)
        if login_user:
            user_display = login_user
            if effective_user and effective_user != login_user:
                user_display = "{} &rarr; {} (sudo)".format(login_user, effective_user)
            rows.append(ROW.format(bg="", label="User", value="<strong>{}</strong>".format(user_display)))
        elif effective_user:
            rows.append(ROW.format(bg="", label="User", value=effective_user))
        if process_name:
            rows.append(ROW.format(bg=ZEBRA, label="Process", value="<code>{}</code>".format(process_name)))
        if process_cwd:
            rows.append(ROW.format(bg="", label="Working Dir", value="<code>{}</code>".format(process_cwd)))

    rows.append(SEPARATOR)
    rows.append(ROW.format(bg="", label="Agent", value="{} ({})".format(agent_name, agent_ip)))
    rows.append(ROW.format(bg=ZEBRA, label="Time", value=timestamp))

    table_rows = "\n      ".join(rows)

    html = """
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family:Arial,Helvetica,sans-serif;margin:0;padding:0;background:#f4f4f4;">
  <div style="max-width:600px;margin:20px auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1);">

    <div style="background:{color};color:#fff;padding:14px 20px;">
      <p style="margin:0;font-size:16px;font-weight:bold;">{desc}</p>
    </div>

    <table style="width:100%;border-collapse:collapse;font-size:14px;">
      {table_rows}
    </table>

    <div style="background:#ecf0f1;padding:8px 20px;font-size:11px;color:#95a5a6;text-align:center;">
      Wazuh FIM Alert
    </div>
  </div>
</body>
</html>
""".format(
        color=color,
        desc=rule_desc,
        table_rows=table_rows,
    )

    return html

def send_email(api_key, hook_url, subject, html_content):
    payload = {
        "sender": {
            "name": SENDER_NAME,
            "email": SENDER_EMAIL,
        },
        "to": RECIPIENT_LIST,
        "subject": subject,
        "htmlContent": html_content,
    }

    data = json.dumps(payload).encode("utf-8")

    req = Request(hook_url, data=data)
    req.add_header("accept", "application/json")
    req.add_header("content-type", "application/json")
    req.add_header("api-key", api_key)

    try:
        response = urlopen(req)
        response_body = response.read().decode("utf-8")
        log("OK: Email terkirim. Response: {}".format(response_body))
    except HTTPError as e:
        error_body = e.read().decode("utf-8") if hasattr(e, "read") else str(e)
        log("ERROR: HTTP {} - {}".format(e.code, error_body))
        sys.exit(1)
    except URLError as e:
        log("ERROR: Koneksi gagal - {}".format(e.reason))
        sys.exit(1)
    except Exception as e:
        log("ERROR: UnExample - {}".format(e))
        sys.exit(1)

def main():
    if len(sys.argv) < 4:
        log("ERROR: Argumen tidak lengkap. Usage: custom-brevo <alert_file> <api_key> <hook_url>")
        sys.exit(1)

    alert_file_path = sys.argv[1]
    api_key = sys.argv[2]
    hook_url = sys.argv[3]

    alert = read_alert(alert_file_path)

    subject = build_subject(alert)
    html = build_html(alert)

    log("Mengirim email untuk rule {} (level {})...".format(
        alert.get("rule", {}).get("id", "?"),
        alert.get("rule", {}).get("level", "?"),
    ))
    send_email(api_key, hook_url, subject, html)

if __name__ == "__main__":
    main()

Script di atas betrujuan untuk membaca file JSON alert dari rule yang tertrigger, kemudian membuat subject email berdasarkan severity (LOW/MEDIUM/HIGH/CRITICAL). Isi dari emailnya berupa HTML yang berisi detail perubahan file, yaitu path, event, diff, user, process, info agent. Email akan dikirim ke daftar penerima yang tertera.

Selanjutnya, melalui menu Server Management > Rules di Wazuh dashboard, tulis rules baru untuk menyaring event yang dibutuhkan untuk mentrigger email alerting.

image.png

Berikut adalah contoh rules untuk menyaring event modifikasi pada file:

<group name="fim_email">

  <rule id="100101" level="10">
    <if_sid>550,553,554</if_sid>
    <description>File berubah di direktori monitored: $(file)</description>
  </rule>

</group>

Rule di atas akan terpicu kalau salah satu dari rule bawaan Wazuh, yaitu 550 (file berubah), 553 (file dihapus), atau 554 (file ditambahkan) tertrigger. Implementasi custom rule bisa jadi sangat luas. berikut:

  1. Detecting git pull
  <rule id="100104" level="10">
    <if_sid>550,553,554</if_sid>
    <field name="file" type="pcre2">\.git/FETCH_HEAD$</field>
    <description>GIT Pull terdeteksi: $(file)</description>
  </rule>
  1. Detecting git clone
  <rule id="100107" level="10">
    <if_sid>554</if_sid>
    <field name="file" type="pcre2">\.git/refs/remotes/[^/]+/HEAD$</field>
    <description>GIT Clone terdeteksi: $(file)</description>
  </rule>
  1. Detecting malicious file
	<rule id="100108" level="12">
	  <if_sid>554</if_sid>
	  <field name="file" type="pcre2">\.(sh|exe)(\.\w+)?$</field>
	  <description>File mencurigakan terdeteksi: $(file)</description>
	</rule>

Kemudian, tambahkan element di dalam ossec.conf milik Wazuh manager. Jangan lupa untuk menyesuaikan name, hook url, dan api key-nya. Pastikan juga element group sudah berisi rules yang sebelumnya dibuat.

nano config/wazuh_cluster/wazuh_manager.conf
<ossec_config>

  <integration>
    <name>custom-brevo</name>
    <hook_url>https://api.brevo.com/v3/smtp/email</hook_url>
    <api_key>YOUR_BREVO_API_KEY</api_key>
    <group>fim_email</group>
    <alert_format>json</alert_format>
  </integration>
  
  ...
  
</ossec_config>

Example output:

image.png

Mount file yang baru saja dibuat ke dalam volume Wazuh manager.

nano docker-compose.yml
services:
  wazuh.manager:
    
    ...
    
    volumes:

      ...
      
			- ./config/wazuh_cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf
			- ./config/wazuh_cluster/integrations/custom-brevo:/var/ossec/integrations/custom-
				brevo

Example output:

image.png

Jangan lupa recreate Wazuh manager dengan cara docker compose down dan up ulang untuk mengapply konfigurasinya.

docker compose down && docker compose up -d

Di dalam wazuh manager, sesuaikan permission dan kepemilikan dari file-file yang baru saja ditambahkan.

docker exec single-node-wazuh.manager-1 chmod 750 /var/ossec/integrations/custom-brevo
docker exec single-node-wazuh.manager-1 chown root:wazuh /var/ossec/integrations/custom-brevo

Berikut adalah contoh dari email yang berhasil terkirim:

image.png

Konfigurasi MySQL Monitoring + Email Alerting
#

MySQL Community Edition pada defaultnya tidak memiliki modul audit bawaan. Logging yang ditawarkan hanya berupa general log yang hanya mencatat setiap query yang dijalankan. Alternatifnya dapat menggunakan plugin audit dari Percona yang compatible dengan MySQL.

Instalasi Percona pada MySQL non-docker
#

Pertama, cek dulu versi MySQL yang digunakan.

mysql -u root -p
SELECT VERSION();
exit

Example output:

image.png

Download percona server sesuai dengan versi MySQL yang digunakan. Sesuaikan X dengan versi yang digunakan.

wget https://downloads.percona.com/downloads/Percona-Server-X.X/Percona-Server-X.X.XX-XX/binary/tarball/Percona-Server-X.X.XX-XX-Linux.x86_64.glibc2.35-minimal.tar.gz

Example output:

image.png

Ekstrak plugin audit dari archive yang telah didownlaod. Sesuaikan X dengan versi yang digunakan.

tar -xzf Percona-Server-X.X.XX-XX-Linux.x86_64.glibc2.35-minimal.tar.gz \
--wildcards '*/lib/plugin/audit_log.so' --strip-components=3

Example output:

image.png

Pindahkan file plugin ke dalam direktori plugin mysql.

sudo cp audit_log.so /usr/lib/mysql/plugin/
# atau
sudo cp audit_log.so /usr/lib64/mysql/plugin/

Example output:

image.png

Lakukan instalasi plugin tersebut beserta konfigurasinya ke dalam MySQL.

mysql -u root -p
INSTALL PLUGIN audit_log SONAME 'audit_log.so';
exit

Example output:

image.png

Verifikasi apakah plugin auditnya sudah terpasang ke dalam MySQL.

SHOW PLUGINS;

Example output:

image.png

Tambahkan konfigurasi plugin audit pada file konfigurasi /etc/mysql/mysql.conf.d/mysqld.cnf

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

Tambahkan konfigurasi berikut pada line paling bawah.

audit-log=FORCE_PLUS_PERMANENT
audit-log-format=JSON
audit-log-file=/var/log/mysql/audit.log
audit-log-policy=ALL

Example output:

image.png

Restart service mysql server

sudo systemctl restart mysql.service

Example output:

image.png

Instalasi Percona pada MySQL docker
#

Pertama, cek dulu versi MySQL yang digunakan. Ganti NAMA_CONTAINER dan PASSWORD sesuai dengan konfigurasi container.

docker exec -it NAMA_CONTAINER mysql -u root -p PASSWORD -e "SELECT VERSION();"

Expected output:

image.png

Download percona server sesuai dengan versi MySQL yang digunakan. Sesuaikan X dengan versi yang digunakan.

wget https://downloads.percona.com/downloads/Percona-Server-X.X/Percona-Server-X.X.XX-XX/binary/tarball/Percona-Server-X.X.XX-XX-Linux.x86_64.glibc2.35-minimal.tar.gz

Example output:

image.png

Ekstrak plugin audit dari archive yang telah didownlaod. Sesuaikan X dengan versi yang digunakan.

tar -xzf Percona-Server-X.X.XX-XX-Linux.x86_64.glibc2.35-minimal.tar.gz \
--wildcards '*/lib/plugin/audit_log.so' --strip-components=3

Example output:

image.png

Berikan plugin tersebut beserta konfigurasinya ke dalam MySQL container. Pada kasus ini menggunakan Docker compose.

nano docker-compose.yml

image.png

Recreate database menjalankan perintah docker compose up -d ulang.

docker compose up -d

Expected output:

image.png

Lakukan instalasi terhadap plugin audit di dalam container. Ganti NAMA_CONTAINER dan PASSWORD sesuai dengan konfigurasi container.

docker exec -it NAMA_CONTAINER mysql -u root -p PASSWORD -e "INSTALL PLUGIN audit_log SONAME 'audit_log.so';"

image.png

Verifikasi apakah plugin auditnya sudah terpasang ke dalam MySQL.

docker exec -it NAMA_CONTAINER mysql -u root -p PASSWORD -e "SHOW PLUGINS;" | grep audit

Expected output:

image.png

Konfigurasi MySQL Custom Rules
#

Pada menu Server Management > Rules, buat custom rules untuk menginterpretasikan log dari MySQL ke dalam sistem alert. Reload konfigurasi pada pop-up notifikasi yang muncul.

Contoh rules MySQL monitoring:

<group name="mysql">

    <rule id="100172" level="7">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)^drop\s+user</field>
        <description>MySQL DROP USER detected</description>
        <mitre>
            <id>T1078</id>
        </mitre>
    </rule>

    <rule id="100156" level="12">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)^delete\s+from\s+</field>
        <description>MySQL DML DELETE detected</description>
        <mitre>
            <id>T1485</id>
        </mitre>
    </rule>

    <rule id="100180" level="10">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)^load\s+data\s+(local\s+)?infile</field>
        <description>MySQL DML LOAD DATA INFILE detected</description>
        <mitre>
            <id>T1059</id>
        </mitre>
    </rule>

    <rule id="100152" level="12">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)^drop\s+table</field>
        <description>MySQL DROP TABLE detected</description>
        <mitre>
            <id>T1485</id>
        </mitre>
    </rule>

    <rule id="100155" level="12">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)^drop\s+database</field>
        <description>MySQL DROP DATABASE detected</description>
        <mitre>
            <id>T1485</id>
        </mitre>
    </rule>

    <rule id="100174" level="10">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)^truncate(\s+table)?\s+</field>
        <description>MySQL TRUNCATE TABLE detected</description>
        <mitre>
            <id>T1485</id>
        </mitre>
    </rule>

    <rule id="100181" level="12">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)into\s+outfile\s+</field>
        <description>MySQL SELECT INTO OUTFILE detected</description>
        <mitre>
            <id>T1048</id>
        </mitre>
    </rule>

    <rule id="100182" level="14">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)into\s+dumpfile\s+</field>
        <description>MySQL SELECT INTO DUMPFILE detected</description>
        <mitre>
            <id>T1048</id>
        </mitre>
    </rule>

</group>

image.png

Rules di atas pada dasarnya menyaring json dari plugin audit Percona yang dikirimkan oleh Wazuh agent. Proses penyaringannya menggunakan filter Regex pada element . Berikut adalah contoh untuk menyaring event revoke pada database:

    <rule id="100199" level="7">
        <decoded_as>json</decoded_as>
        <field name="audit_record.sqltext" type="pcre2">(?i)^revoke\s+</field>
        <description>MySQL REVOKE detected</description>
    </rule>

Selanjutnya, pada server agent, tambahkan konfigurasi di /var/ossec/etc/ossec.conf untuk mengambil log dari database MySQL.

sudo nano /var/ossec/etc/ossec.conf
  <localfile>
    <log_format>syslog</log_format>
    <location>/path/to/audit.log</location>
  </localfile>

image.png

Restart service wazuh agent.

sudo systemctl restart wazuh-agent

Example output:

image.png

Pengujian membuat dan menghapus tabel baru di database untuk memastikan decoder dan rulesnya berjalan dengan lancar.

CREATE TABLE testing (id INT);
DROP TABLE testing;

Example output:

image.png

Konfigurasi Email Alerting untuk MySQL Event
#

Terlihat di atas bahwa eventnya sudah masuk ke dalam alert. Selanjutnya adalah konfigurasi email alerting yang dihubungkan.

Buat custom python script baru bernama custom-brevo-mysql untuk memparsing dan mengirim email ke API Brevo. Script untuk custom external API integration harus dimulai dengan prefic custom-. Letakkan scriptnya di folder wazuh-docker/single-node/config/wazuh_cluster/integrations/

cd wazuh-docker/single-node
nano config/wazuh_cluster/integrations/custom-brevo-mysql

Berikut contoh custom scriptnya. Sesuaikan EMAIL_BREVO, EMAIL_TUJUAN, dan NAMA_PENERIMA.

#!/usr/bin/env python3
import json
import sys
import time

from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

SENDER_EMAIL = "EMAIL_BREVO"
SENDER_NAME = "Wazuh Alert"

RECIPIENT_LIST = [
    {"email": "EMAIL_TUJUAN1", "name": "NAMA_PENERIMA"},
    {"email": "EMAIL_TUJUAN2", "name": "NAMA_PENERIMA"},
]

SUBJECT_PREFIX = "[Wazuh]"

LOG_FILE = "/var/ossec/logs/integrations.log"

def log(msg):
    timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    entry = "{} custom-brevo-mysql: {}\n".format(timestamp, msg)
    try:
        with open(LOG_FILE, "a") as f:
            f.write(entry)
    except Exception:
        pass

def read_alert(alert_file_path):
    try:
        with open(alert_file_path) as f:
            alert = json.load(f)
        return alert
    except Exception as e:
        log("ERROR: Gagal membaca alert file {}: {}".format(alert_file_path, e))
        sys.exit(1)

def build_subject(alert):
    rule = alert.get("rule", {})
    rule_level = rule.get("level", 0)
    description = rule.get("description", "Wazuh Alert")

    if rule_level >= 12:
        severity = "CRITICAL"
    elif rule_level >= 8:
        severity = "HIGH"
    elif rule_level >= 5:
        severity = "MEDIUM"
    else:
        severity = "LOW"

    return "{} [{}] {}".format(SUBJECT_PREFIX, severity, description)

def build_html(alert):
    rule = alert.get("rule", {})
    agent = alert.get("agent", {})
    data = alert.get("data", {})
    audit = data.get("audit_record", {})

    timestamp = alert.get("timestamp", "-")
    rule_level = rule.get("level", 0)
    rule_desc = rule.get("description", "-")

    agent_name = agent.get("name", "-")
    agent_ip = agent.get("ip", "-")

    sqltext = audit.get("sqltext", "-")
    mysql_user = audit.get("user", "-")
    mysql_host = audit.get("host", "-")
    mysql_ip = audit.get("ip", "-")
    mysql_db = audit.get("db", "-")

    if rule_level >= 12:
        color = "#c0392b"
    elif rule_level >= 8:
        color = "#d35400"
    elif rule_level >= 5:
        color = "#e67e22"
    else:
        color = "#2980b9"

    ROW = '<tr{bg}><td style="padding:6px 12px;font-weight:bold;width:130px;color:#555;">{label}</td><td style="padding:6px 12px;">{value}</td></tr>'
    ZEBRA = ' style="background:#f9f9f9;"'
    SEPARATOR = '<tr><td colspan="2" style="border-top:1px solid #eee;padding:0;"></td></tr>'

    rows = []

    rows.append(ROW.format(bg="", label="Query", value="<code>{}</code>".format(sqltext)))

    rows.append(SEPARATOR)
    rows.append(ROW.format(bg="", label="MySQL User", value=mysql_user))
    rows.append(ROW.format(bg=ZEBRA, label="Host", value=mysql_host))
    rows.append(ROW.format(bg="", label="IP", value=mysql_ip if mysql_ip else "-"))
    rows.append(ROW.format(bg=ZEBRA, label="Database", value=mysql_db))

    rows.append(SEPARATOR)
    rows.append(ROW.format(bg="", label="Agent", value="{} ({})".format(agent_name, agent_ip)))
    rows.append(ROW.format(bg=ZEBRA, label="Time", value=timestamp))

    table_rows = "\n      ".join(rows)

    html = """
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family:Arial,Helvetica,sans-serif;margin:0;padding:0;background:#f4f4f4;">
  <div style="max-width:600px;margin:20px auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1);">

    <div style="background:{color};color:#fff;padding:14px 20px;">
      <p style="margin:0;font-size:16px;font-weight:bold;">{desc}</p>
    </div>

    <table style="width:100%;border-collapse:collapse;font-size:14px;">
      {table_rows}
    </table>

    <div style="background:#ecf0f1;padding:8px 20px;font-size:11px;color:#95a5a6;text-align:center;">
      Wazuh MySQL Alert
    </div>
  </div>
</body>
</html>
""".format(
        color=color,
        desc=rule_desc,
        table_rows=table_rows,
    )

    return html

def send_email(api_key, hook_url, subject, html_content):
    payload = {
        "sender": {
            "name": SENDER_NAME,
            "email": SENDER_EMAIL,
        },
        "to": RECIPIENT_LIST,
        "subject": subject,
        "htmlContent": html_content,
    }

    data = json.dumps(payload).encode("utf-8")

    req = Request(hook_url, data=data)
    req.add_header("accept", "application/json")
    req.add_header("content-type", "application/json")
    req.add_header("api-key", api_key)

    try:
        response = urlopen(req)
        response_body = response.read().decode("utf-8")
        log("OK: Email terkirim. Response: {}".format(response_body))
    except HTTPError as e:
        error_body = e.read().decode("utf-8") if hasattr(e, "read") else str(e)
        log("ERROR: HTTP {} - {}".format(e.code, error_body))
        sys.exit(1)
    except URLError as e:
        log("ERROR: Koneksi gagal - {}".format(e.reason))
        sys.exit(1)
    except Exception as e:
        log("ERROR: UnExample - {}".format(e))
        sys.exit(1)

def main():
    if len(sys.argv) < 4:
        log("ERROR: Argumen tidak lengkap. Usage: custom-brevo-mysql <alert_file> <api_key> <hook_url>")
        sys.exit(1)

    alert_file_path = sys.argv[1]
    api_key = sys.argv[2]
    hook_url = sys.argv[3]

    alert = read_alert(alert_file_path)

    subject = build_subject(alert)
    html = build_html(alert)

    log("Mengirim email untuk rule {} (level {})...".format(
        alert.get("rule", {}).get("id", "?"),
        alert.get("rule", {}).get("level", "?"),
    ))
    send_email(api_key, hook_url, subject, html)

if __name__ == "__main__":
    main()

Script di atas betrujuan untuk membaca file JSON alert dari rule yang tertrigger, kemudian membuat subject email berdasarkan severity (LOW/MEDIUM/HIGH/CRITICAL). Isi dari emailnya berupa HTML yang berisi detail perubahan file, yaitu path, event, diff, user, process, info agent. Email akan dikirim ke daftar penerima yang tertera.

Kemudian, tambahkan element di dalam ossec.conf milik Wazuh manager. Jangan lupa untuk menyesuaikan name, hook url, dan api key-nya. Pastikan juga element group sudah merujuk ke rules yang sebelumnya dibuat.

nano config/wazuh_cluster/wazuh_manager.conf
<ossec_config>

  <integration>
    <name>custom-brevo-mysql</name>
    <hook_url>https://api.brevo.com/v3/smtp/email</hook_url>
    <api_key>YOUR_BREVO_API_KEY</api_key>
    <group>mysql</group>
    <alert_format>json</alert_format>
  </integration>
  
  ...
  
</ossec_config>

Example output:

image.png

Mount file yang baru saja dibuat ke dalam volume Wazuh manager.

nano docker-compose.yml
services:
  wazuh.manager:
    
    ...
    
    volumes:

      ...
      
			- ./config/wazuh_cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf
			- ./config/wazuh_cluster/integrations/custom-brevo-mysql:/var/ossec/integrations/c
				ustom-brevo-mysql

Example output:

image.png

Jangan lupa recreate Wazuh manager dengan cara docker compose down dan up ulang untuk mengapply konfigurasinya.

docker compose down && docker compose up -d

Di dalam wazuh manager, sesuaikan permission dan kepemilikan dari file-file yang baru saja ditambahkan.

docker exec single-node-wazuh.manager-1 chmod 750 /var/ossec/integrations/custom-brevo-mysql
docker exec single-node-wazuh.manager-1 chown root:wazuh /var/ossec/integrations/custom-brevo-mysql

Berikut adalah contoh dari email yang berhasil terkirim:

image.png