
В данном цикле статей я опишу реализацию дашбордов по уязвимостям на no-code решении Budibase, разберу собственную систему приоритизации уязвимостей, покажу, как повысить эффективность устранения угроз с помощью EPSS, и как объединить данные из разных решений в области кибербезопасности.
В управлении уязвимостями можно столкнуться с рядом сложностей, которые невозможно решить одним универсальным методом. У компаний различаются организационная структура, бюджет и используемые технические решения. Поэтому я решил не делать пошаговый tutorial, а вместо этого описать свой практический опыт и идеи.
Основная задача - создать внутреннее приложение, способное предоставить всем участникам процесса (SOC, системным администраторам и руководству задействованных подразделений) доступ к актуальной статистике и понимание текущей ситуации по уязвимостям. Вторая задача - реализовать приоритизацию уязвимостей более эффективным способом.
Зачем нужны дашборды и унифицированное решение?
Одна из проблем, которую я наблюдал - это отсутствие единого решения, способного объединить данные из разных источников, вовлечь всех участников процесса управления уязвимостями, а также отсутствие фокуса и контроля со стороны сотрудников, обладающих полномочиями влиять на процесс и принимать вышестоящие решения.

С организационной точки зрения в крупных и регулируемых компаниях может возникать ситуация, когда подразделение информационной безопасности функционирует отдельно от ИТ-подразделения. В результате формируются два независимых руководителя структурных единиц, и основная ответственность за ИБ ложится на подразделение безопасности. Хотя такое разделение часто является обязательным требованием регуляторов и стандартов, оно может создавать конфликт интересов между подразделениями и повышать риски, связанные с человеческим фактором.
Именно поэтому на схеме выше я специально указал вышестоящего руководителя как лицо, обладающее полномочиями давать указания всем структурным подразделениям, участвующим в процессе управления уязвимостями.
Со стороны ИТ-подразделения должно быть чёткое понимание важности устранения уязвимостей, так как это напрямую влияет не только на безопасность, но и на отказоустойчивость инфраструктуры.
Почему Budibase?
Сканировать и отправлять отчёт с уязвимостями, как правило, недостаточно. В идеале должны проводиться совещания между командами для обсуждения результатов, просмотра отчётов и графиков, поиска альтернатив и принятия решений, например, в случаях, когда устранить уязвимость невозможно.
Визуализация результатов работы показывает тренд, задаёт команде более энергичный ритм и позволяет наглядно увидеть эффект от предпринятых действий.
Budibase является open-source решением, поддерживает PostgreSQL, MySQL, REST API, быстро разворачивается и позволяет кастомизировать пользовательский интерфейс. Это хороший выбор для несложного внутреннего приложения.
Установка и первичная настройка Budibase
Для простоты установки системы я воспользовался Docker контейнерами, это наиболее оптимальный способ развертывания.
1. Установка СУБД PostgreSQL
Для начала загрузим образ
docker pull postgres
Создаем файл для хранения пароля например postgres-pwd
echo -n "secretpassword" > postgres-pwd
Назначим права для файла
chmod 600 ./postgres-pwd
Далее попробуем запустить контейнер
sudo docker run -p 5432:5432 \
--name postgres_budibase \
-e POSTGRES_USER=budiuser \
-e POSTGRES_PASSWORD_FILE=/run/secrets/pg_pass \
-v /home/user/postgres_budibase/postgres-pwd:/run/secrets/pg_pass:ro \
-v /var/budibase_test_postgre/pgdata:/var/lib/postgresql/data/ \
-v /home/user/postgres_budibase/csv/:/tmp/postgres/ \
-d postgres
Данную команду нужно будет изменить:
указать путь до файла с паролем - /home/user/postgres_budibase/postgres-pwd
указать путь до каталога хранения СУБД на хосте - /var/budibase_test_postgre/pgdata
а вот этот каталог я использовал для импорта данных из файлов csv - /home/user/postgres_budibase/csv/
2. Установка Budibase:
Для начала загрузим образ
docker pull budibase/budibase:latest
Перейдем в каталог, где расположим файл docker-compose.yml и нужно будет так же поменять значения паролей
version: "3"
services:
budibase:
restart: unless-stopped
image: budibase/budibase
ports:
- "8080:80"
environment:
JWT_SECRET: <secret>
MINIO_ACCESS_KEY: <secret>
MINIO_SECRET_KEY: <secret>
REDIS_PASSWORD: <secret>
COUCHDB_USER: <secret>
COUCHDB_PASSWORD: <secret>
INTERNAL_API_KEY: <secret>
volumes:
- ./budibase_store:/data
Создадим каталог ./budibase_store
mkdir ./budibase_store
Запустим образ.
docker-compose up -d
После этого все данные должны будут сохраниться в локальном каталоге ./budibase_store на хосте. Затем можно перейти в браузер по 8080 порту.
3. Импорт данных из сканера
Какое бы вы решение не использовали для сканирования и инвентаризации активов - MaxPatrol 8, MaxPatrolVM, Nessus или другой сканер, в любом случае определение полей таблиц у всех будет разное, в зависимости от потребностей и возможностей сканера.
Я использовал данные из MaxPatrol VM. Немного поправил официальную python библиотеку API и формировал скриптом PDQL запрос в сторону API ендпоинта и получал выгрузку в формате CSV.
Затем я парсил и объединял файлы из разных источников, добавляя дополнительную информацию - например наименование системы к которой относится актив.

