Статический анализ правил Wazuh: эволюция линтера

Серия “Статический анализ Wazuh”:

  • Часть 1: Декодеры - валидация XML-декодеров
  • Часть 2: Правила (вы здесь) - валидация правил и кросс-типовая проверка

В первой части мы создали линтер для XML-декодеров Wazuh - инструмент, который проверяет структуру, согласованность regex/order и цепочки родительских декодеров. Но декодеры - только половина конвейера обработки событий. Декодеры извлекают поля из сырых логов, а правила решают, что с этими полями делать: генерировать алерт, повысить уровень угрозы или запустить автоматический ответ. Ошибка в правиле - пропущенный алерт или ложное срабатывание - может быть опаснее ошибки в декодере.

Инструмент вырос. Теперь это wazuh-linter - платформа статического анализа, которая валидирует и декодеры, и правила, и связи между ними. В этой статье разберем архитектурную эволюцию инструмента, 24 правила валидации для XML-файлов правил Wazuh и механизм кросс-типовой проверки.

Типичные ошибки в правилах Wazuh

XML-файлы правил Wazuh расположены в /var/ossec/etc/rules/ (пользовательские) и /var/ossec/ruleset/rules/ (стандартные). Каждый файл содержит элементы <rule> внутри корневого <group>. Анализ реальных конфигураций выявляет устойчивые паттерны ошибок.

timeframe без frequency. Правило с timeframe="120", но без атрибута frequency задает временное окно без порога срабатывания. Wazuh молча игнорирует timeframe в этом случае. Исключение - правила с <if_matched_sid> или <if_matched_group>, которые наследуют контекст частоты из базового правила. Обратная ситуация - frequency без timeframe - валидна, Wazuh применяет timeframe по умолчанию.

<!-- Ошибка: timeframe без frequency -->
<rule id="100001" level="10" timeframe="120">
  <description>Missing frequency</description>
</rule>

if_sid на несуществующий ID. Правило объявляет <if_sid>100500</if_sid>, но правило 100500 не существует ни в одном загруженном файле. Правило никогда не сработает, потому что его условие активации не может быть выполнено.

Дубликаты rule ID. Два правила с одинаковым id без overwrite="yes" - неопределенное поведение. Wazuh загрузит одно из них, но какое именно зависит от порядка обработки файлов.

Некорректный формат MITRE ATT&CK. Идентификаторы MITRE должны соответствовать формату Tnnnn или Tnnnn.nnn (например, T1078 или T1078.001). Произвольные строки вроде brute_force в элементе <id> нарушают интеграцию с MITRE-фреймворком.

osmatch в regex-элементах. Атрибут type="osmatch" допустим для <match> и <prematch>, но не для <regex> - osmatch не поддерживает группы захвата, а <regex> существует именно для захвата полей.

Невалидные форматы time/weekday. Элемент <time> принимает диапазоны в 24-часовом формате (6 pm - 8:30 am), элемент <weekday> - названия дней или специальные значения weekdays/weekends. Опечатки вроде Mnday или 25:00 - 26:00 приводят к тому, что временное условие молча игнорируется.

Архитектурная эволюция: от линтера к платформе

Первая версия инструмента - wazuh-decoder-linter - была монолитом: один класс WazuhDecoderLinter с парсингом XML, санитизацией, блочным извлечением и всеми проверками. Когда пришло время добавить валидацию правил, стало очевидно, что копирование логики парсинга XML - тупик. Декодеры и правила используют одинаковые механизмы: чтение файлов, обработку битого XML, экранирование спецсимволов Wazuh, извлечение отдельных блоков при ошибке парсинга.

Решение - вынести общую логику в базовый класс BaseXmlLinter:

BaseXmlLinter
  - Чтение файлов (UTF-8 / latin-1 fallback)
  - Санитизация XML (неэкранированные &, \<, bare <)
  - Парсинг с двухпроходной стратегией
  - Извлечение отдельных блоков при ошибке
  - Форматирование контекста строк
      |
      +-- WazuhDecoderLinter
      |     14 проверок декодеров
      |     Реестр имён декодеров
      |
      +-- WazuhRuleLinter
            24 проверки правил
            Реестр rule ID и групп

