Нечеткая логика в ИБ: сокращаю очередь уязвимостей в 7,5 раза, сравниваю с CVSS, EPSS и методикой ФСТЭК

от автора

Вступление

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

  1. Я использую ИИ (AI) для поиска, анализа и структурирования информации. Но это не отключает мои собственные мозги.

  2. Приведенная ниже работа частично выстроена на рабочей задаче, учебном материале и собственных бурных порывах (идеях). Следовательно могут быть допущены ошибки. Если вы их видите — укажите на них (буду рад услышать объективную критику)

  3. Я попробую от теории перейти к практическому применению в рамках работы и собрать воедино весь разобранный материал. По факту, с самого начала статьи я постепенно объясняю составные модули своей работы и принцип их работы

  4. Я постараюсь доказать, что нечеткая логика — важная составляющая современного ИБ. Она заметно упрощает жизнь (цифры в результатах)

И так, чуть-чуть информации из анналов истории

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

Есть куча песка. Уберем одну песчинку — куча останется кучей. Уберем еще — куча по-прежнему будет кучей. Но если каждый раз убирать по чуть-чуть, то рано или поздно кучу нельзя будет назвать таковой. И, конечно же, границы определения в таком случае сильно размыты. Кто-то мог бы утвердить, что куча начинается от 3 песчинок, но стоит ли такое делать? Не думаю.

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

Вступление

Вступление

Вернемся к информационной безопасности. На данный момент существует вот такой показатель — CVSS (Common Vulnerability Scoring System) в контексте оценки уязвимостей. Оценка от 0 до 10 и грубо говоря она характеризует насколько плохо будет, если соответствующую уязвимость проэксплуатируют. По сути, она состоит из двух частей: насколько легко проэксплуатировать и насколько «больно» это будет.

Так вот, если мы запишем следующее утверждение для какой-нибудь внутренней корпоративной системы мониторинга и управления разработкой/инцидентами, его нельзя будет назвать верным:

if CVSS >= 9.0:    print("Критично")else:    print("Можно отложить")

В 1965 году Лотфи Заде — американский математик и в будущем автор термина «нечеткой логики» публикует следующую максимально простую, но по-своему революционную строчку — он буквально говорит. Если у нас есть некие функции, при заданных условиях которые выдают булевой результат (или его интерпретацию, 1 или 0), то можно также говорить и о том, что соответствующей функции можно присваивать целую область значений — другими словами размывать границы.

μ_A(x) ∈ [0, 1]

Собственно на этом всё. Это и есть нечеткое множество. Указанная величина μ называется степенью принадлежности элемента x к множеству А. И важное уточнение — степень принадлежности это НЕ вероятность.

На простых примерах. Есть две бутылки с жидкостью:

  • На первой написано: «Степень принадлежности множеству питьевых жидкостей = 0.91»

  • На второй написано: «Вероятность того, что это питьевая жидкость, = 0.91»

На выбор?

На выбор?

Надеюсь вы выбрали правильный ответ, первую бутылку. Степень 0.91 в нашем случае означает, что жидкость максимально похожа на питьевую — например с минимальным изменением в составе или загрязнениями, к примеру, из под крана. Пить такую в общем и целом можно. Вероятность 0.91 говорит о том, что в 91 случае из 100 мы получим воду, а в 9 оставшихся случаев — непонятно что.

Вероятность

Степень принадлежности

отвечает на вопрос

случится ли событие

насколько объект подходит под описание

природа неопределенности

случайность

нечеткость границ понятий

после наблюдения

сводится часто к 0 и 1

не меняется

сумма по вариантам

равна 1

не обязана быть ничем

Соответственно функция, которая определяет нашу степень принадлежности, так и называется — функция принадлежности. Она отвечает на вопрос, насколько некое значение x подходит под какие-то критерии.

Например, можно говорить про 4 формы принадлежности, которые покрывают большинство задач.

  1. Треугольная. Три точки, которые можно представить следующим образом: начало, пик, конец.

    Треугольная функция принадлежности

    Треугольная функция принадлежности
  2. Трапециевидная. Представим некую трапецию и пронумеруем ее слева направо. От точки A до точки B будет значение в духе «точно нет», от B до C «точно да», от C до D «уже нет/перебор».

    Трапециевидная функция принадлежности

    Трапециевидная функция принадлежности
  3. Гауссова. «Сглаженная» функция, напоминающая перевернутый колокол. Требуется там, где необходимо отсутствие рывков.

    Гауссова функция принадлежности

    Гауссова функция принадлежности
  4. Сигмоида. «Плавная ступенька». Идеальная для крайних вариантов в духе «все, что выше — точно высокое».

    Сигмоидальная функция принадлежности

    Сигмоидальная функция принадлежности

