Привет, Habr! С вами вновь Анастасия Березовская, инженер по безопасности процессов разработки приложений в Swordfish Security. Сегодня мы продолжим наш нелегкий путь в получении Evidence для SBOM.

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

Сегодня мы поговорим про срезы достижимости и срезы потоков данных. И, самое главное, разберем, как всё это превращается в расширенный SBOM с вхождениями. Приятного чтения!

Reachable Slices

Срезы потоков данных позволяют узнать, как переменные влияют на другие части программы. Из раздела «Program Slicing» мы знаем, что обход может быть как «вперед», так и «назад». Результат среза для обхода вперед в Atom называется Reachable Slices. Эти срезы описывают потоки данных, которые могут исходить из точки входа и достигать внешнего приемника. Если вы имели дело с SAST, то наверняка встречали подобные алгоритмы — это и есть Source‑sink анализ. Это пути, по которым злоумышленник может добраться до известной уязвимости в сторонней библиотеке. Отсюда появились термины «достижимые потоки» или «прямая достижимость». Срезы рассчитываются с использованием автоматически проставленных тегов, «framework‑in» и «framework‑out».

Прежде чем запускать расчет этого типа среза необходимо предварительно получить BOM‑файл для кода. А уже далее запустить алгоритм уже почти привычной командой:

cdxgen -t python -o bom.json --deep .  
atom data-flow -l python

К сожалению, для нашего sample-кода срез получился пустым. Для языка Python в целом подобрать код, дающий полноценный прямой срез, непростая задача. Поэтому рассмотрим код на JavaScript и получим срез для него:

import http from 'http';

let server;

function myfunc(url) {
  sandbox(url); 
}

server = http.createServer(function (req, res) 
{ 
  myfunc(req.url); 
});

Вы быстро поймете, что код на листинге не предоставляет смысла. Однако синтаксически он верен, и этого достаточно для статического анализатора. В этом коде есть два важных момента: обращение к «req.url» при построении CPG будет иметь метку «framework‑in», а вызов функции «sandbox» — метку «framework‑out». Как было сказано выше, именно по ним и происходит анализ потока данных.

