The Wazuh VirusTotal integration does not exist in Wazuh 5.0. The integratord daemon and the <integration> blocks that configured it were removed, and the official migration guide is explicit that bi-directional enrichment integrations such as VirusTotal and Maltiverse “cannot be migrated” because there is no equivalent mechanism. What replaces them is different in shape: hash enrichment happens inside the normalization engine against Wazuh CTI, reference lists become KVDBs, and anything involving a local scanner like YARA runs as a custom active response script you write and own.

What you need

  • A working 5.0 deployment with at least one enrolled agent. See installing the agent on Linux if you do not have one.
  • File integrity monitoring configured on the paths you care about. FIM setup covers the <syscheck> block.
  • Dashboard access with rights over Security Analytics and Explore, for KVDBs and Alerting monitors.
  • Root or Administrator on the endpoint if you intend to deploy an active response script.

What happened to the VirusTotal integration

Two separate things were removed, and it helps to keep them apart.

The first is the notification path. In 4.x, <integration> also carried Slack, PagerDuty and custom webhooks. That part has a direct replacement: Notifications channels plus Alerting monitors, covered in Wazuh notifications. It is a migration, not a loss.

The second is the enrichment path, and that one has no replacement. VirusTotal and Maltiverse worked as a callback loop: the script sent the alert out, received an enriched response, and pushed the response back in as a new alert. Nothing in 5.x does that. Enrichment now happens inline in the normalization engine, before events ever reach the indexer, and the engine’s enrichment plugins are fixed.

So if your 4.x deployment leaned on VirusTotal verdicts appearing as alerts, plan for a design change rather than a config port. You have three documented places to put that logic instead.

What Wazuh 5.0 gives you for malware detection

The compliance documentation states the position directly. Wazuh detects malware using the threat detection ruleset and rootkit detection, and to meet PCI DSS requirement 5 it “uses a combination of out-of-the-box rules, threat intelligence enrichments, and KVDB lists to detect malicious files”.

Those are the three mechanisms. Each one is worth understanding on its own terms.

Built-in CTI hash enrichment

This is the closest thing to a VirusTotal replacement, and it is already on. The FIM use case for detecting malware persistence says it plainly: “By default, Wazuh enriches the file hash with threat intelligence from Wazuh Cyber Threat Intelligence (CTI), providing additional context about the detected file.”

The mechanics sit in the enrichment stage of the pipeline. IOC enrichment inspects the fields declared in the IOC source definitions and checks them against known indicators, covering IP addresses, domains, URLs, and “hashes and other supported indicator values”. A match writes context under wazuh.threat, which can include indicator metadata such as type and identifier, feed or provider details, first and last seen, software or campaign names, provider tags, and the field that triggered the match:

{
  "wazuh": {
    "threat": {
      "indicator": {
        "type": "ipv4-addr",
        "ip": "203.0.113.10"
      },
      "enrichments": [
        {
          "matched": {
            "field": "destination.ip"
          }
        }
      ]
    }
  }
}

That example, from the enrichment documentation, uses a network indicator, but the same structure carries file hash matches. The indicators themselves arrive through the Content Manager, which synchronises CTI content into the wazuh-threatintel-enrichments index and tracks its state in .wazuh-cti-consumers.

Two practical consequences. First, coverage is Wazuh’s CTI feed, not VirusTotal’s multi-engine consensus, so absence of a match means considerably less than it used to. Second, the lookup costs you nothing per event and discloses nothing outside your deployment, which is a real improvement if hash disclosure was ever a concern.

KVDB lookup lists

CDB lists are gone. Their replacement is the Key-Value Database, a reusable key-value map that decoders and other assets query during event processing. Each entry is a string key paired with a JSON value, which means a hash list can carry structure rather than just a boolean.

The helper functions are named in the documentation. For mapping, kvdb_get, kvdb_get_array, kvdb_get_merge, kvdb_get_merge_recursive and kvdb_decode_bitmask. For filtering, kvdb_match, which succeeds when a field value exists as a key in the database, and its inverse kvdb_not_match.

Create one from the dashboard at Security Analytics > KVDBs > Actions > Create, in the Draft space. The form asks for the integration, title, author and the key-value content. The documented usage in a decoder’s normalisation step looks like this:

- threat.feed.description: "kvdb_get('test_KVDB', $source.ip)"