Теперь стоит познакомиться с нудной частью — логическими правилами (выводами)

Они будут состоять из соответствующих переменных, логических операторов, базовых правил и полноты, которые покрывают все комбинации входов и дополнительно проверяют, что нигде не осталось «пробелов»

Например, есть такой термин/сокращение EPSS (Expoloit Prediction Scoring System) — он показывает вероятность от 0 до 1 того, что уязвимость будет эксплуатироваться в ближайшие 30 дней. Человеку за таким не уследить, поэтому обычно такими данными занимается модель машинного обучения. EPSS отвечает на вопрос, на который не в силах ответить CVSS.

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

EPSS = LinguisticVar("epss", -5.0, 0.0, {    "пренебрежимая": trap(-5.0, -5.0, -2.3, -1.9),    "низкая":        trap(-2.5, -2.1, -1.5, -1.15),    "средняя":       trap(-1.7, -1.3, -0.9, -0.6),    "высокая":       tri(-1.0, -0.6, -0.15),    "критическая":   trap(-0.65, -0.1, 0.0, 0.0),})

Моменты, на которые постараюсь ответить сразу:

  1. Диапазон от -5…0 выбран на основании того, что EPSS требует от нас вероятности от 0 до 1. Половина уязвимостей из списка, если не больше, имеет значения ниже 0.001. На линейной оси они бы запросто слились в одну точку. В такой ситуации пришлось изменять немного понятия из формата «во сколько раз больше» (мультипликативная разница) в «на сколько больше» (аддитивная), для корректного отображения.

    0.0 — жесткое значение и является максимум EPSS или другими словами:

    log_{10} 1.0

    -5.0 — наше решение. Т.к. мы не сможем найти логарифм нуля по основанию 10, то подбираем подходящее нам значение. Могли взять -4 (граничное значение 0.0001), могли взять -6 (и увеличить граничное значение до 0.000001) — модель бы в таком случае почти не изменилась, потому что реальных данных настолько близких к соответствующим границам почти нет.

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

    Трапециевидное представление EPSS

    Трапециевидное представление EPSS

Далее, по-хорошему, необходимо осуществлять проверку — пройтись по всей шкале EPSS шаг за шагом, проверить, что в каждой точке каждый терм (слово из набора) может ответить на вопрос, что точка принадлежит ему. Если терм не уверен исходя из заданных условий (выдает низкую степень принадлежности) и другого «владельца» нет, значит в созданной модели образовалась «дыра» — место которое модель фактически не знает как назвать.

def check_coverage(self, n=401, floor=0.5):    """Ищем точки, где максимальная степень по всем термам ниже порога."""    return [x for x in self.grid(n)            if max(mf(x) for mf in self.terms.values()) < floor]

При n=401 шаг между соседними точками = 5.0/400=0.0125 — достаточно для проверки. Значение floor=0.5 — порог, который мы проверяем. Если самый лучший показатель любого терма не превышает отметки 0.5 — значит ни один терм не описывает это значение хотя бы наполовину уверенно — та самая «дыра». В одном из моих прогонов, например, при значении 3.6 выдало ошибку.

Операции И, ИЛИ, НЕ — что тут ломается

Для многих это стандартные логические операции. Но для нечеткой логики добавляют свои правила — «t-нормы» (для оператора И) и «s-нормы» (они же «t-конормы», для ИЛИ):

  • Для И (t-нормы). Самый частый вариант — это выборка минимального из двух значений.

    μ(A И B) = min(μ_A, μ_B)

    И также присутствует альтернатива, которая считается более жесткой. Это произведение.

    μ_A × μ_B

    Если кратко, то умножение (альтернативу) берут, когда хотят, чтобы множество средних условий наказывалось сильнее. Ну а логика выборки минимального проста — качество цепи судится по самому слабому элементу, ведь оно также будет подвергаться нагрузке. Будь хоть все значения равны 1, кроме одного 0,5, то качество все равно будет на уровне 0,5.

  • Для ИЛИ (s-норма). Обычно принято брать максимум из двух элементов.

    max(μ_A, μ_B)

    А как альтернативу — вероятностную сумму.

