В предыдущей статье я рассказывал, как я интегрировал EPSS (Exploit Prediction Scoring System) с системой приоритизации уязвимостей, чтобы уйти от ограничений классической CVSS-модели.
Из практики CVSS не подсказывает, будет ли уязвимость реально эксплуатироваться. Это приводит к перегрузке команд, неэффективному использованию ресурсов и пропуску уязвимостей, которые получат высокий уровень критичность в дальнейшем.

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

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

Основные компоненты и архитектура

Сама приоритизация построена вокруг объединения трёх ключевых показателей: CVSS (критичность уязвимости), EPSS (вероятность эксплуатации), HC (критичность актива). Вместе они позволяют точнее оценивать реальный риск для инфраструктуры и сосредотачиваться на действительно важных уязвимостях.

Источники данных:

  • EPSS ежедневно загружается из открытого источника FIRST EPSS и сохраняется как отдельный файл.

  • CVSS и HC берётся из результата сканирования сканеров на уязвимости.

Технический стек:

  • PostgreSQL — основная СУБД.

  • Budibase — Это центр принятия решений. Интерфейс для аналитиков и руководства: дашборды, фильтры, графики, экспорт.

  • Обновление данных СУБД и первичная обработка источников данных производится скриптом в одном из контейнеров.

(с)Analysis icons created by Freepik - Flaticon
(с)Analysis icons created by Freepik - Flaticon

Результаты внедрения

После добавления метрики EPSS, был проведен сравнительный анализ по данным за месяц. Расчет я вел с помощью Python и библиотеки pandas, по данным, выгруженным в .csv файл за месяц, с датой выпуска EPSS на конец месяца. Ниже в расчетах и таблице резюме ключевых изменений которые получились.

import_vulns = pd.read_csv(csv_file, delimiter=';', encoding='utf8', dtype=str)
import_vulns["VulnDiscoveryTime"] = pd.to_datetime(import_vulns["VulnDiscoveryTime"])
import_vulns["VulnFixedTime"] = pd.to_datetime(import_vulns["VulnFixedTime"])
import_vulns["CVSS3"] = import_vulns["CVSS3"].str.replace(",", ".", regex=False).astype(float)
import_vulns["epssScore"] = import_vulns["epssScore"].astype(int)
import_vulns["epssPercentile"] = import_vulns["epssPercentile"].astype(int)
import_vulns_month = import_vulns[(import_vulns["VulnDiscoveryTime"].dt.year == 2025) &
  (import_vulns["VulnDiscoveryTime"].dt.month == 9)]
# Кол-во переоцененных уязвимостей (CVSS > 7, EPSS < 0.01)
overrated = len(import_vulns_month[(import_vulns_month["CVSS3"] >= 7) & (import_vulns_month["epssScore"] < 1)])
print(f"Кол-во переоцененных уязвимостей (CVSS > 7, EPSS < 0.01) - за месяц: {overrated}")
...

Кол-во переоцененных уязвимостей (CVSS > 7, EPSS < 0.01) - за месяц: 224816
# Кол-во недооцененных уязвимостей (CVSS < 7, EPSS > 0.90)
belowrate = len(import_vulns_month[(import_vulns_month["CVSS3"] < 7) & (import_vulns_month["epssScore"] >= 90)])
print(f"Кол-во недооцененных уязвимостей (CVSS < 7, EPSS > 0.90) - за месяц: {belowrate}")
...

Кол-во недооцененных уязвимостей (CVSS < 7, EPSS > 0.90) - за месяц: 341
# Кол-во уязвимостей в топ-10% по CVSS
threshold = import_vulns_month["CVSS3"].quantile(0.90)
top_10_p = import_vulns_month[import_vulns_month["CVSS3"] >= threshold]
count = len(top_10_p)
print(f"Кол-во уязвимостей в топ-10% по CVSS за месяц: {count}")
...

Кол-во уязвимостей в топ-10% по CVSS за месяц: 89519
# Кол-во уязвимостей в топ-10% по EPSS
top_epss = import_vulns_month[import_vulns_month['epssPercentile'] >= 90]
count = len(top_epss)
print(f"Кол-во уязвимостей в топ-10% по EPSS за месяц: {count}")
...

