Привет, читатели Habr! С вами Анастасия Березовская, инженер по безопасности процессов разработки приложений в Swordfish Security. Сегодня мы расскажем про еще один инструмент, встречающийся в построении процессов Software Composition Analysis (SCA) — сdxgen. Он, как и популярный сканер Trivy, разбирает файлы манифестов, бинарные и другие файлы для извлечения информации о внешних компонентах, используемых в проекте. Кстати, о Trivy мы писали в одной из наших предыдущих статей, заходите почитать.

Главным объектом нашего анализа стал новый и очень интересный режим работы cdxgen под названием evinse, представленный авторами в 2023 году. Evinse по исходному коду предоставляет расширенную информацию об evidence — свидетельства присутствия компонента в исходном коде. На момент написания статьи cdxgen является единственной Open Source-утилитой, которая обладает подобной функциональностью. Мы опишем математику, используемую "под капотом", и объясним, почему решили интегрировать результаты работы режима в наших продуктах.

Статья получилась достаточно объемной, поэтому мы решили разделить её на две части. В первой мы рассмотрим, что представляет собой объект Evidence с точки зрения SBOM. Опишем базовые математические понятия, которые необходимы для понимания работы утилиты evinse в части построения расширенного SBOM. Здесь же рассмотрим первый вид нарезки использования. Во второй части статьи мы поговорим про остальные виды нарезок — срезы потоков данных и достижимости. Разберем, наконец-то, как из них получается SBOM. Итак, погнали!

Evidence & CycloneDX

Понятие Evidence для BOM введено в стандарте CycloneDX 1.5. Подробно о нем можно прочитать в их официальном документе. Evidence — это свидетельство присутствия заявленного в BOM компонента в продукте. Проще говоря, это основание, на котором мы указали компонент в спецификации.

Согласно заявленному формату, его описание имеет следующую структуру:

  • Identity — метаданные свидетельства. Используются для подтверждения связки идентичности компонента и источника его обнаружения;

  • Occurences — данные, отсылающие к исходному коду проекта. Показывают расположение отдельных экземпляров компонента в исходном коде;

  • Call Stack —- данные, схожие с трассировкой стека. Определяют, был ли уязвимый компонент вызван приложением. Показывают, где он был вызван в стеке вызовов, а также указывают достижимость уязвимого компонента и какие данные или функции могли быть затронуты.

Рассмотрим далее каждое из этих полей и приведем для них примеры.

Identity

  • Field – это значение одного из полей описания компонента, к которому относится свидетельство. Может указывать на его purl, cpe или hash;

  • Confidence – вероятность истинности свидетельства. В общем случае показывает степень уверенности в предоставленной информации;

  • Concluded Value – вычисленное значение (именно с точки зрения evidence) уникального имени компонента (purl, cpe и другие возможные поля из перечня в описании компонента);

  • Methods – методы, с помощью которых наличие данного элемента было установлено. Является составным компонентом;

    • Technique – техника обнаружения компонента. Перечень возможных техник определен в стандарте;

    • Value – значение имени компонента (или его хеша), полученное в результате применения техники обнаружения;

    • Confidence – степень уверенности применения данной техники для обнаружения компонента. Для каждой из техник есть рекомендованные значения confidence;

    • Tools – инструменты, с помощью которых был воспроизведен поиск свидетельств компонент. 

А теперь приведем техники установления наличия компонента в продукте вместе с рекомендуемыми для них значениями уверенности.

Техника

Описание

Confidence

ast-fingerprint

Анализ AST-дерева исходного кода или исполняемого файла

0.3 - 1.0

manifest-analysis

Анализ файлов манифеста, используемых пакетными менеджерами для сборки зависимостей и окружений

0.4 - 0.6

dynamic-analysis

Анализ запущенного приложения

0.2 - 0.6

binary-analysis

Анализ исполняемого файла с помощью его реверса

0.2 - 0.7

source-code analysis

Анализ исходного кода без его исполнения

0.3 - 1.0

instrumentation

Анализ трассировки вызовов запущенного приложения

0.3 - 0.8

filename

 

Обнаружение файлов с названием какой-либо известной библиотеки

0-0.1

 

