Привет! На связи Олег Казаков из Spectr.
Мы продолжаем публикацию цикла статей, где делимся опытом и наработками и рассказываем, из чего состоит DevSecOps и как его внедрить в процесс разработки.
В предыдущей части статьи я рассказал о том, что представляет собой процесс DevSecOps в целом, из каких этапов он состоит, и подробно остановился на первом этапе — Pre-commit Checks. Сегодня пришло время для обзора стадии Commit-time Checks и ее инструментов. Поговорим о каждом инструменте отдельно и расскажем, на чем мы все-таки остановили свой выбор.
Commit-time Checks
Суть этапа: проверить код на предмет корректности и безопасности в GIT-репозитории.
Рассмотрим известные классы инструментов.
SAST
SAST (static application security testing) — это процесс тестирования приложения на наличие ошибок и уязвимостей в исходном коде.
По этой ссылке доступен список различных видов SAST. Как видите, их довольно много, но практически у каждого вида есть сложности в использовании. Чаще всего это ложное срабатывание.
Этапы работы SAST-инструментов
Конечно, есть нюансы и различия, но в общем случае этапы следующие:
Построение модели (Modeled Code). На этом этапе инструмент SAST использует исходный код и преобразует его в формат, полезный для выполнения анализа. Одни инструменты компилируют код, другие используют абстрактное синтаксическое дерево для построения модели, третьи преобразовывают их в произвольный формат по своему выбору. Наиболее популярный формат — абстрактное синтаксическое дерево. Большинство инструментов SAST поддерживают несколько языков программирования, и этот шаг необходим для того, чтобы преобразовать код на любом языке в единый формат.
Поиск дефектов (List of Defects). На этом этапе инструменты SAST применяют различные правила к смоделированному коду. Эти правила могут быть определены поставщиком инструмента или написаны пользователем инструмента. Происходит семантический, структурный и прочие анализы, и на выходе мы получаем список дефектов
Виды SAST-инструментов
Среди возможных видов SAST есть платные и бесплатные инструменты, они перечислены ниже:
Платные | Бесплатные |
Checkmarx SAST | Open Source, которые в основном направлены на конкретные языки:
|
SAST в GitLab
Посмотрим, что нам предлагает GitLab. У GitLab есть SAST во всех версиях.
Глядя на эту таблицу, мы видим, какие языки поддерживаются и какие инструменты используются, и видим, что их довольно много.
GitLab предлагает возможность использования версий разных Open Source или Free Community Edition. Их можно включить простым кодом, представленным ниже:
include:
- template: Security/SAST.gitlab-ci.yml
sast:
tags:
- docker
То есть мы просто включаем SAST, добавляем файлики, например PHP-файл и Go-файл, таким образом добавляются стадии проверки при помощи phpcs-security-audit, Semgrep и Gosec (последние 2 на Go):
Ниже представлен результат сканирования phpcs-security-audit:
Можем посмотреть исходники — что здесь есть?
Ниже приведен пример того, как подключенный шаблон этой задачи (Security/Secret-SAST.gitlab-ci.yml) выглядит в исходниках:
sast:
stage: test
artifacts:
reports:
sast: gl-sast-report.json
rules:
- when: never
variables:
SEARCH_MAX_DEPTH: 4
script:job
- echo "$CI_JOB_NAME is used for configuration only, and its script should not be executed"
- exit 1
.sast-analyzer:
extends: sast
allow_failure: true
# `rules` must be overridden explicitly by each child job
# see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
script:
- /analyzer run
В самом начале все по аналогии с тем, что мы видели в Secret Detection в рамках предыдущей статьи: есть сама задача SAST, генерация отчета (только уже с другим именем — gl-sast-report.json). Но отличия все же есть, т. к. GitLab под разные ЯП предлагает различные инструменты SAST, то ест для каждого из этих инструментов есть свое описание.
semgrep-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE_TAG: 4
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:
- if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
when: never
- if: $SAST_EXCLUDED_ANALYZERS =~ /semgrep/
when: never
- if: $CI_COMMIT_BRANCH
exists:
- '**/*.py'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.c'
- '**/*.go'
- '**/*.java'
- '**/*.cs'
- '**/*.html'
- '**/*.scala'
- '**/*.sc'
Выше приведен пример описания задачи для semgrep. Нас тут интересует раздел rules, а если конкретнее, то:
блок exists, в котором идет перечисление масок, то есть для каких файлов применяется инструмент;
проверка переменной $SAST_EXCLUDED_ANALYZERS на вхождение строки с именем инструмента. Таким образом, мы можем выключать определенные инструменты, если они нам не нужны, — это нам пригодится чуть позже.
Проверим, как работают инструменты, добавим различные файлы с уязвимостями, например, воспользовавшись данной коллекцией.
Видим аналогичную картину, как и в Secret Detection в предыдущей статье. Задачи отработали успешно, но в отчете есть уязвимости.
Опять же, стоит заметить, что в бесплатной версии очень мало функционала.
Поэтому мы пойдем по тому же пути (что и в части 1) и немного допишем скрипт, кот
#!/bin/bash
vulnerability_count=$(cat $FILE_REPORT | jq --raw-output '.vulnerabilities | length')
if [ ${vulnerability_count} -gt 0 ]; then
echo "| severity | name | location | scanner |"
echo "|------------------|--------------|------------------|-----------------|"
_jq() {
echo ${row} | base64 --decode | jq -r ${1}
}
for row in $(cat $FILE_REPORT | jq -r '.vulnerabilities[] | @base64'); do
vulnerability_name=$(_jq ".name")
if [ "$vulnerability_name" == "null" ]; then vulnerability_name=$(_jq ".message"); fi
echo '|' $(_jq ".severity") '|' $vulnerability_name '|' $(_jq ".location.file")':'$(_jq ".location.start_line") '|' $(_jq ".scanner.name") '|'
done
fi
exit $vulnerability_count
Дорабатываем задачу SAST, выносим в отдельный файл для удобства (можно в любой момент включать/выключать задачу).
В самом .gitlab-ci.yml подключаем этот отдельный файл с задачей.
stages:
- test
.analyzer_run:
script:
- apk add jq bash coreutils
- /analyzer run
- bash .gitlab/scripts/$NAME_OF_CI_SCRIPT.sh
include:
- local: '/.gitlab/templates/sast.gitlab-ci.yml'
Те же задачи, в которых найдены уязвимости, теперь падают:
Вывод уязвимостей в задаче Bandit (только Python):
Вывод уязвимостей в задаче Flawfinder (только C/C++):
Вывод уязвимостей в задаче Gosec (только Go):
Вывод уязвимостей в задаче Semgrep (поддерживает много языков, в т. ч. Python, Go, C/C++):
Выглядит интересно, SAST в Gitlab включается и настраивается легко. Есть много инструментов, которые могут дополнять друг друга.
Сложности при использовании SAST в Gitlab
Зоопарк технологий. Под каждый ЯП — свой набор инструментов, каждый из которых может работать по-своему. Например, phpcs-security-audit выполняется не под рутом, а это значит, что мы не можем установить свои либы (jq, bash и т. д.).
Ложные срабатывания — одна из главных проблем SAST-инструментов. А тут еще много инструментов, что кратно увеличивает вероятные проблемы.
Как решить эти проблемы
Один из вариантов облегчения — исключить все лишние инструменты. Например, мы видим, что Semgrep покрывает достаточно много ЯП и в целом очень активно развивается. В этом нам поможет переменная SAST_EXCLUDED_ANALYZERS, которая позволяет исключать анализаторы.
sast:
variables:
SAST_EXCLUDED_ANALYZERS: "bandit,gosec,flawfinder" # можно исключать различные инструменты
FILE_REPORT: gl-sast-report.json
NAME_OF_CI_SCRIPT: "sast"
tags:
- docker
Тут мы исключаем Bandit, GoSec и Flawfinder, и выполняется только Semgrep. Таким образом, мы можем оставить какой-то один комплексный инструмент, который будет закрывать много языков, например тот же Semgrep.
В целом и сам Gitlab потихоньку двигается в сторону уменьшения инструментов, объявляя, что перестает поддерживать некоторые из них (например, Bandit, ESLint, GoSec).
Подключение инструментов DevSecOps напрямую
До сих пор мы использовали только встроенные в GitLab-инструменты. В этом есть плюс — настройка разных шагов в CI/CD получается довольно похожей.
Но есть и минусы:
GitLab может ограничивать возможности инструмента;
GitLab может не так активно актуализировать версии инструментов.
Поэтому иногда имеет смысл подключить инструмент самостоятельно. Покажу на примере все того же Semgrep.
У Semgrep есть официальный образ в Docker Hub, и его можно использовать с минимальными доработками. Для этого:
Добавляем новый шаблон.
semgrep:
stage: test
image: semgrep/semgrep
variables:
FILE_REPORT: gl-sast-report.json
SEMGREP_RULES: >-
p/security-audit
p/secrets
p/python
p/django
p/phpcs-security-audit
tags:
- docker
script:
- semgrep ci --gitlab-sast > $FILE_REPORT || true
- apk add jq bash coreutils
- bash .gitlab/scripts/sast.sh
artifacts:
reports:
sast: $FILE_REPORT
SEMGREP_RULES — это список правил, на соответствие которым проверяется весь исходный код. Для поиска правил можно использовать сервис: https://semgrep.dev/r
--gitlab-sast — это специальный флаг, который формирует вывод в таком же формате, как в GitLab.
|| true — по умолчанию при наличии ошибок данная утилита возвращает ошибку и от этого job завершается. Нам же нужно обработать файл с уязвимостями, поэтому подавляем эту ошибку.
Включаем данный шаблон в .gtilab-ci.yml:
include:
- local: '/.gitlab/templates/sast-semgrep.gitlab-ci.yml'
Остановились на SonarQube
Если говорить о нашей компании, то мы в итоге остановились на другом инструменте — SonarQube. Этот инструмент уже не является частью GitLab ни в каком виде, но умеет интегрироваться с ним.
SonarQube нам понравился своим удобным интерфейсом: это и удобная визуализация, и возможность быстрой реакции на найденные уязвимости (можно указать, что это корректное поведение, а можно — что это ложное срабатывание).
По данной ссылке содержится информация о том, как установить SonarQube и интегрировать его с GitLab.
Dependency Scanning
Другой класс инструментов Commit-time Checks — Dependency Scanning (это процесс автоматического обнаружения уязвимостей в зависимостях).
Как работают Dependency Scanning-инструменты
Во многих языках программирования есть пакетные менеджеры, при помощи которых мы можем выкачивать код, и информация об этом сохраняется в различных файлах (go.sum, composer.lock, package-lock.json, yarn.lock, Gemfile.lock, requirements.txt и т. д.). Dependency Scanning-инструменты сканируют эти файлы, смотрят, какие пакеты были выкачаны и какие были версии, далее пакеты проверяют в базе скомпрометированного ПО и, если их там находят, выдают ошибку.
Виды Dependency Scanning-инструментов
Dependency Scanning в GitLab
В GitLab есть инструмент Dependency Scanning, но, к сожалению, в бесплатной версии GitLab этот инструмент уже недоступен ни в каком виде.
Если просто включим шаблон, то ничего не произойдет, задача не будет даже стартовать.
В GitLab используется некий инструмент Gemnasium. Gemnasium — это собственное решение GitLab, при этом оно открытое и его можно использовать. Вернее, с одной стороны, мы его не можем использовать, потому что он в Ultimate-версии, но можем использовать его образ, чтобы использовать в бесплатной версии. Ниже приведен пример того, как это выглядит в исходниках:
Попробуем применить. Для этого создаем собственную задачу, чтобы не было конфликта с задачей GitLab, указываем найденный образ в image, а далее все применяется как и везде.
dependency_scanning_custom:
stage: test
variables:
FILE_REPORT: gl-dependency-scanning-report.json
NAME_OF_CI_SCRIPT: "dependency_scanning"
image: registry.gitlab.com/gitlab-org/security-products/analyzers/gemnasium:3
tags:
- docker
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
script:
- !reference [.analyzer_run, script]
Создаем скрипт обработки артефакта
#!/bin/bash
vulnerability_count=$(cat $FILE_REPORT | jq --raw-output '.vulnerabilities | length')
if [ ${vulnerability_count} -gt 0 ]; then
echo "| severity | name | file | package |"
echo "|------------------|--------------|--------------|-----------------|"
_jq() {
echo ${row} | base64 --decode | jq -r ${1}
}
for row in $(cat $FILE_REPORT | jq -r '.vulnerabilities[] | @base64'); do
echo '|' $(_jq ".severity") '|' $(_jq ".name") '|' $(_jq ".location.file") '|' $(_jq ".location.dependency.package.name") '|'
done
fi
exit $vulnerability_count
Закидываем какие-то пакеты, например, в composer и go.
Видим отчет Dependency Scanning: обнаружены 2 пакета, уязвимостей нет.
У Gemnasium есть отдельный сайт с поиском по БД-уязвимостей. Можно найти какой-то скомпрометированный пакет, выбрать его и включить в пакетный менеджер.
Ниже приведен пример уязвимости:
Добавляем несколько пакетов с уязвимостями — уязвимости в задаче найдены, выводится информация об этом в задаче:
По итогу единственное, что пришлось сделать нестандартно, — найти образ в открытом доступе и указать явно образ, который должен использоваться для сканирования. В остальном все как и в предыдущих этапах.
Итоги
Итак, мы разобрали еще один этап в процессе DevSecOps — Commit-time Checks. Все наработки по коду лежат в этом репозитории.
Следующая часть статьи будет посвящена стадии Post-build Checks.