Рисунок 1. Структура срезов прямого потока данных
Сокращенный результат прямого среза потока данных
{
    "reachables": [
        {
            "flows": [
                {
                    "id": 54,
                    "label": "IDENTIFIER",
                    "name": "http",
                    "code": "http.createServer(function (req, res) \n{ \n  myfunc(req.url); \n})",
                    "parentMethodName": ":program",
                    "lineNumber": 9,
                    "columnNumber": 9,
                    "tags": "pkg:npm/http@0.0.1-security"
                },
                {
                    "id": 22,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "this",
                    "code": "this",
                    "parentMethodName": "anonymous",
                    "lineNumber": 9,
                    "columnNumber": 27,
                    "tags": ""
                },
                {
                    "id": 28,
                    "label": "IDENTIFIER",
                    "name": "this",
                    "code": "myfunc(req.url)",
                    "parentMethodName": "anonymous",
                    "lineNumber": 11,
                    "columnNumber": 2,
                    "tags": "framework-input"
                },
                {
                    "id": 8,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "this",
                    "code": "this",
                    "parentMethodName": "myfunc",
                    "lineNumber": 5,
                    "columnNumber": 0,
                    "tags": ""
                },
                {
                    "id": 13,
                    "label": "IDENTIFIER",
                    "name": "this",
                    "code": "sandbox(u)",
                    "parentMethodName": "myfunc",
                    "lineNumber": 6,
                    "columnNumber": 2,
                    "tags": "framework-output"
                }
            ],
            "purls": [
                "pkg:npm/http@0.0.1-security"
            ]
        },
        {
            "flows": [
                {
                    "id": 30,
                    "label": "IDENTIFIER",
                    "name": "req",
                    "code": "req.url",
                    "parentMethodName": "anonymous",
                    "lineNumber": 11,
                    "columnNumber": 9,
                    "tags": "framework-input"
                },
                {
                    "id": 29,
                    "label": "CALL",
                    "fullName": "<operator>.fieldAccess",
                    "code": "req.url",
                    "parentMethodName": "anonymous",
                    "lineNumber": 11,
                    "columnNumber": 9,
                    "tags": "framework-input"
                },
                {
                    "id": 9,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "u",
                    "code": "u",
                    "parentMethodName": "myfunc",
                    "lineNumber": 5,
                    "columnNumber": 16,
                    "tags": ""
                },
                {
                    "id": 14,
                    "label": "IDENTIFIER",
                    "name": "u",
                    "code": "sandbox(u)",
                    "parentMethodName": "myfunc",
                    "lineNumber": 6,
                    "columnNumber": 10,
                    "tags": "framework-output"
                }
            ],
            "purls": []
        },
        {
            "flows": [
                {
                    "id": 30,
                    "label": "IDENTIFIER",
                    "name": "req",
                    "code": "req.url",
                    "parentMethodName": "anonymous",
                    "lineNumber": 11,
                    "columnNumber": 9,
                    "tags": "framework-input"
                },
                {
                    "id": 29,
                    "label": "CALL",
                    "fullName": "<operator>.fieldAccess",
                    "code": "req.url",
                    "parentMethodName": "anonymous",
                    "lineNumber": 11,
                    "columnNumber": 9,
                    "tags": "framework-input"
                },
                {
                    "id": 9,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "u",
                    "code": "u",
                    "parentMethodName": "myfunc",
                    "lineNumber": 5,
                    "columnNumber": 16,
                    "tags": ""
                },
                {
                    "id": 14,
                    "label": "IDENTIFIER",
                    "name": "u",
                    "code": "sandbox(u)",
                    "parentMethodName": "myfunc",
                    "lineNumber": 6,
                    "columnNumber": 10,
                    "tags": "framework-output"
                }
            ],
            "purls": []
        },
        {
            "flows": [
                {
                    "id": 28,
                    "label": "IDENTIFIER",
                    "name": "this",
                    "isExternal": false,
                    "code": "myfunc(req.url)",
                    "parentMethodName": "anonymous",
                    "lineNumber": 11,
                    "columnNumber": 2,
                    "tags": "framework-input"
                },
                {
                    "id": 8,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "this",
                    "code": "this",
                    "parentMethodName": "myfunc",
                    "lineNumber": 5,
                    "columnNumber": 0,
                    "tags": ""
                },
                {
                    "id": 13,
                    "label": "IDENTIFIER",
                    "name": "this",
                    "code": "sandbox(u)",
                    "parentMethodName": "myfunc",
                    "parentClassName": "main.mjs::program",
                    "lineNumber": 6,
                    "columnNumber": 2,
                    "tags": "framework-output"
                }
            ],
            "purls": []
        },
        {
            "flows": [
                {
                    "id": 54,
                    "label": "IDENTIFIER",
                    "name": "http",
                    "code": "http.createServer(function (req, res) \n{ \n  myfunc(req.url); \n})",
                    "parentMethodName": ":program",
                    "lineNumber": 9,
                    "columnNumber": 9,
                    "tags": "pkg:npm/http@0.0.1-security"
                },
                {
                    "id": 23,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "req",
                    "code": "req",
                    "parentMethodName": "anonymous",
                    "lineNumber": 9,
                    "columnNumber": 37,
                    "tags": ""
                },
                {
                    "id": 29,
                    "label": "CALL",
                    "fullName": "<operator>.fieldAccess",
                    "code": "req.url",
                    "parentMethodName": "anonymous",
                    "parentClassName": "main.mjs::program",
                    "lineNumber": 11,
                    "columnNumber": 9,
                    "tags": "framework-input"
                },
                {
                    "id": 9,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "u",
                    "code": "u",
                    "parentMethodName": "myfunc",
                    "parentClassName": "main.mjs::program",
                    "lineNumber": 5,
                    "columnNumber": 16,
                    "tags": ""
                },
                {
                    "id": 14,
                    "label": "IDENTIFIER",
                    "name": "u",
                    "code": "sandbox(u)",
                    "parentMethodName": "myfunc",
                    "lineNumber": 6,
                    "columnNumber": 10,
                    "tags": "framework-output"
                }
            ],
            "purls": [
                "pkg:npm/http@0.0.1-security"
            ]
        },
        {
            "flows": [
                {
                    "id": 28,
                    "label": "IDENTIFIER",
                    "name": "this",
                    "code": "myfunc(req.url)",
                    "parentMethodName": "anonymous",
                    "lineNumber": 11,
                    "columnNumber": 2,
                    "tags": "framework-input"
                },
                {
                    "id": 8,
                    "label": "METHOD_PARAMETER_IN",
                    "name": "this",
                    "code": "this",
                    "parentMethodName": "myfunc",
                    "lineNumber": 5,
                    "columnNumber": 0,
                    "tags": ""
                },
                {
                    "id": 13,
                    "label": "IDENTIFIER",
                    "name": "this",
                    "code": "sandbox(u)",
                    "parentMethodName": "myfunc",
                    "lineNumber": 6,
                    "columnNumber": 2,
                    "tags": "framework-output"
                }
            ],
            "purls": []
        }
    ]
}