A hash blocklist follows the same shape with a hash field as the key. The companion files for this lesson include a sample KVDB body keyed on SHA-256.

Once the KVDB exists, promote it out of Draft the same way you promote any other content: Security Analytics > Overview > Actions > Promote, Draft to Test, then Test to Custom. Rule authoring against it is covered in writing custom detection rules.

Rootcheck

The <rootcheck> section survives on the agent and configures rootkit, trojan, system anomaly and policy checks. The PCI DSS documentation uses it as its worked malware example: a kernel-mode rootkit hides a process, Rootcheck compares process information from system calls such as getsid and getpgid, spots the discrepancy, and raises a finding that a process is hidden from the system process list. That is genuine on-endpoint malware detection with no feed dependency at all, and it is easy to forget it is there.

Run a YARA scan when a file changes

There is no YARA page in the Wazuh 5.0 beta documentation, and no YARA module. What is documented is every other link in the chain, which is enough to build the pattern yourself and know it will hold.

The chain runs: FIM detects the change, a rule turns the event into a finding, an Alerting active response monitor matches the finding, the manager receives the request and dispatches it, the agent runs your script, and the script’s output is written to the active response log. If that inversion is new to you, read how active response works in 5.0 first, including the argument for leaving it switched off by default.

Deploy the script on the agent

Custom active response scripts can be written in any language the endpoint supports. Place the script in the agent’s active response directory and set ownership and permissions:

sudo cp yara-scan.py /var/ossec/active-response/bin/yara-scan
sudo chmod 750 /var/ossec/active-response/bin/yara-scan
sudo chown root:wazuh /var/ossec/active-response/bin/yara-scan

On macOS the directory is /Library/Ossec/active-response/bin. On Windows it is C:\Program Files (x86)\ossec-agent\active-response\bin, and because the Windows agent executes active responses as executables, you either convert the script with PyInstaller or call it through a batch launcher. Both routes are documented on the custom active response scripts page.

The script must read a JSON message from standard input. The documented structure is:

{
  "wazuh": {
    "active_response": {
      "name": "",
      "executable": "",
      "location": "",
      "agent_id": "",
      "type": "",
      "stateful_timeout": 0
    },
    "agent": {
      "id": "",
      "name": ""
    }
  },
  "source": {},
  "user": {},
  "command": ""
}

Alongside wazuh.active_response and wazuh.agent, the message carries relevant top-level Wazuh Common Schema fields from the finding that triggered it. For a FIM trigger that is what matters: file.path tells the script which file to scan. The command field is enable to run the action and disable to revert it. A YARA scan has nothing to revert, so this is a stateless response and only ever receives enable.

The full script is in the companion files for this lesson. It follows the structure of the official Python active response sample, with the scan call and the result handling as the parts you adapt.

Register the active response and its monitor

Create the active response entry at Explore > Active Responses > Create active response. The fields are Name, Description, Executable, Extra arguments, Location and Type, with an additional Stateful timeout when Type is stateful. Set Location to Local so the scan runs on the agent that produced the finding, and Type to stateless. Restarting the manager is not required; the manager distributes the updated configuration to the agents the response is configured to run on.

Then create the trigger at Explore > Alerting > Monitors > Create monitor, with Monitor type set to Active response. Three constraints apply to this monitor type, and all three are documented requirements rather than advice:

Requirement Value
Target indices must match wazuh-findings-v5-*
Schedule maximum execution interval of 1 minute (60,000 ms)
Trigger type DocumentLevelTrigger only

FIM findings land in the system-activity category, so the index for a FIM-driven scan is wazuh-findings-v5-system-activity. Add the active response as an action on the trigger.

Where the results go

Each active response execution is stored in the wazuh-active-responses index with the command, the execution location, the triggering document, the target agent and the response state. The dashboard view is Security operations > Incident Response > Responses.

Script output goes to the active response log on the endpoint: /var/ossec/logs/active-responses.log on Linux, /Library/Ossec/logs/active-responses.log on macOS, and C:\Program Files (x86)\ossec-agent\active-response\active-responses.log on Windows.

Turning a YARA match into a finding of its own means treating that log as a log source: collect it with <localfile> as described in log collection, write a decoder for your output format as in writing a custom decoder, and write a rule against the normalised fields. The beta documentation ships no YARA decoder, so this part is yours. Emit JSON from the script and the decoder stays short.

Rate limits and the privacy of hash lookups

