
За шесть предыдущих выпусков мы собрали собственный конвейер безопасной разработки: развернули виртуальные машины, подняли инфраструктуру из GitLab, Vault, Nexus, DefectDojo и Dependency-Track, написали CI/CD-пайплайн, подключили сканеры безопасности и настроили резервное копирование.
Остается главный вопрос: сможет ли наш конвейер находить реальные уязвимости, а не просто радовать разработчиков зелеными галочками в интерфейсе GitLab?
Как говорили старые DevSecOps-бояре, «в нашем деле на слово не верят — безопасность нужно проверять».
Поэтому сегодня устроим нашему конвейеру проверку боем. Возьмем уязвимое приложение Reactvulna, загрузим его в GitLab и прогоним через собранный нами пайплайн. После этого разберем результаты сканирования и посмотрим, насколько хорошо собранная нами инфраструктура справляется с обнаружением проблем безопасности.
Дисклеймер
Важно сразу условиться, Путник!
Данный цикл статей не заменяет требования ГОСТ, OWASP SAMM, BSIMM и других серьезных методологий безопасной разработки. Это, скорее, практический пример того, как можно организовать конвейер безопасной разработки ПО с минимальным вложением средств.
Всё описанное в цикле — исключительно мой личный опыт, набитые шишки и результаты долгих вечеров за клавиатурой. Это не официальная позиция моего текущего или бывших работодателей.
Воспринимать статью как руководство к бездумному копированию тоже не стоит. Проверяй всё на тестовых стендах, делай бэкапы и подходи к экспериментам с холодной головой.
Ошибки возможны, и не нужно бояться их совершать. Главное — делать выводы и постепенно строить процессы, которые будут надежнее вчерашних.
На чем будем проверять конвейер
Для проверки конвейера будем использовать Reactvulna — заведомо уязвимое React-приложение для тестирования инструментов безопасности.
На первый взгляд, это обычный веб-сервис для покупки билетов в кино: здесь есть авторизация пользователей, каталог фильмов, чат и другие привычные функции. Однако под капотом разработчики намеренно оставили целый букет типовых уязвимостей. Среди них — XSS, CSRF, тестовые секреты в исходном коде и другие распространенные проблемы безопасности. Рассмотрим несколько примеров.
1. XSS (межсайтовый скриптинг)
В приложении присутствует несколько XSS-уязвимостей. Одна из них находится в функции websocketMessageArrived. Имя пользователя (chatMessage.author.name) и текст сообщения (chatMessage.message) напрямую вставляются в innerHTML элемента <p>.
// Файл: src/Users.js websocketMessageArrived(incoming) { var chatMessage = JSON.parse(incoming.body); console.log('Received: ', chatMessage); if (!this.state.showChatWindow || this.state.currentuserToChatWith.id !== chatMessage.author.id) { this.chatWith(chatMessage.author); } else { const p = document.createElement('p'); p.setAttribute('class', 'chatPanelParagraph'); p.innerHTML = '<span><i>' + chatMessage.author.name + '</i></span><span>: </span><span>' + chatMessage.message + '</span>' document.getElementById('chatPanel').appendChild(p); this.scrollChatWindowDown(); } }
2. CSRF (межсайтовая подделка запроса)
В функции sendChatToBackend PUT-запросы выполняются без использования CSRF-токенов. Если пользователь уже авторизован в приложении, злоумышленник может попытаться заставить его браузер выполнить такой запрос от его имени.
// Файл: src/Users.js (фрагмент отправки сообщения) sendChatToBackend(text, addressee) { // Уязвимость: запрос на отправку сообщения без CSRF-токена fetch(Constants.backendRESTUrlBase + 'messages/chat', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json', 'Cache': 'no-cache' }, body: JSON.stringify({text: text, toUser: addressee}) }); }
3. Хардкод секретов
В исходники Reactvulna я положил файл config.js, где лежат заботливо оставленные мной API-ключи к вымышленному сервису аналитики. Проверим, найдет ли их Gitleaks.

4. Небезопасные заголовки
В конфигурации приложения отсутствует Content Security Policy (CSP). Это одна из тех находок, ради которых мы и подключали Nuclei в наш конвейер.

