Skip to content

CPG слой: интеграция Joern

Документ фиксирует, что именно проверено про Joern применительно к нашему корпусу, и какие выводы из этого следуют для конвейера. Составлен по первоисточникам (исходники joernio/joern, официальная документация), а не по памяти: каждый факт помечен уровнем достоверности.


1. Ключевое открытие, меняющее предусловия

php2cpg не содержит собственного парсера PHP. Он вызывает внешний php с бандленным php-parser.phar.

Подтверждено по исходникам:

  • joern-cli/frontends/php2cpg/README.md: «A PHP to CPG converter based on php-parse. Requirements: PHP (>=7.0)»;
  • PhpParser.scala, метод phpParseCommand: Seq("php", "--php-ini", phpIniPath, phpParserPath) ++ Seq("--with-recovery", "--resolve-names", "--json-dump") ++ filenames;
  • Php2Cpg.createCpg → isPhpVersionSupported запускает ExternalCommand.run(Seq("php", "--version")) и требует ≥ 7.1.0; иначе Failure("php not found or version not supported").

Следствие для конвейера: без php в PATH фронтенд не деградирует, а отказывает. Поэтому в cpg.py появилась отдельная проверка find_php(), а окружение процесса собирается так, чтобы каталог с php был первым в PATH — фронтенд вызывает буквально php, полный путь к php.exe не подходит.

На этой машине: PHP 8.5 найден (C:\tools\php85\php.exe).


2. Проверенные факты о структуре и запуске

Факт Источник Уровень
Фронтенды и их скрипты лежат в joern-cli/frontends/<имя>/ joern-cli/build.sbt: frontendMappings("php2cpg", …) → ref -> s"frontends/$frontendName/$name" подтверждено
Утилиты joern, joern-parse, joern-export, joern-slice — в joern-cli/bin/ механика sbt-native-packager (JavaAppPackaging) вывод, не цитата
Язык задаётся --language PHP (регистр не важен) таблица frontends; buildLanguageList — «Available languages (case insensitive)» подтверждено
--frontend-args передаёт аргументы фронтенду дословно CpgBasedTool.scala: ARGS_DELIMITER = "--frontend-args" подтверждено
--exclude <путь> исключает файлы (путь относительно входа) docs.joern.io/frontends/x2cpg подтверждено
Скриптовый режим требует @main def exec(...) и --param k=v docs.joern.io/interpreter подтверждено
Результат запроса пишется оператором #> в файл docs.joern.io/shell подтверждено
PHP_PARSER_BIN переопределяет путь к парсеру PhpParser.scala: private val PhpParserBinEnvVar = "PHP_PARSER_BIN" подтверждено
Фронтенд всегда передаёт --with-recovery phpParseCommand подтверждено

Не проверено (требует запуска на распакованном дистрибутиве): - точный состав joern-cli/bin и наличие .bat-двойников; - наличие флага одиночной команды у joern (-c/--command) — в документации его нет, в конвейер не закладывается; - поведение -J-Xmx именно у joern-parse.bat.


3. Как извлекать данные (проверено по коду экспортёра)

cpg.bin — бинарник flatgraph, из Python напрямую не читается. Нужен экспорт.

Что нужно --repr Совместимые форматы
CFG / DDG / CDG / PDG cfg/ddg/cdg/pdg только dot
Весь граф со всеми типами рёбер all graphml, graphson, neo4jcsv, dot
Подграф на метод cpg graphml, graphson, neo4jcsv, dot

Из JoernExport.scala: в exportWithFlatgraphFormat ветки есть ровно для All и Cpg, для прочих — throw new NotImplementedError(...). То есть комбинация --repr=pdg --format=graphml не работает.

Принятое решение: основной путь — CPGQL-скрипты с выводом в JSON. Это даёт точный контроль над тем, что именно мы извлекаем (нам нужны вызовы и методы, а не весь граф), и не требует парсинга GraphML/DOT. Скрипты находятся в src/ragraf/cpg.py (QUERY_CALLS, QUERY_METHODS, QUERY_TYPES).

Дополнительно: joern-export сам достраивает оверлеи данных (addDataFlowOverlayIfNonExistent вызывает OssDataFlow), поэтому отдельный прогон оверлея для PDG не нужен.


