Ինչպես գրել կանոններ Checkmarx-ի համար՝ առանց խելագարվելու

Հե՜յ Հաբր։

Մեր աշխատանքում մեր ընկերությունը շատ հաճախ զբաղվում է ստատիկ կոդի վերլուծության տարբեր գործիքներով (SAST): Տուփից դուրս նրանք բոլորն էլ միջին հաշվով աշխատում են: Իհարկե, ամեն ինչ կախված է նախագծից և դրանում կիրառվող տեխնոլոգիաներից, ինչպես նաև, թե որքանով են այդ տեխնոլոգիաները ծածկված վերլուծության կանոններով։ Իմ կարծիքով, SAST գործիք ընտրելիս ամենակարևոր չափանիշներից մեկն այն հարմարեցնելու ունակությունն է ձեր հավելվածների առանձնահատկություններին, մասնավորապես՝ գրել և փոխել վերլուծության կանոնները կամ, ինչպես դրանք ավելի հաճախ կոչվում են, Custom Queries:

Ինչպես գրել կանոններ Checkmarx-ի համար՝ առանց խելագարվելու

Մենք ամենից հաճախ օգտագործում ենք Checkmarx-ը` շատ հետաքրքիր և հզոր կոդերի անալիզատոր: Այս հոդվածում ես կխոսեմ դրա համար վերլուծության կանոններ գրելու իմ փորձի մասին:

Պահեստավորված նյութեր

Մուտք

Սկսելու համար, ես կցանկանայի խորհուրդ տալ ռուսերենով մի քանի հոդվածներից մեկը Checkmarx-ի համար հարցումներ գրելու առանձնահատկությունների մասին: Այն հրապարակվել է Habré-ում 2019 թվականի վերջին՝ վերնագրով. «Բարև, Չեկմարքս»: Ինչպես գրել Checkmarx SAST հարցում և գտնել հիանալի խոցելիություններ.

Այն մանրամասնորեն ուսումնասիրում է, թե ինչպես կարելի է գրել առաջին հարցումները CxQL-ում (Checkmarx Query Language) որոշ թեստային հավելվածի համար և ցույց է տալիս վերլուծության կանոնների աշխատանքի հիմնական սկզբունքները:

Չեմ կրկնի այն, ինչ նկարագրված է դրանում, թեև որոշ խաչմերուկներ դեռ կլինեն։ Իմ հոդվածում ես կփորձեմ կազմել մի տեսակ «բաղադրատոմսերի հավաքածու», կոնկրետ խնդիրների լուծումների ցանկ, որոնք ես հանդիպել եմ Checkmarx-ի հետ աշխատանքի ընթացքում: Ես ստիպված էի ուղեղս խառնել այս խնդիրներից շատերի համար: Երբեմն փաստաթղթերում բավարար տեղեկատվություն չկար, երբեմն նույնիսկ դժվար էր հասկանալ, թե ինչպես անել այն, ինչ պահանջվում է: Հուսով եմ, որ իմ փորձառությունն ու անքուն գիշերներն իզուր չեն անցնի, և այս «Պատվիրված հարցումների բաղադրատոմսերի հավաքածուն» կփրկի ձեզ մի քանի ժամ կամ մի քանի նյարդային բջիջ: Այսպիսով, եկեք սկսենք:

Ընդհանուր տեղեկություններ կանոնների մասին

Նախ, եկեք նայենք մի քանի հիմնական հասկացություններին և կանոնների հետ աշխատելու գործընթացին, ավելի լավ հասկանալու համար, թե ինչ կլինի հետո: Եվ նաև այն պատճառով, որ փաստաթղթերը ոչինչ չեն ասում այս մասին կամ շատ տարածված են կառուցվածքում, ինչը այնքան էլ հարմար չէ:

  1. Կանոնները կիրառվում են սկանավորման ժամանակ՝ կախված սկզբում ընտրված նախադրյալից (ակտիվ կանոնների մի շարք): Դուք կարող եք ստեղծել անսահմանափակ թվով նախադրյալներ, և թե ինչպես դրանք կառուցեք, կախված է ձեր գործընթացի առանձնահատկություններից: Դուք կարող եք դրանք խմբավորել ըստ լեզվի կամ ընտրել նախադրյալներ յուրաքանչյուր նախագծի համար: Ակտիվ կանոնների քանակը ազդում է սկանավորման արագության և ճշգրտության վրա:

    Ինչպես գրել կանոններ Checkmarx-ի համար՝ առանց խելագարվելուՆախադրված կարգավորումը Checkmarx ինտերֆեյսում

  2. Կանոնները խմբագրվում են CxAuditor կոչվող հատուկ գործիքում: Սա աշխատասեղանի հավելված է, որը միանում է Checkmarx աշխատող սերվերին: Այս գործիքն ունի աշխատանքի երկու եղանակ՝ կանոնների խմբագրում և արդեն կատարված սկանավորման արդյունքների վերլուծություն:

    Ինչպես գրել կանոններ Checkmarx-ի համար՝ առանց խելագարվելուCxAudit ինտերֆեյս

  3. Checkmarx-ի կանոնները բաժանված են ըստ լեզվի, այսինքն՝ յուրաքանչյուր լեզու ունի հարցումների իր փաթեթը: Կան նաև որոշ ընդհանուր կանոններ, որոնք գործում են անկախ լեզվից, դրանք այսպես կոչված հիմնական հարցումներն են։ Հիմնական հարցումները ներառում են տեղեկությունների որոնում, որն օգտագործում են այլ կանոններ:

    Ինչպես գրել կանոններ Checkmarx-ի համար՝ առանց խելագարվելուԿանոնների բաժանում ըստ լեզվի

  4. Կանոններն են՝ «Կատարվող» և «Չկատարվող» (Կատարված և Չկատարված): Ոչ այնքան ճիշտ անունը, իմ կարծիքով, բայց դա այն է, ինչ կա: Ներքևի տողն այն է, որ «Գործարկվող» կանոնների կատարման արդյունքը կցուցադրվի UI-ի սկանավորման արդյունքներում, իսկ «Ոչ կատարվող» կանոնները անհրաժեշտ են միայն դրանց արդյունքներն այլ հարցումներում օգտագործելու համար (իրականում դրանք պարզապես գործառույթ են։ ).

    Ինչպես գրել կանոններ Checkmarx-ի համար՝ առանց խելագարվելուՍտեղծելիս կանոնի տեսակի որոշում

  5. Դուք կարող եք ստեղծել նոր կանոններ կամ լրացնել/վերագրել գոյություն ունեցողները: Կանոնը վերաշարադրելու համար հարկավոր է գտնել այն ծառի մեջ, սեղմել աջը և բացվող ընտրացանկից ընտրել «Override»: Այստեղ կարևոր է հիշել, որ նոր կանոններն ի սկզբանե ներառված չեն նախադրյալների մեջ և ակտիվ չեն: Դրանք օգտագործելու համար անհրաժեշտ է դրանք ակտիվացնել գործիքի «Նախադրված կառավարիչ» ցանկում: Վերագրված կանոնները պահպանում են իրենց կարգավորումները, այսինքն՝ եթե կանոնը ակտիվ էր, այն կմնա այդպիսին և անմիջապես կկիրառվի։

    Ինչպես գրել կանոններ Checkmarx-ի համար՝ առանց խելագարվելուՆախադրված կառավարչի ինտերֆեյսի նոր կանոնի օրինակ

  6. Կատարման ընթացքում կառուցվում է հարցումների «ծառ», որը կախված է նրանից, թե ինչից: Կանոնները, որոնք հավաքում են տեղեկատվություն, առաջին հերթին կիրառվում են, իսկ նրանք, ովքեր օգտագործում են այն երկրորդում: Կատարման արդյունքը պահվում է քեշում, այնպես որ, եթե հնարավոր է օգտագործել գոյություն ունեցող կանոնի արդյունքները, ապա ավելի լավ է դա անել, դա կնվազեցնի սկանավորման ժամանակը:

  7. Կանոնները կարող են կիրառվել տարբեր մակարդակներում.

  • Ամբողջ համակարգի համար - կօգտագործվի ցանկացած նախագծի ցանկացած սկանավորման համար

  • Թիմի մակարդակով (Թիմ) - կօգտագործվի միայն ընտրված թիմում նախագծերը սկանավորելու համար:

  • Ծրագրի մակարդակով - Կկիրառվի կոնկրետ նախագծում

    Ինչպես գրել կանոններ 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");

Գործառույթներ հոսքի վերլուծության համար

Այս գործառույթները օգտագործվում են բազմաթիվ կանոններում, և ահա մի փոքր խաբեբայական թերթիկ, թե ինչ են դրանք նշանակում.

// Какие данные 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");

Բայց, կատարման արդյունքը նշանակելով կախարդական փոփոխական արդյունքին, մենք կտեսնենք, թե ինչ է մեզ վերադարձնում այս զանգը.

// Находим элементы 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 և այլ անկանխատեսելի իրավիճակներ, որոնք դուրս են մեր վերահսկողությունից: Այս դեպքում երբեմն տվյալների բազայում անավարտ նիստ է լինում, որը խանգարում է նորից մուտք գործել։ Դա շտկելու համար դուք պետք է կատարեք մի քանի հարցումներ.

8.6-ից առաջ Checkmarx-ի համար.

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

8.6-ից հետո Checkmarx-ի համար.

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

Գրելու կանոններ

Այժմ մենք անցնում ենք ամենահետաքրքիր հատվածին. Երբ սկսում եք կանոններ գրել CxQL-ում, այն, ինչ ձեզ հաճախ պակասում է, ոչ այնքան փաստաթղթավորումն է, որքան որոշ խնդիրներ լուծելու և ընդհանրապես հարցումների գործարկման գործընթացը նկարագրելու կենդանի օրինակներ:

Ես կփորձեմ կյանքը մի փոքր ավելի հեշտացնել նրանց համար, ովքեր սկսում են սուզվել հարցումների լեզվի մեջ և բերելու մի քանի օրինակներ՝ օգտագործելով Custom Queries-ը որոշակի խնդիրներ լուծելու համար: Դրանցից մի քանիսը բավականին ընդհանուր են և կարող են օգտագործվել ձեր ընկերությունում գործնականում առանց փոփոխությունների, մյուսները՝ ավելի կոնկրետ, բայց դրանք կարող են օգտագործվել նաև՝ փոխելով կոդը՝ ձեր հավելվածների առանձնահատկություններին համապատասխան:

Այսպիսով, ահա այն խնդիրները, որոնց մենք առավել հաճախ բախվել ենք.

Առաջադրանք. Կանոնների կատարման արդյունքներում կան մի քանի հոսքեր, և դրանցից մեկը մյուսի բույնն է, դուք պետք է թողնեք դրանցից մեկը:

լուծում: Իրոք, երբեմն Checkmarx-ը ցույց է տալիս մի քանի տվյալների հոսքեր, որոնք կարող են համընկնել և լինել այլերի կրճատված տարբերակը: Նման դեպքերի համար կա հատուկ մեթոդ ReduceFlow. Կախված պարամետրից, այն կընտրի ամենակարճ կամ ամենաերկար հոսքը.

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

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

Առաջադրանք. Ընդլայնել զգայուն տվյալների ցանկը, որոնց արձագանքում է գործիքը

լուծում: Checkmarx-ն ունի հիմնական կանոններ, որոնց արդյունքներն օգտագործվում են բազմաթիվ այլ հարցումների կողմից։ Այս կանոններից մի քանիսը լրացնելով ձեր հավելվածին հատուկ տվյալների հետ՝ դուք կարող եք անմիջապես բարելավել ձեր սկանավորման արդյունքները: Ստորև բերված է օրինակելի կանոն՝ սկսելու համար.

Ընդհանուր_գաղտնիության_խախտման_ցուցակ

Եկեք ավելացնենք մի քանի փոփոխականներ, որոնք օգտագործվում են մեր հավելվածում՝ զգայուն տեղեկատվությունը պահելու համար.

// Получаем результат выполнения базового правила
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());

Առաջադրանք. iOS նախագծերի համար plist-ում զգայուն տվյալների որոնում

լուծում: iOS-ը հաճախ օգտագործում է հատուկ ֆայլեր .plist ընդլայնմամբ՝ տարբեր փոփոխականներ և արժեքներ պահելու համար: Այս ֆայլերում գաղտնաբառեր, նշաններ, ստեղներ և այլ զգայուն տվյալներ պահելը խորհուրդ չի տրվում, քանի որ դրանք կարող են առանց որևէ խնդիրների դուրս հանվել սարքից:

Plist ֆայլերն ունեն առանձնահատկություններ, որոնք ակնհայտ չեն անզեն աչքով, բայց կարևոր են Checkmarx-ի համար: Եկեք մի կանոն գրենք, որը կփնտրի մեզ անհրաժեշտ տվյալները և կասի, թե արդյոք ինչ-որ տեղ նշված են գաղտնաբառեր կամ նշաններ:

Նման ֆայլի օրինակ, որը պարունակում է հետին ծառայության հետ կապի նշան.

<?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 — նշանակում է, որ որոնումը կիրականացվի անտեսելով մեծատառը, այսինքն՝ մեծատառերի անզգայուն

Որպես օրինակ, մենք օգտագործեցինք կանոն, որը նույնացնում է սխալ, անվտանգության տեսանկյունից, ցանցային կապի կարգավորումները 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 ծրագրավորման լեզվի համար, մասնավորապես, ֆունկցիոնալությունը Բառացի արժեքների միացում. Ընդհանուր առմամբ, SQL հարցումին պարամետրեր փոխանցելու համար դուք պետք է օգտագործեք օպերատորը $, որը փոխարինում է տվյալները նախապես ձևավորված SQL հարցման մեջ: Այսինքն, ըստ էության, այն Java-ում Prepared Statement-ի ուղղակի անալոգն է: Բայց, եթե ձեզ անհրաժեշտ է դինամիկ ձևավորել 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));
}

Առաջադրանք. Փնտրեք օգտագործված խոցելի գործառույթները բաց կոդով գրադարաններում

լուծում: Շատ ընկերություններ օգտագործում են բաց կոդով մոնիտորինգի գործիքներ (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-ից, կարողանա գտնել դրա մեջ ինչ-որ օգտակար բան, ինչպես նաև հնարավորություն ունենա կիսվել իր աշխատանքով համայնքի հետ։ Պահեստը բովանդակության լրացման և կառուցվածքի գործընթացի մեջ է, ուստի ներդրողները ողջունելի են:

Շնորհակալություն ձեր ուշադրության համար:

Source: www.habr.com

Добавить комментарий