Привет, Хабр! В своей работе наша компания очень часто имеет дело с различными инструментами статического анализа кода (SAST). Из коробки они все работают средне. Конечно, всё зависит от проекта и используемых в нём технологий, а также, насколько хорошо эти технологии покрываются правилами анализа. На мой взгляд, одним из самых главных критериев при выборе инструмента SAST является возможность настраивать его под особенности своих приложений, а именно писать и изменять правила анализа или, как их чаще называют, Custom Queries. Мы чаще всего используем Checkmarx - очень интересный и мощный анализатор кода. В этой статье я расскажу про свой опыт написания правил анализа для него.  Вступление Общая информация по правилам “Словарь“ для начинающего Операции со списками Все найденные элементы Функции для анализа Flow Получение имени/пути файла Результат выполнения Использование результатов выполнения других правил Решение проблем Логирование Проблема с логином Написание правил Заключение Для начала, я бы хотел порекомендовать одну из немногих статей на русском языке про особенности написания запросов для Checkmarx. Она была опубликована на Хабре в конце 2019 года под заголовком: «Hello, Checkmarx!». Как написать запрос для Checkmarx SAST и найти крутые уязвимости. В ней подробно рассмотрено, как написать первые запросы на языке CxQL (Checkmarx Query Language) для некоторого тестового приложения и показаны основные принципы работы правил анализа.  Я не буду повторять то, что в ней описано, хотя некоторые пересечения всё-таки будут присутствовать. В своей статье я постараюсь составить некоторый “сборник рецептов”, перечень решений конкретных задач, с которыми я сталкивался за время своей работы с Checkmarx. Над многими из этих задач мне пришлось изрядно поломать голову. Порой не хватало данных в документации, а порой и вообще трудно было понять, как сделать то, что требуется. Надеюсь, мой опыт и бессонные ночи не пропадут даром, и этот “сборник Custom Queries рецептов” сэкономит вам несколько часов или пару-тройку нервных клеток. Итак, начнем! Для начала рассмотрим несколько базовых понятий и процесс работы с правилами, для лучшего понимания, что будет происходить дальше. И еще потому что в документации про это не сказано или сильно размазано по структуре, что не очень удобно. Правила применяются при сканировании в зависимости от выбранного при старте пресета (набор активных правил). Пресетов можно создавать неограниченное количество и как именно их структурировать зависит от особенностей вашего процесса. Можно сгруппировать их по языкам или выделить пресеты для каждого проекта. Количество активных правил влияет на скорость и точность сканирования.  Правила редактируются в специальном инструменте под названием CxAuditor. Это десктопное приложение, которое подключается к серверу с Checkmarx. У этого инструмента есть два режима работы: редактирование правил и анализ результатов уже проведенного сканирования.   Правила в Checkmarx разделены по языкам, то есть для каждого языка существует свой набор запросов. Также есть некоторые общие правила, которые применяются вне зависимости от языка, это так называемые базовые запросы. В большинстве своем, базовые запросы содержат в себе поиск информации, которую используют другие правила.   Правила бывают “Executable” и “Non-Executable” (Выполняемые и Не выполняемые). Не совсем корректное название, на мой взгляд, но что есть. Суть в том, что результат выполнения “Executable” правил будет отображен в результатах сканирования в UI, а “Non-Executable” правила нужны только для использования их результатов в других запросах (по сути - просто функция).  Можно создавать новые правила или дополнять/переписывать существующие. Для того, чтобы переписать правило, нужно найти его в дереве, нажать правой кнопкой и в выпадающем меню выбрать пункт “Override“. Тут важно помнить, что новые правила изначально не включены в пресеты и не активны. Чтобы начать их использовать нужно активировать их в меню “Preset Manager” в инструменте. Переписанные правила сохраняют свои настройки, то есть, если правило было активно, таким оно и останется и будет применяться сразу.  Во время выполнения строится “дерево” запросов, что от чего зависит. Первыми выполняются правила, которые собирают информацию, вторыми те, кто ее использует. Результат выполнения кэшируется, так что если есть возможность использовать результаты существующего правила, то лучше так и сделать, это позволит уменьшить время сканирования. Правила можно применять на различных уровнях: Для всей системы - будет использован для любого сканирования любого проекта На уровне команды (Team) - будет применяться только для сканирования проектов в выбранной команде. На уровне проекта - Будет применяться в конкретном проекте  И начну я с нескольких вещей, которые вызывали у меня вопросы, а также покажу ряд приемов, которые существенно упростят жизнь. В рамках сканируемого языка можно получить список абсолютно всех элементов, которые определил Checkmarx (строки, функции, классы, методы и т.д.). Это некоторое пространство объектов, к которому можно обратиться через All. То есть, для поиска объекта с конкретным названием searchMe, можно выполнить поиск, например, по имени по всем найденным объектам: Но, если нужно выполнить поиск по другому языку, который по каким-то причинам не вошел в сканирование (например groovy в проекте для Android), можно расширить наше пространство объектов через переменную: Эти функции используются во многих правилах и вот небольшая шпаргалка, что они означают: Есть несколько атрибутов, которые можно получить из результатов выполнения запроса (имя файла в котором найдено вхождение, строка и т.д.), но как их получить и использовать в документации не сказано. Так вот для того, чтобы это сделать, необходимо обратиться к свойству LinePragma и уже внутри него будут находиться нужные нам объекты: Стоит иметь ввиду, что FileName содержит на самом деле путь к файлу, поскольку мы использовали метод GetFirstGraph. Внутри CxQL предусмотрена специальная переменная result, которая возвращает результат выполнения вашего написанного правила. Она инициализирована сразу и можно записывать в нее промежуточные результаты, изменяя и уточняя их в процессе работы. Но, если внутри правила не будет присвоения этой переменной или функции return- результат выполнения всегда будет нулевым.  Следующий запрос не вернет нам ничего в результате выполнения и всегда будет пустым: Но, присвоив результат выполнения к магической переменной result - увидим, что нам возвращает данный вызов: Правила в Checkmarx можно назвать аналогом функций в обычном языке программирования. При написании правила вы вполне можете использовать результаты других запросов. Для примера, нет необходимости каждый раз искать все вызовы методов в коде, достаточно вызвать нужное правило: Такой подход позволяет сократить код и существенно уменьшить время выполнения правила. При работе с инструментом, иногда не получается сразу написать нужный запрос и приходится экспериментировать, пробуя различные варианты. Для такого случая в инструменте предусмотрено логирование, которое вызывается следующим образом: Но стоит помнить, что на вход этот метод принимает только строку, так что вывести полный список найденных элементов в результате выполнения первой операции не получится. Второй вариант, который используется для отладки - это время от времени присваивать магической переменной result результат выполнения запроса и смотреть, что получится. Такой подход не очень удобен, нужно быть уверенным, что в коде после нет переопределения или операций с этим result или просто комментировать расположенный ниже код. А можно как я, забыть убрать из готового правила несколько таких вызовов и удивляться, почему ничего не работает. Более удобный способ - это вызвать метод return с нужным параметром. В таком случае, выполнение правила закончится и мы сможем увидеть, что же получилось в результате написанного нами: Бывают ситуации, когда не получается зайти в инструмент CxAudit (который используется для написания правил). Причин этому может быть много, аварийное завершение работы, внезапное обновление Windows, BSOD и другие непредвиденные ситуации, которые нам неподвластны. В таком случае иногда остается незавершенная сессия в базе данных, которая не дает зайти повторно. Для исправления необходимо выполнить несколько запросов: Для Checkmarx до 8.6: Для Checkmarx после 8.6: Вот и добрались до самого интересного. Когда начинаешь писать правила на CxQL, чаще не хватает даже не столько документации, сколько каких-то живых примеров решения определенных задач и описания процесса работы запросов в целом.  Я попробую немного упростить жизнь тем, кто начинает погружаться в язык запросов и приведу несколько примеров использования Custom Queries для решения определенных задач. Некоторые из них достаточно общие и могут быть применены в вашей компании практически без изменений, другие более специфичные, но их так же можно использовать, поменяв код под специфику ваших приложений. Итак, вот с какими задачами нам приходилось встречаться чаще всего: Задача: В результатах выполнения правила несколько Flow и один из них является вложением другого, необходимо оставить один из них. Решение: Действительно, иногда Checkmarx показывает несколько Flow движения данных, которые могут пересекаться и быть укороченной версией других. Для таких случаев есть специальный метод ReduceFlow. В зависимости от параметра он выберет самый короткий или самый длинный Flow: Задача: Расширить перечень чувствительных данных, на которые реагирует инструмент Решение: В Checkmarx существуют базовые правила, результат выполнения которых используют многие другие запросы. Дополнив некоторые из таких правил данными, специфичными для вашего приложения, можно сразу улучшить результаты сканирования. Ниже пример правила, с которого можно начать: General_privacy_violation_list Добавим несколько переменных, которые используются в нашем приложении для хранения чувствительной информации: Задача: Расширить перечень переменных с паролями  Решение: Я бы рекомендовал сразу обратить внимание на базовое правило по определению паролей в коде и добавить к нему список имён переменных, которые принято использовать в вашей компании.  Password_privacy_violation_list Задача: Добавить используемые фреймворки, которые не поддерживаются Checkmarx Решение: Все запросы в Checkmarx разделены по языкам, так что дополнять правила необходимо  для каждого языка. Ниже несколько примеров таких правил. Если используются библиотеки, которые дополняют или заменяют стандартный функционал - их легко добавить в базовое правило. Тогда все, кто его используют - сразу узнают о новых вводных. Как пример, библиотеки для логирования в Android - Timber и Loggi. В базовой поставке правил определения не системных вызовов нет, так что если пароль или идентификатор сессии попадет в лог, мы об этом не узнаем. Попробуем добавить в правила Checkmarx определения таких методов. Тестовый пример кода, который использует библиотеку Timber для логирования: А вот пример запроса для Checkmarx, который позволит добавить определение вызова методов Timber, как точку выхода данных из приложения: FindAndroidOutputs И также можно дополнить соседнее правило, но уже относящееся непосредственно к логированию в Android: FindAndroidLog_Outputs Также, если в Android-приложениях используется WorkManager для асинхронной работы, неплохо дополнительно сообщить об этом Checkmarx, добавив метод получения данных из задачи getInputData: FindAndroidRead Задача: Поиск чувствительных данных в plist для iOS проектов Решение: Часто для хранения различных переменных и значений в iOS используются специальные файлы с расширением .plist.  Хранение паролей, токенов, ключей и других чувствительных данных в этих файлах не рекомендуется, так как они без особых проблем могут быть извлечены из устройства. Файлы plist имеют особенности, которые не очевидны невооруженному глазу, но важны для Checkmarx. Напишем правило, которое будет искать нужные нам данные и сообщать нам, если где-то упоминаются пароли или токены.  Пример такого файла, в котором зашит токен для общения с сервисом backend: И правило для Checkmarx, в котором есть несколько нюансов, которые следует учитывать при написании: Задача: Поиск информации в XML Решение: В Checkmarx есть очень удобные функции по работе с XML и поиском значений, тэгов, атрибутов и прочего. Но в документации, к сожалению, допущена ошибка из-за которой ни один пример не работает. Несмотря на то, что в последней версии документации этот недочет устранен - будьте внимательны, если используете более ранние версии документов. Вот неправильный пример из документации: В результате попытки выполнения мы получим ошибку, что у All нет такого метода… И это верно, так как для использования функций для работы с XML есть специальное, отдельное пространство объектов - cxXPath. Вот как выглядит правильный запрос для поиска настройки в Android, разрешающей использование HTTP трафика: Разберем чуть подробнее, так как синтаксис у всех функций похожий, после того, как разобрался с одной, дальше нужно только выбрать нужную. Итак, последовательно по параметрам: "*.xml"- маска файлов, по которым необходимо производить поиск 8 - id языка, для которого применяется правило "cleartextTrafficPermitted"- имя атрибута в xml  "true" - значение этого атрибута false - использование регулярного выражения при поиске true - означает, что поиск будет выполнен с игнорированием регистра, то есть case-insensitive Для примера использовано правило, которое определяет некорректные, с точки зрения безопасности, настройки сетевого соединения в Android, которые разрешают общение с сервером посредством протокола HTTP. Пример настройки, содержащий атрибут cleartextTrafficPermitted со значением true: Задача: Ограничить результаты по имени/пути файла Решение: В одном из больших проектов, связанных с разработкой мобильного приложения под Android, мы столкнулись с ложными срабатываниями правила, которое определяет настройку обфускации. Дело в том, что правило из коробки ищет в файле build.gradle настройку, отвечающую за применение правил обфускации для релизной версии приложения.  Но в больших проектах иногда встречаются дочерние файлы build.gradle, которые относятся к библиотекам, включенным в проект. Особенность в том, что даже если в этих файлах не указана необходимость обфускации, при компиляции будут применяться настройки родительского файла сборки.  Таким образом, задача стоит в отсечении срабатываний в дочерних файлах, которые относятся к библиотекам. Определить их возможно по наличию строки apply 'com.android.library'. Пример кода из файла build.gradle, определяющий необходимость обфускации: Пример файла build.gradle для библиотеки, включенной в проект и не имеющей такой настройки: И правило для Checkmarx: ProGuardObfuscationNotInUse Такой подход может быть достаточно универсальным и пригодится не только для Android приложений, но и для других случаев, когда нужно определять принадлежность результата к определенному файлу. Задача: Добавить поддержку сторонней библиотеки, если синтаксис поддерживается не полностью Решение: Количество всевозможных фреймворков, которые используются в процессе написания кода просто зашкаливает. Конечно, Checkmarx не всегда знает об их существовании и наша задача научить его понимать, что определенные методы относятся именно к этому фреймворку. Иногда это осложняется тем, что фреймворки используют названия функций, которые сильно распространены и нельзя однозначно определить отношение того или иного вызова к конкретной библиотеке.  Сложность заключается в том, что синтаксис таких библиотек не всегда корректно распознается и приходится экспериментировать, чтобы не получить большое количество ложных срабатываний. Существует несколько вариантов, чтобы улучшить точность сканирования и решить поставленную задачу: Первый вариант, мы точно знаем, что библиотека используется в определенном проекте и можем применить правило на уровне команды. Но в случае, если команда решит использовать другой подход или использует несколько библиотек, в которых пересекаются названия функций мы можем получить не очень приятную картину из многочисленных ложных срабатываний Второй вариант, применить поиск по файлам, в которых явно происходит импорт библиотеки. При таком подходе мы сможем быть уверенными, что в данном файле точно применяется нужная нам библиотека И третий вариант, это использование двух вышеперечисленных подходов совместно.  В качестве примера разберем известную в узких кругах библиотеку slick для языка программирования Scala, а именно, функционал Splicing Literal Values. В общем случае, для передачи параметров в SQL-запрос необходимо использовать оператор $, который подставляет данные в предварительно сформированный SQL-запрос. То есть, по факту является прямым аналогом Prepared Statement в Java. Но, в случае необходимости динамически конструировать SQL-запрос, например, если нужно передавать имена таблиц, возможно использовать оператор #$, который напрямую подставит данные в запрос (практически, как конкатенация строк). Пример кода: Checkmarx пока не умеет определять использование Splicing Literal Values и пропускает операторы #$, так что попробуем научить его определять потенциальные SQL-инъекции и подсвечивать нужные места в коде: Задача: Поиск используемых уязвимых функций в Open-Source библиотеках Решение: Во многих компаниях используются инструменты для контроля Open-Source (практика OSA), позволяющие обнаружить использование уязвимых версий библиотек в разрабатываемых приложениях. Иногда обновить такую библиотеку до безопасной версии не представляется возможным. В каких-то случаях есть функциональные ограничения, в других безопасной версии и вовсе нет. В таком случае поможет комбинация практик SAST и OSA, позволяющая определить, что функции, которые приводят к эксплуатации уязвимости, не используются в коде.  Но иногда, особенно, если рассматривать JavaScript, это может быть не совсем тривиальной задачей. Ниже представлено решение, возможно не идеальное, но тем не менее работающее, на примере уязвимостей в компоненте lodash в методах template и *set. Примеры тестового потенциально уязвимого кода в JS файле: И при подключении напрямую в html: Ищем все наши уязвимые методы, которые перечислены в уязвимостях: Задача: Поиск зашитых в приложение сертификатов Решение: Нередко приложения, особенно мобильные, используют сертификаты или ключи для доступа к различным серверам или проверки SSL-Pinning. Если смотреть с точки зрения безопасности - хранить такие вещи в коде не самая лучшая практика. Попробуем написать правило, которое будет искать подобные файлы в репозитории: Задача: Поиск скомпрометированных токенов в приложении Решение: Нередко приходится отзывать скомпроментированные токены или другую важную информацию, которая присутствует в коде. Конечно, хранить их внутри исходников не самая хорошая идея, но ситуации бывают разные. Благодаря запросам CxQL найти такие вещи достаточно просто: Надеюсь, что тем, кто начинает своё знакомство с инструментом Checkmarx будет полезна данная статья. Возможно и те, кто уже давно пишет свои правила, тоже найдут что-то полезное в этом руководстве.  К сожалению, сейчас очень не хватает ресурса, где можно было бы почерпнуть новые идеи в ходе разработки правил для Checkmarx. Поэтому мы создали репозиторий на Github, где будем выкладывать свои наработки, чтобы каждый, кто использует CxQL, смог найти в нем что-то полезное, а также имел возможность поделиться с сообществом своими трудами. Репозиторий в процессе наполнения и структурирования контента, так что contributors are welcome! Спасибо за внимание! Архитектор по информационной безопасности