a + b − a·b

  • Для НЕ обычно используется:

    1 − μ

Теперь, возвращаясь к стандартной логике логических операций, рассмотрим такой пример. В обычной логике выражение «А ИЛИ НЕ А» — всегда истина. В нечеткой, если некое значение x(a)=0.5:

max(0.5, 1 − 0.5) = 0.5

Половина. Не истина. Аналогично ломается и закон противоречия: «А И НЕ А» дает не ноль, а 0.5 (половину). Это не баг, а прямое следствие того, что мы разрешили промежуточные значения.

Здесь уже можно сделать вывод, что не надо строить нечеткие правила так, будто они булевы. Правило «ЕСЛИ A ТО X» и правило «ЕСЛИ НЕ A ТО Y» в нечеткой логике сработают одновременно, каждое со своей силой, и результат окажется между X и Y. Именно на этом и держится наша «плавность».

Ломаем привычное понимание логики (расширяем)

Ломаем привычное понимание логики (расширяем)

Нечеткое правило и база правил

Правило, которое мы сформируем будет выглядеть примерно вот так: Если EPSS = высокая и impact = полный, то срочность = немедленная

Часть до «то» — антецедент (посылка), часть после — консеквент (заключение).

Набор правил в такой ситуации назовем базой правил. И к ней есть два требования:

  1. Полнота. Для любого сочетания входов должно срабатывать хоть одно правило. Если есть сочетание входов, при котором у нас ничего не срабатывает, то система вернет середину шкалы — осмысленный казалось бы ответ, но ответа быть не должно. Проверяется это перебором решетки по всем входам.

  2. Непротиворечивость. Два правила с одинаковой первой частью (той самой посылкой, которую мы упомянули выше) не может иметь два разных заключения.

Соответственно легко посчитать сколько правил нужно. Если у нас n входов и m термов, то это перебор m в степени n. Три входа и три терма — 27 правил. Отсюда и вытекает то, что есть у нас например 5 входов и 5 термов — 3125, такое руками будет написать сложновато (а продумать еще сложнее). Отсюда практическое правило: не больше 3-4 входов на один блок вывода. И сразу обходное допустимое решение: построение каскада блоков (к чему пришел я).

Теперь у нас задача. Мы обработали несколько входных данных и получили какие-то данные. Нужно склеить все воедино.

Алгоритм Мамдани: клеим наши данные обратно на примере уязвимостей

Эбрахам Мамдани в 1975 году впервые применил нечеткие правила к управлению паровой машиной, его схема того времени стала стандартом. Четыре шага.

  1. Фаззификация. Превращаем числа на входе в степени принадлежности.

    Если мы говорили о реальном примере, то вот он — CVE-2025-49113 в Roundcube Webmail на 1 февраля 2026: EPSS = 0.918, impact = 5.9, возраст = 244 дня. Для наглядности чуть снизим EPSS до 0.30 — так сработает больше правил. Переводим в оси (EPSS логарифмируем, возраст тоже) и считаем степени:

    EPSS 0.30  -> ось -0.523 -> {пренебрежимая: 0.00, низкая: 0.00, средняя: 0.00,                             высокая: 0.83, критическая: 0.23}impact 5.9                -> {ограниченный: 0.00, существенный: 0.00, полный: 1.00}возраст 244 -> ось 2.389  -> {свежая: 0.00, недавняя: 1.00, зрелая: 0.00}

    Значение 0.30 принадлежит сразу двум термам: оно «высокое» на 0.83 и «критическое» на 0.23. Именно это перекрытие дает нашу заведомую плавность.

  2. Считаем силу каждого правила. Для правила «ЕСЛИ epss = высокая И impact = полный» сила равна min(0.83, 1.00) = 0.83. Для «ЕСЛИ epss = критическая И impact = полный» — min(0.23, 1.00) = 0.23. Остальные правила получают ноль: их условия не выполняются совсем.

  3. Агрегация. Каждое сработавшее правило «обрезает» функцию принадлежности своего заключения на уровне своей силы. Правило силой 0.83, указывающее на терм «немедленная», даёт форму этого терма, срезанную сверху на высоте 0.83. Это называется импликацией Мамдани и считается как min(сила, ФП_заключения)

    Все обрезанные фигуры объединяют в таком случае в одну по максимуму (не забываем пройтись по каждому значению). Если у вас получился «горный хребет» неправильной формы, то поздравляю, это и есть нечеткий ответ системы.

  4. Дефаззификация. Теперь, когда у нас есть некий «горный хребет», нам осталось это превратить в нечто уже более точное и понятное. Можно в такой ситуации склониться к «тяжести» — в нашем примере оба правила указывают на терм «немедленная», сильнейшее обрезает его на 0.83, и центр тяжести получившейся фигуры даёт 94.4 — корзина «немедленно, окно до 24 часов».

    В виде кода это имеет следующий вид:

    def infer(self, values):    mem = {k: var.fuzzify(values[k]) for k, var in self.inputs.items()}    # шаг 1    fired = [(r, a) for r in self.rules                                     # шаг 2             if (a := r.if_.degree(mem)) > 1e-9]    agg = [0.0] * self.grid_n    for r, a in fired:                                                      # шаг 3        for i, v in enumerate(self._out_curves[r.then]):            agg[i] = max(agg[i], min(a, v))    s = sum(agg)                                                            # шаг 4    return sum(x * m for x, m in zip(self._grid, agg)) / s if s > 1e-12 else 50

    Полный код у меня содержит около 200 строк, библиотеки вроде scikit-fuzzy я не подключал, пускай они и могли облегчить мне жизнь. Моя задача — исследовать работу от и до, разобраться в теме и применить все на практике.

