Перейти к основному содержимому

Как GoCPG строит CPG и чем этот подход отличается от Joern

Техническое устройство CPG-конвейера GoCPG, восстановление типов Python и JavaScript, структурный анализ и правила честного сравнения с Joern.

Корпоративная версия

Code Property Graph — не «карта файлов» и не результат одного парсера. Это единая модель программы, где синтаксическая структура связана с потоком управления, вызовами, типами, зависимостями данных и исходными координатами. CodeGraph использует эти связи для поиска дефектов, архитектурных нарушений и метрик аудита.

Эта статья отвечает на два вопроса:

  1. как текущий GoCPG строит и обогащает граф;
  2. что можно честно сравнивать с Joern без неподтверждённых заявлений о скорости или точности.

Формулы, которые превращают замечания и метрики графа в Q1–Q12 и Health Score, описаны в методологии Dashboard.

Коротко: где проходит граница GoCPG

GoCPG отвечает за построение и хранение CPG проекта. Языковой адаптер (frontend) создаёт базовые узлы и связи, затем упорядоченные проходы анализа (passes) добавляют семантические слои. Результат сохраняется в DuckDB; CodeGraph получает типизированные результаты через сервисные контракты. Прикладной слой не строит второй «канонический» граф поверх первого.

Состав конвейера определяется текущим кодом и профилем. Поэтому документация не фиксирует маркетинговое число проходов: стандартный набор может расширяться доменной разметкой, VCS-enrichment и профильными анализаторами.

1. От исходников до базового графа

Выбор языкового адаптера

GoCPG регистрирует отдельные языковые адаптеры для C, C++, Go, Python, JavaScript, TypeScript, Java, Kotlin, C#, PHP и 1C. Большинство используют tree-sitter. Go использует go/parser и go/ast, а 1C — собственный lexer/parser. Для части языков доступен упрощённый fallback без полной семантической точности.

Frontend отвечает за то, что можно извлечь непосредственно из текста:

  • файлы, namespaces/modules, типы, методы, параметры и локальные переменные;
  • вызовы, литералы, идентификаторы, блоки и управляющие конструкции;
  • импорты, аннотации, комментарии и координаты в исходном коде;
  • связи AST, аргументов, получателей вызова и исходных файлов.

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

Материализация

Полная сборка проходит так:

  1. GoCPG находит файлы, применяет include/exclude rules и расширения выбранного языкового адаптера.
  2. Frontend разбирает файлы параллельно и формирует DiffGraph. Ошибки сохраняются по файлам, а не исчезают из результата.
  3. Идентификаторы адаптера синхронизируются с глобальным генератором, чтобы добавленные проходами узлы не столкнулись с исходными.
  4. Базовый diff записывается в DuckDB и одновременно становится in-memory CPGGraph для следующих стадий.
  5. Семантические проходы читают уже построенные слои, создают новые узлы/рёбра и сохраняют изменения.

Неполный разбор, пропущенный файл или недоступный адаптер ограничивает основу, а не «чистый» результат.

2. Семантический конвейер

Порядок важен: call graph нельзя надёжно строить до imports и type recovery, а PDG — до control/data-flow.

2. Семантический конвейер
Стадия Что добавляется Для чего используется
Базовая топология metadata, files, namespaces, type nodes/declarations, inheritance, parameter links Единая идентичность сущностей и типов.
Восстановление типов типы идентификаторов и вызовов, origin/confidence для call nodes Более точные FQN и цели вызовов в динамических языках.
Поток управления CFG, dominators, post-dominators, CDG Достижимость, ветвления и зависимости управления.
Imports и вызовы import maps, Call FQN propagation, call resolution, dynamic dispatch, FFI edges Межфайловые и межъязыковые связи вызовов.
Поток данных references, alias analysis, reaching definitions, interprocedural propagation Источник значения, побочные эффекты и пути между методами.
Анализ рисков неинициализированные переменные, диапазоны значений, утечки ресурсов, разыменование null, конкурентность, taint, переполнение целых и буферов Замечания с привязкой к графовому пути и исходному коду.
Зависимости программы EVAL_TYPE, return type propagation, PDG и DDG Совместный анализ control- и data-dependencies.
Объектная модель bindings, vtables, dynamic call linking Интерфейсные и виртуальные вызовы, constructor-to-initializer links.
Обогащение метрики методов, комментарии, вложенность, замечания, структурный поиск шаблонов Метрики аудита, поиск шаблонов и объяснимые результаты.
Опциональные слои domain annotations, use-case propagation, VCS tags Доменный контекст, точки входа, авторство и частота изменений.

Каждый проход объявляет зависимости и поддержку инкрементального выполнения. Параллельно могут выполняться только независимые части DAG; «параллельный конвейер» не означает нарушение семантического порядка.

3. Восстановление типов в Python и JavaScript

Для компилируемого проекта тип часто доступен из объявления или toolchain. В Python и JavaScript один и тот же идентификатор может получить тип только во время выполнения. GoCPG не притворяется, что эта неопределённость исчезает: он собирает статические свидетельства, применяет их итеративно и сохраняет происхождение и уверенность там, где это предусмотрено схемой.

