Сработки сканеров и open-source триажер для Nuclei Метаданные Доклад: «Сработки сканеров и open-source триажер для Nuclei» Спикер: Руслан Сафиулин Дата на слайдах: 26.05.2026 Отредактированный текст Всем привет. Меня зовут Руслан Сафиулин. Я эксперт-аналитик команды [неразборчиво]. Сегодня расскажу про тему LLM-триажа, но в данном случае в контексте blackbox-сканера Nuclei: как мы применяем LLM, какие вещи помогли нам улучшить результат, сравнение по benchmark'ам и улучшению false positive. В конце также покажу ссылку на open-source triager для Nuclei, к которому вы можете подключать свои LLM. Начну с того, что такое Nuclei. Nuclei — это open-source сканер, который работает на базе шаблонов. Nuclei templates — это, грубо говоря, сигнатуры проверок, по которым мы можем определить, есть ли на каком-то endpoint'е совпадение по matcher'у. Боль при работе со сканерами Какие боли у нас возникали при работе со сканерами? Во-первых, большое количество alerts на большом потоке. В день может пролетать больше 10 тысяч alerts. Человеку вручную такой триаж делать реально, но на это уйдет большое количество времени. Соответственно, SLA горят, плюс присутствует человеческий фактор: на большом объеме человек начинает ошибаться. Во-вторых, совпадение шаблона не равно уязвимость. Если рассматривать конкретно Nuclei, какое-то количество false positive будет всегда, несмотря на то, насколько хороший шаблон используется. В-третьих, реальные находки тонут в шуме. Есть бесконечное количество info-сработок, и иногда даже в публичных info-шаблонах встречаются интересные находки, которые можно докрутить до критов. И, наконец, триаж руками — это дорого, медленно, скучно, и человек может заняться чем-то более интересным. Почему не только правила Первое, что приходит в голову для решения этой проблемы, — допиливать шаблоны или реализовывать детерминированные правила, по которым мы можем детектировать false positive. Но все это разбивается о контекст. Допустим, мы запустили публичный шаблон, который детектит WordPress. Он сработал на каком-то ресурсе по регулярке `/wp-content/uploads/(.*)`: то есть нашел относительный путь к теме или файлам WordPress. Также в HTML раскрывается `generator` с WordPress и версией. Дополнительно, если сходить на REST API WordPress — `/wp-json`, — он отвечает статусом 200 и реально присутствует. Это настоящая сработка, true positive. И тот же шаблон может сработать на другом ресурсе. Опять та же регулярка на `/wp-content/uploads/(.*)`, но в данном случае это абсолютная ссылка на картинку с другого сайта, где используется WordPress: например, хотлинк с CDN или партнерского домена. Если проверять глубже, никаких следов WordPress на самом ресурсе нет. В HTML нет `generator=WordPress`, `/wp-json` возвращает 404, в header'ах видно `Server: Tilda`. Соответственно, это false positive: сайт сам не на WordPress, он лишь грузит картинку с WordPress. На этом одном шаблоне мы нашли два контекста, в которых сработка может быть как true positive, так и false positive. А шаблонов у Nuclei десятки тысяч, и в каждом может быть много таких контекстов. Если пытаться покрыть все это правилами, это практически невозможно поддерживать. Архитектура LLM-триажа Теперь расскажу, как мы решали задачу с LLM-триажом. По архитектуре мы получаем данные от сканера: сырой debug output Nuclei с request/response. В принципе, это стандартный debug от Nuclei. Первым шагом прогоняем его через preprocessor. Это обычный детерминированный код без каких-либо LLM. Он нужен, чтобы подготовить данные для модели. Чуть дальше расскажу подробнее. На следующем шаге подключаются LLM. Модель выдает verdict с небольшим пояснением: yes, no или unknown. Если verdict — unknown, сработка сразу отправляется эксперту или AI-аналитику, как в предыдущем докладе. Такие AI-аналитики более автономны и могут более углубленно использовать те же инструменты, что и человек-аналитик, чтобы перепроверить находку. Затем мы оцениваем confidence первого verdict'а — тоже через LLM. На последнем шаге мы обогащаем карточку. Помимо человекочитаемых описаний, скриншотов и прочего, добавляем разные signals, по которым дальше можем раскладывать находку в нужные корзины. В итоге получаются три корзины: 1. То, что отправляем эксперту или AI-аналитику: например, если confidence модели достаточно низкий. 2. Уверенное «нет», то есть false positive. 3. Уверенное «да», то есть true positive. Препроцессор После скана мы получили debug output Nuclei. Дальше прогоняем его через preprocessor без GPT/LLM. Рассмотрим на примере шаблона, который детектит Spring Boot actuator endpoint `/actuator/env`, точнее его раскрытие. У нас сделан тестовый запрос, получен response. Response содержит больше мегабайта простыни JSON с метриками и конфигурационными данными, которые открываются по данному endpoint'у: `activeProfiles`, `propertySources` и так далее. Если все это засунуть в LLM вместе с prompt'ом и шаблоном, оно просто вылезет за контекст. Модель начнет галлюцинировать и плохо решать задачу. По опыту, это один из основных источников проблем, в том числе поломки JSON-ответа. Поэтому через preprocessor мы перегоняем весь сырой debug output в компактный вид. Помимо разных метаполей, мы сохраняем, сколько matcher'ов сработало, какой status code получен, какие headers появились. Но основная работа идет именно с response. По matcher'у мы определяем место, где сработал Nuclei, то есть где совпала регулярка или другой matcher. Дальше берем начало body, конец body и окна вокруг места, где сработала регулярка. Обычно этого контекста модели полностью хватает для вынесения verdict'а. И, что самое главное, это не ломает модель. Таким образом, то, что мы подаем в LLM, сжимается примерно с одного мегабайта до считанных килобайт. Вердикт от противного Следующий шаг — verdict. Здесь мы применили прием, который называем «вердикт от противного». Мы не просим LLM, получившую данные от preprocessor, шаблон и prompt, доказать, что это true positive. Мы просим ее рассказать, почему это false positive. То есть мы идем от недоверия к предыдущему сигналу и пытаемся его критиковать. Предыдущий сигнал — это сама сработка Nuclei. На примере с actuator: false positive signals у модели не сработали. Это не WAF и не block page, продукт совпадает, условия matcher'а выполнены. Endpoint `/actuator/env` открыт и отдает `propertySources` с конфигурацией, статус 200, это actuator JSON. Значения чувствительных ключей маскированы. Значит, находка — true positive. False positive signals зашиты в prompt. Например: `waf_or_block_page` — не сработал, потому что это не страница блокировки и не заглушка. `wrong_product` — не сработал, потому что это настоящий actuator JSON. `matcher_contradiction` — не сработал, потому что `propertySources` и `activeProfiles` действительно есть в body. `placeholder_value` — не сработал, потому что это реальная конфигурация, а не placeholder или заглушка. Уверенность через атаку Дальше идет confidence through attack: уверенность через атаку на предыдущий verdict модели. Мы не просим модель просто самой выставить confidence, не имея представления, как она это сделала. Если идти таким путем и просить самооценку напрямую, модель начинает плавать на одних и тех же сработках. Вместо этого мы используем таблицу якорей. Если определяем тип сработки — например, actuator JSON или `/actuator/env`, — у нас срабатывает тип якоря F: подтверждение по контенту. Для него есть фиксированное значение в таблице, например 0.75 confidence. Дальше к этому verdict'у применяем атаку, используя этот anchor. Модель пытается опровергнуть verdict предыдущей модели. При каждом `red_team_count` confidence будет падать: red team в данном случае выступает критиком. Prompt'ы для true positive и false positive разные. Маршрутизация После того как мы получили verdict от модели и перепроверили его через score уверенности с использованием якорей, идет routing. Маршрутизация нужна, чтобы избежать галлюцинаций и ошибок модели и не пропустить что-то важное. Первый пункт routing'а — сам verdict. Если verdict — unknown или confidence с предыдущего этапа ниже порога, который мы выставили, сработка уходит эксперту или AI-аналитику. Дальше идут детерминированные guardrails. Например, если модель говорит, что она не нашла что-то в response, который ей обрезали, но по факту сканер смог вытащить это extractor'ом из сырого debug output, мы не доверяем такому результату и отправляем его на эксперта или AI-аналитика. Затем оцениваем class finding'а: это баннерная проверка или прямая эксплуатация. Если это прямая эксплуатация, мы все равно отправляем на эксперта или AI-аналитика, чтобы перепроверить. Это применяется только для чувствительных находок. Также проверяем качество доказательств. Если чего-то не хватает в данных после preprocessor — в том, что мы подаем модели, — это тоже уходит эксперту или AI-аналитику. И, наконец, важность finding'а. Это один из самых ключевых факторов, по которому обычно срабатывает routing. Critical-сработки мы не отправляем сразу в автоматическую корзину. Мы отправляем их в LLM, чтобы она оценила true positive/false positive, выставила confidence и reason code, но все равно перепроверяем руками, потому что это critical. Результаты и benchmark По итогам мы провели небольшой benchmark по моделям: тестировали, как они справляются, сравнивали GPT-OSS, Qwen 3, Gemma и другие варианты. В итоге выбрали модель `qwen36-fp8` на провайдере vLLM. Работает хорошо. Мы сделали 844 размеченных verdict'а. Как и коллеги из предыдущего доклада, перепроверяли разметку: помимо человека, использовали дополнительную проверку, получили ground truth и на нем прогнали небольшой benchmark. Если вернуться к началу — к preprocessor, — видно, насколько важно подготовить данные для модели, чтобы она не галлюцинировала и нормально работала. Первый prompt, который мы использовали, условно назывался «докажи правду». Это был самый первый prompt, написанный почти как для человека, частично не для LLM. По текущим меркам он очень плохой. Но за счет того, что мы подготовили достаточно хорошие входные данные, accuracy у модели получилась даже немного выше, чем у итогового последнего prompt'а. При этом покрытие было в районе 85%, было много пропусков, и такой результат нас не устраивал. Текущий prompt построен вокруг тех приемов, о которых я говорил на предыдущих слайдах: мы атакуем предыдущие verdict'ы — и сигнал от Nuclei, и verdict самой модели. Precision у нас принципиально не изменился, зато recall и F1 улучшились: модель стала меньше сомневаться, когда мы просим ее искать ложь. По таблице на слайде: для подхода «докажи правду» precision 0.992, recall 0.852, F1 0.917; для подхода «ищи ложь» precision 0.984, recall 0.989, F1 0.945. Итоги Какие выводы мы для себя получили? Первое: подготовка машиночитаемых доказательств невероятно важна при работе с LLM. Это в том числе спасает от галлюцинаций. Модели намного проще понимать подготовленный JSON, чем сырой debug output. Второе: поиск FP signals при выставлении verdict'а на каждом этапе помогает идти от противного и критиковать сработку. Третье: confidence через атаку на verdict помогает не полагаться на самооценку модели. Мы атакуем предыдущий verdict, в том числе verdict самой модели. Четвертое: routing на эксперта или AI-аналитика лучше использовать, чтобы не пропустить что-то важное. И, как обещал, open-source triager для Nuclei опубликован на GitHub: https://github.com/cv3tomuzika/nuclei-autotriage. Там лежат наши первые версии prompt'ов. Грубо говоря, вы закидываете сырой debug Nuclei, включаете Ollama, vLLM или что хотите, подключаете любую свою модель и гоняете Auto-Triager. Он прямо через консоль выставит verdict, а дальше вы уже можете применять это как считаете нужным. У меня все. Спасибо за внимание. Вопросы и ответы Вопрос: Спасибо за доклад. У меня вопрос по девятому слайду, про false positive signals. Там есть первый критерий — `waf_or_block_page`. А что если это может быть не прямо WAF, а, например, load balancer? Допустим, ты слишком много отправил запросов, и тебя редиректит на какую-нибудь заглушку. Это не сам WAF, но по странице [неразборчиво]. Насколько точный критерий оценки, что это не WAF и не заблокированная страница? Ответ: Заглушки обычно достаточно типизированы. Это может быть, например, большой красный крест с сообщением, что вы заблокированы, HTTP 403, текст под объемом response и так далее. Вопрос: По поводу моделей: ты что-нибудь еще тестировал из моделей или только взял Qwen? Ответ: Тестировали GPT-OSS, Qwen 3.5 и Gemma 4. Вопрос: А по размеру? Ответ: До 20 миллиардов параметров. Вопрос: Уже третий доклад подряд подтверждает тему безопасности самого AI-помощника. Ты написал, что у тебя есть детерминированные guardrails. Я так понимаю, это у тебя на rule-based логике сделано. На чем построен guardrail, который защищает агента? Ответ: На самом деле guardrail — это обычные Python-правила. Они достаточно банальные. Если модель возвращает по своим signals, которые мы собираем при enrichment, что что-то не обнаружено — допустим, matcher не совпал или что-то в таком роде, — но при этом в сыром debug output есть какие-то вытащенные данные после скана, то мы такому не доверяем и перепроверяем. Комментарий из зала: Просто как совет: если будет интересно, попробуй взять какой-нибудь LLM guardrail, поставить его в режиме прозрачного proxy и смотреть, какие данные он тебе коллекционирует. Потом можно его потюнить и запустить уже в режиме blocker. Продолжение комментария из зала: Повторю, чтобы было лучше слышно. Я сам свою реальность проверяю так: взять ML/LLM guardrail, он достаточно быстро работает даже на слабом CPU. Запустить его в режиме прозрачного proxy, чтобы он просто размечал файлы, которые попадают в LLM, как safe/unsafe. И, допустим, если есть verdict, тоже сохранять. Дальше лучше смотреть глазами, потому что ничего лучше глаз пока не будет. LLM-as-a-judge — не панацея. Он может спокойно врать, причем очень убедительно. Если у тебя F1 или другая метрика достаточно высокая, можно уже подключать в режиме blocker, и тогда будет неплохая превентивная защита. Это не полноценная защита, это mitigation, но это лучше, чем regex'ы вида `Ignore my instructions`. Есть много вариантов, как отрабатывать нужные link'и и детерминированные guardrails. Это, на мой взгляд, довольно слабая тема. Ответ: Согласен. Спасибо, прикольно. Вопрос: У меня небольшой вопрос. Когда мы получаем ответ от модели, мы просим ее искать ложь. Если модель ее вдруг находит, соответственно, будет уже другой verdict. Такой verdict отправляется специалисту или дальше смотрится только то, что будет выведено самой моделью? Ответ: Я так понял, речь про confidence score, про оценку уверенности. Оценка уверенности происходит именно на тот verdict, который выставился на предыдущем этапе. Если предыдущий этап выставил verdict true, то мы идем от противного и говорим: это не true. Следующий прогон LLM пытается выставить score и доказать, насколько это не true-сработка. С false positive все в обратную сторону. Вопрос: Хорошо, спасибо. Заключительная реплика: Мне кажется, уже накопилась неплохая история с тем, что на этом TechDay люди релизят инструменты. Может, сделаем какую-нибудь GitHub-организацию и будем туда кидать эти prompt'ы и так далее? Готов поделиться? Ответ: Пока еще [неразборчиво]. Неоднозначные места 1. В сырой транскрипции название команды распознано как «CyberOx», но слайды это не подтверждают; в тексте оставлено [неразборчиво]. 2. В начале доклада фраза про «blackbox-сканер Nuclei» восстановлена по контексту; ASR исказил ее как «Blackboard сканера рублей». 3. В блоке про модели слайды подтверждают `qwen36-fp8`, 844 размеченных verdict'а и сравнение prompt'ов. Устная часть про конкретные модели частично неразборчива; оставлены только уверенно распознанные GPT-OSS, Qwen, Gemma. 4. В Q&A вопрос про `waf_or_block_page` частично неразборчив: неясна точная формулировка про страницу блокировки и «правые клиенты». 5. В комментарии из зала про guardrail часть терминов распознана неуверенно: возможно, речь о конкретном продукте или фреймворке для LLM guardrails, но название не восстановлено. 6. Финальная реплика спикера после предложения поделиться prompt'ами неразборчива; смысл оставлен как неопределенный.