Оптимизация кода

В данном разделе рассматриваются методы оптимизации кода VRL, направленные на повышение производительности системы и упрощение работы с кодом.

Оптимизация VRL-кода включает минимизацию избыточных операций, таких как лишнее копирование объектов, неоптимальные вычисления и использование устаревших или неэффективных функций.

Снижение вычислительных затрат

Копирование исходного объекта

Копирование исходного объекта в VRL-коде может привести к избыточным затратам ресурсов, особенно если оно выполняется для каждого события. Для повышения производительности такие операции следует минимизировать.

При написании логики обработки может встречаться лишнее копирование объекта, как в данном примере:

Пример 1. Код с избыточным копированием
...
on_correlate: !vrl |
  state = .
  %state.check = false
  for_each(["управляемые устройства/задача обновления"]) -> |_index, value| {
    if contains(downcase(to_string!(.msg) ?? ""), value) {
      %state.check = true
      %state.cs8 = .msg
      %state.cs8Label = "Antivirus Message"
      %state.msg = "На хосте {{ to_string(.dvc) ?? '-' }} было зафиксировано изменение параметров критичной политики пользователем {{ to_string(.duser) ?? '-' }}"
    }
  }
  %state = state
...

Для устранения проблемы рекомендуется минимизировать операции копирования, сразу работая с переменной %state:

Пример 2. Оптимизированный код
...
on_correlate: !vrl |
  %state = .
  %state.check = false
  for_each(["управляемые устройства/задача обновления"]) -> |_index, value| {
    if contains(downcase(to_string!(.msg) ?? ""), value) {
      %state.check = true
      %state.cs8 = .msg
      %state.cs8Label = "Antivirus Message"
      %state.msg = "На хосте {{ to_string(.dvc) ?? '-' }} было зафиксировано изменение параметров критичной политики пользователем {{ to_string(.duser) ?? '-' }}"
    }
  }
...

Дублирование логики

Другой пример избыточного копирования можно найти в следующем коде, где объект state дублируется на каждом событии:

Пример 3. Код с дублирующимся копированием
...
on_correlate: !vrl |
  state = .
  count_detections = int(%state.count_detections) ?? 0
  count_detections = count_detections + 1
  %state.count_detections = count_detections
  if %state.count_detections == 2 {
    %state.cs8 = .msg
    %state.cs8Label = "Antivirus Message"
    %state.msg = "На хосте {{ to_string(.dvc) ?? '-' }} у пользователя {{ to_string(.duser) ?? '-' }} было обнаружено повторное заражение ВПО"
  }
  %state = state
...

Код можно оптимизировать, убрав лишнее копирование объекта state, что предотвратит ненужные срабатывания логики:

Пример 4. Оптимизированный код
...
on_correlate: !vrl |
  if %state.check != true {
    %state.count_detections = (int(%state.count_detections) ?? 0) + 1
    if %state.count_detections == 2 {
      %state.check = true
      %state.cs8 = .msg
      %state.cs8Label = "Antivirus Message"
      %state.msg = "На хосте {{ to_string(.dvc) ?? '-' }} у пользователя {{ to_string(.duser) ?? '-' }} было обнаружено повторное заражение ВПО"
    }
  }
...

Оптимизация вычислений внутри циклов

При обработке событий в секции on_correlate все вычисления, не зависящие от цикла, следует выносить за его пределы. Это позволяет избежать повторных затрат ресурсов на каждой итерации цикла и повышает производительность. Например, выражение downcase(to_string(.msg) ?? "-") не изменяется в ходе выполнения цикла, но будет вычисляться на каждой итерации, если его не вынести в отдельную переменную.

Пример 5. Пример с избыточными вычислениями
...
on_correlate: !vrl |
  # Список критичных объектов для мониторинга
  critical_objects = ["управляемые устройства/задача обновления"]
  %state = .
  %state.check = false
  for_each(critical_objects) -> |_index, value| {
    if contains(downcase(to_string(.msg) ?? "-"), value) {
      %state.check = true
      %state.cs8 = .msg
      %state.cs8Label = "Antivirus Message"
      %state.msg = "На хосте {{ to_string(.dvc) ?? '-' }} было зафиксировано изменение параметров критичной политики пользователем {{ to_string(.duser) ?? '-' }}"
    }
  }