4. Ограничения php2cpg, критичные для нашего корпуса

4.1. Синтаксис ограничен PHP 8.2

Бандленный парсер — nikic/PHP-Parser ветки 4.x:

  • MODULE.bazel: PHP_PARSER_VERSION = "4.15.10";
  • README форка: «This is a PHP 5.2 to PHP 8.2 parser written in PHP».

Пилот — PHP 8.4. Новые конструкции (property hooks, асимметричная видимость) в грамматике 4.x отсутствуют → синтаксические ошибки.

Смягчающий фактор: фронтенд всегда передаёт --with-recovery, поэтому ожидается неполный AST, а не падение. Насколько неполный — проверяется на прогоне.

Что делать: считать это в метрике полноты. В отчёте появится отдельная строка о файлах, которые php2cpg не разобрал (в логах это Failed to process '<файл>' — предупреждение, при котором файл молча выпадает; такое надо ловить).

4.2. Нет composer.json → неполное разрешение имён

buildFiles ищет только composer.json: SourceFiles.determine(config.inputPath, Set(".json"), …).filter(_.endsWith("composer.json")). У пилота composer.json нет (PHP без фреймворка, автозагрузка через spl_autoload_register).

Смягчающий фактор: фронтенд проходит код дважды — SymbolSummaryPass собирает символы по всем пространствам имён, затем AstCreationPass строит AST с этой сводкой («parses the code twice… This helps resolve symbols during the latter parse»). Связи внутри проекта частично разрешатся.

4.3. JDK 21 вне протестированного диапазона

docs.joern.io/installation: «JDK 19 … newer JDK versions might work, but have not been properly tested». У нас JDK 21.

Отдельное предупреждение в документации срезов: «Class path issues related to Circe are known to rear its head on Java versions later than 17» — касается именно joern-slice.

Что делать: если joern-slice упадёт на JDK 21, альтернатива — срезы нашими средствами через CPGQL (обход CFG/DDG), либо установка JDK 19. Это выяснится на первом прогоне.

4.4. Инкрементального обновления CPG нет

Ни joern-parse, ни joern-export, ни документация не описывают инкрементальную пересборку; joern-scan --overwrite описан как полная регенерация.

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


5. Связь CPG с нашим графом

Узел CPG Ключевые свойства Ключ склейки с нашими узлами
FILE name путь исходника
METHOD name, fullName, filename, lineNumber, lineNumberEnd, isExternal (filename, lineNumber, name)
TYPE_DECL name, fullName, filename, lineNumber классы, интерфейсы, traits, enum
CALL name, code, methodFullName, dispatchType, lineNumber, columnNumber methodFullName — то, чего нет в tree-sitter индексе
IDENTIFIER, LITERAL, LOCAL, PARAMETER name, code, lineNumber, typeFullName переменные и значения

dispatchType различает STATIC_DISPATCH и DYNAMIC_DISPATCH — это готовый признак для разделения доказанных и эвристических связей.

Реализованное разрешение имён (Indexer._make_fqn_resolver): точное совпадение полного имени → совпадение имени метода и файла → отказ. Третий исход принципиален: в PHP без фреймворка короткие имена (get, run) есть у десятков классов, и привязка «по имени» дала бы ровно те выдуманные связи, от которых защищает принцип P3 архитектуры.

Не проверено: база нумерации строк у php-parser (1-based или 0-based). Проверяется сверкой на первом прогоне; при расхождении на 1 нужна поправка в ключе склейки.


6. Что осталось выяснить запуском

Вопрос Как проверяется
Наличие и точные имена исполняемых файлов в дистрибутиве scripts/install-joern.ps1 (делает это автоматически)
Работает ли joern-parse --language PHP на нашем корпусе первый прогон стадии CPG
Сколько файлов php2cpg не разберёт из-за PHP 8.4 сверка с логом Joern и с результатом tree-sitter (у нас 0 ошибок разбора)
Время сборки CPG на 112 тыс. строк PHP замер стадии cpg
Потребление памяти и нужный -Xmx замер + наблюдение
Совпадает ли lineNumber CPG с нашими строками сверка 20 методов вручную
Работает ли joern-slice на JDK 21 отдельный прогон

7. Состояние на момент написания

