Як написати правила для Checkmarx і не збожеволіти

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

У своїй роботі наша компанія часто має справу з різними інструментами статичного аналізу коду (SAST). З коробки всі вони працюють середньо. Звичайно, все залежить від проекту і технологій, що використовуються в ньому, а також, наскільки добре ці технології покриваються правилами аналізу. На мій погляд, одним із найголовніших критеріїв при виборі інструменту SAST є можливість налаштовувати його під особливості своїх додатків, а саме писати та змінювати правила аналізу або, як їх найчастіше називають, Custom Queries.

Як написати правила для Checkmarx і не збожеволіти

Ми найчастіше використовуємо Checkmarx – дуже цікавий та потужний аналізатор коду. У цій статті розповім про свій досвід написання правил аналізу для нього.

Зміст

Вступ

Для початку, я хотів би порекомендувати одну з небагатьох статей російською мовою про особливості написання запитів для Checkmarx. Вона була опублікована на Хабрі наприкінці 2019 року під заголовком: "Hello, Checkmarx!". Як написати запит для Checkmarx SAST та знайти круті вразливості.

У ній докладно розглянуто, як написати перші запити мовою CxQL (Checkmarx Query Language) для деякого тестового додатку та показані основні засади роботи правил аналізу.

Я не повторюватиму те, що в ній описано, хоча деякі перетину все-таки будуть присутні. У своїй статті я намагатимусь скласти деяку “збірку рецептів”, перелік рішень конкретних завдань, з якими я стикався за час роботи з Checkmarx. Над багатьма з цих завдань мені довелося неабияк поламати голову. Часом не вистачало даних у документації, а часом взагалі важко було зрозуміти, як зробити те, що потрібно. Сподіваюся, мій досвід і безсонні ночі не пропадуть даремно, і ця "збірка Custom Queries рецептів" заощадить вам кілька годин або пару-трійку нервових клітин. Тож почнемо!

Загальна інформація щодо правил

Для початку розглянемо кілька базових понять та процес роботи з правилами, для кращого розуміння, що відбуватиметься далі. І ще тому, що в документації про це не сказано чи сильно розмазано структурою, що не дуже зручно.

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

    Як написати правила для Checkmarx і не збожеволітиНалаштування Preset в інтерфейсі Checkmarx

  2. Правила редагуються у спеціальному інструменті під назвою CxAuditor. Це десктопна програма, яка підключається до сервера з Checkmarx. Цей інструмент має два режими роботи: редагування правил та аналіз результатів вже проведеного сканування.

    Як написати правила для Checkmarx і не збожеволітиІнтерфейс CxAudit

  3. Правила Checkmarx розділені за мовами, тобто для кожної мови існує свій набір запитів. Також є деякі загальні правила, які застосовуються незалежно від мови, це звані базові запити. Здебільшого базові запити містять у собі пошук інформації, яку використовують інші правила.

    Як написати правила для Checkmarx і не збожеволітиПоділ правил з мов

  4. Правила бувають “Executable” та “Non-Executable” (Виконані та Не виконувані). Не зовсім коректна назва, як на мене, але що є. Суть у тому, що результат виконання Executable правил буде відображено в результатах сканування в UI, а Non-Executable правила потрібні тільки для використання їх результатів в інших запитах (по суті просто функція).

    Як написати правила для Checkmarx і не збожеволітиВизначення типу правила під час створення

  5. Можна створювати нові правила або доповнювати/переписувати існуючі. Для того, щоб переписати правило, потрібно знайти його в дереві, натиснути правою кнопкою і в меню вибрати пункт “Override“. Тут важливо пам'ятати, нові правила спочатку не включені в пресети і не активні. Щоб почати їх використовувати, потрібно активувати їх у меню “Preset Manager” в інструменті. Переписані правила зберігають свої налаштування, тобто якщо правило було активно, таким воно і залишиться і буде застосовуватися відразу.

    Як написати правила для Checkmarx і не збожеволітиПриклад нового правила в інтерфейсі Preset Manager

  6. Під час виконання будується “дерево” запитів, що від цього залежить. Першими виконуються правила, які збирають інформацію, другим ті, хто її використовує. Результат виконання кешується, тому якщо є можливість використовувати результати існуючого правила, то краще так і зробити, це дозволить зменшити час сканування.

  7. Правила можна застосовувати на різних рівнях:

  • Для всієї системи буде використано для будь-якого сканування будь-якого проекту

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

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

    Як написати правила для Checkmarx і не збожеволітиВизначення рівня, на якому застосовуватиметься правило