...

В данном варианте downcase(to_string(.msg) ?? "-") выполняется на каждой итерации, что приводит к ненужным вычислительным затратам.

Пример 6. Оптимизированный код
on_correlate: !vrl |
  # Список критичных объектов для мониторинга
  critical_objects = ["управляемые устройства/задача обновления"]
  # Оптимизация: предварительное вычисление значения
  downcase_msg = downcase(to_string(.msg) ?? "-")
  %state = .
  %state.check = false
  for_each(critical_objects) -> |_index, value| {
    if contains(downcase_msg, value) {
      %state.check = true
      %state.cs8 = .msg
      %state.cs8Label = "Antivirus Message"
      %state.msg = "На хосте {{ to_string(.dvc) ?? '-' }} было зафиксировано изменение параметров критичной политики пользователем {{ to_string(.duser) ?? '-' }}"
    }
  }

В данном примере выражение downcase(to_string(.msg) ?? "-") вычисляется один раз до начала цикла и сохраняется в переменной downcase_msg. Это устраняет необходимость повторного выполнения вычисления на каждой итерации. В результате оптимизации количество вызовов функций сокращается, что снижает общий объем вычислений, особенно при большом количестве элементов в массиве critical_objects.

Избыточные вычисления при повторяющихся проверках

Когда в логике обработки данных необходимо многократно выполнять одну и ту же проверку, это может привести к избыточным вычислениям и увеличению нагрузки на систему. В VRL отсутствует механизм для прерывания цикла, но избыточные вычисления можно исключить, используя функции, такие как rv_contains_any или rv_ends_with_any, которые выполняют поиск за один проход по строке.

Пример 7. Код с использованием rv_contains_any
...
aliases:
  event:
    filter: !vrl |
      filePath = to_string!(.filePath) ?? ""
      rv_contains_any(filePath, [
        "/var/ossec/logs",
        "/var/run/utmp",
        "/var/log/messages",
        "/var/log/audit",
        "/var/log/secure",
        "/var/log/auth.log",
        "/var/log/kern.log",
        "/var/log/syslog",
        "/var/log/cron.log"
      ])
...
Пример 8. Код с использованием rv_ends_with_any
...
aliases:
  event:
    filter: !vrl |
      filePath = to_string!(.filePath) ?? ""
      rv_ends_with_any(filePath, [
        ".bash_history",
        ".log",
        ".audit"
      ])
...

Для обработки условий на наличие подстрок или совпадений строк можно также использовать функции contains или match-any. Выбор функции зависит от количества условий и требований к производительности.

Функция contains работает эффективно при небольшом количестве условий, например, 2–3 условия.

Пример 9. Код с использованием contains
...
aliases:
  event:
    filter: !vrl |
      filePath = to_string!(.filePath) ?? ""
      contains(filePath, "/var/log/audit") ||
      ends_with(filePath, ".bash_history")
...

При большом числе условий (5 и более) предпочтительнее использовать match_any, поскольку это упрощает код и снижает вероятность ошибок при добавлении новых условий.

Пример 10. Код с использованием match_any
...
aliases:
  event:
    filter: !vrl |
      filePath = to_string!(.filePath) ?? ""
      match_any(filePath, [
        "/var/ossec/logs",
        "/var/run/utmp",
        "/var/log/messages",
        "/var/log/audit",
        "/var/log/secure",
        "/var/log/auth.log",
        "/var/log/kern.log",
        "/var/log/syslog",
        "/var/log/cron.log",
        ".bash_history"
      ])
...

Рекомендации по выбору функций

  • contains: используйте для небольшого количества условий (2–3 условия), где нужно проверить наличие конкретных подстрок.

  • match_any: предпочтительнее при большом количестве условий (5 и более условий), так как она упрощает код и повышает производительность.

  • rv_contains_any или rv_ends_with_any: используйте для проверки наличия хотя бы одной подстроки или суффикса, так как эти функции выполняют поиск за один проход, обеспечивая лучшую производительность.

Учитывайте чувствительность к регистру, если это необходимо, и используйте дополнительный параметр case_sensitive: false.

Использование регулярных выражений

Регулярные выражения требуют больше ресурсов и их стоит использовать только там, где другие функции VRL не могут быть использованы.