Источники типовых фактов

Первая, внутрипроцедурная стадия использует:

  • аннотации и уже извлечённые type declarations;
  • присваивания вида x = ClassName(...);
  • constructor calls;
  • значения локальных переменных и одноимённые identifiers;
  • self/this-поля и member assignments;
  • Python isinstance guards;
  • return types встроенных функций и методов из language-specific Type Knowledge Base.

Для Python и JavaScript есть отдельные наборы встроенных функций и типовых заглушек. Это не полное окружение выполнения: пользовательская метапрограммная логика, monkey patching, eval, динамические импорты и произвольное изменение object shape могут остаться неразрешёнными.

Итерации и сходимость

На последующих итерациях GoCPG распространяет:

  • возвращаемый тип от разрешённого метода к месту вызова;
  • тип результата вызова к переменной в присваивании;
  • тип из return statements и properties;
  • факты динамической диспетчеризации из Type Knowledge Base;
  • временный <returnValue> type для вызова, цель которого известна, а точный return type — нет.

Обновления сначала собираются параллельно, затем применяются детерминированно. По умолчанию конфигурация сохраняет совместимость с тремя базовыми итерациями, но adaptive loop может продолжаться до десяти. После третьей итерации он останавливается, если отношение новых обновлений к предыдущей итерации ниже 1%, или раньше, если обновлений нет.

Для call nodes сохраняются type_origin и type_confidence. Примеры источников: builtin, tkb, constructor, propagated, inferred. Факты конструктора и встроенных функций имеют более высокую уверенность, временный <returnValue> — более низкую. Это позволяет downstream-анализу отличить явный факт от приближения.

Как типы улучшают граф вызовов

После type recovery отдельный pass переписывает method_full_name вызова, используя восстановленный тип receiver и import map. Затем call resolution связывает call node с наиболее подходящим method node, а dynamic linker добавляет virtual dispatch и constructor-to-__init__ связи.

Если данных недостаточно, вызов остаётся неразрешённым или приблизительным. Наличие ребра вызова не доказывает достижимость во время выполнения, а его отсутствие при динамической магии не доказывает отсутствие вызова. Security и architecture rules должны учитывать confidence и completeness basis.

4. Потоки управления и данных

GoCPG строит несколько связанных представлений:

  • CFG показывает допустимый порядок выполнения внутри метода;
  • dominator и post-dominator trees нужны для анализа обязательных путей;
  • CDG связывает узел с условием, от которого зависит его выполнение;
  • REF и reaching-definition edges связывают использование со значением;
  • alias analysis уточняет возможные ссылки на один объект;
  • межпроцедурные reaching definitions переносят факты через места вызова;
  • DDG материализует зависимости данных;
  • PDG объединяет зависимости данных и управления.

Taint, resource-leak, null-dereference и другие анализаторы работают поверх этих слоёв. Поэтому замечание должно сохранять сообщение вместе с координатой в исходном коде, связанный узел и, где применимо, путь доказательства.

Разные CPG-реализации могут материализовать разные вспомогательные узлы и рёбра. Сырое количество узлов, CALL или CDG само по себе не показывает точность без размеченных ожидаемых фактов в размеченном наборе.

5. Структурный анализ Architecture v1

Новый структурный анализ не сводится к поиску импортов. Он классифицирует сущности CPG по компонентам и проверяет разрешённые отношения между ними.

Модель и правила

  • Каждая сущность, участвующая в классификации, получает стабильный UID компонента. Отображаемый id можно менять; uid входит в identity и fingerprint.
  • Parent components группируют листья, но сами не классифицируют сущности.
  • Область, слой, тип, платформа и среда выполнения — независимые измерения. Для обязательной проверки классификация должна быть полной и однозначной.
  • Architecture rules используют spec_version: "2.0", kind: architecture, стабильный rule ID и selectors all, any, not.
  • Preset и rule references всегда version-pinned: id@version. latest и неявное наследование статуса соседнего языка отклоняются.

Architecture v1 содержит три общих template и девять language presets: C, C++, C#, Go, JavaScript, Kotlin, PHP, Python и TypeScript. JavaScript и TypeScript имеют отдельные манифесты возможностей и результаты приёмки.

Полнота, baseline и waiver

Capability registry объявляет, какие семейства фактов даёт каждый language adapter. Недостающая обязательная capability не усредняется с успешными: обязательная проверка получает BLOCKED_INCOMPLETE.

Incremental run считается авторитетным только как incremental_complete, если его fingerprints, baseline/waiver states и semantic digest совпадают с полным запуском. Изменение публичного API, модели, правил, baseline, waiver, рёбер или SCC расширяет invalidation. Частичный результат вызывает один synchronous full retry; только свежий полный результат может заменить его.

Baseline связывает принятый долг с точным rule, fingerprint version, finding fingerprint и semantic hash. Файл в анализируемом репозитории сам по себе ничего не подавляет. Waiver ограничен по времени, требует разных requester/approver и подписывает проект, репозиторий, commit, ветку, хеши и ревизию. Ключи доверия и revocation state приходят из control plane или CI trust config, а не из проверяемого кода.

