We reproduced your scenario in a lab with the same setup and validated three approaches for 4.14.x. Below are the answers to your questions and the steps for each option.
Lab used for the tests
- Wazuh 4.14.8 Docker single-node (manager, indexer and dashboard)
- Ubuntu 24.04 agent with Wazuh agent 4.14.8 and auditd 3.1.2, tested with both log_format = RAW and log_format = ENRICHED in /etc/audit/auditd.conf
- Your execve audit rules for a non-root account (-k user_cmd) and "user_cmd:command" in etc/lists/audit-keys
- Commands run through non-interactive SSH (ssh host 'cmd')
We got exactly the same result as you: rule 80792 fires, data.audit.execve only contains a0="bash" and a1="-c", and the command itself is only available hex-encoded in full_log.
1. Is there a supported way in 4.x to decode these values?
Not out of the box. auditd writes a value in hex whenever it contains spaces, double quotes, control characters or non-ASCII characters. In 4.x:
- The "audit" log format of the logcollector only groups all the records of the same event into a single line. It does not decode anything.
- The default decoder (0040-auditd_decoders.xml) only extracts quoted arguments (aN="..."), and it does not extract PROCTITLE.
- Decoders can extract values with regular expressions, but they cannot transform them, so there is no way to hex-decode a value in analysisd.
Issue #5446 tracks this and it is still open, feel free to add your use case there:
https://github.com/wazuh/wazuh/issues/54462. Is this on the roadmap?
Wazuh 5.0 is still under development, so we cannot confirm yet how this case will be handled in the final release. For now, we recommend the options below for 4.14.x, and reviewing the 5.0 release notes once it is published.
3. Recommended approach
It depends on what you need:
- Writing rules on the command content: option A (manager side) or option C (LAUREL).
- Searching the command in the dashboard: option B (indexer side) or option C (LAUREL).
Options A and B are meant to be used together: A extracts the hex values into their own fields, and B decodes those fields into readable text when the alert is indexed.
Option A: extract hex-encoded arguments with a modified auditd decoder
This follows the documented procedure to modify a default decoder.
Step 1. Copy the default decoder to the custom decoders folder, inside the manager container:
cp /var/ossec/ruleset/decoders/0040-auditd_decoders.xml /var/ossec/etc/decoders/
chown wazuh:wazuh /var/ossec/etc/decoders/0040-auditd_decoders.xml
chmod 660 /var/ossec/etc/decoders/0040-auditd_decoders.xml
In the single-node deployment /var/ossec/etc is stored in the wazuh_etc volume, so the file persists across container restarts.
Step 2. Edit /var/ossec/etc/decoders/0040-auditd_decoders.xml and paste the block below right after the last "auditd-syscall" decoder (the one commented as "PATH - FILE"), just before the comment that starts with "type=CONFIG_CHANGE".
Important: the new decoders must be inside the existing auditd-syscall block of this file. We first tried adding them in a separate file (for example local_decoder.xml) and they were never applied. Sibling decoders are evaluated as a contiguous chain, and decoders loaded from another file are appended after the rest of the auditd decoders (auditd-config_change, auditd-generic, and so on), so the chain never reaches them.
<!-- EXECVE / PROCTITLE: hex-encoded values (custom).
auditd writes a value in hex when it contains spaces, double quotes,
control characters or non-ASCII characters. -->
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a0=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a0_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a1=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a1_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a2=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a2_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a3=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a3_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a4=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a4_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a5=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a5_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a6=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a6_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a7=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.execve.a7_hex</order>
</decoder>
<decoder name="auditd-syscall">
<parent>auditd</parent>
<regex type="pcre2">type=PROCTITLE msg=audit\([^)]+\): proctitle=([0-9A-F]+)(?=\s|$)</regex>
<order>audit.proctitle_hex</order>
</decoder>
Step 3. Exclude the original file. In the single-node Docker deployment, ossec.conf is mounted from config/wazuh_cluster/wazuh_manager.conf on the Docker host. Add this line inside <ruleset>, below <decoder_dir>ruleset/decoders</decoder_dir>:
<decoder_exclude>ruleset/decoders/0040-auditd_decoders.xml</decoder_exclude>
Then restart the manager container (docker compose restart wazuh.manager). On startup the container copies the mounted configuration into /var/ossec/etc.
The alerts keep everything the default decoder extracts and also get these fields when a value is hex-encoded, for example:
data.audit.execve.a2_hex: 2F62696E2F6563686F202268656C6C6F20776F726C6422
data.audit.proctitle_hex: 62617368002D63002F62696E2F6563686F202268656C6C6F20776F726C6422
Step 4. Rules on the command content. Since the value is still hex, the rule has to match the hex-encoded text. You can get the hex of any string with:
printf '%s' 'hello world' | xxd -p -c 256 | tr a-f A-F
68656C6C6F20776F726C64
These are the two rules we validated (add them to /var/ossec/etc/rules/local_rules.xml and adjust the IDs and patterns). The first one checks every argument of the EXECVE record, the second one checks a single extracted field (the "sh -c" payload):
<group name="audit,audit_command,">
<rule id="100200" level="8">
<if_sid>80792</if_sid>
<field name="audit.key">^user_cmd$</field>
<regex type="pcre2">type=EXECVE msg=audit\([^)]+\): argc=\d+.*? a\d+=(?:[0-9A-F]{2})*?68656C6C6F20776F726C64</regex>
<description>Audit: command with "hello world" in its arguments run by a monitored account.</description>
</rule>
<rule id="100201" level="10">
<if_sid>80792</if_sid>
<field name="audit.execve.a1">^-c$</field>
<field name="audit.execve.a2_hex" type="pcre2">^(?:[0-9A-F]{2})*?C3B1616E64C3BA</field>
<description>Audit: shell command containing "ñandú" run by a monitored account.</description>
</rule>
</group>
The "(?:[0-9A-F]{2})*?" part keeps the match aligned to whole bytes, so a pattern cannot match across two characters. auditd writes the hex in uppercase, and multibyte UTF-8 characters work as well (C3B1616E64C3BA is "ñandú").
You can test the decoder and the rules with wazuh-logtest, pasting a full_log line from one of your alerts.
Option B: decode the values in the indexer to search them in the dashboard
This is done with an ingest pipeline that runs a painless script. It requires option A, because it decodes the aN_hex and proctitle_hex fields.
Note: the Wazuh Filebeat pipeline routes alerts to the daily index with a date_index_name processor, and because of that an index.final_pipeline setting on wazuh-alerts-4.x-* is never executed (we tested it). The approach that works is to create a separate pipeline and call it from the end of the Wazuh Filebeat pipeline.
Step 1. Save this as audit-hex-decode-pipeline.json:
{
"description": "Decode hex-encoded auditd EXECVE arguments and PROCTITLE (custom)",
"processors": [
{
"script": {
"tag": "audit_hex_decode",
"if": "ctx.data?.audit != null",
"lang": "painless",
"source": "String hexToString(String h, boolean nulToSpace) {\n if (h == null || h.length() % 2 != 0) { return null; }\n byte[] b = new byte[h.length() / 2];\n for (int i = 0; i < b.length; i++) {\n b[i] = (byte) Integer.parseInt(h.substring(2 * i, 2 * i + 2), 16);\n if (nulToSpace && b[i] == 0) { b[i] = 32; }\n }\n return Base64.getEncoder().encodeToString(b).decodeBase64();\n}\nMap audit = ctx.data.audit;\nif (audit.execve instanceof Map) {\n Map ex = audit.execve;\n for (int i = 0; i < 8; i++) {\n String k = 'a' + i + '_hex';\n if (ex.containsKey(k) && !ex.containsKey('a' + i)) {\n String v = hexToString(ex[k], false);\n if (v != null) { ex['a' + i] = v; }\n }\n }\n}\nif (audit.proctitle_hex != null) {\n String v = hexToString(audit.proctitle_hex, true);\n if (v != null) { audit.proctitle = v.trim(); }\n}",
"ignore_failure": true
}
}
]
}
Step 2. Create the pipeline and add it at the end of the Wazuh Filebeat pipeline (replace the indexer address and the password; jq is required):
curl -sk -u admin:<password> -X PUT "https://<indexer>:9200/_ingest/pipeline/wazuh-audit-hex-decode" -H 'Content-Type: application/json' -d @audit-hex-decode-pipeline.json
curl -sk -u admin:<password> "https://<indexer>:9200/_ingest/pipeline/filebeat-7.10.2-wazuh-alerts-pipeline" -o wazuh-alerts-pipeline.json
jq '.["filebeat-7.10.2-wazuh-alerts-pipeline"] | .processors += [{"pipeline":{"name":"wazuh-audit-hex-decode","if":"ctx.data?.audit != null","ignore_failure":true}}]' wazuh-alerts-pipeline.json > wazuh-alerts-pipeline-patched.json
curl -sk -u admin:<password> -X PUT "https://<indexer>:9200/_ingest/pipeline/filebeat-7.10.2-wazuh-alerts-pipeline" -H 'Content-Type: application/json' -d @wazuh-alerts-pipeline-patched.json
Keep wazuh-alerts-pipeline.json as a backup in case you need to roll back.
New alerts get readable fields next to the hex ones, for example:
data.audit.execve.a2: /bin/echo "hello world" "foo bar"; /bin/echo "ñandú"
data.audit.proctitle: bash -c /bin/echo "hello world"
They are indexed as keyword fields, so you can search them in the dashboard, for example data.audit.execve.a2:*hello* (wildcard searches on keyword fields are case sensitive). If the new fields do not show up in Discover, refresh the wazuh-alerts-* index pattern.
Considerations:
- The decoding happens only when the alert is indexed, so rules on the manager cannot use the decoded values (use option A or C for rules). Alerts that are already indexed are not modified.
- Upgrading Wazuh, or running filebeat setup --pipelines, can overwrite the Wazuh Filebeat pipeline. In that case repeat Step 2. The wazuh-audit-hex-decode pipeline itself is not affected.
Option C: LAUREL and the JSON decoder
LAUREL runs as an auditd plugin and writes each audit event as a single JSON line, with the arguments already decoded (EXECVE.ARGV is a list) and extra context such as the parent process (SPAWNED_BY). We tested LAUREL 0.8.2 with its default configuration, installed following the project instructions.
Step 1. On the agent, monitor the LAUREL output with the JSON log format:
<localfile>
<log_format>json</log_format>
<location>/var/log/laurel/audit.log</location>
</localfile>
Step 2. Add your own rules. There is no official Wazuh ruleset for the LAUREL output, so the default auditd rules (including 80792) do not apply to these events. These are the rules we validated, the base rule reuses your audit-keys list the same way rule 80792 does:
<group name="audit,laurel,">
<rule id="100300" level="3">
<decoded_as>json</decoded_as>
<field name="EXECVE.argc">\d+</field>
<list field="SYSCALL.key" lookup="match_key_value" check_value="command">etc/lists/audit-keys</list>
<description>LAUREL: command run by a monitored account: $(EXECVE.ARGV)</description>
<group>audit_command,</group>
</rule>
<rule id="100301" level="8">
<if_sid>100300</if_sid>
<field name="EXECVE.ARGV">hello world</field>
<description>LAUREL: command with "hello world" in its arguments run by a monitored account.</description>
</rule>
</group>
The alert contains the full command, for example data.EXECVE.ARGV: ["bash", "-c", "/bin/echo \"hello world\" laurel-live"], and the field is searchable in the dashboard.
Considerations:
- Use a single source for these commands. If the agent reads both /var/log/audit/audit.log and the LAUREL log, each command generates two alerts, one from rule 80792 and one from the LAUREL rules.
- Any other auditd rule you rely on (0365-auditd_rules.xml) would need an equivalent written for the JSON fields.
- LAUREL events contain both numeric fields (auid) and resolved names (AUID). In our tests, $(SYSCALL.AUID) in a rule description returned the numeric value, so check a field before using it in descriptions.
4. Is there a reference ruleset for the LAUREL JSON output?
No, there is no official one, this is what issue #14461 requests and it is still open:
https://github.com/wazuh/wazuh/issues/14461The rules in option C can be used as a starting point.
Additional notes
- Use EXECVE, not PROCTITLE, as the source of the executed command. The kernel truncates PROCTITLE to 128 bytes, and in some cases (for example when bash -c executes its last command directly) the PROCTITLE record shows the command line of the calling shell instead of the executed program.
- auditd splits very long arguments into several fields (a2_len, a2[0], a2[1], and so on). The decoders above do not cover that format.
- The decoders above cover arguments a0 to a7, the same as the default decoder.
- Excluding the original decoder file means it will not receive updates in future upgrades, so compare it with the new default file after each upgrade.
References
- Modifying default decoders:
https://documentation.wazuh.com/current/user-manual/ruleset/decoders/custom.html- Sibling decoders:
https://documentation.wazuh.com/current/user-manual/ruleset/decoders/sibling-decoders.html- Decoders syntax (regex type pcre2):
https://documentation.wazuh.com/current/user-manual/ruleset/ruleset-xml-syntax/decoders.html- Rules syntax:
https://documentation.wazuh.com/current/user-manual/ruleset/ruleset-xml-syntax/rules.html- CDB lists:
https://documentation.wazuh.com/current/user-manual/ruleset/cdb-list.html- JSON decoder:
https://documentation.wazuh.com/current/user-manual/ruleset/decoders/json-decoder.html- Testing decoders and rules:
https://documentation.wazuh.com/current/user-manual/ruleset/testing.html- Ruleset configuration (decoder_exclude):
https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/ruleset.html- Localfile configuration:
https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/localfile.html- Auditd configuration:
https://documentation.wazuh.com/current/user-manual/capabilities/system-calls-monitoring/audit-configuration.html- Wazuh Docker container usage:
https://documentation.wazuh.com/current/deployment-options/docker/container-usage.html- LAUREL:
https://github.com/threathunters-io/laurel