Как написать правила для Checkmarx и не сойти с ума

Привет, Хабр!

В своей работе наша компания очень часто имеет дело с различными инструментами статического анализа кода (SAST). Из коробки они все работают средне. Конечно, всё зависит от проекта и используемых в нём технологий, а также, насколько хорошо эти технологии покрываются правилами анализа. На мой взгляд, одним из самых главных критериев при выборе инструмента SAST является возможность настраивать его под особенности своих приложений, а именно писать и изменять правила анализа или, как их чаще называют, Custom Queries.

ed0b96a62f7c4a1c9e0976294a7c0f80.png

Мы чаще всего используем Checkmarx — очень интересный и мощный анализатор кода. В этой статье я расскажу про свой опыт написания правил анализа для него.

Оглавление

Вступление

Для начала, я бы хотел порекомендовать одну из немногих статей на русском языке про особенности написания запросов для Checkmarx. Она была опубликована на Хабре в конце 2019 года под заголовком: «Hello, Checkmarx!». Как написать запрос для Checkmarx SAST и найти крутые уязвимости.

В ней подробно рассмотрено, как написать первые запросы на языке CxQL (Checkmarx Query Language) для некоторого тестового приложения и показаны основные принципы работы правил анализа.

Я не буду повторять то, что в ней описано, хотя некоторые пересечения всё-таки будут присутствовать. В своей статье я постараюсь составить некоторый «сборник рецептов», перечень решений конкретных задач, с которыми я сталкивался за время своей работы с Checkmarx. Над многими из этих задач мне пришлось изрядно поломать голову. Порой не хватало данных в документации, а порой и вообще трудно было понять, как сделать то, что требуется. Надеюсь, мой опыт и бессонные ночи не пропадут даром, и этот «сборник Custom Queries рецептов» сэкономит вам несколько часов или пару-тройку нервных клеток. Итак, начнем!

Общая информация по правилам

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

  1. Правила применяются при сканировании в зависимости от выбранного при старте пресета (набор активных правил). Пресетов можно создавать неограниченное количество и как именно их структурировать зависит от особенностей вашего процесса. Можно сгруппировать их по языкам или выделить пресеты для каждого проекта. Количество активных правил влияет на скорость и точность сканирования.

    Настройка Preset в интерфейсе CheckmarxНастройка Preset в интерфейсе Checkmarx
  2. Правила редактируются в специальном инструменте под названием CxAuditor. Это десктопное приложение, которое подключается к серверу с Checkmarx. У этого инструмента есть два режима работы: редактирование правил и анализ результатов уже проведенного сканирования.

    Интерфейс CxAuditИнтерфейс CxAudit
  3. Правила в Checkmarx разделены по языкам, то есть для каждого языка существует свой набор запросов. Также есть некоторые общие правила, которые применяются вне зависимости от языка, это так называемые базовые запросы. В большинстве своем, базовые запросы содержат в себе поиск информации, которую используют другие правила.

    Разделение правил по языкамРазделение правил по языкам
  4. Правила бывают «Executable» и «Non-Executable» (Выполняемые и Не выполняемые). Не совсем корректное название, на мой взгляд, но что есть. Суть в том, что результат выполнения «Executable» правил будет отображен в результатах сканирования в UI, а «Non-Executable» правила нужны только для использования их результатов в других запросах (по сути — просто функция).

    Определение типа правила при созданииОпределение типа правила при создании
  5. Можно создавать новые правила или дополнять/переписывать существующие. Для того, чтобы переписать правило, нужно найти его в дереве, нажать правой кнопкой и в выпадающем меню выбрать пункт «Override». Тут важно помнить, что новые правила изначально не включены в пресеты и не активны. Чтобы начать их использовать нужно активировать их в меню «Preset Manager» в инструменте. Переписанные правила сохраняют свои настройки, то есть, если правило было активно, таким оно и останется и будет применяться сразу.

    Пример нового правила в интерфейсе Preset ManagerПример нового правила в интерфейсе Preset Manager
  6. Во время выполнения строится «дерево» запросов, что от чего зависит. Первыми выполняются правила, которые собирают информацию, вторыми те, кто ее использует. Результат выполнения кэшируется, так что если есть возможность использовать результаты существующего правила, то лучше так и сделать, это позволит уменьшить время сканирования.

  7. Правила можно применять на различных уровнях:

  • Для всей системы — будет использован для любого сканирования любого проекта

  • На уровне команды (Team) — будет применяться только для сканирования проектов в выбранной команде.

  • На уровне проекта — Будет применяться в конкретном проекте

    Определение уровня, на котором будет применяться правилоОпределение уровня, на котором будет применяться правило