Результаты и release gate

Канонический JSON содержит запуск с привязкой к ревизии, доказательство полноты, результат каждого активного правила, замечания, покрытие и метрики. SARIF, Mermaid и PlantUML — детерминированные проекции того же результата; они не меняют fingerprint. Read-only gRPC QueryService позволяет list/export/validate/describe/explain сохранённый run.

Профили discovery и advisory не разрешают выпуск. enforce_new, enforce_all и strict могут вернуть PASS или FAIL_VIOLATIONS, но только при свежих и полных фактах и достаточном покрытии возможностей. Отсутствующее, устаревшее, частичное, ошибочное доказательство или нулевой знаменатель даёт BLOCKED_INCOMPLETE.

Эти результаты питают прежде всего Q2, Q4, Q6 и Q11 аудита, но не заменяют другие сценарии.

6. Карта решения: GoCPG и Joern

Joern также строит CPG через языковые адаптеры, добавляет семантические слои проходами и предоставляет язык запросов CPGQL на Scala. Официальная документация описывает интерактивную консоль, сценарии, серверный режим, расширяемые проходы и plugins. Frontend и набор overlays зависят от release, поэтому их нужно фиксировать в каждом сравнении.

6. Карта решения: GoCPG и Joern
Аспект GoCPG в CodeGraph Joern
Основной процесс Импорт проекта, автоматические анализаторы, Dashboard, CLI/MCP/REST и управляемые доказательства. Интерактивное исследование уязвимостей через запросы, сценарии, сканирование и серверный режим.
Хранение DuckDB в области проекта, атомарная запись и типизированные сервисные контракты. Собственная графовая база в памяти и модель проектов Joern.
Запросы Сервисы Go, gRPC и контролируемые интерфейсы проекта; SQL остаётся внутри границы хранения. CPGQL на Scala, REPL, сценарии и HTTP-сервер.
Расширение Языковые адаптеры, Go-проходы с зависимостями, правила шаблонов и архитектуры, доменные и VCS-слои. Языковые адаптеры, CPG-проходы, расширения запросов и плагины JVM.
Динамические типы Общий итеративный проход, отдельные типовые заглушки, происхождение/уверенность и распространение FQN. Реализация зависит от адаптера и слоёв зафиксированной версии.
Структурный контроль Версионированная модель и presets, реестр возможностей, подписанное управление, эквивалентность полного/инкрементального анализа и запрет при неполных данных. Архитектурные запросы и расширения строятся средствами CPGQL, проходов и плагинов; правила принятия решения нужно оценивать отдельно.

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

Официальные источники Joern для сверки:

7. Почему здесь нет старых цифр производительности

Историческая версия этой статьи содержала конкретные времена, числа узлов/рёбер и выводы о Python call graph. Эти измерения относились к старым версиям GoCPG и Joern, двум конкретным checkout и известной проблеме нормализации путей Python. Без исходного манифеста, журналов, digest корпуса и повторного запуска такие значения нельзя выдавать за состояние текущего продукта.

Техническая структура исходной статьи восстановлена, а численные выводы намеренно не перенесены. Это не отказ от сравнения, а граница проверяемости.

8. Воспроизводимый benchmark

Для численного вывода нужен неизменяемый манифест измерения (immutable benchmark manifest, далее benchmark manifest):

  • commit GoCPG/CodeGraph, точный release Joern и artifact digests;
  • commit корпуса, языки, правила включения/исключения и список ожидаемых фактов;
  • оборудование, ОС, ограничения выполнения, состояние кеша и параллельная нагрузка;
  • точные команды import, query, export и cleanup;
  • cold/warm policy, число повторов, timeout, failures и outlier treatment;
  • wall time, CPU, peak memory, storage и полнота результата;
  • raw logs, machine-readable output, digest и независимый reviewer.

Сопоставляйте import time и raw graph size с полным путём до полезного ответа:

  1. parser/import completeness;
  2. correctness и completeness на размеченных фактах;
  3. query latency после import;
  4. end-to-end время;
  5. unresolved calls и unsupported constructs;
  6. память, хранение и incremental update;
  7. operator effort, deployment, security и upgrade/rollback.

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

Источники истины GoCPG

  • gocpg/pkg/frontend и gocpg/docs/frontend-guide.md — языковые адаптеры и извлекаемые факты.
  • gocpg/pkg/cpg/schema — узлы, рёбра и свойства.
  • gocpg/pkg/passes/pipeline.go — текущий порядок семантических проходов.
  • gocpg/pkg/passes/types/type_recovery*.go и gocpg/pkg/typestubs — восстановление типов и Type Knowledge Base.
  • gocpg/pkg/passes/controlflow, dataflow, callgraph — CFG/CDG, data-flow и call graph.
  • gocpg/pkg/architecture и gocpg/docs/architecture/v1/ — структурный анализ, governance и форматы результатов.
  • gocpg/pkg/storage/duckdb и gocpg/api/proto/gocpg/v1 — хранение и типизированная интеграция.