Скрытые зависимости в пакетах: почему они опасны и как контролировать риски

Когда вы добавляете в проект одну библиотеку, вы неявно подключаете десятки или сотни других — транзитивных зависимостей. Они не указаны в вашем package.json, pom.xml, requirements.txt или go.mod, но их код выполняется в вашем приложении. Проблема не в их количестве как таковом, а в том, что вы не управляете их выбором, версиями, качеством и безопасностью. Эта статья разбирает, откуда берутся скрытые зависимости, какие именно риски они несут и как построить процесс, который не превратит сборку в «чёрный ящик».

Содержание
  1. Что такое скрытые (транзитивные) зависимости
  2. Почему это становится проблемой: четыре вектора риска
  3. 1. Безопасность: площадь атаки растёт нелинейно
  4. 2. Лицензионные риски: несовместимость и копилефт
  5. 3. Технический долг и непредсказуемое поведение
  6. 4. Операционная сложность: инцидент-респонс и аудит
  7. Как обнаружить и оценить скрытые зависимости
  8. Построение полного графа зависимостей
  9. Анализ достижимости (Reachability Analysis)
  10. Мониторинг изменений графа во времени
  11. Практики снижения рисков
  12. 1. Минимизация прямых зависимостей
  13. 2. Жёсткое закрепление версий (Lockfiles и Pinning)
  14. 3. Политики допуска зависимостей (Allow/Deny Lists)
  15. 4. Изоляция и песочницы для непроверенного кода
  16. 5. Подпись и верификация артефактов (Sigstore/cosign, SLSA)
  17. Сценарии принятия решений: что делать, когда…
  18. Инструментарий: от бесплатного к enterprise
  19. Типичные ошибки и как их избежать
  20. Чек-лист готовности к продакшну (Dependency Readiness)
  21. От чего зависит итоговый риск и как его измерить
  22. С чего начать прямо сейчас

Что такое скрытые (транзитивные) зависимости

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

Менеджеры пакетов (npm, Maven, pip, Cargo, Go modules, Composer и др.) разрешают граф зависимостей и скачивают всё необходимое для сборки. Разработчик видит только вершину айсберга. Типичный npm-пакет может иметь 5–10 прямых зависимостей, но подтягивать 500–2000 транзитивных. В экосистемах Java и .NET графи часто глубже, а в Go — шире за счёт статической линковки.

Скрытые зависимости делятся на несколько категорий по происхождению:

  • Обязательные транзитивные — библиотеки, без которых прямая зависимость не работает (например, парсер JSON внутри HTTP-клиента).
  • Опциональные и peer-зависимости — пакеты, которые прямая зависимость ожидает найти в окружении, но не требует жестко (например, драйверы БД для ORM).
  • Dev-зависимости, утекающие в продакшн — инструменты сборки, линтеры, тестовые фреймворки, которые из-за ошибок конфигурации попадают в итоговый артефакт.
  • Платформенные и системные — нативные библиотеки (OpenSSL, glibc, zlib), которые не управляются менеджером пакетов языка, но влияют на поведение приложения.

Почему это становится проблемой: четыре вектора риска

1. Безопасность: площадь атаки растёт нелинейно

Каждая транзитивная зависимость — это потенциальная уязвимость. Вы не выбирали эти пакеты, не аудировали их код, не следите за их релизами. Если в глубине графа обнаруживается CVE (например, CVE-2021-44228 в Log4j или CVE-2022-22965 в Spring Core), вы узнаёте об этом постфактум — через сканеры, блоги или инцидент.

Опасность усиливается тремя факторами:

  • Отсутствие обновлений. Глубинные зависимости часто обновляются редко. Их мейнтейнеры могут забросить проект, а прямая зависимость не обновляет требования к версиям летями.
  • Конфликты версий и «алмазная» зависимость. Две ваши прямые зависимости могут требовать разные версии одной и той же транзитивной библиотеки. Менеджер пакетов разрешит конфликт (обычно выберет старшую совместимую), но вы не контролируете, какая версия окажется в рантайме.
  • Атаки на цепочку поставок (Supply Chain Attacks). Злоумышленники компрометируют популярный пакет-«лист» графа (как в случае event-stream 2018 года или ua-parser-js 2021 года), и вредоносный код попадает во все проекты, зависящие от него транзитивно.

