На моём тестовом наборе модель показывала 55 из 55 верных вердиктов и ни одной ложной эскалации. Потом я дал ей 100 реальных алертов с лабораторного стенда Wazuh. Она зря подняла на вторую линию 39 из них и к некоторым дописала инцидент, которого не было.
Этот разрыв определил всю дальнейшую работу. Ниже что я построил, какие цифры получил и чего эти цифры не доказывают. Код и веса открыты: GitHub, Hugging Face.
Задача
Первая линия SOC тонет в однотипных алертах, а облачные LLM часто нельзя использовать: логи не должны покидать периметр. Я хотел получить помощника, который работает на обычном ноутбуке с 8 ГБ видеопамяти, ничего не отправляет наружу и готовит для второй линии проверяемую карточку.
Название kezekshi по-казахски значит «дежурный». В основе дообученная Gemma 4 E2B под лицензией Apache 2.0.
На вход один алерт как пакет доказательств E1…En. На выход JSON: вердикт из четырёх классов, краткое описание, утверждения со ссылками на номера доказательств, техники MITRE ATT&CK, чего не хватает и что проверить дальше. Ссылки нужны, чтобы вывод модели можно было проверить кодом, а не верить ему на слово.
Что сработало на обучении
Трудные негативы полезнее новых типов атак. PowerShell -enc из агента SCCM, vssadmin create от бэкапа, скан Nessus по заявке, impossible travel через корпоративный VPN: поверхность выглядит как атака, но в пакете есть явный признак легитимности. Я добавил 14 таких кейсов, и набор негативов вырос с 8 до 22. Ложных эскалаций на нём осталось 0, а recall не просел.
Правило в промпте иногда дешевле дообучения. Рубрика тяжести поначалу уронила recall на незнакомых типах с 20/20 до 16/20: все четыре промаха это OAuth-согласие, прочитанное как штатное действие. Одно правило, что согласие приложению, выдача прав или новый метод MFA без подтверждения в пакете это минимум suspicious, вернуло 20/20 без видеокарты.
Режим размышления дообученной модели вредит. Рабочий режим без него.
Удар о реальный поток
Теневой прогон по реальным алертам стенда показал то, с чего я начал. На высокочастотных правилах, которых нет в синтетике (FIM реестра 751, 752, 750, rootcheck 510), модель завышала тяжесть и сочиняла нарратив.
Я сделал две вещи.
Первая: дообучил модель на 215 реальных benign-кейсах стенда. Замер на 100 отложенных алертах:
Версия | Совпадение вердикта | Лишние эскалации |
|---|---|---|
v4, только синтетика | 39/100 | 39/100 |
v5, плюс реальный шум | 96/100 | 0/100 |
Вторая: перестал звать модель там, где исход от неё не зависит. Инъекции в логах и операционный шум из белого списка решаются детерминированно ещё до модели, причём контекст всё равно эскалирует: изменение в автозапуске или защитной службе из белого списка выпадает. На 3000 алертах стенда 98,1 % решилось без модели, 1,9 % ушло в неё. Это примерно в 50 раз меньше вызовов при тех же решениях об авто-закрытии.
Памятка правил страхует и саму модель. Пример из тестовых алертов в репозитории: на попытке входа по SSH под несуществующей учёткой v6 ответила false_positive, памятка сказала suspicious, и алерт ушёл человеку.
Схема ответа оказалась дешевле новой версии
Шестая версия выросла в обучении до 3546 примеров и стала точнее на знакомых типах, но брак ответа вырос: на типах вне обучения 9 из 20 карточек не проходили проверку. Я посмотрел сырые ответы и увидел причину: в ссылке на доказательство модель писала имя поля ev1 вместо номера E1.
Вместо переобучения я стал собирать схему Ollama под каждый алерт. Разрешены только E-id этого алерта и кандидаты MITRE из локального индекса. Заодно нашлась ошибка подбора: короткие триггеры искались как подстроки, и tor срабатывал на monitor.
Набор | Показатель | Было | Стало |
|---|---|---|---|
Типы вне обучения (20) | брак ответа | 9/20 | 0/20 |
Типы вне обучения (20) | MITRE, полнота | 13/36 | 25/36 |
Типы вне обучения (20) | MITRE, точность | 13/46 | 25/34 |
Типы вне обучения (20) | атаки подняты в L2 | 20/20 | 20/20 |
Знакомые типы (76) | брак ответа | 12/76 | 1/76 |
Знакомые типы (76) | атаки подняты в L2 | 42/42 | 42/42 |
Знакомые типы (76) | ложные эскалации | 0/34 | 1/34 |
Реальный шум (100) | совпадение вердикта | 97/100 | 93/100 |
Реальный шум (100) | ложные эскалации | 0/100 | 1/100 |
Плата видна в таблице: появились единичные лишние эскалации, а проверка ссылок стала формальной. Теперь номер всегда существует, но подходит ли он к утверждению, схема не гарантирует.
Подсказка меняет ответ
Самый неприятный результат этой серии. Когда в индексе появился кандидат T1555.004, более ранняя v5.1 начала писать про «массовый сбор учётных данных» о штатном чтении Credential Manager процессом Outlook. Список кандидатов работает как намёк на атаку.
Я добавил в промпт оговорку, что кандидаты подобраны по словам и атаку не означают. Recall на новых типах упал с 20/20 до 18/20: OAuth-согласие и IDOR закрылись как штатные. Оговорку я убрал. Для SOC лишняя эскалация дешевле пропущенной атаки, но это компромисс, а не решение.
Чего эти цифры не доказывают
Recall на реальных атаках не измерен. В реальном наборе только шум. 20/20 получено на пяти семействах атак, этого мало.
Часть кейсов почти совпадает с обучением. 30 из 76 знакомых и 63 из 100 реальных совпадают с точностью до цифр. Вне этих совпадений вердикт на прежней схеме был 44/46 и 34/37.
Реальный шум не проверяет хвост. Почти весь такой поток закрывает белый список, а строка про 100 алертов описывает модель без него.
Эталон реального шума это суждение одного аналитика, а не решения дежурной смены.
Поэтому контур работает в режиме assist: сам закрывает только детерминированный шум, всё, что дошло до модели, смотрит человек. Как разгрузка от шума и черновик карточки он полезен. Как автономный решатель эскалаций не проверен.
Что открыто
В репозитории код контура поверх Wazuh (только чтение), схема ответа, индекс ATT&CK, скрипты обучения и оценки, руководство оператора. Веса v6 лежат на Hugging Face: GGUF на 4,9 ГБ, который ставится в Ollama одной командой.
Датасета нет, и это осознанно: обучающий набор опирается на материалы учебного курса и на алерты закрытого стенда. Модель можно запустить на своих алертах, но метрики из статьи из открытых файлов не воспроизвести. Как собрать свой набор, описано в docs/dataset.md.
Перед публикацией весов я проверил, не выдаёт ли модель имена машин стенда. Во входах обучения они есть, в ответах нет, а ошибка считается только по ответу. За 306 попыток имена не появились, как и синтетическое имя, которое стояло в 144 ответах из обучения. Риск это снижает, но запоминание полностью не исключает.
Дальше
Следующий шаг это управляемые атаки на стенде и разбор их дежурным: только так появится настоящий recall контура. Скрипт для этого уже есть, eval/recall_eval.py.
Если вы работаете в SOC или MSSP, мне интересно другое: какая доля потока у вас реально решается памяткой и где такой контур сломается первым. Напишите в комментариях.

