- BrainTools - https://www.braintools.ru -

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

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

Всем привет! Меня зовут Андрей, и я работаю в финтех‑направлении Яндекса. Руковожу службой разработки платёжных интерфейсов. Если вы пользуетесь сервисами Яндекса, то наверняка сталкивались с формами оплаты, вот большую их часть делают ребята из моей службы.

Сегодня я расскажу вам о TrustYFox — платформе для поиска уязвимостей в коде при помощи LLM, которую я создал своими руками. С практической точки зрения [1] TrustYFox — это ещё один инструмент, который не заменяет существующие сканеры, а дополняет их, позволяя находить уязвимости.

Статья не претендует на научность или какой‑то RnD, да и я не являюсь экспертом в этих ваших LLM. По большей части это рассказ о том, как получилось (а в итоге получилось) за несколько месяцев пройти путь от прототипа до рабочего решения, в котором ежедневно запускаются аудиты. 

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


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

860 год… К лету 2025 года наша служба доросла до состояния, когда команды самостоятельно (ребята, вы очень большие молодцы) или с помощью руководителей ведут проекты, готовят графики с цифрами, договариваются со смежниками, проводят красивые демо. Другими словами, необходимость моего непосредственного участия существенно сократилась. А это значит, что появилось время наносить вокруг добро в своё удовольствие.

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

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

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

В середине июля на очередном 1–1 со своим руководителем поразмышляли на тему того, что можно сделать не просто интересного, а ещё и полезного. Решили, что раз мы работаем в Финтехе, то нам точно важна безопасность, так что можно попробовать написать сканер на основе LLM, который будет искать уязвимости в коде.

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

Сказано — сделано. Пошёл изучать, что есть готового в этих ваших LLM, чтобы прототип собрать, и наткнулся на LangChain [2], который предлагал на выбор Python или мой любимый TypeScript. Выбор был очевиден.

Первой болезненной ошибкой [3] здесь было сразу сделать космолёт через LangGraph с распределённой мультиагентской схемой. На тот момент фреймворк был откровенно сырой, и спустя потраченные выходные я с сожалением удалил большую часть кода.

Во второй подход решил уже не выпендриваться и начал с самого простого:

  • Прочитал файлы.

  • Отправил их в LLM чанками на анализ, настоятельно попросив вернуть ответ в виде JSON.

  • Распарсил ответ и попросил ещё одну LLM проверить всё это дело на корректность.

С этим проблем уже не возникло.

Прикрутить выгрузку в YT [4] и настроить отображение в DataLens [5] ожидаемо тоже не стало сложной задачей. Спустя пару дней после старта разработки уже можно было посмотреть список найденных уязвимостей в простой табличке.

Само решение уместилось буквально в нескольких строках кода
import { VulnerabilityIssueStructureJSONSchema } from "./types";

export const AnalyticPrompt = `You are a cybersecurity expert.
Analyze the following source code for security vulnerabilities or potential issues.
Analyze source code ONLY REAL security vulnerabilities, not for other issues.
The output MUST BE a JSON array of objects with the following structure:
${JSON.stringify(VulnerabilityIssueStructureJSONSchema, null, 2)}
If the code is safe, respond with an empty array.
`;

export const ReportPrompt = `
You are a cybersecurity leader, analyze the following reports from engineers, remove irrelevant information, and build a security report.
Engineers can make mistakes and false positives, so you need to analyze the report and find only real security vulnerabilities from the real world.
Vulnerabilities are only real security vulnerabilities lead to security incidents, not for other issues.
The output MUST BE the same JSON array of objects as in the input but with additional fields for skipped reports:
- "isFalsePositive" - boolean, true if the report is a false positive, false otherwise.
- "reasonFalsePositive" - string, the reason why the report is a false positive.
Do not write any other text than the output JSON array.
`;

