Здравствуйте! Меня зовут Игорь, и я руковожу несколькими направлениями в команде DevOps-инженеров, включая направление мониторинга.

Сегодня я хочу рассказать вам о нашем уникальном решении для Zabbix. Это решение позволяет быстро уведомлять о найденных уязвимостях, формировать список этих уязвимостей и предоставлять дополнительную информацию о них.

Возьмите вкусняшек, чайку и присаживайтесь поудобнее.

Пролог

Впервые я столкнулся с подобной задачей лет семь-восемь назад, когда работал администратором бок о бок с командой безопасности (КБ).

Перед нами стояла задача найти инструмент для поиска уязвимостей на Linux-хостах и создать понятный отчёт. Мы выбрали инструмент Vulners. Однако было одно условие: чтобы сгенерировать отчёт, нужно было выполнить команду по сбору пакетов (rpm/dpkg) с определёнными аргументами и отправить весь список в API Vulners.

Команда безопасности (КБ) выразила некую обеспокоенность этим условием и попросила выполнить задачу локально, без привлечения третьих лиц.

После некоторых размышлений и изучения информации я обнаружил, что можно загрузить бюллетени с уязвимостями для каждой операционной системы через API в формате JSON (сейчас эта услуга платная). Затем мне осталось только написать скрипт, который будет собирать и анализировать эти данные, формировать отчёт и каким-то образом информировать администраторов и команду безопасности о выявленных уязвимостях.

Через неделю или две я создал этот инструмент. Сразу же мы начали получать подробные отчёты на почту, а в Zabbix появлялось предупреждение о наличии критических уязвимостей на хостах. Это позволяло нам быстро реагировать и устранять уязвимости в соответствии с установленными регламентами.

Концепция

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

Затем я начал искать в интернете и наткнулся на интересный инструмент vuls.io. Мы запустили пилотный проект (параллельно закупая MaxPatrol VM), передали веб-интерфейс КБ и начали тестирование.

В целом инструмент выполняет свою задачу, но когда количество хостов не сотни, а тысячи, веб-интерфейс с отчётами становится неинформативным, сильно тормозит и иногда даже падает. Кроме того, отчёт не содержит всей необходимой информации, и нам было сложно понять, как перенести её в Zabbix. Однако на этом этапе я определил, что нам нужно получить от инструмента:

  • интеграцию с Zabbix;

  • список обновляемых пакетов;

  • сортировку по уровню критичности.

Пришло время для MaxPatrol VM. Мы его установили, создали пользователей и ключи, распределили конфигурации и ключи по всей инфраструктуре и передали команде безопасности (КБ) для настройки сканирования инфраструктуры.

После внедрения системы к нам начали активно поступать задачи, связанные с уязвимостями, списками хостов и карточками активов в MaxPatrol. Однако не всё было гладко:

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

  • Есть сложный собственный язык запросов, без помощи не разобраться.

  • Если уязвимость затрагивает несколько пакетов, то они отображаются в поиске как отдельные хосты, а не объединяются (хотя можно всё посмотреть в карточке).

Также были замечания по поводу настройки сканирования — еë нет. Нам нужно было только собрать список пакетов по хостам и проверить их на наличие уязвимостей. Скорость сканирования некоторых хостов была очень низкой и достигала суток по непонятным причинам.

После того как мы начали работать над устранением уязвимостей и интеграцией инструмента в инфраструктуру, появились дополнительные требования:

  • Необходима автоматизация процессов, включая отправку оповещений, создание отчётов и обновление пакетов.

  • Требуется сформировать команду для обновления необходимых пакетов в зависимости от используемой операционной системы.

  • Нужны подробные инструкции и руководства о том, как работать с инструментом и что делать.

Реализация

Необходимо создать скрипт, который будет отправлять запросы в MaxPatrol и собирать нужные данные для Zabbix в виде массива.

