TrustYFox: путь от пет-проекта до LLM-инструмента для поиска уязвимостей

TrustYFox: путь от пет-проекта до LLM-инструмента для поиска уязвимостей

Меня зовут Андрей, я руковожу службой разработки платёжных интерфейсов в Яндексе. Сегодня расскажу о TrustYFox — платформе на основе LLM для поиска уязвимостей в коде, которую я создал как пет-проект. Это не замена существующим сканерам, а дополнение, помогающее находить уязвимости, которые традиционные инструменты могут пропустить.

Эта история — не научная статья и не R&D. Я не эксперт в LLM, но за несколько месяцев мне удалось пройти путь от прототипа до рабочего решения, в котором ежедневно запускаются аудиты. За полгода разработки были проверены разные концепции, написан и переписан код. Ниже — рассказ о том, как всё начиналось, что получилось, и что можно было сделать лучше.

Как всё начиналось

К лету 2025 года команды в моём подразделении стали достаточно автономными: сами ведут проекты, договариваются со смежниками, проводят демо. Это освободило время для экспериментов.

К тому моменту я уже прошёл путь от эйфории к разочарованию в использовании LLM для разработки. Возникла идея: а что, если декомпозировать задачу, запускать её в дочерних агентах и управлять через MCP? Это могло бы эффективнее решать сложные задачи.

На одном из 1–1 с руководителем мы решили, что в финтехе безопасность — критически важна. Значит, стоит попробовать создать LLM-сканер уязвимостей в коде.

Создание прототипа

Я начал с изучения LangChain и выбрал TypeScript — мой основной язык. Первая ошибка: сразу попытался построить сложную мультиагентную систему на LangGraph. Фреймворк оказался сырым, и через выходные пришлось удалить большую часть кода.

Во втором подходе я начал с простого:

  • Чтение файлов.
  • Отправка чанков в LLM с требованием вернуть ответ в формате JSON.
  • Парсинг и проверка ответа другой LLM.

Проблем не возникло. Интеграция с YT и отображение результатов в DataLens тоже прошли легко. Уже через пару дней можно было смотреть список найденных уязвимостей в табличке.

Прототип работал и находил что-то. Значит, можно было двигаться дальше. Но CLI + DataLens выглядели несерьёзно — нужен был нормальный UI.

Первый подход к UI (v0-alpha)

Проект делался как пет-проект, поэтому я хотел заниматься только интересным, а всё остальное — брать из готового. Для UI выбрал Toolpad: он позволял быстро получить приличный интерфейс «из коробки».

К началу августа у меня уже был прототип с UI. Но я хотел вернуться к идее мультиагентной системы. Это стоило ещё нескольких выходных, но в итоге система с несколькими агентами заработала.

Вот как она работала:

  • Чтение файлов.
  • Получение ранее найденных уязвимостей (размеченных как false positive или «такое хотим видеть»).
  • Построение графа зависимостей через tree-sitter.
  • Роутинг файлов по агентам: LLM решает, в какие агенты отправить каждый файл.
  • Анализ:

Если язык поддерживается tree-sitter:

  • Получаем связанные файлы из графа зависимостей.
  • Через LLM выделяем важные части контекста.
  • Формируем полный контекст: содержимое файла, размеченные уязвимости, системный промпт агента.
  • Отправляем в LLM, парсим результат.

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

  • Сбор результатов, проверка агентов через LLM на постобработке.
  • Парсинг и возврат массива найденных уязвимостей.

Для этого пришлось ввести две ключевые сущности: агенты и граф зависимостей.

Агенты — это промпты вроде «Ты эксперт по SQL-инъекциям, проанализируй этот файл» и краткое описание для роутинга.

Граф зависимостей нужен, чтобы передавать LLM не только сам файл, но и связанный код. Поднимать language server для всех языков в Аркадии не хотелось. Решение — tree-sitter.

