Monitoring end-of-life software with Wazuh

| by | Wazuh 4.14.6
Post icon

Software reaches end-of-life (EOL) when vendors end standard support for a product or version, including regular security updates. Organizations that continue using EOL software face increased security risks because newly discovered vulnerabilities may no longer receive security patches from the vendor, leaving systems exposed to known and potentially exploitable vulnerabilities.

Despite the security risks, EOL software remains common in production environments. Support lifecycles vary by vendor and product version, making it difficult for security teams to identify installed software that has reached its EOL date. Threat actors actively target systems running EOL software. In July 2021, CISA warned that threat actors were targeting SonicWall SRA and SMA appliances running unpatched, end-of-life 8.x firmware as part of a ransomware campaign. In July 2025, Google Threat Intelligence Group reported an ongoing campaign that deployed a backdoor on fully patched but end-of-life SonicWall SMA 100 series appliances.

Addressing this challenge requires visibility into the software installed across the monitored endpoints and their support lifecycles. Wazuh is a free and open source security platform that offers unified XDR and SIEM capabilities for endpoints and cloud workloads. The Wazuh Syscollector module collects software inventory from monitored endpoints, while the endoflife.date API provides lifecycle information for supported products. 

In this blog post, we demonstrate how to combine the Wazuh system inventory with the endoflife.date API to detect end-of-life software across monitored endpoints.

Infrastructure

We use the following infrastructure to demonstrate the detection of EOL software:

  • A pre-built, ready-to-use Wazuh OVA 4.14.6, which includes the Wazuh central components (Wazuh server, Wazuh indexer, and Wazuh dashboard). Follow this guide to download and set up the Wazuh virtual machine.
  • An Ubuntu 24.04 endpoint with the Wazuh agent 4.14.6 installed and enrolled in the Wazuh server.

How it works

The detection pipeline runs on the Wazuh server and performs the following actions:

  1. The Wazuh Syscollector module collects the package and operating system information from every monitored endpoint and builds a system inventory.
  2. A scheduled Python script eol_detector.py authenticates to the Wazuh server API and retrieves the inventory of every Wazuh agent.
  3. The script cross-references each mapped package version and operating system release against the endoflife.date API, which returns the vendor-published support timeline for each product.
  4. The script writes each EOL finding as a JSON event to a log file on the Wazuh server.
  5. The Wazuh log collector reads the log file and forwards events to the Wazuh analysis engine, which evaluates them against the custom rules and generates severity-based alerts.

The endoflife.date project is a community-maintained database that tracks software support lifecycles based on vendor-published release and maintenance policies. For each product, its API provides release cycles, end-of-life dates, latest versions, and release dates. The script performs the EOL evaluation by matching the installed version to its release cycle and checking whether the associated EOL date has passed. When a product is past its EOL date, the script generates a finding, and the number of days since EOL determines the alert severity.

Configuration

Wazuh server

Perform the following steps on the Wazuh server:

  1. Create the findings log directory. Set the ownership to the wazuh user and group so the Wazuh log collector can read the directory. Assign  permissions to allow the owner full access, the group read and execute access, and deny access to other users:
# mkdir -p /var/ossec/logs/eol-findings
# chown wazuh:wazuh /var/ossec/logs/eol-findings
# chmod 750 /var/ossec/logs/eol-findings
  1. Create the EOL detector script /var/ossec/integrations/eol_detector.py with the following content:

Note

This script is a proof of concept. Review and validate it to confirm it meets the operational and security requirements of your environment.

#!/usr/bin/env python3
import requests, json, datetime, os, re, sys

ENV_FILE      = "/etc/eol_detector/eol_detector.env"
MAPPINGS_FILE = "/etc/eol_detector/mappings.json"
EOL_API_BASE  = "https://endoflife.date/api"
ENV_KEYS      = ("WAZUH_API", "WAZUH_USER", "WAZUH_PASS",
                 "VERIFY_SSL", "WAZUH_CA_BUNDLE", "EOL_LOG")