Кол-во уязвимостей в топ-10% по EPSS за месяц: 11584
# Ложноположительная приоритизация
df_prioritized = import_vulns[(import_vulns["VulnDiscoveryTime"].dt.year == 2025) &
(import_vulns["VulnDiscoveryTime"].dt.month == 9)]
df_prioritized["cvss_prioritized"] = df_prioritized["CVSS3"] >= 7
df_prioritized["epss_prioritized"] = df_prioritized["epssScore"] >= 50 #берем вероятность >=50%
#реальные угрозы - появился экслойт/PoC
exp_patern = r'Exploitable: true'
df_prioritized["exploitable"] = df_prioritized["Metrics"].str.contains(exp_patern, case=False, na=False)

# FP CVSS
cvss_fp = df_prioritized[(df_prioritized["cvss_prioritized"]) & (~df_prioritized["exploitable"])]
cvss_fp_rate = len(cvss_fp) / len(df_prioritized[df_prioritized["cvss_prioritized"]]) * 100
# FP EPSS
epss_fp = df_prioritized[(df_prioritized["epss_prioritized"]) & (~df_prioritized["exploitable"])]
epss_fp_rate = len(epss_fp) / len(df_prioritized[df_prioritized["epss_prioritized"]]) * 100

print(f'Ложноположительная приоритизация по CVSS - за месяц: {cvss_fp_rate:.1f}%')
print(f'Ложноположительная приоритизация по EPSS >= 0.5 - за месяц: {epss_fp_rate:.1f}%')
...

Ложноположительная приоритизация по CVSS - за месяц: 97.0%
Ложноположительная приоритизация по EPSS >= 0.5 - за месяц: 89.5%

Таблица с результатами:

Метрика

Результат

Комментарий

Кол-во переоцененных уязвимостей (CVSS > 7, EPSS < 0.01)

224816

Указывает на массовую переоценку риска

Кол-во недооцененных уязвимостей (CVSS < 7, EPSS > 0.90)

341

Если эксплойт появится балл CVSS вырастет

Кол-во уязвимостей в топ‑10% по CVSS

89519

CVSS раздает высокие баллы слишком щедро

Кол-во уязвимостей в топ‑10% по EPSS

11584

Повышенное внимание к угрозам с вероятностью эксплуатации

Доля ложноположительных приоритизаций (false positive rate) по CVSS

97.0%

Выше чем при EPSS

Доля ложноположительных приоритизаций (false positive rate) по EPSS

89.5%

Ниже чем при CVSS

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

Очень большой объём уязвимостей, которые по CVSS считаются “высокими” (≥ 7.0),
но по факту имеют почти нулевую вероятность эксплуатации (EPSS < 1%).
Это указывает на массовую переоценку риска и, как следствие:

  • Напрасные затраты ресурсов на устранение “неопасных” уязвимостей

  • Загрузку аналитиков и системных администраторов

  • Потерю фокуса на действительно критичных рисках

Уязвимости, которые по CVSS считаются как некритичные могут не устраняться оперативно, но у них очень высокая вероятность эксплуатации (EPSS > 90%).

Ложноположительная приоритизация - это уязвимость, которую система пометила как высокоприоритетную, но она не эксплуатируется и не требует срочной реакции.

CVSS помечает как высокий риск 97 из 100 уязвимостей, которые на практике не эксплуатируются в данный момент времени.
Даже EPSS не идеален, но ошибается заметно реже, а при более высоком пороге (например, EPSS > 0.9) точность выше.

Прогноз устранения уязвимостей

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

