У меня есть Telegram-бот, который каждое утро присылает пять трендовых репозиториев GitHub с trendshift.io. Под каждым кнопка: нажал, и бот выдаёт выжимку README, чтобы разобрать идею. Я читаю эту ленту каждый день и иногда ставлю то, что понравилось.

26 сентября в ленте был «Microsoft Activation Scripts»: 1774 звезды, аккуратное описание, теги. Бот оформил его так же, как четыре соседних проекта, с той же кнопкой.

Это была ловушка.

Как она устроена

В репозитории один файл, README. Аккаунт владельца создан за 94 секунды до репозитория. Команда установки выглядит так:

iex(irm(-join"…"[-1..-99]))

Адрес скачивания записан в README задом наперёд. PowerShell сначала переворачивает строку, потом скачивает скрипт и сразу его выполняет. Глазами домен не прочитать, и сканер, который ищет в тексте ссылки, его тоже не видит. Ведёт он на домен, похожий на настоящий.

Настоящие Microsoft Activation Scripts существуют, и у них тоже установка в одну строку через irm | iex. На этом и расчёт: человек видит знакомую команду и вставляет её в PowerShell.

Очевидные фильтры не работают

Первое, что приходит в голову: помечать любую установку вида «скачай и сразу запусти» с сайта не на GitHub. Я прогнал это правило по всему, что бот прислал с 15 июля по 8 октября: 321 репозиторий, 314 из них ещё живы.

Так ставятся 38 из 314 нормальных проектов, среди них codex и opencode, а ещё несколько так же тянут установщики rustup или uv. Фильтр прятал бы каждый восьмой репозиторий из трендов, причём самые полезные.

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

Что сработало

Два независимых признака, любого достаточно.

  1. Замаскированная команда установки в README. Строка, перевёрнутая через -join"…"[-1... iex(irm(…)), где адрес вычисляется. base64, который декодируется и сразу выполняется. Скрытый запуск через mshta, certutil, bitsadmin или -WindowStyle Hidden. Перед проверкой текст очищается от невидимых символов и обратных кавычек PowerShell, которыми разбивают i`e`x.

  2. Одноразовый владелец и нет кода. Аккаунт создан меньше чем за 30 дней до репозитория, а в репозитории меньше 10 КБ или только README и LICENSE. Если GitHub не ответил, это «неизвестно», а не «кода нет»: сбой сети не должен прятать нормальный проект.

На тех же 314 репозиториях каждый признак срабатывает ровно один раз, на поддельном активаторе.

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

Чего фильтр не ловит, тоже записал:

  • обычную команду установки на похожий домен, без маскировки;

  • README со ссылкой на архив на старом аккаунте. В той же ленте были ещё шесть репозиториев с именами вроде Total-Commander, Acrobat-Pro-Software и Monero-Miner. GitHub удалил их раньше, чем я успел их измерить. Скорее всего, это был как раз такой тип, но проверять правило не на чем, поэтому правила нет;

  • вредоносный код внутри настоящего проекта. Фильтр читает только README и метаданные.

Вторая проблема: нейросеть дописывает

Бот просит DeepSeek пересказать каждый репозиторий одной фразой по-русски. В системном промпте прямо сказано не выдумывать. Я собрал 303 настоящих пересказа и сравнил их с источниками.

Иногда модель всё равно дописывала. Проект, у которого в описании было только meat.dev, она назвала «веб-фреймворком для Ruby». Инструмент для тестов сравнила с Cypress, которого в источнике нет. Всё это звучит правдоподобно, и глазами такое не ловится.

Идею проверки я взял у открытого проекта KKKKhazix/AIHOT (MIT): функция grounded() у них проверяет обзоры недельных и месячных выпусков. Правило простое. Каждое слово латиницей с заглавной буквой или цифрой (Ruby, GPT-5) и каждое число из трёх и больше цифр, с точкой или процентом должно встречаться в источнике. Если нет, бот показывает исходное описание с GitHub.

Первая версия отбраковала 9 пересказов из 303. Четыре из них были разным написанием одного и того же: Open-source и «open source», Wi-Fi и «WiFi», 2234 и «2,234». Я добавил сравнение без дефисов и пробелов и убрал разделители тысяч. Осталось 5 отказов. Три из них настоящие выдумки: Ruby, Cypress и Scratch, которых в источнике нет. Два других по сути верны, но источник их не подтверждает: NVIDIA для репозитория NVlabs и «XIX век» вместо «1800s». Проверка отбрасывает всё, что не может подтвердить, и я считаю это правильным для ленты, где пересказ можно заменить исходным описанием.

В первый же день в бою проверка отклонила правильный пересказ: в источнике Baidu был записан только иероглифами, 百度, а модель написала «Baidu». Теперь есть короткая таблица китайских названий компаний. За следующие четыре дня было 20 пересказов и ни одного отказа.

Ограничения те же, что у любой проверки по словам: она не видит утверждений («вдвое быстрее Redis» пройдёт, если Redis есть в источнике), выдумок строчными буквами и чисел из одной-двух цифр.

Где взять

Оба инструмента лежат отдельными репозиториями. Это один файл на Python, только стандартная библиотека, лицензия MIT, тесты на реальных случаях. Ловушка в тестах лежит в обезвреженном виде, все адреса заменены.

  • lure-check: python3 lure_check.py owner/repo, код возврата 1 для ловушки, удобно в CI;

  • ground-check: python3 ground_check.py --source source.txt --text summary.txt.

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