"Словник" для початківця

І почну я з кількох речей, які викликали у мене питання, а також покажу низку прийомів, які суттєво спростять життя.

Операції зі списками

- вычитание одного из другого (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<string> {
	"*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<string>{"*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<string>{"*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-додатках використовується Менеджер роботи для асинхронної роботи, непогано додатково повідомити про це 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:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>DeviceDictionary</key>
	<dict>
		<key>phone</key>
		<string>iPhone 6s</string>
	</dict>
	<key>privatekey</key>
	<string>MIICXAIBAAKBgQCqGKukO1De7zhZj6+</string>
</dict>
</plist>

І правило для 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:

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <trust-anchors>
            <certificates src="@raw/my_ca"/>
        </trust-anchors>
        <domain-config cleartextTrafficPermitted="true">
            <domain includeSubdomains="true">secure.example.com</domain>
        </domain-config>
    </domain-config>
</network-security-config>

Завдання: Обмежити результати по імені/шляху файлу

Рішення: В одному з великих проектів, пов'язаних з розробкою мобільного додатку під 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<string> libraries_path = new List<string> {};

// Проходим через все найденные "дочерние" файлы
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 не завжди знає про їхнє існування і наше завдання навчити його розуміти, що певні методи відносяться саме до цього фреймворку. Іноді це ускладнюється тим, що фреймворки використовують назви функцій, які дуже поширені і не можна однозначно визначити ставлення того чи іншого виклику до конкретної бібліотеки.

Складність полягає в тому, що синтаксис таких бібліотек не завжди коректно розпізнається і доводиться експериментувати, щоб не отримати багато хибних спрацьовувань. Існує кілька варіантів, щоб покращити точність сканування та вирішити поставлене завдання:

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

  • Другий варіант, застосувати пошук файлів, в яких явно відбувається імпорт бібліотеки. При такому підході ми зможемо бути впевненими, що в даному файлі застосовується потрібна нам бібліотека

  • І третій варіант, це використання двох перерахованих вище підходів спільно.

Як приклад розберемо відому у вузьких колах бібліотеку гладкий для мови програмування 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:

<!DOCTYPE html>
<html>
<head>
    <title>Lodash Tutorial</title>
    <script src="./node_modules/lodash.js"></script>
    <script type="text/javascript">
  // Lodash chunking array
        nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];

        let c1 = _.template('<% print("hello " + js); %>!');
        console.log(c1);

        let c2 = _.template('<% print("hello " + js); %>!');
        console.log(c2);
    </script>
</head>
<body></body>
</html>

Шукаємо всі наші вразливі методи, які перераховані в уразливості:

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

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


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

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

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

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

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

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

// Дальше идет часть относящаяся к html файлам, так как в них мы не можем проследить откуда именно идет вызов
// Формируем массив путей файлов, чтобы быть уверенными, что срабатывания уязвимых методов были именно в тех файлах, в которых объявлен lodash
List<string> lodash_path = new List<string> {};
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<string> {"*.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, зміг знайти в ньому щось корисне, а також мав можливість поділитися з спільнотою своїми працями. Репозиторій у процесі наповнення та структурування контенту, так що contributors є welcome!

Дякуємо за увагу!

Джерело: habr.com

Додати коментар або відгук