Оригинал библиотеки для API MaxPatrolVM - https://addons.ptsecurity.com/ptvm-sdk, https://gitlab.com/bsploit/ptvm_sdk/
Немного модифицированная мною библиотека - https://github.com/ErSilh0x/maxpatrolvm/tree/main/api
В ней я добавил функцию exportByPDQLByGroup
, а так же указал параметры proxy для requests
, в качестве прокси использовал BurpSuite, что бы понять что возвращает API и какую логику выстраивать в скрипте. Для получения данных из MaxPatrol VM API необходимо отправить запрос PDQL, а потом отправить запрос на скачивание файла csv, но время, за которое он сформируется на бэкенде неизвестно. В случае если обратиться раньше придет ответ 404. Я это обошел делая запрос в цикле 10 раз подряд с паузой и при ошибке, увеличивая паузу между запросами на определенное количество минут. Из 10 попыток он должен скачать файл и такой скрипт будет более отказоустойчив.
Пример MP VM PDQL запроса с перечнем колонок под ОС Windows:
select(WindowsHost.@NodeVulners.DiscoveryTime as VulnDiscoveryTime,
WindowsHost.@AuditTime as AuditTime,
WindowsHost.IpAddress as IpAddress, @WindowsHost as Hostname,
WindowsHost.OsName as OsName, WindowsHost.OsVersion as OsVersion,
WindowsHost.OsRelease as Release,
WindowsHost.@Vulners.VulnerableEntity.Name as VulnerableEntity,
WindowsHost.KernelVersion as VulnerableEntityVersion,
WindowsHost.DirectoryServicePath as EntityPath,
WindowsHost.@NodeVulners as Vulnerability,
WindowsHost.@NodeVulners.IssueTime,
WindowsHost.@NodeVulners.CVEs as CVE,
WindowsHost.@NodeVulners.Description as VulnDescription,
WindowsHost.@NodeVulners.HowToFix as HowToFix,
WindowsHost.@NodeVulners.Patch.DisplayName as Patch,
WindowsHost.@NodeVulners.SeverityRating as Severity,
WindowsHost.@NodeVulners.CVSS3BaseScore as CVSS3,
WindowsHost.@NodeVulners.Metrics as Metrics,
WindowsHost.@NodeVulners.Status as Status) | filter(Vulnerability) | sort(Hostname) | limit(0)
Вообще PDQL запросов в своем скрипте я делал несколько под разные группы активов, а потом парсил результат, добавлял дополнительные данные из выгрузок других систем и получал готовый файл .csv - каждая строка это уязвимость на конкретном активе.
Хотя Budiabase позволяет выполнять SQL запросы у себя в веб-интерфейсе я периодически пользуюсь командной строкой. Подключиться к СУБД с хоста на котором он установлен следующей командой:
sudo docker exec -it postgres_budibase sh -c "psql -U budibase"
SQL запрос на создание таблицы с данными
CREATE TABLE allassets_tbl ( record_id SERIAL PRIMARY KEY, VulnDiscoveryTime TIMESTAMP, AuditTime TIMESTAMP, IpAddress VARCHAR(255), Hostname VARCHAR(255), OsName VARCHAR(255), OsVersion VARCHAR(255), Release VARCHAR(255), VulnerableEntity VARCHAR(255), VulnerableEntityVersion VARCHAR(255), EntityPath VARCHAR(255), Vulnerability VARCHAR(255), VulnerabilityIssueTime TIMESTAMP, CVE VARCHAR(255), VulnDescription TEXT, HowToFix TEXT, Patch VARCHAR(255), PatchPublishDate TIMESTAMP, Severity VARCHAR(255), CVSS3 VARCHAR(255), Metrics VARCHAR(255), VulnerIsTrend VARCHAR(255), HostImportance VARCHAR(255), Status VARCHAR(255), System TEXT, epssScore smallint, epssPercentile smallint
);
Далее копируем .csv файл с уязвимостями в каталог, доступный для docker контейнера с СУБД. Замечу что у меня файл весит более 5Гб. Через Budibase его не импортировать.
Ниже SQL запрос на импорт .csv файла в таблицу.
COPY allassets_tbl(VulnDiscoveryTime, AuditTime, IpAddress, Hostname, OsName, OsVersion, Release, VulnerableEntity, VulnerableEntityVersion, EntityPath, Vulnerability, VulnerabilityIssueTime, CVE, VulnDescription, HowToFix, Patch, PatchPublishDate, Severity, CVSS3, Metrics, VulnerIsTrend, HostImportance, Status, System, epssScore, epssPercentile)
FROM '/tmp/postgres/vulns.csv'
DELIMITER ';'
CSV HEADER;
В основном для хранения данных я использовал одну основную таблицу. Я не стал создавать сложную реляционную структуру СУБД, так как в таком случае пришлось бы думать об учете и корректировке активов, которые вышли из эксплуатации и наполнение новыми. Это создало бы гораздо больше работы в будущем, усложнило бы SQL запросы и в итоге увеличило бы нагрузку на ЦПУ сервера. Но это всегда можно будет реализовать в будущем, если понадобится развитие. Главное начать хоть с чего-то и вообще понять подойдет ли то или иное решение для выполнения задачи.
В итоге получилась таблица в СУБД с перечнем всех уязвимостей построчно и теперь можно делать SQL запросы для визуализации дашбордов.
Первичный интерфейс:
При первом входе в Budibase, необходимо создать приложение. В Budibase можно создавать множество приложений и в каждом приложении можно создавать множество страниц.