Разборчик дефаззификации.

Центр тяжести (центроид), о котором мы говорили, или точка равновесия:

y* = ∫ y·μ(y) dy / ∫ μ(y) dy

Биссектриса. Точка, делящая площадь пополам. Устойчивее к длинным хвостам.

Среднее по максимумам (MOM). Среднее тех точек, где агрегат достигает максимума. Резче, ближе к «решению», а не к «компромиссу».

Метод высот (он же нулевого порядка Сугено). Не строим фигуру вообще: берём средневзвешенное центров тяжести термов-заключений, где вес — сила правила.

y* = Σ (сила_i × центр_i) / Σ сила_i

Ошибка с которой я столкнулся здесь в том, что верхний объявленный предел обрезал срочность. То есть у меня буквально не существовало композитных понятий в духе «очень + срочно» и «предельно + срочно». Сошелся на том, что версия Мамдани хорошо подойдет при объяснении человеку, а если требуется скорость и подстройка по данным — метод Сугено. Так как задачи ИБ реализуются с участием аудитора, выбор очевиден.

Возвращаемся к нашим CVSS, EPSS и KEV

Мы уже говорили о том, что CVSS (Common Vulnerability Scoring System) — стандарт оценки уязвимостей. На данный момент у него версия 3.1, является основной. Дает число от 0 до 10 и вектор вида: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Расшифровка вот:

Метрика

Что означает

Значения

AV Attack Vector

откуда атакуют

N сеть, A смежная сеть, L локально, P физически

AC Attack Complexity

насколько сложно

L низкая, H высокая

PR Privileges Required

нужны ли права

N нет, L низкие, H высокие

UI User Interaction

нужен ли пользователь

N не нужен, R требуется

S Scope

выходит ли за пределы компонента

U не выходит, C выходит

C/I/A

ущерб конфиденциальности / целостности / доступности

N нет, L низкий, H высокий

Базовая оценка складывается из двух половин:

  • Exploitability subscore — из AV, AC, PR, UI. Максимум 3.9. Отвечает на вопрос «насколько легко дотянуться».

  • Impact subscore — из S, C, I, A. Максимум около 6.0. Отвечает на вопрос «насколько больно, если дотянулись».

*Из этого сразу исключаются: будут ли это эксплуатировать, и что уязвимый компонент стоит в изолированном сегменте. Это просто не учитывается.

KEV (Known Exploited Vulnerabilities) — каталог американского агентства CISA, куда попадают уязвимости с подтвержденными фактами эксплуатации в реальных атаках. В нашем случае будет выступать, как разметка данных.

Тыкаем в CISA KEV

Тыкаем в CISA KEV

Методика ФСТЭК

В России оценка критичности уязвимостей регулируется отдельным методическим документом. Его полное название: Методический документ «Методика оценки уровня критичности уязвимостей программных, программно-аппаратных средств», утверждён ФСТЭК России 30 июня 2025 года. (не путать с документом от 2022 года)