5. Уязвимые зависимости
В package.json присутствуют устаревшие версии библиотек с известными уязвимостями. Такие проблемы должны обнаруживаться инструментами анализа зависимостей и генерации SBOM.
6. Небезопасное хранение токенов
После успешной авторизации JWT-токен сохраняется в localStorage, а не в httpOnly cookie. В сочетании с XSS это позволяет получить доступ к токену пользователя.
// Файл: src/Login.js postLogin(event) { // ... (формирование запроса) fetch(Constants.backendUrlBase + 'login', {/* ... */}) .then(function(response) { return response.json(); }) .then(function(loginResponse) { console.log("login succesfull"); // Уязвимость: токен сохраняется в localStorage window.localStorage.setItem('loggedIn', true); window.localStorage.setItem('loggedInUser', JSON.stringify(loginResponse)); // ... }) }
Если наш конвейер безопасной разработки найдет хотя бы часть этих проблем — уже хорошо. Если что-то пропустит, будем крутить настройки, менять правила для Opengrep и добавлять кастомные проверки в Nuclei. Главное — мы получим честную картину того, что конвейер видит, а что остается в слепой зоне.
Это бесценно для стартапа, который не хочет выйти в прод с граблями под ногами. Лучше один раз упасть на учебном полигоне, чем в боевом. Reactvulna — песочница, манекен для битья. После каждого крупного обновления инструментов полезно заново обкатывать на нем конвейер и смотреть, изменилось ли качество обнаружения проблем. Если прогресс есть — отлично. Если нет — значит, еще есть что докрутить.
Начинаем тестирование
Cначала клонируем репозиторий Reactvulna и загрузим его в GitLab. Затем подготовим необходимые интеграции: создадим продукт в DefectDojo и проект в Dependency-Track, а также добавим в GitLab переменные DEPENDENCY_TRACK_PROJECT_DOCKER_UUID, DEPENDENCY_TRACK_PROJECT_UUID и DOJO_PRODUCT_ID.


Теперь можно запускать конвейер и проверять, как он покажет себя на реальном приложении. Gitleaks прочешет секреты, Opengrep поищет уязвимости в коде, Trivy сгенерирует SBOM, а Nuclei проведет динамическое сканирование запущенного приложения. Полученные отчеты автоматически отправятся в DefectDojo и Dependency-Track.

После завершения конвейера зайдем в DefectDojo и посмотрим, какие находки попали в итоговые отчеты. Затем сравним результаты со списком известных уязвимостей Reactvulna, опубликованным авторами проекта. Попробуем отследить и пропущенные уязвимости, и ложные срабатывания.
Ожидаем, что конвейер безопасной разработки обнаружит от 70 до 90% заложенных уязвимостей. Инструменты не всесильны, поэтому стопроцентного результата мы не ждем. Но секреты в коде Gitleaks должен найти без труда, Trivy обязательно обратит внимание на устаревшие зависимости, а Nuclei не пройдет мимо проблем с безопасностью в заголовках.
Результаты теста
Для начала проверим, загрузился ли собранный артефакт в Nexus. Всё нормально: артефакт на месте и тег BUGS присутствует.

Проверяем Dependency-Track. Импорт тоже выполнен успешно.

Проверяем результаты анализа зависимостей в Dependency-Track. Как мы и прогнозировали, инструмент обнаружил большое количество уязвимостей.

Дополнительно можно посмотреть граф зависимостей приложения.

Не забыли и про контейнер. Для Docker-образа Dependency-Track тоже успешно построил граф зависимостей и выполнил анализ уязвимостей.

Нашлись и какие-то уязвимости в curl.

Теперь проверим, что за дефекты нашлись на этапах secrets-scan, sast, iac-scan, dast. Переходим в DefectDojo и смотрим, появился ли новый Engagement для текущего запуска конвейера. Спойлер: всё прекрасно отработало.

В общей сложности сканеры насобирали 97 находок. Часть из них наверняка окажется ложноположительными срабатываниями, поэтому теперь самое время сравнить результаты со списком известных уязвимостей Reactvulna и посмотреть, насколько точно отработал наш конвейер.

Нужно учитывать, что Opengrep умеет находить не только ошибки в коде, но и некоторые проблемы IaC и секреты. Из-за этого часть дефектов может встречаться несколько раз, но с разными источниками обнаружения.
Чтобы проверить отчеты от сканеров, фильтровать список будем по полю Test Type значениями из Found By. Нам интересны значения Semgrep JSON Report, Gitleaks Scan, Nuclei Scan и Checkov Scan.
Начнем с Semgrep JSON Report. Вот и найденная XSS:

Теперь найдем тестовый секрет:

Выявили отсутствие в конфигурации content-security-policy:

Также был обнаружен дефект Docker-файла:

В целом мне не удалось обнаружить в списке дефектов только CSRF, но возможно я плоховато искал. Можно считать тест успешным!
Важно не только то, какие уязвимости были обнаружены, но и как отработал сам конвейер. Все этапы завершились успешно: сборка приложения, генерация SBOM, поиск секретов, статический анализ, проверка инфраструктурного кода, динамическое сканирование, а также публикация результатов в DefectDojo, Dependency-Track и Nexus.
Отдельно стоит обратить внимание на скорость выполнения. С полным набором проверок конвейер завершил работу за приемлемое время. Для разработчиков это важно не меньше, чем качество обнаружения уязвимостей: если проверки слишком медленные, команда быстро начинает воспринимать их как помеху.
На рисунке ниже показаны джобы выполненного пайплайна и время работы каждого этапа.