В массиве reachables[0].flows мы видим цепочку, которая описывает, как идентификатор “http” достигает точки выхода. Он ссылает нас на внешнюю библиотеку, указанную в BOM-файле. Её purl и лежит в reachables[0].purls. Аналогично в reachables[4].flows описан другой его путь достижимости выхода.

В массивах reachables[1].flows, reachables[2].flows, reachables[3].flows, reachables[5].flows описано изменение идентификаторов, проходящих полные пути от входа до выхода.

Data-flow Slices

Часто достижимость не получается вычислить из-за наличия библиотек-оболочек или митигирующих слоев — анализируемый репозиторий может представлять собой общий модуль, содержащий только приемные методы без точек входа (источников). Например, общие библиотеки компании и модули в отдельных репозиториях, файлах jar и других пакетах. Для решения этой проблемы можно применить классические срезы потоков данных.

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

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

atom data-flow -l python

Результатом является подграф CPG с обычным перечислением его узлов и ребер, а также предварительно рассчитанными путями.

Результат исполнения
{
    "graph": {
        "nodes": [
            {
                "id": 27,
                "label": "IDENTIFIER",
                "name": "requests",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "requests",
                "typeFullName": "requests.py:<module>",
                "parentMethodName": "main.py:<module>.main",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 4,
                "columnNumber": 13,
                "tags": ""
            },
            {
                "id": 23,
                "label": "LITERAL",
                "name": "",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "'http://example.com'",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>.main",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 4,
                "columnNumber": 26,
                "tags": ""
            },
            {
                "id": 13,
                "label": "LITERAL",
                "name": "",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "requests",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": ""
            },
            {
                "id": 11,
                "label": "IDENTIFIER",
                "name": "requests",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "requests",
                "typeFullName": "requests.py:<module>",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": ""
            },
            {
                "id": 12,
                "label": "LITERAL",
                "name": "",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": ""
            },
            {
                "id": 15,
                "label": "CALL",
                "name": "import",
                "fullName": "<unknownFullName>",
                "signature": "",
                "isExternal": false,
                "code": "import(, requests)",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": "UNKNOWN_IMPORT"
            }
        ],
        "edges": [
            {
                "src": 27,
                "dst": 23,
                "label": "CFG"
            },
            {
                "src": 12,
                "dst": 13,
                "label": "CFG"
            },
            {
                "src": 23,
                "dst": 27,
                "label": "REACHING_DEF"
            },
            {
                "src": 15,
                "dst": 13,
                "label": "POST_DOMINATE"
            },
            {
                "src": 12,
                "dst": 15,
                "label": "REACHING_DEF"
            },
            {
                "src": 13,
                "dst": 15,
                "label": "REACHING_DEF"
            },
            {
                "src": 15,
                "dst": 13,
                "label": "AST"
            },
            {
                "src": 23,
                "dst": 27,
                "label": "POST_DOMINATE"
            },
            {
                "src": 13,
                "dst": 15,
                "label": "DOMINATE"
            },
            {
                "src": 15,
                "dst": 12,
                "label": "ARGUMENT"
            },
            {
                "src": 15,
                "dst": 13,
                "label": "ARGUMENT"
            },
            {
                "src": 12,
                "dst": 13,
                "label": "DOMINATE"
            },
            {
                "src": 27,
                "dst": 23,
                "label": "DOMINATE"
            },
            {
                "src": 15,
                "dst": 11,
                "label": "REACHING_DEF"
            },
            {
                "src": 15,
                "dst": 12,
                "label": "AST"
            },
            {
                "src": 11,
                "dst": 27,
                "label": "REACHING_DEF"
            },
            {
                "src": 13,
                "dst": 15,
                "label": "CFG"
            },
            {
                "src": 13,
                "dst": 12,
                "label": "POST_DOMINATE"
            }
        ]
    },
    "paths": [
        [
            23,
            27
        ],
        [
            12,
            15,
            11,
            27
        ],
        [
            13,
            15,
            11,
            27
        ],
        [
            15,
            11,
            27
        ]
    ]
}

Рисунок 2. Структура вывода срезов потока данных

