Серия “Статический анализ 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 |
| Диапазон ID | ERROR | id - целое число от 1 до 999999 |
| Диапазон level | ERROR | level - целое число от 0 до 16 |
| Уникальность ID | ERROR | Дубликаты ID в пределах файла запрещены |
| Неизвестные элементы | WARNING | Дочерние элементы вне набора 82 допустимых |
| Описание | WARNING | Правило должно содержать <description> |
| Атрибуты rule | WARNING | Неизвестные атрибуты на <rule> |
Логические проверки
| Правило | Уровень | Описание |
|---|---|---|
| frequency/timeframe | ERROR | Взаимозависимость (ослаблено для if_matched_*) |
| Формат if_sid | ERROR | Корректные целые числа через запятую |
| Формат if_level | ERROR | Целое число в диапазоне 0-16 |
| Значение overwrite | ERROR | Только yes или no |
| Контекст корреляции | WARNING | same_*/different_* требуют frequency или if_matched_* |
Форматные проверки
| Правило | Уровень | Описание |
|---|---|---|
| Типы regex | ERROR | type только osmatch, osregex или pcre2 |
| Атрибут negate | ERROR | Только yes/no; допустим только на matching-элементах |
| Синтаксис OS_Regex | WARNING | Неподдерживаемые конструкции в osregex |
| Значения options | ERROR | Только допустимые значения опций |
| Формат time | ERROR | Корректный диапазон времени (24ч, 12ч am/pm, ! для инверсии) |
| Формат weekday | ERROR | Корректные названия дней или weekdays/weekends |
| Формат MITRE ID | WARNING | <id> должен соответствовать Tnnnn или Tnnnn.nnn |
| Атрибуты list | ERROR | <list> требует field=; допустимые значения lookup= |
Кросс-файловые проверки
| Правило | Уровень | Описание |
|---|---|---|
| Цепочка if_sid | WARNING | <if_sid> должен ссылаться на существующие ID |
| Цепочка if_matched_sid | WARNING | <if_matched_sid> должен ссылаться на существующие ID |
| Дубликаты ID между файлами | ERROR | Нет дубликатов без overwrite="yes" |
| decoded_as | INFO | <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. В следующей части серии рассмотрим расширение возможностей инструмента.
Дополнительные материалы
- Часть 1: Статический анализ декодеров Wazuh - валидация XML-декодеров
- Безопасность образов контейнеров с Wazuh и Trivy - автоматизированная валидация безопасности
- RAG для документации Wazuh: Часть 1 - построение систем извлечения информации
- Wazuh LLM: Llama 3.1 для анализа событий безопасности - ИИ-модель для анализа событий