
Сканер для атаки на цепочку поставок keyv/cacheable: обнаруживает скомпрометированные npm-пакеты, проверяет хэши полезной нагрузки и находит импланты персистентности в режимах repo и host.
Английский | Português
Автономный сканер для инцидента в цепочке поставок keyv/cacheable («Shai-Hulud: Here We Go Again», 4 авг 2026) — 440+ npm-пакетов скомпрометировано самораспространяющимся червём, который крадёт облачные и CI-учётные данные и устанавливает персистентность с механизмом «мертвеца» (dead-man's switch).
Обнаруживает за минуты, без установки чего-либо:
package-lock.json, npm-shrinkwrap.json, yarn.lock (v1 и Berry), pnpm-lock.yaml и bun.lock — включая транзитивные зависимости, с полной цепочкой (например, eslint → file-entry-cache → flat-cache → [email protected]);node_modules (имя + SHA-256-хэш известных артефактов);SUSPECT, никогда не подтверждаются без хэша;.claude/settings.json и .vscode/tasks.json, временные артефакты (bun-dl-*);keyv/cacheable была скомпрометирована; атакующий опубликовал новые версии с хуком "preinstall": "node setup.mjs" — кодом, который выполняется до установки пакета, с правами того, кто запустил npm install.setup.mjs скачивает среду выполнения Bun из GitHub и запускает в ней полезную нагрузку — обход защиты, которая отслеживает только процессы node.Math_Symbol.js (~728 КБ, обфусцированный) крадёт учётные данные: метаданные инстансов AWS, ключи AWS/GCP/Azure, токены Vault, сервисные аккаунты Kubernetes, секреты GitHub Actions, npm-токены, плюс общий regex-поиск приватных ключей и bearer-токенов на диске.npm install/npm ci с включёнными скриптами жизненного цикла с 2026-08-04 09:35 UTC. С --ignore-scripts хук не выполнялся..claude/settings.json (SessionStart) и .vscode/tasks.json (folderOpen), которые запускают загрузчик при открытии клонированной папки — без npm install, ничего не устанавливается. Сюда входят те, кто клонировал репозиторий для расследования инцидента, и AI-агенты, открывшие директорию, — один из первых публичных случаев использования хуков AI-агентов (.claude/) как вектора атаки на цепочку поставок.Имплант устанавливает «watcher» (gh-token-monitor), который поддерживается в живых LaunchAgent (macOS) или пользовательским сервисом systemd + loginctl enable-linger (Linux). Каждые 60 секунд он проверяет украденный GitHub-токен через API. Пока токен работает, ничего не происходит. Когда ответ становится 4xx — то есть в момент отзыва токена — он выполняет через eval содержимое ~/.config/gh-token-monitor/handler: произвольную команду, заданную удалённо атакующим. Публичный анализ не знает, что там — это может быть уничтожение данных, повторная имплантация, программа-вымогатель или ничего. Риск не поддаётся оценке; именно поэтому порядок реагирования абсолютен.
Три свойства, которые меняют реагирование:
host.[email protected] вышел с валидной аттестацией SLSA. Происхождение (provenance) подтверждает целостность сборки, а не исходного кода: легитимный workflow компилировал уже троянизированный код.package.json (хук preinstall) и двух новых файлах, добавленных в пакет (setup.mjs, Math_Symbol.js).eslint → file-entry-cache → flat-cache → keyv. Поэтому сканер показывает цепочку в каждой находке.scan.mjs обладает следующими свойствами — это важно для любого, кто реагирует на инцидент в цепочке поставок:
npm install. Проверьте весь scan.mjs за 15 минут перед запуском.--update (загрузка свежего манифеста IOC), явная и необязательная.docker run --network=none или изолированная машина: просто скопируйте scan.mjs + iocs.json.Требование: Node.js ≥ 18 (на любой машине с npm он уже есть). Скачайте два файла — scan.mjs + iocs.json — и всё: никакой установки.
Внимание: если вы клонировали весь этот репозиторий, папка
fixtures/содержит инертные IOC, используемые в тестах (реальные имена и версии, фиктивное содержимое — без вредоносного ПО). Сканер пропускает её автоматически и предупреждает в выводе; находки из неё появятся, только если сканировать её намеренно.
Есть два режима запуска, отвечающих на разные вопросы, — и именно это определяет где запускать:
repo читает lock-файлы и node_modules — а lock-файлы живут в git, поэтому он может быть централизован: один человек сканирует все репозитории компании.host ищет имплант (watcher, LaunchAgent/systemd, хуки IDE), который живёт на машине, где выполнялся код — этого нет в git, и это нельзя централизовать.node scan.mjs repo /folder/with/all/the/repos --json=result.json --html=report.html
Отвечает на вопрос «какие проекты затронуты» за минуты, никого не привлекая. Принимает несколько путей; обходит поддиректории (включая монорепозитории и workspaces).
Для каждого проекта с находкой определите, кто трогал его с 2026-08-04 09:35 UTC (git log, логи CI). Эти люди запускают на своей машине:
node scan.mjs # current directory + host, in ~30 seconds
В область действия входят все, кто (a) запускал npm install/npm ci в этом окне; или (b) просто клонировал и открыл папку в VS Code или в AI-агенте — вектору B установка не нужна.
Поскольку стоимость ~30 секунд, а воронка может протекать (случайный клон, личный проект), самое безопасное внутреннее сообщение: каждый разработчик запускает node scan.mjs один раз и отправляет --json/--html в AppSec. Отправка вручную — это осознанное решение: у сканера нет телеметрии (нулевой исходящий трафик).
Высший приоритет: именно там живут самые ценные учётные данные. Здесь у сканера две разные роли — одна для прошлого, одна для будущего:
Триаж прошлого — НЕ сканируйте раннер, чтобы принять решение. Вопрос «был ли затронут этот раннер?» не решается сканированием: если какая-либо задача устанавливала затронутую версию без --ignore-scripts с 08-04, учётные данные уже были украдены в этот момент, а хост раннера редко сохраняет улики (эфемерные раннеры уничтожают контейнер в конце задачи; watcher самоочищается за ~24 часа). Отвечают на этот вопрос lock-файлы из шага 1 и логи CI. Если ответ «да, устанавливал»: пересоберите раннер и отзовите его секреты — раннеры эфемерны, чистить их нет смысла.
Профилактика на будущее — ДА, запускайте его в пайплайне. Добавьте сканер как шаг сборки, в режиме repo, после checkout и до npm install. Он не проверяет хост раннера — он проверяет код, который собираются установить, и код возврата останавливает сборку до того, как вредоносный preinstall получит шанс выполниться:
# example (GitHub Actions / GitLab CI — adapt):
- run: node scan.mjs repo . --json # exit 0 clean · 1 findings · 2 COMPROMISED
- run: npm ci --ignore-scripts # only runs if the previous step passed
node scan.mjs # scan the current directory + the host
node scan.mjs repo /path/a /path/b
node scan.mjs host # persistence/implants on the machine only
node scan.mjs repo . --json=result.json --html=report.html
node scan.mjs --update # update iocs.json (the only network operation)
| Уровень | Значение | Действие |
|---|---|---|
COMPROMISED | Вредоносная версия установлена в node_modules, полезная нагрузка подтверждена по хэшу или найден имплант персистентности | Считайте хост скомпрометированным; следуйте порядку реагирования — очистите имплант до ротации учётных данных |
EXPOSED | Вредоносная версия закреплена в lock-файле, нет доказательств выполнения | Закрепите безопасную версию, удалите node_modules, переустановите с --ignore-scripts |
AT_RISK | Диапазон (^/~) в package.json, допускающий вредоносную версию | Закрепите точную версию или заблокируйте её на прокси реестра |
SUSPECT | Эвристика (имя файла червя с несовпадающим хэшем, подозрительный скрипт жизненного цикла) | Проверьте вручную — это может быть новый вариант или ложное срабатывание |
INFO | Вектор присутствует, но IOC нет (например, общая задача folderOpen) | Проанализировать |
--html создаёт автономный отчёт с меткой времени, именем хоста, версией манифеста IOC и собственным SHA-256 сканера — он пригоден как приложение к уведомлению об инциденте и как аудиторский след.
Пока не отзывайте и не ротируйте никакие учётные данные — это триггер ловушки. Последовательность:
1. ИЗОЛИРУЙТЕ — отключите машину от сети. Это безопасно: без HTTP-ответа нет 4xx, ловушка не срабатывает, эксфильтрация прекращается. Не выключайте питание (энергозависимая память — улика).
2. СОХРАНИТЕ — перед удалением чего-либо (watcher самоуничтожается за ~24 часа):
mkdir -p /tmp/evidence && cp -r ~/.config/gh-token-monitor /tmp/evidence/ 2>/dev/null
cp /tmp/gh-token-monitor.*.log /tmp/evidence/ 2>/dev/null
shasum -a 256 /tmp/evidence/* 2>/dev/null
Файл handler — это команда атакующего, которая должна была выполниться, — не запускайте его, не вставляйте в шелл; обращайтесь с ним как с инертным текстом. Файл started_at ограничивает окно воздействия (аудитор и регулятор спросят его).
3. УДАЛИТЕ — сначала убейте процесс watcher, затем:
# macOS
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
# Linux
systemctl --user disable --now gh-token-monitor.service
loginctl disable-linger "$USER"
rm -f ~/.config/systemd/user/gh-token-monitor.service
# both
rm -rf ~/.config/gh-token-monitor ~/.local/bin/gh-token-monitor.sh /tmp/bun-dl-*
Также удалите вредоносные хуки из .claude/settings.json и .vscode/tasks.json, файлы setup.mjs/Math_Symbol.js/math_init.js и очистите кэши (~/.npm/_cacache, pnpm, yarn). Снова запустите node scan.mjs host, пока он не вернёт чистый результат.
4. ОТЗОВИТЕ — только когда все затронутые хосты очищены и проверены (одного живого watcher достаточно, чтобы сработал триггер). Сначала отзовите npm-токен (останавливает распространение червя); затем GitHub (PAT, deploy-ключи), AWS/GCP/Azure, Vault, Kubernetes, секреты CI — и любой секрет, который был на диске, потому что проводился regex-поиск.
5. ПРОВЕДИТЕ АУДИТ — червь действует от вашего имени: ищите в ваших организациях репозитории с описанием Shai-Hulud: Here We Go Again, неожиданно опубликованные с 08-04 npm-версии (депрекейтните их и уведомите потребителей) и использование учётных данных в CloudTrail/Audit Logs в окне started_at.
После этого: удалите node_modules, переустановите из чистого lock-файла с --ignore-scripts. CI-раннеры и хосты с подтверждённым выполнением: пересобирайте с нуля, всегда — выполнялся произвольный код, и список известных артефактов не гарантирует полноты.
Регулируемые организации (BR): подтверждённая компрометация с доступом к учётным данным может повлечь обязательства по уведомлению (Res. CMN 4.893/2021, Res. BCB 85/2021; LGPD ст. 48, если затронуты персональные данные). Документируйте таймлайн в UTC —
started_at, обнаружение, сдерживание, устранение, ротация — и подтверждайте сроки с юристами/комплаенсом.
Инцидент активен, список растёт. Чтобы получить свежий манифест:
node scan.mjs --update # the only operation that touches the network
--update забирает iocs.json из этого репозитория (securest8/npm-incident-response), никогда у третьих лиц — Securest8 является воротами курирования. Вы получаете то, что было опубликовано здесь последним.
Список пакетов берётся из публичного фида Wiz; хэши, C2-домены, IOC персистентности и безопасные версии статичны и курируются в tools/gen-iocs.mjs. Снимок CSV Wiz хранится в tools/keyv-packages.csv для воспроизводимости и офлайн-запусков.
node tools/gen-iocs.mjs # fetch the latest Wiz CSV, regenerate iocs.json + refresh the snapshot
node tools/gen-iocs.mjs --offline # regenerate from the committed snapshot, no network
node tools/gen-iocs.mjs --allow-shrink # allow a package count lower than the snapshot (guarded by default)
Генератор идемпотентен: он сохраняет существующий manifest_version и не перезаписывает файл, если ничего существенного не изменилось, а также отказывается записывать пустой или уменьшенный манифест (защита от усечённого/изменённого вышестоящего фида).
Автоматизация: .github/workflows/update-iocs.yml запускает генератор ежедневно (и по требованию) и коммитит в main только когда IOC действительно меняются — так что пользовательский --update отслеживает фид Wiz с задержкой примерно в день, а вся история доступна для аудита в коммитах. Переключите это на шаг pull-request (отмечено в workflow), когда инцидент утихнет и вы захотите ручное слияние.
node test/run-tests.mjs # 15 assertions against fixtures/demo-repo
fixtures/demo-repo — тестовый репозиторий с инертными IOC (реальные имена и версии, фиктивное содержимое — без вредоносного ПО). При сканировании собственного репозитория сканера папка fixtures/ пропускается автоматически (с предупреждением в выводе); тесты сканируют её, передавая путь явно.
Инструмент под один инцидент, создан для быстрого триажа в активном окне атаки — не замена Socket, Snyk или аналогичных. Исследование и IOC: Socket.dev, Wiz Research (публичный CSV), Kodem Security.
Поддерживается Securest8. Лицензия MIT.