const callModel = async (state: typeof MessagesAnnotation.State) => {
    const { messages } = state;
    const lastMessage = messages[messages.length - 1];
    if (lastMessage instanceof AIMessage) {
        return { messages: lastMessage };
    }

    const textParser = new StringOutputParser();
    const src = await textParser.invoke(lastMessage);

    const documents = await loadFiles(src);
    const jsonParser = new JsonOutputParser();

    const results = [];

    for (const doc of documents) {
        const codeContent = doc.pageContent;

        const analysis = await expertLLM.invoke([
            new SystemMessage(AnalyticPrompt),
            new HumanMessage(JSON.stringify({ filePath, codeContent }, null, 2)),
        ]);  // выполнение LLM на заданный prompt
        const parsed = await jsonParser.invoke(analysis);
        console.log(`Анализ файла ${filePath} завершён.`);
        if (parsed && Array.isArray(parsed) && parsed.length > 0) {
            results.push({ file: filePath, report: parsed });
        }
    }

    console.log(`Анализ файлов завершён, приступаем к анализу найденных уязвимостей и составлению отчёта.`);

    const reportResult = await reportLLM.invoke([
        new SystemMessage(ReportPrompt),
        new HumanMessage(JSON.stringify(results, null, 2)),
    ]);

    return { messages: reportResult };
};
TrustYFox: путь от пет‑проекта до LLM‑инструмента для поиска уязвимостей - 3

Итого прототип работает и даже что‑то находит. Значит, нужно продолжать активно копать. А копать есть куда, потому что CLI + DataLens выглядят как‑то не очень серьёзно.

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

Напомню, что делал я это всё хотя и вполне официально, но в дополнение к основной работе, как пет‑проект. И поэтому хотелось делать только интересное, а для всего остального уже использовать готовое. И вот таким готовым решением стал Toolpad [6]. Здесь «из коробки» можно получить симпатичный готовый UI без особых усилий.

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

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

Диаграмма всей это красоты выглядит следующим образом: 

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

Предлагаю вместе разобрать то, как эта схема работает:

  • Читаем файлы.

  • Получаем на вход найденные ранее уязвимости (которые были размечены как false positive или «такое хотим видеть»).

  • Строим граф зависимостей при помощи tree‑sitter.

  • Роутим файлы по агентам: берём их и передаём в LLM вместе с содержимым файлов с просьбой вернуть JSON с тем, в какие агенты какие файлы отправить.

  • Парсим ответ от LLM.

    • Если язык проекта поддержан в tree‑sitter, то вызываем для каждого файла анализ независимо каждым из выбранных агентов:

      • Получаем связанные файлы из графа зависимостей.

      • Просим в LLM выделить только важные части из всей этой каши содержимого соседних файлов.

      • Берём подготовленный контекст файла, содержимое самого файла, размеченные уязвимости для этого файла (при их наличии) и системный промпт агента.

      • Отправляем это всё в LLM и говорим, в каком формате вернуть.

      • Парсим найденные уязвимости.

    • Если язык проекта не поддержан в tree‑sitter, то чанками отправляем в агента на анализ с передачей всех размеченных уязвимостей в качестве петли обратной связи.

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

  • Парсим ответ с этапа «постобработки» и возвращаем массив найденных уязвимостей.

Чтобы вся эта схема вообще заработала, пришлось ввести новые сущности: агенты и граф зависимостей.

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

[
  {
    "name": "xss_expert",
    "description": "XSS expert",
    "prompt": "You are an XSS expert. You are responsible for analyzing the code ONLY for XSS vulnerabilities."
  },
  {
    "name": "general_expert",
    "description": "General expert",
    "prompt": "You are an general expert. You are responsible for analyzing the code for vulnerabilities. You are not specialized in any particular vulnerability."
  },
  {
    "name": "sql_injection_expert",
    "description": "SQL injection expert",
    "prompt": "You are an SQL injection expert. You are responsible for analyzing the code ONLY for SQL injection vulnerabilities."
  }
]

Граф зависимостей. В какой‑то момент я понял, что обрабатывать файлы просто пачками не очень эффективно. Модели нужен больший контекст: важно передавать не только сам файл, но и части кода, с которыми он связан.

