Безопасность в пайплайне: 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 показывает только новые уязвимости, позволяя строить исполнимую политику.
Проверьте себя
1. Чем DAST отличается от SAST?
ADAST анализирует исходный код, SAST — работающее приложение
BSAST статически анализирует исходный код, DAST динамически проверяет работающее приложение снаружи
CОни идентичны
DDAST проверяет только зависимости
2. Что показывает виджет безопасности в Merge Request?
AВсе уязвимости в интернете
BНовые уязвимости, внесённые именно этим MR по сравнению с целевой веткой
CСписок раннеров
DВремя сборки