ESET PROTECT Hub allows administrators to manage identities, licenses, and users across various ESET services from a single location. These services, including ESET PROTECT, ESET Inspect, and ESET Cloud Office Security, are designed to provide threat detection and endpoint protection solutions for businesses and individuals.
Wazuh is an open source security platform designed for threat detection, compliance monitoring, and incident response across diverse environments, including on-premises, cloud, and hybrid infrastructures. Its open source nature allows users to build and extend integrations that meet their specific security needs.
Integrating ESET PROTECT Hub with Wazuh enables users to directly ingest detection data from the ESET services into their Wazuh instance, enabling centralized monitoring, and threat correlation. Wazuh collects detection data from ESET at custom intervals, improving visibility across endpoints, servers, and cloud environments. Custom rules in Wazuh allow teams to trigger alerts, notifications, or automated actions based on incoming ESET events, supporting efficient incident response workflows.
This blog post shows how to integrate ESET PROTECT Hub with Wazuh.
Requirements
We use the following infrastructure to demonstrate the integration of ESET PROTECT Hub with Wazuh:
- A pre-built, ready-to-use Wazuh OVA 4.12.0 that hosts the Wazuh central components (Wazuh server, Wazuh indexer, and Wazuh dashboard). Follow this guide to download the virtual machine.
- An ESET Connect API user account.
- A Windows 11 endpoint with the ESET endpoint protection tool downloaded from the ESET Connect API user account. This is the monitored endpoint against threats.
- An Ubuntu 24 endpoint to simulate attacks against the monitored endpoint.
Configuring the integration
Perform the steps below on the Wazuh server with root privileges.
- Download the ESET PROTECT Hub and Wazuh integration detection rules:
# curl -o /var/ossec/etc/rules/eset_local_rules.xml https://raw.githubusercontent.com/eset/ESET-Integration-Wazuh/69ec85343541f1f8d435028a0120ab49066f0826/eset_local_rules.xml
- Create an
eset_integration.log
file in the/var/log/
directory to store the ESET events pulled by the integration:
# touch /var/log/eset_integration.log
- Edit the
/var/ossec/etc/ossec.conf
file and append the settings below to configure Wazuh to monitor the ESET log file/var/log/eset_integration.log
:
<ossec_config> <!--Configuration of other local files --> <localfile> <log_format>json</log_format> <location>/var/log/eset_integration.log</location> </localfile> </ossec_config>
- Restart the Wazuh manager service to apply the changes:
# systemctl restart wazuh-manager
- Create a new directory
eset_integration
in the/opt
directory:
# mkdir /opt/eset_integration
- Create an empty
/opt/eset_integration/last_detection_time.yml
file to store and track the time of occurrence of the last events. This is to ensure the script always pulls the most recent events since the last pull.
# touch /opt/eset_integration/last_detection_time.yml
- Create an
eset_integration.py
script in the/opt/eset_integration
directory:
import json import logging import os import time import tzlocal # pip install tzlocal import requests import yaml from datetime import datetime, timedelta, timezone from pathlib import Path from dateutil.parser import isoparse from dateutil import parser from dotenv import load_dotenv # === Configuration === load_dotenv() USERNAME = os.getenv("USERNAME_INTEGRATION") PASSWORD = os.getenv("PASSWORD_INTEGRATION") INSTANCE_REGION = os.getenv("INSTANCE_REGION").lower() IAM_URL = f"https://{INSTANCE_REGION}.business-account.iam.eset.systems/oauth/token" API_BASE = f"https://{INSTANCE_REGION}.incident-management.eset.systems" OUTPUT_FILE = "/var/log/eset_integration.log" INTERVAL = int(os.getenv("INTERVAL", "3")) # in minutes LAST_TIME_FILE = "/opt/eset_integration/last_detection_time.yml" DATA_SOURCE = "EP" # Event type key # === Last Time Handling === def load_last_detection_time() -> str: try: with open(LAST_TIME_FILE, "rb") as f: ldt = yaml.safe_load(f) if ldt is None: ldt = {} except FileNotFoundError: ldt = {} last_time = ldt.get(DATA_SOURCE) if not last_time: return ( datetime.now(timezone.utc) - timedelta(minutes=30) ).isoformat(timespec='milliseconds').replace("+00:00", "Z") return last_time def save_last_detection_time(new_time: str) -> None: try: with open(LAST_TIME_FILE, "rb") as f: ldt = yaml.safe_load(f) if ldt is None: ldt = {} except FileNotFoundError: ldt = {} # Parse the UTC time string utc_dt = parser.isoparse(new_time) # Convert to local system time zone local_tz = tzlocal.get_localzone() local_dt = utc_dt.astimezone(local_tz) # Format back to ISO 8601 with 'Z' removed, because it's no longer UTC formatted_local_time = local_dt.isoformat(timespec='milliseconds') ldt[DATA_SOURCE] = formatted_local_time with open(LAST_TIME_FILE, "w") as f: yaml.safe_dump(ldt, f) # === Main Logic === def fetch_and_save_detections(): try: # 1. Get Access Token token_data = { "grant_type": "password", "username": USERNAME, "password": PASSWORD, "refresh_token": "" } headers = { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded" } logging.info("Requesting access token...") response = requests.post(IAM_URL, data=token_data, headers=headers) response.raise_for_status() access_token = response.json().get("access_token") if not access_token: raise Exception("Failed to obtain access token") # 2. Fetch Detections start_time = load_last_detection_time() api_headers = {"Authorization": f"Bearer {access_token}"} detections_url = f"{API_BASE}/v1/detections" params = {"startTime": start_time} logging.info(f"Fetching detections from {start_time}...") detections_resp = requests.get(detections_url, headers=api_headers, params=params) if detections_resp.status_code != 200: logging.error(f"API response: {detections_resp.status_code} - {detections_resp.text}") detections_resp.raise_for_status() detections = detections_resp.json().get("detections", []) if not detections: logging.info("No new detections found.") return # 3. Save each detection to the log file with open(OUTPUT_FILE, "a") as f: for detection in detections: detection["providerName"] = "ESET" wrapped = {"eset": detection} f.write(json.dumps(wrapped) + "\n") logging.info(f"{len(detections)} detections saved.") # 4. Update last detection time detect_times = [isoparse(d["occurTime"]) for d in detections if d.get("occurTime")] if detect_times: newest_time = (max(detect_times) + timedelta(seconds=1)).isoformat().replace("+00:00", "Z") save_last_detection_time(newest_time) logging.info(f"Updated last detection time to {newest_time}.") else: logging.warning("No valid occurTime found in detections.") except Exception as e: logging.error(f"Error: {e}", exc_info=True) # === Runner === def main_loop(): logging.basicConfig( format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S" ) logging.info("Starting ESET event fetcher.") while True: fetch_and_save_detections() time.sleep(INTERVAL * 60) if __name__ == "__main__": main_loop()
The original script was obtained from bayuski labs and customized to suit our implementation. This script periodically retrieves threat detection events, wraps each in a standardized JSON format, and appends them to the /var/log/eset_integration.log
file.
Note
The INTERVAL
is set to 300 seconds. You can set it to any value suitable for your requirements.
- Install the Python modules required to run the script:
# pip install requests pyyaml python-dotenv python-dateutil tzlocal
- Create a
.env
file in the/opt/eset_integration
directory to store the integration variables:
USERNAME_INTEGRATION=<USERNAME_INTEGRATION> PASSWORD_INTEGRATION=<PASSWORD_INTEGRATION> INSTANCE_REGION=<INSTANCE_REGION>
Replace:
<USERNAME_INTEGRATION>
with the ESET Connect API user login username or email address.<PASSWORD_INTEGRATION>
with the ESET Connect API user password.<INSTANCE_REGION>
with the location of your ESET PROTECT, ESET Inspect, or ESET Cloud Office Security instance. The allowed options areca, de, eu, jpn, us
.
- Run the following command to make the
/opt/eset_integration/eset_integration.py
script executable:
# chmod +x /opt/eset_integration/eset_integration.py
- Create the
/etc/systemd/system/eset_integration.service
SystemD service file to run the integration script as a daemon:
[Unit] Description=ESET Detection Fetcher Daemon After=network.target [Service] Restart=always RestartSec=10 ExecStart=/usr/bin/python3 /opt/eset_integration/eset_integration.py WorkingDirectory=/opt/eset_integration/ User=root Group=root StandardOutput=append:/var/log/eset_integration.out.log StandardError=append:/var/log/eset_integration.err.log [Install] WantedBy=multi-user.target
Note
The file /var/log/eset_integration.err.log
stores the service logs and can be used for troubleshooting purposes.
- Start and enable the service to execute the script on startup:
# systemctl daemon-reload # systemctl enable eset_integration.service # systemctl start eset_integration.service
Testing the integration
To ensure the integration works as expected, we perform the following tests:
Brute-force attempt
We simulate a brute-force attack against the monitored Windows endpoint using Hydra.
Attack emulation
Perform the steps below on the attacking endpoint to simulate authentication failure attempts on the monitored system.
- Install Hydra and use it to execute the brute-force attack:
# apt update # apt install -y hydra
- Create a text file
passwords.txt
with 10 random passwords:
# for i in {1..10}; do openssl rand -base64 12 >> passwords.txt; done
- Run the following command to execute brute-force attacks against the Windows endpoint, replacing
<WINDOWS_IP>
with the IP address of the Windows endpoint:
# hydra -l trudy -P password.txt rdp://<WINDOWS_IP>
- Check the
/var/log/eset_integration.log
file on the Wazuh server for any new entries related to the simulated brute-force attack to confirm the integration:
{ "eset": { "uuid": "6182ead6-bb16-9742-1bb8-f1f9651077bc", "typeName": "", "category": "DETECTION_CATEGORY_FIREWALL_RULE", "displayName": "RDP.Attack.Bruteforce", "objectName": "192.168.0.115->192.168.0.115:3389", "objectTypeName": "", "objectHashSha1": "55EFB424933087D755B18468BC574DB4463D9CE6", "objectUrl": "", "context": { "userName": "NT AUTHORITY\NETWORK SERVICE", "process": { "path": "C:\Windows\System32\svchost.exe" }, "deviceUuid": "97e08510-deef-48b9-8e8f-bd011863d6f6", "circumstances": "" }, "severityLevel": "SEVERITY_LEVEL_MEDIUM", "occurTime": "2025-06-03T01:41:58Z", "networkCommunication": { "localIpAddress": "192.168.0.115", "remoteIpAddress": "192.168.0.115", "localPort": 3389, "remotePort": 0, "direction": "NETWORK_COMMUNICATION_DIRECTION_INBOUND", "protocolName": "TCP" }, "responses": [], "providerName": "ESET" } }
Detection results
Follow the steps below to view the alerts generated on the Wazuh dashboard once the attack attempt is completed.
- Navigate to Threat intelligence > Threat Hunting, and click the Events tab.
- Click + Add filter. Then filter by
rule.groups
. - In the Operator field, select
is
. - Search and select
eset
in the Values field. - Click Save.