Tree-sitter — генератор парсеров, позволяющий строить синтаксические деревья и эффективно обновлять их. Я написал обёртку — библиотеку @trusty_fox/ast_indexer — и использовал её для поиска связанных файлов.

Анализ включал:

  • Извлечение импортов, экспорта, вызовов функций, названий классов и типов.
  • Построение графа с учётом специфики Аркадии.

Из-за ошибок LLM в запросах к tree-sitter я не стал покрывать все языки, а ограничился TypeScript, JavaScript, Python и Go — теми, что были под рукой.

Подготовка к продакшену (v0.1)

К 18 августа проект был развёрнут на виртуалке, и я мог делиться ссылкой. Заинтересовались коллеги из других команд — появилась обратная связь от реальной аудитории.

Решено было доводить проект до продакшена. Нужно было решить несколько задач:

  • Переезд с SQLite на PostgreSQL. Да, стоило сделать сразу, но сначала был фокус на скорости.
  • Сохранение состояния аудита: процессы могут падать или попадать под деплой.
  • Ролевая модель: разделить доступ между сотрудниками СИБ (полный) и обычными пользователями (ограниченный).
  • Отображение логов в UI — чтобы видеть, что происходит.
  • Перенос промптов из кода в базу данных.
  • Настройка CI/CD: деплой в несколько локаций, балансировка, мониторинг.
  • Придумать название и брендинг.

Название появилось из сочетания: я работаю в Trust, люблю лисичек, и есть Яндекс. Так родился TrustYFox. Логотип сгенерировала нейросеть.

TrustYFox перестал быть просто CLI-инструментом — он стал продуктом.

Отображение логов аудита

Логи нужно было выводить в UI, а не заставлять пользователей искать их в хранилище. Прототип изначально был библиотекой без UI, и логи писались в stdout. Но для фоновых задач нужен был иной подход.

Я внедрил структурированные логи с указанием скоупа (например, «обработка файлов», «анализ контекста»). Это позволило легко сделать древовидное представление логов в UI.

Сохранение стейта

Аудиты могут длиться долго и прерываться — из-за деплоя, падения воркера и т.д. Без сохранения состояния пришлось бы начинать с нуля.

Я переработал архитектуру и ввёл ReportAnalysisStateManager. Каждый этап аудита (роутинг, анализ, постобработка) теперь:

  • Сохраняет и получает результаты через state manager.
  • При старте проверяет, не завершён ли уже этап.

State manager также предоставляет AbortController, сигнал которого передаётся в запросы к LLM. Это даёт graceful shutdown.

Ролевая модель

В TrustYFox три роли:

  • Пользователь — только просмотр.
  • Аудитор — просмотр и запуск аудитов.
  • Админ — полный доступ, включая редактирование промптов.

Но был нюанс: нельзя было позволить всем сканировать Финтех. Даже при доверительной культуре доступ к финансам — суперсекьюрен.

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

Последние штрихи

К середине сентября TrustYFox запустили для небольшой группы пользователей. Но это был не финальный продакшен — нужно было сделать UI удобным и понятным.

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

Перенос промптов, настройка CI/CD и поднятие окружения прошли без особых сложностей — просто сделано.

Что происходит сейчас

Прошло почти четыре месяца с момента первого PoC. Сейчас я работаю над улучшением TrustYFox.

Запуск аудитов

Аудит — это задача поиска уязвимостей по выбранному пути с указанной конфигурацией (модели, промпты, агенты). Задачи хранятся в БД и выполняются воркером.

Чтобы можно было сравнивать результаты, ввели систему тегов для промптов и агентов. Теперь можно запустить два аудита — со stable- и экспериментальной версией — и сравнить, какая лучше.

Промпты и агенты

Промпты и агенты — это системные промпты. Разница в том, что:

  • Промпты — фиксированный набор правил для всех этапов аудита (роутинг, анализ, постобработка).
  • Агентов может быть сколько угодно, каждый специализируется на определённой уязвимости (например, SQL-инъекции).

