MAATRIX / Блог / Аудит зависимостей раз в квартал: что на сервере уже не обновляется автором

Аудит зависимостей раз в квартал: что на сервере уже не обновляется автором

MAATRIX

На сервере годами тихо работает контейнер с образом, который никто не трогал с момента установки. Работает — и ладно, думает админ, пока не выясняется, что автор проекта не публиковал коммитов три года, а в issues висит десяток непочиненных уязвимостей. Разовое обновление системы не решает эту проблему: пакеты ОС обновляются автоматически, а вот зависимости внутри приложений — Docker-образы, npm-модули, Python-библиотеки, отдельные бинарники — живут своей жизнью, и без регулярной ревизии вы просто не узнаете, что часть стека уже никто не поддерживает.

Почему это не решается автообновлением

Автоматические обновления безопасности (unattended-upgrades, dnf-automatic, Watchtower) закрывают только часть картины — ту, где апстрим ещё выпускает патчи. Они работают внутри уже подключённых репозиториев и реестров: если пакет там есть и у него вышла новая версия, обновление подхватится. Но если автор проекта перестал релизить вообще, никакой автообновлятор об этом не сообщит — он просто продолжит держать последнюю доступную версию, и всё будет выглядеть так, будто всё в порядке.

Та же логика касается собственноручно установленных зависимостей: библиотеки в requirements.txt, package.json, Docker-образы сторонних разработчиков, форкнутые репозитории с GitHub, скачанные бинарники без пакетного менеджера. Про них система вообще не сообщит — она про них не знает. Разница принципиальна: то, что обновляется, — это не то же самое, что то, что поддерживается. Проект может формально не иметь открытых обновлений просто потому, что до него полтора года никто не касался.

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

Что считается «зависимостью» на практике

Прежде чем аудировать, нужно очертить периметр. На типичном сервере с несколькими Docker-сервисами в зависимости попадает не только код приложения — практически весь стек:

  • Базовые образыFROM python:3.11-slim, FROM node:20-alpine и подобные строки в Dockerfile.
  • Образы сторонних сервисов — готовые контейнеры типа ghcr.io/someauthor/app:latest, которые вы не пишете сами, а только конфигурируете.
  • Пакеты языковых экосистем — записи в requirements.txt, package.json, go.mod, composer.json, Gemfile.
  • Системные демоны и утилиты, поставленные не из репозитория ОС — сторонние .deb/.rpm, установка через curl-скрипт, сборка из исходников.
  • Плагины и расширения — модули nginx, плагины CMS, темы, аддоны для CI-раннеров.
  • Инфраструктурные обвязки — Terraform-провайдеры, Ansible-роли из Galaxy, экшены GitHub Actions от сторонних авторов.

Часть этого списка живёт вне пакетных менеджеров и никогда не попадёт в отчёт apt list --upgradable. Именно эта часть — источник главного риска: про неё легко забыть, потому что формально она «не просит обновления».

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как составить список используемых компонентов

Первый шаг квартального аудита — не оценка, а инвентаризация. Пока нет полного списка, оценивать нечего.

Для Docker-стека самый быстрый способ получить список используемых образов:

docker ps -a --format '{{.Image}}' | sort -u

Чтобы увидеть точные версии (тег и digest) для сверки с апстримом:

docker images --digests --format '{{.Repository}}:{{.Tag}} {{.Digest}}'

Для Python-проектов — зафиксированные версии из окружения, а не абстрактный requirements.txt с диапазонами:

pip freeze > /tmp/audit-python-$(date +%F).txt

Для Node.js — то же самое через npm ls --all --json > /tmp/audit-node.json или, если используется pnpm/yarn, соответствующий аналог. Полезно сразу прогнать npm outdated, но помните: «outdated» показывает разницу версий, а не то, жив ли автор пакета — это разные вопросы, и дальше мы их разводим.

Системные пакеты не из репозитория ОС стоит выписывать вручную в отдельный файл — по опыту, именно они чаще всего оказываются забытыми. Практический приём: заведите один текстовый реестр /opt/audit/tracked-deps.md со столбцами «компонент — источник — как установлен — дата последней проверки» и обновляйте его при каждом квартальном аудите, а не только при первом. Без этого файла каждый новый аудит начинается с нуля, потому что то, что было «неочевидным сторонним компонентом» полгода назад, никто не запомнил.

Как проверять активность проекта

Когда список компонентов есть, для каждого — особенно для того, что не идёт через apt/dnf, — нужно ответить на три вопроса: когда был последний релиз, когда был последний коммит в принципе (не обязательно релиз), и есть ли открытые неисправленные уязвимости.

