Logo Craft Homelab Docs Нейросети Хостинг Обучение Контакты
VEX и Trivy: как убрать ложные уязвимости из отчётов сканера Трендовые github проекты в нашем телеграм канале. Подпишись →
15 октября 2026 г.

Шум в отчётах сканера уязвимостей и формат OpenVEX

Сканер в CI/CD падает на CRITICAL, релиз блокируется, а разбор показывает, что уязвимая функция лежит в транзитивной зависимости и из вашего кода не вызывается ни разу. Через неделю та же CVE снова валит пайплайн, и кто-то в очередной раз объясняет в чате, почему её можно игнорировать. Формат VEX (Vulnerability Exploitability eXchange) решает именно эту задачу: решение по конкретной уязвимости записывается в машиночитаемый файл, хранится рядом с кодом и автоматически применяется сканером.

Три исхода после сканирования

Сканер (например, Trivy или Syft для генерации SBOM) строит SBOM — список всех прямых и транзитивных компонентов с версиями и лицензиями — и сверяет его с базами уязвимостей. Каждая найденная CVE попадает в одну из трёх категорий:

  1. Подтверждена и эксплуатируема. Чинить: обновлять зависимость, патчить код.
  2. Подтверждена, но быстро исправить нельзя. Патча от вендора нет или исправление требует несоразмерной работы. Пример — уязвимость в OpenTelemetry внутри Kubernetes 1.29: в апстриме её закрыли только ближе к 1.31 после глубокой переработки кода, а самостоятельный бэкпорт в 1.29 — огромный объём работы. Такие уязвимости митигируют конфигурацией или организационными мерами.
  3. False positive. Уязвимого кода в продукте фактически нет.

Статус зависит от контекста. Та же уязвимость OpenTelemetry реальна, но если сбор телеметрии в control plane выключен в конфигурации, эксплуатировать её в этом кластере нельзя, и для него она становится ложноположительной.

Откуда берутся ложные срабатывания

Простой пример — утилита на Go, которая принимает имя образа и печатает его манифест. Для этого используется remote.Image из github.com/google/go-containerregistry/pkg/v1/remote, больше ничего:

ref, err := name.ParseReference(os.Args[1])
if err != nil { panic(err) }
img, err := remote.Image(ref, remote.WithContext(context.Background()))
if err != nil { panic(err) }
manifest, err := img.Manifest()

Trivy на этом проекте показывает:

go.mod (gomod)
Total: 5 (UNKNOWN: 0, LOW: 1, MEDIUM: 3, HIGH: 0, CRITICAL: 1)

В списке есть CVE-2021-41092 в github.com/docker/cli — при определённых условиях возможна утечка учётных данных реестра. Эту зависимость никто явно не объявлял: её подтянул go-containerregistry как indirect. Код утилиты функциональность docker-cli не вызывает.

Большинство сканеров не делают reachability analysis — не проверяют, достижим ли уязвимый код из точки входа программы. Логика простая: библиотека в SBOM есть, по ней есть CVE, значит весь артефакт уязвим. Пока анализ достижимости не стал нормой, решение «эта CVE к нам не относится» приходится принимать человеку и где-то его фиксировать.

Что лежит внутри VEX

VEX-документ привязывает идентификатор уязвимости (CVE) к конкретному продукту и указывает статус: затронут продукт или нет, с обоснованием и комментарием. Компоненты идентифицируются через PURL (Package URL), например pkg:golang/github.com/docker/cli или pkg:apk/distro/git@2.39.0-r1?arch=x86_64.

Trivy понимает два формата VEX: CycloneDX и OpenVEX. OpenVEX — компактный JSON с метаданными (автор, время, версия) и массивом statements:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://openvex.dev/docs/example/vex-9fb3463de1b57",
  "author": "Wolfi J Inkinson",
  "timestamp": "2023-01-08T18:02:03.647787998-06:00",
  "version": 1,
  "statements": [
    {
      "vulnerability": { "name": "CVE-2014-123456" },
      "products": [
        { "@id": "pkg:apk/distro/git@2.39.0-r1?arch=armv7" },
        { "@id": "pkg:apk/distro/git@2.39.0-r1?arch=x86_64" }
      ],
      "status": "fixed"
    }
  ]
}

Для статуса not_affected указывается justification — машинное обоснование, например component_not_present.

Подавляем CVE через vexctl

Руками JSON писать не нужно — его генерирует vexctl из проекта OpenVEX:

vexctl create \
  --author "Team <security@example.com>" \
  --product "pkg:golang/github.com/docker/cli" \
  --vuln "CVE-2021-41092" \
  --status "not_affected" \
  --justification "component_not_present" \
  --impact-statement "Affected functions not used" \
  > known_vulnerabilities.vex

Повторный запуск с флагом --vex:

trivy fs . --vex=known_vulnerabilities.vex
go.mod (gomod)
Total: 4 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 0, CRITICAL: 1)

CVE-2021-41092 из основного отчёта ушла. С --show-suppressed она выводится отдельным блоком Suppressed Vulnerabilities (Total: 1), так что решение видно при аудите, и его легко пересмотреть, когда изменится контекст — например, если код начнёт вызывать docker-cli.

Где хранить VEX: Git и OCI Registry

Логичное правило — VEX лежит там же, где источник правды для артефакта:

  • для исходников — в Git, в директории сборки конкретного образа:
modules/
  module/
    images/
      image1/
        <исходный код и сборочные инструкции>
        known_vulnerabilities.vex
      image2/
  • для собранных образов — в OCI Registry, как аттестация к образу.

Trivy умеет забирать OpenVEX прямо из реестра. Аттестация публикуется как отдельный OCI-артефакт с тегом по схеме sha256-<digest_образа>.att. Для образа registry.example/app@sha256:abc... VEX-артефакт будет лежать под тегом registry.example/app:sha256-abc....att. Публиковать и подписывать такие аттестации можно через Cosign из Sigstore, образы собирать обычным Docker — специфичный тулинг не нужен.

Проверка на kube-apiserver 1.29, где сидит та самая неисправимая уязвимость OpenTelemetry:

trivy image registry.example/sys/platform@sha256:b38222f1... \
  --vex=oci --show-suppressed
usr/bin/kube-apiserver (gobinary)
Total: 8 (UNKNOWN: 0, LOW: 0, MEDIUM: 4, HIGH: 4, CRITICAL: 0)

Suppressed Vulnerabilities (Total: 1)

С --vex=oci Trivy по дайджесту образа сам находит аттестацию в реестре, применяет обоснование и переносит CVE в подавленные. В CI для этого хватает консольного Trivy, для непрерывного аудита в рантайме подходит Trivy Operator.

Схема для пайплайна

  1. Разработчик пушит код; пайплайн собирает образ и кладёт его в реестр по неизменяемому дайджесту SHA-256.
  2. Шаг trivy image ... --vex=oci блокирует релиз только на уязвимостях, по которым нет VEX-решения.
  3. Новая CVE разбирается один раз: если она неэксплуатируема или митигирована, в репозиторий рядом с образом коммитится запись в known_vulnerabilities.vex, ревью идёт обычным merge request.
  4. При сборке VEX публикуется как подписанная аттестация sha256-<digest>.att.
  5. Периодически стоит прогонять отчёт с --show-suppressed и пересматривать подавленные CVE: обоснование component_not_present устаревает, как только кто-то начинает использовать уязвимый пакет.

Ложные срабатывания этот подход не устраняет — сканер по-прежнему их находит. Зато решение по каждому из них принимается один раз, хранится с историей в Git и в реестре и перестаёт блокировать CI/CD.