На этапе анализа промпты агентов вставляются в тег <role>, формируя конкретную роль для LLM.

Теги и тюнинг промптов

Сначала промпты лежали в JSON внутри библиотеки. Это не подходило для продакшена — нужно было вынести в БД и обновлять через UI.

Но простое обновление не позволяло тестировать гипотезы. Я хотел сравнивать версии и выбирать лучшую. Варианты — использовать VCS или LLM-ревизии — оказались либо избыточными, либо неработающими.

Решение — теги:

  • Stable — версия по умолчанию.
  • Теги для промптов и агентов независимы — можно собирать любые комбинации.
  • Можно клонировать stable в другой тег и редактировать.
  • После обновления experimental можно запустить два аудита для сравнения.
  • Любой тег можно промоутить в stable: текущие сущности помечаются как удалённые (soft delete), а новый тег становится stable.

Конфигурации и модели

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

Найденные уязвимости и их разметка

После анализа и фильтрации уязвимости сохраняются в БД. Каждая включает:

  • Описание уязвимости.
  • Уязвимый код.
  • Рекомендуемый фикс.

Даже с контекстом LLM не всегда понимает, что считать false positive. Поэтому ввели петлю обратной связи: пользователи могут размечать уязвимости вручную.

Возможные действия:

  • False Positive — уязвимость логична, но не применима. Можно указать причину (на русском, LLM переведёт на английский).
  • Looks Good — найдено что-то ценное, хочется больше таких результатов.
  • Удалить — если это мусор, просочившийся через постобработку.

Tool Calling

Tool calling позволяет LLM вызывать инструменты из списка. Раньше использовался AST Indexer, но у него были ограничения:

  • Ограниченный набор языков.
  • Неточность построения графа зависимостей.
  • Недостаточный контекст — только ближайшие файлы.

Tool calling стал более гибким решением. Диаграмма взаимодействия:

  • LLM получает список инструментов, путь к файлу и промпт с просьбой собрать контекст.
  • Запускается цикл:
  • LLM запрашивает вызов инструмента (например, чтение файла).
  • TrustYFox вызывает инструмент и возвращает результат.
  • LLM в какой-то момент завершает цикл.

Tool calling работает лучше, но есть нюансы:

  • LLM может зацикливаться, повторяя одни и те же действия.
  • Упирается в лимиты — но это лучше, чем бесконечный цикл.
  • Проблемы с RegExp: LLM плохо генерирует корректные регулярки, пришлось перейти на поиск по подстроке.
  • Повторные вызовы одного инструмента с одинаковыми аргументами.
  • Добавление двоеточий в аргументы — вероятно, баг, но быстро не найден.

Что можно было сделать лучше

Ретроспективно есть несколько косяков:

  • Проект начинался как эксперимент без чёткого видения конечного результата.
  • Выбор SQLite был сделан ради скорости, но пришлось переезжать на PostgreSQL.
  • Стоило сразу идти в сторону tool calling. Время, потраченное на tree-sitter, можно было использовать эффективнее.
  • Много времени ушло на переделку решений, сделанных «лишь бы работало».

Что дальше

Проект уже полгода работает в продакшене: проведено несколько сотен аудитов, найдено более 100 потенциальных уязвимостей.

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

Сейчас TrustYFox — это v1:

  • Стабильно работает в нескольких локациях, переживает закрытие и продолжает прерванные аудиты.
  • Удобное тестирование гипотез через теги.
  • Возможность запуска аудита на выбранной ветке или ревизии.
  • Сбор контекста через tool calling.
  • Аудиторы и пользователи не мешают друг другу.
  • Права строго разграничены.
  • Есть механизмы observability: логи, метрики.
  • Выдерживает нагрузку на чтение (проверено через jmeter).

Главная задача остаётся прежней — рост качества. Этим я и займусь дальше.

Читать оригинал