В следующем обновлении Svace мы существенно расширим возможности для создания пользовательских детекторов с помощью SvaceAPI.
За годы разработки мы добавили в Svace большое количество детекторов, находящие ошибки самых разных типов, среди которых:
Тем не менее этот список будет всегда оставаться неполным. Многие проекты используют библиотеки, которые не поддерживаются в Svace, вводят свои правила написания кода или используют различные стандарты безопасной разработки. Хотя наша команда всегда стремится расширить возможности Svace, часто нецелесообразно пытаться добавить новые детекторы в Svace по ряду причин:
Это оставляет разработчикам по сути один вариант — ручной поиск ошибок на код-ревью, что, конечно, тоже важная часть совместной разработки, но не позволяет добиться таких же результатов, чем использование статического анализа.
Чтобы решить эту проблему, мы создали SvaceAPI — инструмент, позволяющий нашим пользователям создавать свои детекторы, используя продвинутый внутри- и межпроцедурный анализ Svace для всех поддерживаемых в рамках основного анализа языках.
Создавая SvaceAPI мы хотели создать гибкий API с широкими возможностями, удобный для конечных пользователей. Но также важным было обеспечить высокую производительность на уровне основного анализа Svace. Для этого мы решили разработать поддержку плагинов на языке Java, интегрируемые с основным анализом Svace с помощью системы модулей Java. Эта система позволяет основному анализу Svace во время работы динамически загружать сторонний код и запускать пользовательские детекторы. Чтобы реализовать детектор, необходимо расширить класс SvaceLightPlugin
и реализовать три метода. Для сборки плагинов нужно подключить только библиотеку svace-api.jar
(для удобства рекомендуется использовать систему сборки gradle, подробности содержатся в документации Svace).
Основываясь на нашем опыте разработки статического анализатора, мы выделили следующие качества, которыми должен был удовлетворять хороший API для пользовательских детекторов:
Абстрагирование от деталей реализации отдельных языков.
Хотя и невозможно полностью исключить различия между языками программирования, многие конструкции удаётся унифицировать. Более того, можно унифицировать и сам формат представления инструкций. В SvaceAPI используется SSA-форма из унифицированных инструкций — сущностей. Они представляются в виде иерархии интерфейсов, наследуемых от CodeEntity
. Сущности делятся на две широкие категории — операторы (примерно эквивалентны инструкциям) и их операнды (аргументы). Например, выражение b = 10 / a
представляется оператором BinaryOperator.Division
с возвращаемым значением Variable
и двумя аргументами: Constant.Integer
и Variable
.
Декларативный и самоописывающий API.,
Чтобы упростить разработку и поддержку детекторов, мы решили отойти от традиционной схемы с обработчиками операторов и использовать более декларативный подход. Основным инструментом при создании детектора являются паттерны — выражения, описывающие свойства отдельных сущностей. Например:
entityType(BinaryOperator.Division.type())
— что соответствует оператору деления;interval(SvaceCond.Equals, 1)
проверит, что интервал возможных значений операнда равен единице;Комбинируя паттерны, можно выражать более сложные свойства:
arg
позволит проверить, что аргумент оператора соответствует какому-то паттерну arg(1, interval(SvaceCond.Equals, 1))
— проверка, что второй аргумент (нумерация с 0) равен единице;and
и двух предыдущих паттернов можно составить паттерн находящий деление на 1: and(entityType(BinaryOperator.Division.type()), arg(1, interval(SvaceCond.Equals, 1)))
.Доступ к анализам Svace.
В Svace уже реализовано достаточное количество сложных анализов, результаты которых можно использовать в детекторах SvaceAPI. Например, паттерн interval
(из предыдущего пункта) использует межпроцедурный анализ интервалов, реализованный в Svace. Более того, используя механизм SvaceAPI, можно создавать свои атрибуты — формальные описания свойств значений в программе, которыми оперирует анализ Svace. Используя аккуратно составленные атрибуты, детекторы SvaceAPI могут использовать межпроцедурный анализ Svace и находить ошибки, анализируя поток данных между различными функциями и модулями программы.
Давайте разработаем простейший детектор для поиска в кодовой базе вызовов функции requests.get
с использованием протокола http
.
Это хороший пример применимости SvaceAPI. Во многих приложениях использование протокола http
абсолютно оправданно: запросы могут заведомо происходить в рамках одного сервера или одной сети, или не содержать никаких чувствительных данных. Но для отдельных проектов, где нет этих предположений ограничить использование http
оправданно.
Как уже было сказано, плагины создаются на языке Java, подробная инструкция о том, как настроить проект и среду разработки есть в документации SvaceAPI. Отдельно отметим, что при использовании IDE будет доступна документация к отдельным классам и методам SvaceAPI.
Начнём с создания детектора для самого простого случая:
requests.get('http://example.com')
Для этого создадим класс InsecureGetPlugin
, реализующий интерфейс SvaceLightPlugin
, в том числе методы name
с человекочитаемым именем плагина, registerWarningType
, где создадим предупреждение BAD_REQUEST
, и пустой метод createChecker
, где и будет описана логика детектора:
public class InsecureGetPlugin extends SvaceLightPlugin {
private SvaceUserWarning insecureGetWarning = null;
@Override
public String name() {
return "Insecure get checker";
}
@Override
public void registerWarningType(SvaceUserWarningRegister register) {
insecureGetWarning = register.registerWarning(
"INSECURE_GET",
Visibility.Enabled,
Language.PYTHON
);
}
@Override
public void createChecker() {
// Осталось только написать правила для детектора здесь
}
}
Остаётся написать правила для детектора внутри метода createChecker
. Для составления паттерна для случая requests.get('http://example.com')
нам потребуется проверить, что вызвалась функция и её имя requests.get
:
var badGet = and(
entityType(FunctionCall.type()),
tokenRegex("requests.get")
);
И что её 0 аргумент — это строка соответствующая регулярному выражению http://.*
:
var badGet = and(
entityType(FunctionCall.type()),
tokenRegex("requests.get"),
arg(0, and(
entityType(Constant.String.type()),
tokenRegex("http://.*")
))
);
Так как проверка аргументов функций на соответствие регулярному выражению это часто встречающаяся операция в SvaceAPI, то предусмотрены паттерны для этих операций:
var badGet = funcCall(
tokenRegex("requests.get"),
0,
tokenRegex("http://.*", Constant.String.type())
);
Остаётся зарегистрировать этот паттерн методом registerChecker
, собрать плагин и Svace выдаст предупреждение:
var badGet = funcCall(
tokenRegex("requests.get"),
0,
tokenRegex("http://.*", Constant.String.type())
);
registerChecker(insecureGetWarning, "Don't request via http", badGet);
Однако находить только столь простой случай малополезно. В реальном коде аргумент requests.get
:
может быть сохранён в переменную:
url = 'http://example.com'
# ...
requests.get(url)
может выбираться в зависимости от условия:
if no_ssl:
url = 'http://example.com'
else:
url = 'https://example.com'
requests.get(url)
может быть возвращаемым значением другой функции:
def get_url():
return 'http://example.com'
requests.get(get_url())
может получаться в результате обработки форматной строки:
url = 'http://example.com/api/{}'.format(param)
requests.get(url)
или всё сразу:
def get_url(param):
if no_ssl:
url = 'http://example.com/api/{}'.format(param)
else:
url = 'https://example.com/api/{}'.format(param)
return url
requests.get(get_url(param))
Проблемы в таких примерах трудно найти анализом АСД и подобными техниками (и, практически невозможно, если функция и её вызов происходят в разных файлах). Однако, используя пользовательские атрибуты в SvaceAPI эта задача становится тривиальной.
Одна из ключевых фич нового обновления SvaceAPI — это возможность создавать атрибуты.
Неформально, атрибут — это некоторое свойство данных в программе. Чтобы решить задачу по нахождению ошибки в коде, надо задаться вопросом: “Какие свойства данных в программе указывают на возможную проблему?”. Для рассматриваемого примера это свойство можно неформально сформулировать как вопрос: “Эти данные - это строка, начинающаяся с http://
?”. Вооружившись результатами анализа проблемы, заведём соответствующий атрибут:
var isHttpString = makeAttribute(
"isHttpString",
false,
(left, right) -> left || right,
AttributeScope.INTERPROCEDURAL
);
Аргументами makeAttribute
являются:
http://
?”, то по умолчанию присвоим ложное значение. Атрибут может быть не только булевого типа. Допустимы любые примитивные типы, enum и объекты классов, реализующих equals
и hashCode
. Запрещено только присваивать значение null
;Join-функция. Эта специальная функция которая применяется, чтобы разрешить ситуацию когда в разных путях выполнения атрибут принимает разное значение. Например, в примере:
if no_ssl:
url = 'http://example.com'
else:
url = 'https://example.com'
requests.get(url)
переменная url
может удовлетворять или не удовлетворять желаемому свойству. Так как в качестве Join-функции используется “ИЛИ”, то атрибут, после объединения, будет иметь истинное значение и предупреждение выдастся (когда будет реализована оставшаяся часть детектора, конечно же). Если это нежелательно, то можно выбрать функцию “И”, тогда атрибут примет ложное значение и предупреждение выдано не будет;
AttributeScope.INTERPROCEDURAL
, которая делает атрибут межпроцедурным. По умолчанию Svace автоматически распространяет атрибуты только внутри одной функции, чтобы избежать замедления анализа. Чтобы атрибут распространялся между функциями, необходимо использовать эту настройку.Воспользуемся созданным атрибутом. Будем выставлять его в истинное значение для строковых констант, попадающих под регулярное выражение http://.*
как и ранее — используя паттерн constantDefinition
и метод registerAttributeRule
, куда нужно передать атрибут, значение, позицию (аргумент или в этом случае результат операции) и паттерн, при срабатывании которого должно выставляться значение атрибута:
var httpUrlDefinition = constantDefinition(tokenRegex("http://.*", Constant.String.type()));
registerAttributeRule(isHttpString, true, ApplyTo.result(), httpUrlDefinition);
Зарегистрируем предупреждение, но вместо паттерна для аргумента tokenRegex("http://.*", Constant.String.type()),
используем attributeIs(isHttpString, true)
, проверяющий созданный атрибут:
var badGet = funcCall(tokenRegex("requests.get"), 0, attributeIs(isHttpString, true));
registerChecker(badRequestWarning, "Don't request via http", badGet);
Детектор сможет обнаруживать ошибку в следующих случаях:
url = 'http://example.com'
# ...
requests.get(url)
if no_ssl:
url = 'http://example.com'
else:
url = 'https://example.com'
requests.get(url)
def get_url():
return 'http://example.com'
requests.get(get_url())
Нам остаётся разобраться со случаями, использующими метод format
. Чтобы находить такую ошибку, надо, чтобы результат вызова format
сохранял атрибут, если он выставлен на форматной строке. Для этого достаточно создать ещё один attribute rule, проверяющий с помощью паттерна thisArg
атрибут аргумента self
метода format
:
var formatString = funcCall("format", thisArg(attributeIs(badGet, true)));
registerAttributeRule(isHttpString, true, ApplyTo.result(), formatString);
Полный код плагина:
public class InsecureGetPlugin extends SvaceLightPlugin {
private SvaceUserWarning insecureGetWarning = null;
@Override
public String name() {
return "Insecure get checker";
}
@Override
public void registerWarningType(SvaceUserWarningRegister register) {
insecureGetWarning = register.registerWarning(
"INSECURE_GET",
Visibility.Enabled,
Language.PYTHON
);
}
@Override
public void createChecker() {
var isHttpString = makeAttribute(
"isHttpString",
false,
(left, right) -> left || right,
AttributeScope.INTERPROCEDURAL
);
var formatString = funcCall("format", thisArg(attributeIs(badGet, true)));
registerAttributeRule(isHttpString, true, ApplyTo.result(), formatString);
var badGet = funcCall(tokenRegex("requests.get"), 0, attributeIs(isHttpString, true));
registerChecker(badRequestWarning, "Don't use requests.get with http", badGet);
}
}
Мы надеемся, что с этим обновлением SvaceAPI станет полезным инструментом в арсенале наших пользователей. Мы планируем расширять набор поддерживаемых паттернов, а также планируем в ближайшее время добавить инструмент для визуализации сущностей промежуточного представления, свойств и пользовательских атрибутов, чтобы упростить разработку детекторов.
Также мы приглашаем ознакомиться с репозиторием детекторов на основе SvaceAPI с открытым исходным кодом и призываем создавать merge-request’ы с детекторами, которыми Вы хотели бы поделиться с другими пользователями Svace.