Поднимать language server под неизвестное количество языков в Аркадии мне совсем не хотелось. Хотелось быстро и малой кровью получать связанные файлы (даже если их будет немного с запасом). Беглый поиск привёл меня к tree‑sitter.

Tree‑sitter [7] — это генератор парсеров и библиотека для инкрементального анализа. Он позволяет построить конкретное синтаксическое дерево для исходного файла и эффективно обновлять его по мере редактирования исходного файла.

Соответственно, я взял tree‑sitter, написал необходимую обёртку, назвал библиотеку @trusty_fox/ast_indexer, положил рядышком с уже существующей @trusty_fox/llm и там же её заиспользовал. На деле это не то чтобы AST Indexer, а, скорее, библиотека для поиска связанных файлов. Работает она следующим образом:

  • Каждый файл анализируется при помощи tree‑sitter и из них извлекаются:

    • Импорты и экспорты.

    • Вызовы функций.

    • Названия функций, классов, типов.

  • Имея на руках все файлы, можно строить граф (я же всё‑таки фронтендер, а тут алгоритмы какие‑то сложные) с учётом специфики Аркадии.

Из‑за того, что LLM‑агент с уверенным видом сдавал кривые запросы к tree‑sitter, проще было самому проверять через Syntax Tree Playground [8]. Поэтому я не стал покрывать все языки, а выбрал те, проекты на которых были под рукой — TypeScript, JavaScript, Python и Go.

Вот так выглядят запросы к tree‑sitter:

{
  imports: `
    (import_spec path: (interpreted_string_literal) @path)
  `,
  funcDecls: `
    (function_declaration name: (identifier) @name)
    (method_declaration name: (field_identifier) @name)
  `,
  classDecls: ``,
  typeDecls: `
    (type_spec name: (type_identifier) @name)
  `,
  calls: `
    (call_expression function: (identifier) @callee)
    (call_expression function: (selector_expression field: (field_identifier) @callee))
  `,
  identifiers: `
    (identifier) @id
  `,
  definitions: `
    (function_declaration name: (identifier) @name)
    (method_declaration name: (field_identifier) @name)
    (type_spec name: (type_identifier) @name)
    (var_spec name: (identifier) @name)
    (const_spec name: (identifier) @name)
  `,
}

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

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

Поэтому решено было продолжать и причёсывать проект для продакшена, тем более что для этого всё было почти готово, нужно было только причесать пару моментов.

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

Что подразумевается под «парой моментов»:

  • Переехать с SQLite на PostgreSQL. Да, я сам себе злой Буратино, нужно было сразу делать нормально, но что есть, то есть.

  • Аудиты могут идти долго и попасть в шов бесшовного деплоя. Нужно придумать механизм, как сохранять и восстанавливать состояние аудита.

  • Добавить ролевую модель. Пользователей нужно разделять на сотрудников СИБ с полным доступом и обычных пользователей с доступом поменьше.

  • Нужно научиться отображать логи в UI, чтобы понимать, что происходит (может, ошибки какие или запросы застряли где‑то).

  • Все промпты нужно перенести из библиотеки в базу. До этого момента они хранились в отдельной директории в рамках библиотеки.

  • Добавить CI/CD, а именно деплой в несколько локаций, балансеры, ну и прочие необходимые для продакшена вещи, которые спрашивают на архитектурной секции.

  • Придумать название и брендинг. Чтобы инструмент точно запомнился.

Пробежимся по самым интересным пунктам из этого списка.

Брендинг

Начал я, конечно же, с последнего пункта. Так как я работаю в Trust, то хотелось как‑то в названии это упомянуть, да и Яндекс тоже. А, ну и лисичек я люблю. И вот из этого всего родилось название TrustYFox. 

А нейросеть сгенерила мне иконки под это дело. Угадаете, какая стала логотипом?

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

TrustYFox перестал быть просто CLI‑тулзой и начал приобретать черты продукта. 

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

