Встроенный SAST в GitLab CI: Semgrep находит SQL-инъекции в легаси, о которых не подозревали
Включаем SAST в GitLab Ultimate 13.x прямо в merge request pipeline: встроенный статический анализ, severity threshold и реальные находки в легаси-коде без блокировки деплоя.
GitLab Ultimate 13.x интегрирует SAST/DAST/Dependency Scanning прямо в merge request pipeline без дополнительной настройки
В GitLab Ultimate 13.x появилась возможность запускать SAST, DAST и Dependency Scanning прямо в рамках merge request pipeline - без отдельных CI-систем и дополнительной настройки. Мы включили это на одном из клиентских проектов, где до этого статический анализ кода существовал только в разговорах о том, что "надо бы".
Что именно включилось
В GitLab 13.x для языков с поддержкой встроенного анализатора достаточно добавить в .gitlab-ci.yml:
include:
- template: Security/SAST.gitlab-ci.yml
Pipeline автоматически подбирает анализатор под язык проекта. Для Python - bandit, для PHP - phpcs-security-audit, для Java - spotbugs, и так далее. Результаты появляются прямо в интерфейсе merge request: вкладка «Security» с перечнем находок, severity, местом в коде и ссылкой на правило.
Каждый анализатор работает на основе паттернов AST, а не просто текстового поиска, что даёт меньше ложных срабатываний по сравнению с grep-подходами. Насколько меньше - зависит от кодовой базы, но на нашей выборке был виден разрыв.
Что нашли в легаси
Проект - PHP-приложение, писалось примерно с 2013 года, несколько рук сменилось. Никакого статического анализа никогда не было. Включили SAST - и в первом же прогоне получили несколько десятков находок.
Часть ожидаемая: var_dump в продакшн-коде, eval на входных данных без валидации, md5 для хешей паролей. На эти грехи мы и рассчитывали наткнуться.
Но были и находки, которые никто в команде не отмечал как проблему. Несколько мест с прямой конкатенацией переменной в SQL-запрос:
// Найдено SAST: SQL Injection (CWE-89)
$query = "SELECT * FROM users WHERE login = '" . $_GET['user'] . "'";
$result = mysqli_query($conn, $query);
Код работал. Никто не падал на него при code review, потому что вокруг было много похожего кода, и глаз замылился. SAST это не пропустил.
Всего по категории High было несколько подобных мест - не десятки, но достаточно, чтобы отнестись серьёзно. Это был не тестовый стенд, а работающий продукт. О том, что там такое есть, разработчики честно сказали: «да, наверное есть, но где - не знаем».
Проблема с порогами
Здесь начинается организационная часть, которая оказалась важнее технической.
Если включить SAST и поставить pipeline на блокировку при любой находке уровня Medium и выше - деплой останавливается сразу. В легаси-проекте с несколькими десятками находок это означает: либо команда неделю разбирает их все перед первым merge, либо SAST тихо отключают и больше к нему не возвращаются. Второе случается чаще.
Мы выбрали другой подход. В GitLab CI можно настроить SAST_EXCLUDED_PATHS и переменную fail_on_severity, которая управляет тем, при каком уровне pipeline падает:
variables:
SAST_EXCLUDED_PATHS: "tests/, vendor/"
# Блокируем только Critical, Warning и Info - не блокируют
SAST_FAIL_ON_UNKNOWN_SEVERITY: "false"
Реального fail_on_severity как переменной в этой версии шаблона нет - пришлось дорабатывать через allow_failure:
semgrep-sast:
allow_failure: true # не блокируем pipeline
artifacts:
reports:
sast: gl-sast-report.json
Результаты при этом всё равно попадают в MR и показываются в Security-вкладке. Разработчик видит находки, но merge не блокируется. Блокировать решили только Critical - это настраивается через политики в GitLab Ultimate.
Такой режим позволил не останавливать работу пока разбираются исторические долги, при этом новые Critical-находки сразу видны и должны быть закрыты до merge.
Как это встраивается в процесс
После первого прогона мы провели разбор с командой: прошлись по находкам, отсортировали по severity, выделили реальные проблемы от информационного шума. High и Critical оформили как задачи в трекере с указанием конкретного файла и строки - GitLab это удобно позволяет прямо из интерфейса.
Находки уровня Info (например, var_dump в не-продакшн ветке) - добавили в исключения через конфигурацию.
Через две недели разработчики сами стали смотреть на Security-вкладку перед merge - не потому что заставляем, а потому что находки появляются конкретные и понятные: файл, строка, правило, ссылка на CWE. Это не «у вас плохой код», это «вот эта строка, вот почему».
Что по Dependency Scanning и DAST
Dependency Scanning включили параллельно - шаблон аналогичный:
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
Нашёл несколько устаревших PHP-библиотек с известными CVE. Здесь история похожая: обновления давно были доступны, никто просто не смотрел.
DAST (динамический анализ) в рамках этого проекта не включали - он требует работающего staging-окружения в pipeline, что у клиента пока не настроено. Это отдельная задача.
Итог
Встроенный SAST в GitLab Ultimate снижает порог входа до одной строки в .gitlab-ci.yml. Реальная работа - настройка порогов и процесса разбора находок. Без этого либо pipeline постоянно падает, либо на находки никто не смотрит.
Для проектов с историческим кодом имеет смысл начинать с режима «показывать, не блокировать», разобрать накопленное, и только потом ужесточать пороги. Запускать это в рамках аудита безопасности - разумно: сразу понятно, что критично, что терпит, а что вообще ложное срабатывание.
SQL-инъекции в работающем продакшне, о которых команда «наверное догадывалась» - это не гипотетическая угроза. SAST нашёл их за один прогон.