CodeQL- инструмент для анализа исходного кода

Из-за того, что каждое приложение по-своему небезопасно, очень сложно разработать анализатор, который одинаково хорошо работал бы для каждого из них. Кроме того, большинство решений, связанных с анализом исходного кода, имеют различные недостатки, такие как: стоимость, непредсказуемость результатов, большое количество ложных срабатываний (как ложноположительных, так и ложноотрицательных) и недостаточные возможности для настройки тестов.В сегодняшней статье я расскажу о CodeQL, интересном инструменте и языке для анализа исходного кода, который набирает популярность и выглядит очень многообещающим. CodeQL помогает сделать еще один шаг ближе к исправлению недостатков обычных анализаторов кода. Он будет полезен как аналитикам ИБ, так и разработчикам в целом, желающим улучшить качество своего кода.

Что такое CodeQL

CodeQL — это инструмент с открытым исходным кодом и язык запросов, который чем-то напоминает SQL и позволяет программно ссылаться на определенные разделы кода и выполнять определенные аналитиком проверки диаграммы потока данных / управления и структуры исходного кода в целом. Правила поиска уязвимостей, настраиваемые в инструментах статического тестирования безопасности приложений (SAST), аналогичны этому подходу.

CodeQL был первоначально разработан Semmle, которая была приобретена GitHub в сентябре 2020 года и реализована в их Security Lab. С этого момента разработкой продукта занимается сам GitHub и небольшое сообщество энтузиастов. В настоящее время официальный репозиторий содержит в общей сложности более 2000 запросов QL, охватывающих широкий спектр проблем с кодом, от поиска недопустимых регулярных выражений в JavaScript до обнаружения использования небезопасных криптографических алгоритмов в коде C ++.

Говоря по простому, это инструмент, с помощью которого вы можете проверить некоторые гипотезы о коде или данных, которые проходят через него. Например, вы можете составлять запросы, которые проверяют, существует ли путь для данных от места ввода пользователя до небезопасной части кода, где эти данные создаются. С точки зрения экзамена на качество кода вы можете, например, найти все функции, которые имеют более 5 аргументов, или найти пустые циклы for / while.

В конце концов такой анализ оказывается очень полезным для упрощения процесса оценки качества и безопасности разрабатываемого кода и в долгосрочной перспективе приводит к общему увеличению зрелости приложения.

Одной из положительных сторон CodeQL является то, что он может не только искать проблемные участки кода по определенному шаблону (например, инструмент SemGrep), но и «понимать» структуру кода на уровне отдельных инструкций и выражений, различать вызов функции от вызова метода класса, а также отслеживание пути значения с помощью выражений и других операций с этими выражениями (например, присвоение значения переменной, вызов функции с этой переменной в качестве параметра, а затем присвоение результата новой переменной).

В настоящее время поддерживаются следующие языки с разной степенью полноты: C / C ++, C #, Java, Go, Python, JavaScript / TypeScript. Кроме того, для каждого языка существует набор поддерживаемых фреймворков, упрощающих написание запросов.

CodeQL предоставляется в нескольких вариантах:

  1. Консольная утилита, позволяющая встроить проверки в CI/CD цикл и осуществлять сканирование кода из командной строки.
  2. Расширение для Visual Studio Code для удобного написания и ad-hoc исполнения запросов.
  3. Онлайн-консоль LGTM, позволяющая писать запросы и проводить тестовое сканирование приложения из заданного GitHub-репозитория.

Кроме этого можно подключить сканирование своих репозиториев непосредственно в CI/CD на GitHub.

Варианты применения CodeQL

Давайте посмотрим, какие есть потенциальные варианты использования CodeQL:

  1. Самый простой сценарий состоит в том, что мы просто запускаем сканер со всем набором стандартных запросов и разбираем результаты, среди которых будут и проблемы с качеством кода, и проблемы с безопасностью.
  2. Сценарий посложнее включает в себя два этапа запуска сканера со встроенными запросами. Первый – только с запросами, относящимися к качеству кода (пустые блоки, избыточные комментарии, большое количество параметров функции и т. п.), а второй – с запросами непосредственно на проверку безопасности. В дальнейшем результаты разбирают две независимые группы ответственных сотрудников.
  3. Самый продуктивный сценарий включает в себя модификацию базовых запросов и/или написание собственных новых запросов, исходя из специфики конкретного приложения и появляющихся угроз.
  4. Например при выходе очередной 0-day уязвимости аналитик безопасности может составить запрос, который будет проверять все проекты на ее наличие. Или при анализе дефектов, найденных в процессе внутреннего аудита, каждый такой дефект может быть переписан на языке QL, чтобы не допустить проблем в других проектах.
  5. Также CodeQL может быть использован для исследования кода в целом (например определить все точки входа в приложение, чтобы впоследствии эту информацию передать аналитикам, занимающимся динамическим анализом приложения).