Подведем итог. Конвейер успешно прошел проверку на уязвимом приложении. Большинство ожидаемых проблем было обнаружено автоматически, а интеграции с внешними сервисами отработали без сбоев.
Заключение
Вернемся к нашему царству стартапошному.
Помнишь, с чего всё начиналось? Молодой CEO царь, горящие дедлайны, хакеры-злодеи на пороге, базы данных утекают, пользователи разбегаются… А теперь давай посмотрим, что мы с боярами-разработчиками успели построить.
За семь статей мы шаг за шагом построили собственный конвейер безопасной разработки, используя бесплатные инструменты и обычное железо. У нас не было золотых подписок, облачных кластеров за миллион монеток и армии DevSecOps-инженеров. Мы пришли с бесплатным VirtualBox, пятью виртуалками на домашнем ПК и открытыми инструментами.
Чего мы добились в нашем царстве? Бояре-разработчики:
Научились грамоте безопасной.
Перестали хранить пароли и секреты в репозитории — Gitleaks ловит их на лету.
Перестали писать дырявый код — Opengrep подсвечивает дефекты безопасности и опасные функции.
Перестали собирать контейнеры от root — Checkov ругается на Dockerfile.
Перестали выкатывать в прод без проверок — Nuclei сканирует живое приложение.
Перестали терять зависимости — SBOM и Dependency-Track следят за каждым пакетом.
Перестали гадать, где чей образ, — Nexus хранит всё с понятными тегами (и даже
:bugs, если что-то сломано).Перестали теряться в отчетах — DefectDojo собирает всё в одном месте и дедуплицирует.
Перестали бояться сбоев — бэкапы делаются каждую ночь, даже если ПК уснул.
Работы у разработчиков, конечно, прибавилось. Но не настолько, чтобы поднимать бунт. Договорились исправлять реальные уязвимости в первую очередь, а остальное оставлять на плановый рефакторинг.
А что же царь?
Царь даже наградил бояр — не золотом, а новыми серверами и разрешением наконец-то перестать экономить каждый мегабайт. Но старую лабораторию оставили — для памяти и для новых экспериментов.
В царстве остался целый сундук знаний:
Как установить GitLab, Nexus, Vault, DefectDojo, Dependency-Track и CLI-сканеры.
Как делать резервное копирование и не плакать, если диск умрет.
Как проверить всю систему на уязвимом приложении и убедиться, что она работает.
Главный урок всего цикла получился довольно простым. Безопасность не появляется сама по себе. Ее приходится строить постепенно, как избу — бревно за бревном. Но если делать это последовательно, то даже из самых скромных материалов можно собрать добротный терем.
Сказка – ложь, да в ней намек…
…а этот цикл статей — вовсе не сказка, а быль. Я сам прошел этот путь, набил шишки и постарался подытожить свой опыт в этом цикле. Теперь ты сможешь повторить тот же путь быстрее и увереннее, не наступая на мои грабли. А если найдешь новые — обязательно расскажи о них.
А пока ступай, царевич-стартапер. Строй свои конвейеры, защищай код, развивай свое царство и внедряй безопасную разработку там, где вчера на нее не хватало ни времени, ни ресурсов. Пусть твои пайплайны будут зелеными, секреты — надежно спрятанными, а отчеты о сканировании — скучными и пустыми :)
Если этот цикл оказался полезным — делись статьями с коллегами, ставь плюсы и пиши комментарии на Хабре.
Что дальше?
За время работы над этим циклом я убедился в одной вещи: даже такой, казалось бы, законченный конвейер всегда можно сделать лучше.
Несмотря на финал этой серии, тем для продолжения еще хватает. Можно настроить оповещения в мессенджер при появлении новых уязвимостей, встроить в процесс разработки внутреннее хранилище зависимостей, подключить новые инструменты анализа и разобрать множество других практических сценариев.
Конец цикла или начало нового? Решать именно тебе!

PURP — Telegram-канал, где кибербезопасность раскрывается с обеих сторон баррикад
t.me/purp_sec — инсайды и инсайты из мира этичного хакинга и бизнес-ориентированной защиты от специалистов Бастиона
kenomimi
Почему все его так активно используют? Он жутко тормозной, ресурсоемкий, фильтра сделаны ужасно, и вообще UI/UX от инопланетян...