Если кратко, то специалисты (в т.ч. начинающие как я), должны усвоить, что требования заложены:

  1. В приказ ФСТЭК №21 (персональные данные)

  2. В приказ ФСТЭК №239 (критическая информационная инфраструктура)

  3. В приказ ФСТЭК №117 (государственные информационные системы)

Формула действующей редакции:

V = I_{cvss} × I_{infr} × (I_{at} + I_{imp})

Соответственно они собирают между собой влияние на функционирование системы, наличие средств эксплуатации, последствия эксплуатации (по порядку перечисления):

 \begin{aligned}  [1ex] I_{infr} &= 0,5 \cdot K + 0,2 \cdot L + 0,3 \cdot P \\ I_{at} &= 1,0 \cdot E \\ I_{imp} &= 1,0 \cdot H \end{aligned}

Таблицы значений:

K — тип компонента

L — доля уязвимых компонентов

компоненты критических процессов

1,1

более 70%

1,0

межсетевые экраны

0,9

50-70%

0,8

сетевые устройства и шлюзы

0,9

10-50%

0,6

телекоммуникационное оборудование

0,8

менее 10%

0,5

серверы

0,7

пользовательские устройства

0,5

P — защита периметра

системы хранения данных

0,4

доступно из Интернета

1,1

другие компоненты

0,1

недоступно из интернета

0,6

E — эксплуатация

H — последствия

эксплуатируется в реальных атаках

выполнение произвольного кода

0,5

средства эксплуатации в открытом доступе

повышение привилегий

0,5

сведений об эксплуатации нет

обход механизмов безопасности

0,4

внедрение кода

0,34

Уровни критичности

получение конфиденциальной информации

0,3

критический

V > 8,0

нарушение целостности данных

0,3

высокий

5,0 ≤ V ≤ 8,0

отказ в обслуживании

0,26

средний

2,0 ≤ V < 5,0

чтение/запись локальных файлов

0,18-0,2

низкий

V < 2,0

межсайтовый скриптинг

0,1

Сравнивая с предыдущей редакцией, мы видим следующие доработки:

  1. Множитель предыдущей формулы лежал в диапазоне 0,50…0,90 — итог мог отличаться от базового CVSS максимум в 1,8 раз. В новой редакции 0,066 и 1,19 позволяет создать разброс в 18 раз и получить больше «власти» над результатом.

  2. Добавились два фактора: факт эксплуатации и тип последствий.

Два контура работы и 38 правил.

Соответственно про архитектуру проекта.

  • Контур №1 — техническая срочность. Работает на данных, которые есть публично для любой CVE: вектор CVSS, оценка EPSS, дата публикации. Его можно проверить на исторических записях.

  • Контур №2 — поправка на контекст. Принимает результаты первого плюс то, что знает исключительно владелец системы: доступность компонентов, критичность самого актива, силу компенсирующих мер. Публичные данные в такой работе бесполезны, начнем с того, что их просто нет.

Контур такой работы позволяет следовать современным тенденциям анализа, мы просто даем этому название и объяснение. На деле думаю все до этого доходят в формате «само собой разумеется».

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

Кандидаты на вход

Кандидаты на вход

Правила, которые можно применить ниже.

TECH_RULES = [    Rule(And([Is("epss","критическая"), Is("impact","полный")]), "немедленная"),    Rule(And([Is("epss","критическая"), Is("impact","ограниченный")]), "срочная",),    ...        Rule(And([Is("epss","пренебрежимая"), Is("impact","полный"),              Is("freshness","свежая")]), "повышенная"),    Rule(And([Is("epss","пренебрежимая"), Is("impact","полный"),              Is("freshness","зрелая")]), "отложить",)]

Это обрезанный вариант, суммарно у меня двадцать правил в первом контуре, восемнадцать во втором. На выходе число 0…100, которое раскладывается в «корзины»: немедленно (до 24ч), срочно (7 дней), повышенный (30 дней), плановый (90 дней), наблюдение (принятие риска).

Проверка данных

Берем данные от 1 февраля 2026 года (условно время Т0) — 193 120 уязвимостей, опубликованных до этой даты (по факту, получается такой снапшотик), у которых есть вектор CVSS v3.1 и оценка EPSS. Те, что уже были в каталоге KEV на эту дату — выбрасываем, с ними все понятно.

Мы сравним попал ли этот CVE в каталог CISA KEV после 1 февраля. Другими словами, проверяем что случилось с нашими «подопытными» за последние 7 месяцев.

