SonarQube и OWASP Dependency-Check в GitLab CI: первый прогон нашёл 12 критических находок, о которых никто не знал
Внедрили SAST и сканирование зависимостей в GitLab CI: SonarQube + OWASP Dependency-Check выявили 12 критических уязвимостей в продовых библиотеках с первого запуска.
DevSecOps-практики набирают зрелость: интеграция статического анализа (SAST) и сканирования зависимостей в CI/CD становится стандартом для команд, которые серьёзно относятся к безопасности кода.
Разговор про безопасность кода обычно начинается не с желания команды, а с какого-нибудь неприятного события. В этом случае неприятным событием послужило требование клиента по managed-сопровождению: перед выходом нового продукта на прод нужно показать, что в коде нет очевидных дыр. Требование разумное, формулировка расплывчатая, инструментов в пайплайне - ноль.
Команда разработки у клиента небольшая, примерно десять человек, монорепозиторий на GitLab, деплой в Kubernetes. Код проходит ревью, тесты есть, но вопрос уязвимостей в зависимостях никто системно не отслеживал. Классическая ситуация.
Что выбрали и почему
Задача распадается на две независимые части: анализ кода (SAST) и анализ зависимостей (SCA - Software Composition Analysis). Это разные инструменты с разной логикой.
Для SAST выбрали SonarQube. Решение не новое, но надёжное: есть хорошая интеграция с GitLab через merge request decoration, поддерживает Java и Python, которые использует клиент, плагины для дополнительных правил подключаются без боли. Community Edition бесплатен и покрывает основные нужды.
Для сканирования зависимостей - OWASP Dependency-Check. Инструмент проверяет используемые библиотеки против базы CVE (NVD - National Vulnerability Database). На входе - список зависимостей или собранный артефакт, на выходе - отчёт с найденными уязвимостями, их CVSS-оценкой и ссылками на описание.
Альтернативы рассматривали: Snyk, WhiteSource. Оба хороши, но для клиента с бюджетными ограничениями и желанием не тащить данные об исходниках во внешние облака - OWASP Dependency-Check проще обосновать.
Как добавляли в пайплайн
GitLab CI с раннерами в Kubernetes - среда знакомая. Добавили два отдельных stage: sast и dependency-check. Запускаются параллельно после стандартного build, блокируют deploy если находки выше порога.
Для SonarQube развернули отдельный инстанс на выделенной VM. Первая попытка поднять в Kubernetes - отказались, SonarQube с PostgreSQL в качестве хранилища приятнее живёт на стейтфул ноде, чем в поде с persistent volume. Может, когда-нибудь вернёмся к K8s, но не сегодня.
Dependency-Check запускается прямо в CI-джобе как Docker-образ. База CVE скачивается и кешируется в GitLab CI cache - без этого каждый запуск тратил бы несколько минут только на загрузку базы.
Пример упрощённой конфигурации stage в .gitlab-ci.yml:
dependency-check:
stage: sast
image: owasp/dependency-check:latest
script:
- /usr/share/dependency-check/bin/dependency-check.sh
--project "$CI_PROJECT_NAME"
--scan ./
--format HTML
--format JSON
--failOnCVSS 7
--out reports/
artifacts:
paths:
- reports/
when: always
Ключевой параметр - --failOnCVSS 7. Джоб падает при нахождении уязвимости с CVSS 7.0 и выше (High). Критические (9+) блокируют пайплайн жёстко, средние (4-6.9) идут в отчёт как предупреждения, но не блокируют.
Первый прогон: 12 критических, никто не знал
Запустили на существующей кодовой базе. SonarQube нашёл несколько десятков предупреждений разной степени серьёзности - типичная картина для кода, который никогда не проходил статический анализ. Большинство - code smells и minor issues, несколько реальных potential bugs, два security hotspot-а требующих внимания.
Dependency-Check выдал 12 уязвимостей с CVSS выше 7.0 в используемых библиотеках. Из них четыре - с CVSS 9+ (Critical).
Реакция команды была предсказуемой: сначала недоверие («это же просто библиотека для парсинга XML, какие там уязвимости?»), потом тихое «ну ладно».
Список находок выглядел примерно так:
- Apache Commons Collections старой версии - известная цепочка десериализации, CVE с высоким рейтингом. Библиотека подтянулась транзитивно через три уровня зависимостей - никто её не добавлял осознанно.
- Jackson Databind - несколько CVE на polymorphic type handling. Версия устарела на полтора года.
- Spring Security - одна CVE на аутентификацию, версия не обновлялась с начала проекта.
- Ещё несколько библиотек с уязвимостями меньшего масштаба.
Половина находок - транзитивные зависимости. Команда явно не добавляла ни одну из них - они пришли как зависимости зависимостей. Это стандартная проблема для любого Java или Python проекта с приличным числом библиотек.
Что делали дальше
Исправление разбили на два потока.
Быстрые обновления - библиотеки, где можно поднять версию без breaking changes. Прошлись по каждой CVE, нашли версию с патчем, обновили, запустили тесты. Большинство обновлений прошли чисто, в двух случаях потребовалось небольшое рефакторирование API.
Сложные случаи - там, где библиотека транзитивная и прямого контроля над ней нет. Для таких случаев Maven и Gradle позволяют явно исключить уязвимую транзитивную зависимость и добавить нужную версию напрямую. Не элегантно, но работает.
После первой волны обновлений из 12 критических осталось три - в библиотеках, где апдейт требует более серьёзной проработки. Они завели задачи, установили дедлайн.
Что получилось в итоге
Пайплайн теперь выглядит так: каждый MR проходит через SonarQube и Dependency-Check автоматически. Если появляется новая зависимость с критической CVE - MR блокируется до разбора ситуации. SonarQube показывает delta - только новые проблемы в изменённом коде, не весь технический долг скопом.
Это убирает целый класс вопросов из code review: ревьюер не должен помнить, какие библиотеки уязвимы. Инструмент помнит.
Несколько практических наблюдений по итогам:
-
Ложные срабатывания есть. Dependency-Check иногда находит CVE в библиотеке, которая в конкретном проекте используется так, что уязвимый код вообще не вызывается. Нужен механизм suppression - явного помечания таких находок как неактуальных с обоснованием. У OWASP Dependency-Check для этого есть suppression XML-файл, его кладём рядом с конфигом пайплайна.
-
Время выполнения. Dependency-Check на среднем Java-проекте с локальным кешем базы CVE - около трёх минут. SonarQube - зависит от размера кода, у клиента около четырёх минут. Параллельный запуск держит общий оверхед в пределах пяти минут на пайплайн - терпимо.
-
Обновление базы CVE. Если запускать обновление в каждом джобе - медленно. Если кешировать слишком долго - пропускаешь свежие CVE. Остановились на ежедневном обновлении через scheduled pipeline, в обычных MR-пайплайнах используется вчерашний кеш.
Двенадцать критических находок при первом запуске - это не катастрофа и не повод паниковать. Это нормальный результат для кодовой базы, которая до этого никогда не сканировалась. Главное, что теперь эти находки видны и управляемы.