После создания приложения подключимся к PostgreSQL. Сделать это можно в редакторе приложения, во вкладке Data.

Разграничение прав доступа для всех участников процесса
В Budibase есть возможность разграничить доступы на страницы и объекты. По сути задается RBAC - доступ на базе ролей.

Я сделал так, что бы например администраторы определенных серверов могли видеть только свои данные. И так для каждой группы ответственной за обновление той или иной категории активов.
Оптимизируем SQL запросы с помощью Materialized View
Каждый дашборд формируется с помощью SQL запроса к СУБД. Каждый такой запрос нагружает СУБД и требуется время на расчет статистики, а если данных много, например миллионы строк, то начнут возникать проблемы.
Для дашбордов хорошим методом оптимизации будет использование Materialized View в PostgreSQL. Materialized View это физическая копия результата SQL‑запроса, сохранённая в виде таблицы. При создании, запрос выполняется один раз, а результат сохраняется.
CREATE MATERIALIZED VIEW mat_assets AS
SELECT * FROM assets_tbl;
После создания данные остаются статичными до обновления, поэтому при изменении первичных таблиц в СУБД они не отражаются автоматически и их нужно обновлять например таким запросом:
REFRESH MATERIALIZED VIEW mat_assets;
Сами запросы хранится в Budibase во вкладке Data определенного приложения.

Задаем автоматизацию
Одной из полезных фич Budibase является возможность создания автоматизированных задач.

Например, materialized views требуют периодического расчета новых данных для обновления своих таблиц. Я создал задачу, которая периодически производит расчет после импорта данных. В ней задал SQL запрос, в котором прописал запрос на обновление, пример:
"DO $$
BEGIN
REFRESH MATERIALIZED VIEW allassets;
REFRESH MATERIALIZED VIEW mat_Count_vuln_active_by_found_fixed;
REFRESH MATERIALIZED VIEW mat_Chart_area_count_month_severity_active;
END $$;"
Примеры дашбордов по подсчету количества активов и их типов
Вот так например выглядит первая страница с подсчетом активов.

Формируются дашборды с помощью запроса SQL из materialized views таблиц, о которых упоминал ранее - это значительно сокращает время и экономит вычислительные мощности.
Дашборд 1 - показывает количество серверов Windows от всех активов.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_count_windows_server AS
SELECT COUNT(DISTINCT hostname) AS numofwin FROM allassets_tbl WHERE osname ILIKE '%windows 20%';
Запрос для дашборда:
SELECT * FROM mat_count_windows_server;
Дашборд 2 - показывает количество рабочих станций от всех активов.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_count_windows_arm AS
SELECT COUNT(DISTINCT hostname) AS numofwin FROM allassets_tbl WHERE osname ILIKE '%windows 1%';
Запрос для дашборда:
SELECT * FROM mat_count_windows_arm;
Дашборд 3 - показывает количество сетевых устройств от всех активов.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_cisco_ios AS
SELECT COUNT(DISTINCT hostname) numofios FROM allassets_tbl WHERE osname ILIKE '%ios%';
Запрос для дашборда:
SELECT * FROM mat_cisco_ios;
Дашборд 4 - показывает количество гипервизоров.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_сount_esxi_os AS
SELECT COUNT(DISTINCT hostname) AS numoflin FROM allassets_tbl WHERE osname ILIKE '%esxi%';
Запрос для дашборда:
SELECT * FROM mat_сount_esxi_os;
Примеры дашбордов для уязвимостей Windows серверов
Для каждой категории активов я делал отдельные дашборды, как пример для Windows серверов я создал несколько вкладок и в каждой разместил серию дашбордов. В данной статье я покажу дашборды из первой вкладки - тренды уязвимостей и прогресс устранения. Другие вкладки для приоритизации уязвимостей я опишу в следующий раз.