hash-comparison

 

Расчёт и последующее сравнение хеша файла с базой данных хешей открытых библиотек

0.7 - 1.0

 

attestation

Прямое указание экспертом

0.7 - 1.0

other

...

-

Один из примеров стандарта:

Рисунок 1. Разбор примера объекта Identity
Рисунок 1. Разбор примера объекта Identity

Occurences

  • Location — указание на файл исходного кода проекта вместе со строкой, в которой используется указанный компонент.

Рассмотрим небольшой скрипт на Python, который использует библиотеку requests.

import requests

def main():
  response = requests.get('http://example.com')

if __name__ == '__main__':
  main()

Разберем описание компонента requests в соответствующем для представленного выше SBOM с вхождениями.

Рисунок 2. Вхождения библиотеки requests для кода из листинга 1
Рисунок 2. Вхождения библиотеки requests для кода из листинга 1

Call Stack

  • Frames — цепочка, в которой описаны различные уровни стека вызовов с использованием обнаруженного компонента;

  • Package — пакет, в котором обнаружено использование компонента;

  • Module — модуль, в котором обнаружено использование компонента;

  • Function — имя функции или метода, в котором обнаружено использование компонента;

  • Parameters — параметры, передаваемые в функцию или метод;

  • Line — номер строки исходного кода, где обнаружено использование;

  • Column — номер столбца исходного кода, где обнаружено использование;

  • fullFilename — полное имя файла с исходным кодом.

Разберем описание компонента numpy в соответствующем для представленного выше SBOM c трассировкой вызовов.

Рисунок 3. Стек вызовов библиотеки requests для кода из листинга 1
Рисунок 3. Стек вызовов библиотеки requests для кода из листинга 1

Evidenсe & cdxgen

Для указания Identity в объекте Evindense не требуется сложных вычислений. Cdxgen указывает его в большинстве случаев на основании типа файла, из которого найден компонент. Примеры такого подхода можно найти в исходном коде cdxgen в файле utils.js.

Больший интерес предоставляют алгоритмы, используемые для получения данных о вхождениях (occurrences) и стеке вызовов (call stack). Авторы cdxgen используют утилиту Atom для анализа исходного кода и получения связанных с ним срезов, последующая обработка которых позволяет создать “расширенную” версию SBOM.

Program Slicing

Понятие среза программы (program slicing) возникло достаточно давно в области Computer Science и было введено Вайсером в 1980 году. Для решения тех или иных задач не всегда предоставляет интерес весь код программы, иногда достаточно только части, связанной с определенным поведением приложения. Обратимся вновь к коду из листинга 1 и выделим ту часть, которая связанна с переменной response.

import requests
response = requests.get('http://example.com')

Выделенный код и называется срезом программы. Выбор поведения, который можно выразить через значения определенных наборов переменных в конкретных операторах, называется критерием среза. Сам по себе срез — полноценная исполняемая программа, поведение которой идентично указанному подмножеству поведения исходной программы.

Существуют два типа среза относительно критерия нарезки: прямой и обратный. Прямой начинается с определенной точки в программе и выделяет все инструкции и зависимости, которые могут произойти после этой точки. Он выделяет все места, которые находятся под влиянием критерия среза. Обратный срез, напротив, начинается с конечного результата или интересующей точки в программе и определяет все причины и зависимости, влияющие на этот результат. Он выделяет все места, влияющие на критерий среза. Пример обратного среза показан на листинге 2.

Срезы находят применение в различных задачах, таких как отладка (дебаггинг), рефакторинг, параллелизация исходной программы и анализ ее безопасности.

Для автоматического получения срезов используются обходы различных графов. В оригинальной статье Вайера было предложено два алгоритма: один из них использует граф потока управления (Control Flow Graph, CFG) и альтернативный ему алгоритм, который вычисляет срезы как обратные проходы графа зависимости программы (Program Dependency Graph, PDG). Позднее другие авторы многократно модифицировали эти алгоритмы, повышая их скорость и точность.

На этом моменте нам немного окунемся в мир программного анализа и убедимся лишний раз в том, что графы в университете преподавали не зря.

Code Graphs