Например, заглядывая на будущее

У тех 68 CVE, которые реально начали эксплуатировать после 1 февраля, EPSS в феврале был медианно 0.022 (то есть модель EPSS сама ещё не подозревала об опасности), а сегодня, в сентябре — уже 0.520 (после того как эксплуатация стала известна, EPSS у этих же CVE резко вырос, потому что модель EPSS тоже учитывает свежие данные об эксплуатации). Если бы мы взяли сегодняшний EPSS (0.520), то наша метрика бы учитывала соответствующий рост важности, чего быть не должно.

Мы сравним по итогу данные по CVSS, по EPSS, по «жесткому дереву» (готовая методика SSVC, decision-tree подход, дающее 5 возможных категорий вместо непрерывной шкалы).

Проблема которая тут сразу вскрывается: у CVSS всего несколько десятков различных значений на 193 тысячи записей (потому что CVSS считается по формуле с ограниченным набором входов — 8.8, 9.8, 7.5 и так далее, много CVE получают ровно одно и то же число). У «жёсткого дерева» вообще всего 5 разных выходных значений. Значит, тысячи CVE имеют одинаковый ранг.

Для фикса этой проблем используем «случайный разрыв совпадений» (random tie-breaking) — стандартный статистический прием для ситуации.

for st, en in zip(starts, ends):          # группы одинаковых значений    size, pos = en - st, labels[st:en].sum()    if seen + size <= k:        found += pos; seen += size    else:        found += pos * (k - seen) / size  # частичное попадание группы в топ-k        break

Результатики

Мы уже знаем, что среди такого гигантского количества уязвимостей, 68 из них реально были проэксплуатированы.

Политика

Задач

Найдено из 68

На 1000 задач

CVSS ≥ 9.0 — распространённая практика

27 480

24

0.87

CVSS ≥ 7.0

101 903

61

0.60

EPSS ≥ 0.1

7 528

32

4.25

EPSS ≥ 0.1 ИЛИ CVSS ≥ 9.0

32 230

46

1.43

нечёткая: корзина «немедленно»

3 654

25

6.84

нечёткая: «немедленно» + «срочно»

9 087

32

3.52

нечёткая: плюс «повышенный»

23 000

46

2.00

Тут мы стараемся понять, как уязвимости стоит «чинить» в первую очередь. Способы простые: два наивных порога по CVSS, порог по EPSS, комбинация или-или, и три варианта нечеткой модели с разным бюджетом.

  • «Найдено из 68» — сколько бы мы реально нашли «опасными» из зафиксированных в будущем.

  • «На 1000 задач» — смысл: если бы моя очередь была ровно из 1000 задач, сколько из них в среднем окажутся реально важными.

Сразу фиксируем, что нечеткой логикой мы обнаружили 25 уязвимостей, перебрав меньшее количество вариантов — в 7,5 раз меньше работы (Если сравнивать значения 3 654 против 27 480 данных).

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

Метод

AUC

Работ до 50% полноты

до 80%

CVSS

0.732

40 198

88 049

EPSS

0.745

16 580

110 104

Взвешенная сумма

0.843

9 584

51 219

Жесткое дерево

0.852

14 022

39 604

Нечеткие выводы

0.861

9 550

39 418

AUC (Area Under Curve, площадь под кривой) — число, которое обобщает качество ранжирования по всей очереди целиком, а не в одной конкретной точке сознательно. Типо если наугад вы возьмете одну реальную проэксплуатированную уязвимость (из тех 68) и одну обычную (из 193 052), AUC — вероятность, что метод поставил опасную выше, чем обычную.

Под работой до полноты подразумевается следующее: если я хочу найти например половину (34 из 68, 50% полнота) реально опасных уязвимостей — сколько задач для этого придется перебрать и проверить, если сортировать по этому методу от самого срочного к самому неважному. Тут мы можем проследить, что EPSS неплохо справляется на старте, но до 80% ему внезапно требуется в разы больше усилий. Об этом мы уже писали ранее.

Полнота в первых k строках очереди

Полнота в первых k строках очереди

Слепое пятно EPSS

Если EPSS так хорош, то зачем вообще другие модели поверх него?

Сравнение EPSS и док-во, что EPSS нужно время

Сравнение EPSS и док-во, что EPSS нужно время

