Integrating ESET PROTECT Hub with Wazuh

| by | Wazuh 4.12.0
Post icon

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.

  1. 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
  1. 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
  1. 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>
  1. Restart the Wazuh manager service to apply the changes:
# systemctl restart wazuh-manager
  1. Create a new directory eset_integration in the /opt directory: 
# mkdir /opt/eset_integration
  1. 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
  1. 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.

  1. Install the Python modules required to run the script:
# pip install requests pyyaml python-dotenv python-dateutil tzlocal
  1. 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 are ca, de, eu, jpn, us.
  1. Run the following command to make the /opt/eset_integration/eset_integration.py script executable:
# chmod +x /opt/eset_integration/eset_integration.py
  1. 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.

  1. 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.

  1. Install Hydra and use it to execute the brute-force attack:
# apt update
# apt install -y hydra
  1. Create a text file passwords.txt with 10 random passwords:
# for i in {1..10}; do openssl rand -base64 12 >> passwords.txt; done
  1. 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>
  1. 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.

  1. Navigate to Threat intelligence > Threat Hunting, and click the Events tab.
  2. Click + Add filter. Then filter by rule.groups.
  3. In the Operator field, select is.
  4. Search and select eset  in the Values field.
  5. 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.

  1. Navigate to Threat intelligence > Threat Hunting, and click the Events tab.
  2. Click + Add filter. Then filter by rule.groups.
  3. In the Operator field, select is.
  4. Search and select eset  in the Values field.
  5. 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.

References