Як напісаць правілы для 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-прыкладаннях выкарыстоўваецца 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:

<?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 are welcome!

Дзякуй за ўвагу!

Крыніца: habr.com

Дадаць каментар