Язык QL очень гибок и позволяет решить большое количество задач, связанных с анализом кода, при этом давая инструменты, чтобы точечно отсекать потенциальные места возникновения ложных результатов.

Как работает CodeQL

Однако давайте посмотрим на синтаксис запроса и результат CodeQL на примере запроса в консоли LGTM.

Ниже приведен простой запрос, который мы используем для поиска всех пустых блоков кода в конкретном тестовом приложении. Мы подробнее рассмотрим, что здесь находится, позже в этой статье.

Простой запрос CodeQL по поиску пустых блоков
Обнаруженный пустой участок кода

Или более сложный случай, когда мы ищем проблемы с Cross-Site Scripting:

Запрос CodeQL, обнаруживающий XSS путём отслеживания путей недоверенных данных

Результат включает в себя все промежуточные шаги (в данном примере только стартовый и конечный), которые привели данные от места ввода до вывода и участки кода, в которых заключена проблема:

А вот так тот же результат выглядит в расширении для VSCode:

На скриншоте выше мы видим инструкции CodeQL (верхняя панель), которые отслеживают путь к данным от точек ввода удаленного пользователя (например, параметров запроса GET) до конструкций кода, которые могут отображать эти ненадежные данные пользователю. В этом случае отдельная инструкция isSanitizer указывает, что код содержит функцию очистки и, соответственно, если через эту функцию проходит подозрительный поток данных, он больше не должен рассматриваться как ненадежный. Это один из нескольких способов уменьшить количество ложных срабатываний.

В результатах запроса (нижняя панель) мы снова можем посмотреть на раздел кода, в котором в приложении отображаются ненадежные данные (т. н. source), и раздел кода, в котором они отображаются (т. н. sink).

Консоль LGTM

Чтобы поэкспериментировать с языком без локальной установки CodeQL, вы можете использовать онлайн-консоль LGTM (Looks Good To Me). Он включает в себя простой редактор запросов и возможность выполнять этот запрос на предварительно установленной кодовой базе нескольких open-source проектов или в вашем собственном проекте GitHub.

Давайте сразу попробуем исполнить простейшие запросы и начать практическое знакомство с CodeQL:

  1. Переходим в онлайн-консоль: https://lgtm.com/query/.
  2. Выбираем в качестве языка JavaScript, а в качестве проекта meteor/meteor.
  3. Копируем нижеприведенные запросы.
  4. Нажимаем Run и смотрим результаты в панели под полем ввода кода.

Простой запрос, отображающий все места в анализируемом исходном коде, где объявляются классы выглядит так:

import javascript
from ClassExpr ce
select ce 

Более сложный запрос, который покажет нам все места в файле client.js, где происходит вызов функции eval(), а также аргументы этой функции:

import javascript
from CallExpr call
where call.getCalleeName() = "eval" 
and call.getLocation().getFile().getRelativePath().matches("%client.js")
select call, call.getAnArgument() 

Более продвинутые запросы позволяют задать точку входа данных (например конкретный параметр некоторой функции) и точку выхода (например параметр той же функций eval() ) для того, чтобы отследить проходят ли данные от начальной до конечной точки.

Установка CodeQL

Для использования в своих проектах на постоянной основе консоль LGTM не очень удобна, поэтому есть смысл установить CodeQL CLI и библиотеки локально.

Процесс установки всего пакета в целом несложен, но требует понимания ряда нюансов.

Простой вариант, с которого можно начать, выглядит так:

  1. Установить VSCode и CodeQL extension.
  2. Скачать и распаковать CodeQL CLI в директорию, например, codeql.
  3. Прописать путь до директории codeql в %PATH%.
  4. Скачать стартовый воркспейс VSCode для работы с CodeQL (впоследствии можно будет сделать свой, но для начала работы можно использовать готовый):
  5. git clone https://github.com/github/vscode-codeql-starter/ git submodule update --init --remote В нем мы будем работать (то есть писать наши запросы) в папке, соответствующей интересующему нас языку (например для JS это codeql-custom-queries-javascript).
  6. Скачиваем тестовую кодовую базу (то есть базу, в которой определенным образом хранятся все необходимые данные о коде и внутренних взаимосвязях в нем, о чем подробнее будет рассказано ниже), например (для JS) https://github.com/githubsatelliteworkshops/codeql/releases/download/v1.0/esbenabootstrap-pre-27047javascript.zip
  7. Чуть ниже мы посмотрим как создавать свои собственные кодовые базы для наших проектов.
  8. Опционально распаковываем архив с кодовой базой.
  9. В VSCode выбираем Open workspace и открываем файл стартового воркспейса.
  10. В VSCode на закладке CodeQL добавляем папку (или архив) с кодовой базой, против которой будет запускаться анализ кода.
  11. Готово. Теперь в соответствующей папке воркспейса (см. шаг 4) можно открыть файл example.ql и в нем начать писать свои запросы.
  12. Исполняем тестовый запрос и убеждаемся, что всё работает
import javascript
from Expr e
select “Wazzup!” 

Кодовая база

В CodeQL весь анализируемый код должен быть организован особым образом в т.н. кодовую базу, по которой мы будем запрашивать позже. Он содержит полное иерархическое представление этого кода, включая абстрактное синтаксическое дерево (AST), граф потока данных и граф потока управления. Библиотеки языка CodeQL определяют классы, которые добавляют уровень абстракции относительно таблиц этой базы данных. Другими словами, у нас есть возможность писать запросы в базе кода, используя принципы ООП. Это именно та функциональность, которая отличает CodeQL от инструментов, которые ищут проблемы в коде с помощью предопределенных шаблонов и регулярных выражений.

Кодовая база CodeQL также включает в себя архив с исходным кодом и его зависимостями. Этот же исходный код потом используется, когда мы смотрим результаты выполнения запроса.

Для разных языков процесс создания базы немного отличается. Например создание кодовой базы для JS в папке my-js-codebase выполняется следующей командой в директории, которая содержит исходный код:

codeql database create my-js-codebase --language=javascript

Для компилируемых языков требуется, чтобы в системе был соответствующий компилятор и сборщик (например Maven для Java)

Следующий шаг – загрузить информацию о базе в VSCode. Это делается в редакторе на соответствующей вкладке CodeQL → “Choose Database from Folder”

К сожалению, механизма обновления кодовой базы нет, поэтому, если в исходный код вносятся какие-либо изменения, необходимо воссоздавать всю базу целиком.

Обычный CodeQL запрос

Давайте разберем, из чего вообще состоит типичный запрос в CodeQL на примере кодовой базы на языке JavaScript.

Самый базовый запрос, который выводит все jQuery-функции с именем “$“ (типа $(arg1, arg2)) и их первый аргумент, выглядит так, как показано ниже. Вы можете самостоятельно выполнить его для любой кодовой базы с jQuery:

/**
* @name QueryName
* @kind problem
* @id my_id_1
*/
// -- метаданные
import javascript 
//  Выражение для подключения библиотеки CodeQL для работы 
//   с конструкциями JavaScript.
// Также есть возможность подключения других библиотек для 
//   работы с другими языками, фреймворками и технологиями.
// Например semmle.javascript.NodeJS или python.

from CallExpr dollarCall, Expr dollarArg 
// Объявление переменной dollarCall типа CallExpr 
//   и переменной dollarArg типа Expr.
// CallExpr - это класс из стандартной библиотеки, представляющий 
//   коллекцию всех вызовов функций в интересующем нас коде.
// Expr - класс, представляющий коллекцию всех выражений. 
// Например в Object.entries = function(obj) выражениями являются 
//   вся строка целиком, Object, Object.entries, entries, 
//   function(obj), obj.

where dollarCall.getCalleeName() = "$"
// Логические формулы, которые мы применяем к объявленным переменным.
// Мы проверяем, что результат выполнения предиката (т.е. логической 
//   инструкции) getCaleeName() (который возвращает название 
//   вызываемой функции) нашего объекта dollarCall (который содержит 
//   все вызовы функций) равен "$"

and dollarArg = dollarCall.getArgument(0)
// Эта логическая формула операцией AND соединяется с предыдущей 
//   и уточняет условие, применяемое в запросе.
// В итоге мы из всех вызовов функций, в названии которых есть $ 
//   выбираем в переменную dollarArg первые аргументы (как сущности, 
//   а не как конкретные значения аргументов).
select dollarCall, dollarArg 
// Указание на то, какие выражения (значение каких переменных или 
//   предикатов) мы хотим вывести в результате. 

Как вы можете заметить, синтаксис языка схож с синтаксисом SQL, но при этом в основе лежат концепции ООП. В последующих частях мы познакомимся чуть поглубже с нюансами, терминами и идеями, которые лежат в основе CodeQL.

Заключение

Обеспечение безопасности проектов с открытым исходным кодом в мире — непростая задача. Во-первых, масштаб: экосистема JavaScript содержит более миллиона пакетов с открытым исходным кодом. К тому же не хватает специалистов по безопасности, около 500 разработчиков на эксперта. Наконец, есть координация: мировые эксперты по безопасности работают на тысячи компаний.