В алгоритме получения срезов этого типа в начале из графа извлекаются все узлы с меткой «CALL», соответствующие ряду условий: являются внешними по отношению к модулю, не являются операторами, вызовами исключений и анонимными функциями.

Для кода из листинга 1 такому набору ограничений соответствует только вызов функции get. Использование этой функции связано с идентификатором «requests». Если обратиться вновь к PDG на рисунке 9, можно увидеть, как другие идентификаторы связаны с ними. Важно помнить, что здесь применяется обратный обход графа.

Рисунок 3. Подграф CPG, соответствующий срезу потока данных

Разберём немного усложненный пример:

import requests as req

def run(): 
	url = input()
	response = req.get(url)
	rett = req.post("http://example.com/")
	return rett.status_code
    
print(run())

Центральными точками будут вызовы get(url) и post(”http://example.com/”). В срезе потоков данных обозначим все пути, которые ведут к этим идентификаторам.

Результат нарезки кода из листинга 3
{
    "graph": {
        "nodes": [
            {
                "id": 42,
                "label": "LITERAL",
                "name": "",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "\"http://example.com/\"",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>.run",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 6,
                "columnNumber": 18,
                "tags": ""
            },
            {
                "id": 17,
                "label": "IDENTIFIER",
                "name": "req",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "req",
                "typeFullName": "requests.py:<module>",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": ""
            },
            {
                "id": 19,
                "label": "LITERAL",
                "name": "",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "requests",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": ""
            },
            {
                "id": 31,
                "label": "CALL",
                "name": "input",
                "fullName": "__builtin.input",
                "signature": "",
                "isExternal": true,
                "code": "input()",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>.run",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 4,
                "columnNumber": 8,
                "tags": ""
            },
            {
                "id": 59,
                "label": "CALL",
                "name": "run",
                "fullName": "main.py:<module>.run",
                "signature": "",
                "isExternal": false,
                "code": "run()",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 9,
                "columnNumber": 7,
                "tags": ""
            },
            {
                "id": 32,
                "label": "IDENTIFIER",
                "name": "url",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "url",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>.run",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 4,
                "columnNumber": 2,
                "tags": ""
            },
            {
                "id": 46,
                "label": "IDENTIFIER",
                "name": "req",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "req",
                "typeFullName": "requests.py:<module>",
                "parentMethodName": "main.py:<module>.run",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 6,
                "columnNumber": 9,
                "tags": ""
            },
            {
                "id": 20,
                "label": "LITERAL",
                "name": "",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "req",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": ""
            },
            {
                "id": 38,
                "label": "IDENTIFIER",
                "name": "req",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "req",
                "typeFullName": "requests.py:<module>",
                "parentMethodName": "main.py:<module>.run",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 5,
                "columnNumber": 13,
                "tags": ""
            },
            {
                "id": 18,
                "label": "LITERAL",
                "name": "",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": ""
            },
            {
                "id": 22,
                "label": "CALL",
                "name": "import",
                "fullName": "<unknownFullName>",
                "signature": "",
                "isExternal": false,
                "code": "import(, requests, req)",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 1,
                "columnNumber": 1,
                "tags": "UNKNOWN_IMPORT"
            },
            {
                "id": 34,
                "label": "IDENTIFIER",
                "name": "url",
                "fullName": "",
                "signature": "",
                "isExternal": false,
                "code": "url",
                "typeFullName": "ANY",
                "parentMethodName": "main.py:<module>.run",
                "parentMethodSignature": "",
                "parentFileName": "main.py",
                "parentPackageName": "",
                "parentClassName": "",
                "lineNumber": 5,
                "columnNumber": 21,
                "tags": ""
            }
        ],
        "edges": [
            {
                "src": 38,
                "dst": 46,
                "label": "REACHING_DEF"
            },
            {
                "src": 20,
                "dst": 22,
                "label": "DOMINATE"
            },
            {
                "src": 17,
                "dst": 46,
                "label": "REACHING_DEF"
            },
            {
                "src": 22,
                "dst": 20,
                "label": "ARGUMENT"
            },
            {
                "src": 34,
                "dst": 38,
                "label": "POST_DOMINATE"
            },
            {
                "src": 17,
                "dst": 38,
                "label": "REACHING_DEF"
            },
            {
                "src": 19,
                "dst": 20,
                "label": "DOMINATE"
            },
            {
                "src": 46,
                "dst": 42,
                "label": "CFG"
            },
            {
                "src": 20,
                "dst": 22,
                "label": "REACHING_DEF"
            },
            {
                "src": 19,
                "dst": 20,
                "label": "CFG"
            },
            {
                "src": 46,
                "dst": 42,
                "label": "DOMINATE"
            },
            {
                "src": 18,
                "dst": 22,
                "label": "REACHING_DEF"
            },
            {
                "src": 22,
                "dst": 19,
                "label": "AST"
            },
            {
                "src": 42,
                "dst": 46,
                "label": "POST_DOMINATE"
            },
            {
                "src": 34,
                "dst": 38,
                "label": "REACHING_DEF"
            },
            {
                "src": 19,
                "dst": 22,
                "label": "REACHING_DEF"
            },
            {
                "src": 22,
                "dst": 17,
                "label": "REACHING_DEF"
            },
            {
                "src": 20,
                "dst": 22,
                "label": "CFG"
            },
            {
                "src": 20,
                "dst": 19,
                "label": "POST_DOMINATE"
            },
            {
                "src": 22,
                "dst": 19,
                "label": "ARGUMENT"
            },
            {
                "src": 32,
                "dst": 34,
                "label": "REACHING_DEF"
            },
            {
                "src": 22,
                "dst": 18,
                "label": "AST"
            },
            {
                "src": 31,
                "dst": 32,
                "label": "REACHING_DEF"
            },
            {
                "src": 22,
                "dst": 20,
                "label": "AST"
            },
            {
                "src": 38,
                "dst": 34,
                "label": "CFG"
            },
            {
                "src": 38,
                "dst": 34,
                "label": "DOMINATE"
            },
            {
                "src": 42,
                "dst": 46,
                "label": "REACHING_DEF"
            },
            {
                "src": 22,
                "dst": 20,
                "label": "POST_DOMINATE"
            },
            {
                "src": 18,
                "dst": 19,
                "label": "CFG"
            },
            {
                "src": 18,
                "dst": 19,
                "label": "DOMINATE"
            },
            {
                "src": 19,
                "dst": 18,
                "label": "POST_DOMINATE"
            },
            {
                "src": 38,
                "dst": 34,
                "label": "REACHING_DEF"
            },
            {
                "src": 22,
                "dst": 18,
                "label": "ARGUMENT"
            }
        ]
    },
    "paths": [
        [
            31,
            32,
            34,
            38,
            46
        ],
        [
            20,
            22,
            17,
            38,
            34
        ],
        [
            20,
            22,
            17,
            46
        ],
        [
            18,
            22,
            17,
            38,
            34
        ],
        [
            22,
            17,
            38,
            46
        ],
        [
            42,
            46
        ],
        [
            19,
            22,
            17,
            38,
            34
        ],
        [
            18,
            22,
            17,
            46
        ],
        [
            19,
            22,
            17,
            46
        ]
    ]
}

Рисунок 4. Подграф CPG достижимости идентификаторов, связанных с вызовом внешним вызовом

Результаты срезов для более сложных проектов можно найти в репозитории от разработчиков Atom.

Evinse

Вы уже могли и забыть, зачем здесь все эти графы, срезы…А мы напомним! Нам необходимо получить вхождения и стек вызовов, чтобы однозначно утверждать об использовании компонента в проекте.

Получить расширенный BOM можно с помощью утилиты «Evinse» — она входит в проект cdxgen. В начале необходимо запустить из корневой папки проекта cdxgen в «deep» режиме для получения обычного BOM, а затем ввести команду наподобие:

Получить расширенный BOM можно с помощью утилиты — она входит в проект cdxgen. В начале необходимо запустить из корневой папки проекта cdxgen в «deep» режиме для получения обычного BOM, а затем ввести команду наподобие:

evinse -i bom.json -o bom.evinse.json -l javascript --with-reachables . -p

По умолчанию она запускает только расчет Usage Slices, из которого, как мы увидим далее, можно получить вхождения внешних библиотек проекта. Call Stack рассчитывается по срезам потока данных. Для этого необходимо добавить флаги «‑with‑reachables» и/или «‑with‑dataflow» соответственно.

Occurrences

Начнем с простого режима. Запустим его, чтобы получить occurences для кода из листинга 1:

python3 -m venv venv; source venv/bin/activate 
cdxgen -t python -o bom.json --deep .
evinse -i bom.json -o bom.evinse.json -l python . -p

Результаты нарезки для этого кода были представлены на листинге 1, а SBOM отображен на рисунке 2 в предыдущей части статьи. Из них мы видим, что модуль «requests» импортируется в первой строке. При этом вызов функции «get» в occurences не попадёт. Аналогичный результат будет для кода из листинга 9.

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

Reachable Call Stack

Для получения стека вызовов вернемся к примеру js-кода:

cdxgen -t javascript -o bom.json --deep . 
evinse -i bom.json -o bom.evinse.json -l javascript --with-reachables . -p

Рассмотрим SBOM, описывающий трассировку стека для библиотеки http:

{
"purl": "pkg:npm/http@0.0.1-security",
"evidence": {
    "identity": {
        "field": "purl",
     },
    "occurrences": [
        {
            "location": "main.mjs#1"
        }
    ],
    "callstack": {
        "frames": [
            {
                "package": "<global>",
                "module": "main.mjs::program",
                "function": ":program",
                "line": 9,
                "column": 9,
                "fullFilename": "main.mjs"
            },
            {
                "package": "<global>",
                "module": "main.mjs::program",
                "function": "anonymous",
                "line": 9,
                "column": 37,
                "fullFilename": "main.mjs"
            },
            {
                "package": "<global>",
                "module": "main.mjs::program",
                "function": "anonymous",
                "line": 11,
                "column": 9,
                "fullFilename": "main.mjs"
            },
            {
                "package": "<global>",
                "module": "main.mjs::program",
                "function": "myfunc",
                "line": 5,
                "column": 16,
                "fullFilename": "main.mjs"
            },
            {
                "package": "<global>",
                "module": "main.mjs::program",
                "function": "myfunc",
                "line": 6,
                "column": 10,
                "fullFilename": "main.mjs"
            }
        ]
    }
  }
}

Эта трассировка полностью соответствует потоку, описанному в reachables[0].flows. Как мы уже говорили, это показывает цепочку от использования библиотеки до некоторой точки «выхода».

Data-flow Call Stack

Стек вызовов также можно получить из срезов потока данных:

python3 -m venv venv; source venv/bin/activate 
cdxgen -t python -o bom.json --deep .
evinse -i bom.json -o bom.evinse.json -l python --with-dataflow . -p

В результате трассировка вызовов будет иметь вид пути от импорта библиотеки до ее непосредственного использования в функции «get».

"callstack": {
    "frames": [
        {
            "package": "",
            "module": "",
            "function": "main.py:<module>",
            "line": 1,
            "column": 1,
            "fullFilename": "main.py"
        },
        {
            "package": "",
            "module": "",
            "function": "main.py:<module>",
            "line": 1,
            "column": 1,
            "fullFilename": "main.py"
        },
        {
            "package": "",
            "module": "",
            "function": "main.py:<module>",
            "line": 1,
            "column": 1,
            "fullFilename": "main.py"
        },
        {
            "package": "",
            "module": "",
            "function": "main.py:<module>.main",
            "line": 4,
            "column": 13,
            "fullFilename": "main.py"
        }
    ]
}

Этот метод хоть и считается более точным, но работает медленнее.

Summary

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

Сделаем некоторое саммари по статье, чтобы вам не приходилось обращаться к ChatGPT :) Основная идея заключается в построении графа свойств исходного кода. На его базе формируются срезы использования, на основе которых создаются occurences в SBOM. Из графа потока управления, одного из слоев CPG, образуются два типа срезов — прямые и обратные, а из них формируется call stack.

Вот так математика из университетских времен может помочь как разработчику, так и инженеру по информационной безопасности.

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

Используемая литература

Weiser, Mark D.. “Program Slicing.IEEE Transactions on Software Engineering SE-10 (1981): 352–357.

F. Yamaguchi, N. Golde, D. Arp and K. Rieck, “Modeling and Discovering Vulnerabilities with Code Property Graphs,” 2014 IEEE Symposium on Security and Privacy, Berkeley, CA, USA, 2014, pp. 590–604, doi: 10.1109/SP.2014.44.

Learning Type Inference for Enhanced Dataflow Analysis

Seidel, L., Baker Effendi, S.D., Pinho, X., Rieck, K., van der Merwe, B., Yamaguchi, F. (2024). Learning Type Inference for Enhanced Dataflow Analysis. In: Tsudik, G., Conti, M., Liang, K., Smaragdakis, G. (eds) Computer Security — ESORICS 2023. ESORICS 2023. Lecture Notes in Computer Science, vol 14 347