Перейти к содержимому
HoginHogin
Назад

Wazuh: пишем свои правила и decoders, чтобы SIEM не захлебнулся шумом (часть 3/6)

9 мин чтения

В первой части мы подняли manager, во второй — подключили агентов и включили FIM, события поехали. Но решение «что из этого инцидент» пока принимает не вы: из коробки Wazuh грузит тысячи правил, написанных под чужие инфраструктуры и чужие пороги. Третья часть — про то, как забрать это решение себе: анатомия правил и decoders, свой детект на брутфорс SSH и JSON-логи приложения, тестирование без рестарта manager и локальные файлы, которые переживают обновления ruleset.

Содержание

Открыть содержание

Что происходит с событием после агента

Каждое событие, долетевшее до manager, попадает в analysisd и проходит три фазы:

Путь события: decoder затем правила

Ключевая мысль этой схемы: правило может сопоставить только то, что извлёк 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>

По элементам:

То есть дефолтный порог брутфорса — 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>

Теперь правила работают по полям, а не по подстрокам:

<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’а:

Оба загружаются после официального ruleset, поэтому override с overwrite="yes" выигрывает, а сами файлы не принадлежат пакету и переживают обновление. Файлы в /var/ossec/etc/rules/ и /var/ossec/etc/decoders/ тоже работают, но local_* — первый кандидат на review и бэкап: один файл на всё кастомное легче держать в git. Дефолтный каталог /var/ossec/ruleset/ не трогаем вовсе — он перезаписывается при каждом обновлении.

Обновление ruleset: правка дефолта против local-файлов

И держите свои id в диапазоне 100000–120000: id ниже — территория Wazuh, и следующий апдейт ruleset может завести своё правило на занятый вами номер.

ВопросПравки дефолтного rulesetlocal_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.


Поделиться:

Следующая статья
Wazuh: подключаем агентов, включаем FIM и rootcheck на Linux и Windows (часть 2/6)