Пример 11. Пример использования регулярных выражений
...
on_correlate: !vrl |
  message = parse_regex_all!(.raw, r'(?P<key_value>(\S*))')
...
Пример 12. Оптимизированный код
...
on_correlate: !vrl |
  parsed_message = parse_key_value(.raw) ?? {}
...

Контроль структуры кода

Приоритеты операторов

При использовании нескольких логических операторов важно явно задавать приоритет операций с помощью скобок, чтобы избежать двусмысленных условий.

Если написать код без скобок:

.dvender == "Microsoft" && .externalId == "4688" || .externalId == "4688"

Этот код будет интерпретирован как два отдельных блока:

  • .dvender == "Microsoft" И .externalId == "4688"

  • .externalId == "4688"

Если цель заключается в том, чтобы условие .dvender == "Microsoft" было связано с одним из значений .externalId, необходимо использовать скобки для группировки операторов.

Пример 13. Пример с использованием скобок
.dvender == "Microsoft" && (.externalId == "4688" || .externalId == "1234")

Для сложных условий с несколькими процессами также требуется группировка:

.dvender == "Microsoft" && (.dproc == "ipconfig.exe" || .dproc == "nltest.exe" || .dproc == "ping.exe")

Приведение полей к нужному типу

В блоке on_correlate рекомендуется приводить значения полей к ожидаемому типу данных. Это помогает избежать ошибок при обработке данных и обеспечивает корректное выполнение правил корреляции.

Рекомендуемые значения для различных типов данных:

Таблица 1. Типы данных и их значения по умолчанию
Тип данных Значение по умолчанию

LCString

пустая строка ("")

DateTime

t"1970-01-01T00:00:00.000Z"

Enum

-1

IPv6

::

UInt

0

Array

[]

MAC

пустая строка ("")

Пример 14. Примеры преобразования значений к нужным типам
to_string(.dproc) ?? ""                # Преобразование к строке LCString
to_int(dvcpid) ?? 0                    # Преобразование к UInt
ip_to_ipv6(agent.ip) ?? "::"           # Преобразование к IPv6

Шаблонизация строк

При написании RObject-конфигураций рекомендуется использовать строковую шаблонизацию для формирования сообщений. Это позволяет улучшить читаемость кода и избежать ошибок, связанных с объединением строк вручную.

Вместо использования функции join для формирования строки, можно применить шаблонизированные строки, которые поддерживаются в VRL. В шаблонизированной строке переменные значения заключаются в фигурные скобки {}, и их значения автоматически подставляются в строку.

Пример 15. Пример кода с использованием функции join:
...
on_correlate: !vrl |
  %state = .
  %state.msg = join(["На хосте", (to_string(.dvc) ?? "-"),
                     "пользователем", (to_string(.duser) ?? "-"),
                     "было зафиксировано изменение параметров критичной политики"],
                     separator: " ") ?? "-"
...

В данном примере функция join используется для объединения строк и переменных. Этот подход требует явного указания разделителей и может приводить к сложностям при добавлении или изменении текста.

Пример 16. Пример кода с использованием шаблонизации:
...
on_correlate: !vrl |
  %state = .
  %state.msg = "На хосте {to_string(.dvc) ?? '-'} пользователем {to_string(.duser) ?? '-'} было зафиксировано изменение параметров критичной политики"
...

Использование шаблонизации позволяет подставлять значения переменных прямо в строку, что упрощает структуру кода и снижает вероятность ошибок.

Контроль дублирования строк

Дублирование строк в правилах может затруднить их поддержку и привести к ошибкам при изменении кода. Повторяющиеся строки следует избегать или объединять.

Пример 17. Код с дублированием
...
on_correlate: !vrl |
  . |= compact({
    "name": %state.name,
    "dhost": %state.dhost,
    "dntdom": %state.dntdom,  # Дублирование
    "dvc": %state.dvc,
    "dntdom": %state.dntdom,  # Дублирование
    ...
  })
...
Пример 18. Оптимизированный код
...
on_correlate: !vrl |
  . |= compact({
    "name": %state.name,
    "dhost": %state.dhost,
    "dntdom": %state.dntdom,
    "dvc": %state.dvc,
    ...
  })
...