
Detección y seguimiento automatizados de interacciones falsas en GitHub — CI diaria, infraestructura cero
Detección y seguimiento automatizado de interacciones falsas en GitHub
Un proyecto de JS Labs —
parte de la iniciativa AI Slop Intelligence.
Se ejecuta cada día. Puntúa cada cuenta sospechosa. Detecta campañas coordinadas de bots.
Abre issues directamente en repositorios comprometidos para que los mantenedores puedan actuar.
Apoya este proyecto
BTC 3QjWqhQbHdHgWeYHTpmorP8Pe1wgDjJy54
ETH 0x5851e6145F4773d1585b8686095FB16E368a4dA1
ZEC t1KSR5YkNPbjqRSCoLKo5AddFWdm9Kzxh1B
Las estrellas de GitHub son una señal de confianza. Así es como los desarrolladores deciden qué evaluar, de qué depender y qué recomendar. Esa señal está siendo corrompida sistemáticamente.
Durante el auge de la IA de 2024-2026, surgió una industria de granjas de bots para fabricar credibilidad para repositorios de baja calidad, a menudo maliciosos. Un proyecto con 800 estrellas en 48 horas se lee como legítimo para un desarrollador que escanea resultados de búsqueda. Ese es el punto. El objetivo de las interacciones falsas no son las estrellas en sí mismas; es la prueba social que esas estrellas producen, y las decisiones posteriores que esa prueba social influye.
El patrón es identificable. Cuentas creadas la misma semana, sin biografía, sin seguidores, sin repositorios originales, dando estrella a los mismos 15 repositorios en un lapso de 2 horas. No una campaña, sino decenas ejecutándose simultáneamente, cada día, a través de miles de cuentas. Los datos muestran repositorios donde 185 de cada 185 interactuantes son bots. Una tasa de falsedad del 100%. Colocaciones enteras en tendencias construidas sobre nada.
phantomstars fue construido porque este problema es abordable. La relación señal-ruido en la API pública de GitHub es, por ahora, aún lo suficientemente alta como para que las campañas coordinadas dejen huellas claras. Este proyecto lee esas huellas, publica los datos sin procesar y notifica directamente a los mantenedores de repositorios afectados.
Esto es parte del trabajo más amplio de AI Slop Intelligence en JS Labs, una investigación en curso sobre la mecánica y los efectos medibles del contenido de baja calidad generado por IA que inunda los ecosistemas de desarrolladores. Las interacciones falsas no son un problema periférico. Es el mecanismo de distribución que pone el slop frente a usuarios reales.
phantomstars ejecuta un trabajo diario de GitHub Actions que:
r/osinttools y r/coolgithubprojects extrayendo enlaces de repositorios de GitHub de los últimos 2 díasSin servidores. Sin bases de datos. Sin factura de infraestructura.
Sí. Cuando la tasa de falsedad de un repositorio supera el 40% o se detecta una campaña coordinada, phantomstars abre un issue directamente en ese repositorio. El issue contiene la tabla completa de sospechosos, membresía de la campaña, puntuaciones compuestas y fechas de creación de cuentas: todo lo que un mantenedor necesita para investigar y reportar a GitHub.
Si los issues están deshabilitados en un repositorio atacado, la notificación se omite silenciosamente y se registra en el registro del escaneo.
Sí.
owner/repo y ejecuta un escaneo dirigido.Por qué la separación:
Sí. Si tu cuenta aparece en data/suspects.jsonl y crees que la clasificación es incorrecta, abre un issue de falso positivo usando la plantilla proporcionada. Los informes son revisados manualmente antes de cualquier adición a la lista blanca. La lista blanca se almacena en data/allowlist.txt; las cuentas listadas allí se excluyen de todos los escaneos futuros y del libro mayor de sospechosos.
Un ID de campaña (ej. c-a3f9b2e1) es una huella digital hexadecimal determinista de 8 caracteres derivada del hash SHA-256 del conjunto ordenado de logins de miembros en esa campaña. El mismo grupo de cuentas producirá el mismo ID de campaña en ejecuciones de escaneo independientes, lo que permite el seguimiento longitudinal. No es un nombre de repositorio, un nombre de usuario ni ningún identificador externo.
Estabilidad: el ID es estable mientras el conjunto de miembros de la campaña no cambie. Si se añaden o suspenden bots entre escaneos, el ID cambia porque la membresía cambió. Esto es esperado y refleja la deriva real en la composición de las granjas de bots.
Sí. La fecha de creación de cada cuenta se obtiene de la API GraphQL de GitHub (campo createdAt) y se almacena en cada registro de sospechoso como account_created_at. También es la entrada principal para la puntuación de antigüedad de la cuenta, la señal individual más fuerte para cuentas falsas. Las cuentas creadas dentro de los 2 días posteriores a la interacción obtienen 1.0 solo por antigüedad.
Las puntuaciones individuales conllevan tasas significativas de falsos positivos. Un desarrollador nuevo con un perfil escaso legítimamente obtiene 0.75+. La herramienta considera esto al requerir evidencia a nivel de campaña antes de abrir issues; una sola cuenta sospechosa no es suficiente. Un grupo coordinado de 40+ cuentas, todas creadas la misma semana, todas puntuando 0.75+, todas interactuando dentro de 90 minutos, es un asunto diferente. Ahí es donde la confianza se vuelve procesable.
Los datos son siempre probabilísticos. Los cuerpos de los issues lo dicen explícitamente. El objetivo es dar a los mantenedores la señal y la evidencia en bruto para que tomen su propio juicio.
Cada cuenta recibe una puntuación compuesta de sospecha (0.0 = limpia, 1.0 = probablemente falsa) a partir de cuatro señales:
Umbrales de clasificación:
| Puntuación | Clasificación |
|---|---|
| ≥ 0.75 | likely_fake |
| ≥ 0.45 | suspicious |
| < 0.45 | clean (no almacenado) |
Una campaña es un grupo de ≥ 4 cuentas sospechosas que todas interactuaron con el mismo repositorio dentro de una ventana de 3 horas. El algoritmo utiliza union-find para construir componentes conectados; las cuentas que co-interactuaron dentro de la ventana se fusionan, y cualquier componente por encima del tamaño mínimo se marca como una campaña coordinada.
Los ID de campaña son huellas digitales SHA-256 estables del conjunto ordenado de miembros. La misma campaña detectada en días consecutivos tendrá el mismo ID mientras la membresía no cambie.
Por qué las campañas son la señal real: Las puntuaciones individuales tienen tasas de falsos positivos significativas. Un desarrollador nuevo con un perfil escaso puede obtener 0.80 solo. Cuarenta cuentas todas puntuando 0.75+, creadas la misma semana, todas dando estrella al mismo repositorio en 90 minutos, no es una coincidencia. La señal de campaña es donde los datos se vuelven procesables: la diferencia entre un punto de datos sospechoso y evidencia de una operación coordinada.
Todos los hallazgos se confirman en data/suspects.jsonl y data/repos.jsonl, un registro JSON por línea, solo adición. El resumen del trabajo de GitHub Actions (visible en la interfaz de Actions después de cada ejecución) proporciona un informe por escaneo formateado.
suspects.jsonl — un registro por cuenta marcada por escaneo:```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 registro por repositorio objetivo por escaneo:```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"
}
Ejemplos de consultas:```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
---
## Configuración
### 1. Haz un fork de este repositorio
Tu fork es propietario de los datos. Los resultados se confirman en `data/suspects.jsonl` y `data/repos.jsonl` en tu fork después de cada ejecución diaria.
### 2. Añade un secreto de PAT de GitHub
Crea un **classic** Personal Access Token con los alcances:
- `public_repo`: leer eventos de repositorios públicos y stargazers, crear issues en repositorios públicos
- `read:user`: obtener perfiles de usuario a través de GraphQL
**Settings → Secrets and variables → Actions → New repository secret** → nómbralo `GH_TOKEN`.
> El `GITHUB_TOKEN` predeterminado tiene límites de tasa restringidos y no puede llamar al endpoint de GraphQL de usuario a plena capacidad. Se requiere un PAT.
### 3. Habilita Actions
**Actions → Enable GitHub Actions** en tu fork. El flujo de trabajo se ejecuta a las **07:00 hora del Reino Unido diariamente** usando el reloj de `Europe/London`:
- **06:00 UTC** durante el horario de verano británico
- **07:00 UTC** durante el horario de invierno (Greenwich Mean Time)
No se requiere ninguna variable de entorno de programación adicional. El cron de GitHub Actions solo usa UTC, por lo que el flujo de trabajo se activa en ambas horas UTC y solo procede cuando la hora local de Londres es 07:00. Disparador manual disponible a través de **Actions → Daily Phantom Stars Scan → Run workflow**.
Después de cada ejecución, el informe de escaneo formateado es visible en **Actions → [ejecución] → Summary**.
### 4. Ejecutar localmente```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
Para una ejecución local ad hoc después de la configuración:```bash GH_TOKEN=ghp_your_token python -m phantomstars.main
Para escanear un repositorio en lugar del conjunto de descubrimiento normal:```bash
PHANTOMSTARS_TARGET_REPO=owner/repo GH_TOKEN=ghp_your_token python -m phantomstars.main
Los usuarios pueden solicitar una verificación de repositorio única de dos maneras:
Repo Check Request y proporcionar el repositorio objetivo más la profundidad solicitada.target_repo: owner/reporequest_depth: recent o lifetime-requestComportamiento actual:
recent: ejecuta el escaneo de participación reciente dirigido inmediatamente.lifetime-request: ejecuta un escaneo de toda la vida dirigido a través de estrellas y bifurcaciones históricas solo para ese repositorio.Límites para el modo de toda la vida:
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
---
## Limitaciones y modos de fallo conocidos
- **Límite de la API de eventos:** máximo 300 eventos recientes por repositorio. Los repositorios con miles de estrellas en un día tienen cobertura parcial.
- **Indicador de cobertura:** los repositorios que alcanzan el límite de 300 eventos se marcan como `capped` en informes y paneles; las proporciones en esos repositorios son muestras conservadoras, no conteos de día completo.
- **Retraso del índice de búsqueda:** el índice de búsqueda de GitHub es eventualmente consistente. Los repositorios creados segundos antes del límite de escaneo pueden pasarse por alto.
- **Desviación heurística:** los operadores de bots se adaptan. Los pesos de las puntuaciones pueden requerir ajustes periódicos; ajusta las constantes en `config.py`.
- **Falsos positivos individuales:** un desarrollador nuevo con un perfil escaso obtiene una puntuación de 0.75+ de forma aislada. La pertenencia a una campaña es la señal de alta confianza.
- **Deriva del ID de campaña:** si la membresía de un grupo de bots cambia entre escaneos (bots suspendidos, nuevos bots añadidos), el ID de campaña cambia. Esto refleja la evolución real de la campaña, no un error.
- **Límites de tasa:** 5,000 solicitudes de API por hora en un PAT autenticado. Dentro de los límites para tamaños de página de tendencias estándar.
- **Issues deshabilitados:** algunos repositorios objetivo deshabilitan los issues. Las notificaciones para esos repositorios se omiten silenciosamente.
---
## Proceso de falso positivo
Si tu cuenta aparece en `data/suspects.jsonl` y crees que está clasificada incorrectamente:
1. Encuentra tu entrada: `jq 'select(.login == "YOUR_LOGIN")' data/suspects.jsonl`
2. [Abre un issue de falso positivo](https://raw.githubusercontent.com/tg12/issues/new?template=false_positive.yml) con tu usuario, clasificación, fecha de escaneo y explicación
3. Los informes se revisan manualmente. Los falsos positivos verificados se añaden a `data/allowlist.txt` y se excluyen de todos los escaneos futuros, proporciones de repositorios y notificaciones de issues.
Nota: abrir un issue no modifica ni elimina ningún dato existente. El libro de sospechosos es de solo añadidura. La lista de permitidos solo afecta a escaneos futuros.
---
## Contribuir```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.
Esta herramienta realiza análisis de solo lectura de datos públicos de GitHub utilizando la API oficial de GitHub. Cuando se presentan incidencias en repositorios objetivo, contienen hallazgos probabilísticos y están claramente etiquetadas como automatizadas. Los hallazgos son indicadores, no acusaciones. Existen falsos positivos y se esperan.
Construida con IA como compañera de codificación, en respuesta a un problema del ecosistema creado en parte por la IA.
Apache 2.0. Ver LICENSE
Construido por tg12 · GitHub
Un proyecto de JS Labs · Paneles de inteligencia sobre basura de IA
| Fecha | Escaneados | Probablemente Falso | Sospechosos | Campañas | Nuevos Falsos (24h) |
|---|
| 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 |
| Repo | Interactuantes | Probablemente Falso | Falso Conocido % | % de Falsedad | Campañas | Cobertura | Fuentes |
|---|
| 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 |
| Señal | Peso | Medición |
|---|
| Antigüedad de la cuenta | 35% | < 2 días → 1.00 · < 7 días → 0.90 · < 30 días → 0.55 · < 90 días → 0.20 · más antigua → 0.00 |
| Integridad del perfil | 30% | Puntos por: sin biografía (+0.25), sin ubicación (+0.15), sin empresa (+0.10), cero seguidores (+0.30), cero siguiendo (+0.10), nombre de usuario con patrón de bot (+0.20) |
| Patrón de repositorios | 25% | Cero repos → 0.90 · todos los repos son forks → 0.80 · ratio de forks >85% → 0.55 |
| Historial de actividad | 10% | Cuentas >14 días con cero repos + cero gráfico social → 0.80 (cuentas fantasma). Solo cero repos → 0.60. Todos forks + sin gráfico social → 0.50 |