Основная математическая структура, которая лежит в основе получения срезов в Atom, — это граф свойств кода (Сode Property Graph, CPG). Для работы с ним авторы используют библиотеку, которая соответствует спецификации Joern. Это не случайно: один из основных разработчиков проекта Atom еще и является крупным контрибьютором проекта Joern.

Граф свойств кода впервые был представлен Фабианом Ямагучи и Нико Гол в 2014 году. Интересно, что на первый взгляд это сугубо математическое понятие было введено именно с целью моделирования и обнаружения уязвимостей, то есть в разрезе информационной безопасности. Их подход продолжал уже существующую практику анализа графов в тестировании приложений.

Суть их идеи заключается в объединении абстрактного синтаксического дерева (Abstract Syntax Tree, AST), графа потока управления и графа зависимостей программы в единую конструкцию.

Базой объединения послужил граф свойств – другое понятие, которое вне математической среды используется для хранения структурированных данных в графовых базах данных, таких как ArangoDB, Orient DB. Граф свойств представляет собой ориентированный помеченный мультриграф с атрибутами.

Помимо стандартного набора для графа в виде множества вершин V и ребер E, он включает в себя пометку ребра, которая назначает метку из алфавита \sum каждому ребру из множества E:

\lambda: E \rightarrow \sum

Каждому ребру и узлу назначаются свойства функцией

\mu: (V \cup E) \times K → S

Для моделирования CPG сначала каждое из представлений преобразуется в граф свойств. А потом их сливают в единое представление. Напомним далее описание для каждой из вышеупомянутых структур.

AST

Абстрактные синтаксические деревья — это упорядоченные деревья, в которых внутренние узлы представляют операторы (например, сложения или присваивания), а листовые узлы соответствуют операндам (например, константам или идентификаторам).

Узлы представляют собой программные конструкции. Сюда входят конструкции языка низкого уровня, такие как методы, переменные и структуры управления, а также конструкции более высокого уровня, такие как конечные точки или результаты запросов HTTP.

Каждый узел имеет тип, который указывает на вид программной конструкции, представленной узлом. Например, узел типа METHOD представляет метод, а узел типа LOCAL — объявление локальной переменной.

Рисунок 4. AST кода из листинга 1 (версия atom)
Рисунок 4. AST кода из листинга 1 (версия atom)

Синтаксическое дерево, наверное, одно из самых широко известных промежуточных представлений. Оно хорошо подходит для простых преобразований кода и для идентификации семантически похожего кода. Однако для более сложного анализа, такого как обнаружение мертвого кода или неинициализированных переменных, AST недостаточен — при таком представлении кода ни поток управления, ни зависимости данных не становятся явными.

CFG

Граф потока управления явно описывает порядок выполнения операторов кода и условия, которые необходимо выполнить для выбора определенного пути выполнения.

Операторы и предикаты представляются узлами, которые соединяются направленными ребрами, обозначающими передачу управления. Каждому ребру необходимо присвоить метку “true”, “false” или “e”. Операторы имеют одно выходящее ребро, помеченное как e, тогда как узлы предикатов имеют два исходящих ребра, соответствующие истинной или ложной оценке предиката.

Граф потока управления может быть построен из абстрактных синтаксических деревьев. В представленном примере кода на листинге 1 нет условных операторов, поэтому граф представляет собой простую цепочку:

Рисунок 5. CFG кода из листинга 1 (версия atom)
Рисунок 5. CFG кода из листинга 1 (версия atom)

Этот вид графов часто использовали в контексте безопасности, например, для обнаружения вариантов известных вредоносных приложений и для управления инструментами фаззинг-тестирования. Тем не менее они не могут предоставить информацию о потоке данных. В частности, для анализа уязвимостей это означает, что графы потоков управления не могут быть легко использованы для идентификации операторов, которые обрабатывают находящиеся под влиянием злоумышленника данные.

PDG

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

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

Ребра графа зависимости можно вычислить на основе графа потока управления. Для этого необходимо сначала определить набор переменных, которые определяются и используются каждым оператором, а затем вычислить "определения достижения" для каждого оператора и предиката. Этот процесс является стандартной задачей при разработке компиляторов.

