Практический разбор по мотивам доклада Руслана Сафиулина
Blackbox-сканеры хорошо масштабируют проверку периметра, но плохо масштабируют внимание аналитика. Nuclei может быстро прогнать Nuclei templates по большому числу эндпоинтов, найти совпадение по matcher и вернуть поток alerts. Дальше начинается не менее важная часть: понять, где совпадение шаблона действительно указывает на уязвимость или значимый факт, а где это очередной false positive.
В докладе Руслана Сафиулина речь шла именно об этом участке процесса: как применять LLM triage к сработкам Nuclei, почему нельзя считать matcher доказательством уязвимости, как сжимать debug output до машинно читаемых доказательств/evidence, зачем просить модель искать ложь, а не доказывать правду, и как после verdict yes/no/unknown раскладывать findings в три корзины.
Главный тезис: LLM здесь не должна быть оракулом, который «посмотрел на лог и всё понял». Она становится частью инженерного pipeline: scan data -> preprocessor -> verdict -> confidence -> enrichment -> buckets. Важны подготовка данных, явные FP signals, confidence through attack и маршрутизация к эксперту или AI-аналитику там, где автоматике не хватает оснований.
Боль: больше 10 тысяч alerts и ручной triage
На большом потоке Nuclei может приносить больше >10k alerts в день. Теоретически человек может открыть каждую карточку, посмотреть request/response, проверить matcher, сходить в дополнительные эндпоинты и руками решить, true positive это или false positive. Практически такой triage быстро становится отдельной производственной линией: SLA горят, аналитик устает, а однотипные решения повышают риск ошибки.
Проблема усугубляется тем, что совпадение шаблона не равно уязвимость. Nuclei templates описывают проверки, а matcher фиксирует совпадение с условием. Но само совпадение может быть следствием контекста, который шаблон не различает: чужая картинка, CDN, заглушка, редирект, block page, неверный продукт, тестовое значение, усечённый response.
Из-за этого реальные находки тонут в шуме. Даже info-сработки иногда оказываются полезными: их можно развить до более серьёзной проблемы. Но если поток завален очевидными false positive, у команды остаётся меньше времени на те случаи, где действительно нужен инженерный разбор.
Почему шаблон не равен уязвимости
Хороший пример из доклада — WordPress detect. Допустим, Nuclei запускает шаблон, который ищет признаки WordPress. В первом случае matcher срабатывает по регулярке /wp-content/uploads/(.*). В HTML при этом есть путь к теме, раскрывается generator с WordPress и версией, а дополнительная проверка /wp-json возвращает 200 и показывает, что REST API WordPress реально доступен.
Это true positive: сайт действительно работает на WordPress. Сработал не один случайный фрагмент, а набор согласованных признаков: путь к WordPress-ресурсам, generator, живой /wp-json.
Во втором случае тот же matcher может сработать на совсем другом сайте. В body есть строка вида https://cdn.partner.com/wp-content/uploads/2024/promo.png: сайт просто грузит картинку с чужого WordPress. На самом ресурсе нет generator=WordPress, /wp-json отвечает 404, а в headers виден Server: Tilda.
Это false positive. Matcher формально прав: строка /wp-content/uploads/ действительно есть в body. Но смысл с точки зрения безопасности другой: проверяемый сайт не на WordPress, он только содержит ссылку на внешний ресурс.
Эта пара примеров хорошо объясняет, почему одними правилами задачу не закрыть. Можно дописать исключение для абсолютных ссылок, потом исключение для CDN, потом для партнерских доменов, потом для конкретных CMS, но таких контекстов у десятков тысяч Nuclei templates будет слишком много. Правила полезны, но без интерпретации evidence они быстро становятся хрупкими.
Pipeline: scan data -> preprocessor -> verdict -> confidence -> enrichment -> buckets
Архитектура LLM triage в докладе выглядит как последовательный конвейер.
Сначала система получает scan data: сырой debug output Nuclei с request/response и служебными полями. Это исходный материал, но в таком виде его нельзя просто отправить модели. Там может быть слишком много шума, длинный body, повторяющиеся headers, большие JSON-ответы и куски, не относящиеся к matcher.
Затем работает preprocessor. Это детерминированный код без LLM. Его задача — превратить сырой debug output в компактный машинно читаемый набор доказательств/evidence: status code, headers, число совпадений matcher, контекст вокруг совпадений, начало и конец body, extracted results, признаки mismatch и другие поля, которые помогают модели рассуждать.
После preprocessor подключается LLM triage. Модель получает подготовленные данные, шаблон и prompt, а на выходе возвращает verdict yes/no/unknown с пояснением. yes означает, что сработка похожа на true positive. no означает уверенный false positive. unknown нужен для случаев, где данных недостаточно или доказательства противоречивы.
Следующий шаг — confidence. Но confidence не берётся как простая самооценка модели. Для него используется отдельная логика confidence through attack: предыдущий verdict атакуется критиком, а итоговая уверенность считается с учётом якорей и попыток опровержения.
Потом идёт enrichment. Карточка обогащается reason codes, FP signals, человекочитаемым описанием, техническими доказательствами/evidence и полями, по которым её можно маршрутизировать.
В конце findings раскладываются в три корзины:
- эксперт/AI-аналитик:
unknown, низкий confidence, sensitive class, critical или слабое качество доказательств/evidence; - уверенное «нет»: false positive;
- уверенное «да»: true positive.
Эта схема важна именно как система с fallback. Цель не в том, чтобы LLM закрывала всё подряд, а в том, чтобы автоматизировать очевидное и отправлять сомнительное туда, где нужен человек или более автономный AI-аналитик.
Preprocessor: сжать debug output и не сломать модель
Сырой debug output Nuclei может быть слишком большим для прямой подачи в LLM. В докладе пример строился вокруг эндпоинта Spring Boot Actuator /actuator/env. Сканер делает GET-запрос, получает 200 OK, а response содержит большой JSON с данными вроде activeProfiles, propertySources и другими конфигурационными полями.
Если положить в prompt полный мегабайт body вместе с инструкциями, шаблоном и request/response, модель начинает работать хуже. Контекст распухает, JSON-ответ может ломаться, а рассуждение уходит в случайные фрагменты длинного тела.
Поэтому preprocessor делает несколько вещей.
Он сохраняет summary: был ли body truncated, сколько matcher сработало, какой status code получен, какие headers проверялись. Он вытаскивает matcher evidence: какой pattern совпал, в какой части response, какой был локальный context вокруг совпадения. Он добавляет compact response snippet и extracted results. В примере со Spring Boot actuator в evidence остаются именно те места, где видны propertySources и activeProfiles, а не вся простыня JSON.
Упрощённо это выглядит так:
raw debug output Nuclei
-> request/response parsing
-> matcher evidence extraction
-> body start/end + windows around matches
-> compact JSON for LLM
Практический смысл не только в экономии токенов. Модель получает не «много текста про всё», а подготовленные доказательства, связанные с matcher. Это снижает шанс hallucination и делает verdict воспроизводимее.
Verdict от противного: пусть модель ищет false positive
Наивный prompt для triage звучал бы так: «докажи, что это true positive». В докладе выбран другой приём — verdict от противного. Модель просят не подтверждать сработку, а критиковать её: найти причины, почему это может быть false positive.
Это меняет рамку рассуждения. Сработка Nuclei уже является первичным сигналом. Вместо того чтобы автоматически доверять этому сигналу, LLM triage проверяет его на типовые способы ошибиться.
На примере /actuator/env модель смотрит на FP signals:
waf_or_block_page: не похоже на WAF или block page;wrong_product: response выглядит как настоящий actuator JSON, а не страница другого продукта;matcher_contradiction:propertySourcesиactiveProfilesдействительно есть в body;placeholder_value: видна реальная конфигурация, а не placeholder или заглушка.
Если FP signals не сработали, эндпоинт /actuator/env открыт, status code 200, body похож на actuator JSON и содержит propertySources, модель может вернуть true positive. В примере чувствительные значения были маскированы, но сам факт раскрытия actuator-эндпоинта сохраняет смысл с точки зрения безопасности находки.
Сильная сторона подхода в том, что он дисциплинирует модель. Ей не предлагают написать красивое подтверждение. Ей предлагают сначала поискать причины для отказа. Если таких причин нет, verdict становится более обоснованным.
Confidence through attack: уверенность не как самооценка
LLM плохо подходит для прямой самооценки в стиле «насколько ты уверен от 0 до 1». На одних и тех же сработках такая оценка может плавать, а объяснение уверенности не всегда связано с реальным качеством решения.
В описанном pipeline confidence считается иначе. Сначала verdict получает якорь из таблицы. Например, для actuator JSON может сработать anchor F: подтверждение по контенту. На слайде этот anchor давал confidence_score: 0.75 и reason code endpoint_exposed_values_masked.
Дальше включается attack: отдельный шаг пытается опровергнуть предыдущий verdict. Если исходный verdict был true positive, критик пытается доказать, что это не true. Если исходный verdict был false positive, атака идёт в обратную сторону. При каждом успешном red team сомнении confidence падает, а confidence_red_team_count фиксирует число таких атак.
Такой подход полезен по двум причинам. Во-первых, он отделяет verdict от confidence: модель сначала принимает решение, а затем это решение проверяется другой рамкой. Во-вторых, confidence привязывается не к настроению модели, а к evidence, anchors и попыткам опровержения.
Routing и guardrails: что нельзя отдавать автоматике
Даже хороший LLM triage не должен быть последней инстанцией для всех findings. После verdict и confidence нужен routing.
Первый очевидный маршрут — unknown или confidence ниже порога. Такие сработки уходят эксперту или AI-аналитику. Это нормальный результат: pipeline честно признает, что доказательств недостаточно для автоматического решения.
Дальше работают детерминированные guardrails. Например, если модель говорит, что не нашла какое-то значение в response, но сканер извлёк его из сырого debug output через extractor, такому выводу нельзя доверять. Причина может быть простой: preprocessor обрезал body, и модель не видела полный response. Значит, finding нужно отправить на дополнительную проверку, а не закрывать автоматически.
Также учитывается класс находки. Если речь о прямой эксплуатации или чувствительной находке, её безопаснее перепроверить человеком или AI-аналитиком, даже если LLM дала уверенный verdict. Отдельно оценивается качество доказательств: хватает ли данных после preprocessor, видны ли matcher evidence, нет ли противоречий между request/response и выводом модели.
Важность finding тоже влияет на маршрут. Critical-сработки не стоит отправлять сразу в автоматическую корзину. LLM может помочь оценить true positive/false positive, выставить confidence и reason code, но финальная проверка остаётся за экспертом.
Enrichment и три корзины
Enrichment превращает результат модели в операционный объект. В карточке остаются не только verdict и текстовое объяснение, но и signals, reason codes, evidence из preprocessor, confidence, anchor, результаты guardrails и признаки для routing.
После этого finding можно положить в одну из трёх корзин.
Первая корзина — эксперт/AI-аналитик. Туда попадает всё, что не нужно закрывать автоматически: unknown, низкий confidence, важные классы находок, critical, слабые доказательства/evidence, противоречия между raw debug output и тем, что увидела модель.
Вторая корзина — уверенное «нет». Это false positive, где FP signals и доказательства/evidence дают достаточно оснований не грузить аналитика ручной проверкой.
Третья корзина — уверенное «да». Это true positive, где доказательства согласованы: matcher сработал в релевантном месте, продукт совпадает, response подтверждает смысл шаблона, нет признаков block page, placeholder или wrong product.
В этой логике LLM triage не заменяет vulnerability management-процесс. Она делает поток управляемым: очевидный шум уходит из ручной очереди, очевидные подтверждения маркируются, а спорное не теряется.
Benchmark: «ищи ложь» против «докажи правду»
В докладе показан небольшой benchmark на 844 размеченных verdict. Сравнивались два подхода к prompt.
Первый условно назывался «докажи правду». Это ранний prompt, который просил модель подтверждать сработку. По таблице он дал precision 0.992, recall 0.852, F1 0.917, TP 701, FP 5, FN 122. Precision высокий, но recall проседает: модель чаще сомневается или не подтверждает то, что нужно было подтвердить.
Второй подход — «ищи ложь». Он соответствует идее verdict от противного: атаковать первичный сигнал Nuclei и искать FP signals. Для него на слайде указаны precision 0.984, recall 0.989, F1 0.945, TP 814, FP 13, FN 9.
Precision немного снизился, но recall и F1 выросли. Для triage это важный компромисс: подход стал меньше пропускать реальные true positive, сохраняя высокий уровень точности.
В benchmark также указана модель qwen36-fp8. В устной части упоминалось, что проверялись разные модели, включая GPT-OSS, Qwen и Gemma, а выбранная рабочая конфигурация была на провайдере vLLM. Эти детали лучше воспринимать как конкретный результат эксперимента, а не универсальную рекомендацию: главный вывод доклада не в названии модели, а в том, как подготовлены данные, prompt и routing.
Open-source triager для Nuclei
Финальная часть доклада — open-source triager для Nuclei: github.com/cv3tomuzika/nuclei-autotriage.
Идея инструмента простая: подать сырой debug output Nuclei, подключить свою LLM через Ollama, vLLM или другой вариант inference, получить verdict и использовать его дальше в своём процессе. В репозитории опубликованы первые версии prompt'ов, то есть это не только демонстрация подхода, но и стартовая точка для собственных экспериментов.
Практически такой triager полезен как лаборатория. На нем можно прогнать свои Nuclei templates, собрать собственный ground truth, посмотреть, какие matcher чаще дают false positive, подобрать FP signals под свои продукты и решить, какие buckets можно автоматизировать, а какие оставить за экспертом.
Но запускать подобную систему в production без benchmark и routing было бы рискованно. Нужны размеченные verdict, понятный порог confidence, guardrails на случай обрезанного debug output, политика для critical и процесс ручной перепроверки.
Что забрать в свой процесс
Первое: не отправлять raw debug output в LLM как есть. Preprocessor должен превратить request/response в компактные доказательства/evidence, связанное с matcher. Для больших ответов вроде /actuator/env это принципиально: модели нужен не мегабайт JSON, а status, headers, matcher context, snippets вокруг propertySources и activeProfiles, а также признаки того, что body был truncated.
Второе: не считать matcher доказательством. Пример с WordPress detect показывает, что одна и та же регулярка /wp-content/uploads/(.*) может означать и настоящий WordPress, и hotlink на чужую картинку. Нужен контекст: /wp-json, generator, headers, продукт, источник URL.
Третье: строить verdict от противного. Просьба «найди, почему это false positive» лучше соответствует задаче triage, чем просьба «подтверди сработку». Она заставляет модель проверять waf_or_block_page, wrong_product, matcher_contradiction, placeholder_value и другие FP signals.
Четвёртое: считать confidence через атаку, а не через прямую самооценку. Anchors, red team count и попытки опровергнуть предыдущий verdict дают более управляемую схему, чем просьба модели самой назвать число.
Пятое: всегда оставлять корзину для эксперта или AI-аналитика. unknown, low confidence, critical, sensitive classes и слабые доказательства/evidence должны уходить на дополнительную проверку. Это не провал автоматизации, а нормальная защита от скрытых false negative.
Шестое: мерить изменения на benchmark. В докладе переход от «докажи правду» к «ищи ложь» был показан цифрами: recall и F1 выросли, хотя precision слегка снизился. Без такой таблицы легко принять красивый prompt за хороший.