Большое обновление SvaceAPI

В следующем обновлении Svace мы существенно расширим возможности для создания пользовательских детекторов с помощью SvaceAPI.

Мотивация

За годы разработки мы добавили в Svace большое количество детекторов, находящие ошибки самых разных типов, среди которых:

Тем не менее этот список будет всегда оставаться неполным. Многие проекты используют библиотеки, которые не поддерживаются в Svace, вводят свои правила написания кода или используют различные стандарты безопасной разработки. Хотя наша команда всегда стремится расширить возможности Svace, часто нецелесообразно пытаться добавить новые детекторы в Svace по ряду причин:

Это оставляет разработчикам по сути один вариант — ручной поиск ошибок на код-ревью, что, конечно, тоже важная часть совместной разработки, но не позволяет добиться таких же результатов, чем использование статического анализа.

Чтобы решить эту проблему, мы создали SvaceAPI — инструмент, позволяющий нашим пользователям создавать свои детекторы, используя продвинутый внутри- и межпроцедурный анализ Svace для всех поддерживаемых в рамках основного анализа языках.

Особенности SvaceAPI

Создавая SvaceAPI мы хотели создать гибкий API с широкими возможностями, удобный для конечных пользователей. Но также важным было обеспечить высокую производительность на уровне основного анализа Svace. Для этого мы решили разработать поддержку плагинов на языке Java, интегрируемые с основным анализом Svace с помощью системы модулей Java. Эта система позволяет основному анализу Svace во время работы динамически загружать сторонний код и запускать пользовательские детекторы. Чтобы реализовать детектор, необходимо расширить класс SvaceLightPlugin и реализовать три метода. Для сборки плагинов нужно подключить только библиотеку svace-api.jar (для удобства рекомендуется использовать систему сборки gradle, подробности содержатся в документации Svace).

Основываясь на нашем опыте разработки статического анализатора, мы выделили следующие качества, которыми должен был удовлетворять хороший API для пользовательских детекторов:

  1. Абстрагирование от деталей реализации отдельных языков.

    Хотя и невозможно полностью исключить различия между языками программирования, многие конструкции удаётся унифицировать. Более того, можно унифицировать и сам формат представления инструкций. В SvaceAPI используется SSA-форма из унифицированных инструкций — сущностей. Они представляются в виде иерархии интерфейсов, наследуемых от CodeEntity. Сущности делятся на две широкие категории — операторы (примерно эквивалентны инструкциям) и их операнды (аргументы). Например, выражение b = 10 / a представляется оператором BinaryOperator.Division с возвращаемым значением Variable и двумя аргументами: Constant.Integer и Variable.

  2. Декларативный и самоописывающий 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))).
  3. Доступ к анализам Svace.

    В Svace уже реализовано достаточное количество сложных анализов, результаты которых можно использовать в детекторах SvaceAPI. Например, паттерн interval (из предыдущего пункта) использует межпроцедурный анализ интервалов, реализованный в Svace. Более того, используя механизм SvaceAPI, можно создавать свои атрибуты — формальные описания свойств значений в программе, которыми оперирует анализ Svace. Используя аккуратно составленные атрибуты, детекторы SvaceAPI могут использовать межпроцедурный анализ Svace и находить ошибки, анализируя поток данных между различными функциями и модулями программы.

Создаем детектор с SvaceAPI

Давайте разработаем простейший детектор для поиска в кодовой базе вызовов функции 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:

Проблемы в таких примерах трудно найти анализом АСД и подобными техниками (и, практически невозможно, если функция и её вызов происходят в разных файлах). Однако, используя пользовательские атрибуты в SvaceAPI эта задача становится тривиальной.

Атрибуты

Одна из ключевых фич нового обновления SvaceAPI — это возможность создавать атрибуты.

Неформально, атрибут — это некоторое свойство данных в программе. Чтобы решить задачу по нахождению ошибки в коде, надо задаться вопросом: “Какие свойства данных в программе указывают на возможную проблему?”. Для рассматриваемого примера это свойство можно неформально сформулировать как вопрос: “Эти данные - это строка, начинающаяся с http://?”. Вооружившись результатами анализа проблемы, заведём соответствующий атрибут:

var isHttpString = makeAttribute(
        "isHttpString",
        false,
        (left, right) -> left || right,
        AttributeScope.INTERPROCEDURAL
);

Аргументами makeAttribute являются:

Улучшение детектора с помощью атрибутов

Воспользуемся созданным атрибутом. Будем выставлять его в истинное значение для строковых констант, попадающих под регулярное выражение 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);

Детектор сможет обнаруживать ошибку в следующих случаях:

Нам остаётся разобраться со случаями, использующими метод 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.