Зачем? Чтобы в моменте понимать, что происходит в аудите. Нельзя же просить пользователей идти искать в хранилище логов то, что происходит в их аудите, — нужно это выводить в UI.

Важно отметить, что прототип я начинал делать в качестве библиотеки, без UI и бэкендов. Мне показалась, что это вполне годная мысль — максимально изолировать библиотеки и заставить бэкенд зависеть от интерфейсов библиотеки. В целом‑то логично [9], так все и делают. Но что делать с логами? Ведь если запускать утилиту из командной строки, то логи можно писать в stdout, а куда их писать при запуске аудита в фоне, чтобы можно было посмотреть логи без поиска в БД? Можно передавать логгер в библиотеку и логи писать в БД. 

Собственно, я так и сделал, но есть нюанс…
import { ReportStage } from './audit/state_manager';
import type { LLMLoggerModelUsageStatistics } from './logger';

export enum StructuredMessages {
 RetryAttempt = 'retry_attempt',
 Error = 'error',
 ErrorParsingJson = 'error_parsing_json',
 MessageTooLarge = 'message_too_large',
 AuditAborted = 'audit_aborted',
 AuditRestored = 'audit_restored',

 LLMQueryModel = 'llm_query_model',
 LLMQueryCompleted = 'llm_query_completed',
 LLMQueryError = 'llm_query_error',
}



export interface StructuredContext extends Record<StructuredMessages, {}> {
 [StructuredMessages.RetryAttempt]: {
   attempt: number;
 };
 [StructuredMessages.Error]: {
   error: string;
 };
 [StructuredMessages.ErrorParsingJson]: {
   parsingJson: string;
   error: string;
 };
 [StructuredMessages.MessageTooLarge]: {};
 [StructuredMessages.AuditAborted]: {};
 [StructuredMessages.AuditRestored]: {};
 [StructuredMessages.LLMQueryModel]: {
   modelId: string;
   modelName: string;
   stage: ReportStage;
   temperature: number;
   maxTokens: number;
   maskedApiKey: string;
 };
 [StructuredMessages.LLMQueryError]: {
   modelId: string;
   modelName: string;
   stage: ReportStage;
   temperature: number;
   maxTokens: number;
   error: string;
 };
 [StructuredMessages.LLMQueryCompleted]: LLMLoggerModelUsageStatistics;
}

export const structuredMessages: Record<StructuredMessages, string> = {
 [StructuredMessages.RetryAttempt]: 'Попробуем ещё раз',
 [StructuredMessages.Error]: 'Произошла ошибка',
 [StructuredMessages.ErrorParsingJson]: 'Произошла ошибка при парсинге JSON',
 [StructuredMessages.MessageTooLarge]:
   'Сообщение слишком большое для отправки',
 [StructuredMessages.AuditAborted]: 'Аудит прерван',
 [StructuredMessages.AuditRestored]: 'Аудит возобновлён',
 [StructuredMessages.LLMQueryModel]: 'Запрос к LLM-модели',
 [StructuredMessages.LLMQueryCompleted]: 'Получен ответ от модели',
 [StructuredMessages.LLMQueryError]: 'Произошла ошибка при запросе к модели'
};

export declare function wrapLLMLogger(
 nativeLogger: LLMLogger
): WrappedLLMLoggerOutOfScope;


llmLogger.info(StructuredMessages.LLMQueryModel, {
 modelId: model.id,
 modelName: model.name,
 stage,
 temperature: model.temperature,
 maxTokens: model.maxTokens,
});

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

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

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

Аудиты идут долго: процесс может упасть, в этот момент может случиться деплой новой версии. Да вообще что угодно может произойти и прервать аудит до завершения. А пока аудит не завершился, всё лежит только в памяти [10]. Без стейт‑менеджера, если воркер упадёт в процессе аудита, придётся начинать всё сначала. В общем, вся эта конструкция выглядит не слишком устойчиво.