2. Лицензионные риски: несовместимость и копилефт

Вы можете тщательно проверять лицензии прямых зависимостей (MIT, Apache-2.0, BSD), но транзитивная зависимость может оказаться под GPL-3.0, AGPL или коммерческой лицензией с ограничениями. Это создаёт юридические риски при распространении продукта:

  • Копилефт-лицензии (GPL, AGPL) могут требовать открыть исходный код всего приложения.
  • Некоторые лицензии запрещают коммерческое использование или требуют атрибуции в конкретном виде.
  • Лицензии могут меняться между минорными версиями — мейнтейнер имеет право перелицензировать свой код.

Ручное отслеживание лицензий в графе из сотен пакетов невозможно. Нужен автоматизированный аудит на каждом этапе CI/CD.

3. Технический долг и непредсказуемое поведение

Скрытые зависимости вносят неявные контракты:

  • Глобальное состояние. Библиотеки могут регистрировать обработчики сигналов, менять локаль, настраивать пулы потоков, перехватывать исключения — и конфликтовать друг с другом.
  • Размер артефакта и время запуска. В статически линкуемых языках (Go, Rust) или при бандлинге (Webpack, esbuild) каждый лишний килобайт кода увеличивает бинарь и время холодного старта.
  • Недетерминизм сборки. Если lock-файл не зафиксирован или повреждён, одна и та же конфигурация может дать разный набор транзитивных зависимостей на разных машинах или в разных контейнерах.

4. Операционная сложность: инцидент-респонс и аудит

Когда обнаруживается уязвимость в транзитивной зависимости, команда сталкивается с вопросами:

  • Где именно она используется — в продакшн-коде, в тестах, в инструменте сборки?
  • Есть ли прямой путь от нашего кода к уязвимой функции (call graph reachability)?
  • Можно ли обновить прямую зависимость, чтобы подтянуть исправленную версию транзитивной, не сломав API?
  • Если прямой зависимости нет фикса — форкать, патчить или мигрировать на альтернативу?

Без инструментов, строящих полный граф зависимостей с достижимостью (reachability analysis), ответы требуют дней ручной работы.

Как обнаружить и оценить скрытые зависимости

Построение полного графа зависимостей

Базовый шаг — генерация SBOM (Software Bill of Materials) в стандартном формате: SPDX, CycloneDX или SWID. SBOM содержит полный список компонентов с версиями, лицензиями, хешами и связями. Современные инструменты делают это на этапе сборки:

  • npm/yarn/pnpm: npm sbom, yarn sbom, pnpm sbom (CycloneDX/SPDX).
  • Maven/Gradle: плагины cyclonedx-maven-plugin, cyclonedx-gradle-plugin.
  • pip/poetry/pipenv: cyclonedx-bom, pip-licenses.
  • Go: go sbom (начиная с Go 1.22), cyclonedx-gomod.
  • Cargo: cargo cyclonedx, cargo sbom.
  • Универсальные: Syft (Anchore), Trivy (Aqua), Microsoft SBOM Tool.

SBOM нужно хранить как артефакт сборки (artifact) и версионировать вместе с релизом. Это базовая гигиена для любого продакшн-проекта.

Анализ достижимости (Reachability Analysis)

Наличие уязвимого пакета в SBOM не значит, что уязвимость эксплуатируема. Reachability-анализ строит граф вызовов (call graph) от вашего кода к функциям транзитивных зависимостей. Если уязвимый код недостижим — риск ниже, но не нулевой (рефакторинг или конфигурация могут включить этот путь в будущем).

Инструменты с reachability:

  • OWASP Dependency-Track — платформа для управления SBOM и уязвимостями с reachability.
  • Endor Labs, Socket, Snyk, Mend, JFrog Xray — коммерческие решения с глубоким статическим анализом.
  • Open Source: govulncheck (Go), cargo-audit с флагами, pip-audit — базовая проверка без полного reachability.

Мониторинг изменений графа во времени

Зафиксируйте базовый SBOM релиза. Настройте сравнение (diff) SBOM между сборками в CI: новые пакеты, удалённые, смены версий, смены лицензий. Любое неожиданное изменение — триггер для ревью. Это ловит:

  • Скрытое добавление тяжёлых зависимостей через peerDependencies или опциональные.
  • Подмену пакета на вредоносный (typosquatting, dependency confusion).
  • Дрейф версий из-за некорректных диапазонов в lock-файле.

Практики снижения рисков

1. Минимизация прямых зависимостей

Каждая прямая зависимость — это вход в подграф транзитивных. Прежде чем добавить пакет, задайте вопросы:

  • Можно ли реализовать нужную функциональность 20–50 строками своего кода? (Утилиты для работы с датами, строки, мелкие хелперы часто не стоят зависимости.)
  • Есть ли альтернатива с меньшим графом зависимостей? (Сравните lodash vs нативные методы массивов, moment.js vs date-fns / dayjs / Temporal API.)
  • Активно ли поддерживается пакет? Есть ли релизы за последние 12 месяцев? Отвечают ли на issues/PR?
  • Есть ли у пакета флаг sideEffects: false (для tree-shaking) и правильные exports / main / module поля?

2. Жёсткое закрепление версий (Lockfiles и Pinning)

Lock-файл (package-lock.json, yarn.lock, Pipfile.lock, go.sum, Cargo.lock) — единственный источник истины о версиях транзитивных зависимостей. Правила:

  • Lock-файл всегда коммитится в репозиторий (кроме библиотек, публикуемых в реестр — там он может мешать потребителям).
  • CI должен падать, если lock-файл не синхронизирован с манифестом (npm ci, pip install —require-hashes, go mod verify).
  • Используйте npm ci / yarn install —frozen-lockfile / pip install —require-hashes — они гарантируют воспроизводимость.
  • Для контейнеров: закрепляйте базовые образы по хешу (FROM ubuntu@sha256:…), а не по тегу.

3. Политики допуска зависимостей (Allow/Deny Lists)

Внедрите политики на уровне CI, которые блокируют сборку при:

  • Появлении пакетов с лицензиями из запрещённого списка (GPL, AGPL, SSPL, proprietary без согласования).
  • Появлении пакетов с критическими уязвимостями (CVSS ≥ 7.0), для которых нет исключения с обоснованием и сроком действия.
  • Превышении порога транзитивных зависимостей на прямую зависимость (например, > 200 — требует ревью архитектора).
  • Наличии пакетов из недоверенных реестров или без подписи (sigstore/cosign).

Инструменты для политик: OPA/Gatekeeper, Kyverno, встроенные политики Dependency-Track, Snyk, Mend, или самописные скрипты над SBOM.

4. Изоляция и песочницы для непроверенного кода

Если вы обязаны использовать пакет с рискованным графом (например, легаси-интеграция или специфический драйвер), изолируйте его:

  • Вынесите в отдельный микросервис / sidecar с минимальными правами (least privilege), собственным сетевым пространством и без доступа к секретам основного приложения.
  • Используйте WebAssembly (Wasm) для запуска непроверенных зависимостей в песочнице (Wasmtime, Wasmer, Spin) — это даёт capability-based security.
  • Для Python/Node — запускайте в отдельном процессе с seccomp профилем, gVisor или Kata Containers.

5. Подпись и верификация артефактов (Sigstore/cosign, SLSA)

Убедитесь, что пакет, который вы скачиваете, — именно тот, что опубликовал мейнтейнер. SLSA (Supply Chain Levels for Software Artifacts) Level 1+ требует:

  • Сборку в изолированном окружении (GitHub Actions, GitLab CI с ephemeral runners).
  • Генерацию provenance (доказательства происхождения) в формате in-toto.
  • Подпись артефакта и provenance ключом, привязанным к идентичности мейнтейнера (OIDC токен CI).
  • Верификацию подписи потребителем перед установкой (cosign verify, npm audit signatures, pip install —require-hashes с проверкой attestations).

Это защищает от компрометации реестра (npmjs, PyPI, Maven Central) и атак dependency confusion.

Сценарии принятия решений: что делать, когда…

Ситуация Действие Критерии выхода из ситуации
В транзитивной зависимости найдена критическая CVE, прямой зависимости нет фикса 1. Проверить reachability — достижим ли уязвимый код.
2. Если да — временный патч через pnpm overrides / npm overrides / resolutions / dependencyOverrides (Maven) / replace (Go).
3. Завести issue в прямую зависимость с просьбой обновить транзитивную.
4. Если мейнтейнер не отвечает — планировать миграцию на альтернативу.
Патч применён, тесты проходят, есть план миграции с дедлайном.
Транзитивная зависимость сменила лицензию на несовместимую (GPL/AGPL) 1. Зафиксировать версию до смены лицензии через lock-файл / overrides.
2. Проанализировать: используется ли функционал, требующий эту зависимость.
3. Искать альтернативу прямой зависимости с чистым графом.
4. Если альтернативы нет — согласовать исключение с юридическим департаментом, задокументировать риск.
Исключение одобрено юристами, есть план ухода, мониторинг новых версий включён.
Прямая зависимость тянет 500+ транзитивных пакетов для простой функции 1. Оценить: можно ли заменить на легковесный аналог или свой код.
2. Проверить: не дублирует ли функционал уже имеющиеся в проекте зависимости.
3. Если замена сложна — изолировать в отдельный сервис/процесс.
Граф сокращён на ≥ 50% или изоляция внедрена.
Сборка стала недетерминированной (разные результаты на разных машинах) 1. Проверить lock-файл на коммит.
2. Проверить версии менеджера пакетов и рантайма (Node, Python, Go) — зафиксировать в Dockerfile / .tool-versions / asdf.
3. Отключить автообновление lock-файла в CI.
4. Включить npm ci / pip install —require-hashes.
Две последовательные сборки в чистом окружении дают бит-в-бит идентичные артефакты (reproducible builds).
Подозрение на вредоносный код в новой версии транзитивной зависимости 1. Заморозить версию до последней проверенной.
2. Проверить diff пакета (npm diff, pnpm diff, GitHub compare).
3. Запустить статический анализ (Semgrep, CodeQL) на код пакета.
4. Сообщить в реестр (npm security, PyPI safety) и мейнтейнеру прямой зависимости.
Версия заморожена, отчёт отправлен, альтернатива найдена или патч применён.

Инструментарий: от бесплатного к enterprise

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

Уровень Задачи Инструменты (примеры)
Базовый (CI-local) Генерация SBOM, проверка известных CVE, проверка лицензий, lock-файл в репо Syft + Grype / Trivy, CycloneDX CLI, npm audit / pip-audit / cargo audit / govulncheck, license-checker, FOSSLight
Продвинутый (Policy-as-Code) Политики allow/deny в CI, reachability-анализ, diff SBOM между билдами, исключения с TTL OWASP Dependency-Track, OPA/Gatekeeper + Syft, Endor Labs (free tier), Socket.dev, StepSecurity
Enterprise (SBOM Management + VEX) Централизованное хранилище SBOM, VEX (Vulnerability Exploitability eXchange), интеграция с GRC, SLSA provenance, подпись артефактов JFrog Xray, Snyk, Mend, Anchore Enterprise, Chainguard, Microsoft Defender for Cloud, IBM Cloud Security

Начинайте с базового уровня: SBOM на каждом билде + Trivy/Gripe в CI + проверка лицензий. Это уже закрывает 80% рисков за минимальные усилия. Reachability и VEX добавляйте, когда объём ложных срабатываний станет большим для ручной триажи.

Типичные ошибки и как их избежать

  • «У нас есть npm audit — этого достаточно». npm audit проверяет только известные CVE в реестре npm, не делает reachability, не видит лицензионные проблемы, не работает с приватными реестрами и не строит SBOM. Используйте его как один из слоёв, а не единственный.
  • Lock-файл не в коммите «чтобы не создавать конфликты». Конфликты в lock-файле — это нормально и полезно: они заставляют команду осознанно решать, какая версия попадёт в продакшн. Решайте их через npm install / pnpm install после мержа, а не игнорируйте файл.
  • Игнорирование devDependencies в аудите. Компрометированный линтер или тестовый фреймворк может выполнить код на машине разработчика или в CI (supply chain attack). Аудитируйте весь граф, включая dev-зависимости, но разделяйте политики: для продакшн-артефакта — строже, для CI-окружения — с учётом изоляции.
  • Автоматическое обновление зависимостей (Dependabot, Renovate) без гейтов. Auto-merge patch-версий безопасен только при наличии тестов, reachability-проверки и подписи provenance. Minor/major обновления требуют ручного ревью изменений API и графа зависимостей.
  • Вера в «популярный пакет — значит безопасный». Популярность не защищает от компрометации аккаунта мейнтейнера, уязвимостей в глубинных зависимостях или намеренного вредоносного кода (protestware). Примеры: event-stream (2.6 млн загрузок/неделю), ua-parser-js (7.8 млн), colors/faker (протест 2022 года).

Чек-лист готовности к продакшну (Dependency Readiness)

Перед релизом убедитесь, что выполнены пункты:

  1. SBOM (CycloneDX/SPDX) сгенерирован, подписан (cosign), загружен в artefact registry и привязан к тегу релиза.
  2. Lock-файл закоммичен и соответствует манифесту (CI проверяет npm ci / pip install —require-hashes / go mod verify).
  3. Полный граф зависимостей просканирован на уязвимости (Grype/Trivy/Dependency-Track) — критические (CVSS ≥ 9.0) и высокие (CVSS ≥ 7.0) с reachability=true — нулевые или имеют утверждённое исключение с TTL.
  4. Лицензии всех компонентов (включая транзитивные) проверены — нет несовместимых с политикой компании (GPL/AGPL/SSPL без согласования).
  5. Настроен diff SBOM между текущим и предыдущим релизом — неожиданных изменений нет.
  6. Прямые зависимости, тянущие > N транзитивных (N по соглашению команды, например 100), имеют обоснование в ARCHITECTURE.md или ADR.
  7. Для критических путей (криптография, аутентификация, парсинг внешних данных) достижимость уязвимостей подтверждена нулевой или есть план миграции.
  8. Базовый образ контейнера закреплён по sha256, сканирован Trivy/Grype, минимален (distroless / alpine / scratch / chainguard).
  9. Документирован процесс реагирования на инцидент в цепочке поставок: кто получает алерт, как роллбекается версия, как коммуницируется с заинтересованными сторонами.

От чего зависит итоговый риск и как его измерить

Риск не равен количеству транзитивных зависимостей. Он функция от:

  • Глубины графа — чем глубже, тем сложнее аудит и дольше путь фикса.
  • Концентрации — если 80% графа дают 3 прямые зависимости, фокус усилий на них даёт максимальный эффект.
  • Возраста и активности мейнтейнеров — пакеты без релизов > 2 лет и без ответов в issues — зона риска.
  • Наличия reachability — недостижимая уязвимость ниже приоритета, но требует мониторинга.
  • Изоляции рантайма — изолированный микросервис с минимальными правами снижает blast radius.

Количественная метрика для дашборда: Weighted Risk Score = Σ (CVSS × ReachabilityFactor × ExposureFactor × AgeFactor) по всем транзитивным зависимостям с CVE. ReachabilityFactor: 1.0 — достижимо, 0.3 — потенциально, 0.0 — недостижимо. ExposureFactor: 1.0 — публичный API / обработка внешних данных, 0.5 — внутренний сервис, 0.1 — оффлайн-утилита. AgeFactor: 1.0 — пакет без обновлений > 2 лет, 0.5 — 1–2 года, 0.2 — < 1 года. Цель — снижать скор релиз за релизом.

С чего начать прямо сейчас

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

  • Если SBOM нет: добавьте генерацию CycloneDX в CI за 15 минут (Syft / Trivy / нативные команды менеджера пакетов). Сохраняйте как артефакт.
  • Если SBOM есть, но не читаете: подключите Grype/Trivy к SBOM в CI, настройте фейл на критических CVE с reachability (Dependency-Track или govulncheck/cargo-audit для своих экосистем).
  • Если сканер шумит: внедрите политики исключений с TTL (например, 30 дней) и обязательным обоснованием. Начните собирать reachability-данные.
  • Если процесс отлажен: переходите на SLSA Level 2+ — подписывайте provenance, верифицируйте потребителями, публикуйте VEX для известных недостижимых CVE.

Главный принцип: видимость превосходит незнание. Даже несовершенный SBOM, который вы регулярно смотрите, лучше идеального инструмента, который никто не настраивает. Начните с генерации списка компонентов на каждой сборке — это фундамент, на котором строятся все остальные меры.

Материал носит информационный характер и не заменяет профессионального аудита безопасности или юридической экспертизы. Выбор инструментов, порогов риска и политик исключений зависит от-threat model вашей организации, регуляторных требований (GDPR, PCI DSS, ФЗ-152 и др.) и критичечности продукта. При принятии решений, влияющих на безопасность продакшн-систем, привлекайте специалистов по AppSec и Supply Chain Security.

PEFile.ru