Рисунок 6. PDG кода из листинга 1
Рисунок 6. PDG кода из листинга 1

Code Property Graph

Узлами графа свойств кода являются узлы абстрактного синтаксического дерева. Ребрами и их пометками в этом графе служит объединение всех ребер и меток из трех промежуточных представлений.

Ребра и их пометки отражают отношения между программными конструкциями (узлами). Например, чтобы показать, что метод содержит локальную переменную, создается ребро с меткой CONTAINS от узла метода до узла локальной переменной. Использование помеченных ребер позволяет представлять несколько типов отношений в одном графе.

Узлы содержат пары ключ-значение (атрибуты), где допустимые ключи зависят от типа узла. Например, у метода есть как минимум имя и подпись, а у локального объявления — имя и тип объявленной переменной.

Больше подробностей о построении CPG можно найти в оригинальной статье. Граф спецификации Joern несколько сложнее, его слои состоят из большего количество графов. Описание всех его слоев вместе с описанием всех соответствующим им узлов, ребер и свойств можно найти документации репозитория Atom или спецификации Joern.

Даже для нашего небольшого примера граф выглядит достаточно объемно и плохо читается. Убедитесь сами:

Рисунок 7. CPG кода из листинга 1 (версия atom)
Рисунок 7. CPG кода из листинга 1 (версия atom)

Графы были получены с помощью утилиты Atom командой:

atom -o app_2.atom -l python --export-atom --export-format dot --export-dir ./ --with-data-deps main.py

Также их можно получить с помощью утилиты Joern. Для этого после ее установки нужно выполнить следующие команды:

joern-parse /src/directory
joern-export --repr all --out outdir

Графы, полученные с помощью разных утилит, будут слегка отличаться. Joern, к примеру, генерирует несколько синтаксических деревьев. Для нашего кода один из вариантов будет соответствовать рисунку 4. Другой вариант будет содержать узлы для условного выражения из строки 7. Он и будет использован для поиска использования зависимостей в исходном коде.

Рисунок 8. Вариант AST, полученный с помощью Joern, для кода из листинга 1
Рисунок 8. Вариант AST, полученный с помощью Joern, для кода из листинга 1

Экспортировать только CPG можно с помощью команды:

joern-export --repr cpg --out outdir 

При таком виде экспорта будет видно, что внутри Joern есть также отдельные подграфы CPG для каждого из методов. Аналогично можно экспортировать PDG-граф, который будет более объемный, чем на рисунке 6.

Рисунок 9. PDG кода из листинга 1, полученный с помощью утилиты Joern
Рисунок 9. PDG кода из листинга 1, полученный с помощью утилиты Joern

Не будем упоминать другие типы графов, основанных на AST с рисунка 8, так как они более сложные и менее удобочитаемые. 

Usage Slices

Joern определяет операцию срезов на СPG. Программная нарезка, описанная выше, в общем случае представляет собой процесс сокращения программы. Нарезание графа свойств кода сокращает полный граф данной кодовой базы до подмножества полезных узлов.

Реализовано два типа срезов: срезы использования (usage slices) и срезы потока данных (data-flow slices). В Atom эти алгоритмы оптимизированы именно для анализа зависимостей. В этой части исходного кода можно найти извлечение срезов использования, а здесь создание срезов потока данных.

Срезы использования позволяют находить и выделять фрагменты кода, описывающие, как объект применяется в пределах одной процедуры (метода) с учетом вызовов методов, присваиваний и других операций. В самом алгоритме нарезки объекты являются переменными. Ссылающие идентификаторы отслеживаются для определения, что вызывает сама переменная и что вызывает ее, образуя аргумент.

Запустим алгоритм нарезки для кода из листинга 1 с помощью команды:

atom usages -l python
Скрытый текст
{
    "objectSlices": [
        {
            "code": "",
            "fullName": "main.py:<module>",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 1,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "<empty>",
                        "typeFullName": "ANY",
                        "lineNumber": 1,
                        "columnNumber": 1,
                        "label": "UNKNOWN"
                    },
                    "definedBy": {
                        "name": "<empty>",
                        "typeFullName": "ANY",
                        "lineNumber": 1,
                        "columnNumber": 1,
                        "label": "UNKNOWN"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "requests",
                        "typeFullName": "requests.py:<module>",
                        "lineNumber": 1,
                        "columnNumber": 1,
                        "label": "LOCAL"
                    },
                    "definedBy": {
                        "name": "import",
                        "typeFullName": "import",
                        "resolvedMethod": "import",
                        "isExternal": null,
                        "lineNumber": 1,
                        "columnNumber": 1,
                        "label": "CALL"
                    },
                    "invokedCalls": [
                        {
                            "callName": "get",
                            "resolvedMethod": "requests.py:<module>.get",
                            "paramTypes": [
                                "ANY"
                            ],
                            "returnType": "ANY",
                            "isExternal": true,
                            "lineNumber": 4,
                            "columnNumber": 13
                        }
                    ],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<operator>.assignment",
                        "typeFullName": "ANY",
                        "resolvedMethod": "<operator>.assignment",
                        "isExternal": true,
                        "lineNumber": 3,
                        "columnNumber": 1,
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "<operator>.assignment",
                        "typeFullName": "ANY",
                        "resolvedMethod": "<operator>.assignment",
                        "isExternal": true,
                        "lineNumber": 3,
                        "columnNumber": 1,
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<operator>.equals",
                        "typeFullName": "ANY",
                        "resolvedMethod": "<operator>.equals",
                        "isExternal": true,
                        "lineNumber": 6,
                        "columnNumber": 4,
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "<operator>.equals",
                        "typeFullName": "ANY",
                        "resolvedMethod": "<operator>.equals",
                        "isExternal": true,
                        "lineNumber": 6,
                        "columnNumber": 4,
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                }
            ]
        },
        {
            "code": "",
            "fullName": "main.py:<module>.main",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 3,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "main",
                        "typeFullName": "ANY",
                        "resolvedMethod": "main.py:<module>.main",
                        "isExternal": false,
                        "lineNumber": 7,
                        "columnNumber": 2,
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "main",
                        "typeFullName": "ANY",
                        "resolvedMethod": "main.py:<module>.main",
                        "isExternal": false,
                        "lineNumber": 7,
                        "columnNumber": 2,
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<operator>.fieldAccess",
                        "typeFullName": "ANY",
                        "resolvedMethod": "<operator>.fieldAccess",
                        "isExternal": true,
                        "lineNumber": 4,
                        "columnNumber": 13,
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "<operator>.fieldAccess",
                        "typeFullName": "ANY",
                        "resolvedMethod": "<operator>.fieldAccess",
                        "isExternal": true,
                        "lineNumber": 4,
                        "columnNumber": 13,
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "get",
                        "typeFullName": "ANY",
                        "resolvedMethod": "requests.py:<module>.get",
                        "isExternal": true,
                        "lineNumber": 4,
                        "columnNumber": 13,
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "get",
                        "typeFullName": "ANY",
                        "resolvedMethod": "requests.py:<module>.get",
                        "isExternal": true,
                        "lineNumber": 4,
                        "columnNumber": 13,
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                }
            ]
        },
        {
            "code": "",
            "fullName": "main.py:<module>.main",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 3,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "get",
                        "typeFullName": "",
                        "resolvedMethod": "requests.py:<module>.get",
                        "isExternal": true,
                        "lineNumber": null,
                        "columnNumber": null,
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "get",
                        "typeFullName": "",
                        "resolvedMethod": "requests.py:<module>.get",
                        "isExternal": true,
                        "lineNumber": null,
                        "columnNumber": null,
                        "label": "CALL"
                    },
                    "invokedCalls": [
                        {
                            "callName": "get",
                            "resolvedMethod": "requests.py:<module>.get",
                            "paramTypes": [],
                            "returnType": "",
                            "isExternal": true,
                            "lineNumber": 4,
                            "columnNumber": 13
                        }
                    ],
                    "argToCalls": []
                }
            ]
        },
        {
            "code": "",
            "fullName": "main.py:<module>",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 1,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "main",
                        "typeFullName": "",
                        "resolvedMethod": "main.py:<module>.main",
                        "isExternal": false,
                        "lineNumber": 3,
                        "columnNumber": 1,
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "main",
                        "typeFullName": "",
                        "resolvedMethod": "main.py:<module>.main",
                        "isExternal": false,
                        "lineNumber": 3,
                        "columnNumber": 1,
                        "label": "CALL"
                    },
                    "invokedCalls": [
                        {
                            "callName": "main",
                            "resolvedMethod": "main.py:<module>.main",
                            "paramTypes": [],
                            "returnType": "",
                            "isExternal": true,
                            "lineNumber": 7,
                            "columnNumber": 2
                        }
                    ],
                    "argToCalls": []
                }
            ]
        }
    ],
    "userDefinedTypes": []
}