«Словарь» для начинающего

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

Операции со списками

- вычитание одного из другого (list2 - list1)
* пересечение списков (list1 * list2)
+ сложение списков (list1 + list2)

& (логическое И) - объединяет списки по совпадению (list1 & list2), аналогично пересечению (list1 * list2)
| (логическое ИЛИ) - объединяет списки по широкому поиску (list1 | list2)

Со списками не работает:  ^  &&  ||  %  / 

Все найденные элементы

В рамках сканируемого языка можно получить список абсолютно всех элементов, которые определил Checkmarx (строки, функции, классы, методы и т.д.). Это некоторое пространство объектов, к которому можно обратиться через All. То есть, для поиска объекта с конкретным названием searchMe, можно выполнить поиск, например, по имени по всем найденным объектам:

// Такой запрос выдаст все элементы
result = All;

// Такой запрос выдаст все элементы, в имени которых присутствует "searchMe"
result = All.FindByName("searchMe");

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

result = AllMembers.All.FindByName("searchMe");

Функции для анализа Flow

Эти функции используются во многих правилах и вот небольшая шпаргалка, что они означают:

// Какие данные second влияют на first.
// Другими словами - ТО (second) что влияет на  МЕНЯ (first).
result = first.DataInfluencedBy(second);

// Какие данные first влияют на second.
// Другими словами - Я (first) влияю на ТО (second).
result = first.DataInfluencingOn(second);

Получение имени/пути файла

Есть несколько атрибутов, которые можно получить из результатов выполнения запроса (имя файла в котором найдено вхождение, строка и т.д.), но как их получить и использовать в документации не сказано. Так вот для того, чтобы это сделать, необходимо обратиться к свойству LinePragma и уже внутри него будут находиться нужные нам объекты:

// Для примера найдем все методы
CxList methods = Find_Methods();

// В методах найдем по имени метод scope
CxList scope = methods.FindByName("scope");

// Таким образом можо получить путь к файлу
string current_filename = scope.GetFirstGraph().LinePragma.FileName;

// А вот таким - строку, где нашлось срабатывание
int current_line = scope.GetFirstGraph().LinePragma.Line;

// Эти параметры можно использовать по разному
// Например получить все объекты в файле
CxList inFile = All.FindByFileName(current_filename);

// Или найти что происходит в конкретной строке
CxList inLine = inFile.FindByPosition(current_line);

Стоит иметь ввиду, что FileName содержит на самом деле путь к файлу, поскольку мы использовали метод GetFirstGraph.

Результат выполнения

Внутри CxQL предусмотрена специальная переменная result, которая возвращает результат выполнения вашего написанного правила. Она инициализирована сразу и можно записывать в нее промежуточные результаты, изменяя и уточняя их в процессе работы. Но, если внутри правила не будет присвоения этой переменной или функции return— результат выполнения всегда будет нулевым.

Следующий запрос не вернет нам ничего в результате выполнения и всегда будет пустым:

// Находим элементы foo
CxList libraries = All.FindByName("foo");

Но, присвоив результат выполнения к магической переменной result — увидим, что нам возвращает данный вызов:

// Находим элементы foo
CxList libraries = All.FindByName("foo");

// Выводим, как результат выполнения правила
result = libraries

// Или еще короче
result = All.FindByName("foo");

Использование результатов выполнения других правил

Правила в Checkmarx можно назвать аналогом функций в обычном языке программирования. При написании правила вы вполне можете использовать результаты других запросов. Для примера, нет необходимости каждый раз искать все вызовы методов в коде, достаточно вызвать нужное правило:

// Получаем результат выполнения другого правила
CxList methods = Find_Methods();

// Ищем внутри метод foo. 
// Второй параметр false означает, что ищем без чувствительности к регистру
result = methods.FindByShortName("foo", false);

Такой подход позволяет сократить код и существенно уменьшить время выполнения правила.

Решение проблем

Логирование

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

// Находим что-то
CxList toLog = All.FindByShortName("log");