Слева — частота реальной эксплуатации по возрасту уязвимости. Справа — медианный EPSS по той же разбивке. Свежие уязвимости эксплуатируют в 6–10 раз чаще (20.7 случая на 10 000 в первый месяц против 2.0–3.1 у тех, что старше года), а EPSS у них при этом самый низкий: медиана 0.00036 против 0.00327 у записей старше трёх лет.

Проверка на подвыборке подтверждает: из 68 проэксплуатированных 11 имели на T0 EPSS ниже 0.001, и медианный возраст этих одиннадцати — 51 день, тогда как у тех, где EPSS был высоким, — 1507 дней.

Документация FIRST (собственно они этим занимаются, гласит примерно следующее):

«В день раскрытия уязвимости публично доступной информации может быть мало. Обсуждение ещё не накопилось. Кода эксплойта может не существовать». И далее: «по мере накопления сигнала в последующие дни и недели оценки обновляются, отражая его».

Наглядный график AUC

Наглядный график AUC

Без возраста AUC падает с 0.861 до 0.742 — почти весь «выигрыш» (если его таким можно назвать) над EPSS исчезает. То есть вклад даёт не сам факт применения нечёткой логики, а конкретное знание о слепом пятне, оформленное в правило.

Проверяем с методикой ФСТЭК

Публичной методики сближения нет, поэтому приближаем самостоятельно. Часть коэффициентов из-за этого например вычислить невозможно, из-за этого мы вынуждены выносить это как важное НО:

  1. K (тип компонента) — зафиксирован на 0,7 (серверы) как типичный случай.

  2. L (доля уязвимых компонентов) — известна только владельцу системы. Фиксируем на 0,5.

  3. P (защита периметра) — сопоставлен вектору атаки AV:N → 1,1, иначе → 0,6.

  4. H (последствия) — сопоставлен по типу слабости CWE согласно таблице методики; при отсутствии CWE — по вектору CVSS (например, C:H/I:H/A:H → «выполнение произвольного кода» 0,5).

  5. E (эксплуатация) — в двух вариантах: (a) E = 0,1 для всех — честное состояние знания на наше время февраля 2026; (б) E = 0,3 при EPSS ≥ 0,1, иначе 0,1 — EPSS как измеримое приближение к «средства в открытом доступе»

Я понимаю, что методики отличаются, и в целом для сортировки почти 200 000 методика ФСТЭК ну просто не предназначена. Поэтому формулируем вопрос в духе: то останется от ее различающей способности, когда организационную часть зафиксировали?

Проверяем старым способом (2022 год):

Уровень (2022)

Записей

% популяции

проэксплуатировано

критичный

491

0,25%

3

высокий

89 972

46,6%

50

средний

102 657

53,2%

15

AUC тут у нас где-то 0,710, то есть даже чуть ниже, чем у голого CVSS (0,732 ранее). Не забываем, что на 2022 год у нас были заметные ограничения.

При прогоне исходя из новой редакции получилось, что на уровне «критический» и «высокий» — по ноль записей. Это не ошибка расчетов, данные перепроверены по формуле.

Если делать вывод, то пока нет статуса эксплуатации уязвимости, то уровень «критический» недостижим. При типичных K и L недостижим также и уровень «высокий» . Поток в таком случае 4,68 — «средний».

Методика резервирует высшие уровни за уязвимостями с подтвержденной эксплуатацией. Следовательно «инфляции критичности» (которая была в редакции 2022 года) больше нет.

сравнение редакций методики и добавленных методов

сравнение редакций методики и добавленных методов

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

Если идти по методике (б), которую мы отмечали чуть ранее, проговаривая п.6 E (эксплуатацию), то получаем вот такие данные:

Политика

Задач

Найдено из 68

На 1000 задач

2022 крит+выс

89 972

50

0,56

2025 (E=0,1)

все в среднем/низком

2025 (E из EPSS): крит+выс

3 107

19

6,12

нечеткий вариант: крит

7 991

27

3,38

нечеткий вариант: крит+выс

21 178

41

1,94

для сравнения: CVSS ≥ 9.0

27 480

24

0,87

При маленьком бюджете (сколько проверок условно может позволить себе команда), методика с измеримым Е получается точнее всех: 3 107 задач, 19 находок. Это 28%. Нечеткий вариант находит больше, но и требует большего объема работы.

Меняем текущую редакцию методики ФСТЭК и применяем правила нечеткой логики

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

