Безопасность в пайплайне: SAST, DAST, dependency scanning
Встраиваем сканеры безопасности и понимаем, где какой применять.
SAST (Static Application Security Testing) — статический анализ исходного кода на уязвимости без запуска приложения.
Безопасность как часть конвейера
Идея «shift left» — двигать проверки безопасности влево, ближе к разработке, чтобы ловить уязвимости при написании кода, а не на проде. GitLab встраивает несколько сканеров прямо в пайплайн, подключаемых шаблонами, и показывает находки в Merge Request — рядом с кодом, который их вызвал.
За лозунгом «shift left» стоит вполне экономическая логика. Уязвимость, найденная в момент написания кода, исправляется правкой в той же ветке за минуты. Та же уязвимость, найденная в проде, означает инцидент: разбор, экстренный релиз, возможно утечку данных и репутационный удар — цена отличается на порядки. Чем правее в жизненном цикле всплывает проблема, тем дороже её устранение, поэтому здравая стратегия — встретить её как можно раньше, прямо в пайплайне на каждый merge request, когда контекст ещё свежий и автор помнит, что писал. Безопасность перестаёт быть отдельным этапом «перед релизом, если успеем» и становится фоновой проверкой, такой же рутинной, как прогон тестов.
Важно сразу задать правильную планку ожиданий. Автоматические сканеры — это не «теперь мы в безопасности», а первый, дешёвый эшелон обороны. Они отлично ловят известные паттерны: знакомые CVE в зависимостях, типовые небезопасные конструкции, забытые в коде секреты. Но логические дыры в бизнес-правилах, ошибки в управлении доступом, проблемы архитектуры они не видят — это работа ручного аудита и моделирования угроз. Правильная ментальная модель: сканеры снимают с людей рутину и ловят «глупые» ошибки массово, освобождая внимание экспертов для действительно сложного. Они дополняют ревью безопасности, а не заменяют его.
Виды сканеров
| Сканер | Что проверяет |
| SAST | исходный код на типовые уязвимости (инъекции, небезопасные функции) |
| Dependency Scanning | зависимости на известные уязвимости (CVE) в библиотеках |
| Container Scanning | собранный Docker-образ на уязвимости в системных пакетах |
| DAST | работающее приложение «снаружи», как делал бы атакующий |
| Secret Detection | случайно закоммиченные секреты (токены, ключи) |
SAST и Dependency Scanning — статические и быстрые, их включают всегда. DAST динамический: ему нужно поднятое приложение (часто — review app), поэтому он медленнее и запускается реже.
Статика против динамики: они дополняют друг друга
Полезно понять, почему сканеров так много и зачем нужны и статические, и динамические. Статические (SAST, Dependency, Secret Detection) смотрят на код и артефакты, не запуская приложение: они видят строку, где конкатенируется SQL-запрос, или версию библиотеки с известной CVE. Они быстры и дёшевы, но не знают, достижим ли уязвимый код в реальном запуске. Динамический DAST смотрит с другой стороны — на работающее приложение снаружи, как настоящий атакующий: шлёт запросы, пробует инъекции, ищет открытые эндпоинты. Он ловит то, что видно только в рантайме (например, неправильную конфигурацию заголовков или поведение реального стека), но требует поднятого приложения и работает медленнее. Один тип сканеров не отменяет другой: статика говорит «вот подозрительное место в коде», динамика — «вот что реально пробивается снаружи».
Container Scanning стоит особняком: он проверяет не ваш код и не ваши npm/pip-зависимости, а системные пакеты внутри собранного Docker-образа — устаревший openssl или дырявую libc базового образа. Поэтому его логично ставить после стадии сборки образа, тогда как SAST и Dependency можно гнать ещё на стадии тестов, до сборки. Расстановка по пайплайну выглядит примерно так:
test : SAST, Dependency Scanning, Secret Detection (быстро, на каждый MR)
build : сборка образа
-> Container Scanning (по готовому образу)
deploy : review app -> DAST (по работающему приложению)Подключение
Сканеры подключаются официальными шаблонами одной строкой:
include:
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml
stages:
- test
- build
- deployШаблоны добавляют джобы, которые сами определяют язык проекта и запускают подходящие анализаторы. Никакого кода писать не нужно — только подключить.
Заметьте, что подключение сканеров — это в точности include: template из прошлого урока: безопасность переиспользует тот же механизм DRY-пайплайнов. Внутри шаблона спрятана джоба, которая сначала анализирует репозиторий (какие файлы, какие манифесты зависимостей лежат рядом — package.json, requirements.txt, go.mod), а затем подбирает и запускает нужный анализатор под конкретный язык. Поэтому одна строка include «волшебным образом» работает и для Python-, и для Node-проекта — детект встроен в шаблон. Это же объясняет, почему шаблоны лучше не копировать к себе целиком, а именно подключать: GitLab обновляет анализаторы и правила, и через include вы получаете свежие версии автоматически.
Отчёты в Merge Request
Каждый сканер выдаёт отчёт в стандартном формате (reports: sast, reports: dependency_scanning и т.д.). GitLab сравнивает находки ветки с целевой и показывает в виджете MR именно новые уязвимости, внесённые этим MR. Это позволяет настроить политику: не сливать MR, добавляющий критическую уязвимость.
Деталь «показываем только новые уязвимости» важнее, чем кажется, и именно она делает сканеры пригодными для жизни. У любого зрелого проекта в момент внедрения сканеров уже есть длинный хвост существующих находок — вывалить все разом значило бы утопить разработчика в сотнях предупреждений, которые он начнёт игнорировать (и тогда сканер бесполезен). Diff-подход решает это: GitLab сравнивает находки вашей ветки с целевой и в виджете MR подсвечивает ровно то, что добавил этот MR. Разработчик видит маленький, релевантный список «вот что сломал твой код» — и чинит его, пока в контексте. Старый долг разгребается отдельно и постепенно, не блокируя текущую работу. На этом и строят политики: «merge нельзя, если он вносит новую критическую уязвимость» — точечное, исполнимое правило, а не недостижимое «ноль уязвимостей в проекте».
Как работает под капотом
Джоба сканера — обычная джоба, выполняющая анализатор в контейнере и формирующая JSON-отчёт с уязвимостями (файл, строка, серьёзность, описание, ссылка на CVE). Сервер GitLab разбирает отчёт, дедуплицирует находки, сравнивает с базовой веткой и наполняет вкладку Security и виджет MR. На платных тарифах доступна Security Dashboard с агрегированной картиной по всем проектам.
Под капотом весь фокус держится на одном механизме — artifacts: reports. Сканер не общается с GitLab напрямую: он просто кладёт JSON-файл стандартного формата как артефакт особого типа, а сервер уже знает, как его прочитать. Это тот же контракт, что у JUnit-отчётов о тестах: джоба производит файл, платформа его интерпретирует. Красота в том, что благодаря этому контракту вы можете подключить и сторонний сканер: если он умеет выдавать отчёт в формате GitLab security report, его находки попадут в тот же виджет MR и ту же вкладку Security рядом со встроенными. Платформа не привязана к конкретному анализатору — она привязана к формату отчёта.
Сравнение с GitHub Actions проясняет философию. В GitHub роль «вкладки Security» играет GitHub Advanced Security: CodeQL для SAST, Dependabot для зависимостей, secret scanning, а общий формат отчётов — SARIF (аналог GitLab security report). Концепции совпадают почти один в один: статический анализ кода, проверка зависимостей по базе CVE, детект секретов, сведение находок в Security-вкладку и diff в pull request. Отличается упаковка: GitLab даёт сканеры готовыми include-шаблонами «из коробки» в едином продукте, GitHub — отдельными продуктами/actions (CodeQL Action, Dependabot), частично завязанными на тариф Advanced Security. И там, и там итог один: безопасность как встроенная, автоматическая часть каждого MR/PR, а не разовый аудит «когда-нибудь потом».
Частые ошибки
- Считать SAST заменой ревью безопасности — это лишь автоматический первый эшелон, ложные срабатывания и пропуски возможны.
- Запускать DAST без поднятого приложения — ему нечего сканировать.
- Игнорировать Secret Detection и продолжать коммитить токены вместо использования CI/CD-переменных.
- Вывалить на команду все исторические находки разом вместо diff-подхода «только новое» — предупреждения начнут игнорировать.
- Гнать тяжёлый DAST на каждый коммит вместо запуска по расписанию или на ключевых ветках — пайплайн становится невыносимо медленным.
- Найдя секрет через Secret Detection, просто удалить его из последнего коммита — он остаётся в истории git, секрет нужно ротировать.
Итоги
- GitLab встраивает SAST, DAST, dependency/container scanning и secret detection как подключаемые шаблоны.
- «Shift left»: ловим уязвимости на каждом MR, дёшево, а не на проде дорого.
- SAST и dependency — быстрые статические; DAST динамический и требует работающего приложения; они дополняют друг друга.
- Сканеры — первый эшелон, не замена ручному аудиту безопасности.
- Всё держится на
artifacts: reports; виджет MR показывает только новые уязвимости, позволяя строить исполнимую политику.