Cтатья будет полезна разработчикам и инженерам по ИБ, желающим повысить уровень безопасности приложений за счет внедрения проверок, запрещающих вносить в ПО сторонние компоненты с известными уязвимостями.
Автор:
Голяков Сергей
DevSecOps, в информационной безопасности с 2013 года, в СПАО «Ингосстрах» встраиваю безопасность в процесс разработки
При написании этой статьи я вновь пришел к мысли, что каждый ИТ-шник и ИБ-шник должен быть хоть чуточку разработчиком для более эффективного решения своих задач.
Проблематика
Разработчики программного обеспечения часто применяют уязвимые зависимости, особенно на первых этапах разработки нового проекта:
Скрытый текст
Когда возможна угроза использования известных уязвимостей в зависимых компонентах, то это приводит к тому, что ПО становится уязвимым.
Помощник Тим и уязвимости
Чтобы привлечь внимание разработчиков к данной проблеме, мы используем нашего корпоративного кота Тим. Познакомиться с ним и его друзьями можно в нашем блоге: В самое сердечко: ребрендинг ИТ «Ингосстраха».
Тим способен испытывать разные эмоции, например:
[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.
CVE ID |
|
---|---|
GHSA ID |
|
CWEs |
CWE-20: Improper Input Validation |
Опубликована |
01.12.2022 |
Уточнена |
19.11.2023 |
Критичность (Score) по шкале CVSS v. 3 |
9.8 / 10 - Critical |
Проще говоря, если злоумышленнику удастся воспользоваться данной уязвимостью - будет плохо, а именно злоумышленник может выполнить нужный ему код на сервере от имени конечного ПО в котором используется org.yaml:snakeyaml@1.33
.
Часто бывает так, что разработчики, особенно из подрядных организаций, предоставляя нам готовое ПО или дорабатывая имеющееся ПО, применяют не самые «безопасные» версии заимствованных компонентов.
Консольный агент CodeScoring
Существует широкий спектр решений для выполнения композиционного анализа. В рамках стратегии импортозамещения можно рассмотреть интеграцию CodeScoring в процесс проверки каждого pull request в защищённых ветвях кода.
CodeScoring предлагает различные методы анализа репозиториев. В данной статье мы сосредоточимся исключительно на функционале, который может быть представлен консольным агентом Johnny в максимально доступной и понятной форме - это информация об уязвимостях:
Согласно документации, консольный агент Johnny
обладает широкими возможностями, но мы сосредоточимся на одной из функций - создание дерева зависимостей и формирование SBOM
.
SBOM
Software Bill of Materials - это список:
-
зависимостей с подробной информацией:
-
лицензий под которой опубликована зависимость и ее компоненты:
-
имеющихся известных уязвимостях в зависимостях:
Проще говоря, 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-процесс композиционного анализа:
В общем и целом, компонентная схема работы с 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
в ту же папку, откуда был запущен. Вне зависимости от результата валидационной сборки, файл будет выглядеть следующим образом:
Как упоминалось ранее, в рамках данной статьи нас интересуют только уязвимости. Проверка рискованных лицензий или других вопросов не рассматривается. Следовательно, нас интересует раздел vulnerabilities
файла SBOM
:
Обрабатываем 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 = "<span style=`"color:DarkRed`">${severity}</span>"
$score = "<span style=`"color:DarkRed`">${score}</span>"
}
elseif($severity -eq "high"){
$severity = "<span style=`"color:Red`">${severity}</span>"
$score = "<span style=`"color:Red`">${score}</span>"
}
elseif($severity -eq "medium"){
$severity = "<span style=`"color:Orange`">${severity}</span>"
$score = "<span style=`"color:Orange`">${score}</span>"
}
elseif($severity -eq "low"){
$severity = "<span style=`"color:Green`">${severity}</span>"
$score = "<span style=`"color:Green`">${score}</span>"
}
elseif($severity -eq "none"){
$severity = "<span style=`"color:LightBlue`">${severity}</span>"
$score = "<span style=`"color:LightBlue`">${score}</span>"
}
# Заполняем уровень риска 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("> <span style=`"color:#0053bd`">**Идентификатор уязвимости**</span>: $($vuln.id)")
$vulns_text.Add("<span style=`"color:#0053bd`">Критичность</span>: $($vuln.ratingString)")
$vulns_text.Add("<span style=`"color:#0053bd`">Описание уязвимости</span>: $($vuln.description)")
if($null -ne $($vuln.references_text))
{
$vulns_text.Add("<span style=`"color:#0053bd`">Полезные ссылки</span>: $($vuln.references_text)")
}
else{ Write-Host "<span style=`"color:#0053bd`">Полезные ссылки</span>: отсутствуют" }
if($null -ne $($vuln.affects_text)) {
$vulns_text.Add("<span style=`"color:#0053bd`">Затрагиваемые пакеты</span>: $($vuln.affects_text)")
}
else{
Write-Host "<span style=`"color:#0053bd`">Затрагиваемые пакеты</span>: отсутствуют"
}
$vulns_text.Add("<span style=`"color:#0053bd`">Рекомендация</span>: повысить до [$($vuln.pkgName)@$($vuln.recommendation)]($($vuln.artiURL_fixed)) (если пакета нет, см. [FAQ](https://<URI нашего wiki>))")
$vulns_text.Add("----------------------------------------------------<br/>")
}
# Очищаем итоговый текст от различных вариаций переноса строк,
# которые имеются в описании уязвимостей.
# Пропустим данный блок
Подготавливаем ссылку на артефакт валидационной сборки с 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://<URI нашего wiki> чтобы понять как действовать дальше."
Завершаем валидационную сборку с ошибкой, если выявлены уязвимости. Это не позволит разработчику внести известную уязвимость в код:
Write-Host "##[command]Согласно конфигу ${configFile} разрешено фейлить все валидационные сборки при известных уязвимостей"
Write-Host "##vso[task.logissue type=error;]Обнаружены известные уязвимости (${failed_vulns} шт.)" # Фейлим сборку с красивой ошибкой
Write-Host "##vso[task.complete result=SucceededWithIssues;]"
На данном этапе в журнале валидационной сборки разработчик может увидеть следующую информацию:
Оставляем комментарий в pull request
Подготовка комментария
Подготавливаем содержимое комментария:
Скрытый текст
[string] $footer = "
<details>
<summary>Полезная информация:</summary>
> **Риск согласно OWASP**: [A6 Vulnerable and Outdated Components](https://<URI на наш wiki>)
**Что делать дальше?**: См. инструкцию [CodeScoring FAQ](https://<URI на наш wiki>)
**Перечень найденных уязвимостей**: См. в файл [${reportName}](${buildResultPath}), либо журнал сборки [${buildDefinitionName}](${buildURI})
</details>
<details>
<summary>Список известных уязвимостей в зависимостях ($($failed_vulns.Count) шт.):</summary>
${vulns_string}
</details>
${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 достигается обязательностью нашей валидационной сборки:
Полезная информация об уязвимости
Если развернуть markdown текст, разработчик сможет ознакомиться со списком выявленных уязвимостей:
Видно, что комментарий включает в себя:
Всю необходимую информацию об уязвимости;
Ссылку на Wiki, где изложены действия разработчика в случае обнаружения такого комментария, а также раздел FAQ.
Ссылки на файл с уязвимостью нет ни в SBOM ни в журнале работы Johhny
. Не каждому разработчику удается найти ту самую верхнеуровневую зависимость, в которую встроена уязвимая транзитивная зависимость.
Как мы помогаем в поиске той самой верхнеуровневой зависимости
Не каждому разработчику удается найти ту самую верхнеуровневую зависимость, в которую встроена уязвимая транзитивная зависимость. Вот тут и приходит на помощь CodeScoring. В отличие от JFrog Xray, этот инструмент способен отображать верхнеуровневую зависимость в 90% случаев.
Когда разработчик сдался:
Пример поиска уязвимой зависимости по дереву зависимостей средствами CodeScoring в нашем искусственном проекте:
Если у разработчика отсутствуют необходимые ресурсы для создания дерева зависимостей или полученное дерево не дает ясности относительно того, какая из зависимостей первого уровня является основной, то решение CodeScoring эффективно решает эту проблему:
Базовый функционал Johnny по блокировке pull request
На самом деле, можно было бы просто блокировать pull request средствами самого Johnny
и вообще ничего не программировать. В журнале валидационной сборки выглядело бы это так:
Я абсолютно убежден, что вывод данной информации неудобен тем, что:
Разработчик вынужден тратить время на переход к журналу сборки, что может вызывать у него тревогу и раздражение;
Он также должен прокручивать журнал валидационной сборки, при этом в 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?
Охотно ли команды исправляют найденные уязвимости?
Как вы мотивируете разработчиков?
Другие мои статьи по безопасной разработке
Другие мои кейсы в части безопасности разработки смотри в статьях:
Выявление bidirectional unicode троянов (не все unicode символы нужны в исходном коде);
Дерево атак на исходный код в Azure Repos (руководство по защите от атак, направленных на компрометацию исходного кода и выбору мер защиты);
Шпаргалка по сегментации приложений от OWASP (автор шпаргалки);
Небезопасная разработка в Github (примеры публичных ошибок разработчиков);
История утечки персональных данных через Github (пример публичных ошибок одного разработчика).