# Прогноз устранения уязвимостей
df_forecast = import_vulns[(import_vulns["VulnDiscoveryTime"].dt.year == 2025) &
(import_vulns["VulnDiscoveryTime"].dt.month == 9)]
#Условный порог
df_forecast["cvss_high"] = df_forecast["CVSS3"] >= 7
df_forecast["epss_high"] = df_forecast["epssScore"] >= 50
df_forecast["cvss_only"] = df_forecast["cvss_high"] & (~df_forecast["epss_high"])
df_forecast["epss_only"] = df_forecast["epss_high"] & (~df_forecast["cvss_high"])
df_forecast["both"] = df_forecast["cvss_high"] & df_forecast["epss_high"]
df_forecast["exploitable"] = df_forecast["Metrics"].str.contains(exp_patern, case=False, na=False)
#Фильтруем по активным
df_forecast_active = df_forecast[df_forecast["Status"] == 'new']
df_forecast_active["cvss_high"] = df_forecast_active["CVSS3"] >= 7
df_forecast_active["epss_high"] = df_forecast_active["epssScore"] >= 50
df_forecast_active["cvss_only"] = df_forecast_active["cvss_high"] & (~df_forecast_active["epss_high"])
df_forecast_active["epss_only"] = df_forecast_active["epss_high"] & (~df_forecast_active["cvss_high"])
df_forecast_active["both"] = df_forecast_active["cvss_high"] & df_forecast_active["epss_high"]
#Фильтруем по устраненным
df_forecast_fixed = df_forecast[df_forecast["Status"] == 'fixed']
df_forecast_fixed["cvss_high"] = df_forecast_fixed["CVSS3"] >= 7
df_forecast_fixed["epss_high"] = df_forecast_fixed["epssScore"] >= 50
df_forecast_fixed["cvss_only"] = df_forecast_fixed["cvss_high"] & (~df_forecast_fixed["epss_high"])
df_forecast_fixed["epss_only"] = df_forecast_fixed["epss_high"] & (~df_forecast_fixed["cvss_high"])
df_forecast_fixed["both"] = df_forecast_fixed["cvss_high"] & df_forecast_fixed["epss_high"]

#Возможно переоцененные уязвимости - не имели высокого EPSS и устранялись
forecast_fixed_overprioritized = df_forecast_fixed[df_forecast_fixed["cvss_only"]]
print(f"Кол-во переоцененных уязвимостей (CVSS >= 7, EPSS < 0,5) - устранено за месяц: {len(forecast_fixed_overprioritized)}")
#Возможно недооцененные уязвимости - имели высокий EPSS, но низкий CVSS и не устранялись
forecast_missed_priority = df_forecast_active[df_forecast_active["epss_only"]]
forecast_missed_priority_exp = df_forecast_active[(df_forecast_active["epss_only"]) & (df_forecast_active["exploitable"])]
print(f"Кол-во недооцененных уязвимостей (CVSS < 7, EPSS > 0,5) - неустраненные за месяц: "
  f"{len(forecast_missed_priority)}, из них с эксплойтом: {len(forecast_missed_priority_exp)}")
...

Кол-во переоцененных уязвимостей (CVSS >= 7, EPSS < 0,5) - устранено за месяц: 115462
Кол-во недооцененных уязвимостей (CVSS < 7, EPSS > 0,5) - неустраненные за месяц: 3430, из них с эксплойтом: 11
# Сравнение MTTR(среднее время до полного устранения)
df_mttr = import_vulns[(import_vulns["VulnDiscoveryTime"].dt.year == 2025) &
(import_vulns["VulnDiscoveryTime"].dt.month == 9)]
df_mttr_fixed = df_mttr[df_mttr["Status"] == 'fixed']
df_mttr_fixed["mttr_days"] = (df_mttr_fixed["VulnFixedTime"] - df_mttr_fixed["VulnDiscoveryTime"]).dt.days
df_mttr_fixed["cvss_high"] = df_mttr_fixed["CVSS3"] >= 7
df_mttr_fixed["epss_high"] = df_mttr_fixed["epssScore"] >= 50 & (df_mttr_fixed["cvss_high"])
df_mttr_fixed["cvss_only"] = df_mttr_fixed["cvss_high"]
df_mttr_fixed["epss_only"] = df_mttr_fixed["epss_high"]
mttr_cvss_fp = df_mttr_fixed[df_mttr_fixed["cvss_only"]]["mttr_days"].mean()
mttr_epss_real = df_mttr_fixed[df_mttr_fixed["epss_only"]]["mttr_days"].mean()
print(f"Cреднее время до полного устранения уязвимостей высокого уровня по CVSS за месяц: {mttr_cvss_fp:.1f} дней")
print(f"Cреднее время до полного устранения уязвимостей высокого уровня c учетом EPSS >= 0.5 за месяц: {mttr_epss_real:.1f} дней")
...