Каждый специализированный линтер наследует BaseXmlLinter и реализует только доменную логику. WazuhDecoderLinter проверяет regex/order, цепочки parent, plugin_decoder. WazuhRuleLinter проверяет frequency/timeframe, if_sid chains, MITRE ID, форматы времени.

Второе архитектурное решение - LintSession. Это объект общего состояния, который связывает линтеры декодеров и правил в единый проход валидации. Когда линтер декодеров обрабатывает файлы, он регистрирует имена всех найденных декодеров в сессии. Когда линтер правил встречает <decoded_as>sshd</decoded_as>, он проверяет по сессии, существует ли декодер sshd. Без LintSession такая кросс-типовая валидация невозможна.

Третье изменение - авто-определение типа файла. CLI анализирует содержимое XML и определяет, что перед ним: файл декодеров (содержит <decoder>) или файл правил (содержит <group> с дочерними <rule>). Это позволяет запускать единую команду wazuh-lint на директорию со смешанными файлами.

24 правила валидации для XML-правил Wazuh

Линтер правил реализует 24 проверки, сгруппированные по типу.

Структурные проверки

ПравилоУровеньОписание
Обязательные атрибутыERROR<rule> должен иметь id и level
Диапазон IDERRORid - целое число от 1 до 999999
Диапазон levelERRORlevel - целое число от 0 до 16
Уникальность IDERRORДубликаты ID в пределах файла запрещены
Неизвестные элементыWARNINGДочерние элементы вне набора 82 допустимых
ОписаниеWARNINGПравило должно содержать <description>
Атрибуты ruleWARNINGНеизвестные атрибуты на <rule>

Логические проверки

ПравилоУровеньОписание
frequency/timeframeERRORВзаимозависимость (ослаблено для if_matched_*)
Формат if_sidERRORКорректные целые числа через запятую
Формат if_levelERRORЦелое число в диапазоне 0-16
Значение overwriteERRORТолько yes или no
Контекст корреляцииWARNINGsame_*/different_* требуют frequency или if_matched_*

Форматные проверки

ПравилоУровеньОписание
Типы regexERRORtype только osmatch, osregex или pcre2
Атрибут negateERRORТолько yes/no; допустим только на matching-элементах
Синтаксис OS_RegexWARNINGНеподдерживаемые конструкции в osregex
Значения optionsERRORТолько допустимые значения опций
Формат timeERRORКорректный диапазон времени (24ч, 12ч am/pm, ! для инверсии)
Формат weekdayERRORКорректные названия дней или weekdays/weekends
Формат MITRE IDWARNING<id> должен соответствовать Tnnnn или Tnnnn.nnn
Атрибуты listERROR<list> требует field=; допустимые значения lookup=

Кросс-файловые проверки

ПравилоУровеньОписание
Цепочка if_sidWARNING<if_sid> должен ссылаться на существующие ID
Цепочка if_matched_sidWARNING<if_matched_sid> должен ссылаться на существующие ID
Дубликаты ID между файламиERRORНет дубликатов без overwrite="yes"
decoded_asINFO<decoded_as> должен ссылаться на существующий декодер

Рассмотрим несколько проверок детальнее.

Корреляционный контекст. Элементы <same_source_ip>, <different_source_ip> и другие корреляционные элементы (40 всего) имеют смысл только в контексте агрегации - при наличии <frequency> или <if_matched_sid>. Без них корреляционный элемент молча игнорируется:

<!-- Ошибка: same_source_ip без frequency -->
<rule id="100002" level="8">
  <if_sid>5710</if_sid>
  <same_source_ip />
  <description>Should correlate but cannot</description>
</rule>

Валидация MITRE ATT&CK. Линтер проверяет, что идентификаторы внутри <mitre><id> соответствуют формату Tnnnn или Tnnnn.nnn. Это не полная валидация по реестру MITRE, но она отсекает очевидные ошибки вроде текстовых описаний вместо идентификаторов.

Атрибуты list. Элемент <list> для CDB-списков требует обязательный атрибут field и допускает lookup со значениями match_key, not_match_key, match_key_value, address_match_key, not_address_match_key, address_match_key_value. Отсутствие field - ошибка, некорректный lookup - тоже.