В нем есть два списка срезов: с ключом «objectSlices» (соответствуют объекту “MethodUsageSlice” в исходном коде, что, по нашему мнению, лучше отражает его смысл) и с ключом «userDefinedTypes». Описание каждого есть в документации Atom.

Из CPG в начале извлекаются узлы, соответствующие методам (по определению, срезы ограничены рамками своей процедуры). Извлекаются не все методы, а с дополнительными ограничениями на выбор: метод не может быть оператором или должен вызывать хотя бы один другой метод.

В коде, который мы сегодня разбираем, таких метода два: исполняемый модуль и функция “main”. Их мы и видим как основные объекты списка “objectSlices”. Далее разберем описывающую их структуру.

Рисунок 10. Описание метода в рамках срезов использования
Рисунок 10. Описание метода в рамках срезов использования

Для каждого из методов необходимо найти и вернуть все операции, связанные с идентификаторами внутри этого узла — вызовы методов, присваивания, использование в условиях и другие действия.

Вернемся к нашему коду и сократим его до действий на уровне модуля:

import request

main = def main(); ...

if __main__ == __name__:
    main()

Перечислим операции: импорт внешнего модуля, операторы присваивания и сравнения на равенство, вызов функции “main”. Их описание мы и видим в массиве “usages” (для objectArray[0]). Заметим, что включение в исполняемый модуль функции “main” будет описано отдельно в objectSlices[3].

Каждый из элементов массива в объекте “usages” и есть один срез. Он определяется как тройка, состоящая из двух определений и набора вызовов. Первое определение в этой тройке — вызов объекта, идентификатор или литерал, определяющий данные целевого объекта. Соответствует ключу ”definedBy” в единичном объекте массива “usages”.

Второе понятие — целевой объект определения. Соответствует ключу “targetObj”. Оба определения являются параметром, и в этом случае объекты ”definedBy” и “targetObj” будут идентичны. В таком случае объекты ”definedBy” и “targetObj” будут идентичны. Вызовы — это то, что запрашивает объект или то, для чего он является параметром. Более точное математическое описание можно найти в статье.

Рисунок 11. Описание среза
Рисунок 11. Описание среза

На рисунке 11 под источником формирования данных подразумевается место, где данные определены и могут быть присвоены какой-либо переменной или использованы в аргументе. Сократим код из листинга 1 до уровня функции “main”, которая будет включать в себя всего одну строку:

response = requests.get('http://example.com')

Операции для этой строки: оператор присваивания, оператор доступа к полю объекта, вызов функции “get”. Оператор присваивания уже был включен в предыдущий список. Срезы для остальных операций мы находим в objecySlices[1]. Здесь также включение функции “get” в функцию main будет продублировано в отдельный элемент массива objectSlices[2].

На рисунке 12 небольшой подграф, который приближенно иллюстрирует результаты нарезки.

Рисунок 9. Подграф CPG
Рисунок 9. Подграф CPG

Давайте рассмотрим чуть более сложный код:

import requests as req

def run(): 
	response = req.get('http://example.com')
	status = response.status_code
	url = 'https://www.w3schools.com/python/demopage.php'
  myobj = {'somekey': 'somevalue'}
  x = req.post(url, json = myobj)
        
print("Finish!")

Здесь опять два основных метода. Можно предположить, что в первом наборе будут описаны операции импорта и присваивания. В отдельном наборе для этого же метода будет вызов функции “print”. Для функции “run” будут описаны операции взятия поля объекта, создания словаря, обращения по индексу (для словаря), функции “get” и “post” (обе продублированы в отдельных наборах). А вот и результат, подтверждающий наши предположения:

Скрытый текст
{
    "objectSlices": [
        {
            "fullName": "main.py:<module>",
            "usages": [
                {
                    "targetObj": {
                        "name": "<empty>",
                    },
                    "definedBy": {
                        "name": "<empty>",
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "req",
                        "typeFullName": "requests.py:<module>",
                        "label": "LOCAL"
                    },
                    "definedBy": {
                        "name": "import",
                        "label": "CALL"
                    },
                    "invokedCalls": [
                        {
                            "callName": "get",
                            "resolvedMethod": "requests.py:<module>.get",
                        },
                        {
                            "callName": "post",
                            "resolvedMethod": "requests.py:<module>.post",
                        }
                    ],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<operator>.assignment",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "<operator>.assignment",
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "print",
                        "resolvedMethod": "__builtin.print",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "print",
                        "resolvedMethod": "__builtin.print",
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                }
            ]
        },
        {
            "code": "",
            "fullName": "main.py:<module>.run",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 3,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "get",
                        "resolvedMethod": "requests.py:<module>.get",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "get",
                        "resolvedMethod": "requests.py:<module>.get",
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<operator>.dictLiteral",
                        "resolvedMethod": "<operator>.dictLiteral",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "<operator>.dictLiteral",
                        "resolvedMethod": "<operator>.dictLiteral",
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<operator>.indexAccess",
                        "resolvedMethod": "<operator>.indexAccess",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "<operator>.indexAccess",
                        "resolvedMethod": "<operator>.indexAccess",
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<operator>.fieldAccess",
                        "resolvedMethod": "<operator>.fieldAccess",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "<operator>.fieldAccess",
                        "resolvedMethod": "<operator>.fieldAccess",
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "<empty>",
                        "label": "UNKNOWN"
                    },
                    "definedBy": {
                        "name": "<empty>",
                        "label": "UNKNOWN"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                },
                {
                    "targetObj": {
                        "name": "post",
                        "resolvedMethod": "requests.py:<module>.post",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "post",
                        "resolvedMethod": "requests.py:<module>.post",
                        "label": "CALL"
                    },
                    "invokedCalls": [],
                    "argToCalls": []
                }
            ]
        },
        {
            "code": "",
            "fullName": "main.py:<module>.run",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 3,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "get",
                        "resolvedMethod": "requests.py:<module>.get",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "get",
                        "typeFullName": "",
                        "resolvedMethod": "requests.py:<module>.get",
                        "label": "CALL"
                    },
                    "invokedCalls": [
                        {
                            "callName": "get",
                            "resolvedMethod": "requests.py:<module>.get",
                            "paramTypes": [],
                            "returnType": "",
                            "isExternal": true,
                            "lineNumber": 4,
                            "columnNumber": 13
                        }
                    ],
                    "argToCalls": []
                }
            ]
        },
        {
            "code": "",
            "fullName": "main.py:<module>.run",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 3,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "post",
                        "resolvedMethod": "requests.py:<module>.post",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "post",
                        "resolvedMethod": "requests.py:<module>.post",
                        "label": "CALL"
                    },
                    "invokedCalls": [
                        {
                            "callName": "post",
                            "resolvedMethod": "requests.py:<module>.post",
                        }
                    ],
                    "argToCalls": []
                }
            ]
        },
        {
            "code": "",
            "fullName": "main.py:<module>",
            "signature": "",
            "fileName": "main.py",
            "lineNumber": 1,
            "columnNumber": 1,
            "usages": [
                {
                    "targetObj": {
                        "name": "print",
                        "resolvedMethod": "__builtin.print",
                        "label": "CALL"
                    },
                    "definedBy": {
                        "name": "print",
                        "resolvedMethod": "__builtin.print",
                        "label": "CALL"
                    },
                    "invokedCalls": [
                        {
                            "callName": "print",
                            "resolvedMethod": "__builtin.print",
                        }
                    ],
                    "argToCalls": []
                }
            ]
        }
    ],
    "userDefinedTypes": []
}

На основании Usage Slices далее будут рассчитываться вхождения, которые попадают в расширенный BOM.

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

Комментарии (0)