Руководство по созданию расширений
для Svace 4.0.250829
Система пользовательских плагинов Svace предоставляет интерфейс для работы с унифицированным промежуточным представлением для языков C/C++, Java, Kotlin, Go; внутренними аттрибутами и системой выдачи ошибок.
Чтобы понять как написать свой плагин, разберем как устроен плагин
RedundantDivisionChecker, который находится в
plugins/redundant-division.
Замечание: как создать простой проект с gradle можно прочитать здесь.
Для реализации пользовательских расширений анализатор представляет
публичный интерфейс svace-api. Для успешной сборки и запуска
необходимо добавить lib/svace-api.jar в зависимости
проекта. Для того чтобы сделать это в системе gradle,
необходимо подключить плагин java-library и добавить
lib/svace-api.jar как внешний api:
plugins {
// Плагин для сборки кода на Java
id 'java-library'
}
repositories {
flatDir {
// Подключаем svace/lib как библиотечную директорию,
// чтобы иметь досутп к `lib/svace-api.jar`
dirs '../../lib'
}
}
dependencies {
// Добавляем зависимость
compileOnlyApi files(:svace-api)
}Svace подключает расширения с помощью системы модулей языка Java
(версии 9 и новее). Для того чтобы плагин был доступен анализатору,
необходимо добавить файл module-info.java внутрь папки
src и указать зависимость от svace-api, а также
указать класс реализующий плагин:
module ru.isp.svace.plugin.redundant.division {
requires svace.api;
provides SvaceLightPlugin with RedundantDivisionChecker;
}Структура папок расширения:
redundant-division
├── build.gradle
├── settings.gradle
└── src
├── module-info.java
└── ru.isp.svace.plugin.redundant.division
└── RedundantDivisionChecker.java
Для реализации плагина необходимо, чтобы класс плагина (в этом случае
RedundantDivisionChecker) расширял класс
SvaceLightPlugin и реализовывал три метода:
name, registerWarningType и
createChecker:
Метод name должен возвращать текстовое описание
плагина.
Метод registerWarningType используется, чтобы
создать типы предупреждений, которые могут выдавать детекторы
реализованные в плагине. Класс WarningTypeProperties
позволяет выполнить настройку типа предупреждения. В данном случае
WarningTypeProperties props =
WarningTypeProperties.create(Language.ANY).setVisibility(Visibility.Disabled);предупреждение может выдаваться для любого языка и отключено по
умолчанию. После чего используя параметр register
регистрируются сами предупреждения, а результат вызова сохраняется в
поле класса, чтобы использовать далее в методе
createChecker:
divisionByOne = register.registerWarning("DIVISION_BY_ONE_DEMO", props);Метод createChecker используется непосредственно для
описания логики детекторов.
Для создания детектора необходимо определить паттерн, при
срабатывании которого будет выдаваться предупреждение. Создание паттерна
происходит при помощи вызова методов класса
SvaceLightPlugin. Рассмотрим на примере создания паттерна
для детектора деления на единицу.
Оператор деления это либо оператор / либо функция
div_vec. Чтобы определить оператор деления, используется
паттерн entityType() с параметром соответствующим типу
оператора деления BinaryOperator.Division.type(), вызов
функции можно задать паттерном patternFuncCall("div_vec") и
объединить с помощью or т.к. выдавать предупреждение надо
на любой из них:
SvaceLightPattern divisionOperator = or(
entityType(BinaryOperator.Division.type()),
funcCall("div_vec"));Чтобы проверить возможные значения некоторой сущности,
используется паттерн interval. Для проверки равенства
значения в точности единице interval(SvaceCond.Equals, 1).
Чтобы применить этот паттерн к первому (нумерация с нуля) аргументу
деления можно использовать arg следующим образом:
SvaceLightPattern divisorEqualsToOne = arg(1, interval(SvaceCond.Equals, 1));Остаётся скомбинировать паттерны из пунктов 1 и 2 при помощи
and чтобы искать операторы деления и операторы,
где первый аргумент равен единице. Для создания детектора полученный
паттерн и созданный ранее тип предупреждения необходимо передать методу
registerChecker. Дополнительно этот метод также требует
строку сообщения для предупреждения (таким образом используя разные
паттерны можно выдавать разные сообщения для одного типа
предупреждений):
SvaceLightPattern divisionPattern = and(divisionOperator, divisorEqualsToOne);
registerChecker(divisionByOne, "Division by one is redundant.", divisionPattern);При создании этого детектора мы оперировали двумя понятиями: сущности и паттерны.
При анализе кода каждая инструкция промежуточного представления
порождает набор сущностей. В SvaceAPI эти сущности представляются
иерархией классов CodeEntity. Для каждой инструкции
создаётся одна сущность типа Operator, соответствующая типу
инструкции и несколько сущностей типа Operand
соотвтетствующие аргументам. Т.к. сущности образуют иерархию можно
использовать паттерны entityType чтобы покрывать сразу
множество сущностей с общими свойствами и в дальнейшем по мере
расширения API новыми сущнстями детектор будет обрабатывать их без
дополнительного вмешательства.
Паттерны это предикаты, применяемые к каждой сущности инструкции, и
оператору, и опреанду. Паттерн arg позволяет обрабатывать
зависимости между ними. Стоит отметить, что паттерны могут не только
обрабатывать информацию непосредственно пресутствующую в сущности, но и
некоторые свойства построенные анализом Svace. Так например паттерн
interval способны не только определять величину констант,
но и использовать междпроцедурный аттрибут движка Svace, который может
вычислять возможный интервал значений в существенно более сложных
случаях. Все возможные паттерны это методы класса
SvaceLightPlugin возвращающие тип
SvaceLightPattern.
Детекторы созданные с использованием только паттернов ограничены поиском ошибок локализованных в одной инструкции. Чтобы находить дефекты происходящии в результате выполнения нескольких инструкций (или даже нескольких функций) SvaceAPI предоставляет возможность создавать пользовательские аттрибуты.
Аттрибут это способ представления информации о некотором значении в
рамках анализа. Аттрибут можно выставить во время
анализа одной инструкции и позже обнаружить с помощью паттерна при
анализе другой инструкции использующей то же самое значение. Svace
автоматически распостраняет выставленные аттрибуты по потоку данных
програм, например при присвоении в переменные. В предыдущем примере
использовался паттерн interval. Он использует внутренний
аттрибут Svace представляющий информацию о возможном интервале
целочисленных значений. Точно так же SvaceAPI предоставляет возможность
создавать пользовательски аттрибуты.
Рассмотрим пример детектора для языка python находящего вызовы к
функции requests.get использующие протокол
http:// вместо безопасного https://, например
requests.get('http://example.com/api'). Это возможная
уязвимость при использовании внешнего API т.к. при использовании http
возможна утечка секретов при перехвате незашифрованного трафика.
Самый простой детектор можно создать используя следующий паттерн:
var httpUrl = tokenRegex("http://.*", Constant.String.type());
var badGet = funcCall("requests.get", arg(0, httpUrl));Он будет обнаруживать вызовы в роде
requests.get('http://example.com/api') но не справится с
более сложными примерами, где аргумент это не константа
if condition:
url = 'http://example.com/api'
else:
url = 'https://example2.com/api'
requests.get(url)а одно из 2‑х возможных значений (одно из которых использует протокол
http://). Другой пример это вызов метода
str.format:
url = 'http://127.0.1.1:5000/api/post/{}'.format(username)
requests.get(url)здесь строковая константа не используется напрямую в
requests.get но очевидно что метод str.format
не изменит того, что был использован протокол
http://.
Чтобы улучшить детектор, воспользуемся механизмом аттрибутов. Сперва определим аттрибут и его свойства:
var httpUsed = makeAttribute("httpUsed", false, (left, right) -> left || right);Для определения аттрибута необходимо задать имя, значение по
умолчанию и join-функцию. Значение по умолчанию присваивается в случае
если значение не было присвоенно с помощью
registerAttributeRule. Join-функция используется когда в
ходе анализа необходимо согласовать аттрибуты с разных путей выполения.
В примере выше переменной url было присвоено два разных
значения в разных ветвях if. Т.к. используется join-функция
“или”, то детектор обнаружит проблему потому что при выполнении хотя бы
одной из двух ветвей if может возникнуть эта проблема. И наоборот если
использовать функцию “и”, то аттрибут будет истинным только если он
истинен на всех путях выполения.
Далее напишем правило выставляющее аттрибут httpUsed в
истинное состояние для любой строковой константы содержащей подстроку
http://. Для этого необходимо создать паттерн срабатывающий
на сущность ConstantDefinition:
var httpUrlDefinition = constantDefinition(tokenRegex("http://.*", Constant.String.type()));
registerAttributeRule(httpUsed, true, ApplyTo.result(), httpUrlDefinition);registerAttributeRule принимает три аргумента - аттрибут
созданый ранее, значение в которое выставляется аттрибут, на какую
переменную выставляется аттрибут (может быть результат, как в этом
случае, this/self вызова метода или один из
аргументов) и паттерн при срабатывании которого будет выставляться
аттрибут.
Наконец используем паттерн attributeIs чтобы проверить
аттрибут при вызове requests.get и выдать ошибку:
var badGet = funcCall("requests.get", 0, attributeIs(httpUsed, true));
registerChecker(badRequestWarning, "Don't request via http", badGet);Рассмотрим ближе как этот детектор работает на примере с
if:
if condition:
url = 'http://example.com/api' # httpUsed для url = true
else:
url = 'https://example2.com/api' # httpUsed для url = false (значение по умолчанию)
# httpUsed для url = true || false = true, использована join-функция
requests.get(url) # httpUsed для url = true, функция называется requests.get, выдано предупреждениездесь выбор join-функции “или” сыграл ключевую роль. Хотя только при
выполнении первой ветки присваивается url с протоколом
http://, а при выполнении другой нет в итоге значение
аттрибута всё равно истинное. Если не желательно находить ошибку в такой
ситуации (например мы предполагаем, что наличие условия свидетельствует
о том, что разработчик сознательно написал этот код, например для
тестирования и т.п.), то можно применить join-функцию “и”
(left, right) -> left && right и предупреждение
не будет выдано.
Чтобы выдавать предупреждение при использовании
str.format добавим ещё одно правило:
var formatString = funcCall("format", thisArg(attributeIs(httpUsed, true)));
registerAttributeRule(httpUsed, true, ApplyTo.result(), formatString);это правило проверяет что для аргумента
this/self выставлен аттрибут
httpUsed и выставляет его заново на возвращаемое значение.
В ранее описанно примере это изменение сработает следующим образом:
url = 'http://127.0.1.1:5000/api/post/{}' # паттерн httpUrlDefinition подходит, httpUsed для url = true
url = url.format(username) # паттерн formatString подходит, httpUsed для url = true
requests.get(url) # httpUsed для url = true, функция называется requests.get, выдано предупреждение