FSTEC_V = LinguisticVar("V", 0.0, 12.0, {    "низкий":      trap(0.0, 0.0, 1.0, 3.2),    "средний":     trap(1.4, 2.6, 4.2, 5.8),    "высокий":     trap(4.2, 5.2, 7.2, 9.2),    "критический": trap(7.0, 8.8, 12.0, 12.0),})

Что изменилось?

  1. Трехступенчатый E заменен непрерывной шкалой. Методика различает три состояния; EPSS дает плавный переход между ними и обновляется ежедневно. (Напоминаю, что в оригинале у E это 3 варианта — 0,1 в значении нет данных, 0.3 есть публичный эксплойт, 0,6 реальная эксплуатация и тут получается присутствует резкий переход между значениями)

  2. Бинарный P становиться степенью (было 0,6 или 1,1). А мы делаем шкалу 0…1.

  3. Пороги 8,0 / 5,0 / 2,0 перестают быть обрывами.

  4. Добавляется возраст (то самое слепое пятно, которые мы ранее обнаружили и из-за которого E опаздывает)

Результат: AUC 0,841 против 0,730 у методики в варианте (а) и 0,791 у методики в варианте (б). В уровне «критический» — 7 991 запись и 27 найденных из 68.

Что из этого следует:

  1. Для инженера. Если вы применяете нынешнюю методику — на потоке с зафиксированными коэффициентами, то вы не найдете критов и высокого приоритета.

  2. Для организации. Коэффициенты K и L может выставлять только организация, а так как тут учебный проект — наши данные имеют место быть неполноценными.

  3. Предложение для регуляторов. Заменить трехступенчатый E непрерывной оценкой вероятности эксплуатации, бинарный P — степенью, а жесткие пороги — плавными переходами.

Рубрика «Косячки» (что сломалось)

Да, я столкнулся с багами/дефектами/ошибками — называйте как хотите.

  1. Насыщение Мамдани. В первой версии верхний терм EPSS был плато со степенью 1.0 на всём диапазоне 0.32…1.0. Уязвимости с EPSS 0.35 и 0.97 получали одинаковую срочность, и в топ-1000 модель проигрывала EPSS: 13.0% против 16.2%. Поэтому внутри верхней корзины нечеткий вывод не ранжирует вообще.

  2. Сеть безопасности, которая у меня находится в контуре №2, сработала на половину. Из 11 невидимых для EPSS уязвимостей модель подняла только 2, еще 5 попали в плановый, а 4 остались под наблюдением. Как итог, соответствующие правила мне помогли только когда тяжесть последствий действительно высока.

  3. Была идейка добавить вход «существует ли публичный эксплойт». Проблема оказалась в базах NVD. Они буквально обновляют данные за прошедшие даты, следовательно отличить февральское от будущего невозможно. Пришлось отказаться от соответствующей идеи.

Я готов!

Я готов!

Чуть-чуть объективности

О моей работе:

  1. Не предсказывает эксплуатацию лучше EPSS

  2. Не спасает от нулевого дня (ну мало ли кто-то подумал об этом)

  3. 68 положительных примеров — слишком мало

  4. KEV — неполная разметка. И ее еще труднее наложить на российскую специфику

  5. Второй контур я прогнал на тестовом стенде и получил результат достаточно короткий: нужны реальные данные.

Источники:

  • Разметка (факты эксплуатации). CISA Known Exploited Vulnerabilities, официальное зеркало — github.com/cisagov/kev-data, файл known_exploited_vulnerabilities.json, версия каталога 2026.09.10, 1705 записей. Первоисточник — cisa.gov, включение регулируется директивой BOD 22-01.

  • Вероятность эксплуатации. EPSS v4, архив исторических снимков github.com/empiricalsec/epss_scores, файлы 2026/epss_scores-2026-02-01.csv.gz (313 234 CVE) и 2026/epss_scores-2026-09-09.csv.gz (370 894 CVE). Определение и документированные ограничения модели — first.org/epss и first.org/epss/how-it-works.html.

  • Векторы CVSS и метаданные CVE. Годовые фиды NVD — github.com/fkie-cad/nvd-json-data-feeds, 389 479 записей. Спецификация CVSS v3.1 — first.org/cvss.

  • Методика ФСТЭК. Полное название: методический документ «Методика оценки уровня критичности уязвимостей программных, программно-аппаратных средств». Действующая редакция утверждена 30.06.2025.

ссылка на оригинал статьи https://habr.com/ru/articles/1081470/