Приложение выпущено месяц назад. Его код с тех пор не менялся, в репозитории тишина, пайплайн не запускался. А сегодня у одной из библиотек, которые попали в релиз, появилась новая запись об уязвимости. Ничего в вашем проекте не сломалось, но картина риска изменилась. Вопрос в том, кто вам об этом скажет, если единственный момент проверки был в день сборки.
Эта статья про то, как устроить наблюдение не за очередным коммитом, а за составом уже выпущенных версий. Инструмент для этого называется Dependency-Track, а место для него мы выберем своё: отдельный VPS, где живёт платформа и хранится состав всех релизов.
Снимок и наблюдение: две разные задачи
Сканер в пайплайне отвечает на вопрос: «Что не так с тем, что я собираюсь выпустить прямо сейчас?» Это снимок на момент сборки. Он полезен: он может остановить релиз с заведомо плохой зависимостью. Но у него есть слепая зона во времени. Базы уязвимостей пополняются, а ваш артефакт после выпуска остаётся прежним. Сканирование, которое прошло зелёным в марте, в апреле смотрело бы на тот же состав уже другими глазами, но в апреле его никто не запускал.
Наблюдение устроено иначе. Вы один раз фиксируете, из чего состоит каждый релиз, и дальше платформа сама сопоставляет этот состав с обновляющимися данными об уязвимостях. По документации Dependency-Track, система принимает BOM в формате CycloneDX и анализирует компоненты при загрузке, а затем повторно проверяет их на известные уязвимости; описан регулярный ежедневный анализ (документация Dependency-Track: CI/CD). Для нас важна именно вторая часть: новое предупреждение может появиться у проекта, в который вы ничего не загружали уже несколько недель.
Можно провести бытовую аналогию. Сканер в сборке похож на проверку продуктов в момент покупки. Dependency-Track больше напоминает список того, что лежит в вашем холодильнике, с пометками от производителей об отзывах партий. Купили вы давно, а отзыв пришёл вчера, и важно, что вы знаете, в каком контейнере лежит нужная партия.
Что именно кладём в SBOM и почему это не «просто файл»
SBOM — это перечень компонентов приложения. Его ценность целиком зависит от одного условия: он должен описывать то, что реально развёрнуто, а не то, что разработчик имел в виду. Если состав собран по манифесту зависимостей на ноутбуке, а в образ попали другие версии из-за lock-файла, кэша слоёв или системных пакетов базового образа, вы будете наблюдать за воображаемым приложением. Предупреждения придут, но не про то, что работает.
Поэтому практическое правило: генерировать SBOM нужно из финального артефакта сборки, того самого, который уходит в реестр или на сервер, и привязывать результат к его идентификатору. Подробнее о том, как получить такой перечень и какие библиотеки на самом деле оказываются на сервере, мы разбирали в статье SBOM для приложения: какие библиотеки действительно попали на сервер. Здесь важно закрепить связку: один выпущенный артефакт — один SBOM — одна версия проекта в Dependency-Track.
Хорошая проверка на честность выглядит так: возьмите работающий контейнер или пакет на сервере, посмотрите его дайджест или хеш и убедитесь, что тот же идентификатор записан рядом с загруженным SBOM, например в описании версии проекта. Если идентификаторы не совпадают, значит, вы наблюдаете за чем-то другим.
Площадка для платформы: зачем отдельный сервер
Dependency-Track — серверное приложение с базой данных, веб-интерфейсом и фоновыми задачами анализа. Ему нужен постоянный хозяин, который будет включён и тогда, когда никто не нажимает кнопки: ведь смысл в том, чтобы повторные проверки шли без вашего участия. Рабочий ноутбук или временная виртуалка в CI для этого не подходят: данные об истории релизов должны жить долго.
VPS здесь подходит по простой причине: это постоянный сервер с root-доступом, на котором вы сами решаете, как развернуть контейнеры, где держать том с данными и кто имеет доступ по сети. У MAATRIX это NVMe-диски и процессоры AMD EPYC; дисковая подсистема нужна базе платформы, а процессорное время и память — фоновому анализу. Какой тариф выбрать, зависит от числа проектов и размера их составов, и честно это можно понять только на собственных данных. Для пробы на нескольких приложениях логично начать с небольшого варианта вроде Uab (2 vCPU, 4 ГБ RAM, 80 ГБ NVMe) или Heka (4 vCPU, 8 ГБ, 160 ГБ), а затем смотреть на фактическую загрузку. Это ориентир для старта, а не гарантия ёмкости: ресурсы тарифа не равны обещанию, что на них поместится конкретное число проектов.
Отдельно подумайте о сетевом доступе. Платформа хранит сведения о составе ваших приложений, а это ценная для атакующего карта: она показывает, какие уязвимые версии у вас есть. Не публикуйте интерфейс без необходимости, ограничьте доступ адресами команды или закройте его за обратным прокси с аутентификацией. Бэкапы тома с данными настраивайте сами: автоматические копии на тарифах мы здесь не предполагаем.
Как релиз попадает в проект
Модель данных в Dependency-Track простая. Есть проект, в нём версии, и у каждой версии свой набор компонентов. Загрузка BOM привязывается к проекту и требует API key (CI/CD в документации). То есть из пайплайна вы обращаетесь к платформе с ключом, который имеет право загружать данные в нужный проект и не более того. Отдельный ключ для каждого проекта или для группы пайплайнов удобнее, чем один общий: его легче отозвать, и по нему понятно, откуда пришла загрузка.
Теперь главное решение: как называть версии. Есть два соблазна.
- Загружать всё в одну «вечную» версию проекта, затирая состав при каждой сборке. Выглядит аккуратно, но вы теряете историю: невозможно ответить, был ли затронут релиз трёхнедельной давности.
- Создавать версию на каждую сборку, включая промежуточные. Тогда список зарастает сотнями записей, и важное тонет.
Разумный компромисс: версия проекта соответствует выпущенному релизу, то есть тому, что побывало в проде или у клиентов. Промежуточные сборки можно проверять сканером в пайплайне, а в платформу отправлять только то, что реально выпущено. Тогда список версий читается как перечень развёртываний.
Загрузка происходит из шага пайплайна, который выполняется после публикации артефакта и после генерации SBOM из него. В ходе этого шага передаются имя проекта, версия, файл BOM и ключ. Конкретный синтаксис запроса возьмите из документации той версии платформы, которую вы установили: нам здесь важна последовательность, а не конкретная строка.
- Собран и опубликован артефакт.
- Из этого артефакта сформирован SBOM в CycloneDX.
- SBOM загружен в проект с версией, совпадающей с номером релиза.
- Рядом записан идентификатор артефакта (дайджест, хеш), чтобы потом можно было сверить состав с реально работающим.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Приходит предупреждение. Что за ним стоит
Допустим, через месяц платформа сообщает: у библиотеки, входящей в релиз 2.4.0, найдена известная уязвимость. Первый вопрос не «как срочно чинить», а «о чём именно нас предупредили».
Запись в базе уязвимостей связывает идентификатор проблемы с диапазоном версий компонента. Платформа сопоставляет ваш компонент с этим диапазоном и показывает совпадение. Это значит: версия, которая у вас в составе, попадает под описание. Это не значит, что кто-то нашёл способ атаковать именно ваш сервис, и не значит, что уязвимый код вообще вызывается вашим приложением. Предупреждение — основание для разбора, а не вердикт об эксплуатации.
Разбор удобно вести по четырём вопросам.
Какие версии приложения затронуты
Здесь и окупается привычка хранить версию на каждый релиз. Открываем компонент и смотрим, в каких проектах и версиях он встречается. Может оказаться, что затронуты 2.4.0 и 2.3.1, а в 2.5.0 библиотека уже обновлена по другой причине. Тогда задача сводится к вопросу «какие из затронутых версий ещё работают?».
Где это реально развёрнуто
Состав в платформе — это бумага, а развёртывание — жизнь. Нужно сверить с реальностью: какие версии сейчас запущены на серверах, у какого заказчика, в каком окружении. Бывает, что релиз давно снят с продакшена, но остался в тестовом стенде или у клиента, который не обновляется. Бывает и обратное: в проде работает не та версия, которую вы считали. Для сверки как раз нужен идентификатор артефакта, записанный при загрузке: по нему видно, тот ли образ запущен.
Достижим ли уязвимый код
Это уже инженерная оценка, а не данные платформы. Библиотека может входить в состав транзитивно и использоваться лишь в тестовом режиме. Уязвимость может касаться функции, которую ваше приложение не вызывает, или сценария, требующего конфигурации, которой у вас нет. А может быть и наоборот: уязвимая функция стоит прямо на входе пользовательских данных. Отвечать на это должен тот, кто знает код. Платформа здесь помощник, но не судья.
Что даёт публичная информация о проблеме
В описании записи обычно сказано, при каких условиях проблема проявляется, есть ли исправленная версия и какова оценка серьёзности. Оценка серьёзности — общая, она не знает вашей архитектуры. Уязвимость с высоким баллом в сервисе, закрытом от внешней сети и не принимающем чужие данные, и та же уязвимость во внешнем API — разные риски, хотя цифра одна.
Совпадение в базе и решение о срочном выпуске
Теперь самое важное различие. Совпадение в базе уязвимостей — факт о составе. Решение выпускать исправление немедленно — управленческое, и оно складывается из нескольких обстоятельств. Полезно мысленно разложить предупреждения по такой таблице.
| Обстоятельство | Склоняет к срочному исправлению | Допускает плановое обновление |
|---|---|---|
| Доступность уязвимого места | Код обрабатывает внешние данные | Код вызывается только внутри доверенного контура |
| Условия проявления | Проявляется в конфигурации по умолчанию | Требуется нестандартная настройка, которой нет |
| Исправление | Есть простое обновление с совместимой версией | Нужна крупная миграция, обновление опасно |
| Развёртывание | Версия работает в проде | Версия давно снята или стоит на закрытом стенде |
Таблица — не алгоритм, а способ не принимать решение по одной цифре. Иногда предупреждение стоит пометить как неприменимое и записать причину: платформа позволяет фиксировать такие решения по компоненту, чтобы следующий человек не разбирал то же самое заново. Важна формулировка: «не затронуты, потому что уязвимая функция не вызывается, проверено таким-то способом», а не просто «ложное срабатывание». Первое можно перепроверить, второе — нет.
И обратная осторожность. Пустой список предупреждений не доказывает безопасность. Платформа сообщает только об известных проблемах из подключённых источников данных. Если уязвимость ещё не описана публично или компонент в BOM указан неточно и не сопоставился, вы не увидите ничего. Отсутствие предупреждений означает лишь, что известных совпадений для записанного состава сейчас нет.
Исправили. Как закрыть работу честно
Допустим, решение принято: обновляем библиотеку и выпускаем 2.4.1. Дальше часто совершают одну небрежность: считают задачу закрытой в момент слияния исправления. Но риск живёт не в ветке, а в развёрнутом артефакте. Закрывать работу нужно после трёх событий.
- Собран новый артефакт, из него сформирован свежий SBOM и загружен в проект как версия 2.4.1.
- Платформа показывает, что для 2.4.1 это предупреждение не применяется. Это проверка того, что обновление действительно изменило состав, а не осталось в манифесте, который в итоговый образ не попал.
- Новая версия реально развёрнута, а идентификатор запущенного артефакта совпадает с записанным.
Третий пункт чаще всего забывают. Исправленный релиз, лежащий в реестре, никого не защищает, пока его не запустили.
Старые версии, которые ещё работают
Выпуск 2.4.1 не отменяет существования 2.4.0. Если у кого-то она ещё работает, например в отдельном окружении или у заказчика на собственной инсталляции, предупреждение по ней остаётся актуальным. Поэтому в платформе стоит различать активные версии и те, что выведены из эксплуатации. Когда релиз действительно снят, его можно пометить неактивным, чтобы он не создавал шум в списке. Но делать это нужно по факту отключения, а не по ощущению, что «мы давно обновились».
На практике полезно вести простое соответствие: версия — где работает — кто отвечает за обновление — срок поддержки. Часть этого можно хранить в описании версии проекта, часть в вашей обычной системе учёта развёртываний. Платформа показывает, что в составе релиза не так, но не знает, у кого он запущен. Эту связь придётся поддерживать вам.
Если вы поддерживаете несколько линеек сразу, предупреждение превращается в небольшую задачу планирования: исправить ли только последнюю версию, выпустить патч для предыдущей линейки или объявить старую устаревшей. Здесь список затронутых версий из платформы становится основой разговора с командой и клиентами, потому что вместо расплывчатого «возможно, что-то уязвимо» вы называете конкретные номера.
Что настроить на сервере, чтобы всё это не развалилось
Когда процесс описан, остаются бытовые вещи, которые определяют, доживёт ли система до второго года работы.
- Хранение данных. Том базы должен лежать на постоянном диске, а не внутри временного контейнера. Резервную копию нужно настроить самостоятельно и проверить восстановление хотя бы один раз.
- Ключи. Каждому пайплайну свой API key, хранится в секретах CI, а не в репозитории. Отозвать ключ должно быть дёшево.
- Оповещения. Предупреждения полезны, только если кто-то их видит. Подключите уведомления в рабочий канал команды, но разбирайте их по смыслу, а не реагируйте на каждое одинаково: у слабой уязвимости в закрытом контуре и у серьёзной во внешнем API разный ритм реакции.
- Обновление самой платформы. Это тоже приложение со своими зависимостями. Следите за выпусками и обновляйте по плану, а версию и возможности сверяйте с документацией именно той редакции, которую поставили.
- Размер сервера. Наблюдайте за памятью и диском в первые недели. Если фоновый анализ начинает упираться в ресурсы, переход на следующий тариф MAATRIX, например с Uab на Heka или Maat, делается по факту нагрузки, а не по догадке.
Размещать платформу можно в любой доступной локации; выбирайте ближайшую к команде, которая будет заходить в интерфейс и отправлять загрузки из CI. Доступность конкретных образов и операционных систем лучше проверить в форме заказа: полагаться на то, что везде одинаково, не стоит.
Возвращаясь к релизу, который никто не трогал
Вернёмся к приложению месячной давности. Оно лежит на сервере, в Git тишина: ни коммитов, ни тегов, ни открытых запросов на слияние. Легко решить, что раз ничего не меняется, то и оценивать нечего. Но риск — это отношение между составом артефакта и тем, что мир знает о его компонентах. Состав остался прежним, а знание о нём выросло. Поэтому оценка риска у неподвижного релиза продолжает меняться, даже когда меняется не он, а справочник уязвимостей.
Задача Dependency-Track на вашем VPS — не пугать новыми записями и не изображать абсолютную защиту, а сохранить за каждым выпущенным релизом возможность задать простой вопрос: из чего он сделан и что про это известно сегодня. Ответ может быть «ничего, что требует действий», и это тоже полезный результат, пока вы помните, что он относится только к известным на сегодня проблемам.