Для проектов на GitHub дату последнего коммита и релиза можно получить через API без авторизации (с невысоким лимитом запросов, но для ручного квартального аудита этого достаточно):

curl -s https://api.github.com/repos/OWNER/REPO | jq '.pushed_at, .archived'
curl -s https://api.github.com/repos/OWNER/REPO/releases/latest | jq '.published_at, .tag_name'

Поле archived: true — самый явный сигнал: репозиторий официально закрыт автором, дальше обновлений не будет никогда, вопрос закрыт сразу. Дата pushed_at старше 12–18 месяцев при активно используемом компоненте — повод присмотреться внимательнее, даже если явного архивирования нет: проект может быть просто «в спячке», а может быть заброшен без формального объявления.

Полезные проверочные точки, которые стоит смотреть глазами, а не скриптом:

  • Открытые issues и PR — если счётчик открытых issues растёт годами без единого ответа мейнтейнера, а последние мерджи датированы позапрошлым годом — это красноречивее даты последнего коммита.
  • Файл SECURITY.md в репозитории — если он есть и описывает поддерживаемые версии, там прямо сказано, какие ветки ещё получают патчи безопасности.
  • Страница проекта на deps.dev или на Libraries.io — оба сервиса агрегируют метаданные экосистемы и иногда напрямую помечают проект как «unmaintained» на основе активности.
  • Форки с активными коммитами — если оригинальный репозиторий мёртв, но у одного из форков идёт активная разработка, это кандидат на миграцию, а не повод сразу списывать компонент.

Для Docker-образов дополнительно стоит смотреть страницу на Docker Hub или GHCR — там видна дата последнего пуша тега, и если latest не обновлялся два года, а тегов с версиями вообще нет, это уже сигнал сам по себе, независимо от активности апстрим-репозитория на GitHub.

Как искать открытые уязвимости

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

Для сканирования Docker-образов на CVE практично использовать Trivy — он не требует запуска демона и разбирает образ локально:

trivy image --severity HIGH,CRITICAL nginx:1.24-alpine

Вывод покажет CVE с указанием, есть ли уже исправленная версия пакета внутри образа (Fixed Version) — пустое поле в этой колонке при высокой критичности означает, что патча ещё нет вообще, независимо от того, жив автор или нет. Это отдельная, более срочная категория: не «заброшено», а «уязвимо прямо сейчас».

Для зависимостей языковых экосистем есть отдельные проверенные пути:

# Python
pip install pip-audit && pip-audit -r requirements.txt

# Node.js
npm audit --omit=dev

# Go
govulncheck ./...

Полезно свериться и с базой NVD (nvd.nist.gov) или с GitHub Security Advisories по конкретному пакету напрямую — иногда сканер локальной версии отстаёт от базы на несколько дней, и ручная проверка по имени компонента подтверждает или снимает подозрение.

Отдельно стоит подчеркнуть: заброшенность проекта и наличие открытой CVE — это два разных признака, которые не всегда совпадают. Активно поддерживаемый проект может иметь свежую неисправленную уязвimость просто потому, что патч ещё не готов — это нормальный жизненный цикл, и часто нужно просто подождать несколько дней. А заброшенный проект без единой известной CVE в базах — это не значит, что он безопасен: скорее всего, его просто никто не сканировал и не искал в нём дыры. Отсутствие записи в NVD для мёртвого нишевого проекта ни о чём не говорит, кроме того, что до него не дошли руки исследователей.

Матрица решений: что делать с находкой

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

Статус проектаКритичный компонент (публичный интерфейс, обработка входных данных, аутентификация)Некритичный компонент (внутренний тул, вспомогательный скрипт)
Активен, есть открытая CVE без патчаВременный компенсирующий контроль (WAF-правило, отключение уязвимой функции) + план обновления сразу после патчаМониторинг статуса патча, обновление в штатном порядке
Заброшен (архив/нет коммитов 18+ мес.), CVE нетПлан миграции на поддерживаемую альтернативу в горизонте 1–2 кварталовОсознанное решение оставить + запись в реестр рисков
Заброшен и есть открытая CVEМиграция в приоритете, до неё — изоляция (отдельная сеть, минимальные права, WAF)Миграция или изоляция в течение квартала
Просто давно не обновлялся, но автор активен в issuesНе трогать, это не проблема сама по себеНе трогать

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