Компонент Состояние
JDK 21 (Temurin 21.0.12.1) ✅ установлен в .tools/jdk, проверен запуском
PHP CLI 8.5 ✅ найден (C:\tools\php85\php.exe) — предусловие php2cpg выполнено
Joern 4.0.99 ✅ установлен в .tools/joern-cli (1633 МБ), php подтверждён в --list-languages
Слой CPG в конвейере (src/ragraf/cpg.py) ✅ работает: capability=full на пилоте
Разбор экспорта (src/ragraf/joern_export.py) ✅ neo4jcsv через ijson
Стадия cpg в индексаторе ✅ слияние подтверждённых вызовов с нашим графом
Скрипт установки ✅ scripts/install-joern.ps1

8. Что выяснилось только запуском (факты, которых нет в документации)

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

№ Обнаружено Как проявлялось Решение
1 php2cpg требует внешний php в PATH (в нём нет собственного парсера) без PHP фронтенд не стартует вообще find_php() + подстановка каталога с php первым в PATH
2 php2cpg не принимает --exclude Unknown option --exclude → RuntimeException: unable to parse XTypeStubsParserConfig отсев служебных путей ПОСЛЕ экспорта (is_noise_file)
3 Скриптовый режим (--script) на JDK 21 не работает ошибка компиляции: нет predef-скрипта (importCpg, cpg не найдены) полностью перешли на joern-export
4 Экспорт GraphSON падает по памяти даже на 18 ГБ OutOfMemoryError: Required array length 2147483640 + 8 is too large формат neo4jcsv: потоковые CSV, 223 МБ, 27 с
5 --repr=cpg создаёт файл на каждый метод и падает 731 файл, 739 МБ, FileSystemException --repr=all + фильтрация при разборе
6 JAVA_OPTS не доходит до JVM OutOfMemoryError при любом значении переменной лимит передаётся аргументом -J-Xmx18g (форма без =)
7 Пути Joern не совпадают с нашими ведущая точка и обратные слеши; сопоставление всегда давало отказ normalize_joern_path
8 Методы Joern пишутся через ->, а не :: App\Core\Config->get не сопоставлялось ни с чем normalize_joern_name; результат: 411 связей по полному имени
9 AST-обход без границы метода захватывает всё заглушка <global> есть в каждом файле и содержит весь файл: 179 330 из 183 357 вызовов получили её владельцем остановка обхода на вложенных METHOD
10 В экспорте Joern массив вершин называется vertices, а не nodes разбор давал 0 узлов при 83 542 разобранных рёбрах ключ @value.vertices.item; свойства — в properties под именами в ВЕРХНЕМ РЕГИСТРЕ
11 Большинство CALL — операторы языка 50 тыс. из 183 тыс. — <operator>.assignment _is_noise_call; осталось 17 822 настоящих вызова
12 Встроенные функции PHP надо классифицировать отдельно 13 008 из 17 822 «не разрешено» были array_push, is_array, Iterator.next _is_builtin_target: 7 819 распознаны как норма, честный остаток — 4 985

Урок для проекта. Из 12 пунктов 7 приводили к молчаливой потере данных: код не падал, но результат оказывался нулевым или искажённым. Такие дефекты ловятся только сверкой независимых путей — ровно как было с CPGQL (работал в теории, не работал на практике) и с фильтром <global> (формально обход корректен, фактически почти все данные у одного владельца).


9. Результат на пилоте

Показатель Значение
capability full (CPG подключена)
Время полной индексации с CPG 71 с (из них CPG — 64 с)
Сборка CPG 18.6 МБ, ~50 с на 323 файла
Экспорт 223.8 МБ, 132 файла, ~27 с
Значимых вершин извлечено 193 442
Рёбер извлечено 3 348 516 (в т.ч. 444 464 CFG, 930 567 потоков данных)
Вызовов после фильтрации 17 822 из 183 357
Связей подтверждено в графе 603 (411 по полному имени, 192 по имени и файлу)
Встроенных функций PHP распознано 7 819
Честно не разрешено 4 985
slice работает (был недоступен в режиме degraded)
Влияние на M2 recall 0,771 → 0,858; медиана 1,000; нулевых случаев 0 (было 1); полная связность у 13 из 20 (было 11)