Чтобы поддержать сохранение состояния аудита, пришлось немного переработать архитектуру библиотеки и ввести ReportAnalysisStateManager.

Любой этап аудита (роутинг, аудит, обработка) теперь для хранения и получения результатов использует ReportAnalysisStateManager. State Manager, в свою очередь, при каждом изменении стейта вызывает колбэк с актуальным состоянием, чтобы вызывающий код мог это состояние сохранить. Ну и каждый этап при старте явно смотрит в ReportAnalysisStateManager: вдруг этап уже завершён и там лежат результаты этого шага.

А ещё ReportAnalysisStateManager отдаёт наружу возможность вызвать AbortController, signal которого передаётся во все запросы к LLM, что на сдачу от работы со стейтом даёт нам полноценный graceful shutdown.

Пример того, как применяется ReportAnalysisStateManager (если интересно):
function start() {
  const state = ReportAnalysisStateManager.RestoreCurrentState(savedState);

  await prepareAudit(documents, llmLogger, stateManager);
  await processAudit(
    stateManager,
    indexer,
    documents,
    relatedDocuments,
    llmLogger
  );
  await postProcessAudit(stateManager, llmLogger);

  const { postProcesedResults } = stateManager.getStateByStage(
    ReportStage.PostProcessing
  );
}

export default async function processAudit(
  stateManager: ReportAnalysisStateManager,
  indexer: ProjectASTIndexer,
  allDocuments: Document[],
  relatedDocuments: Document[],
  llmLogger: WrappedLLMLogger
) {
  const state = ReportAnalysisStateManager.RestoreCurrentState(savedState);

  if (stateManager.getCurrentStage() !== ReportStage.Processing) {
    return;
  }

  const {
    configuration,
    falsePositiveReports,
    looksGoodReports,
    filesToAnalyzeByAgentsIds,
  } = stateManager.getStateByStage(ReportStage.Processing);

  // В процессе работы стейт можно и нужно обновлять, чтобы в случае чего продолжить с момента, на котором остановились
  stateManager.setStateByStage(ReportStage.Processing, (currentState) => ({
    ...currentState,
    processedFilesByAgentsIds: {
      ...currentState.processedFilesByAgentsIds,
      [agent.id]: [
        ...(currentState.processedFilesByAgentsIds[agent.id] || []),
        document.metadata.source,
      ],
    },
  }));

  if (state.isAborted()) throw new Error('Audit aborted');
}

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

Теперь что касается ролей. На тот момент в инструменте было (да и осталось) три роли:

  • Пользователь (можно смотреть, трогать нельзя).

  • Аудитор (можно смотреть и можно трогать аудиты).

  • Админ (можно трогать вообще всё, включая промпты, смотреть тоже можно).

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

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

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

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

Мне очень не хотелось открывать TrustYFox на большее количество пользователей с кривым UI. Поэтому было решено допилить UI до состояния, когда всё максимально удобно, понятно и очевидно. Потому что могу и потому что хочу. В процессе причёсывания UI попутно правил какие‑то баги на бэкенде и в библиотеке для аудитов. В общем, готовил продукт к продакшену, выступая заодно и в роли тестировщика.

Вот столько появилось миграций за полтора месяца причёсывания UI и обработки обратной связи от пользователей.

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

Про перенос промптов, настройку CI/CD и поднятие продакшен‑окружения сказать, честно говоря, особенно нечего. Ну настроил и настроил, чего бубнить‑то.

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

Прошло почти четыре месяца с того момента, как я решил запустить AI‑Secreview и обрадовался табличке с уязвимостями в DataLens. И вот над чем я работаю теперь.

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

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

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

Чтобы появилась возможность запускать аудиты с разными конфигурациями и сравнивать результаты, была введена система тегов для промптов и агентов. Если коротко, то можно обновить агентов или системные промпты, после чего запустить одновременно два аудита: со stable‑версией и с экспериментальной. А в конце сравнить найденные уязвимости и принять решение, какую версию оставляем.

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

