Integrating ESET PROTECT Hub with Wazuh

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.
We use the following infrastructure to demonstrate the integration of ESET PROTECT Hub with Wazuh:
Perform the steps below on the Wazuh server with root privileges.
# curl -o /var/ossec/etc/rules/eset_local_rules.xml https://raw.githubusercontent.com/eset/ESET-Integration-Wazuh/69ec85343541f1f8d435028a0120ab49066f0826/eset_local_rules.xml
eset_integration.log
file in the /var/log/
directory to store the ESET events pulled by the integration:# touch /var/log/eset_integration.log
/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>
# systemctl restart wazuh-manager
eset_integration
in the /opt
directory: # mkdir /opt/eset_integration
/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
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.
# pip install requests pyyaml python-dotenv python-dateutil tzlocal
.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 are ca, de, eu, jpn, us
./opt/eset_integration/eset_integration.py
script executable:# chmod +x /opt/eset_integration/eset_integration.py
/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.
# systemctl daemon-reload # systemctl enable eset_integration.service # systemctl start eset_integration.service
To ensure the integration works as expected, we perform the following tests:
We simulate a brute-force attack against the monitored Windows endpoint using Hydra.
Perform the steps below on the attacking endpoint to simulate authentication failure attempts on the monitored system.
# apt update # apt install -y hydra
passwords.txt
with 10 random passwords:# for i in {1..10}; do openssl rand -base64 12 >> passwords.txt; done
<WINDOWS_IP>
with the IP address of the Windows endpoint:# hydra -l trudy -P password.txt rdp://<WINDOWS_IP>
/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" } }
Follow the steps below to view the alerts generated on the Wazuh dashboard once the attack attempt is completed.
rule.groups
.is
.eset
in the Values field.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.
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 } }
Follow the steps below to view the alerts generated on the Wazuh dashboard when the malware intrusion is detected.
rule.groups
.is
.eset
in the Values field.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.
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.