Композиционный анализ при помощи CodeScoring
Cтатья будет полезна разработчикам и инженерам по ИБ, желающим повысить уровень безопасности приложений за счет внедрения проверок, запрещающих вносить в ПО сторонние компоненты с известными уязвимостями.
Автор:
DevSecOps, в информационной безопасности с 2013 года, в СПАО »Ингосстрах» встраиваю безопасность в процесс разработки
При написании этой статьи я вновь пришел к мысли, что каждый ИТ-шник и ИБ-шник должен быть хоть чуточку разработчиком для более эффективного решения своих задач.
Проблематика
Разработчики программного обеспечения часто применяют уязвимые зависимости, особенно на первых этапах разработки нового проекта:
Скрытый текст
Пример сработки композиционного анализа в PR
Когда возможна угроза использования известных уязвимостей в зависимых компонентах, то это приводит к тому, что ПО становится уязвимым.
Помощник Тим и уязвимости
Чтобы привлечь внимание разработчиков к данной проблеме, мы используем нашего корпоративного кота Тим. Познакомиться с ним и его друзьями можно в нашем блоге: В самое сердечко: ребрендинг ИТ «Ингосстраха».
Кот Тим
Тим способен испытывать разные эмоции, например:
[string] $ingoCatImage = "" # Определяем эмоцию кота Фича:
if($failed_vulns.Count -gt 30){ # Выявлено более 30 CVE
$ingoCatImage = "![Sadness IngoCat](<ссылка на картинку> ""Sadness IngoCat"")"
}
elseif($failed_vulns.Count -gt 20)
{
$ingoCatImage = "![Evil IngoCat](<ссылка на картинку> ""Evil IngoCat"")"
}elseif($failed_vulns.Count -gt 10)
{
$ingoCatImage = "![Annoyance IngoCat](<ссылка на картинку> ""Annoyance IngoCat"")"
}elseif($failed_vulns.Count -gt 0)
{
$ingoCatImage = "![Surprised IngoCat](<ссылка на картинку> ""Surprised IngoCat"")"
}
Как уже вы догадались, эмоции Тима варьируются от количества выявленных уязвимостей. Когда много уязвимостей где-то страдает один котик Тим ☹ .
Тим устал напоминать об уязвимостях
Композиционный анализ
Композиционный анализ представляет собой процесс построения дерева зависимостей программного обеспечения и идентификацию известных уязвимостей на основе этого дерева.
Рассмотрим пример известной (опубликованной) уязвимости в пакете org.yaml:snakeyaml@1.33
, используемой в качестве зависимости.
CVE-2022–1471
Оригинальное описание уязвимости:
SnakeYaml’s Constructor () class does not restrict types which can be instantiated during deserialization. Deserializing yaml content provided by an attacker can lead to remote code execution. We recommend using SnakeYaml’s SafeConsturctor when parsing untrusted content to restrict deserialization. We recommend upgrading to version 2.0 and beyond.
Проще говоря, если злоумышленнику удастся воспользоваться данной уязвимостью — будет плохо, а именно злоумышленник может выполнить нужный ему код на сервере от имени конечного ПО в котором используется org.yaml:snakeyaml@1.33
.
Часто бывает так, что разработчики, особенно из подрядных организаций, предоставляя нам готовое ПО или дорабатывая имеющееся ПО, применяют не самые «безопасные» версии заимствованных компонентов.
Консольный агент CodeScoring
Существует широкий спектр решений для выполнения композиционного анализа. В рамках стратегии импортозамещения можно рассмотреть интеграцию CodeScoring в процесс проверки каждого pull request в защищённых ветвях кода.
CodeScoring предлагает различные методы анализа репозиториев. В данной статье мы сосредоточимся исключительно на функционале, который может быть представлен консольным агентом Johnny в максимально доступной и понятной форме — это информация об уязвимостях:
Пример некоторых возможностей CodeScoring
Согласно документации, консольный агент Johnny
обладает широкими возможностями, но мы сосредоточимся на одной из функций — создание дерева зависимостей и формирование SBOM
.
SBOM
Software Bill of Materials — это список:
зависимостей с подробной информацией:
Пример информации о зависимости ch.qos.logback/logback-classic@1.2.11
лицензий под которой опубликована зависимость и ее компоненты:
Пример лицензий ch.qos.logback/logback-classic@1.2.11
имеющихся известных уязвимостях в зависимостях:
Пример уязвимостей ch.qos.logback/logback-classic@1.2.11
Проще говоря, SBOM
можно сравнить с этикеткой на упаковке товара в магазине, но с более обширной информацией о компонентах и даже о «плохих».
Я встречал SBOM
объемом до десятков мегабайт, только представьте, сколько строк может содержаться в таком файле…
Применение SBOM от CodeScoring в pull request
Методика обработки SBOM
Для того, чтобы обработать SBOM
от CodeScoring при проверке каждого pull request нам потребуется:
Запустить консольный плагин
Johnny
, передав ему папку с исходным кодом в качестве аргумента;Johnny
найдет среди поддерживаемых манифестов информацию, которая нас интересует, и отправит дерево зависимостей и lock-файлы на сервер CodeScoring;CodeScoring осуществит композиционный анализ полученного дерева и вернет файл
SBOM
;Изучить файл
bom.json
, полученный отJohnny
;Прикрепить
bom.json
в качестве артефакта нашей валидационной сборки;Принять решение о блокировке pull request на основе информации из
bom.json
, предварительно исключив ранее внесенные уязвимости в исключения:Заблокировать pull request;
Пропустить pull request, сохранив уязвимость.
Примерно так выглядел бы максимально упрощенный BPNM-процесс композиционного анализа:
Упрощённый процесс SCA и его место среди других проверок
В общем и целом, компонентная схема работы с CodeScoring представлена ниже:
Компонентная схема CodeScoring
Далее каждый элемент схемы и его действия будут раскрыты подробнее.
Как мы обрабатываем уязвимости в pull request
Об этом я уже писал в другой статье нашего блога: Внедряем Gitleaks для анализа pull request на наличие секретов в Azure DevOps Server. Если кратко, то наличие хотя бы одной уязвимости, выявленной в процессе анализа pull request, приводит к блокировке завершения pull request. Разработчик не сможет добавить уязвимый код, пока AppSec-инженер не внесет уязвимость в исключения или пока разработчик не обновит зависимость до версии, свободной от данной проблемы. Таким образом, мы гарантируем наименьшее количество известных уязвимостей в нашем ПО.
Кроме того, в упомянутой статье можно ознакомиться с тем, как мы осуществляем техническую реализацию указанного подхода к обработке pull request.
Вызываем Johnny
Для того, чтобы вызвать Johnny
, необходимо в конвейере запустить скрипт, который уже запустит Johnny
. В этом процессе валидационная сборка, инициируемая при создании pull request, вызывает нужный скрипт:
- task: Bash@3
displayName: "Композиционный анализ"
env:
# Токен, выданный сервером, CodeScoring и передаваемый Johnny
token: $(token)
# Уточняем какую версию Johnny использовать:
johnny: johnny-linux-amd64-$(token)
inputs:
targetType: filePath
filePath: $(System.DefaultWorkingDirectory)/папка/CodeScoring.sh
arguments: $(token)
continueOnError: false
Для вызова Johnny
в CodeScoring.sh
используется довольно простая конструкция:
ignore="--ignore .APK --ignore .IPA" # Список файлов, не сканируемых CodeScoring
command="$johnnyFilePath/$johnny scan dir
"$SYSTEM_DEFAULTWORKINGDIRECTORY/$BUILD_REPOSITORY_NAME/"
--api_token $token
--api_url "https://site.domain"
--project $SYSTEM_TEAMPROJECT/$BUILD_REPOSITORY_NAME
--timeout 1200
--stage dev
$ignore
$command # Запускаем SCA при помощи CodeScoring johnny
# Далее идет проверка того, что SCA выполнился успешно, пропустим неинтересную часть
Johnny
выполняет заданную команду и сохраняет файл bom.json
в ту же папку, откуда был запущен. Вне зависимости от результата валидационной сборки, файл будет выглядеть следующим образом:
Пример прикрепления SBOM в артефакты сборки
Как упоминалось ранее, в рамках данной статьи нас интересуют только уязвимости. Проверка рискованных лицензий или других вопросов не рассматривается. Следовательно, нас интересует раздел vulnerabilities
файла SBOM
:
Vulnerabilities из bom.json
Обрабатываем vulnerabilities из SBOM
Для того, чтобы получить список уязвимостей из SBOM
, нам необходимо пробежаться по дереву и выбрать всю полезную информацию по каждой уязвимости.
Чтобы инициировать данную активность, запустим скрипт:
- task: PowerShell@2
env:
TFS_TOKEN: $(token)
displayName: "Принятие решения"
inputs:
targetType: filePath
filePath: $(System.DefaultWorkingDirectory)/папка/CodeScoring-failer.ps1
arguments: >
-reportPath $(System.DefaultWorkingDirectory) -Token $env:TFS_TOKEN
# Где -reportPath - папка в которой искать bom.json и куда сохранять
# какие-нибудь файлы
continueOnError: false
enabled: true
Собственно, именно часть DevOps в DevSecOps на этом заканчивается и начинается разработка.
CodeScoring-failer.ps1
начинает свою работу с того, что начинает обход секции vulnerabilities
в SBOM
:
if (!(Test-Path $SBOM_file_path -PathType leaf)){ # Проверка существования SBOM
Write-Host "##[error]Файл отсутствует"
# Фейлим сборку если файл не создался:
Write-Host "##vso[task.complete result=SucceededWithIssues;]"
}
else
{
# Подгружаем список исключений,
# о которых ранее договорились с разработчиками репозитория
# Пропустим данный код
$SBOM_file = Get-Content $SBOM_file_path -Encoding UTF8
$CodeScoring_Report_Object = $SBOM_file | ConvertFrom-Json
$vulnerabilities = $CodeScoring_Report_Object.vulnerabilities
if($vulnerabilities.Count -gt 0)
{
Write-Host "##[command]Выполняю анализ уязвимостей" -ForegroundColor Blue
$failed_vulns = [System.Collections.Generic.List[PSCustomObject]]::new()
$isNotParsedVulns = $false # Есть ли уязвимости, для которых нет обработчика
foreach($vuln in $vulnerabilities)
{
if($null -ne $vuln.recommendation -and $vuln.ratings.score -gt 5)
{ # Вычисляем ссылки на базы уязвимостей
$references_text = ""
$references_Array = [System.Collections.Generic.List[PSCustomObject]]::new()
if($null -ne $($vuln.references)){
foreach($ref in $($vuln.references)){
$references_text = $references_text + "$($ref.source.name) : [$($ref.id)]($($ref.source.url)) "
$references_Array.Add($($ref.id))
}
}
$isExludedIssue = $false # По умолчанию все уязвимости не исключены
# Проверяем уязвимость на возможность исключения
# Пропустим данный блок. По ходу проверки на исключения, значение
# переменной $isExludedIssue может измениться на True
if($isExludedIssue -eq $true){ # Если уязвимость исключена,сохраним ее
$excludedCount = $excludedCount + 1
}else{ # Заполняем карточку уязвимости, уязвимость не исключена:
# Парсим PURL уязвимости
# Пропустим данный блок
foreach($rating in $($vuln.ratings)) # Выводим только полезную инфу
{ # об уязвимости
# Раскрашиваем цвет риска CVSSv3
$severity = $rating.severity.ToUpper()
$score = $rating.score
if($severity -eq "critical"){
$severity = "${severity}"
$score = "${score}"
}
elseif($severity -eq "high"){
$severity = "${severity}"
$score = "${score}"
}
elseif($severity -eq "medium"){
$severity = "${severity}"
$score = "${score}"
}
elseif($severity -eq "low"){
$severity = "${severity}"
$score = "${score}"
}
elseif($severity -eq "none"){
$severity = "${severity}"
$score = "${score}"
}
# Заполняем уровень риска CVSSv3
if($rating.severity -and $rating.score -and $rating.method)
{
if($rating.severity -and $rating.severity -notlike "none" -and $rating.score -and $rating.method)
{ # Есть и severity и score
$ratingString = $ratingString + " " + $rating.method + ": " + $severity + " (" + $score + ")"
}
elseif($rating.severity -and $rating.severity -notlike "none" -and ($null -eq $rating.score -or $rating.score -eq "") -and $rating.method)
{ # Нет score, но есть severity
$ratingString = $ratingString + " " + $rating.method + ": " + $severity
}
elseif($rating.score -and ($rating.severity -like "none" -or $null -eq $rating.severity -or $rating.severity -eq "") -and $rating.method)
{ # Нет severity, но есть score
$ratingString = $ratingString + " " + $rating.method + ": " + $score
}
}
}
}
# Подчищаем описание уязвимости, чтобы marckdows в PR не ломался
$vulnDescription = $($vuln.description).Replace("", "'")
$vulnDescription = $vulnDescription.Replace("<", "'")
$vulnDescription = $vulnDescription.Replace(">", "'")
$failed_check_object = [PSCustomObject]@{
id = $($vuln.id)
ratingString = $ratingString
description = $($vuln.description) -replace "<", "'"
references_text = $references_text
affects_text = $affects_text
artiURL = $artiURL
recommendation = $($vuln.recommendation)
artiURL_fixed = $artiURL_fixed
pkgName = $pkgName
pkgVersion = $pkgVersion
}
$failed_vulns.Add($failed_check_object)
}
}
}
# В переменную ниже будем записывать красивый текст с уязвимостями
$vulns_text = [System.Collections.Generic.List[string]]::new()
}
Получив сведения о выявленной уязвимости, мы формируем текст комментария, который наш бот разместит в pull request по факту обнаружения этой проблемы:
Скрытый текст
foreach($vuln in $failed_vulns) # Формируем красивый markdown текст для PR
{
$vulns_text.Add("> **Идентификатор уязвимости**: $($vuln.id)")
$vulns_text.Add("Критичность: $($vuln.ratingString)")
$vulns_text.Add("Описание уязвимости: $($vuln.description)")
if($null -ne $($vuln.references_text))
{
$vulns_text.Add("Полезные ссылки: $($vuln.references_text)")
}
else{ Write-Host "Полезные ссылки: отсутствуют" }
if($null -ne $($vuln.affects_text)) {
$vulns_text.Add("Затрагиваемые пакеты: $($vuln.affects_text)")
}
else{
Write-Host "Затрагиваемые пакеты: отсутствуют"
}
$vulns_text.Add("Рекомендация: повысить до [$($vuln.pkgName)@$($vuln.recommendation)]($($vuln.artiURL_fixed)) (если пакета нет, см. [FAQ](https://))")
$vulns_text.Add("----------------------------------------------------
")
}
# Очищаем итоговый текст от различных вариаций переноса строк,
# которые имеются в описании уязвимостей.
# Пропустим данный блок
Подготавливаем ссылку на артефакт валидационной сборки с bom.json
:
$reportName = "bom.json"
$buildDefinitionName = $($env:BUILD_DEFINITIONNAME)
$buildResultPath = "${instance}${project}/_build/results?buildId=${buildID}&view=artifacts&pathAsName=false&type=publishedArtifacts"
$buildURI = "${instance}${project}/_build/results?buildId=${buildID}"
Прикреплятьbom.json
к артефактам сборки будем так:
# Шаг запуска CodeScoring-failer.ps1
# Публикация отчета CodeScoring в любом случае
- task: PublishBuildArtifacts@1
displayName: "Публикация SBOM"
inputs:
pathToPublish: $(System.DefaultWorkingDirectory)/bom.json
artifactName: CodeScoring
continueOnError: false
enabled: true
Оставляем информацию для разработчика по работе композиционного анализа в журнале валидационной сборки:
Скрытый текст
Write-Host "##[error] Выявленные известные уязвимости в зависимостях: ($($failed_vulns.Count) шт.):" # Выгружаем в лог ссылку на файл отчета с уязвимостями
Write-Host "##[group]Развернуть <-- уязвимости в зависимостях"
Write-Host $vulns_string # Выгружаем в лог список уязвимостей в зависимостях
Write-Host "##[endgroup]"
# Подсвечиваем разработчикам что делать, если выявлена уязвимость:
Write-Host "##[error] Обнаружены зависимости с известными уязвимостями, валидационный билд будет помечен как неуспешный."
Write-Host "##[error] Композиционный анализ завершен, если конвейер упал, значит возможен один из вариантов для текущего запроса на вытягивание:"
Write-Host "##[error] - обнаружена реальная уязвимость в зависимости;"
Write-Host "##[error] - уязвимость не актуальна;"
Write-Host "##[error] - сработка является ошибочной и требует внесения в исключения."
Write-Host "##[error] В любом случае см. инструкцию: https:// чтобы понять как действовать дальше."
Завершаем валидационную сборку с ошибкой, если выявлены уязвимости. Это не позволит разработчику внести известную уязвимость в код:
Write-Host "##[command]Согласно конфигу ${configFile} разрешено фейлить все валидационные сборки при известных уязвимостей"
Write-Host "##vso[task.logissue type=error;]Обнаружены известные уязвимости (${failed_vulns} шт.)" # Фейлим сборку с красивой ошибкой
Write-Host "##vso[task.complete result=SucceededWithIssues;]"
На данном этапе в журнале валидационной сборки разработчик может увидеть следующую информацию:
Пример журнала валидационной сборки
Оставляем комментарий в pull request
Подготовка комментария
Подготавливаем содержимое комментария:
Скрытый текст
[string] $footer = "
Полезная информация:
> **Риск согласно OWASP**: [A6 Vulnerable and Outdated Components](https://)
**Что делать дальше?**: См. инструкцию [CodeScoring FAQ](https://)
**Перечень найденных уязвимостей**: См. в файл [${reportName}](${buildResultPath}), либо журнал сборки [${buildDefinitionName}](${buildURI})
Список известных уязвимостей в зависимостях ($($failed_vulns.Count) шт.):
${vulns_string}
${ingoCatImage}
"
Публикуем комментарий в pull request:
[string] $PR_message = ":warning:Выявлены уязвимые компоненты:warning:"
$PR_message = $PR_message + $footer
$responseCommen = $pr.postNewThread($PR_message, $true, "Выявлены уязвимые компоненты") # Вторая передаваемая переменная boolean отвечает за необходимость удаления предыдущих комментов
if ($responseCommen.StatusCode -eq 200){
Write-Host "##[debug]Комментарий к PR успешно оставлен."
}
Write-Host "##vso[task.setvariable variable=BUILD_FAILED;]" # Фейлим сборку
Пример комментария
В случае, если была выявлена хотя бы одна уязвимость, в своем pull request разработчик увидит следующий комментарий:
Пример комментария и эмоция Тима
При этом разработчик не сможет завершить pull request, как мы указывали ранее, о чем свидетельствует:
Статус блокировки pull request
Технически же блокировка pull request достигается обязательностью нашей валидационной сборки:
Параметры валидационной сборки защищаемой ветви кода
Полезная информация об уязвимости
Если развернуть markdown текст, разработчик сможет ознакомиться со списком выявленных уязвимостей:
Пример известной уязвимости из базы CVE
Видно, что комментарий включает в себя:
Всю необходимую информацию об уязвимости;
Ссылку на Wiki, где изложены действия разработчика в случае обнаружения такого комментария, а также раздел FAQ.
Ссылки на файл с уязвимостью нет ни в SBOM ни в журнале работы Johhny
. Не каждому разработчику удается найти ту самую верхнеуровневую зависимость, в которую встроена уязвимая транзитивная зависимость.
Как мы помогаем в поиске той самой верхнеуровневой зависимости
Не каждому разработчику удается найти ту самую верхнеуровневую зависимость, в которую встроена уязвимая транзитивная зависимость. Вот тут и приходит на помощь CodeScoring. В отличие от JFrog Xray, этот инструмент способен отображать верхнеуровневую зависимость в 90% случаев.
Когда разработчик сдался:
Пример запроса источника уязвимости
Пример поиска уязвимой зависимости по дереву зависимостей средствами CodeScoring в нашем искусственном проекте:
Простота поиска источника уязвимых транзитивных зависимостей
Если у разработчика отсутствуют необходимые ресурсы для создания дерева зависимостей или полученное дерево не дает ясности относительно того, какая из зависимостей первого уровня является основной, то решение CodeScoring эффективно решает эту проблему:
Ссылка на зависимость уровнем выше (уровень 3)
Ссылка на файл с верхнеуровневой зависимостью в UI (уровень 0)
Базовый функционал Johnny по блокировке pull request
На самом деле, можно было бы просто блокировать pull request средствами самого Johnny
и вообще ничего не программировать. В журнале валидационной сборки выглядело бы это так:
Пример с сайта CodeScoring
Я абсолютно убежден, что вывод данной информации неудобен тем, что:
Разработчик вынужден тратить время на переход к журналу сборки, что может вызывать у него тревогу и раздражение;
Он также должен прокручивать журнал валидационной сборки, при этом в Azure Pipelines журнал
Johnny
не сворачивается, как это делается в других CI/CD системах:
Пример короткого журнала
Этот метод представления данных не включает кликабельные гиперссылки на нашу Wiki, репозиторий артефактов или информацию об уязвимостях в самом интерфейсе CodeScoring;
Вывод не может быть настроен под конкретные нужды;
Таблица с данными об уязвимостях довольно ясна и удобна, однако, таблица с нарушениями политик требует дополнительного времени для понимания:
Пример журнала сработки политик
В этой статье мы не затрагивали политики CodeScoring, а результаты в формате, отличном от консольного вывода в Johnny
пока не внедрены. Возможно, эта тема будет освещена в будущих публикациях.
Фиксируем весь материал
На первый взгляд, внедрение композиционного анализа кажется довольно простым: запустил тулзу, она что‑то нашла, заблокировала pull request/сборку, пускай разработчик сам разберется в логах тулзы или возьмет сработку себе в бэклог.
Тем не менее, если стремиться сделать данный анализ максимально эффективным и удобным для разработчиков, без программирования не обойтись. Мы не откладываем устранение уязвимостей, не ждем завершения pull request и сборки ПО, а проводим анализ на этапе pull request. Причем делаем это в блокирующем режиме, при этом не забываем развеселить некоторых коллег, впервые столкнувшихся с Тимом, несмотря на многолетний опыт работы в компании.
Делюсь полезными рекомендациями для тех, кто планирует реализовать композиционный анализ в pull request:
Установить пороговые уровни уязвимостей по шкале CVSS, начиная с которых pull request будет блокироваться без исключений, и постепенно повышать планку:
понижать минимальный уровень по шкале CVSS;
блокировать protestware либо вести свой «черный» список зависимостей;
блокировать не только по наличию CVE, но и по наличию CWE;
и т.д.
Освоить методику построения дерева зависимостей для всех языков программирования, применяемых в компании, и зафиксировать это методику в инструкциях на Wiki;
Разработать детальные инструкции и FAQ для разработчиков и коллег;
Установить на сборочные агенты все доступные системы сборки и восстановления пакетов, поддерживаемые решением для анализа;
Подготовить коллектив соответствующим образом, например, так:
Пример уведомления о грядущих блокировках pull request
Выбрать систему исключений: собственную или реализованную в композиционном анализаторе;
Не оставлять разработчика без поддержки, оказывайте помощь и консультируйте его. В нашей компании мы придерживаемся принципа клиентоцентричности: каждый коллега = клиент, и следует оперативно предложить помощь в повышении версии пакета либо внесению уязвимости в исключения;
Комментарии в pull request должны быть краткими, но содержательными;
Будьте открытыми, объясняйте командам структуру процесса, его преимущества для разработчиков и тех, кто занимается продажей «безопасного» ПО, а также проводите митапы.
И хотел бы узнать мнение сообщества по следующим вопросам
С какими сложностями вы столкнулись при внедрении композиционного анализа в pull request?
Охотно ли команды исправляют найденные уязвимости?
Как вы мотивируете разработчиков?
Другие мои статьи по безопасной разработке
Другие мои кейсы в части безопасности разработки смотри в статьях:
Выявление bidirectional unicode троянов (не все unicode символы нужны в исходном коде);
Дерево атак на исходный код в Azure Repos (руководство по защите от атак, направленных на компрометацию исходного кода и выбору мер защиты);
Шпаргалка по сегментации приложений от OWASP (автор шпаргалки);
Небезопасная разработка в Github (примеры публичных ошибок разработчиков);
История утечки персональных данных через Github (пример публичных ошибок одного разработчика).