Фактически что промпты, что агенты — это всё системные промпты. Отличие только в том, что набор промптов строго фиксирован и промпты описывают общие правила игры. Они определяют поведение [11] на каждом из этапов аудита: роутинг, подготовка, контекста, анализ, постобработка.

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

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

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

Промпты из агентов просто собираются на этапе анализа внутрь тега <role>:

new SystemMessage(
  [
    ['<role>', agent.prompt, '</role>'].join('n'),
    feedbacks,
    [
      '<analysis_rules>',
      configuration.multipleFilesModePrompt,
      '</analysis_rules>',
    ].join('n'),
    attachAdditionalContextsToPrompt(
      configuration.agentContextPrompts || [],
      agent.includedContexts
    ),
  ]
    .filter(Boolean)
    .join('n')
)

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

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

Как вы помните, изначально все промпты и агенты лежали просто в json внутри библиотеки и обновлялись вместе с релизом. Было сразу понятно, что такое решение не подходит для продакшена и нужно будет всё выносить в БД. Собственно, первый подход как раз и был такой, что я вынес все промпты (и агентов) в БД, после чего мог обновлять их через UI.

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

Какие вообще были варианты? Один из вариантов — вынести промпты в главный репозиторий и заиспользовать уже готовую VCS. Но это казалось каким‑то космолётом для относительно простой задачи. Второй вариант — реализовать ревизии при помощи LLM‑инструментов. Собственно, LLM‑агент всё переделал на ревизии, и в итоге после потраченных на это выходных мне пришлось откатить пул‑реквест, потому что в нём было всё слишком усложнено (куча разных ревизий, бардак в БД, в общем, мне не понравилось). Да и вообще, оно не работало.

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

И я просто ввёл теги:

  • Stable — это то, с чем работают аудиты по умолчанию.

  • Теги для промптов и агентов не зависят друг от друга, поэтому их можно собирать в любые комбинации.

  • Можно клонировать stable в любой другой тег и редактировать в рамках нового тега агентов и промпты как душе угодно.

  • Обновив тег experimental, можно запустить два аудита: на теге stable и на экспериментальном теге.

  • Любой тег можно запромоутить в stable, тем самым применив новые изменения.

    • Текущие сущности из stable удаляются через soft delete (проставляем текущий таймстемп в deletedAt).

    • В выбранном теге меняем тег на stable.

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

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

Конфигурация — это связующее звено между промптами/агентами/моделями и аудитами.

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

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

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

После того как агенты нашли уязвимости и уязвимости были отфильтрованы на постобработке, результаты сохраняются в БД.

Что включает в себя сущность найденной уязвимости:

  • Описание уязвимости.

  • Сам уязвимый код.

  • Рекомендуемый фикс.

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

К сожалению, даже с передачей инфраструктурного контекста и с докидыванием связанных файлов LLM не понимает, что мы хотим видеть, а что считаем false positive. Нужно ей как‑то накидывать примеры, которые можно разметить в ручном режиме.

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

  • Отметить уязвимость как False Positive. Значит, уязвимость логичная, но неприменима в нашем случае. Тогда стоит указать причину, почему это у нас считается нормальным. Написать можно на русском, LLM переведёт на английский. Можно вообще не писать, но дополнительная информация бывает очень полезна.

  • Отметить уязвимость как Looks Good. Это значит, что TrustYFox нашёл что‑то стоящее и что хочется видеть больше такого.

  • Удалить уязвимость. На случай, если совсем какой‑то мусор просочился через постобработку.

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

Tool Calling

Что такое tool calling? Фактически это подход, который позволяет LLM самим вызывать необходимые инструменты из предоставленного списка. Выше я уже упоминал, что LLM очень любит находить проблемы там, где их нет, а без контекста тем более. Частично эту проблему получилось решить при помощи AST Indexer и просьбы к LLM чуть пофильтровать контекст. Но там было несколько проблем:

  1. Ограниченный скоуп языков (да, можно добавить больше, но это ручная работа).

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

  3. Контекст собирается только из ближайших соседей, а этого часто недостаточно для хорошего анализа.