// Формируем строку и отправляем в лог
cxLog.WriteDebugMessage ("number of DOM elements =” + All.Count);

Но стоит помнить, что на вход этот метод принимает только строку, так что вывести полный список найденных элементов в результате выполнения первой операции не получится. Второй вариант, который используется для отладки — это время от времени присваивать магической переменной result результат выполнения запроса и смотреть, что получится. Такой подход не очень удобен, нужно быть уверенным, что в коде после нет переопределения или операций с этим result или просто комментировать расположенный ниже код. А можно как я, забыть убрать из готового правила несколько таких вызовов и удивляться, почему ничего не работает.

Более удобный способ — это вызвать метод return с нужным параметром. В таком случае, выполнение правила закончится и мы сможем увидеть, что же получилось в результате написанного нами:

// Находим что-то
CxList toLog = All.FindByShortName("log");

// Выводим результат выполнения
return toLog

//Все, что написано дальше не будет выполнено
result = All.DataInfluencedBy(toLog)

Проблема с логином

Бывают ситуации, когда не получается зайти в инструмент CxAudit (который используется для написания правил). Причин этому может быть много, аварийное завершение работы, внезапное обновление Windows, BSOD и другие непредвиденные ситуации, которые нам неподвластны. В таком случае иногда остается незавершенная сессия в базе данных, которая не дает зайти повторно. Для исправления необходимо выполнить несколько запросов:

Для Checkmarx до 8.6:

// Проверяем, что есть залогиненые пользователи, выполнив запрос в БД
SELECT COUNT(*) FROM [CxDB].[dbo].LoggedinUser WHERE [ClientType] = 6;
 
// Если что-то есть, а на самом деле даже если и нет, попробовать выполнить запрос
DELETE FROM [CxDB].[dbo].LoggedinUser WHERE [ClientType] = 6;

Для Checkmarx после 8.6:

// Проверяем, что есть залогиненые пользователи, выполнив запрос в БД
SELECT COUNT(*) FROM LoggedinUser WHERE (ClientType = 'Audit');
 
// Если что-то есть, а на самом деле даже если и нет, попробовать выполнить запрос
DELETE FROM [CxDB].[dbo].LoggedinUser WHERE (ClientType = 'Audit');

Написание правил

Вот и добрались до самого интересного. Когда начинаешь писать правила на CxQL, чаще не хватает даже не столько документации, сколько каких-то живых примеров решения определенных задач и описания процесса работы запросов в целом.

Я попробую немного упростить жизнь тем, кто начинает погружаться в язык запросов и приведу несколько примеров использования Custom Queries для решения определенных задач. Некоторые из них достаточно общие и могут быть применены в вашей компании практически без изменений, другие более специфичные, но их так же можно использовать, поменяв код под специфику ваших приложений.

Итак, вот с какими задачами нам приходилось встречаться чаще всего:

Задача: В результатах выполнения правила несколько Flow и один из них является вложением другого, необходимо оставить один из них.

Решение: Действительно, иногда Checkmarx показывает несколько Flow движения данных, которые могут пересекаться и быть укороченной версией других. Для таких случаев есть специальный метод ReduceFlow. В зависимости от параметра он выберет самый короткий или самый длинный Flow:

// Оставить только длинные Flow
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceSmallFlow);

// Оставить только короткие Flow
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceBigFlow);

Задача: Расширить перечень чувствительных данных, на которые реагирует инструмент

Решение: В Checkmarx существуют базовые правила, результат выполнения которых используют многие другие запросы. Дополнив некоторые из таких правил данными, специфичными для вашего приложения, можно сразу улучшить результаты сканирования. Ниже пример правила, с которого можно начать:

General_privacy_violation_list

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

// Получаем результат выполнения базового правила
result = base.General_privacy_violation_list();

// Ищем элементы, которые попадают под простые регулярные выражения. Можно дополнить характерными для вас паттернами.
CxList personalList = All.FindByShortNames(new List {
	"*securityToken*", "*sessionId*"}, false);

// Добавляем к конечному результату
result.Add(personalList);

Задача: Расширить перечень переменных с паролями

Решение: Я бы рекомендовал сразу обратить внимание на базовое правило по определению паролей в коде и добавить к нему список имён переменных, которые принято использовать в вашей компании.

