
Détection et suivi automatisés des faux engagements sur GitHub — CI quotidienne, zéro infrastructure
Détection et suivi automatisés des faux engagements sur GitHub
Un projet de JS Labs —
faisant partie de l'initiative AI Slop Intelligence.
Tourne chaque jour. Attribue un score à chaque compte suspect. Détecte les campagnes de bots coordonnées.
Ouvre des issues directement sur les dépôts compromis afin que les mainteneurs puissent agir.
Soutenez ce projet
BTC 3QjWqhQbHdHgWeYHTpmorP8Pe1wgDjJy54
ETH 0x5851e6145F4773d1585b8686095FB16E368a4dA1
ZEC t1KSR5YkNPbjqRSCoLKo5AddFWdm9Kzxh1B
Les étoiles GitHub sont un signal de confiance. C'est ainsi que les développeurs décident quoi évaluer, de quoi dépendre et quoi recommander. Ce signal est en train d'être systématiquement corrompu.
Pendant le boom de l'IA de 2024-2026, une industrie de fermes de bots a émergé pour fabriquer de la crédibilité pour des dépôts de mauvaise qualité, souvent malveillants. Un projet avec 800 étoiles en 48 heures paraît légitime à un développeur qui parcourt les résultats de recherche. C'est tout l'intérêt. L'objectif des faux engagements n'est pas les étoiles en elles-mêmes ; c'est la preuve sociale que ces étoiles produisent, et les décisions en aval que cette preuve sociale influence.
Le schéma est identifiable. Des comptes créés la même semaine, sans bio, sans abonnés, sans dépôts d'origine, qui mettent une étoile aux mêmes 15 dépôts en l'espace de 2 heures. Pas une seule campagne, mais des dizaines qui tournent simultanément, chaque jour, à travers des milliers de comptes. Les données montrent des dépôts où 185 personnes sur 185 ayant interagi sont des bots. Un taux de fausseté de 100 %. Des places entières dans le classement construites sur du vide.
phantomstars a été créé parce que ce problème est traitable. Le rapport signal/bruit dans l'API publique de GitHub est, pour l'instant, encore suffisamment élevé pour que les campagnes coordonnées laissent des empreintes claires. Ce projet lit ces empreintes, publie les données brutes et notifie directement les mainteneurs de dépôts concernés.
Ce projet fait partie des travaux plus larges d'AI Slop Intelligence menés chez JS Labs, une recherche continue sur les mécanismes et les effets mesurables du contenu de mauvaise qualité généré par l'IA qui inonde les écosystèmes de développeurs. Les faux engagements ne sont pas un problème périphérique. C'est le mécanisme de distribution qui met le slop sous les yeux des vrais utilisateurs.
phantomstars exécute un job GitHub Actions quotidien qui :
r/osinttools et r/coolgithubprojects en extrayant les liens de dépôts GitHub des 2 derniers joursPas de serveurs. Pas de bases de données. Pas de facture d'infrastructure.
Oui. Lorsque le taux de fausseté d'un dépôt dépasse 40 % ou qu'une campagne coordonnée est détectée, phantomstars ouvre une issue directement sur ce dépôt. L'issue contient le tableau complet des suspects, l'appartenance à la campagne, les scores composites et les dates de création des comptes : tout ce dont un mainteneur a besoin pour enquêter et faire un signalement à GitHub.
Si les issues sont désactivées sur un dépôt ciblé, la notification est ignorée silencieusement et consignée dans le journal d'analyse.
Oui.
owner/repo et lancez une analyse ciblée.Pourquoi cette distinction :
Oui. Si votre compte apparaît dans data/suspects.jsonl et que vous pensez que la classification est incorrecte, ouvrez une issue de faux positif en utilisant le modèle fourni. Les signalements sont examinés manuellement avant toute ajout à la liste blanche. La liste blanche est stockée dans data/allowlist.txt ; les comptes qui y figurent sont exclus de toutes les analyses futures et du registre des suspects.
Un identifiant de campagne (par ex. c-a3f9b2e1) est une empreinte hexadécimale déterministe de 8 caractères dérivée du hachage SHA-256 de l'ensemble trié des identifiants des membres de cette campagne. Le même groupe de comptes produira le même identifiant de campagne lors d'analyses indépendantes, ce qui permet un suivi longitudinal. Ce n'est ni un nom de dépôt, ni un nom d'utilisateur, ni un identifiant externe.
Stabilité : l'identifiant est stable tant que l'ensemble des membres de la campagne ne change pas. Si des bots sont ajoutés ou suspendus entre deux analyses, l'identifiant change car la composition des membres a changé. C'est attendu et reflète l'évolution réelle de la composition des fermes de bots.
Oui. La date de création de chaque compte est récupérée depuis l'API GraphQL GitHub (champ createdAt) et stockée dans chaque enregistrement de suspect sous le nom account_created_at. C'est aussi l'entrée principale du score d'ancienneté du compte, le signal individuel le plus fort pour les faux comptes. Les comptes créés dans les 2 jours suivant leur interaction obtiennent un score de 1.0 sur la seule ancienneté.
Les scores individuels comportent des taux de faux positifs significatifs. Un nouveau développeur avec un profil peu fourni obtient légitimement un score de 0.75 ou plus. L'outil en tient compte en exigeant des preuves au niveau de la campagne avant d'ouvrir des issues ; un seul compte suspect ne suffit pas. Une grappe coordonnée de plus de 40 comptes, tous créés la même semaine, tous avec un score de 0.75 ou plus, tous ayant interagi en 90 minutes, est une autre affaire. C'est là que la confiance devient exploitable.
Les données sont toujours probabilistes. Les corps des issues le disent explicitement. L'objectif est de donner aux mainteneurs le signal et les preuves brutes pour se faire leur propre jugement.
Chaque compte reçoit un score de suspicion composite (0.0 = propre, 1.0 = probablement faux) à partir de quatre signaux :
Seuils de classification :
| Score | Classification |
|---|---|
| ≥ 0.75 | likely_fake |
| ≥ 0.45 | suspicious |
| < 0.45 | clean (non stocké) |
Une campagne est un groupe d'au moins 4 comptes suspects ayant tous interagi avec le même dépôt dans une fenêtre de 3 heures. L'algorithme utilise l'union-find pour construire des composantes connexes ; les comptes ayant co-interagi dans la fenêtre sont fusionnés, et toute composante au-dessus de la taille minimale est signalée comme campagne coordonnée.
Les identifiants de campagne sont des empreintes SHA-256 stables de l'ensemble trié des membres. La même campagne détectée plusieurs jours consécutifs aura le même identifiant tant que la composition des membres reste inchangée.
Pourquoi les campagnes sont le véritable signal : Les scores individuels ont des taux de faux positifs significatifs. Un nouveau développeur avec un profil peu fourni peut obtenir un score de 0.80 à lui seul. Quarante comptes ayant tous un score de 0.75 ou plus, créés la même semaine, mettant tous une étoile au même dépôt en 90 minutes, n'est pas une coïncidence. Le signal de campagne est là où les données deviennent exploitables : la différence entre un point de donnée suspect et une preuve d'opération coordonnée.
Tous les résultats sont commités dans data/suspects.jsonl et data/repos.jsonl, un enregistrement JSON par ligne, en ajout seul. Le résumé du job GitHub Actions (visible dans l'interface Actions après chaque exécution) fournit un rapport formaté pour chaque analyse.
suspects.jsonl — un enregistrement par compte signalé et par analyse :```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** — un enregistrement par dépôt ciblé par analyse :```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"
}
Exemples de requêtes :```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
---
## Configuration
### 1. Forkez ce dépôt
Les données appartiennent à votre fork. Les résultats sont commités sur `data/suspects.jsonl` et `data/repos.jsonl` de votre fork après chaque exécution quotidienne.
### 2. Ajoutez un secret PAT GitHub
Créez un Personal Access Token **classique** avec les scopes :
- `public_repo` : lire les événements des dépôts publics et les stargazers, créer des issues sur les dépôts publics
- `read:user` : récupérer les profils utilisateurs via GraphQL
**Paramètres → Secrets et variables → Actions → Nouveau secret de dépôt** → nommez-le `GH_TOKEN`.
> Le `GITHUB_TOKEN` par défaut a des limites de débit restreintes et ne peut pas appeler le point de terminaison GraphQL utilisateur à pleine capacité. Un PAT est requis.
### 3. Activez Actions
**Actions → Activer GitHub Actions** sur votre fork. Le workflow s'exécute chaque jour à **07:00, heure du Royaume-Uni**, en utilisant l'horloge `Europe/London` :
- **06:00 UTC** pendant l'heure d'été britannique
- **07:00 UTC** pendant l'heure moyenne de Greenwich
Aucune variable d'environnement de planification supplémentaire n'est requise. Le cron de GitHub Actions est en UTC uniquement, donc le workflow se déclenche aux deux heures UTC et ne continue que lorsque l'heure locale de Londres est 07:00. Le déclenchement manuel est disponible via **Actions → Daily Phantom Stars Scan → Exécuter le workflow**.
Après chaque exécution, le rapport de scan formaté est visible dans **Actions → [run] → Résumé**.
### 4. Exécutez en local```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
Pour une exécution locale ad hoc après configuration :```bash GH_TOKEN=ghp_your_token python -m phantomstars.main
Pour analyser un seul dépôt au lieu de l'ensemble de découverte normal :```bash
PHANTOMSTARS_TARGET_REPO=owner/repo GH_TOKEN=ghp_your_token python -m phantomstars.main
Les utilisateurs peuvent demander une vérification ponctuelle de dépôt de deux manières :
Repo Check Request et fournir le dépôt cible ainsi que la profondeur demandée.target_repo : owner/reporequest_depth : recent ou lifetime-requestComportement actuel :
recent : exécute immédiatement l'analyse ciblée d'engagement récent.lifetime-request : exécute une analyse ciblée sur l'ensemble de la durée de vie (lifetime) des étoiles et des forks historiques pour ce dépôt uniquement.Garde-fous pour le mode lifetime :
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
---
## Limitations et modes de défaillance connus
- **Plafond de l'API Events:** maximum 300 événements récents par dépôt. Les dépôts qui reçoivent des milliers d'étoiles en une journée ont une couverture partielle.
- **Indicateur de couverture:** les dépôts qui atteignent le plafond de 300 événements sont marqués comme `capped` dans les rapports et les tableaux de bord ; les ratios sur ces dépôts sont des échantillons conservateurs, pas des comptages sur la journée complète.
- **Latence de l'index de recherche:** l'index de recherche de GitHub est à cohérence éventuelle. Les dépôts créés quelques secondes avant la limite d'analyse peuvent être manqués.
- **Dérive des heuristiques:** les opérateurs de bots s'adaptent. Les poids des scores peuvent nécessiter un ajustement périodique ; modifiez les constantes dans `config.py`.
- **Faux positifs individuels:** un nouveau développeur avec un profil peu fourni obtient un score de 0.75+ isolément. L'appartenance à une campagne est le signal de haute confiance.
- **Dérive de l'ID de campagne:** si la composition d'une ferme de bots change entre deux analyses (bots suspendus, nouveaux bots ajoutés), l'ID de campagne change. Cela reflète l'évolution réelle de la campagne, pas un bug.
- **Limites de taux:** 5,000 requêtes API/heure avec un PAT authentifié. Bien en dessous des limites pour les tailles de pages tendances standard.
- **Problèmes désactivés:** certains dépôts ciblés désactivent les problèmes. Les notifications pour ces dépôts sont ignorées silencieusement.
---
## Processus de faux positif
Si votre compte apparaît dans `data/suspects.jsonl` et que vous pensez qu'il est classé incorrectement :
1. Trouvez votre entrée : `jq 'select(.login == "YOUR_LOGIN")' data/suspects.jsonl`
2. [Ouvrez un problème de faux positif](https://raw.githubusercontent.com/tg12/issues/new?template=false_positive.yml) avec votre identifiant, la classification, la date d'analyse et une explication
3. Les rapports sont examinés manuellement. Les faux positifs vérifiés sont ajoutés à `data/allowlist.txt` et exclus de toutes les analyses futures, des ratios de dépôts et des notifications de problèmes.
Remarque : ouvrir un problème ne modifie ni ne supprime aucune donnée existante. Le registre des suspects est en append-only. La liste d'autorisation n'affecte que les analyses futures.
---
## Contribuer```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.
Cet outil effectue une analyse en lecture seule des données publiques de GitHub à l'aide de l'API GitHub officielle. Lorsque des issues sont déposées sur des dépôts ciblés, elles contiennent des résultats probabilistes et sont clairement étiquetées comme automatisées. Les résultats sont des indicateurs, pas des accusations. Les faux positifs existent et sont attendus.
Construit avec l'IA comme partenaire de codage, en réponse à un problème d'écosystème créé en partie par l'IA.
Apache 2.0. Voir LICENSE
Construit par tg12 · GitHub
Un projet de JS Labs · AI Slop Intelligence Dashboards
| Date | Analysés | Probablement faux | Suspects | Campagnes | Nouveaux faux (24 h) |
|---|
| 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 |
| Dépôt | Interacteurs | Probablement faux | % faux connus | % de fausseté | Campagnes | Couverture | Sources |
|---|
| freeCodeCamp/freeCodeCamp | 264 | 26 | 0.0% | 9.8% | 1 | complete | github_trending |
| Lolner95/use-kimi-on-cursor | 116 | 17 | 26.7% | 14.7% | 1 | complete | github_search_recent |
| zmustafa/AzureSupportAgent | 33 | 16 | 36.4% | 48.5% | 1 | complete | github_search_recent |
| Free-TV/IPTV | 291 | 14 | 0.3% | 4.8% | 1 | complete | github_trending |
| Panniantong/Agent-Reach | 292 | 14 | 0.3% | 4.8% | 1 | capped | github_trending |
| jwasham/coding-interview-university | 288 | 12 | 0.3% | 4.2% | 1 | capped | github_trending |
| Alex-Shayo/bakkes-mod-install | 37 | 10 | 0.0% | 27.0% | 1 | complete | github_search_recent |
| Timgt86/yt-downloader-savetube | 37 | 10 | 0.0% | 27.0% | 1 | complete | github_search_recent |
| devassisthub/Zelda-TP-PC-Port | 35 | 9 | 0.0% | 25.7% | 1 | complete | github_search_recent |
| imohammedyasin/steam-tools | 35 | 9 | 0.0% | 25.7% | 1 | complete | github_search_recent |
| lol-toolkit/ltk-manager-lol | 35 | 9 | 0.0% | 25.7% | 1 | complete | github_search_recent |
| tor-browser-download/tor-browser | 36 | 9 | 0.0% | 25.0% | 1 | complete | github_search_recent |
| claude-code-ai-anthropic/free-claude-code-ai-desktop-app | 38 | 9 | 0.0% | 23.7% | 1 | complete | github_search_recent |
| vitaliikapliuk/modelharness | 60 | 9 | 31.7% | 15.0% | 1 | complete | github_search_recent |
| shiyu-coder/Kronos | 282 | 9 | 1.1% | 3.2% | 1 | complete | github_trending |
| snanas/Forza-Horizon-Spotify-Radio | 34 | 8 | 0.0% | 23.5% | 1 | complete | github_search_recent |
| bingook/bingo | 45 | 8 | 2.2% | 17.8% | 2 | complete | github_search_recent |
| darricke/claude-fable-5-desktop-free | 49 | 8 | 0.0% | 16.3% | 1 | complete | github_search_recent |
| Ponzuu84/MaaNTE | 32 | 7 | 0.0% | 21.9% | 1 | complete | github_search_recent |
| iDesignStudioz/yellowkey-bitlocker | 33 | 7 | 0.0% | 21.2% | 1 | complete | github_search_recent |
| chatwoot/chatwoot | 230 | 7 | 0.0% | 3.0% | 1 | complete | github_trending |
| Open-Builders/pumpfun-bundler-pump.fun-bundler-solana-token-bundler-bot | 18 | 6 | 33.3% | 33.3% | 1 | complete | github_search_recent |
| taisly/agent | 23 | 6 | 17.4% | 26.1% | 2 | complete | github_search_recent |
| iptv-org/iptv | 193 | 6 | 0.0% | 3.1% | 1 | complete | github_trending |
| itsfatduck/optimizerDuck | 293 | 6 | 0.3% | 2.0% | 1 | complete | github_trending |
| Signal | Poids | Mesure |
|---|
| Ancienneté du compte | 35% | < 2 jours → 1.00 · < 7 jours → 0.90 · < 30 jours → 0.55 · < 90 jours → 0.20 · plus ancien → 0.00 |
| Complétude du profil | 30% | Points pour : pas de bio (+0.25), pas de localisation (+0.15), pas d'entreprise (+0.10), zéro abonné (+0.30), zéro abonnement (+0.10), nom d'utilisateur à motif de bot (+0.20) |
| Schéma de dépôts | 25% | Zéro dépôt → 0.90 · tous les dépôts sont des forks → 0.80 · ratio de forks > 85 % → 0.55 |
| Historique d'activité | 10% | Comptes de plus de 14 jours avec zéro dépôt + zéro graphe social → 0.80 (comptes fantômes). Zéro dépôt uniquement → 0.60. Tous forks + aucun graphe social → 0.50 |