maxpatrol_vulns.py
#!/usr/bin/env python3
import requests
import json
import csv
import sys
import argparse

from urllib3.exceptions import InsecureRequestWarning
from io import StringIO

oauth2Payload = {
    "client_id": "mpx",
    "grant_type": "password",
    "response_type": "code id_token",
    "scope": "offline_access mpx.api ptkb.api",
}
maxErrors = 50


class MaxPatrolClient():

    def __init__(self, oauth2Payload):
        parser = argparse.ArgumentParser()
        parser.add_argument("-u", "--user", help="Username for login in MaxPatrol", required=True)
        parser.add_argument("-p", "--password", help="Password for login in MaxPatrol", required=True)
        parser.add_argument("-c", "--client-secret", help="Client secret for OAuth2", required=True)
        parser.add_argument("-H", "--host", help="MaxPatrol host", required=True)
        parser.add_argument("-s", "--severity", help="The importance of vulnerability", default="critical")
        args = parser.parse_args()

        self.hostUrl = "https://" + args.host
        self.severity = args.severity

        creds = {
            "client_secret": args.client_secret,
            "username": args.user,
            "password": args.password
        }

        # Suppress only the single warning from urllib3 needed.
        requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
        self.headers = {
          'Accept': 'application/json',
          'Authorization': f'Bearer {self.createToken(self.hostUrl, {**oauth2Payload, **creds} )}',
          'Content-Type': 'application/json'
        }
        self.errorCount = 0 # Sum of all atemps to make request, prevent entering in crash loop

    def createToken(self, hostUrl, oauth2Payload):
        response = requests.post(hostUrl + ":3334/connect/token", data=oauth2Payload, verify=False)
        return json.loads(response.text)['access_token']

    def pushQuery(self, query):
        # Push query to processing queue
        url = f"{self.hostUrl}/api/assets_temporal_readmodel/v1/assets_grid"
        response = requests.post(url, headers=self.headers, data=self.createQueryPayload(query), verify=False)

        # Get result after query processed
        data = json.loads(response.text)
        url = f"{self.hostUrl}/api/assets_temporal_readmodel/v1/assets_grid/export?pdqlToken={data['token']}"
        self.errorCount += 1 # Increase errorCount
        response = requests.get(url, headers=self.headers, verify=False)

        if response.status_code == 404:

            if self.errorCount >= maxErrors:
                print(f"[ERROR] TO MANY ATTEMPS, last response: {response.text}")
                sys.exit(1)

            return self.pushQuery(query)
        self.errorCount = 0
        return response.text

    def getVulnedPackages(self):
        # Push query to processing queue
        if self.severity == "critical":
            query = f"select(@UnixHost, UnixHost.UnameNodeName, UnixHost.Packages.@NodeVulners.CVSS3Score, UnixHost.Packages.@NodeVulners.istrend, UnixHost.Packages.@NodeVulners.metrics.Exploitable, \
                UnixHost.Packages.Name, UnixHost.Packages.Version, UnixHost.Packages.@NodeVulners.KB, UnixHost.@Id, UnixHost.Packages.@NodeVulners, \
                UnixHost.Packages.@NodeVulners.Status, UnixHost.OsName, UnixHost.@Importance) | filter(UnixHost.Packages.@NodeVulners) | \
                filter((UnixHost.Packages.@NodeVulners.Status not in ['Fixed', 'Excluded', 'awaitingFix']) AND (UnixHost.Packages.@NodeVulners.Status != null)) | \
                filter((UnixHost.Packages.@NodeVulners.SeverityRating = 'Critical' AND UnixHost.Packages.@NodeVulners.istrend = true AND \
                UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR (UnixHost.Packages.@NodeVulners.SeverityRating = 'Critical' AND \
                UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR \
                (UnixHost.Packages.@NodeVulners.SeverityRating = 'High' AND UnixHost.Packages.@NodeVulners.istrend = true AND \
                UnixHost.Packages.@NodeVulners.metrics.Exploitable = true  AND UnixHost.@Importance = 'H'))"
        elif self.severity == "high":
            query = f"select(@UnixHost, UnixHost.UnameNodeName, UnixHost.Packages.@NodeVulners.CVSS3Score, UnixHost.Packages.@NodeVulners.istrend, UnixHost.Packages.@NodeVulners.metrics.Exploitable, \
                UnixHost.Packages.Name, UnixHost.Packages.Version, UnixHost.Packages.@NodeVulners.KB, UnixHost.@Id, UnixHost.Packages.@NodeVulners, \
                UnixHost.Packages.@NodeVulners.Status, UnixHost.OsName, UnixHost.@Importance) | filter(UnixHost.Packages.@NodeVulners) | \
                filter((UnixHost.Packages.@NodeVulners.Status not in ['Fixed', 'Excluded', 'awaitingFix']) AND (UnixHost.Packages.@NodeVulners.Status != null)) | \
                filter((UnixHost.Packages.@NodeVulners.SeverityRating = 'High' AND UnixHost.Packages.@NodeVulners.istrend = true AND \
                UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR (UnixHost.Packages.@NodeVulners.SeverityRating = 'High' AND \
                UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR \
                (UnixHost.Packages.@NodeVulners.SeverityRating = 'Medium' AND UnixHost.Packages.@NodeVulners.istrend = true AND \
                UnixHost.Packages.@NodeVulners.metrics.Exploitable = true  AND UnixHost.@Importance = 'H'))"
        elif self.severity == "medium":
            query = f"select(@UnixHost, UnixHost.UnameNodeName, UnixHost.Packages.@NodeVulners.CVSS3Score, UnixHost.Packages.@NodeVulners.istrend, UnixHost.Packages.@NodeVulners.metrics.Exploitable, \
                UnixHost.Packages.Name, UnixHost.Packages.Version, UnixHost.Packages.@NodeVulners.KB, UnixHost.@Id, UnixHost.Packages.@NodeVulners, \
                UnixHost.Packages.@NodeVulners.Status, UnixHost.OsName, UnixHost.@Importance) | filter(UnixHost.Packages.@NodeVulners) | \
                filter((UnixHost.Packages.@NodeVulners.Status not in ['Fixed', 'Excluded', 'awaitingFix']) AND (UnixHost.Packages.@NodeVulners.Status != null)) | \
                sort(UnixHost.@Importance ASC) | filter((UnixHost.Packages.@NodeVulners.SeverityRating = 'High') OR \
                (UnixHost.Packages.@NodeVulners.SeverityRating = 'Medium' AND UnixHost.Packages.@NodeVulners.istrend = true AND \
                UnixHost.Packages.@NodeVulners.metrics.Exploitable = false  AND UnixHost.@Importance = 'ND') OR \
                (UnixHost.Packages.@NodeVulners.SeverityRating = 'Medium' AND UnixHost.Packages.@NodeVulners.istrend = true AND \
                UnixHost.Packages.@NodeVulners.metrics.Exploitable = true  AND UnixHost.@Importance = 'ND') OR (UnixHost.Packages.@NodeVulners.SeverityRating = 'Low' AND \
                UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true  AND UnixHost.@Importance = 'H'))"
        else:
            print('Некорректное значение.')

        csvResult = self.pushQuery(query)

        # CSV result parsing and processing to JSON
        jsonResult = {}
        f = StringIO(csvResult)
        reader = csv.reader(f, delimiter=';')

        # Drop metadata line from CSV result
        next(reader)

        for row in reader:
            hostName = row[0].split(" ")[0]
            hostCVSS3Score = float(row[2].replace(',','.'))
            if not jsonResult.__contains__(hostName):
                jsonResult[hostName] = {
                    'Host': row[1],
                    'HostCVSS3Score': hostCVSS3Score,
                    'HostInfoUrl': f"{self.hostUrl}/#/assets/view/{row[8]}",
                    'UpdateCommand': set(),
                    'OS': row[11]
                }
            if jsonResult[hostName]['HostCVSS3Score'] < hostCVSS3Score:
                jsonResult[hostName]['HostCVSS3Score'] = hostCVSS3Score

            jsonResult[hostName]['UpdateCommand'].add(row[5])

        # Some processing...
        for hostName in jsonResult:
            host = jsonResult[hostName]
            rhel = ['CentOS', 'Oracle Linux']
            deb = ['Debian', 'Ubuntu']
            
            if host['OS'] in deb:
                host['UpdateCommand'] = f"apt -y install --only-upgrade {' '.join(host['UpdateCommand'])}"
            if host['OS'] in rhel:
                host['UpdateCommand'] = f"yum -y update {' '.join(host['UpdateCommand'])}"

        return json.dumps(jsonResult)

    def createQueryPayload(self, query):
        return json.dumps({
          "pdql": str(query),
          "selectedGroupIds": [],
          "additionalFilterParameters": {
            "groupIds": [],
            "assetIds": []
          },
          "includeNestedGroups": True,
          "utcOffset": "+03:00"
        })