План миграции, когда компонент критичен

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

  1. Зафиксируйте причину и срок. Впишите в реестр рисков: что за компонент, почему опасен, кто владелец задачи, к какой дате должна быть готова замена. Без даты задача тонет среди текущих приоритетов.
  2. Найдите альтернативу заранее, не в момент инцидента. Посмотрите на активные форки, на функциональные аналоги в той же экосистеме, сравните API — совместимая замена мигрируется за часы, несовместимая может растянуться на недели тестирования.
  3. Разверните замену параллельно, не вместо. Поднимите новый сервис рядом со старым, переключите часть трафика или прогоните staging-нагрузку, прежде чем выключать оригинал. Это тот же принцип, что и в безопасном деплое без простоя — риск миграции снижается за счёт постепенности, а не решительности.
  4. Держите план отката до полного переключения. Пока новая версия не отработала под реальной нагрузкой хотя бы неделю-две, старая должна оставаться доступной для быстрого возврата.
  5. Закройте задачу явно. После переключения — удалите старый образ/пакет из окружения физически, а не просто «перестаньте использовать»: висящий неиспользуемый, но установленный компонент с открытой CVE — это тоже риск, просто менее заметный.

Если компонент нельзя заменить прямо сейчас (нет альтернатив, слишком глубокая интеграция, миграция стоит дороже риска) — временная изоляция снижает ущерб: минимальные сетевые права, отдельный под/контейнер без доступа во внутреннюю сеть, WAF перед публичным интерфейсом, урезанные системные привилегии. Это не решает проблему, но покупает время без прямого риска для остальной инфраструктуры.

Как встроить аудит в регламент, а не делать разово

Разовый аудит зависимостей полезен, но его ценность быстро выцветает — через полгода список снова устареет. Практический способ закрепить это как процесс:

  • Календарь, а не память. Заведите повторяющуюся задачу раз в квартал (первая неделя квартала — удобная точка привязки) и держите её отдельно от текущих релизных задач, чтобы она не терялась среди спринтов.
  • Чек-лист, а не «посмотреть на глаз». Пройдитесь по всем пунктам из разделов выше — список компонентов, дата последнего коммита/релиза, CVE-сканирование, обновление реестра рисков — в одном и том же порядке каждый раз.
  • Автоматизация там, где это возможно. Renovate или Dependabot закрывают часть рутины — автоматически открывают PR на обновление версий, но не заменяют человеческую проверку «а жив ли автор вообще»: бот обновит версию до последней доступной, даже если последняя доступная вышла три года назад.
  • Владелец задачи. У аудита должен быть конкретный ответственный — если задача «общая», её обычно не делает никто.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Чем аудит зависимостей отличается от обычного сканирования уязвимостей?

Сканирование уязвимостей (Trivy, pip-audit, Lynis) отвечает на вопрос «что уязвимо прямо сейчас». Аудит зависимостей на активность — на вопрос «что перестанет получать патчи в будущем», даже если сегодня открытых CVE ещё нет. Это дополняющие, а не взаимозаменяющие проверки — стоит делать обе.

Что считать «заброшенным» — конкретный срок в месяцах?

Универсального порога нет. Ориентир — 12–18 месяцев без коммитов при активно используемом компоненте, плюс явные признаки: закрытый архив на GitHub, незакрытые issues без ответов годами, отсутствие раздела SECURITY.md. Для нишевых стабильных утилит (например, консольный инструмент, который просто делает одну вещь и не нуждается в новом функционале) долгое отсутствие коммитов само по себе не всегда означает заброшенность.

Нужно ли аудировать зависимости, если сервер не смотрит в интернет напрямую?

Да, но приоритет ниже. Внутренний сервис за VPN или в приватной сети — не защищён навсегда: атака может прийти через скомпрометированного сотрудника, через соседний контейнер в той же сети, через цепочку поставки самого пакета. Критичность снижает срочность миграции, но не отменяет необходимость знать, что происходит со стеком.

Что делать, если альтернативы заброшенному компоненту просто нет?

Такое бывает с узкоспециализированными библиотеками. В этом случае реалистичный путь — либо взять поддержку на себя (форкнуть и патчить самостоятельно то немногое, что действительно нужно), либо изолировать компонент максимально жёстко и держать под усиленным мониторингом именно этой части системы.

Стоит ли автоматизировать проверку даты последнего коммита через CI?

Да, если компонентов много — скрипт, который раз в квартал проходит по списку репозиториев из реестра и дёргает GitHub API на pushed_at/archived, экономит время. Но финальное решение — мигрировать или оставить — всё равно требует человека: automation хорошо находит кандидатов, но плохо взвешивает критичность и цену миграции.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →