Когда вы добавляете в проект одну библиотеку, вы неявно подключаете десятки или сотни других — транзитивных зависимостей. Они не указаны в вашем package.json, pom.xml, requirements.txt или go.mod, но их код выполняется в вашем приложении. Проблема не в их количестве как таковом, а в том, что вы не управляете их выбором, версиями, качеством и безопасностью. Эта статья разбирает, откуда берутся скрытые зависимости, какие именно риски они несут и как построить процесс, который не превратит сборку в «чёрный ящик».
- Что такое скрытые (транзитивные) зависимости
- Почему это становится проблемой: четыре вектора риска
- 1. Безопасность: площадь атаки растёт нелинейно
- 2. Лицензионные риски: несовместимость и копилефт
- 3. Технический долг и непредсказуемое поведение
- 4. Операционная сложность: инцидент-респонс и аудит
- Как обнаружить и оценить скрытые зависимости
- Построение полного графа зависимостей
- Анализ достижимости (Reachability Analysis)
- Мониторинг изменений графа во времени
- Практики снижения рисков
- 1. Минимизация прямых зависимостей
- 2. Жёсткое закрепление версий (Lockfiles и Pinning)
- 3. Политики допуска зависимостей (Allow/Deny Lists)
- 4. Изоляция и песочницы для непроверенного кода
- 5. Подпись и верификация артефактов (Sigstore/cosign, SLSA)
- Сценарии принятия решений: что делать, когда…
- Инструментарий: от бесплатного к enterprise
- Типичные ошибки и как их избежать
- Чек-лист готовности к продакшну (Dependency Readiness)
- От чего зависит итоговый риск и как его измерить
- С чего начать прямо сейчас
Что такое скрытые (транзитивные) зависимости
Прямая зависимость — это пакет, который вы явно объявили в файле манифеста проекта. Транзитивная (скрытая) зависимость — это пакет, который тянется автоматически, потому что он нужен вашей прямой зависимости, или зависимости вашей зависимости, и так далее по графу.
Менеджеры пакетов (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)
Перед релизом убедитесь, что выполнены пункты:
- SBOM (CycloneDX/SPDX) сгенерирован, подписан (cosign), загружен в artefact registry и привязан к тегу релиза.
- Lock-файл закоммичен и соответствует манифесту (CI проверяет npm ci / pip install —require-hashes / go mod verify).
- Полный граф зависимостей просканирован на уязвимости (Grype/Trivy/Dependency-Track) — критические (CVSS ≥ 9.0) и высокие (CVSS ≥ 7.0) с reachability=true — нулевые или имеют утверждённое исключение с TTL.
- Лицензии всех компонентов (включая транзитивные) проверены — нет несовместимых с политикой компании (GPL/AGPL/SSPL без согласования).
- Настроен diff SBOM между текущим и предыдущим релизом — неожиданных изменений нет.
- Прямые зависимости, тянущие > N транзитивных (N по соглашению команды, например 100), имеют обоснование в ARCHITECTURE.md или ADR.
- Для критических путей (криптография, аутентификация, парсинг внешних данных) достижимость уязвимостей подтверждена нулевой или есть план миграции.
- Базовый образ контейнера закреплён по sha256, сканирован Trivy/Grype, минимален (distroless / alpine / scratch / chainguard).
- Документирован процесс реагирования на инцидент в цепочке поставок: кто получает алерт, как роллбекается версия, как коммуницируется с заинтересованными сторонами.
От чего зависит итоговый риск и как его измерить
Риск не равен количеству транзитивных зависимостей. Он функция от:
- Глубины графа — чем глубже, тем сложнее аудит и дольше путь фикса.
- Концентрации — если 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.
