Привет! На связи Олег Казаков из 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 есть платные и бесплатные инструменты, они перечислены ниже: 

Платные

Бесплатные

LGTM 

Checkmarx SAST
Contrast 
Coverity Scan
SonarQube
Semgrep
PVS-Studio
Reshift
Mend SAST
HCL AppScan Source

Open Source, которые в основном направлены на конкретные языки:
Bandit
Brakeman
Phpcs-security-audit
Gosec
Security Code Scan
Flawfinder
ESLint security plugin


Free Community Edition

Contrast Scan
SonarQube
Semgrep

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, и его можно использовать с минимальными доработками. Для этого:

  1. Добавляем новый шаблон.

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 завершается. Нам же нужно обработать файл с уязвимостями, поэтому подавляем эту ошибку.

  1. Включаем данный шаблон в .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.

Комментарии (1)


  1. MrB4el
    27.05.2024 07:37
    +1

    Вот прям отдельное спасибо за опенсорс решения в каждой вашей статье!