Password_privacy_violation_list

CxList allStrings = All.FindByType("String"); 
allStrings.Add(All.FindByType(typeof(StringLiteral))); 
allStrings.Add(Find_UnknownReference());
allStrings.Add(All.FindByType(typeof (Declarator)));
allStrings.Add(All.FindByType(typeof (MemberAccess)));
allStrings.Add(All.FindByType(typeof(EnumMemberDecl))); 
allStrings.Add(Find_Methods().FindByShortName("get*"));

// Дополняем дефолтный список переменных
List < string > pswdIncludeList = new List{"*password*", "*psw", "psw*", "pwd*", "*pwd", "*authKey*", "pass*", "cipher*", "*cipher", "pass", "adgangskode", "benutzerkennwort", "chiffre", "clave", "codewort", "contrasena", "contrasenya", "geheimcode", "geslo", "heslo", "jelszo", "kennwort", "losenord", "losung", "losungswort", "lozinka", "modpas", "motdepasse", "parol", "parola", "parole", "pasahitza", "pasfhocal", "passe", "passord", "passwort", "pasvorto", "paswoord", "salasana", "schluessel", "schluesselwort", "senha", "sifre", "wachtwoord", "wagwoord", "watchword", "zugangswort", "PAROLACHIAVE", "PAROLA CHIAVE", "PAROLECHIAVI", "PAROLE CHIAVI", "paroladordine", "verschluesselt", "sisma",
                "pincode",
								"pin"};
								
List < string > pswdExcludeList = new List{"*pass", "*passable*", "*passage*", "*passenger*", "*passer*", "*passing*", "*passion*", "*passive*", "*passover*", "*passport*", "*passed*", "*compass*", "*bypass*", "pass-through", "passthru", "passthrough", "passbytes", "passcount", "passratio"};

CxList tempResult = allStrings.FindByShortNames(pswdIncludeList, false);
CxList toRemove = tempResult.FindByShortNames(pswdExcludeList, false);
tempResult -= toRemove;
tempResult.Add(allStrings.FindByShortName("pass", false));

foreach (CxList r in tempResult)
{
	CSharpGraph g = r.data.GetByIndex(0) as CSharpGraph;
	if(g != null && g.ShortName != null && g.ShortName.Length < 50)
	{
		result.Add(r);
	}
}

Задача: Добавить используемые фреймворки, которые не поддерживаются Checkmarx

Решение: Все запросы в Checkmarx разделены по языкам, так что дополнять правила необходимо для каждого языка. Ниже несколько примеров таких правил.

Если используются библиотеки, которые дополняют или заменяют стандартный функционал — их легко добавить в базовое правило. Тогда все, кто его используют — сразу узнают о новых вводных. Как пример, библиотеки для логирования в Android — Timber и Loggi. В базовой поставке правил определения не системных вызовов нет, так что если пароль или идентификатор сессии попадет в лог, мы об этом не узнаем. Попробуем добавить в правила Checkmarx определения таких методов.

Тестовый пример кода, который использует библиотеку Timber для логирования:

package com.death.timberdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import timber.log.Timber;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Timber.e("Error Message");
        Timber.d("Debug Message");

        Timber.tag("Some Different tag").e("And error message");
    }
}

А вот пример запроса для Checkmarx, который позволит добавить определение вызова методов Timber, как точку выхода данных из приложения:

FindAndroidOutputs

// Получаем результат выполнения базового правила
result = base.Find_Android_Outputs();

// Дополняем вызовами, которые приходят из библиотеки Timber
CxList timber = All.FindByExactMemberAccess("Timber.*") +
    All.FindByShortName("Timber").GetMembersOfTarget();

// Добавляем к конечному результату
result.Add(timber);

И также можно дополнить соседнее правило, но уже относящееся непосредственно к логированию в Android:

FindAndroidLog_Outputs

// Получаем результат выполнения базового правила
result = base.Find_Android_Log_Outputs();

// Дополняем вызовами, которые приходят из библиотеки Timber
result.Add(
  All.FindByExactMemberAccess("Timber.*") +
  All.FindByShortName("Timber").GetMembersOfTarget()
);

Также, если в Android-приложениях используется WorkManager для асинхронной работы, неплохо дополнительно сообщить об этом Checkmarx, добавив метод получения данных из задачи getInputData:

FindAndroidRead

// Получаем результат выполнения базового правила
result = base.Find_Android_Read();

// Дополняем вызовом функции getInputData, которая используется в WorkManager
CxList getInputData = All.FindByShortName("getInputData");

// Добавляем к конечному результату
result.Add(getInputData.GetMembersOfTarget());

Задача: Поиск чувствительных данных в plist для iOS проектов

Решение: Часто для хранения различных переменных и значений в iOS используются специальные файлы с расширением .plist. Хранение паролей, токенов, ключей и других чувствительных данных в этих файлах не рекомендуется, так как они без особых проблем могут быть извлечены из устройства.

Файлы plist имеют особенности, которые не очевидны невооруженному глазу, но важны для Checkmarx. Напишем правило, которое будет искать нужные нам данные и сообщать нам, если где-то упоминаются пароли или токены.

Пример такого файла, в котором зашит токен для общения с сервисом backend:





	DeviceDictionary
	
		phone
		iPhone 6s
	
	privatekey
	MIICXAIBAAKBgQCqGKukO1De7zhZj6+

И правило для Checkmarx, в котором есть несколько нюансов, которые следует учитывать при написании:

// Используем результат выполнения правила по поиску файлов plist, чтобы уменьшить время работы правила и 
CxList plist = Find_Plist_Elements();

// Инициализируем новую переменную
CxList dictionarySettings = All.NewCxList();

// Теперь добавим поиск всех интересующих нас значений. В дальнейшем можно расширять этот список.
// Для поиска значений, как ни странно, используется FindByMemberAccess - поиск обращений к методам. Второй параметр внутри функции, false, означает, что поиск нечувствителен к регистру
dictionarySettings.Add(plist.FindByMemberAccess("privatekey", false));
dictionarySettings.Add(plist.FindByMemberAccess("privatetoken", false));

// Для корректного поиска из-за особенностей структуры plist - нужно искать по типу "If statement"
CxList ifStatements = plist.FindByType(typeof(IfStmt));

// Добавляем в результат, перед этим получив родительский узел - для правильного отображения
result = dictionarySettings.FindByFathers(ifStatements);

Задача: Поиск информации в XML

Решение: В Checkmarx есть очень удобные функции по работе с XML и поиском значений, тэгов, атрибутов и прочего. Но в документации, к сожалению, допущена ошибка из-за которой ни один пример не работает. Несмотря на то, что в последней версии документации этот недочет устранен — будьте внимательны, если используете более ранние версии документов.

Вот неправильный пример из документации:

// Код работать не будет
result = All.FindXmlAttributesByNameAndValue("*.app", 8, "id”, "error- section", false, true);

В результате попытки выполнения мы получим ошибку, что у All нет такого метода… И это верно, так как для использования функций для работы с XML есть специальное, отдельное пространство объектов — cxXPath. Вот как выглядит правильный запрос для поиска настройки в Android, разрешающей использование HTTP трафика:

// Правильный вариант с использованием cxXPath
result = cxXPath.FindXmlAttributesByNameAndValue("*.xml", 8, "cleartextTrafficPermitted", "true", false, true);

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

  • "*.xml"— маска файлов, по которым необходимо производить поиск

  • 8 — id языка, для которого применяется правило

  • "cleartextTrafficPermitted"— имя атрибута в xml

  • "true" — значение этого атрибута

  • false — использование регулярного выражения при поиске

  • true — означает, что поиск будет выполнен с игнорированием регистра, то есть case-insensitive

Для примера использовано правило, которое определяет некорректные, с точки зрения безопасности, настройки сетевого соединения в Android, которые разрешают общение с сервером посредством протокола HTTP. Пример настройки, содержащий атрибут cleartextTrafficPermitted со значением true:


    
        example.com
        
            
        
        
            secure.example.com
        
    

Задача: Ограничить результаты по имени/пути файла

Решение: В одном из больших проектов, связанных с разработкой мобильного приложения под Android, мы столкнулись с ложными срабатываниями правила, которое определяет настройку обфускации. Дело в том, что правило из коробки ищет в файле build.gradle настройку, отвечающую за применение правил обфускации для релизной версии приложения.

Но в больших проектах иногда встречаются дочерние файлы build.gradle, которые относятся к библиотекам, включенным в проект. Особенность в том, что даже если в этих файлах не указана необходимость обфускации, при компиляции будут применяться настройки родительского файла сборки.

Таким образом, задача стоит в отсечении срабатываний в дочерних файлах, которые относятся к библиотекам. Определить их возможно по наличию строки apply 'com.android.library'.

Пример кода из файла build.gradle, определяющий необходимость обфускации:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"
    defaultConfig {
        ...
    }

    buildTypes {
        release {
            minifyEnabled true
            ...
        }
    }
}