def load_env(path):
    """Read KEY=VALUE pairs from the env file. Environment variables win."""
    values = {}
    try:
        with open(path) as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#") or "=" not in line:
                    continue
                key, value = line.split("=", 1)
                values[key.strip()] = value.strip().strip('"').strip("'")
    except FileNotFoundError:
        pass
    values.update({k: os.environ[k] for k in ENV_KEYS if k in os.environ})
    return values


def load_mappings(path):
    """Read the product and OS maps from the mappings file."""
    try:
        with open(path) as f:
            data = json.load(f)
        return data["products"], data["os"]
    except (OSError, ValueError, KeyError) as err:
        print(f"cannot load mappings from {path}: {err}", file=sys.stderr)
        sys.exit(1)


_env       = load_env(ENV_FILE)
WAZUH_API  = _env.get("WAZUH_API", "")
WAZUH_USER = _env.get("WAZUH_USER", "")
WAZUH_PASS = _env.get("WAZUH_PASS", "")
EOL_LOG    = _env.get("EOL_LOG", "/var/ossec/logs/eol-findings/eol.json")

if not all((WAZUH_API, WAZUH_USER, WAZUH_PASS)):
    print(f"missing WAZUH_API, WAZUH_USER, or WAZUH_PASS in {ENV_FILE}",
          file=sys.stderr)
    sys.exit(1)

VERIFY = _env.get("WAZUH_CA_BUNDLE") or \
         _env.get("VERIFY_SSL", "false").lower() == "true"
if VERIFY is False:
    import urllib3
    urllib3.disable_warnings()

EOL_PRODUCTS, EOL_OS = load_mappings(MAPPINGS_FILE)

_eol_cache = {}


def get_token():
    r = requests.post(
        f"{WAZUH_API}/security/user/authenticate?raw=true",
        auth=(WAZUH_USER, WAZUH_PASS), verify=VERIFY, timeout=10,
    )
    r.raise_for_status()
    return r.text.strip()


def get_eol_data(slug):
    if slug in _eol_cache:
        return _eol_cache[slug]
    try:
        r = requests.get(f"{EOL_API_BASE}/{slug}.json", timeout=10)
        if r.ok:
            _eol_cache[slug] = r.json()
            return _eol_cache[slug]
        print(f"warning: endoflife.date returned HTTP {r.status_code} "
              f"for {slug}", file=sys.stderr)
    except Exception as err:
        print(f"warning: endoflife.date query failed for {slug}: {err}",
              file=sys.stderr)
    _eol_cache[slug] = None   # cache the failure to avoid re-querying this run
    return None


def cycle_candidates(version_str):
    """Return the possible release cycles for a version, most specific first."""
    if ":" in version_str:
        version_str = version_str.split(":", 1)[1]
    version_str = re.split(r"[-~+]", version_str)[0]
    parts = version_str.split(".")
    candidates = []
    if len(parts) >= 2 and parts[1]:
        candidates.append(f"{parts[0]}.{parts[1]}")
    if parts[0]:
        candidates.append(parts[0])
    return candidates


def check_eol(candidates, slug):
    """Match the cycle candidates against the endoflife.date product data."""
    eol_data = get_eol_data(slug)
    if not eol_data:
        return None
    today = datetime.date.today()
    for candidate in candidates:
        for rel in eol_data:
            if str(rel.get("cycle", "")).strip() != candidate:
                continue
            eol_date = rel.get("eol")
            if not eol_date or eol_date is True or eol_date is False:
                break   # cycle matched but has no EOL date; try the next candidate
            try:
                d = datetime.date.fromisoformat(str(eol_date))
            except ValueError:
                break
            if d < today:
                return {
                    "cycle":            candidate,
                    "eol_date":         eol_date,
                    "days_past_eol":    (today - d).days,
                    "latest_available": rel.get("latest", "unknown"),
                }
            break   # cycle matched and is still supported; stop this candidate
    return None