To have a more detailed description of each event, click on the Inspect document details button at the far left of the event to open the detailed view.

Malware detection
In this section, we conduct a controlled test by downloading a known malware sample on the Windows endpoint protected by ESET security software. Upon detection, ESET generates a security event, which is successfully forwarded and ingested by the Wazuh manager through the custom integration script.
To confirm the integration, we check the /var/log/eset_integration.log
file on the Wazuh server for any new entries related to the malware detection:
{ "eset": { "providerName": "ESET", "responses": [], "category": "DETECTION_CATEGORY_ANTIVIRUS", "displayName": "Python/Spy.Agent.AAF", "objectHashSha1": "E579E8C286E4D7F15D1F2B35F41D25BCC7BDE559", "objectName": "file:///C:/Users/rolly/Downloads/8c4daf5e4ced10c3b7fd7c17c7c75a158f08867aeb6bccab6da116affa424a89", "objectTypeName": "File", "objectUrl": "", "occurTime": "2025-06-30T18:28:51Z", "TimeGenerated": "2025-06-30T18:32:58Z", "severityLevel": "SEVERITY_LEVEL_MEDIUM", "typeName": "", "groupSize": 1, "severityScore": null, "userName": "", "objectSizeBytes": null, "edrRuleUuid": null, "note": null, "resolved": null, "cloudOfficeTenantUuid": null, "scanUuid": null, "detectionUuid": "edb25397-8bf8-8242-8bbb-d10670c727ef", "deviceDisplayName": null, "deviceUuid": "442926a2-3eb7-422e-beba-9acd3d44faaf", "userNameBase": "WINBOX\rolly", "processPath": "C:\Program Files\WinRAR\WinRAR.exe", "processUuid": null, "processCommandline": null } }
Detection results
Follow the steps below to view the alerts generated on the Wazuh dashboard when the malware intrusion is detected.
- Navigate to Threat intelligence > Threat Hunting, and click the Events tab.
- Click + Add filter. Then filter by
rule.groups
. - In the Operator field, select
is
. - Search and select
eset
in the Values field. - Click Save.

To have a more detailed description of each event, click on the Inspect document details button at the far left of the event to open the detailed view.

Conclusion
Integrating the ESET PROTECT Hub with Wazuh helps centralize monitoring of security events. By forwarding ESET detections to Wazuh, organizations gain real-time visibility, customizable alerting, and correlation across multiple data sources within a single security operations platform. This integration streamlines incident response and enhances threat hunting capabilities. It also supports compliance initiatives by centralizing and retaining security logs.
You can refer to the Wazuh documentation for more information on other use cases. If you have any questions about this blog post or Wazuh, we invite you to join our community, where our team can assist you.