dependencies {
  ...
}

Пример файла build.gradle для библиотеки, включенной в проект и не имеющей такой настройки:

apply plugin: 'android-library'

dependencies {
  compile 'com.android.support:support-v4:18.0.+'
}

android {
  compileSdkVersion 14
  buildToolsVersion '17.0.0'
  ...
}

И правило для Checkmarx:

ProGuardObfuscationNotInUse

// Поиск метода release среди всех методов в Gradle файлах
CxList releaseMethod = Find_Gradle_Method("release");

// Все объекты из файлов build.gradle
CxList gradleBuildObjects = Find_Gradle_Build_Objects();

// Поиск того, что находится внутри метода "release" среди всех объектов из файлов build.gradle
CxList methodInvokesUnderRelease = gradleBuildObjects.FindByType(typeof(MethodInvokeExpr)).GetByAncs(releaseMethod);

// Ищем внутри gradle-файлов строку "com.android.library" - это значит, что данный файл относится к библиотеке и его необходимо исключить из правила
CxList android_library = gradleBuildObjects.FindByName("com.android.library");

// Инициализация пустого массива
List libraries_path = new List {};

// Проходим через все найденные "дочерние" файлы
foreach(CxList library in android_library)
{
    // Получаем путь к каждому файлу
	string file_name_library = library.GetFirstGraph().LinePragma.FileName;
    
    // Добавляем его в наш массив
	libraries_path.Add(file_name_library);
}

// Ищем все вызовы включения обфускации в релизных настройках
CxList minifyEnabled = methodInvokesUnderRelease.FindByShortName("minifyEnabled");

// Получаем параметры этих вызовов
CxList minifyValue = gradleBuildObjects.GetParameters(minifyEnabled, 0);

// Ищем среди них включенные
CxList minifyValueTrue = minifyValue.FindByShortName("true");

// Немного магии, если не нашли стандартным способом :D
if (minifyValueTrue.Count == 0) {
	minifyValue = minifyValue.FindByAbstractValue(abstractValue => abstractValue is TrueAbstractValue);
} else {
    // А если всё-таки нашли, то предыдущий результат и оставляем
	minifyValue = minifyValueTrue;	
}

// Если не нашлось таких методов
if (minifyValue.Count == 0)
{
    // Для более корректного отображения места срабатывания в файле ищем или buildTypes или android
	CxList tempResult = All.NewCxList();
	CxList buildTypes = Find_Gradle_Method("buildTypes");
	if (buildTypes.Count > 0) {
		tempResult = buildTypes;
	} else {
		tempResult = Find_Gradle_Method("android");
	}
	
	// Для каждого из найденных мест срабатывания проходим и определяем, дочерний или основной файлы сборки
	foreach(CxList res in tempResult)
	{
        // Определяем, в каком файле был найден buildType или android методы
		string file_name_result = res.GetFirstGraph().LinePragma.FileName;
        
        // Если такого файла нет в нашем списке "дочерних" файлов - значит это основной файл и его можно добавить в результат
		if (libraries_path.Contains(file_name_result) == false){
			result.Add(res);
		}
	}
}

Такой подход может быть достаточно универсальным и пригодится не только для Android приложений, но и для других случаев, когда нужно определять принадлежность результата к определенному файлу.

Задача: Добавить поддержку сторонней библиотеки, если синтаксис поддерживается не полностью

Решение: Количество всевозможных фреймворков, которые используются в процессе написания кода просто зашкаливает. Конечно, Checkmarx не всегда знает об их существовании и наша задача научить его понимать, что определенные методы относятся именно к этому фреймворку. Иногда это осложняется тем, что фреймворки используют названия функций, которые сильно распространены и нельзя однозначно определить отношение того или иного вызова к конкретной библиотеке.

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

  • Первый вариант, мы точно знаем, что библиотека используется в определенном проекте и можем применить правило на уровне команды. Но в случае, если команда решит использовать другой подход или использует несколько библиотек, в которых пересекаются названия функций мы можем получить не очень приятную картину из многочисленных ложных срабатываний

  • Второй вариант, применить поиск по файлам, в которых явно происходит импорт библиотеки. При таком подходе мы сможем быть уверенными, что в данном файле точно применяется нужная нам библиотека

  • И третий вариант, это использование двух вышеперечисленных подходов совместно.

