
GitHub의 가짜 참여 자동 탐지 및 추적 — 일일 CI, 인프라 제로
GitHub에서 가짜 참여를 자동 탐지하고 추적합니다
JS Labs 프로젝트 —
AI Slop Intelligence 이니셔티브의 일환.
매일 실행됩니다. 모든 의심 계정을 점수화합니다. 조직적인 봇 캠페인을 탐지합니다.
손상된 저장소에 직접 이슈를 등록하여 관리자가 조치를 취할 수 있게 합니다.
이 프로젝트 지원하기
BTC 3QjWqhQbHdHgWeYHTpmorP8Pe1wgDjJy54
ETH 0x5851e6145F4773d1585b8686095FB16E368a4dA1
ZEC t1KSR5YkNPbjqRSCoLKo5AddFWdm9Kzxh1B
GitHub 스타는 신뢰 신호입니다. 개발자들이 무엇을 평가하고, 무엇에 의존하며, 무엇을 추천할지 결정하는 기준입니다. 그 신호가 조직적으로 훼손되고 있습니다.
2024~2026년 AI 붐 동안 품질이 낮거나 악성인 저장소에 신뢰도를 조작하기 위한 봇 팜 업계가 등장했습니다. 48시간 만에 800개의 스타를 받은 프로젝트는 검색 결과를 훑는 개발자에게 정상적인 프로젝트로 보입니다. 그것이 바로 요점입니다. 가짜 참여의 목표는 스타 자체가 아니라, 그 스타가 만들어내는 사회적 증명과 그 사회적 증명이 영향을 미치는 후속 결정입니다.
그 패턴은 식별 가능합니다. 같은 주에 생성된 계정, 프로필 소개 없음, 팔로워 없음, 원본 저장소 없음, 2시간 창 안에서 같은 15개 저장소에 스타를 누른 경우. 단일 캠페인이 아니라 매일 수천 개의 계정에 걸쳐 수십 개의 캠페인이 동시에 실행되고 있습니다. 데이터는 참여자 185명 중 185명이 봇인 저장소를 보여줍니다. 100% 가짜 비율입니다. 트렌딩 순위 전체가 아무것도 없는 기반 위에 세워졌습니다.
phantomstars는 이 문제가 해결 가능하기 때문에 만들어졌습니다. GitHub 공개 API의 신호 대비 잡음 비율은 현재까지도 충분히 높아서 조직적인 캠페인이 뚜렷한 지문을 남깁니다. 이 프로젝트는 그 지문을 읽고, 원시 데이터를 공개하며, 영향을 받은 저장소 관리자에게 직접 알립니다.
이 프로젝트는 JS Labs의 광범위한 AI Slop Intelligence 연구의 일부로, 저품질 AI 생성 콘텐츠가 개발자 생태계를 범람하는 메커니즘과 그 측정 가능한 영향을 조사합니다. 가짜 참여는 주변부 문제가 아닙니다. 그것은 슬롭(slop)을 실제 사용자 앞에 전달하는 유통 메커니즘입니다.
phantomstars는 매일 실행되는 GitHub Actions 작업으로 다음을 수행합니다.
r/osinttools와 r/coolgithubprojects의 최근 Reddit 게시물에서 지난 2일 동안 GitHub 저장소 링크를 추출하여 추가 후보 저장소를 시드합니다서버 없음. 데이터베이스 없음. 인프라 비용 없음.
네. 저장소의 가짜 비율이 40%를 초과하거나 조직적 캠페인이 탐지되면 phantomstars는 해당 저장소에 직접 이슈를 엽니다. 이슈에는 전체 의심 계정 표, 캠페인 구성원, 종합 점수, 계정 생성 날짜가 포함됩니다. 관리자가 조사하고 GitHub에 신고하는 데 필요한 모든 정보입니다.
표적 저장소에서 이슈가 비활성화되어 있으면 알림은 조용히 건너뛰고 스캔 로그에 기록됩니다.
네.
owner/repo 형식으로 저장소를 제출하고 대상 스캔을 실행하면 됩니다.이렇게 나눈 이유:
네. 계정이 data/suspects.jsonl에 나타나고 분류가 잘못되었다고 생각되면 제공된 템플릿을 사용하여 오탐지 이슈 열기를 통해 신고하세요. 보고서는 허용 목록에 추가되기 전에 수동으로 검토됩니다. 허용 목록은 data/allowlist.txt에 저장되며, 여기에 있는 계정은 향후 모든 스캔과 의심 계정 원장에서 제외됩니다.
캠페인 ID(예: c-a3f9b2e1)는 해당 캠페인의 구성원 로그인 정렬 집합의 SHA-256 해시에서 파생된 결정적 8자리 16진수 지문입니다. 동일한 계정 그룹은 독립적인 스캔 실행에서도 동일한 캠페인 ID를 생성하므로 종단 추적이 가능합니다. 저장소 이름, 사용자 이름 또는 외부 식별자가 아닙니다.
안정성: 캠페인의 구성원 집합이 변경되지 않는 한 ID는 안정적입니다. 스캔 사이에 봇이 추가되거나 정지되면 구성원이 변경되었으므로 ID도 변경됩니다. 이는 예상된 동작이며 봇 팜 구성의 실제 변화를 반영합니다.
네. 모든 계정의 생성 날짜는 GitHub GraphQL API(createdAt 필드)에서 가져와 각 의심 계정 레코드에 account_created_at으로 저장됩니다. 또한 가짜 계정을 판별하는 가장 강력한 단일 신호인 계정 연령 점수의 주요 입력값이기도 합니다. 참여 2일 이내에 생성된 계정은 연령만으로 1.0점을 받습니다.
개별 점수는 상당한 오탐률을 지닙니다. 프로필이 빈약한 신규 개발자도 정당하게 0.75+ 점수를 받을 수 있습니다. 이 도구는 이슈를 제기하기 전에 캠페인 수준의 증거를 요구함으로써 이를 보완합니다. 의심 계정 하나만으로는 충분하지 않습니다. 같은 주에 생성되고 모두 0.75+ 점수를 받았으며 90분 안에 모두 참여한 40개 이상의 계정으로 구성된 조정된 클러스터는 다른 문제입니다. 그 지점에서 신뢰도가 실행 가능해집니다.
데이터는 항상 확률적입니다. 이슈 본문에도 그렇게 명시되어 있습니다. 목표는 관리자에게 신호와 원시 증거를 제공하여 스스로 판단하게 하는 것입니다.
각 계정은 네 가지 신호로부터 종합 의심 점수(0.0 = 정상, 1.0 = 가짜 가능성 높음)를 받습니다:
분류 임계값:
| 점수 | 분류 |
|---|---|
| ≥ 0.75 | likely_fake |
| ≥ 0.45 | suspicious |
| < 0.45 | clean (저장되지 않음) |
캠페인은 3시간 창 안에서 같은 저장소에 모두 참여한 ≥ 4개의 의심 계정 그룹입니다. 알고리즘은 union-find를 사용해 연결 요소를 구성합니다. 창 안에서 함께 참여한 계정은 병합되며, 최소 크기 이상의 모든 구성 요소는 조직적 캠페인으로 플래그됩니다.
캠페인 ID는 정렬된 구성원 집합의 안정적인 SHA-256 지문입니다. 연속된 날짜에 탐지된 동일한 캠페인은 구성원이 변경되지 않는 한 동일한 ID를 갖습니다.
캠페인이 진짜 신호인 이유: 개별 점수는 상당한 오탐률을 지닙니다. 프로필이 빈약한 신규 개발자는 혼자서 0.80점을 받을 수 있습니다. 같은 주에 생성되고 모두 0.75+ 점수를 받았으며 90분 안에 같은 저장소에 스타를 누른 40개의 계정은 우연이 아닙니다. 캠페인 신호는 데이터가 실행 가능해지는 지점입니다. 의심스러운 데이터 포인트 하나와 조직적 작업의 증거 사이의 차이입니다.
모든 결과는 data/suspects.jsonl 및 data/repos.jsonl에 커밋되며, 줄마다 JSON 레코드 하나씩 append-only로 기록됩니다. GitHub Actions 작업 요약(각 실행 후 Actions UI에서 확인 가능)은 형식화된 검사별 보고서를 제공합니다.
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`: 공개 리포지토리의 이벤트와 스타를 누른 사용자를 읽고, 공개 리포지토리에 이슈를 생성합니다.
- `read:user`: GraphQL을 통해 사용자 프로필을 가져옵니다.
**설정 → 비밀 및 변수 → Actions → 새 리포지토리 비밀** → 이름을 `GH_TOKEN`으로 지정하세요.
> 기본 `GITHUB_TOKEN`은 요청 한도가 제한되어 있으며 사용자 GraphQL 엔드포인트를 전체 용량으로 호출할 수 없습니다. PAT가 필요합니다.
### 3. Actions 활성화
포크에서 **Actions → Enable GitHub Actions**를 활성화하세요. 워크플로는 `Europe/London` 시간대를 기준으로 매일 **영국 시간 07:00**에 실행됩니다:
- 영국 서머타임(BST) 동안 **06:00 UTC**
- 그리니치 표준시(GMT) 동안 **07:00 UTC**
별도의 스케줄링 환경 변수는 필요하지 않습니다. GitHub Actions 크론은 UTC만 지원하므로 워크플로는 두 UTC 시간에 모두 트리거되며, 런던 현지 시간이 07:00일 때만 진행됩니다. 수동 트리거는 **Actions → Daily Phantom Stars Scan → Run workflow**에서 사용할 수 있습니다.
각 실행 후 포맷된 스캔 보고서는 **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
사용자는 두 가지 방법으로 일회성 저장소 검사를 요청할 수 있습니다:
Repo Check Request 이슈 템플릿을 열고 대상 저장소와 요청 깊이를 제공합니다.target_repo: owner/reporequest_depth: recent 또는 lifetime-request현재 동작:
recent: 대상 최근 참여(recent-engagement) 스캔을 즉시 실행합니다.lifetime-request: 해당 저장소에 대해서만 과거 스타와 포크 전반에 걸친 대상 lifetime 스캔을 실행합니다.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
---
## 제한 사항 및 알려진 실패 모드
- **이벤트 API 한도:** 리포지토리당 최근 이벤트 최대 300개. 하루에 수천 개의 스타를 받는 리포지토리는 부분적으로만 포함됩니다.
- **커버리지 플래그:** 300개 이벤트 한도에 도달한 리포지토리는 보고서와 대시보드에서 `capped`로 표시됩니다. 해당 리포지토리의 비율은 전체 하루 집계가 아닌 보수적인 표본입니다.
- **검색 인덱스 지연:** GitHub의 검색 인덱스는 결과적으로 일관성(eventually consistent)을 갖습니다. 스캔 경계 직전에 생성된 리포지토리는 누락될 수 있습니다.
- **휴리스틱 드리프트:** 봇 운영자는 적응합니다. 점수 가중치는 주기적인 조정이 필요할 수 있습니다. `config.py`에서 상수를 조정하세요.
- **개별 오탐:** 프로필이 빈약한 새 개발자는 단독으로 0.75+ 점수를 받을 수 있습니다. 캠페인 구성원 자격이 높은 신뢰도의 신호입니다.
- **캠페인 ID 드리프트:** 봇 팜의 구성원이 스캔 사이에 변경되면(봇 정지, 새 봇 추가) 캠페인 ID가 변경됩니다. 이는 버그가 아니라 실제 캠페인 변화를 반영합니다.
- **속도 제한:** 인증된 PAT로 시간당 5,000회의 API 요청. 표준 트렌딩 페이지 규모에는 충분합니다.
- **이슈 비활성화:** 일부 대상 리포지토리는 이슈를 비활성화합니다. 해당 리포지토리의 알림은 자동으로 건너뜁니다.
---
## 오탐(false positive) 처리 절차
`data/suspects.jsonl`에 귀하의 계정이 나타나고 잘못 분류되었다고 생각되면:
1. 항목 찾기: `jq 'select(.login == "YOUR_LOGIN")' data/suspects.jsonl`
2. 로그인, 분류, 스캔 날짜, 설명과 함께 [오탐 이슈 열기](../../issues/new?template=false_positive.yml)
3. 보고서는 수동으로 검토됩니다. 확인된 오탐은 `data/allowlist.txt`에 추가되며 모든 향후 스캔, 리포지토리 비율, 이슈 알림에서 제외됩니다.
참고: 이슈를 열어도 기존 데이터는 수정되거나 제거되지 않습니다. 용의자 원장(suspects ledger)은 추가 전용(append-only)입니다. 허용 목록(allowlist)은 향후 스캔에만 영향을 미칩니다.
---
## 기여하기```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 API를 사용하여 공개 GitHub 데이터에 대한 읽기 전용 분석을 수행합니다. 대상 저장소에 이슈가 제출되는 경우, 해당 이슈에는 확률적 결과가 포함되어 있으며 자동 생성된 것으로 명확히 표시됩니다. 발견 사항은 지표이지 고발이 아닙니다. 오탐은 존재하며 예상되는 사항입니다.
AI를 코딩 파트너로 사용하여, 부분적으로 AI로 인해 발생한 생태계 문제에 대응하기 위해 제작되었습니다.
Apache 2.0. LICENSE를 참조하세요.
tg12 제작 · GitHub
| 날짜 | 검사 대상 | 가능성 높은 가짜 | 의심 계정 | 캠페인 | 신규 가짜 (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 | 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 |
| 신호 | 가중치 | 측정 기준 |
|---|
| 계정 연령 | 35% | < 2 days → 1.00 · < 7 days → 0.90 · < 30 days → 0.55 · < 90 days → 0.20 · 더 오래된 경우 → 0.00 |
| 프로필 완성도 | 30% | 프로필 소개 없음(+0.25), 위치 없음(+0.15), 회사 없음(+0.10), 팔로워 0명(+0.30), 팔로잉 0명(+0.10), 봇 패턴 사용자 이름(+0.20) |
| 저장소 패턴 | 25% | 저장소 0개 → 0.90 · 모든 저장소가 포크 → 0.80 · 포크 비율 >85% → 0.55 |
| 활동 기록 | 10% | 14일 이상 지난 계정에 저장소 0개 + 소셜 그래프 0개 → 0.80 (유령 계정). 저장소만 0개 → 0.60. 전체 포크 + 소셜 그래프 없음 → 0.50 |