В первой части мы подняли manager, во второй — подключили агентов и включили FIM, события поехали. Но решение «что из этого инцидент» пока принимает не вы: из коробки Wazuh грузит тысячи правил, написанных под чужие инфраструктуры и чужие пороги. Третья часть — про то, как забрать это решение себе: анатомия правил и decoders, свой детект на брутфорс SSH и JSON-логи приложения, тестирование без рестарта manager и локальные файлы, которые переживают обновления ruleset.
Содержание
Открыть содержание
- Что происходит с событием после агента
- Анатомия правила
- Тюнинг false positives: override, а не правка дефолта
- Свой decoder для JSON-логов приложения
- Практика: брутфорс SSH со своим порогом
- Тестирование через wazuh-logtest без рестарта
- Организация файлов, чтобы обновления не затирали кастом
- Как проверить, что всё работает
- Итог
Что происходит с событием после агента
Каждое событие, долетевшее до manager, попадает в analysisd и проходит три фазы:
- predecode — из syslog-подобной строки вытаскиваются timestamp, hostname и program name;
- decode — decoder разбирает полезную нагрузку на поля: srcip, user, action и так далее;
- rules — декодированное событие прогоняется по дереву правил; сработавшие правила с уровнем выше порога становятся алертами (порог — секция
<alerts>в ossec.conf).
Ключевая мысль этой схемы: правило может сопоставить только то, что извлёк decoder. Если ваше приложение пишет JSON-лог, для которого нет decoder’а, движок видит неразобранную строку: максимум — generic-алерт «что-то пришло», ни полей, ни корреляции. Поэтому кастомное детектирование — это почти всегда два шага: сначала научить Wazuh читать ваш лог, потом писать правила по полям.
Прежде чем писать своё, проверьте, чего уже нет: три тысячи правил покрывают неожиданно много. grep -ri "description" /var/ossec/ruleset/rules/ | grep -i ssh — быстрее, чем изобретать детект, который лежит двумя файлами выше. Своё пишут тогда, когда нужного поведения нет (чужой порог, чужой формат, свой бизнес-событие) — а не «потому что могу».
И важный горизонт: формат правил и decoders — декларативный XML, который не менялся годами и не зависит от беты 5.0 с её переписанным протоколом агент-сервер. Из всего стека Wazuh это самая долгоживущая инвестиция времени.
Анатомия правила
Разберём реальный кусок дефолтного 0095-sshd_rules.xml — цепочка про неудачные SSH-логины:
<group name="syslog,sshd,">
<rule id="5700" level="0" noalert="1">
<decoded_as>sshd</decoded_as>
<description>SSHD messages grouped.</description>
</rule>
<rule id="5760" level="5">
<if_sid>5700</if_sid>
<match>Failed password|Failed keyboard|authentication error</match>
<description>sshd: authentication failed.</description>
</rule>
<rule id="5763" level="10" frequency="8" timeframe="120" ignore="60">
<if_matched_sid>5760</if_matched_sid>
<same_source_ip />
<description>sshd: brute force trying to get access to the system. Authentication failed.</description>
</rule>
</group>
По элементам:
- id — уникальный номер; для своих правил Wazuh резервирует диапазон 100000–120000;
- level — уровень значимости: 0 — «декодировано, но не алертить», 3–6 — контекст и шум, 7–10 — стоит посмотреть, 12+ — звать людей;
- group — теги, по которым правила ищут и фильтруют;
<if_sid>5700</if_sid>— «срабатывать только если сработал родитель», так строится дерево; - match — подстрока/regex по сырому логу; для проверки декодированных полей есть
<field name="...">; - frequency + timeframe + if_matched_sid — корреляция: сработать, если правило 5760 сработало 8 раз за 120 секунд;
<same_source_ip />сужает окно до одного источника,ignoreглушит повторные алерты на 60 секунд.
То есть дефолтный порог брутфорса — 8 неудачных попыток с одного IP за две минуты. Для одних это поздно, для других — поток ложных срабатываний от мониторинга и CI. Порог — это параметр вашего риск-аппетита, и дефолт тут ничем не лучше средней температуры по больнице.
Тюнинг false positives: override, а не правка дефолта
Первый рефлекс при ложном срабатывании — открыть 0095-sshd_rules.xml и поправить. Так делать нельзя: каталог /var/ossec/ruleset принадлежит пакету, и следующее обновление молча затрёт ваши правки. Правильных инструмента два.
Override с тем же id. Копируете дефолтное правило в local_rules.xml, меняете нужное и добавляете overwrite="yes" — правило с тем же id заменяет дефолтное:
<!-- local_rules.xml: дефолтный level 5 -> 10 -->
<rule id="5710" level="10" overwrite="yes">
<if_sid>5700</if_sid>
<match>illegal user|invalid user</match>
<description>sshd: Attempt to login using a non-existent user</description>
</rule>
Ограничение: условия связи (if_sid, if_group, if_matched_sid и соседние) перезаписать нельзя — они сохраняются из оригинала, менять можно level, description, match и другие параметры.
Child-правило с level 0. Если заглушить нужно не всё правило, а известный доброкачественный паттерн — монитор ломится к серверу каждые полчаса — пишите своё правило-наследник с нулевым уровнем:
<rule id="100110" level="0">
<if_sid>5760</if_sid>
<srcip>10.20.30.40</srcip>
<description>Known monitoring host — not an incident</description>
</rule>
Правило 5760 для этого IP по-прежнему сработает, но ваша ветка перехватит событие с уровнем 0 — в алерты оно не попадёт, а в статистике останется.
Свой decoder для JSON-логов приложения
Допустим, биллинг пишет в syslog однострочный JSON:
{"app":"billing","event":"payment_declined","user":"a.ivanov","src_ip":"203.0.113.7","outcome":"fail"}
Писать regex для JSON руками не нужно — в Wazuh есть встроенный JSON-decoder, до которого надо только «дозвониться» prematch’ем. В /var/ossec/etc/decoders/local_decoder.xml:
<decoder name="billing-json">
<prematch>^{"app":"billing"</prematch>
<plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>
- prematch — как выбрать «свою» строку среди тысяч чужих; чем конкретнее, тем меньше шансов перехватить чужой лог;
- plugin_decoder — после prematch строку разбирает JSON-движок: поля верхнего уровня становятся динамическими полями (
event,user,src_ip,outcome), вложенности разворачиваются в пути через точку. Если JSON идёт после префикса (например,json_data: {...}), добавьтеoffset="after_prematch".
Теперь правила работают по полям, а не по подстрокам:
<group name="billing,">
<rule id="100200" level="5">
<decoded_as>billing-json</decoded_as>
<field name="outcome">^fail$</field>
<description>billing: payment declined</description>
</rule>
<rule id="100201" level="12" frequency="10" timeframe="300">
<if_matched_sid>100200</if_matched_sid>
<description>billing: many declined payments — possible carding</description>
</rule>
</group>
<field> сопоставляет декодированное поле с regex — это тот же механизм, которым дефолтные правила читают srcip и user, просто поля ваши. Дальше вся мощь движка доступна: frequency, timeframe, группы, active response.
Практика: брутфорс SSH со своим порогом
Собираем обещанное — правило 100100 в local_rules.xml, агрессивнее дефолтного 5763: 4 неудачи за 60 секунд с одного IP вместо 8 за 120:
<group name="local,sshd,authentication_failures,">
<rule id="100100" level="12" frequency="4" timeframe="60" ignore="30">
<if_matched_sid>5760</if_matched_sid>
<same_source_ip />
<description>sshd: aggressive brute force — 4 failures in 60s from one source</description>
<mitre>
<id>T1110</id>
</mitre>
</rule>
</group>
Наследуясь от дефолтного 5760 через if_matched_sid, вы бесплатно получаете весь его матчинг «Failed password» — сам дефолт не тронут, оба правила живут рядом. В четвёртой части цикла повесим на такой level 12 active response: firewall-drop на 10 минут — и брутфорс начнёт сам себя лечить.
Тестирование через wazuh-logtest без рестарта
Главный страх кастомных правил — «у меня production-поток, а я тут правлю XML». Для этого есть /var/ossec/bin/wazuh-logtest на manager (в Docker из части 1 — docker exec -ti wazuh.manager /var/ossec/bin/wazuh-logtest): утилита грузит ruleset в собственную сессию и прогоняет строку через все фазы, не трогая живой analysisd.
Вставляете реальную строку лога и видите три фазы: pre-decode (timestamp, hostname, program), decode (поля) и rules — какое правило сработало, с каким level и group. Для нашего SSH-примера:
Phase 3: Completed rule (level: 5): 5760 — sshd: authentication failed.
Важная деталь: частотные правила накапливаются внутри сессии. Вставьте строку Failed password ... четыре раза подряд — на четвёртый раз logtest покажет срабатывание 100100, и вы увидите свой порог в действии до того, как правило увидит продакшен. Только после этого применяйте изменения к живому manager:
systemctl restart wazuh-manager # Docker: docker restart wazuh.manager
Отдельный плюс logtest для отладки decoder’а: если Phase 2 показывает «no decoder» или пустые поля — prematch не попал или JSON не однострочный, чините до правил, а не после. И маленькая преемственность: в старых гайдах вы встретите ossec-logtest — в Wazuh 4.2+ его заменил wazuh-logtest с сессиями и накоплением frequency внутри них.
Организация файлов, чтобы обновления не затирали кастом
Всё, что мы написали, живёт в двух файлах manager’а:
/var/ossec/etc/rules/local_rules.xml— свои правила и override’ы;/var/ossec/etc/decoders/local_decoder.xml— свои decoder’ы.
Оба загружаются после официального ruleset, поэтому override с overwrite="yes" выигрывает, а сами файлы не принадлежат пакету и переживают обновление. Файлы в /var/ossec/etc/rules/ и /var/ossec/etc/decoders/ тоже работают, но local_* — первый кандидат на review и бэкап: один файл на всё кастомное легче держать в git. Дефолтный каталог /var/ossec/ruleset/ не трогаем вовсе — он перезаписывается при каждом обновлении.
И держите свои id в диапазоне 100000–120000: id ниже — территория Wazuh, и следующий апдейт ruleset может завести своё правило на занятый вами номер.
| Вопрос | Правки дефолтного ruleset | local_rules.xml + local_decoder.xml |
|---|---|---|
| Нестандартные логи | generic-алерт без полей | decoder → поля → точные правила |
| Пороги | чужие (8/120 для SSH) | ваши, под риск-аппетит |
| Ложные срабатывания | живут до следующего обновления | overwrite и child-правила с level 0 |
| Обновление Wazuh | молча затирает правки | локальные файлы и id 100000+ не тронуты |
| Level | уровни из чужих сценариев | уровни под ваш процесс реагирования |
Как проверить, что всё работает
Полный smoke-тест одной сессией logtest:
docker exec -ti wazuh.manager /var/ossec/bin/wazuh-logtest
Вставьте четыре раза строку с реальным IP:
Oct 5 06:12:41 app01 sshd[28412]: Failed password for invalid user admin from 203.0.113.7 port 51234 ssh2
На четвёртый раз Phase 3 покажет id: '100100', level: '12' и группу authentication_failures. То же с JSON-строкой биллинга — убедитесь, что Phase 2 выдал поля, а Phase 3 — правило 100200. После рестарта manager проверьте живой поток: tail -f /var/ossec/logs/alerts/alerts.json | grep 100100 — и в dashboard → Security events фильтр rule.id: 100100.
Итог
Дефолтный ruleset — это честная стартовая точка: он знает SSH, sudo, auditd и ещё сотню источников, но пороги и уровни в нём — чужие, а ваши приложения он не понимает вовсе. Реальная ценность SIEM начинается не там, где закончилась установка, а там, где вы начинаете писать правила под свою инфраструктуру: decoder для своего JSON, порог под свой мониторинг, level под свой процесс реагирования — иначе это просто дорогой агрегатор логов. Формат переживёт и 5.0, а время, вложенное в него, не сгорит. В четвёртой части — active response: как заставить Wazuh не только увидеть брутфорс по правилу 100100, но и самому закрыть атакующему IP на firewall.