В качестве примера разберем известную в узких кругах библиотеку slick для языка программирования Scala, а именно, функционал Splicing Literal Values. В общем случае, для передачи параметров в SQL-запрос необходимо использовать оператор $, который подставляет данные в предварительно сформированный SQL-запрос. То есть, по факту является прямым аналогом Prepared Statement в Java. Но, в случае необходимости динамически конструировать SQL-запрос, например, если нужно передавать имена таблиц, возможно использовать оператор #$, который напрямую подставит данные в запрос (практически, как конкатенация строк).

Пример кода:

// В общем случае - значения, контролируемые пользователем
val table = "coffees"
sql"select * from #$table where name = $name".as[Coffee].headOption

Checkmarx пока не умеет определять использование Splicing Literal Values и пропускает операторы #$, так что попробуем научить его определять потенциальные SQL-инъекции и подсвечивать нужные места в коде:

// Находим все импорты
CxList imports = All.FindByType(typeof(Import));

// Ищем по имени, есть ли в импортах slick
CxList slick = imports.FindByShortName("slick");

// Некоторый флаг, определяющий, что импорт библиотеки в коде присутствует
// Для более точного определения - можно применить подход с именем файла
bool not_empty_list = false;
foreach (CxList r in slick)
{
    // Если встретили импорт, считаем, что slick используется
	not_empty_list = true;
}

if (not_empty_list) {
    // Ищем вызовы, в которые передается SQL-строка
	CxList sql = All.FindByShortName("sql");
	sql.Add(All.FindByShortName("sqlu"));
	
	// Определяем данные, которые попадают в эти вызовы
	CxList data_sql = All.DataInfluencingOn(sql);
	
	// Так как синтакис не поддерживается, можно применить подход с регулярными выражениями
	// RegExp стоит использовать крайне осторожно и не применять его на большом количестве данных, так как это может сильно повлиять на производительность
	CxList find_possible_inj = data_sql.FindByRegex(@"\#\$", true, true, true);

    // Избавляемся от лишних срабатываний, если они есть и выводим в результат
	result = find_possible_inj.FindByType(typeof(BinaryExpr));
}

Задача: Поиск используемых уязвимых функций в Open-Source библиотеках

Решение: Во многих компаниях используются инструменты для контроля Open-Source (практика OSA), позволяющие обнаружить использование уязвимых версий библиотек в разрабатываемых приложениях. Иногда обновить такую библиотеку до безопасной версии не представляется возможным. В каких-то случаях есть функциональные ограничения, в других безопасной версии и вовсе нет. В таком случае поможет комбинация практик SAST и OSA, позволяющая определить, что функции, которые приводят к эксплуатации уязвимости, не используются в коде.

Но иногда, особенно, если рассматривать JavaScript, это может быть не совсем тривиальной задачей. Ниже представлено решение, возможно не идеальное, но тем не менее работающее, на примере уязвимостей в компоненте lodash в методах template и *set.

Примеры тестового потенциально уязвимого кода в JS файле:

/**
 * Template example
 */

'use strict';
var _ = require("./node_modules/lodash.js");


// Use the "interpolate" delimiter to create a compiled template.
var compiled = _.template('hello <%= js %>!');
console.log(compiled({ 'js': 'lodash' }));
// => 'hello lodash!'

// Use the internal `print` function in "evaluate" delimiters.

var compiled = _.template('<% print("hello " + js); %>!');
console.log(compiled({ 'js': 'lodash' }));
// => 'hello lodash!'

И при подключении напрямую в html:




    Lodash Tutorial
    
    













Ищем все наши уязвимые методы, которые перечислены в уязвимостях:

// Ищем все строки: в которых встречается строка lodash (предполагаем, что это объявление импорта библиотеки
CxList lodash_strings = Find_String_Literal().FindByShortName("*lodash*");