Cреднее время до полного устранения уязвимостей высокого уровня по CVSS за месяц: 10.0 дней
Cреднее время до полного устранения уязвимостей высокого уровня c учетом EPSS >= 0.5 за месяц: 10.0 дней

Метрика

Результат

Комментарий

Кол-во переоцененных уязвимостей (CVSS >= 7, EPSS < 0,5) - устранено за месяц

115462

Слишком много ресурсов может тратиться на переоцененные уязвимости

Кол-во недооцененных уязвимостей (CVSS < 7, EPSS > 0.90)

3430, с эксплойтом: 11

Опасные уязвимости игнорируются

Cреднее время до полного устранения уязвимостей высокого уровня по CVSS за месяц

10 дней

MTTR по CVSS и EPSS ≈ одинаковый (10 дней)

Cреднее время до полного устранения уязвимостей высокого уровня c учетом EPSS >= 0.5 за месяц

10 дней

MTTR по CVSS и EPSS ≈ одинаковый (10 дней)

Из результата выше можно сделать следующие выводы:

  • Десятки тысяч уязвимостей, маловероятных к эксплуатации, устраняются как высокий риск

  • Есть уязвимости которые не были устранены, потому что по CVSS считались некритичными, однако по модели EPSS они реально эксплуатируются и 11 уже имеют активные эксплойты

  • MTTR по CVSS и EPSS одинаковый(10 дней) говорит о том, что переход на EPSS не требует больших усилий, поскольку команды уже способны работать с такими сроками устранения. Это также делает EPSS-приоритизацию легко внедряемой, без увеличения нагрузки.

Затем я сделал анализ MTTR и посмотрел как устраняются уязвимости с учетом разных EPSS порогов:

# Сравнение MTTR устраненных уязвимостей с разным порогом EPSS - полезно для адаптации порога под конкретную среду
for thr in range(10, 100, 10):
	mttr_thr = df_mttr_fixed[df_mttr_fixed["epssScore"] >= thr]["mttr_days"].mean()
	print(f"MTTR устраненных уязвимостей за месяц c EPSS порогом {thr / 100}: {mttr_thr:.1f} дней")

...
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.1: 16.7 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.2: 13.4 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.3: 12.9 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.4: 12.4 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.5: 11.6 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.6: 11.8 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.7: 10.9 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.8: 10.9 дней
MTTR устраненных уязвимостей за месяц c EPSS порогом 0.9: 11.3 дней
  • Результат показал здравую практику, где уязвимости с более высокой вероятностью эксплуатации устраняются быстрее

  • Это подтверждает, что EPSS уже используется интуитивно, но без явного контроля или автоматизации.

Сравнение с решениями вендоров

Параллельно с оптимизацией собственной модели приоритизации, я проводил обзор и сравнение популярных решений для управления уязвимостями, представленных на рынке.
Целью было сравнить решения и выбрать на будущее, наиболее оптимальные варианты.

При сравнении я заметил, что EPSS не поддерживается. В некоторых системах приоритет по-прежнему определяется исключительно CVSS-баллoм или шаблонными тегами.

CVSS-центричное мышление доминирует и почти во всех, рассмотренных решениях уязвимости с CVSS > 7 автоматически попадают в категорию “критичные”, даже если EPSS < 0.01 и по факту отсутствует эксплуатация.

Большинство решений представляют приоритизацию в виде таблицы и карточек, где:

  • отображается CVSS, тип уязвимости, время обнаружения;

  • нет поля “вероятность эксплуатации”;

  • отсутствует агрегированная метрика риска, отражающая комбинированный подход CVSS, EPSS и контекста.

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

Выводы

CVSS переоценивает риски в огромном масштабе. EPSS однозначно имеет смысл использовать при приоритизации и принятии решения о критичности одиночной уязвимости.

Пропущенные 14 уязвимостей уровня средний и ниже по CVSS - в течение месяца с эксплойтом становятся уровнем высокий.

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

Комментарии (0)