if __name__ == "__main__":
    client = MaxPatrolClient(oauth2Payload)
    print('{"data":[' + client.getVulnedPackages() + ']}')

Автор — @absoluterenatus Скрипт принимает на вход несколько аргументов:

  • "-u", "--user" — пользователь в MaxPatrol;

  • "-p", "--password" — пароль от пользователя в MaxPatrol;

  • "-c", "--client-secret» — ключ доступа в MaxPatrol;

  • "-H", "--host" — адрес MaxPatrol;

  • "-s", "--severity" — важность определения уязвимости диктует выбор необходимого запроса и формирование массива данных для LLD — низкоуровневого обнаружения.

Основа всего, это запросы, которые предоставляют нам все необходимые данные. Рассмотрим это на примере поиска критически уязвимых хостов.

Запрос
select(@UnixHost, UnixHost.UnameNodeName, UnixHost.Packages.@NodeVulners.CVSS3Score, UnixHost.Packages.@NodeVulners.istrend, UnixHost.Packages.@NodeVulners.metrics.Exploitable, UnixHost.Packages.Name, UnixHost.Packages.Version, UnixHost.Packages.@NodeVulners.KB, UnixHost.@Id, UnixHost.Packages.@NodeVulners, UnixHost.Packages.@NodeVulners.Status, UnixHost.OsName, UnixHost.@Importance) | filter(UnixHost.Packages.@NodeVulners) | filter((UnixHost.Packages.@NodeVulners.Status not in ['Fixed', 'Excluded', 'awaitingFix']) AND (UnixHost.Packages.@NodeVulners.Status != null)) | filter((UnixHost.Packages.@NodeVulners.SeverityRating = 'Critical' AND UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR (UnixHost.Packages.@NodeVulners.SeverityRating = 'Critical' AND UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR (UnixHost.Packages.@NodeVulners.SeverityRating = 'High' AND UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true  AND UnixHost.@Importance = 'H'))

Где:

  • select(@UnixHost, UnixHost.UnameNodeName, UnixHost.Packages.@NodeVulners.CVSS3Score, UnixHost.Packages.@NodeVulners.istrend, UnixHost.Packages.@NodeVulners.metrics.Exploitable, UnixHost.Packages.Name, UnixHost.Packages.Version, UnixHost.Packages.@NodeVulners.KB, UnixHost.@Id, UnixHost.Packages.@NodeVulners, UnixHost.Packages.@NodeVulners.Status, UnixHost.OsName, UnixHost.@Importance) — выбирает нужные нам поля для формирования отчёта. Не все поля нужны, но так как этот запрос активно используется в работе МП, мы просто перенесли его в скрипт.

  • filter((UnixHost.Packages.@NodeVulners.Status not in ['Fixed', 'Excluded', 'awaitingFix']) AND (UnixHost.Packages.@NodeVulners.Status != null)) — фильтр исключает записи, которые были исправлены, удалены, ожидают исправления или имеют пустой статус.

  • filter((UnixHost.Packages.@NodeVulners.SeverityRating = 'Critical' AND UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR (UnixHost.Packages.@NodeVulners.SeverityRating = 'Critical' AND UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true) OR (UnixHost.Packages.@NodeVulners.SeverityRating = 'High' AND UnixHost.Packages.@NodeVulners.istrend = true AND UnixHost.Packages.@NodeVulners.metrics.Exploitable = true  AND UnixHost.@Importance = 'H')) — фильтр выбирает критические и высокие уязвимости, которые находятся в тренде и имеют доступные эксплойты.

Вывод запроса в MaxPatrol
Вывод запроса в MaxPatrol

Поля, необходимые для создания метрик и получения нужной информации:

  • 'Host' — имя сервера;

  • 'HostCVSS3Score' — рейтинг уязвимости;

  • 'HostInfoUrl' — ссылка на карточку сервера в MaxPatrol;

  • 'UpdateCommand' — команда для обновления уязвимых пакетов;

  • 'OS' — операционная система.

Пример массива данных для LLD:

LLD
{
  "data": [
    {
      "host1": {
        "Host": "host1",
        "HostCVSS3Score": 7.2,
        "HostInfoUrl": "https://localhost/#/assets/view/195780a2-bf00-0001-0000-0000000000f0",
        "UpdateCommand": "apt -y install --only-upgrade linux-image-generic",
        "OS": "Ubuntu"
      },
      "host2": {
        "Host": "host2",
        "HostCVSS3Score": 7.2,
        "HostInfoUrl": "https://localhost/#/assets/view/1a292f70-5c80-0001-0000-000000000749",
        "UpdateCommand": "yum -y update kernel-uek",
        "OS": "Oracle Linux"
      },
      "host3": {
        "Host": "host3",
        "HostCVSS3Score": 7,
        "HostInfoUrl": "https://localhost /#/assets/view/1a3b0f44-b900-0001-0000-0000000007e9",
        "UpdateCommand": "yum -y update conmon",
        "OS": "Oracle Linux"
      },
      "host4": {
        "Host": "host4",
        "HostCVSS3Score": 7,
        "HostInfoUrl": "https://localhost/#/assets/view/1a55ccf0-0d80-0001-0000-000000000a30",
        "UpdateCommand": "yum -y update nginx nginx-core nginx-filesystem",
        "OS": "Oracle Linux"
      },
      "host5": {
        "Host": "host5",
        "HostCVSS3Score": 7,
        "HostInfoUrl": "https://localhost/#/assets/view/1a55ccf0-8200-0001-0000-000000000a34",
        "UpdateCommand": "yum -y update nginx-filesystem",
        "OS": "Oracle Linux"
      },
      "host6": {
        "Host": "host6",
        "HostCVSS3Score": 7,
        "HostInfoUrl": "https://localhost/#/assets/view/1a5f336e-0180-0001-0000-000000000b04",
        "UpdateCommand": "yum -y update conmon",
        "OS": "Oracle Linux"
      }
    }
  ]
}

Скрипт готов, теперь нужно создать шаблон для Zabbix.

Template Vulnerabilities by MaxPatrol
zabbix_export:
  version: '6.0'
  date: '2024-06-28T20:15:33Z'
  groups:
    - uuid: 8fb62aef15ca465398f1bc519daa3f5b
      name: 'Templates Custom'
  templates:
    - uuid: 6aa142b202b24713bfb6f8d70c21c762
      template: 'Template Vulnerabilities by MaxPatrol'
      name: 'Template Vulnerabilities by MaxPatrol'
      description: 'Мониторинг уязвимостей через MaxPatrol VM.'
      groups:
        - name: 'Templates Custom'
      items:
        - uuid: 0a554398196d4daebd8ffcf8b445bb06
          name: 'Get critical vulnerabilities info'
          type: EXTERNAL
          key: 'maxpatrol_vulns.py["-u","{$MP.API.USER}","-p","{$MP.API.PASSWORD}","-c","{$MP.API.SECRET}","-H","{$MP.API.URL}","-s","critical"]'
          delay: 15m
          history: '0'
          trends: '0'
          value_type: TEXT
        - uuid: 20d5640a2782498092eab4daa40d941d
          name: 'Get high vulnerabilities info'
          type: EXTERNAL
          key: 'maxpatrol_vulns.py["-u","{$MP.API.USER}","-p","{$MP.API.PASSWORD}","-c","{$MP.API.SECRET}","-H","{$MP.API.URL}","-s","high"]'
          delay: 15m
          history: '0'
          trends: '0'
          value_type: TEXT
      discovery_rules:
        - uuid: bf6fc7594559409a8a550dc1c3164cbd
          name: 'Get critical vulned hosts'
          type: DEPENDENT
          key: get.critical.vulned.hosts
          delay: '0'
          lifetime: 1d
          item_prototypes:
            - uuid: 3691c8e42ee340d6b0826cc8f8e9eeb7
              name: '{#VULNED_HOST}'
              type: DEPENDENT
              key: 'critical.vulned.host.score[{#VULNED_HOST}]'
              delay: '0'
              value_type: FLOAT
              description: |
                Карточка хоста: {#VULNED_INFO}
                Для закрытия уязвимости выполните команду: {#VULNED_CMD}
                Роль для закрытия уязвимостей: ссылка на роль автоматизации
                Регламент: ссылка на регламент
                После выполнения обновления запустите сканирование согласно инструкции: ссылка на инструкцию
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$..[?(@.Host==''{#VULNED_HOST}'')].HostCVSS3Score.first()'
                  error_handler: CUSTOM_VALUE
                  error_handler_params: '0'
                - type: DISCARD_UNCHANGED_HEARTBEAT
                  parameters:
                    - '21600'
              master_item:
                key: 'maxpatrol_vulns.py["-u","{$MP.API.USER}","-p","{$MP.API.PASSWORD}","-c","{$MP.API.SECRET}","-H","{$MP.API.URL}","-s","critical"]'
              tags:
                - tag: CVSSScore
                  value: '{#VULNED_SCORE}'
                - tag: Host
                  value: '{#VULNED_HOST}'
                - tag: OS
                  value: '{#VULNED_OS}'
              trigger_prototypes:
                - uuid: eb474a294bc94785933092305e430768
                  expression: 'last(/Template Vulnerabilities by MaxPatrol/critical.vulned.host.score[{#VULNED_HOST}])>0'
                  name: 'Host {#VULNED_HOST} has critical vulnerabilities'
                  url: '{#VULNED_INFO}'
                  priority: DISASTER
                  description: |
                    Карточка хоста: {#VULNED_INFO}
                    Для закрытия уязвимости выполните команду: {#VULNED_CMD}
                    Роль для закрытия уязвимостей: ссылка на роль автоматизации
                    Регламент: ссылка на регламент
                    После выполнения обновления запустите сканирование согласно инструкции: ссылка на инструкцию
                  manual_close: 'YES'
                  tags:
                    - tag: CVSSScore
                      value: '{ITEM.LASTVALUE}'
                    - tag: Host
                      value: '{#VULNED_HOST}'
                    - tag: OS
                      value: '{#VULNED_OS}'
                    - tag: templatename
                      value: maxpatrol
                    - tag: triggeritem
                      value: '{#VULNED_HOST}'
                    - tag: triggername
                      value: vulnerabilities_critical
          master_item:
            key: 'maxpatrol_vulns.py["-u","{$MP.API.USER}","-p","{$MP.API.PASSWORD}","-c","{$MP.API.SECRET}","-H","{$MP.API.URL}","-s","critical"]'
          lld_macro_paths:
            - lld_macro: '{#VULNED_CMD}'
              path: $..UpdateCommand.first()
            - lld_macro: '{#VULNED_HOST}'
              path: $..Host.first()
            - lld_macro: '{#VULNED_INFO}'
              path: $..HostInfoUrl.first()
            - lld_macro: '{#VULNED_OS}'
              path: $..OS.first()
            - lld_macro: '{#VULNED_SCORE}'
              path: $..HostCVSS3Score.first()
          preprocessing:
            - type: JSONPATH
              parameters:
                - '$.data.[0].*'
              error_handler: DISCARD_VALUE
        - uuid: 4256689274f0487da57c28a081b8d14f
          name: 'Get high vulned hosts'
          type: DEPENDENT
          key: get.high.vulned.hosts
          delay: '0'
          lifetime: 1d
          item_prototypes:
            - uuid: 5f24eeb98b9c4fc986b62965c78eebe2
              name: '{#VULNED_HOST}'
              type: DEPENDENT
              key: 'high.vulned.host.score[{#VULNED_HOST}]'
              delay: '0'
              value_type: FLOAT
              description: |
                Карточка хоста: {#VULNED_INFO}
                Для закрытия уязвимости выполните команду: {#VULNED_CMD}
                Роль для закрытия уязвимостей: ссылка на роль автоматизации
                Регламент: ссылка на регламент
                После выполнения обновления запустите сканирование согласно инструкции: ссылка на инструкцию
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$..[?(@.Host==''{#VULNED_HOST}'')].HostCVSS3Score.first()'
                  error_handler: CUSTOM_VALUE
                  error_handler_params: '0'
                - type: DISCARD_UNCHANGED_HEARTBEAT
                  parameters:
                    - '21600'
              master_item:
                key: 'maxpatrol_vulns.py["-u","{$MP.API.USER}","-p","{$MP.API.PASSWORD}","-c","{$MP.API.SECRET}","-H","{$MP.API.URL}","-s","high"]'
              tags:
                - tag: CVSSScore
                  value: '{#VULNED_SCORE}'
                - tag: Host
                  value: '{#VULNED_HOST}'
                - tag: OS
                  value: '{#VULNED_OS}'
              trigger_prototypes:
                - uuid: 34f580d6f13a47a6993fae59bbf43c58
                  expression: 'last(/Template Vulnerabilities by MaxPatrol/high.vulned.host.score[{#VULNED_HOST}])>0'
                  name: 'Host {#VULNED_HOST} has high vulnerabilities'
                  url: '{#VULNED_INFO}'
                  priority: HIGH
                  description: |
                    Карточка хоста: {#VULNED_INFO}
                    Для закрытия уязвимости выполните команду: {#VULNED_CMD}
                    Роль для закрытия уязвимостей: ссылка на роль автоматизации
                    Регламент: ссылка на регламент
                    После выполнения обновления запустите сканирование согласно инструкции: ссылка на инструкцию
                  manual_close: 'YES'
                  tags:
                    - tag: CVSSScore
                      value: '{ITEM.LASTVALUE}'
                    - tag: Host
                      value: '{#VULNED_HOST}'
                    - tag: OS
                      value: '{#VULNED_OS}'
                    - tag: templatename
                      value: maxpatrol
                    - tag: triggeritem
                      value: '{#VULNED_HOST}'
                    - tag: triggername
                      value: vulnerabilities_high
          master_item:
            key: 'maxpatrol_vulns.py["-u","{$MP.API.USER}","-p","{$MP.API.PASSWORD}","-c","{$MP.API.SECRET}","-H","{$MP.API.URL}","-s","high"]'
          lld_macro_paths:
            - lld_macro: '{#VULNED_CMD}'
              path: $..UpdateCommand.first()
            - lld_macro: '{#VULNED_HOST}'
              path: $..Host.first()
            - lld_macro: '{#VULNED_INFO}'
              path: $..HostInfoUrl.first()
            - lld_macro: '{#VULNED_OS}'
              path: $..OS.first()
            - lld_macro: '{#VULNED_SCORE}'
              path: $..HostCVSS3Score.first()
          preprocessing:
            - type: JSONPATH
              parameters:
                - '$.data.[0].*'
              error_handler: DISCARD_VALUE

Шаблон состоит из двух основных элементов, которые запускают скрипт с нужными параметрами (critical/high), и двух правил LLD. Эти правила используются для создания метрик с рейтингом, необходимой метаинформацией и триггерами.

Не буду подробно описывать каждый элемент данных и правила предобработки. Всё это уже описано в предыдущих статьях. Вместо этого, я опишу только базовую логику создания элементов и срабатывания триггеров:

  • Если в выполняемом запросе есть уязвимые серверы, то они будут включены в массив данных.

  • На основе массива данных создаются прототипы со специальной метрикой — рейтингом оценки уязвимости (CVSS).

  • Если уязвимость на сервере устранена, то он исчезает из списка, а показатель метрики устанавливается на 0.

  • Триггер активируется, когда условие больше 0.

  • Серверы, которые больше не имеют уязвимостей, исключаются из Zabbix в течение 24 часов после устранения проблемы.

Активные триггеры в Zabbix (при наличии уязвимостей)
Активные триггеры в Zabbix (при наличии уязвимостей)
Метрики в Zabbix (latest data)
Метрики в Zabbix (latest data)

Пример сообщения в чате:

? PROBLEM: Host host01 has high vulnerabilities
Сервер: MaxPatrol
Время срабатывания: 2029.05.19 03:21:11 (MSK)
Владелец: @ital/@owner
Команда: Кибербезопасность (https://corp.portal.ru/company/teams/666)
Сервис: #maxpatrol (#audit)
Важность триггера: High

ℹ️ Описание:
???
Карточка хоста: https://localhost/#/assets/view/1a7bdc2d-ea00-0001-0000-000000000e1c
Для закрытия уязвимости выполните команду: yum -y update kernel-uek
Роль для закрытия уязвимостей: ссылка на роль
Регламент: ссылка на регламент
После выполнения обновления запустите сканирование согласно инструкции: ссылка на инструкцию

Эпилог

Это решение значительно облегчило работу администраторам и команде безопасности, и улучшило наше взаимодействие между командами.

Для команды безопасности:

  • Теперь не нужно вручную составлять списки серверов и создавать задачи.

  • В чате появились уведомления, чтобы мы могли быстрее реагировать на сообщения.

Для администраторов:

  • Быстрая реакция и устранение уязвимостей.

  • Сразу же формируется команда и составляется список пакетов для обновления.

  • Появилась новая роль Ansible для устранения уязвимостей.

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

Планы на будущее:

  • Оптимизировать код и запросы, удалить всё ненужное.

  • Интегрировать систему с Jira для автоматического создания задач по устранению уязвимостей.

  • Добавить возможность обновлять пакеты прямо из Zabbix (стоит ли это делать — вопрос спорный, но сама функция интересная).

А как вы реализовывали похожие задачи? Поделитесь своим опытом в комментариях.

На этом у меня всë, всем спасибо за уделенное время и удачи!

Комментарии (2)


  1. AlexUl
    18.07.2024 09:23
    +1

    А на портал расширений свою работу не выкладывали?
    https://addons.ptsecurity.com/


    1. ownhrd Автор
      18.07.2024 09:23

      Даже не знал о нем. Возьму на заметку, спасибо!