TL;DR

Волны атак Shai-Hulud в экосистеме NPM показали, что сканирование зависимостей и SBOM не гарантируют обнаружение компрометации: вредонос может сработать ещё на этапе установки, вне релиза и вне «правильных» веток.

На тестовом проекте Trivy и Dependency-Track в конфигурации по умолчанию не нашли проблем, Grype нашёл, но дал много ложных срабатываний, а OSV-Scanner обнаружил компрометации, но без привычных оценок критичности и «исправленных» версий. Главная причина расхождений — источники данных: многие заражённые пакеты не имеют CVE, а уведомления GitHub о вредоносном ПО часто не попадают в стандартные потоки и API; OSV агрегирует их лучше.

Вывод: надёжность таких проверок упирается в фиды и настройки, а не в факт «у нас есть сканер», поэтому цепочку поставок нужно защищать несколькими слоями и регулярно проверять, что именно ваши инструменты реально видят.

2025 год стал тем самым моментом, когда безопасность цепочки поставок ПО перестала быть теоретическим риском и превратилась в практический кошмар для всех, кто работает с файлом package.json. Недавние волны атак на экосистему NPM показали это во всей красе: доверенные библиотеки превратились в векторы атаки и нарушили безопасность пайплайнов ещё до того, как код вообще добрался до продакшена.

Сначала компрометация нескольких популярных NPM-пакетов, включая chalk и debug, показала, насколько легко один фишинговый инцидент с одним разработчиком может иметь масштабные последствия. Всего через неделю первая волна Shai-Hulud принесла самореплицирующегося «червя» и массовую кражу учётных данных с машин разработчиков. Совсем недавно Shai-Hulud 2 (он же Sha1-Hulud) довёл эту стратегию до предела и добавил коварную особенность — удаление пользовательских файлов в случае попытки нейтрализации.

Как командам разработки и ИБ справляться с такими ситуациями и выявлять компрометации? Во время обсуждения этого вопроса обычно всплывают Software Composition Analysis (SCA) через сканирование зависимостей и/или Software Bill of Materials (SBOM). Когда вы знаете все свои зависимости, должно быть несложно автоматически выявлять скомпрометированные и дальше запускать процесс снижения рисков.

Однако в случае Shai-Hulud у этого подхода есть врождённые ограничения:

  • Вредоносная нагрузка не обязана попадать в релиз, она выполняется в процессе сборки. Это означает, что риск несёт любой запуск — включая CI-пайплайны для любой ветки. При этом SCA часто запускают только на основной ветке или на релизных артефактах.

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

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

Оценка инструментов сканирования зависимостей

Когда я запустил Trivy на проекте, предположительно затронутом атакой, меня удивило, что инструмент не смог выявить никаких проблем. Тогда я устроил опрос в Mastodon, чтобы понять, не упускаю ли я чего-то:

shaihulud-poll-1.png

Из 50 участников большинство (68 %) разделили мои ожидания.

Это заставило нас копнуть глубже и изучить, как ведут себя самые популярные open-source инструменты для Software Composition Analysis (SCA) и сканирования зависимостей. Помимо Trivy от Aqua Security, сюда входят Grype от Anchore, OSV-Scanner от Google и OWASP Dependency-Track. Из-за ограниченной доступности мы не анализировали коммерческие инструменты вроде Snyk, Aikido или GitLab Ultimate Dependency Scanning.

Для этого сравнения я собрал демонстрационный проект с скомпрометированными версиями ansi-regex и kill-port:

{
  "dependencies": {
    "ansi-regex": "6.2.1",
    "kill-port": "2.0.2"
  }
}

Пакет ansi-regex попал под первую крупную волну компрометаций NPM-пакетов в сентябре 2025 года, а kill-port был скомпрометирован в рамках Shai-Hulud 2.

Поскольку эти версии быстро удалили из NPM (и, разумеется, они содержат вредоносный код), мы не устанавливали их на самом деле, а подготовили фиктивные файлы package.json и package-lock.json. Мы убедились, что все сканеры подхватывают эти «поддельные» версии из файлов метаданных.

Если не указано иначе, использовались конфигурации по умолчанию для всех инструментов. Все тесты проводились с актуальной информацией об уязвимостях по состоянию на 2025-12-03.

Trivy

Как уже упоминалось выше, Trivy не выявил никаких проблем. Это было верно и при сканировании проекта напрямую (trivy fs), и при сканировании заранее сгенерированного SBOM (trivy sbom):

Report Summary

┌───────────────────┬──────┬─────────────────┐
│      Target       │ Type │ Vulnerabilities │
├───────────────────┼──────┼─────────────────┤
│ package-lock.json │ npm  │        0        │
└───────────────────┴──────┴─────────────────┘

Grype

При запуске на SBOM, сгенерированном Trivy (grype bom.json), Grype действительно нашёл обе уязвимости и пометил их как Critical:

NAME        INSTALLED  TYPE  VULNERABILITY        SEVERITY  EPSS  RISK
ansi-regex  6.2.1      npm   GHSA-jvhh-2m83-6w29  Critical  N/A   N/A
kill-port   2.0.2      npm   GHSA-3j2r-p9f6-rw66  Critical  N/A   N/A

При запуске напрямую на папке проекта (grype .) он тоже выявил эти проблемы — но вместе с ними выдал и множество ложных срабатываний:

NAME                   INSTALLED  FIXED IN  TYPE  VULNERABILITY        SEVERITY  EPSS           RISK
json5                  1.0.1      1.0.2     npm   GHSA-9c47-m6qq-7p4h  High      37.3% (97th)   27.2
json5                  2.2.1      2.2.2     npm   GHSA-9c47-m6qq-7p4h  High      37.3% (97th)   27.2
trim-newlines          1.0.0      3.0.1     npm   GHSA-7p7h-4mm5-852v  High      1.3% (78th)    0.9

# [...]

ansi-regex             6.2.1                npm   GHSA-jvhh-2m83-6w29  Critical  N/A            N/A
kill-port              2.0.2                npm   GHSA-3j2r-p9f6-rw66  Critical  N/A            N/A

Обратите внимание на алерты уровня High для json5 и trim-newlines. Они (и многие другие, которые я опустил) появились потому, что Grype спустился в каталог node_modules, прочитал метаданные package.json всех модулей и ошибочно посчитал их devDependencies частью нашего проекта. Вероятно, это можно исправить настройками, но мы оставили конфигурацию по умолчанию и не стали с этим возиться.

OSV-Scanner

OSV-Scanner отработал (почти) идеально: он выявил проблемы и при сканировании непосредственно каталога проекта, и при проверке SBOM от Trivy. Однако он не смог дать информацию о степени критичности или о версиях, в которых проблема исправлена:

╭─────────────────────────────────┬──────┬───────────┬────────────┬─────────┬───────────────┬──────────╮
│ OSV URL                         │ CVSS │ ECOSYSTEM │ PACKAGE    │ VERSION │ FIXED VERSION │ SOURCE   │
├─────────────────────────────────┼──────┼───────────┼────────────┼─────────┼───────────────┼──────────┤
│ https://osv.dev/MAL-2025-46966  │      │ npm       │ ansi-regex │ 6.2.1   │ --            │ bom.json │
│ https://osv.dev/MAL-2025-191116 │      │ npm       │ kill-port  │ 2.0.2   │ --            │ bom.json │
╰─────────────────────────────────┴──────┴───────────┴────────────┴─────────┴───────────────┴──────────╯

OWASP Dependency-Track

Наконец, посмотрим на OWASP Dependency-Track. В отличие от других инструментов, его не запускают из командной строки — он работает как веб-приложение. Кроме того, он не умеет сам выполнять анализ состава компонентов и всегда требует на вход уже готовый SBOM. Для этой цели я снова использовал файл SBOM от Trivy.

В конфигурации по умолчанию Dependency-Track не смог обнаружить никаких уязвимостей:

shaihulud-dependencytrack-1.png

Однако Dependency-Track позволяет настраивать источники данных, из которых он получает сведения об уязвимостях. Помимо встроенного фида NVD CVE, из публично доступных дополнительных вариантов есть GitHub Advisories и Open Source Vulnerabilities (OSV — тот самый источник данных, который использует OSV-Scanner).

Включение GitHub Advisories ничего не изменило — уязвимости всё равно не обнаруживались. А вот источник OSV, хоть он и находится в статусе Beta, при включении действительно повлиял на результат:

shaihulud-dependencytrack-2.png

Изучаем источники данных

Какие источники информации об уязвимостях обычно используют инструменты сканирования зависимостей?

Самый известный — программа Common Vulnerabilities and Exposures (CVE) от NVD, то есть база CVE. Помимо постоянных проблем с качеством данных в целом, здесь есть крупный подвох в случае скомпрометированных NPM-пакетов: для (почти?) всех из них никто даже не удосужился завести CVE! Поэтому этот источник можно исключить как способ выявлять Shai-Hulud и подобные истории.

GitHub, который, кстати, ещё и управляет реестром пакетов NPM, ведёт собственную базу GitHub Security Advisories (GHSA). Каждый раз, когда GitHub удалял скомпрометированную версию пакета, он также публиковал соответствующий security advisory. Но это особый тип — уведомления о вредоносном ПО (Malware), — и поэтому они хорошо спрятаны: чтобы найти их, нужно добавить к поисковому запросу специальный фильтр type:malware.

Официальное объяснение GitHub звучит так:

Наши уведомления о вредоносном ПО (malware advisories) в основном относятся к атакам подмены (substitution attacks). При таком типе атаки злоумышленник публикует пакет в публичном реестре под тем же именем, что и зависимость, на которую пользователи рассчитывают в стороннем или приватном реестре, — в надежде, что будет подтянута вредоносная версия. [...] Пользователи, которые корректно ограничили область видимости своих зависимостей, не должны пострадать от вредоносного ПО.

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

По умолчанию malware-advisories не возвращаются через API GitHub Security Advisories — вероятно, именно поэтому OWASP Dependency-Track и Trivy (который тоже использует GHSA как источник данных для NPM-пакетов) их не подхватывают. Похоже, Grype устроен иначе и включает malware-advisories по умолчанию.

Третий крупный источник данных — проект Google Open Source Vulnerabilities (OSV). Хотя он «всего лишь» агрегирует информацию об уязвимостях из других источников, включая GHSA, он всё же включает malware-advisories GitHub в основной фид. Оттуда информация попадает в OSV-Scanner и (опционально) в OWASP Dependency-Track.

Кто-то может возразить, что заражения вредоносным ПО не должны быть частью баз уязвимостей: это ведь не эксплуатируемые уязвимости, а ситуации, когда компрометация уже произошла в цепочке поставок. Мы считаем это различие чисто теоретическим. В пользу этого говорит и то, что в CWE (Common Weakness Enumeration — системе категоризации из экосистемы CVE) действительно есть запись про самореплицирующийся вредоносный код.

В конечном счёте такие зависимости откровенно небезопасны. Разумеется, мы хотим узнавать о них из фида уязвимостей! Более того — риск здесь, скорее, выше, чем у пакета с уязвимостью, которая ещё даже не была использована.

Защита конечных устройств спешит на помощь?

С учётом ограничений, о которых говорилось в начале, и того, что сканеры зависимостей работают, мягко говоря, непредсказуемо, не стоит ли перейти к другой линии обороны? В конце концов, в волнах Shai-Hulud значительная часть риска связана с тем, что вредонос устанавливается на машины разработчиков. Может ли решение для защиты конечных устройств (антивирус/EDR) выявить его и предотвратить дальнейший ущерб?

Чтобы проверить эту идею, я провёл ещё один опрос в Mastodon — и большинство снова подтвердило её:

shaihulud-poll-2.png

Поскольку решения для защиты конечных устройств печально известны тем, что их сложно тестировать, мы посмотрели результаты VirusTotal для известных полезных нагрузок Shai-Hulud 2. VirusTotal в первую очередь проверяет статические сигнатуры, тогда как продвинутый EDR в идеале должен ловить вредоносное поведение. Однако если сигнатура файла даже не помечается как подозрительная, то первая линия обороны уже провалилась. В конце концов, в данном случае обнаружение по сути сводится к тому, чтобы находить файлы по нескольким известным контрольным суммам.

Вот каким был результат для одного из вредоносных файлов по состоянию на 2025-11-26 — через два дня после первого обнаружения:

shaihulud-virustotal-1.png

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

С другой вредоносной полезной нагрузкой на 2025-11-26 всё выглядело ещё хуже:

shaihulud-virustotal-2.png

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

Вывод

Хотя GitHub, как оператор NPM, оперативно удалял версии пакетов, затронутые масштабными компрометациями (по крайней мере в большинстве случаев), то, как распространяется информация об этих инцидентах, явно можно улучшать — начиная с пометок на страницах пакетов в NPM и заканчивая выпуском бюллетеней безопасности нужного типа. Из-за этого использование сканеров зависимостей оказывается менее надёжным, чем могло бы быть, а обнаружение становится слишком зависимым от конкретных источников данных.

Нас удивило, сколько сканеров не смогли выявить компрометации — ни на уровне анализа состава компонентов, ни на уровне защиты конечных устройств. Да, «правильные» сканеры обычно находят проблемы или хотя бы могут быть настроены так, чтобы находить, но это не лишено подводных камней, и далеко не каждый продукт даёт результат, который можно назвать удовлетворительным. Это особенно тревожно, учитывая масштаб последствий и широкий резонанс вокруг недавних компрометаций в NPM. Остаётся только гадать, что будет в случае более тонких атак, которые не получат такого внимания.


Истории вроде Shai-Hulud показывают: безопасность цепочки поставок — это часть архитектуры, а не отчёт сканера. На курсе Software Architect разбирается проектирование отказоустойчивых и масштабируемых систем: DDD/CQRS, event-driven, API-first и наблюдаемость (метрики, логи, алерты) — через моделирование и диаграммы. Пройдите входной тест по курсу, чтобы оценить свои знания и навыки.

Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:

  • 26 января в 20:00. «Готовые решения и лучшие практики для надёжной защиты API в архитектуре бэкенда». Записаться

  • 4 февраля в 20:00. «Apache Camel в архитектуре решений бэкэнда». Записаться

  • 18 февраля в 20:00. «Паттерны декомпозиции сервисов». Записаться

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