Дашборд 1 - показывает общее количество активных и устраненных уязвимостей по Windows серверам.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_Count_vuln_windows_server AS
SELECT COUNT(hostname) AS numofvuln FROM allassets_tbl WHERE osname ILIKE '%windows 20%';
Запрос для дашборда:
SELECT * FROM mat_Count_vuln_windows_server;
Дашборд 2 - показывает общее количество только активных уязвимостей по Windows серверам.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_Count_vuln_active_windows_server AS
SELECT COUNT(hostname) AS numofvuln FROM allassets_tbl WHERE osname ILIKE '%windows 20%' AND status = 'new';
Запрос для дашборда:
SELECT * FROM mat_Count_vuln_active_windows_server;
Дашборд 3 - показывает процент активных уязвимостей от всех по Windows серверам.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_Count_vuln_active_windows_server_percent AS
SELECT
active.vuln,
ROUND(active.vuln * 100.0 / all_win.total_vuln , 0) AS percent
FROM ( SELECT COUNT(hostname) AS vuln
FROM allassets_tbl WHERE osname ILIKE '%windows 20%' AND status = 'new' )
AS active,
( SELECT COUNT(hostname) AS total_vuln
FROM allassets_tbl
WHERE osname ILIKE '%windows 20%' )
AS all_win;
Запрос для дашборда:
SELECT * FROM mat_Count_vuln_active_windows_server_percent;
Дашборд 4 - показывает количество критических уязвимостей по Windows серверам.
SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_Count_vuln_active_windows_server_critical AS
SELECT COUNT(hostname) AS numofvuln FROM allassets_tbl WHERE osname ILIKE '%windows 20%' AND status = 'new' AND severity = 'critical';
Запрос для дашборда:
SELECT * FROM mat_Count_vuln_active_windows_server_critical;
Дашборд 5 - показывает статус уязвимостей в разрезе по месяцам, по Windows серверам.

SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_Count_vuln_active_by_found_fixed AS
WITH VulnerabylitiesFixed AS (
SELECT COUNT(hostname) as fixed, EXTRACT(MONTH from VulnDiscoveryTime) AS month
FROM allassets_tbl
WHERE osname ILIKE '%windows 20%' AND status = 'fixed'
GROUP BY month
ORDER BY month),
VulnerabylitiesNew AS ( SELECT COUNT(hostname) AS numvulns,
EXTRACT(MONTH from VulnDiscoveryTime) AS month
FROM allassets_tbl
WHERE osname ILIKE '%windows 20%' AND status = 'new'
GROUP BY month
ORDER BY month)
SELECT v.numvulns, vf.fixed, v.month
FROM VulnerabylitiesNew v
JOIN VulnerabylitiesFixed vf ON v.month = vf.month;
Запрос для дашборда:
SELECT * FROM mat_Count_vuln_active_by_found_fixed;
Дашборд 6 - показывает текущий статус уязвимостей с рейтингом риска в разрезе по месяцам, по Windows серверам.

SQL запрос на создание MATERIALIZED VIEW:
CREATE MATERIALIZED VIEW mat_Chart_area_count_month_severity_active AS
SELECT
EXTRACT(MONTH from VulnDiscoveryTime) AS month, COUNT(hostname) AS total,
COUNT(hostname) FILTER (WHERE severity = 'critical') AS critical,
COUNT(hostname) FILTER (WHERE severity = 'high') AS high,
COUNT(hostname) FILTER (WHERE severity = 'medium') AS medium,
COUNT(hostname) FILTER (WHERE severity = 'low') AS low,
COUNT(hostname) FILTER (WHERE severity = 'none') AS none
FROM allassets_tbl
WHERE osname ILIKE '%windows 20%' AND status = 'new'
GROUP BY month
ORDER BY month;
Запрос для дашборда:
SELECT * FROM mat_Chart_area_count_month_severity_active;
Итог первой части
Ну вот, уже что-то получилось сделать. Пока данные дашборды дают общую информацию. В следующей статье я опишу приоритизацию устранения уязвимостей и мои идеи по данной теме, так что она будет в любом случае полезна, даже если вы не планируете разворачивать свои дашборды или заниматься разработкой собственного решения.
Было бы здорово услышать идеи о том как бы вы вовлекали в процесс устранения уязвимостей смежные подразделения и мотивируете коллег обновлять активы быстрее?