AST Indexer выручал, пока не было tool calling, но именно как промежуточное решение с понятными компромиссами.

Вот диаграмма взаимодействия LLM с tools при сборе контекста:

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

И пошагово:

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

  • Далее запускается цикл общения с LLM.

    • LLM отвечает сообщением, в котором запрашивает вызов инструмента с некоторыми параметрами (например, хочет прочитать содержимое файла или найти все вхождения подстроки в файлах проекта).

    • TrustYFox видит это сообщение, вызывает инструмент и отправляет LLM ответ с результатом.

    • В какой‑то момент LLM решает, что хватит, и отвечает уже финальным сообщением.

Tool calling, безусловно, работает лучше, чем AST Indexer, но и здесь есть целый набор своих нюансов. Одна из самых частых проблем: LLM не всегда решает, что ей хватит, и начинает повторять [12] одни и те же действия с полной уверенностью, что получит какой‑то другой результат…

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

Ну и ожидаемо упирается в лимиты (но лимиты всё же лучше, чем бесконечный цикл).

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

Какие ещё есть проблемы, которые пока не решены до конца:

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

  • LLM временами любит запрашивать через tools одно и то же. То есть буквально делать два вызова одного и того же инструмента с одинаковыми аргументами.

  • LLM зачем‑то постоянно добавляет двоеточие в некоторые аргументы (скорее всего, где‑то просто баг, но быстро найти не получилось) и потом удивляется, что ничего не нашлось.

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

Вообще, есть некоторые моменты, которые ретроспективно выглядят как косяки. Ну куда же без ретро после запуска проекта?

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

  • С самого начала было решено взять SQLite, чтобы не плодить инфру и быстро всё натыкать. В итоге всё равно пришлось переезжать.

  • Скорее всего, стоило сразу идти в историю с tool calling. Было интересно поиграться с tree‑sitter, но если бы это время было вложено в tool calling, качество достигло бы приемлемого уровня гораздо быстрее.

  • Как следствие из первого пункта, очень много времени было потрачено на то, чтобы переделать то, что было сделано с позиции «лишь бы работало» на старте. С другой стороны, на тот момент с учётом тех входных данных было важно как можно быстрее проверить гипотезу и показать PoC.

Что дальше

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

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

А что касается настоящего, сейчас проект дорос до состояния, когда его можно назвать v1:

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

  • Можно удобно тестировать гипотезы на разных промптах и аудитах через механизм тегов.

  • Можно запускать аудит в выбранной ветке или на выбранной ревизии.

  • Для сбора контекста используется Tool Calling.

  • Аудиторы и пользователи не пересекаются и не мешают друг другу.

  • Права и доступы корректно разграничены по наличию доступов. 

  • Есть все необходимые механизмы observability, в частности метрики в мониторинге.

  • Успешно обстрелян через jmeter, нагрузку на чтение держит хорошо. 

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

Автор: frimuchkov

Источник [13]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/28371

URLs in this post:

[1] зрения: http://www.braintools.ru/article/6238

[2] LangChain: https://www.langchain.com/

[3] ошибкой: http://www.braintools.ru/article/4192

[4] YT: https://ytsaurus.tech/ru

[5] DataLens: https://datalens.ru/

[6] Toolpad: https://mui.com/toolpad/core/introduction/

[7] Tree‑sitter: https://tree-sitter.github.io/tree-sitter/

[8] Syntax Tree Playground: https://tree-sitter.github.io/tree-sitter/7-playground.html#syntax-tree-playground

[9] логично: http://www.braintools.ru/article/7640

[10] памяти: http://www.braintools.ru/article/4140

[11] поведение: http://www.braintools.ru/article/9372

[12] повторять: http://www.braintools.ru/article/4012

[13] Источник: https://habr.com/ru/companies/yandex/articles/1013714/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1013714

www.BrainTools.ru

Rambler's Top100