def check_packages(token, agent_id, agent_name):
    headers = {"Authorization": f"Bearer {token}"}
    findings, offset = [], 0
    while True:
        r = requests.get(
            f"{WAZUH_API}/syscollector/{agent_id}/packages",
            headers=headers,
            params={"limit": 500, "offset": offset},
            verify=VERIFY, timeout=15,
        )
        if not r.ok:
            print(f"warning: package query failed for agent {agent_id}: "
                  f"HTTP {r.status_code}", file=sys.stderr)
            break
        batch = r.json().get("data", {}).get("affected_items", [])
        if not batch:
            break
        for pkg in batch:
            name    = pkg.get("name", "").lower().split(":")[0]
            version = pkg.get("version", "")
            slug    = EOL_PRODUCTS.get(name)
            if not slug or not version:
                continue
            result = check_eol(cycle_candidates(version), slug)
            if result:
                findings.append({
                    "integration": "eol_detector",
                    "eol": {
                        "type":       "package",
                        "agent_id":   agent_id,
                        "agent_name": agent_name,
                        "product":    pkg.get("name"),
                        "version":    version,
                        **result,
                    },
                })
        offset += 500
        if len(batch) < 500:
            break
    return findings


def check_os(token, agent_id, agent_name):
    headers = {"Authorization": f"Bearer {token}"}
    r = requests.get(
        f"{WAZUH_API}/syscollector/{agent_id}/os",
        headers=headers, verify=VERIFY, timeout=10,
    )
    if not r.ok:
        print(f"warning: OS query failed for agent {agent_id}: "
              f"HTTP {r.status_code}", file=sys.stderr)
        return []
    items = r.json().get("data", {}).get("affected_items", [])
    if not items:
        return []
    os_info  = items[0].get("os", {})
    platform = os_info.get("platform", "").lower()
    slug     = EOL_OS.get(platform)
    if not slug:
        return []
    major, minor = os_info.get("major", ""), os_info.get("minor", "")
    candidates = [f"{major}.{minor}", major] if minor else [major]
    result = check_eol(candidates, slug)
    if result:
        return [{
            "integration": "eol_detector",
            "eol": {
                "type":       "os",
                "agent_id":   agent_id,
                "agent_name": agent_name,
                "product":    os_info.get("name", platform),
                "version":    os_info.get("version", result["cycle"]),
                **result,
            },
        }]
    return []


def get_agents(token):
    """Return every active and disconnected agent, paginated."""
    headers = {"Authorization": f"Bearer {token}"}
    agents, offset = [], 0
    while True:
        r = requests.get(
            f"{WAZUH_API}/agents",
            headers=headers,
            params={"limit": 500, "offset": offset,
                    "status": "active,disconnected"},
            verify=VERIFY, timeout=10,
        )
        r.raise_for_status()
        batch = r.json().get("data", {}).get("affected_items", [])
        agents += batch
        offset += 500
        if len(batch) < 500:
            break
    return agents


def main():
    token  = get_token()
    agents = get_agents(token)

    timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
    total = 0
    with open(EOL_LOG, "a") as log:
        for agent in agents:
            aid, aname = agent.get("id"), agent.get("name")
            astatus = agent.get("status", "unknown")
            if not aid:
                continue
            findings = (check_packages(token, aid, aname)
                        + check_os(token, aid, aname))
            total += len(findings)
            for f in findings:
                f["eol"]["agent_status"] = astatus
                f["timestamp"] = timestamp
                log.write(json.dumps(f) + "\n")
                e = f["eol"]
                print(f"[EOL] Agent {aname} ({aid}, {astatus}): {e['product']} "
                      f"{e['version']}  EOL {e['eol_date']} "
                      f"({e['days_past_eol']}d ago)")
    print(f"scan complete: {len(agents)} agents scanned, {total} findings",
          file=sys.stderr)


if __name__ == "__main__":
    try:
        main()
    except Exception as err:
        print(f"eol_detector failed: {err}", file=sys.stderr)
        sys.exit(1)
  1. Set the ownership to the wazuh user and group, and permissions to the detector script:
# chown wazuh:wazuh /var/ossec/integrations/eol_detector.py
# chmod 750 /var/ossec/integrations/eol_detector.py
  1. Create the EOL script configuration directory and set the ownership and permissions to the wazuh user and group:
# mkdir /etc/eol_detector
# chown wazuh:wazuh /etc/eol_detector
# chmod 750 /etc/eol_detector
  1. Create the mappings file /etc/eol_detector/mappings.json with the following content. The products map covers packages, and the os map covers operating systems. The products and os dictionaries define which software and operating systems the detector monitors. Different Linux distributions often use different package names for the same software. The mapping allows the EOL detector to translate those names into the identifiers expected by the endoflife.date API. They map the software names collected by the Wazuh Syscollector module to the product names used by the endoflife.date API. Packages or operating systems without a mapping are skipped.
{
  "products": {
    "python":                 "python",
    "python3":                "python",
    "python3-libs":           "python",
    "nodejs":                 "nodejs",
    "ruby":                   "ruby",
    "ruby3.1":                "ruby",
    "ruby3.2":                "ruby",
    "ruby3.3":                "ruby",
    "php":                    "php",
    "php-cli":                "php",
    "php-fpm":                "php",
    "mysql-server":           "mysql",
    "mysql-community-server": "mysql",
    "mariadb-server":         "mariadb",
    "postgresql":             "postgresql",
    "postgresql-server":      "postgresql",
    "mongodb-org":            "mongodb",
    "redis":                  "redis",
    "redis-server":           "redis",
    "nginx":                  "nginx",
    "apache2":                "apache",
    "httpd":                  "apache",
    "openssl":                "openssl",
    "openssl-libs":           "openssl",
    "perl":                   "perl",
    "varnish":                "varnish",
    "ansible-core":           "ansible-core",
    "golang":                 "go",
    "golang-go":              "go"
  },
  "os": {
    "ubuntu":  "ubuntu",
    "debian":  "debian",
    "centos":  "centos",
    "rhel":    "rhel",
    "amzn":    "amazon-linux",
    "fedora":  "fedora",
    "sles":    "sles"
  }
}

To monitor additional software:

  1. Identify the package name reported by the Wazuh Syscollector module or the system package manager.
  2. Find the corresponding product slug:
# curl -s https://endoflife.date/api/all.json
  1. Add the mapping to products. For example:
"grafana": "grafana",

The new product is automatically included in the next scheduled scan

  1. Retrieve the Wazuh server API certificate to enable secure communication between the detector script and the Wazuh server API:
# openssl s_client -connect localhost:55000 </dev/null 2>/dev/null | openssl x509 -outform PEM | sudo tee /etc/eol_detector/wazuh-api.pem
  1. Create the environment file /etc/eol_detector/eol_detector.env with the following values:
WAZUH_API=https://<WAZUH_SERVER_ADDRESS>:55000
WAZUH_USER=<WAZUH_SERVER_API_USERNAME>
WAZUH_PASS=<WAZUH_SERVER_API_PASSWORD>
WAZUH_CA_BUNDLE=/etc/eol_detector/wazuh-api.pem

Replace:

  • <WAZUH_SERVER_ADDRESS>: the value of the subjectAltName from the /etc/eol_detector/wazuh-api.pem certificate. This can be obtained by running the command:
# openssl x509 -in /etc/eol_detector/wazuh-api.pem -noout -subject -ext subjectAltName
  • <WAZUH_SERVER_API_USERNAME>: the username of your Wazuh server API user.
  • <WAZUH_SERVER_API_PASSWORD>: the password of your Wazuh server API user.

Use the following command to retrieve your Wazuh server API username and password:

# grep -E "username:|password:" /usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml | head -2
  1. Add the following block inside the <ossec_config> section of the server configuration file /var/ossec/etc/ossec.conf:
  <localfile>
    <log_format>json</log_format>
    <location>/var/ossec/logs/eol-findings/eol.json</location>
  </localfile>
  1. Create a file called eol_rules.xml in the /var/ossec/etc/rules/ directory and insert the following custom rules:
<group name="eol,">

  <!-- Base rule: any EOL finding -->
  <rule id="100400" level="7">
    <decoded_as>json</decoded_as>
    <field name="integration">^eol_detector$</field>
    <description>EOL software detected: $(eol.product) $(eol.version) on agent $(eol.agent_name) - EOL since $(eol.eol_date)</description>
    <options>no_full_log</options>
    <group>eol_software,pci_dss_6.3.3,nist_800_53_CM.8,hipaa_164.312.a.1,</group>
  </rule>

  <!-- 100 to 999 days past EOL -->
  <rule id="100401" level="10">
    <if_sid>100400</if_sid>
    <field name="eol.type">^package$</field>
    <field name="eol.days_past_eol" type="pcre2">^\d{3}$</field>
    <description>HIGH RISK EOL: $(eol.product) $(eol.version) on agent $(eol.agent_name) is $(eol.days_past_eol) days past end-of-life</description>
    <options>no_full_log</options>
    <group>eol_software,pci_dss_6.3.3,nist_800_53_CM.8,hipaa_164.312.a.1,</group>
  </rule>

  <!-- 1000 or more days past EOL -->
  <rule id="100402" level="13">
    <if_sid>100400</if_sid>
    <field name="eol.type">^package$</field>
    <field name="eol.days_past_eol" type="pcre2">^\d{4,}$</field>
    <description>CRITICAL RISK EOL: $(eol.product) $(eol.version) on agent $(eol.agent_name) is $(eol.days_past_eol) days past end-of-life</description>
    <options>no_full_log</options>
    <group>eol_software,pci_dss_6.3.3,nist_800_53_CM.8,hipaa_164.312.a.1,</group>
  </rule>

  <!-- EOL operating system -->
  <rule id="100403" level="12">
    <if_sid>100400</if_sid>
    <field name="eol.type">^os$</field>
    <description>EOL operating system: $(eol.product) $(eol.version) on agent $(eol.agent_name) - EOL since $(eol.eol_date)</description>
    <options>no_full_log</options>
    <group>eol_os,pci_dss_6.3.3,nist_800_53_CM.8,hipaa_164.312.a.1,</group>
  </rule>

</group>

Where:

  • Rule ID 100400 is the base rule. It matches every event from the detector and generates a level 7 alert. 
  • Rule ID 100401 matches packages that are 100 to 999 days past EOL and raises the alert to level 10.
  • Rule ID 100402 matches packages that are 1,000 or more days past EOL and raises the alert to level 13.
  • Rule ID 100403 matches EOL operating systems and raises the alert to level 12.
  1. Restart the Wazuh server to apply the changes:
# systemctl restart wazuh-manager
  1. Add the following entry to /etc/crontab on the Wazuh server to run the detector daily at 01:00. Adjust the frequency to match your patching cycle:
0 1 * * * root /usr/bin/python3 /var/ossec/integrations/eol_detector.py

Results

The script scans every enrolled Wazuh agent through the Wazuh server API. In our test environment, the scanner detects nine EOL packages across two endpoints. 

Perform the following steps on the Wazuh dashboard to visualize the EOL events:

  1. Navigate to Threat intelligence > Threat Hunting > Events on the Wazuh dashboard. 
  2. In the search bar, type rule.groups:eol, and click Update.
Figure 1: EOL events.
Figure 1: EOL events.

Conclusion

In this blog post, we demonstrated how to detect end-of-life software with Wazuh by combining the Wazuh system inventory with the endoflife.date API. This approach provides centralized visibility into unsupported software across managed endpoints. It also generates alerts that help security teams prioritize upgrades based on how long a product has been out of support. By identifying software that no longer receives standard security updates, organizations can prioritize upgrades and reduce the security risks associated with running unsupported products.

Wazuh is a free and open source security platform with an active community. If you have any questions about this blog post or Wazuh, we invite you to join our community.

References