// Ищем все данные: которые взаимодействуют с этими строками
CxList data_on_lodash = All.InfluencedBy(lodash_strings);


// Задаем список уязвимых методов
List vulnerable_methods = new List {"template", "*set"};

// Ищем все наши уязвимые методы, которые перечисленны в уязвимостях и отфильтровываем их только там, где они вызывались
CxList vulnerableMethods = All.FindByShortNames(vulnerable_methods).FindByType(typeof(MethodInvokeExpr));

//Находим все данные: которые взаимодействуют с данными методами
CxList vulnFlow = All.InfluencedBy(vulnerableMethods);

// Если есть пересечение по этим данным - кладем в результат
result = vulnFlow * data_on_lodash;

// Формируем список путей по которым мы уже прошли, чтобы фильтровать в дальнейшем дубли
List lodash_result_path = new List {};

foreach(CxList lodash_result in result)
{
    // Очередной раз получаем пути к файлам
	string file_name = lodash_result.GetFirstGraph().LinePragma.FileName;
	lodash_result_path.Add(file_name);
}

// Дальше идет часть относящаяся к html файлам, так как в них мы не можем проследить откуда именно идет вызов
// Формируем массив путей файлов, чтобы быть уверенными, что срабатывания уязвимых методов были именно в тех файлах, в которых объявлен lodash
List lodash_path = new List {};
foreach(CxList string_lodash in lodash_strings)
{
	string file_name = string_lodash.GetFirstGraph().LinePragma.FileName;
	lodash_path.Add(file_name);
}

// Перебираем все уязвимые методы и убеждаемся, что они вызваны в тех же файлах, что и объявление/включение lodash
foreach(CxList method in vulnerableMethods)
{
	string file_name_method = method.GetFirstGraph().LinePragma.FileName;
	if (lodash_path.Contains(file_name_method) == true && lodash_result_path.Contains(file_name_method) == false){
		result.Add(method);
	}
}

// Убираем все UknownReferences и оставляем самый "длинный" из путей, если такие встречаются
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceSmallFlow) - result.FindByType(typeof(UnknownReference));

Задача: Поиск зашитых в приложение сертификатов

Решение: Нередко приложения, особенно мобильные, используют сертификаты или ключи для доступа к различным серверам или проверки SSL-Pinning. Если смотреть с точки зрения безопасности — хранить такие вещи в коде не самая лучшая практика. Попробуем написать правило, которое будет искать подобные файлы в репозитории:

// Найдем все сертификаты по маске файла
CxList find_certs = All.FindByShortNames(new List {"*.der", "*.cer", "*.pem", "*.key"}, false);

// Проверим, где в приложении они используются
CxList data_used_certs = All.DataInfluencedBy(find_certs);

// И для мобильных приложений - можем поискать методы, где вызывается чтение сертификатов
// Для других платформ и приложений могут быть различные методы
CxList methods = All.FindByMemberAccess("*.getAssets");

// Пересечение множеств даст нам результат по использованию локальных сертификатов в приложении
result = methods * data_used_certs;

Задача: Поиск скомпрометированных токенов в приложении

Решение: Нередко приходится отзывать скомпроментированные токены или другую важную информацию, которая присутствует в коде. Конечно, хранить их внутри исходников не самая хорошая идея, но ситуации бывают разные. Благодаря запросам CxQL найти такие вещи достаточно просто:

// Получаем все строки, которые содержатся в коде
CxList strings = base.Find_Strings();

// Ищем среди всех строк нужное нам значение. В примере токен в виде строки "qwerty12345"
result = strings.FindByShortName("qwerty12345");

Заключение

Надеюсь, что тем, кто начинает своё знакомство с инструментом Checkmarx будет полезна данная статья. Возможно и те, кто уже давно пишет свои правила, тоже найдут что-то полезное в этом руководстве.

К сожалению, сейчас очень не хватает ресурса, где можно было бы почерпнуть новые идеи в ходе разработки правил для Checkmarx. Поэтому мы создали репозиторий на Github, где будем выкладывать свои наработки, чтобы каждый, кто использует CxQL, смог найти в нем что-то полезное, а также имел возможность поделиться с сообществом своими трудами. Репозиторий в процессе наполнения и структурирования контента, так что contributions welcome!

Спасибо за внимание!

© Habrahabr.ru