
Автоматическое обнаружение и отслеживание фальшивой активности на GitHub — ежедневный CI, без инфраструктуры
Автоматическое обнаружение и отслеживание накрутки активности на GitHub
Проект JS Labs —
часть инициативы AI Slop Intelligence.
Запускается каждый день. Оценивает каждый подозрительный аккаунт. Обнаруживает скоординированные бот-кампании.
Открывает issues напрямую в скомпрометированных репозиториях, чтобы мейнтейнеры могли отреагировать.
Поддержите этот проект
BTC 3QjWqhQbHdHgWeYHTpmorP8Pe1wgDjJy54
ETH 0x5851e6145F4773d1585b8686095FB16E368a4dA1
ZEC t1KSR5YkNPbjqRSCoLKo5AddFWdm9Kzxh1B
Звёзды GitHub — это сигнал доверия. По ним разработчики решают, что оценивать, от чего зависеть и что рекомендовать. Этот сигнал систематически портится.
Во время ИИ-бума 2024–2026 годов появилась индустрия ферм ботов, создающих видимость надёжности для низкокачественных, часто вредоносных репозиториев. Проект с 800 звёздами за 48 часов выглядит легитимным для разработчика, просматривающего результаты поиска. В этом и смысл. Цель накрутки — не сами звёзды, а социальное доказательство, которое они создают, и последующие решения, на которые это доказательство влияет.
Закономерность легко распознать. Аккаунты, созданные на одной неделе, без био, без подписчиков, без собственных репозиториев, ставящие звёзды одним и тем же 15 репозиториям в течение двух часов. Не одна кампания, а десятки, работающие одновременно, каждый день, через тысячи аккаунтов. Данные показывают репозитории, где 185 из 185 вовлечённых — боты. 100% фальшивки. Целые позиции в трендах построены на пустоте.
phantomstars был создан, потому что эта задача решаема. Соотношение сигнала и шума в публичном API GitHub пока ещё достаточно высокое, чтобы скоординированные кампании оставляли чёткие отпечатки. Этот проект считывает эти отпечатки, публикует сырые данные и напрямую уведомляет мейнтейнеров затронутых репозиториев.
Это часть более широкой работы AI Slop Intelligence в JS Labs — продолжающегося исследования механизмов и измеримых последствий низкокачественного ИИ-контента, заполоняющего экосистему разработчиков. Накрутка — не периферийная проблема. Это механизм распространения, который доставляет мусорный контент реальным пользователям.
phantomstars запускает ежедневное задание GitHub Actions, которое:
r/osinttools и r/coolgithubprojects, извлекая ссылки на GitHub-репозитории за последние 2 дняНикаких серверов. Никаких баз данных. Никаких расходов на инфраструктуру.
Да. Когда доля накрутки репозитория превышает 40% или обнаружена скоординированная кампания, phantomstars открывает issue прямо в этом репозитории. Issue содержит полную таблицу подозреваемых, состав кампании, композитные оценки и даты создания аккаунтов: всё, что нужно мейнтейнеру для расследования и обращения в GitHub.
Если в целевом репозитории отключены issues, уведомление молча пропускается и фиксируется в журнале сканирования.
Да.
owner/repo и запустите таргетированное сканирование.Почему такое разделение:
Да. Если ваш аккаунт появился в data/suspects.jsonl и вы считаете классификацию неверной, откройте issue о ложном срабатывании, используя предоставленный шаблон. Сообщения проверяются вручную перед добавлением в список исключений. Список исключений хранится в data/allowlist.txt; аккаунты из него исключаются из всех будущих сканов и из журнала подозреваемых.
ID кампании (например, c-a3f9b2e1) — это детерминированный 8-символьный шестнадцатеричный отпечаток, полученный из SHA-256 хеша отсортированного набора логинов участников этой кампании. Одна и та же группа аккаунтов будет давать один и тот же ID кампании при независимых запусках сканирования, что позволяет отслеживать динамику. Это не имя репозитория, не имя пользователя и не внешний идентификатор.
Стабильность: ID стабилен, пока набор участников кампании не меняется. Если боты добавлены или заблокированы между сканами, ID меняется, потому что изменился состав. Это ожидаемо и отражает реальное изменение состава бот-ферм.
Да. Дата создания каждого аккаунта берётся из GitHub GraphQL API (поле createdAt) и сохраняется в каждой записи подозреваемого как account_created_at. Это также основной вход для оценки возраста аккаунта — самого сильного сигнала для фейковых аккаунтов. Аккаунты, созданные в течение 2 дней до вовлечённости, получают 1.0 только за возраст.
Индивидуальные оценки несут значимый уровень ложных срабатываний. Новый разработчик с незаполненным профилем легитимно получает 0.75+. Инструмент учитывает это, требуя доказательств уровня кампании перед открытием issues; одного подозрительного аккаунта недостаточно. Скоординированный кластер из 40+ аккаунтов, созданных на одной неделе, с оценками 0.75+, вовлечённых в течение 90 минут, — это другое дело. Вот где уверенность становится применимой на практике.
Данные всегда вероятностные. Об этом прямо сказано в тексте issues. Цель — дать мейнтейнерам сигнал и сырые доказательства для собственного решения.
Каждый аккаунт получает композитную оценку подозрительности (0.0 = чисто, 1.0 = вероятно фейк) по четырём сигналам:
Пороги классификации:
| Оценка | Классификация |
|---|---|
| ≥ 0.75 | likely_fake |
| ≥ 0.45 | suspicious |
| < 0.45 | clean (не сохраняется) |
Кампания — это группа из ≥ 4 подозрительных аккаунтов, которые все взаимодействовали с одним и тем же репозиторием в течение 3-часового окна. Алгоритм использует union-find для построения связных компонентов; аккаунты, взаимодействовавшие в пределах окна, объединяются, и любой компонент, превышающий минимальный размер, помечается как скоординированная кампания.
ID кампаний — стабильные SHA-256 отпечатки отсортированного набора участников. Одна и та же кампания, обнаруженная в последующие дни, будет иметь тот же ID, пока состав не изменился.
Почему кампании — настоящий сигнал: Индивидуальные оценки имеют значимый уровень ложных срабатываний. Новый разработчик с незаполненным профилем может получить 0.80 в одиночку. Сорок аккаунтов с оценками 0.75+, созданных на одной неделе и ставящих звёзды одному репозиторию в течение 90 минут, — это не совпадение. Сигнал кампании — это место, где данные становятся применимыми на практике: разница между подозрительной точкой данных и доказательством скоординированной операции.
Все находки фиксируются в data/suspects.jsonl и data/repos.jsonl — по одной JSON-записи на строку, только добавление. Сводка задания GitHub Actions (видна в UI Actions после каждого запуска) предоставляет форматированный отчёт по каждому сканированию.
suspects.jsonl — одна запись на каждый помеченный аккаунт за каждое сканирование:```json { "login": "user98432", "account_age_score": 0.9, "profile_score": 0.8, "repo_pattern_score": 0.8, "activity_score": 0.85, "composite": 0.842, "classification": "likely_fake", "campaign_id": "c-a3f9b2e1", "scan_date": "2026-05-17", "account_created_at": "2026-05-15", "target_repos": ["owner/repo-a", "owner/repo-b"] }
**repos.jsonl** — одна запись на каждый целевой репозиторий за каждое сканирование:```json
{
"full_name": "owner/suspicious-repo",
"total_scanned": 87,
"likely_fake": 62,
"suspicious": 18,
"known_likely_fake": 27,
"known_likely_fake_ratio": 0.310,
"repeat_offenders": 11,
"allowlisted_excluded": 3,
"fakeness_ratio": 0.713,
"classification": "likely_fake",
"campaign_count": 3,
"discovery_sources": ["github_search_recent", "reddit_osinttools"],
"event_sample_complete": false,
"scan_date": "2026-05-17"
}
Примеры запросов:```bash
jq 'select(.scan_date == "2026-05-17" and .classification == "likely_fake") | .login' data/suspects.jsonl
jq 'select(.account_created_at >= "2026-05-14") | [.login, .account_created_at, .classification] | @tsv' -r data/suspects.jsonl
jq 'select(.scan_date == "2026-05-17") | [.full_name, .fakeness_ratio, .likely_fake] | @tsv' -r data/repos.jsonl | sort -t$'''\t''' -k2 -rn
jq 'select(.scan_date == "2026-05-17") | [.full_name, .known_likely_fake_ratio, .repeat_offenders] | @tsv' -r data/repos.jsonl | sort -t$'''\t''' -k2 -rn
jq 'select(.campaign_id == "c-a3f9b2e1") | [.login, .account_created_at, .composite] | @tsv' -r data/suspects.jsonl
jq 'select(.login == "user98432") | .target_repos[]' data/suspects.jsonl
jq 'select(.fakeness_ratio >= 0.6) | [.full_name, .fakeness_ratio, .campaign_count] | @tsv' -r data/repos.jsonl | sort -t$'\t' -k2 -rn
---
## Настройка
### 1. Сделайте форк этого репозитория
Ваш форк владеет данными. Результаты после каждого ежедневного запуска коммитятся обратно в `data/suspects.jsonl` и `data/repos.jsonl` в вашем форке.
### 2. Добавьте секрет GitHub PAT
Создайте **классический** персональный токен доступа (Personal Access Token) с областями:
- `public_repo`: чтение событий публичных репозиториев и списков звёзд, создание issues в публичных репозиториях
- `read:user`: получение профилей пользователей через GraphQL
**Настройки → Секреты и переменные → Actions → Новый секрет репозитория** → назовите его `GH_TOKEN`.
> У `GITHUB_TOKEN` по умолчанию ограничены скоростные лимиты, и он не может вызывать пользовательскую конечную точку GraphQL в полном объёме. Требуется PAT.
### 3. Включите Actions
**Actions → Включить GitHub Actions** в вашем форке. Рабочий процесс запускается ежедневно в **07:00 по времени Великобритании** с использованием часового пояса `Europe/London`:
- **06:00 UTC** в период британского летнего времени
- **07:00 UTC** в период среднего времени по Гринвичу
Никакая дополнительная переменная окружения для расписания не требуется. Cron в GitHub Actions работает только по UTC, поэтому рабочий процесс срабатывает в оба часа UTC и продолжает выполнение, только когда местное время в Лондоне равно 07:00. Ручной запуск доступен через **Actions → Daily Phantom Stars Scan → Запустить рабочий процесс**.
После каждого запуска отформатированный отчёт о сканировании виден в **Actions → [run] → Сводка**.
### 4. Запуск локально```bash
git clone https://github.com/YOUR_USERNAME/phantomstars.git
cd phantomstars
python -m venv venv && source venv/bin/activate
pip install -e .
GH_TOKEN=ghp_your_token python -m phantomstars.main
Для разового локального запуска после настройки:```bash GH_TOKEN=ghp_your_token python -m phantomstars.main
Чтобы просканировать один репозиторий вместо обычного набора обнаружения:```bash
PHANTOMSTARS_TARGET_REPO=owner/repo GH_TOKEN=ghp_your_token python -m phantomstars.main
Users can request a one-off repo check in two ways:
Repo Check Request issue template and provide the target repo plus requested depth.target_repo: owner/reporequest_depth: recent or lifetime-requestCurrent behavior:
recent: runs the targeted recent-engagement scan immediately.lifetime-request: runs a targeted lifetime scan across historical stars and forks for that repo only.Guardrails for lifetime mode:
phantomstars/ ├── .github/ │ ├── workflows/daily-scan.yml # Runs daily at 07:00 Europe/London │ └── ISSUE_TEMPLATE/false_positive.yml ├── src/phantomstars/ │ ├── config.py # All constants, no argparse, no env parsing │ ├── models.py # Frozen dataclasses │ ├── github_client.py # REST + GraphQL, tenacity retries, rate-limit aware │ ├── heuristics.py # Per-user composite scoring engine │ ├── campaigns.py # Timestamp clustering + union-find │ ├── storage.py # JSONL append + query helpers │ ├── reporter.py # README dashboard injector │ ├── notifier.py # GitHub Issues notifier (files on targeted repos) │ └── main.py # Orchestration entry point ├── tests/ │ ├── conftest.py │ ├── test_heuristics.py │ └── test_campaigns.py ├── data/ │ ├── suspects.jsonl # Append-only account findings ledger │ ├── repos.jsonl # Append-only per-repo intelligence │ └── allowlist.txt # Accounts excluded from future scans └── pyproject.toml
---
## Ограничения и известные сценарии отказов
- **Ограничение Events API:** максимум 300 последних событий на репозиторий. Репозитории с тысячами звёзд в день имеют частичное покрытие.
- **Флаг покрытия:** репозитории, достигшие лимита в 300 событий, помечаются как `capped` в отчётах и дашбордах; показатели по этим репозиториям являются консервативными выборками, а не полнодневными подсчётами.
- **Задержка поискового индекса:** поисковый индекс GitHub в конечном счёте консистентен. Репозитории, созданные за секунды до границы сканирования, могут быть пропущены.
- **Дрейф эвристик:** операторы ботов адаптируются. Веса оценок могут требовать периодической настройки; отрегулируйте константы в `config.py`.
- **Индивидуальные ложные срабатывания:** новый разработчик с неполным профилем в изоляции набирает 0.75+. Членство в кампании — сигнал высокой уверенности.
- **Дрейф ID кампании:** если состав бот-фермы меняется между сканированиями (боты заблокированы, добавлены новые боты), ID кампании изменяется. Это отражает реальную эволюцию кампании, а не ошибку.
- **Лимиты API:** 5 000 запросов в час на аутентифицированный PAT. С большим запасом укладывается в лимиты для стандартных размеров страниц трендов.
- **Отключённые issues:** некоторые целевые репозитории отключают issues. Уведомления по этим репозиториям молча пропускаются.
---
## Процесс обработки ложных срабатываний
Если ваш аккаунт появился в `data/suspects.jsonl` и вы считаете, что он классифицирован неверно:
1. Найдите свою запись: `jq 'select(.login == "YOUR_LOGIN")' data/suspects.jsonl`
2. [Откройте issue о ложном срабатывании](../../issues/new?template=false_positive.yml) с указанием вашего логина, классификации, даты сканирования и объяснения
3. Отчёты проверяются вручную. Подтверждённые ложные срабатывания добавляются в `data/allowlist.txt` и исключаются из всех будущих сканирований, показателей репозиториев и уведомлений об issues.
Примечание: открытие issue не изменяет и не удаляет существующие данные. Журнал подозреваемых — только на добавление. Список исключений влияет только на будущие сканирования.
---
## Внесение вклада```bash
pip install -e ".[dev]"
python -m black .
python -m ruff check .
python -m mypy src
python -m pytest
All four must pass before a PR.
Этот инструмент выполняет анализ только для чтения публичных данных GitHub с использованием официального GitHub API. Когда на целевых репозиториях открываются issue, они содержат вероятностные результаты и явно помечены как автоматические. Находки — это индикаторы, а не обвинения. Ложные срабатывания существуют, и это ожидаемо.
Создан с использованием ИИ в качестве партнёра по программированию, в ответ на проблему экосистемы, частично созданную ИИ.
Apache 2.0. См. LICENSE
Создано tg12 · GitHub
Проект JS Labs · AI Slop Intelligence Dashboards
| Дата | Проверено | Вероятно фейк | Подозрительные | Кампании | Новые фейки (24ч) |
|---|
| 2026-06-16 | 1846 | 221 | 1625 | 56 | 190 |
| 2026-06-15 | 2274 | 418 | 1856 | 61 | 397 |
| 2026-06-14 | 1953 | 355 | 1598 | 44 | 310 |
| 2026-06-13 | 2012 | 301 | 1711 | 47 | 251 |
| 2026-06-12 | 2298 | 336 | 1962 | 57 | 300 |
| 2026-06-11 | 1957 | 385 | 1572 | 42 | 356 |
| 2026-06-10 | 2043 | 687 | 1356 | 50 | 665 |
| 2026-06-09 | 2199 | 690 | 1509 | 44 | 632 |
| 2026-06-08 | 1913 | 450 | 1463 | 50 | 424 |
| 2026-06-07 | 1797 | 658 | 1139 | 30 | 618 |
| 2026-06-06 | 2625 | 712 | 1913 | 40 | 620 |
| 2026-06-05 | 2403 | 673 | 1730 | 53 | 617 |
| 2026-06-04 | 2237 | 441 | 1796 | 41 | 367 |
| 2026-06-03 | 2331 | 488 | 1843 | 53 | 431 |
| 2026-06-02 | 2795 | 773 | 2022 | 37 | 616 |
| 2026-06-01 | 2490 | 533 | 1957 | 39 | 355 |
| 2026-05-31 | 2302 | 458 | 1844 | 32 | 280 |
| 2026-05-30 | 2576 | 530 | 2046 | 20 | 356 |
| 2026-05-29 | 2838 | 733 | 2105 | 42 | 369 |
| 2026-05-28 | 2748 | 694 | 2054 | 39 | 396 |
| 2026-05-27 | 2193 | 560 | 1633 | 32 | 491 |
| 2026-05-26 | 1930 | 236 | 1694 | 43 | 190 |
| 2026-05-25 | 1526 | 214 | 1312 | 32 | 158 |
| 2026-05-24 | 2170 | 358 | 1812 | 39 | 265 |
| 2026-05-23 | 2548 | 426 | 2122 | 43 | 317 |
| 2026-05-22 | 2318 | 340 | 1978 | 47 | 247 |
| 2026-05-21 | 1981 | 348 | 1633 | 25 | 277 |
| 2026-05-20 | 1613 | 268 | 1345 | 23 | 163 |
| 2026-05-19 | 5463 | 630 | 4121 | 67 | 442 |
| 2026-05-18 | 8838 | 670 | 7950 | 128 | 340 |
| Репозиторий | Вовлечено | Вероятно фейк | Известный фейк % | Накрутка % | Кампании | Охват | Источники |
|---|
| freeCodeCamp/freeCodeCamp | 264 | 26 | 0.0% | 9.8% | 1 | полный | github_trending |
| Lolner95/use-kimi-on-cursor | 116 | 17 | 26.7% | 14.7% | 1 | полный | github_search_recent |
| zmustafa/AzureSupportAgent | 33 | 16 | 36.4% | 48.5% | 1 | полный | github_search_recent |
| Free-TV/IPTV | 291 | 14 | 0.3% | 4.8% | 1 | полный | github_trending |
| Panniantong/Agent-Reach | 292 | 14 | 0.3% | 4.8% | 1 | ограничен | github_trending |
| jwasham/coding-interview-university | 288 | 12 | 0.3% | 4.2% | 1 | ограничен | github_trending |
| Alex-Shayo/bakkes-mod-install | 37 | 10 | 0.0% | 27.0% | 1 | полный | github_search_recent |
| Timgt86/yt-downloader-savetube | 37 | 10 | 0.0% | 27.0% | 1 | полный | github_search_recent |
| devassisthub/Zelda-TP-PC-Port | 35 | 9 | 0.0% | 25.7% | 1 | полный | github_search_recent |
| imohammedyasin/steam-tools | 35 | 9 | 0.0% | 25.7% | 1 | полный | github_search_recent |
| lol-toolkit/ltk-manager-lol | 35 | 9 | 0.0% | 25.7% | 1 | полный | github_search_recent |
| tor-browser-download/tor-browser | 36 | 9 | 0.0% | 25.0% | 1 | полный | github_search_recent |
| claude-code-ai-anthropic/free-claude-code-ai-desktop-app | 38 | 9 | 0.0% | 23.7% | 1 | полный | github_search_recent |
| vitaliikapliuk/modelharness | 60 | 9 | 31.7% | 15.0% | 1 | полный | github_search_recent |
| shiyu-coder/Kronos | 282 | 9 | 1.1% | 3.2% | 1 | полный | github_trending |
| snanas/Forza-Horizon-Spotify-Radio | 34 | 8 | 0.0% | 23.5% | 1 | полный | github_search_recent |
| bingook/bingo | 45 | 8 | 2.2% | 17.8% | 2 | полный | github_search_recent |
| darricke/claude-fable-5-desktop-free | 49 | 8 | 0.0% | 16.3% | 1 | полный | github_search_recent |
| Ponzuu84/MaaNTE | 32 | 7 | 0.0% | 21.9% | 1 | полный | github_search_recent |
| iDesignStudioz/yellowkey-bitlocker | 33 | 7 | 0.0% | 21.2% | 1 | полный | github_search_recent |
| chatwoot/chatwoot | 230 | 7 | 0.0% | 3.0% | 1 | полный | github_trending |
| Open-Builders/pumpfun-bundler-pump.fun-bundler-solana-token-bundler-bot | 18 | 6 | 33.3% | 33.3% | 1 | полный | github_search_recent |
| taisly/agent | 23 | 6 | 17.4% | 26.1% | 2 | полный | github_search_recent |
| iptv-org/iptv | 193 | 6 | 0.0% | 3.1% | 1 | полный | github_trending |
| itsfatduck/optimizerDuck | 293 | 6 | 0.3% | 2.0% | 1 | полный | github_trending |
| Сигнал | Вес | Измерение |
|---|
| Возраст аккаунта | 35% | < 2 дня → 1.00 · < 7 дней → 0.90 · < 30 дней → 0.55 · < 90 дней → 0.20 · старше → 0.00 |
| Полнота профиля | 30% | Баллы за: нет био (+0.25), нет локации (+0.15), нет компании (+0.10), ноль подписчиков (+0.30), ноль подписок (+0.10), имя-паттерн бота (+0.20) |
| Паттерн репозиториев | 25% | Ноль репозиториев → 0.90 · все репозитории — форки → 0.80 · доля форков >85% → 0.55 |
| История активности | 10% | Аккаунты старше 14 дней с нулём репозиториев и нулевым социальным графом → 0.80 (аккаунты-призраки). Только ноль репозиториев → 0.60. Все форки + нет социального графа → 0.50 |