Кросс-типовая валидация: decoded_as

Самая интересная возможность новой архитектуры - проверка связей между правилами и декодерами. Элемент <decoded_as> в правиле фильтрует события по имени декодера. Если указанный декодер не существует, правило никогда не сработает.

Рассмотрим пример. В файле правил:

<rule id="100100" level="5">
  <decoded_as>custom-nginx</decoded_as>
  <description>Custom nginx event detected</description>
</rule>

Но в файлах декодеров нет декодера с именем custom-nginx. Правило формально валидно с точки зрения XML, но функционально бесполезно.

LintSession решает эту проблему. При запуске через единую точку входа wazuh-lint создается объект сессии. Линтер декодеров заполняет реестр имен (session.decoder_names). Затем линтер правил получает этот реестр и проверяет каждый <decoded_as> по нему.

from wazuh_linter import WazuhDecoderLinter, WazuhRuleLinter, LintSession

session = LintSession()

decoder_linter = WazuhDecoderLinter()
decoder_report = decoder_linter.lint_paths(
    ["decoders/"], session=session
)

rule_linter = WazuhRuleLinter()
rule_report = rule_linter.lint_paths(
    ["rules/"], session=session
)

# session.decoder_names заполнен линтером декодеров
# rule_linter использовал его для проверки decoded_as
for result in rule_report.results:
    print(f"[{result.severity}] {result.file}:{result.line} - {result.message}")

Вывод при обнаружении ошибки:

[INFO] local_rules.xml:12 - Rule '100100': <decoded_as> references
  decoder 'custom-nginx' which was not found in scanned decoder files

Уровень серьезности - INFO, а не ERROR, потому что декодер может существовать в файлах, не включенных в текущее сканирование (например, стандартные декодеры Wazuh).

Использование CLI и интеграция с CI/CD

Обновленный инструмент предоставляет три точки входа:

# Авто-определение типа файла (рекомендуется)
wazuh-lint /var/ossec/etc/

# Принудительный выбор типа
wazuh-lint --type rule /var/ossec/etc/rules/
wazuh-lint --type decoder /var/ossec/etc/decoders/

# Устаревшие алиасы (идентичны wazuh-lint, сохранены для обратной совместимости)
wazuh-rule-lint /var/ossec/etc/rules/
wazuh-decoder-lint /var/ossec/etc/decoders/

Команда wazuh-lint автоматически определяет тип каждого XML-файла и создает LintSession для кросс-типовой валидации. Используйте --type для принудительного выбора режима. Команды wazuh-rule-lint и wazuh-decoder-lint - алиасы для wazuh-lint, они не форсируют тип. Опции --strict, --format json, --show-info работают для всех режимов.

Обновленный пример GitHub Actions для валидации всей конфигурации:

name: Lint Wazuh Configuration
on:
  push:
    paths:
      - 'decoders/**'
      - 'rules/**'
  pull_request:
    paths:
      - 'decoders/**'
      - 'rules/**'

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install wazuh-linter
        run: pip install git+https://github.com/pyToshka/wazuh-linter.git

      - name: Lint decoders and rules
        run: wazuh-lint --strict --format json decoders/ rules/ > lint-results.json

      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: lint-results
          path: lint-results.json

Pre-commit хук для обоих типов файлов:

repos:
  - repo: local
    hooks:
      - id: wazuh-lint
        name: Wazuh Lint
        entry: wazuh-lint --strict
        language: python
        files: '\.xml$'
        types: [file]

Для подробного разбора линтера декодеров и всех 14 проверок декодеров обратитесь к первой части серии.

Заключение и следующие шаги

wazuh-linter теперь покрывает обе стороны конвейера обработки событий Wazuh: декодеры (14 проверок) и правила (24 проверки), связанные через механизм кросс-типовой валидации LintSession. Архитектура с BaseXmlLinter делает добавление новых типов анализа прямолинейным.

Инструмент доступен как открытый исходный код под лицензией BSD 3-Clause на github.com/pyToshka/wazuh-linter. В следующей части серии рассмотрим расширение возможностей инструмента.

Дополнительные материалы


Смотрите также