Привет! На связи Олег Казаков из Spectr.
В предыдущей части статьи я говорил о таком этапе DevSecOps как Commit-time Checks. Он отвечает за контроль корректности и безопасности кода в GIT-репозитории. А в этой части материала я расскажу, что же такое Post-build Checks, и как на этом этапе используется такой класс инструментов, как Container Cheсks.
Post-build Checks
Цель: контроль безопасности артефактов сборки. Обычно — это Docker-образы, а для их сканирования используется Container Scanning. Схема этого процесса представлена ниже.
Первый этап — сборка, далее происходит анализ образа, проверка синтаксиса, проверка зависимостей и проверка ОС.
Известные инструменты Container Scanning:
— Trivy;
— Grype;
— Clair;
— Harbor;
— GitLab Container Scanning (который использует Trivy и Grype).
Реализация в GitLab
Попробуем это все сделать в GitLab. В отличие от предыдущих этапов, тут нам нужно сделать небольшую подготовку. Для того, чтобы были артефакты сборки, нужно сделать этап сборки. Для этого:
— возьмем любой образ из Docker Hub, например (здесь прописано, как его использовать внутри самого Docker-файла — как написано, так и делаем);
— далее добавляем стадию сборки и задачу. В GitLab есть собственное хранилище для Docker-образов, поэтому тут всё стандартно: авторизуемся в Docker-хранилище GitLab по доступам в стандартных переменных, делаем Build из Docker-файла, пушим.
Особое внимание я здесь обращаю на IMAGE_NAME — здесь указан тот формат, по которому Container Scanning будет искать образ.
stages:
- build
- test
build:
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
stage: build
image: docker:20.10.16
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
services:
- docker:20.10.16-dind
tags:
- docker
script:
- docker build -f examples/container_scanning/Dockerfile -t $IMAGE_NAME .
- docker push $IMAGE_NAME
Далее добавляем сканирование. Включаем шаблон и дорабатываем задачу.
include:
- template: Security/Container-Scanning.gitlab-ci.yml
container_scanning:
stage: test
tags:
- docker
rules:
- if: $CI_COMMIT_BRANCH
script:
- sudo apt-get update
- sudo apt-get -y install jq
- gtcs scan
- exit $(cat gl-container-scanning-report.json | jq --raw-output '.vulnerabilities | length')
В отличие от предыдущих решений, тут другой скрипт для запуска, т.к. используется иная команда и slim вместо alpine:
А также есть вывод внутри самого job, то есть нам не нужно самим делать вывод. На скрине ниже представлен вывод сканирования одного из популярных Docker-образов — было найдено 160 уязвимостей. Это довольно свежая версия, и на PHP 7.4 в районе 300 уязвимостей:
Уязвимостей оказалось довольно много — что же с ними делать?
Работа с уязвимостями
У Trivy есть возможность указывать режим вывода ошибок — начиная с какого уровня будут регистрироваться уязвимости.
Например, не будем обрабатывать Unknown, Low, Medium, а начнем с High и Critical.
container_scanning:
stage: test
variables:
CS_SEVERITY_THRESHOLD: "High"
tags:
- docker
rules:
- if: $CI_COMMIT_BRANCH
script:
- sudo apt-get update
- sudo apt-get -y install jq
- gtcs scan
- exit $(cat gl-container-scanning-report.json | jq --raw-output '.vulnerabilities | length')
Стало 65 уязвимостей, что уже лучше. Но это все равно очень много, учитывая что взят стандартный и популярный образ.
Можно увидеть, что довольно много уязвимостей в curl. Давайте будем считать, что мы их исправили. Вернее, именно исправить нельзя, можно либо использовать другой образ, либо посчитать, что уязвимость для нас не критична.
Представим, что мы посчитали, что уязвимость для нас не критична, например, мы не используем curl в работе. В этом случае нам на помощь приходит вот такой файлик — Vulnerability allowisting.
При этом в первом пункте указано, что git_strategy должен быть установлен в fetch (по умолчанию для данного шага git_strategy=none, то есть выкачивание кода не происходит совсем, т.к. анализируется образ). Так и делаем, перечисляем уязвимости, устанавливаем git_strategy:
generalallowlist:
CVE-2023-23914: curl
CVE-2023-27536: curl
CVE-2023-42916: curl
CVE-2023-43551: curl
CVE-2023-27533: curl
CVE-2023-27534: curl
CVE-2023-27535: curl
container_scanning:
stage: test
variables:
CS_SEVERITY_THRESHOLD: "High"
GIT_STRATEGY: "fetch"
tags:
- docker
rules:
- if: $CI_COMMIT_BRANCH
script:
- sudo apt-get update
- sudo apt-get -y install jq
- gtcs scan
- exit $(cat gl-container-scanning-report.json | jq --raw-output '.vulnerabilities | length')
Теперь все эти уязвимости помечаются как подтвержденные и пропадают из итогового артефакта, теперь их количество — 51.
Но все равно уязвимостей здесь очень много. Мы решили проверить, есть ли какой-то образ в Docker Hub без уязвимостей, но пока такие, увы, не нашли.
Ключевые проблемы инструмента
Есть множество уязвимостей на уровне операционной системы, на которые мы никак не можем повлиять. Максимум, что мы можем — это сделать свой образ, и в нем перебирать ОС в поисках самой безопасной. При этом мы существенно можем влиять на содержимое только самого Docker-файл. А что если мы не хотим заморачиваться с поиском и исправлением уязвимостей ОС?
Реализация на IaC от GitLab
Мы можем попробовать использовать IaC (Infrastructure as Code) scanning от Gitlab. Детальнее на этом инструменте мы остановимся в следующих частях, сейчас нам важно знать, что он позволяет сканировать Dockerfile.
Включить его можно точно так же, как и SAST-инструменты из предыдущей части, только шаблон будет другой:
include:
- template: Security/SAST-IaC.latest.gitlab-ci.yml
kics-iac-sast:
variables:
FILE_REPORT: gl-sast-report.json
NAME_OF_CI_SCRIPT: "sast_iac"
tags:
- docker
allow_failure: false
script:
- !reference [.analyzer_run, script]
Добавляем скрипт для обработки json-артефакта.
#!/bin/bash
vulnerability_count=$(cat $FILE_REPORT | jq --raw-output '.vulnerabilities | length')
if [ ${vulnerability_count} -gt 0 ]; then
echo "| severity | name | location |"
echo "|------------------|--------------|------------------|"
_jq() {
echo ${row} | base64 --decode | jq -r ${1}
}
for row in $(cat $FILE_REPORT | jq -r '.vulnerabilities[] | @base64'); do
echo '|' $(_jq ".severity") '|' $(_jq ".identifiers[0].name") '|' $(_jq ".location.file")':'$(_jq ".location.start_line") '|'
done
fi
exit $vulnerability_count
Пушим и смотрим результат.
Тут есть критическая уязвимость Missing User Instruction — это означает, что образ будет запускаться под пользователем root, а это небезопасно.
Также есть уязвимость с низким уровнем опасности — Healthcheck Instruction Missing Это означает, что отсутствует инструкция HEALTHCHECK. Данная инструкция нужна, чтобы Docker мог протестировать состояние контейнера. Добавлять ее не обязательно, но чтобы сканер не ругался, мы можем все-таки добавить, либо добавить эту уязвимость в исключения. Пока что для упрощения добавим инструкцию, а о том, как исключать, расскажем в следующих частях статьи.
Ниже представлен результат после исправления. Как видим, теперь проблем в Dockerfile нет.
Итак, мы рассмотрели 2 способа сканирования образов: использование инструментов Container Scanning (позволяет сканировать ОС на наличие уязвимостей) и использование IaC Scanning (предоставляет упрощенный вариант, который сканирует только Dockerfile).
Что из этого использовать — каждый выберет для себя сам. Я лишь скажу, о том, что если вы выбираете IaC Scanning, то лучше в любом случае минимизировать риски для безопасности и, по возможности, использовать минимальный базовый образ без лишних зависимостей.
Итоги
Итак, мы разобрали третью стадию в проверке безопасности, в следующей части мы перейдем к этапу тестирования функционала.