If you do decide to call an external reputation API from an active response script or a webhook, two things bite.

Rate limits bite first. Public API tiers are typically a handful of requests per minute. A FIM realtime watch on a directory that a package manager touches will produce hundreds of changes in seconds, and your script will be throttled, silently, exactly when something interesting is happening. Deduplicate on hash before you call, cache verdicts locally, and never place the call in the hot path of a response that also needs to act.

Disclosure bites second and is harder to undo. A file hash is a fingerprint of content. For small, structured or templated files, anyone with a matching corpus can recover the original. Public services also retain the querying account, which leaks that you looked and when. This is a decision that belongs with whoever owns the data classification policy, not with whoever writes the script. The built-in CTI enrichment avoids the question entirely, which is a point in its favour that is easy to undervalue.

Common errors

The active response never runs

Check the monitor first. An active response monitor that targets an index outside wazuh-findings-v5-*, or that uses a trigger type other than DocumentLevelTrigger, will not produce execution requests. Then check that a finding actually exists for your test: an event in wazuh-events-v5-* is not a finding, and only findings drive these monitors. Detectors run on a schedule, so allow for the delay between the event and the finding.

The script runs but nothing happens

Look in the active response log on the endpoint. The documented sample script logs Started, then the raw input message, then Ended. If the input message is absent, the script is not reading standard input correctly. If you see Decoding JSON has failed, invalid input format, the script received something other than the expected JSON message.

The script cannot find the file

The active response payload only contains a file.path if the triggering finding carried one. A rule matching on something other than a FIM event will not populate it. The documented remove-file sample guards against this explicitly, logging No file.path present in the active response payload and exiting rather than acting on nothing. Do the same.

Permission denied on the executable

The script must be owned by root:wazuh with mode 750 on Linux and macOS. Anything looser and the agent refuses to run it; anything tighter and the agent user cannot read it.

Nothing in the Responses tab after three days

That is the retention policy doing its job. wazuh-active-responses keeps 3 days by default.

Where this leaves malware detection

The honest summary for 5.0 as it stands in beta: hash reputation is built in but narrower than VirusTotal, list-based detection is better than it was because KVDB values carry structure, on-endpoint scanning is entirely yours to build, and the bi-directional enrichment pattern is gone with no successor. Design around the first three. If a specific detection depended on VirusTotal verdicts arriving as alerts, rebuild it as an out-of-band enrichment in your automation platform, as described in SOAR with Wazuh, rather than waiting for a feature that the migration guide says is not coming.

Next: Wazuh Cloud Logs: AWS CloudTrail, Azure, GCP, M365.

Files for this lesson

Every file this lesson refers to, exactly as used. Open a file to read it in the browser, or download the set as a zip. Download wazuh-virustotal-yara.zip. The full set for all lessons is at wazuh-tutorial-files.zip.

Frequently asked questions

Does Wazuh 5.0 still have a VirusTotal integration?

No. The integratord daemon and the ossec.conf integration blocks that carried it were removed. The official migration guide states plainly that bi-directional integrations such as VirusTotal and Maltiverse have no equivalent mechanism in 5.x and cannot be migrated. The beta documentation has no VirusTotal page at all.

How does Wazuh 5.0 detect malware without VirusTotal?

With three documented mechanisms. The normalization engine enriches file hashes against Wazuh CTI indicators of compromise and writes matches to wazuh.threat. Detection rules and KVDB lookup lists match on file paths, hashes and behaviour. Rootcheck still detects hidden processes and rootkit artefacts on the endpoint.

Can I still run YARA scans with Wazuh 5.0?

Yes, but you write the plumbing yourself. The beta documentation has no YARA page. The supported route is a custom active response script on the agent, triggered by an Alerting active response monitor over FIM findings. YARA itself runs as an ordinary binary on the endpoint.

Where does the CTI hash enrichment show up in a finding?

Under wazuh.threat. A match adds indicator metadata such as type and identifier, feed information, optional malware or campaign names, provider tags, and a wazuh.threat.enrichments array naming the event field that matched. The file hash fields themselves are file.hash.md5, file.hash.sha1 and file.hash.sha256.

Should I send file hashes to a third-party reputation service?

Treat it as a data disclosure decision, not a technical one. A hash is a fingerprint of file content, and for small or structured files it can be reversed by anyone holding a matching corpus. Public reputation services also log the querying account. Check the policy position before you wire an active response script to an external API.