The Wazuh cheat sheet covers SIEM/XDR architecture, agent deployment, custom rules and decoders, MITRE ATT&CK mapping, SCA, FIM, vulnerability detection, threat intelligence, API usage, integrations, and SOC tuning.
# --- Agent status ---
/var/ossec/bin/agent_control -l # list all agents
/var/ossec/bin/agent_control -i <id> # agent detail
/var/ossec/bin/agent_control -R <id> # restart agent remotely
# --- Service control ---
/var/ossec/bin/wazuh-control start|stop|restart|status|reload
# --- Rule/decoder testing ---
/var/ossec/bin/wazuh-logtest # interactive log tester (Wazuh 4.2+)
/var/ossec/bin/wazuh-logtest -v # verbose (show all decoders tried)
# Legacy name: ossec-logtest (symlinked in some installs)
echo "Apr 10 10:00:00 host sshd[1234]: Failed password for root from 10.0.0.1 port 22 ssh2" \
| /var/ossec/bin/wazuh-logtest
# --- Real-time alert tailing ---
tail -f /var/ossec/logs/alerts/alerts.log
tail -f /var/ossec/logs/alerts/alerts.json
# --- Config validation ---
/var/ossec/bin/wazuh-analysisd -t # test ossec.conf syntax
# --- Groups ---
/var/ossec/bin/agent_groups -l # list groups
/var/ossec/bin/agent_groups -s -i <id> # show agent group membership
# --- FIM manual scan trigger ---
/var/ossec/bin/agent_control -a -u <id> # run syscheck on agent
# --- API token (quick) ---
TOKEN=$(curl -su wazuh:wazuh -k -X POST \
"https://localhost:55000/security/user/authenticate?raw=true")
| Item | Path |
|---|---|
/var/ossec/etc/ossec.conf |
|
| Agent shared config | /var/ossec/etc/shared/<group>/agent.conf |
/var/ossec/etc/rules/local_rules.xml |
|
| Custom decoders | /var/ossec/etc/decoders/local_decoder.xml |
| CDB lists | /var/ossec/etc/lists/ |
| SCA policies | /var/ossec/etc/shared/ |
/var/ossec/logs/alerts/alerts.log |
|
| Alerts JSON | /var/ossec/logs/alerts/alerts.json |
| Archives | /var/ossec/logs/archives/archives.log |
| Manager log | /var/ossec/logs/ossec.log |
| API log | /var/ossec/logs/api.log |
/var/ossec/ruleset/rules/ |
|
| Built-in decoders | /var/ossec/ruleset/decoders/ |
| Component | Role | Default Port |
|---|---|---|
| Core engine: rules, decoders, analysis, alerts | 1514 TCP/UDP | |
| Wazuh Agent | Endpoint collector: logs, FIM, SCA, inventory | -> 1514 |
| Agent registration & key exchange | 1515 TCP | |
| Syslog Collector | Agentless: network devices, firewalls | 514 UDP |
| OpenSearch-based alert storage & search | 9200, 9300 | |
| OpenSearch Dashboards web UI | 443 | |
| Filebeat | Log shipper: Manager to Indexer | internal |
| Multi-manager orchestration node | 1516 TCP | |
| Cluster Worker | Distributed processing node | 1516 TCP |
ENDPOINTS WAZUH MANAGER BACKEND
───────── ───────────── ───────
[Linux Agent] ─┐ ┌─────────────────────────┐
[Win Agent ] ─┤ 1514/TCP │ wazuh-remoted │ ┌───────────────┐
[macOS Agent] ─┼──(encrypted)──│ wazuh-analysisd │──────>│ Wazuh Indexer │
[Docker Agent] ┘ │ wazuh-maild │ │ (OpenSearch) │
│ wazuh-monitord │ │ :9200/:9300 │
[Firewall ] ─┐ 514/UDP │ wazuh-logcollector │ └───────┬───────┘
[Router ] ─┤──(syslog)────>│ wazuh-execd │ │
[Switch ] ─┘ │ wazuh-db │ ┌───────▼───────┐
└────────────┬────────────┘ │ Dashboard │
[Agentless ] ──(SSH/SNMP)────> │ │ :443 │
Filebeat └───────────────┘
│
┌───────▼───────┐
│ Active │
│ Response │
│ (firewall- │
│ drop, etc.) │
└───────────────┘
CLUSTER (optional):
[Master :1516] <──────────────> [Worker :1516] <── Agents
# Import GPG key
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH \
| gpg --no-default-keyring \
--keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg \
--import
chmod 644 /usr/share/keyrings/wazuh.gpg
# Add repository
echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] \
https://packages.wazuh.com/4.x/apt/ stable main" \
| tee /etc/apt/sources.list.d/wazuh.list
# Install with manager address pre-set
apt-get update
WAZUH_MANAGER="10.0.0.1" apt-get install -y wazuh-agent
# Enable and start
systemctl daemon-reload
systemctl enable --now wazuh-agent
# Add repo
cat > /etc/yum.repos.d/wazuh.repo << 'EOF'
[wazuh]
gpgcheck=1
gpgkey=https://packages.wazuh.com/key/GPG-KEY-WAZUH
enabled=1
name=EL-$releasever - Wazuh
baseurl=https://packages.wazuh.com/4.x/yum/
protect=1
EOF
# Install
WAZUH_MANAGER="10.0.0.1" yum install -y wazuh-agent
systemctl daemon-reload
systemctl enable --now wazuh-agent
# Download installer
Invoke-WebRequest `
-Uri "https://packages.wazuh.com/4.x/windows/wazuh-agent-4.x.x-1.msi" `
-OutFile "$env:TEMP\wazuh-agent.msi"
# Silent install with manager and group
msiexec /i "$env:TEMP\wazuh-agent.msi" /q `
WAZUH_MANAGER="10.0.0.1" `
WAZUH_AGENT_GROUP="windows-servers" `
WAZUH_REGISTRATION_PASSWORD="MySecret"
# Start service
NET START WazuhSvc
# Verify
Get-Service WazuhSvc
# Download pkg
curl -so wazuh-agent.pkg \
"https://packages.wazuh.com/4.x/macos/wazuh-agent-4.x.x-1.pkg"
# Pre-configure manager
echo "WAZUH_MANAGER='10.0.0.1'" > /tmp/wazuh_envs
echo "WAZUH_AGENT_GROUP='macos'" >> /tmp/wazuh_envs
# Install
installer -pkg wazuh-agent.pkg -target /
# Start
/Library/Ossec/bin/wazuh-control start
docker run -d --name wazuh-agent \
--restart unless-stopped \
-e WAZUH_MANAGER="10.0.0.1" \
-e WAZUH_AGENT_GROUP="docker-hosts" \
-e WAZUH_REGISTRATION_PASSWORD="MySecret" \
-v /var/log:/var/log:ro \
-v /etc:/etc:ro \
--pid=host \
--network=host \
--privileged \
wazuh/wazuh/wazuh-agent:latest
# On agent: register with manager enrollment service (port 1515)
/var/ossec/bin/agent-auth \
-m 10.0.0.1 \
-p 1515 \
-G linux-servers \
-P "EnrollmentPassword"
# Verify key was issued
cat /var/ossec/etc/client.keys
# Create group
/var/ossec/bin/agent_groups -a -g linux-servers -q
/var/ossec/bin/agent_groups -a -g windows-servers -q
/var/ossec/bin/agent_groups -a -g dmz -q
# List all groups
/var/ossec/bin/agent_groups -l
# Assign agent to group
/var/ossec/bin/agent_groups -a -i 003 -g linux-servers -q
# Show group members
/var/ossec/bin/agent_groups -s -g linux-servers
# Remove agent from group
/var/ossec/bin/agent_groups -r -i 003 -g linux-servers -q
# API: create group
curl -k -X POST "https://localhost:55000/groups" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"group_id":"linux-servers"}'
# API: assign agent to group
curl -k -X PUT \
"https://localhost:55000/agents/003/group/linux-servers" \
-H "Authorization: Bearer $TOKEN"
<!-- /var/ossec/etc/shared/linux-servers/agent.conf -->
<!-- Pushed automatically to all agents in the group -->
<agent_config>
<!-- Extra log collection for this group -->
<localfile>
<log_format>syslog</log_format>
<location>/var/log/auth.log</location>
</localfile>
<!-- Group-specific FIM paths -->
<syscheck>
<directories realtime="yes" whodata="yes">/opt/app</directories>
<directories check_all="yes" report_changes="yes">/etc/nginx</directories>
</syscheck>
<!-- Group-specific SCA policy -->
<sca>
<policies>
<policy>etc/shared/cis_debian_linux_rcl.yml</policy>
</policies>
</sca>
</agent_config>
<!-- /var/ossec/etc/ossec.conf -->
<ossec_config>
<global>
<email_notification>yes</email_notification>
<email_to>[email protected]</email_to>
<smtp_server>mail.company.com</smtp_server>
<email_from>[email protected]</email_from>
<email_maxperhour>12</email_maxperhour>
<email_log_source>alerts.log</email_log_source>
<agents_disconnection_time>10m</agents_disconnection_time>
<agents_disconnection_alert_time>0</agents_disconnection_alert_time>
</global>
<!-- Ruleset paths -->
<ruleset>
<decoder_dir>ruleset/decoders</decoder_dir>
<rule_dir>ruleset/rules</rule_dir>
<rule_exclude>0215-policy_rules.xml</rule_exclude>
<list>etc/lists/audit-keys</list>
<list>etc/lists/amazon/aws-eventnames</list>
<!-- Custom lists for threat intel -->
<list>etc/lists/malicious-ips</list>
<list>etc/lists/malicious-hashes</list>
</ruleset>
<!-- Remote agent connection -->
<remote>
<connection>secure</connection>
<port>1514</port>
<protocol>tcp</protocol>
<queue_size>131072</queue_size>
</remote>
<!-- Alerts config -->
<alerts>
<log_alert_level>3</log_alert_level>
<email_alert_level>12</email_alert_level>
</alerts>
</ossec_config>
<!-- /var/ossec/etc/ossec.conf (on agent) -->
<ossec_config>
<client>
<server>
<address>10.0.0.1</address>
<port>1514</port>
<protocol>tcp</protocol>
</server>
<config-profile>ubuntu, ubuntu22</config-profile>
<notify_time>10</notify_time>
<time-reconnect>60</time-reconnect>
<auto_restart>yes</auto_restart>
<enrollment>
<enabled>yes</enabled>
<manager_address>10.0.0.1</manager_address>
<port>1515</port>
</enrollment>
</client>
<!-- Windows event channels -->
<localfile>
<log_format>eventchannel</log_format>
<location>Security</location>
<query>Event/System[EventID != 4688]</query>
</localfile>
<localfile>
<log_format>eventchannel</log_format>
<location>Microsoft-Windows-Sysmon/Operational</location>
</localfile>
<localfile>
<log_format>eventchannel</log_format>
<location>Microsoft-Windows-PowerShell/Operational</location>
</localfile>
</ossec_config>
| Field | Type | Description |
|---|---|---|
id |
attr | Unique rule ID (100000-999999 for custom) |
level |
attr | Alert severity 0-15 |
frequency |
attr | |
timeframe |
attr | Seconds window for frequency counting |
overwrite |
attr | yes to replace existing built-in rule |
noalert |
attr | 1 to fire but suppress alert output |
<if_sid> |
elem | Match if a prior rule ID has fired |
<if_group> |
elem | Match if prior event was in this group |
<if_level> |
elem | Match if prior event reached this level |
<if_matched_sid> |
elem | |
<if_matched_group> |
elem | Frequency match on group within timeframe |
<match> |
elem | Substring/sregex match against full log |
<regex> |
elem | POSIX regex against full log |
<field name=""> |
elem | |
<srcip> |
elem | Source IP or CIDR match |
<dstip> |
elem | Destination IP or CIDR match |
<user> |
elem | Username match (supports negate) |
<program_name> |
elem | Process name match |
<hostname> |
elem | Agent hostname match |
<url> |
elem | URL field match |
<status> |
elem | Status field match |
<action> |
elem | Action field match |
<same_srcip> |
elem | Correlation: same source IP across events |
<different_user> |
elem | Correlation: different user across events |
<list field=""> |
elem | |
<description> |
elem | Human-readable alert message |
<group> |
elem | Comma-separated alert categories |
<mitre><id> |
elem | MITRE ATT&CK technique mapping |
<options> |
elem | Flags: no_full_log, no_log, alert_by_email |
<!-- /var/ossec/etc/rules/local_rules.xml -->
<group name="local,custom,">
<!-- ── BASIC: match string in decoded field ── -->
<rule id="100001" level="6">
<if_group>syslog</if_group>
<match>sudo: authentication failure</match>
<description>Failed sudo attempt</description>
<group>authentication_failure,pam,</group>
</rule>
<!-- ── COMPOSITE: brute force (5 failures / 120 sec from same IP) ── -->
<rule id="100002" level="12" frequency="5" timeframe="120">
<if_matched_sid>100001</if_matched_sid>
<same_srcip />
<description>Brute force: multiple sudo failures from $(srcip)</description>
<group>authentication_failures,brute_force,</group>
<mitre>
<id>T1110</id>
</mitre>
</rule>
<!-- ── FIELD: match specific decoded field with PCRE2 ── -->
<rule id="100003" level="10">
<if_group>web</if_group>
<field name="url" type="pcre2">(?i)\.(php|asp|aspx|jsp)\?.*=.*(\.\./|cmd=|exec=)</field>
<status>200</status>
<description>Web app exploit attempt: LFI/RFI or command injection</description>
<group>web_attack,attack,</group>
<mitre>
<id>T1190</id>
</mitre>
</rule>
<!-- ── NEGATION: alert if sshd event is NOT from allowed subnet ── -->
<rule id="100004" level="8">
<if_group>sshd</if_group>
<action>^Accepted</action>
<srcip negate="yes">10.0.0.0/8</srcip>
<srcip negate="yes">192.168.0.0/16</srcip>
<description>SSH login from unexpected source: $(srcip)</description>
<group>authentication_success,anomaly,</group>
<mitre>
<id>T1078</id>
</mitre>
</rule>
<!-- ── CDB LIST: alert if srcip is in threat intel list ── -->
<rule id="100005" level="14">
<if_group>firewall</if_group>
<list field="srcip" lookup="address_match_key">etc/lists/malicious-ips</list>
<description>Connection from known malicious IP: $(srcip)</description>
<group>threat_intel,attack,</group>
<mitre>
<id>T1071</id>
</mitre>
</rule>
<!-- ── OVERWRITE: lower level of noisy built-in rule ── -->
<rule id="5710" level="0" overwrite="yes">
<if_sid>5710</if_sid>
<description>SSH failed login (suppressed - handled by custom rule)</description>
</rule>
</group>
| Level | Severity | Use Case | Example |
|---|---|---|---|
| 0 | Ignored | Suppress noisy events | Routine cron output |
| 1-3 | Low-value events | Successful logins, normal ops | |
| 4-6 | Noteworthy activity | Config changes, new connections | |
| 7-9 | Security-relevant events | Failed logins, FIM changes | |
| 10-11 | Attack indicators | Privilege escalation attempts | |
| 12-13 | Active attack | Brute force, exploit attempts | |
| 14-15 | Confirmed compromise | Rootkit, credential dump |
<group name="threat_detection,custom,">
<!-- T1003 - Mimikatz / credential dumping -->
<rule id="110001" level="15">
<if_group>windows</if_group>
<field name="win.eventdata.commandLine" type="pcre2">
(?i)(sekurlsa|lsadump|privilege::debug|token::elevate|kerberos::ptt|dpapi::)
</field>
<description>Mimikatz execution detected: $(win.eventdata.commandLine)</description>
<group>credential_access,attack,</group>
<mitre><id>T1003</id></mitre>
</rule>
<!-- T1550.002 - Pass-the-Hash (EventID 4624 logon type 3 with NtLm) -->
<rule id="110002" level="14">
<if_group>windows</if_group>
<field name="win.system.eventID">^4624$</field>
<field name="win.eventdata.logonType">^3$</field>
<field name="win.eventdata.authenticationPackageName">^NTLM$</field>
<field name="win.eventdata.workstationName" negate="yes">$(win.eventdata.targetDomainName)</field>
<description>Possible Pass-the-Hash: NTLM Type3 from $(win.eventdata.ipAddress)</description>
<group>lateral_movement,credential_access,</group>
<mitre><id>T1550.002</id></mitre>
</rule>
<!-- T1021.002 - PsExec lateral movement (Sysmon EventID 1 or SC service creation) -->
<rule id="110003" level="13">
<if_group>windows</if_group>
<field name="win.eventdata.commandLine" type="pcre2">(?i)(psexec|psexesvc)</field>
<description>PsExec execution detected on $(agent.name)</description>
<group>lateral_movement,attack,</group>
<mitre><id>T1021.002</id></mitre>
</rule>
<!-- T1041 - Data exfiltration over HTTP (large POST to external IP) -->
<rule id="110004" level="12">
<if_group>web_log</if_group>
<field name="http.method">^POST$</field>
<field name="http.response_bytes" type="pcre2">^[1-9][0-9]{6,}$</field>
<description>Large outbound POST - possible data exfiltration: $(srcip)</description>
<group>exfiltration,attack,</group>
<mitre><id>T1041</id></mitre>
</rule>
<!-- T1505.003 - Webshell creation detected by FIM -->
<rule id="110005" level="15">
<if_group>syscheck</if_group>
<field name="file" type="pcre2">(?i)\.(php|asp|aspx|jsp|cfm)$</field>
<field name="syscheck.event">added</field>
<field name="file" type="pcre2">(?i)(www|html|public_html|htdocs|webroot)</field>
<description>Webshell possibly dropped in web root: $(file)</description>
<group>web_attack,attack,</group>
<mitre><id>T1505.003</id></mitre>
</rule>
<!-- T1078.003 - Privilege escalation: new sudoer added -->
<rule id="110006" level="14">
<if_group>syscheck</if_group>
<field name="file">/etc/sudoers|/etc/sudoers.d/</field>
<field name="syscheck.event">modified|added</field>
<description>Sudoers file modified - possible privilege escalation</description>
<group>privilege_escalation,attack,</group>
<mitre><id>T1078.003</id></mitre>
</rule>
<!-- T1053.005 - Scheduled task created (Windows EventID 4698) -->
<rule id="110007" level="12">
<if_group>windows</if_group>
<field name="win.system.eventID">^4698$</field>
<description>Scheduled task created: $(win.eventdata.taskName) by $(win.eventdata.subjectUserName)</description>
<group>persistence,attack,</group>
<mitre><id>T1053.005</id></mitre>
</rule>
<!-- T1136.001 - New local user created (Windows EventID 4720) -->
<rule id="110008" level="11">
<if_group>windows</if_group>
<field name="win.system.eventID">^4720$</field>
<description>New Windows user created: $(win.eventdata.targetUserName) by $(win.eventdata.subjectUserName)</description>
<group>persistence,account_management,</group>
<mitre><id>T1136.001</id></mitre>
</rule>
<!-- T1070.001 - Security log cleared (Windows EventID 1102) -->
<rule id="110009" level="14">
<if_group>windows</if_group>
<field name="win.system.eventID">^1102$</field>
<description>Windows Security event log cleared by $(win.eventdata.subjectUserName)</description>
<group>defense_evasion,attack,</group>
<mitre><id>T1070.001</id></mitre>
</rule>
<!-- T1059.001 - Suspicious PowerShell (encoded commands / download cradles) -->
<rule id="110010" level="13">
<if_group>windows</if_group>
<field name="win.eventdata.commandLine" type="pcre2">
(?i)(-enc|-encodedcommand|iex\s*\(|invoke-expression|downloadstring|webclient|bitstransfer)
</field>
<description>Suspicious PowerShell execution: $(win.eventdata.commandLine)</description>
<group>execution,attack,</group>
<mitre><id>T1059.001</id></mitre>
</rule>
</group>
| Field | Description |
|---|---|
<prematch> |
Regex the log must match before field extraction; anchors decoder to log type |
<program_name> |
Match against syslog program name field |
<parent> |
|
<regex> |
POSIX/PCRE2 with capture groups () for field extraction |
<order> |
Comma-separated field names mapping to regex capture groups |
<type> |
Log category: syslog, firewall, ids, web-log, windows, ossec |
<accumulate> |
Correlate multi-line events via decoded id field |
<plugin_decoder> |
Use built-in parsers: JSON_Decoder, PF_Decoder, OSSECAlert_Decoder |
<fts> |
First-time-seen: alert on first occurrence of field value |
<var name=""> |
Define reusable regex variable at file level |
<!-- /var/ossec/etc/decoders/local_decoder.xml -->
<!-- ── Variable definition ── -->
<decoder-list>
<var name="IP_REGEX">(?:\d{1,3}\.){3}\d{1,3}</var>
</decoder-list>
<!-- ── Parent: identifies the log source ── -->
<decoder name="myapp">
<program_name>^myapp$</program_name>
<prematch>^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}</prematch>
<type>syslog</type>
</decoder>
<!-- ── Child: extracts fields from auth events ── -->
<decoder name="myapp-auth-success">
<parent>myapp</parent>
<prematch>AUTH_SUCCESS</prematch>
<regex>User (\S+) authenticated from ($IP_REGEX) port (\d+)</regex>
<order>user, srcip, srcport</order>
</decoder>
<!-- ── Child: extracts fields from auth failures ── -->
<decoder name="myapp-auth-fail">
<parent>myapp</parent>
<prematch>AUTH_FAILED</prematch>
<regex>Failed login for (\S+) from ($IP_REGEX): (\S+)</regex>
<order>user, srcip, extra_data</order>
</decoder>
<!-- ── JSON decoder (for structured log output) ── -->
<decoder name="myapp-json">
<parent>myapp</parent>
<prematch>{</prematch>
<plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>
<!-- Test: echo '2024-01-01T10:00:00 AUTH_SUCCESS User bob authenticated from 10.0.0.5 port 52001' | /var/ossec/bin/wazuh-logtest -->
<!-- Single technique -->
<rule id="120001" level="13">
<if_group>syslog</if_group>
<match>su: FAILED</match>
<description>Failed su command</description>
<mitre>
<id>T1548.003</id>
</mitre>
</rule>
<!-- Multiple techniques in one rule -->
<rule id="120002" level="14">
<if_group>windows</if_group>
<field name="win.eventdata.commandLine" type="pcre2">
(?i)(net\s+user|net\s+localgroup|dsquery|Get-ADUser)
</field>
<description>Active Directory enumeration detected</description>
<mitre>
<id>T1087.002</id> <!-- Account Discovery: Domain Account -->
<id>T1069.002</id> <!-- Permission Groups Discovery: Domain Groups -->
</mitre>
</rule>
# Kibana/OpenSearch Dashboards - filter by technique
rule.mitre.id: "T1059.001"
# All credential access techniques
rule.mitre.tactic: "Credential Access"
# Top MITRE techniques (last 24h)
# Dashboard > MITRE ATT&CK > Framework tab > filter timerange
# API: get alerts by MITRE technique
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/alerts?q=rule.mitre.id=T1059.001&limit=100&pretty=true"
| Tactic | Common Techniques | Wazuh Rule Groups |
|---|---|---|
| T1190, T1133, T1566 | web_attack, phishing |
|
| T1059, T1053, T1204 | execution, windows |
|
| T1053, T1136, T1543 | persistence, account_management |
|
| T1548, T1078, T1134 | privilege_escalation |
|
| T1070, T1562, T1055 | defense_evasion |
|
| T1003, T1110, T1558 | credential_access, brute_force |
|
| Lateral Movement | T1021, T1550, T1563 | lateral_movement |
| Exfiltration | T1041, T1048, T1052 | exfiltration |
<!-- In ossec.conf or agent.conf -->
<syscheck>
<!-- Scan interval (seconds); default 43200 = 12h -->
<frequency>43200</frequency>
<!-- Scheduled scan at specific time -->
<scan_time>02:00</scan_time>
<scan_day>saturday</scan_day>
<!-- Disable at startup -->
<scan_on_start>yes</scan_on_start>
<!-- ── Realtime monitoring (inotify/FSEvents) ── -->
<directories realtime="yes">/var/www/html</directories>
<!-- ── Whodata: who changed the file (audit subsystem) ── -->
<directories whodata="yes" realtime="yes">/etc</directories>
<!-- ── Full options ── -->
<directories
check_all="yes"
report_changes="yes"
realtime="yes"
whodata="yes"
recursion_level="3"
tags="critical_config"
>/etc/ssh,/etc/pam.d,/etc/sudoers.d</directories>
<!-- ── Windows: critical system paths ── -->
<directories check_all="yes" realtime="yes">%WINDIR%\System32\drivers</directories>
<directories check_all="yes" realtime="yes">%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Startup</directories>
<!-- ── Ignore patterns ── -->
<ignore>/etc/mtab</ignore>
<ignore>/etc/mnttab</ignore>
<ignore type="sregex">\.log$|\.tmp$|\.swp$</ignore>
<!-- ── Nodiff: suppress content diff for sensitive files ── -->
<nodiff>/etc/ssl/private</nodiff>
<nodiff type="sregex">\.key$|\.pem$|id_rsa</nodiff>
<!-- ── Windows Registry monitoring ── -->
<windows_registry>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run</windows_registry>
<windows_registry arch="both">HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services</windows_registry>
<!-- ── Registry ignore ── -->
<registry_ignore>HKEY_LOCAL_MACHINE\Security\Policy\Secrets</registry_ignore>
<!-- ── Restrict: only watch specific file types ── -->
<directories restrict="\.conf$|\.cfg$|\.ini$">/opt/app/config</directories>
</syscheck>
| Mode | Trigger | CPU Impact | Use Case |
|---|---|---|---|
scheduled |
Periodic scan (frequency) | Non-critical paths | |
realtime |
inotify/FSEvents event | Web roots, critical configs | |
whodata |
Audit daemon + inotify | Forensic-grade: user + PID tracked |
<!-- In ossec.conf or agent.conf -->
<sca>
<enabled>yes</enabled>
<!-- Scan interval -->
<interval>12h</interval>
<scan_on_start>yes</scan_on_start>
<!-- Skip failed policies -->
<skip_nfs>yes</skip_nfs>
<policies>
<!-- Enable specific built-in policies -->
<policy>etc/shared/cis_debian_linux_rcl.yml</policy>
<policy>etc/shared/cis_rhel9_linux_rcl.yml</policy>
<!-- Custom policy -->
<policy>etc/shared/custom_hardening.yml</policy>
<!-- Disable a policy -->
<policy enabled="no">etc/shared/web_vulnerabilities.yml</policy>
</policies>
</sca>
# /var/ossec/etc/shared/custom_hardening.yml
policy:
id: "custom_linux_hardening"
file: "custom_hardening.yml"
name: "Custom Linux Hardening Policy"
description: "Organization-specific security checks"
references:
- "https://wiki.internal/hardening"
requirements:
title: "Check Linux is installed"
description: "Requirements to run this policy"
condition: all
rules:
- 'f:/etc/os-release'
variables:
$sshd_config: /etc/ssh/sshd_config
$pam_config: /etc/pam.d/common-password
checks:
- id: 1000
title: "SSH root login is disabled"
description: "Ensure PermitRootLogin is set to no"
rationale: "Preventing direct root login reduces attack surface"
remediation: "Set PermitRootLogin no in /etc/ssh/sshd_config and restart sshd"
compliance:
- cis: "5.2.8"
- pci_dss: "2.2.4"
condition: all
rules:
- 'f:$sshd_config -> !r:^# && r:PermitRootLogin && r:no'
- id: 1001
title: "SSH MaxAuthTries is 4 or less"
condition: all
rules:
- 'f:$sshd_config -> n:MaxAuthTries\s+(\d+) compare <= 4'
- id: 1002
title: "No accounts with empty passwords"
condition: none
rules:
- 'f:/etc/shadow -> r:^\w+::+'
- id: 1003
title: "Firewall is active"
condition: any
rules:
- 'p:ufw'
- 'p:firewalld'
- 'c:iptables -L -> r:Chain INPUT'
- id: 1004
title: "Audit daemon is running"
condition: all
rules:
- 'p:auditd'
- id: 2000
title: "Windows: SMBv1 is disabled"
condition: all
rules:
- 'r:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters -> SMB1 -> 0'
# Get SCA summary for all agents
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/sca?pretty=true"
# Get SCA results for specific agent
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/sca/003?pretty=true"
# Get failed checks for an agent
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/sca/003/checks/custom_linux_hardening?result=failed&pretty=true"
<!-- In /var/ossec/etc/ossec.conf on manager -->
<vulnerability-detection>
<enabled>yes</enabled>
<!-- Index vulnerability data to Wazuh Indexer -->
<index-status>yes</index-status>
<!-- Feed update interval (min 60m) -->
<feed-update-interval>12h</feed-update-interval>
</vulnerability-detection>
<!-- Syscollector: must be enabled on agents to collect inventory -->
<wodle name="syscollector">
<disabled>no</disabled>
<interval>1h</interval>
<scan_on_start>yes</scan_on_start>
<packages>yes</packages>
<os>yes</os>
<processes>yes</processes>
<ports all="no">yes</ports>
<network>yes</network>
</wodle>
# Get all vulnerabilities for an agent
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/vulnerability/003?pretty=true"
# Filter by severity
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/vulnerability/003?severity=critical&pretty=true"
# Get CVE details
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/vulnerability/003?cve=CVE-2021-44228&pretty=true"
# Get all agents with critical vulns
curl -k -H "Authorization: Bearer $TOKEN" \
"https://localhost:55000/vulnerability?severity=critical&limit=500&pretty=true"
# Dashboard KQL - find critical vulns
# vulnerability.severity: "critical" AND vulnerability.status: "active"
vulnerability.cve - CVE identifier
vulnerability.severity - critical/high/medium/low
vulnerability.cvss2_score - CVSS v2 score
vulnerability.cvss3_score - CVSS v3 score
vulnerability.package.name - Affected package
vulnerability.package.version - Installed version
vulnerability.status - active/solved
# Get JWT token
TOKEN=$(curl -su wazuh:wazuh -k -X POST \
"https://localhost:55000/security/user/authenticate?raw=true")
# Or with custom credentials
TOKEN=$(curl -su myuser:mypass -k -X POST \
"https://localhost:55000/security/user/authenticate?raw=true")
# All requests use: -H "Authorization: Bearer $TOKEN"
# API config: /var/ossec/api/configuration/api.yaml
# Default port: 55000
| Method | Endpoint | Description |
|---|---|---|
GET |
/agents |
|
GET |
/agents/summary/status |
Agent count by status |
GET |
/agents/{id} |
Specific agent detail |
PUT |
/agents/{id}/restart |
Restart an agent |
DELETE |
/agents |
Remove agents (query param: agents_list) |
GET |
/agents/{id}/config/{component}/{configuration} |
Get agent config |
GET |
/groups |
|
POST |
/groups |
Create group |
PUT |
/agents/{id}/group/{group_id} |
Assign agent to group |
GET |
/rules |
List detection rules |
GET |
/rules?search=mimikatz |
Search rules |
GET |
/decoders |
List decoders |
GET |
/alerts |
|
GET |
/syscheck/{id} |
FIM events for agent |
PUT |
/syscheck/{id} |
Run FIM scan on agent |
GET |
/sca/{id} |
SCA results for agent |
GET |
/vulnerability/{id} |
Vulnerability scan results |
GET |
/manager/info |
Manager version and info |
GET |
/manager/logs |
Manager log tail |
PUT |
/manager/restart |
Restart manager |
GET |
/cluster/nodes |
Cluster node status |
BASE="https://localhost:55000"
H="Authorization: Bearer $TOKEN"
# --- Agents ---
# List active agents
curl -sk -H "$H" "$BASE/agents?status=active&pretty=true"
# Get disconnected agents
curl -sk -H "$H" "$BASE/agents?status=disconnected&select=id,name,lastKeepAlive&pretty=true"
# Get agent OS inventory
curl -sk -H "$H" "$BASE/syscollector/003/os?pretty=true"
# Get agent packages
curl -sk -H "$H" "$BASE/syscollector/003/packages?pretty=true&limit=100"
# Get agent processes
curl -sk -H "$H" "$BASE/syscollector/003/processes?pretty=true"
# Restart ALL agents
curl -sk -H "$H" -X PUT "$BASE/agents/restart?agents_list=all"
# --- Rules ---
# Find rules by group
curl -sk -H "$H" "$BASE/rules?group=authentication_failure&pretty=true"
# Get rule by ID
curl -sk -H "$H" "$BASE/rules?rule_ids=100001&pretty=true"
# --- Alerts ---
# Recent 50 critical alerts
curl -sk -H "$H" "$BASE/alerts?level=14&limit=50&pretty=true"
# --- FIM ---
# Recent FIM changes for agent
curl -sk -H "$H" "$BASE/syscheck/003?sort=-date&limit=50&pretty=true"
# FIM events filtered by path
curl -sk -H "$H" "$BASE/syscheck/003?q=file=/etc/passwd&pretty=true"
<!-- In /var/ossec/etc/ossec.conf -->
<integration>
<name>slack</name>
<hook_url>https://hooks.slack.com/services/T00/B00/XXXXXXX</hook_url>
<!-- Only send alerts level 10+ -->
<level>10</level>
<alert_format>json</alert_format>
</integration>
<!-- Scope to specific rule groups -->
<integration>
<name>slack</name>
<hook_url>https://hooks.slack.com/services/T00/B00/XXXXXXX</hook_url>
<group>authentication_failures,web_attack,attack</group>
<alert_format>json</alert_format>
</integration>
<!-- FIM + VirusTotal: check file hashes on modification -->
<integration>
<name>virustotal</name>
<api_key>YOUR_VT_API_KEY</api_key>
<!-- Must use syscheck group -->
<group>syscheck</group>
<alert_format>json</alert_format>
</integration>
<!-- Trigger rule for VT positive result (auto-generated by integration) -->
<!-- Rule 87105: VirusTotal alert - X engines detected the file -->
<global>
<email_notification>yes</email_notification>
<email_to>[email protected]</email_to>
<smtp_server>mail.company.com</smtp_server>
<email_from>[email protected]</email_from>
<email_maxperhour>20</email_maxperhour>
<!-- Granular: send email for specific rules regardless of level -->
<email_alert_level>12</email_alert_level>
</global>
<!-- Per-rule email override in alert section -->
<email_alerts>
<email_to>[email protected]</email_to>
<rule_id>110001,110009</rule_id>
<do_not_delay />
</email_alerts>
<integration>
<name>custom-w2thive</name>
<!-- Path to custom Python script -->
<hook_url>http://thehive.company.com:9000</hook_url>
<api_key>YOUR_THEHIVE_API_KEY</api_key>
<level>10</level>
<alert_format>json</alert_format>
</integration>
<!-- Custom script at: /var/ossec/integrations/custom-w2thive -->
<!-- chmod 750 /var/ossec/integrations/custom-w2thive -->
<!-- chown root:wazuh /var/ossec/integrations/custom-w2thive -->
<integration>
<name>pagerduty</name>
<api_key>PAGERDUTY_ROUTING_KEY</api_key>
<level>12</level>
<alert_format>json</alert_format>
</integration>
<!-- In /var/ossec/etc/ossec.conf (Manager) -->
<!-- Step 1: define command -->
<command>
<name>firewall-drop</name>
<executable>firewall-drop</executable>
<!-- Allow timeout-based reversal -->
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>disable-account</name>
<executable>disable-account</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<!-- Custom Python/Bash script -->
<command>
<name>isolate-host</name>
<executable>isolate-host.sh</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<!-- Step 2: bind command to trigger -->
<active-response>
<command>firewall-drop</command>
<!-- local=triggering agent | server=manager | all=every agent | defined-agent=specific ID -->
<location>local</location>
<!-- Trigger on specific rule IDs -->
<rules_id>100002,5712,5716</rules_id>
<!-- Or trigger on level -->
<!-- <level>12</level> -->
<!-- Or trigger on group -->
<!-- <rules_group>authentication_failures</rules_group> -->
<!-- Unblock after 3600 seconds -->
<timeout>3600</timeout>
</active-response>
<active-response>
<command>disable-account</command>
<location>local</location>
<rules_id>110008</rules_id>
<timeout>3600</timeout>
</active-response>
| Script | Effect | Platform |
|---|---|---|
firewall-drop |
Linux | |
ip-customblock |
Customizable IP block | Linux |
disable-account |
Lock user account | Linux/Windows |
restart-wazuh |
Restart Wazuh agent service | Any |
netsh |
Windows Firewall block rule | Windows |
route-null |
Null-route IP via routing table | Linux |
wazuh-slack |
Send Slack notification | Any |
kaspersky |
Kaspersky AV scan trigger | Windows |
<!-- /var/ossec/etc/rules/local_rules.xml -->
<group name="tuning,">
<!-- Lower level of noisy rule (overwrite="yes" replaces built-in) -->
<rule id="5712" level="3" overwrite="yes">
<if_sid>5712</if_sid>
<description>SSH failed login (downgraded - high volume environment)</description>
</rule>
<!-- Completely suppress a rule -->
<rule id="5715" level="0" overwrite="yes">
<if_sid>5715</if_sid>
<description>SSH reverse mapping (suppressed)</description>
</rule>
<!-- Suppress for specific user -->
<rule id="100900" level="0">
<if_sid>5710</if_sid>
<user>backup-svc</user>
<description>SSH failure from service account (expected)</description>
</rule>
<!-- Suppress for specific IP -->
<rule id="100901" level="0">
<if_sid>5710</if_sid>
<srcip>10.0.0.50</srcip>
<description>SSH failure from monitoring host (expected)</description>
</rule>
<!-- Raise level of specific event for VIP host -->
<rule id="100902" level="15" overwrite="yes">
<if_sid>5710</if_sid>
<hostname>dc01.corp.local</hostname>
<description>SSH failure on domain controller - elevated priority</description>
</rule>
</group>
# Create whitelist file
cat > /var/ossec/etc/lists/whitelist-ips << 'EOF'
10.0.0.50:scanner
10.0.0.51:monitoring
192.168.1.100:pentest-approved
EOF
# Create malicious IP list
cat > /var/ossec/etc/lists/malicious-ips << 'EOF'
185.220.101.1:c2
45.33.32.156:scanner
EOF
# Register lists in ossec.conf ruleset block
# <list>etc/lists/whitelist-ips</list>
# <list>etc/lists/malicious-ips</list>
# Rebuild CDB binary index (automatic on manager restart)
# wazuh-analysisd re-reads lists on start/restart
/var/ossec/bin/wazuh-control restart
# Verify list loaded:
grep "lists" /var/ossec/logs/ossec.log | tail -5
<!-- Use whitelist in rule to suppress -->
<rule id="100910" level="0">
<if_sid>5710,5711,5712</if_sid>
<list field="srcip" lookup="address_match_key">etc/lists/whitelist-ips</list>
<description>SSH failure from whitelisted host - suppressed</description>
</rule>
<!-- Use threat intel list to escalate -->
<rule id="100911" level="15">
<if_group>syslog,firewall,web</if_group>
<list field="srcip" lookup="address_match_key">etc/lists/malicious-ips</list>
<description>Traffic from known malicious IP: $(srcip)</description>
<group>threat_intel,attack,</group>
<mitre><id>T1071</id></mitre>
</rule>
<global>
<!-- Ignore repeated alerts from same IP within N seconds -->
<white_list>10.0.0.50</white_list>
<white_list>192.168.1.0/24</white_list>
<!-- Increase log output verbosity -->
<logall>yes</logall>
<logall_json>yes</logall_json>
</global>
<!-- Rule ignore (flood prevention) - per-rule attribute -->
<!-- <rule id="100001" level="6" ignore="60"> -->
<!-- ignore="60" means don't re-fire within 60 seconds for same source -->
# ── Authentication ──
rule.groups: "authentication_failure" AND rule.level: [10 TO 15]
data.srcip: "192.168.1.50" AND rule.groups: "authentication_failure"
agent.name: "web01" AND rule.groups: "web_attack"
# ── MITRE ATT&CK ──
rule.mitre.id: "T1059.001"
rule.mitre.tactic: "Lateral Movement"
rule.mitre.id: ("T1003" OR "T1550.002" OR "T1558")
# ── FIM / Syscheck ──
rule.groups: "syscheck" AND syscheck.event: "added" AND syscheck.path: "/etc/*"
rule.groups: "syscheck" AND syscheck.path: "/var/www/html/*" AND syscheck.event: "added"
# ── Vulnerability ──
vulnerability.severity: "critical" AND vulnerability.status: "active"
vulnerability.cve: "CVE-2021-44228"
vulnerability.package.name: "log4j*"
# ── Windows Event IDs ──
data.win.system.eventID: "4625" AND agent.name: "dc01"
data.win.system.eventID: ("4720" OR "4722" OR "4724" OR "4728" OR "4732")
# ── SCA Failures ──
rule.groups: "sca" AND data.sca.result: "failed"
rule.groups: "sca" AND data.sca.policy: "custom_linux_hardening"
# ── High-volume source (potential scanner) ──
# Dashboard > Visualize > Metric by data.srcip, filter rule.level >= 6
# ── Rare process execution (first-time-seen) ──
rule.groups: "syscheck" AND rule.firedtimes: [1 TO 1]
# ── Active Response triggered ──
rule.groups: "active_response"
| Hunt | KQL Filter | What to look for |
|---|---|---|
rule.mitre.id: "T1003" |
LSASS access, registry SAM | |
rule.mitre.tactic: "Lateral Movement" |
New admin logins, psexec | |
rule.mitre.tactic: "Persistence" |
New scheduled tasks, services, users | |
rule.mitre.tactic: "Defense Evasion" |
Log clears, AV disables | |
rule.mitre.tactic: "Exfiltration" |
Large outbound, DNS tunneling | |
| Webshell | syscheck.path: "/var/www/*" AND syscheck.event: "added" |
New PHP/ASP files in web root |
# Full service control
/var/ossec/bin/wazuh-control start
/var/ossec/bin/wazuh-control stop
/var/ossec/bin/wazuh-control restart
/var/ossec/bin/wazuh-control reload # reload rules without restart
/var/ossec/bin/wazuh-control status # show all daemons
# Individual daemon restart
systemctl restart wazuh-manager # systemd (preferred)
systemctl restart wazuh-indexer
systemctl restart wazuh-dashboard
# Config syntax test before restart
/var/ossec/bin/wazuh-analysisd -t
/var/ossec/bin/wazuh-remoted -t
# Check for config errors
grep -i "error\|warn" /var/ossec/logs/ossec.log | tail -50
# Force agent reconnect
/var/ossec/bin/agent_control -R <agent_id>
# Delete agent
/var/ossec/bin/manage_agents -r <agent_id>
# OR via API:
curl -sk -H "Authorization: Bearer $TOKEN" -X DELETE \
"https://localhost:55000/agents?agents_list=003&status=disconnected"
# Run FIM scan immediately on agent
/var/ossec/bin/agent_control -a -u <agent_id>
# Check cluster status
/var/ossec/bin/cluster_control -l # list nodes
/var/ossec/bin/cluster_control -i # node info
# Verify rule fired (logtest)
echo "Jan 10 12:00:01 myhost sshd[1234]: Failed password for root from 10.0.0.1 port 52020 ssh2" \
| /var/ossec/bin/wazuh-logtest -v
# Stats
/var/ossec/bin/wazuh-analysisd --diag # analysis engine diag dump